Markdown parser, done right. 100% CommonMark support, extensions, syntax plugins & high speed
https://markdown-it.github.io/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
447 lines
11 KiB
447 lines
11 KiB
10 years ago
|
/*eslint-env browser*/
|
||
|
/*global $, _*/
|
||
10 years ago
|
|
||
1 year ago
|
import mdurl from 'mdurl';
|
||
|
import hljs from 'highlight.js';
|
||
|
|
||
|
// plugins
|
||
|
import md_abbr from 'markdown-it-abbr';
|
||
|
import md_container from 'markdown-it-container';
|
||
|
import md_deflist from 'markdown-it-deflist';
|
||
|
import md_emoji from 'markdown-it-emoji';
|
||
|
import md_footnote from 'markdown-it-footnote';
|
||
|
import md_ins from 'markdown-it-ins';
|
||
|
import md_mark from 'markdown-it-mark';
|
||
|
import md_sub from 'markdown-it-sub';
|
||
|
import md_sup from 'markdown-it-sup';
|
||
|
|
||
9 years ago
|
|
||
10 years ago
|
var mdHtml, mdSrc, permalink, scrollMap;
|
||
|
|
||
|
var defaults = {
|
||
|
html: false, // Enable HTML tags in source
|
||
|
xhtmlOut: false, // Use '/' to close single tags (<br />)
|
||
|
breaks: false, // Convert '\n' in paragraphs into <br>
|
||
|
langPrefix: 'language-', // CSS language prefix for fenced blocks
|
||
|
linkify: true, // autoconvert URL-like texts to links
|
||
|
typographer: true, // Enable smartypants and other sweet transforms
|
||
10 years ago
|
|
||
10 years ago
|
// options below are for demo only
|
||
|
_highlight: true,
|
||
|
_strict: false,
|
||
|
_view: 'html' // html / src / debug
|
||
|
};
|
||
|
|
||
|
defaults.highlight = function (str, lang) {
|
||
9 years ago
|
var esc = mdHtml.utils.escapeHtml;
|
||
|
|
||
|
try {
|
||
|
if (!defaults._highlight) {
|
||
|
throw 'highlighting disabled';
|
||
|
}
|
||
10 years ago
|
|
||
9 years ago
|
if (lang && lang !== 'auto' && hljs.getLanguage(lang)) {
|
||
9 years ago
|
|
||
9 years ago
|
return '<pre class="hljs language-' + esc(lang.toLowerCase()) + '"><code>' +
|
||
4 years ago
|
hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
|
||
9 years ago
|
'</code></pre>';
|
||
9 years ago
|
|
||
|
} else if (lang === 'auto') {
|
||
|
|
||
|
var result = hljs.highlightAuto(str);
|
||
|
|
||
9 years ago
|
/*eslint-disable no-console*/
|
||
9 years ago
|
console.log('highlight language: ' + result.language + ', relevance: ' + result.relevance);
|
||
|
|
||
|
return '<pre class="hljs language-' + esc(result.language) + '"><code>' +
|
||
|
result.value +
|
||
|
'</code></pre>';
|
||
|
}
|
||
|
} catch (__) { /**/ }
|
||
10 years ago
|
|
||
1 year ago
|
return '<pre><code class="hljs">' + esc(str) + '</code></pre>';
|
||
10 years ago
|
};
|
||
10 years ago
|
|
||
10 years ago
|
function setOptionClass(name, val) {
|
||
|
if (val) {
|
||
|
$('body').addClass('opt_' + name);
|
||
|
} else {
|
||
|
$('body').removeClass('opt_' + name);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function setResultView(val) {
|
||
|
$('body').removeClass('result-as-html');
|
||
|
$('body').removeClass('result-as-src');
|
||
|
$('body').removeClass('result-as-debug');
|
||
|
$('body').addClass('result-as-' + val);
|
||
|
defaults._view = val;
|
||
|
}
|
||
|
|
||
|
function mdInit() {
|
||
|
if (defaults._strict) {
|
||
|
mdHtml = window.markdownit('commonmark');
|
||
|
mdSrc = window.markdownit('commonmark');
|
||
|
} else {
|
||
|
mdHtml = window.markdownit(defaults)
|
||
1 year ago
|
.use(md_abbr)
|
||
|
.use(md_container, 'warning')
|
||
|
.use(md_deflist)
|
||
|
.use(md_emoji)
|
||
|
.use(md_footnote)
|
||
|
.use(md_ins)
|
||
|
.use(md_mark)
|
||
|
.use(md_sub)
|
||
|
.use(md_sup);
|
||
10 years ago
|
mdSrc = window.markdownit(defaults)
|
||
1 year ago
|
.use(md_abbr)
|
||
|
.use(md_container, 'warning')
|
||
|
.use(md_deflist)
|
||
|
.use(md_emoji)
|
||
|
.use(md_footnote)
|
||
|
.use(md_ins)
|
||
|
.use(md_mark)
|
||
|
.use(md_sub)
|
||
|
.use(md_sup);
|
||
10 years ago
|
}
|
||
|
|
||
10 years ago
|
// Beautify output of parser for html content
|
||
|
mdHtml.renderer.rules.table_open = function () {
|
||
|
return '<table class="table table-striped">\n';
|
||
|
};
|
||
|
// Replace emoji codes with images
|
||
9 years ago
|
mdHtml.renderer.rules.emoji = function (token, idx) {
|
||
10 years ago
|
return window.twemoji.parse(token[idx].content);
|
||
|
};
|
||
10 years ago
|
|
||
9 years ago
|
|
||
10 years ago
|
//
|
||
|
// Inject line numbers for sync scroll. Notes:
|
||
|
//
|
||
|
// - We track only headings and paragraphs on first level. That's enough.
|
||
|
// - Footnotes content causes jumps. Level limit filter it automatically.
|
||
9 years ago
|
function injectLineNumbers(tokens, idx, options, env, slf) {
|
||
10 years ago
|
var line;
|
||
|
if (tokens[idx].map && tokens[idx].level === 0) {
|
||
|
line = tokens[idx].map[0];
|
||
9 years ago
|
tokens[idx].attrJoin('class', 'line');
|
||
|
tokens[idx].attrSet('data-line', String(line));
|
||
10 years ago
|
}
|
||
9 years ago
|
return slf.renderToken(tokens, idx, options, env, slf);
|
||
10 years ago
|
}
|
||
10 years ago
|
|
||
10 years ago
|
mdHtml.renderer.rules.paragraph_open = mdHtml.renderer.rules.heading_open = injectLineNumbers;
|
||
|
}
|
||
|
|
||
|
function setHighlightedlContent(selector, content, lang) {
|
||
|
if (window.hljs) {
|
||
4 years ago
|
$(selector).html(window.hljs.highlight(content, { language: lang }).value);
|
||
10 years ago
|
} else {
|
||
|
$(selector).text(content);
|
||
10 years ago
|
}
|
||
10 years ago
|
}
|
||
10 years ago
|
|
||
10 years ago
|
function updateResult() {
|
||
|
var source = $('.source').val();
|
||
10 years ago
|
|
||
10 years ago
|
// Update only active view to avoid slowdowns
|
||
|
// (debug & src view with highlighting are a bit slow)
|
||
|
if (defaults._view === 'src') {
|
||
|
setHighlightedlContent('.result-src-content', mdSrc.render(source), 'html');
|
||
10 years ago
|
|
||
10 years ago
|
} else if (defaults._view === 'debug') {
|
||
|
setHighlightedlContent(
|
||
|
'.result-debug-content',
|
||
|
JSON.stringify(mdSrc.parse(source, { references: {} }), null, 2),
|
||
|
'json'
|
||
|
);
|
||
10 years ago
|
|
||
10 years ago
|
} else { /*defaults._view === 'html'*/
|
||
|
$('.result-html').html(mdHtml.render(source));
|
||
|
}
|
||
10 years ago
|
|
||
10 years ago
|
// reset lines mapping cache on content update
|
||
|
scrollMap = null;
|
||
10 years ago
|
|
||
10 years ago
|
try {
|
||
|
if (source) {
|
||
|
// serialize state - source and options
|
||
10 years ago
|
permalink.href = '#md3=' + mdurl.encode(JSON.stringify({
|
||
10 years ago
|
source: source,
|
||
|
defaults: _.omit(defaults, 'highlight')
|
||
10 years ago
|
}), '-_.!~', false);
|
||
10 years ago
|
} else {
|
||
10 years ago
|
permalink.href = '';
|
||
|
}
|
||
10 years ago
|
} catch (__) {
|
||
|
permalink.href = '';
|
||
10 years ago
|
}
|
||
10 years ago
|
}
|
||
|
|
||
|
// Build offsets for each line (lines can be wrapped)
|
||
|
// That's a bit dirty to process each line everytime, but ok for demo.
|
||
|
// Optimizations are required only for big texts.
|
||
|
function buildScrollMap() {
|
||
|
var i, offset, nonEmptyList, pos, a, b, lineHeightMap, linesCount,
|
||
|
acc, sourceLikeDiv, textarea = $('.source'),
|
||
|
_scrollMap;
|
||
|
|
||
|
sourceLikeDiv = $('<div />').css({
|
||
|
position: 'absolute',
|
||
|
visibility: 'hidden',
|
||
|
height: 'auto',
|
||
|
width: textarea[0].clientWidth,
|
||
|
'font-size': textarea.css('font-size'),
|
||
|
'font-family': textarea.css('font-family'),
|
||
|
'line-height': textarea.css('line-height'),
|
||
|
'white-space': textarea.css('white-space')
|
||
|
}).appendTo('body');
|
||
|
|
||
|
offset = $('.result-html').scrollTop() - $('.result-html').offset().top;
|
||
|
_scrollMap = [];
|
||
|
nonEmptyList = [];
|
||
|
lineHeightMap = [];
|
||
|
|
||
|
acc = 0;
|
||
9 years ago
|
textarea.val().split('\n').forEach(function (str) {
|
||
10 years ago
|
var h, lh;
|
||
10 years ago
|
|
||
10 years ago
|
lineHeightMap.push(acc);
|
||
|
|
||
10 years ago
|
if (str.length === 0) {
|
||
|
acc++;
|
||
|
return;
|
||
|
}
|
||
10 years ago
|
|
||
10 years ago
|
sourceLikeDiv.text(str);
|
||
|
h = parseFloat(sourceLikeDiv.css('height'));
|
||
|
lh = parseFloat(sourceLikeDiv.css('line-height'));
|
||
|
acc += Math.round(h / lh);
|
||
|
});
|
||
|
sourceLikeDiv.remove();
|
||
|
lineHeightMap.push(acc);
|
||
|
linesCount = acc;
|
||
10 years ago
|
|
||
10 years ago
|
for (i = 0; i < linesCount; i++) { _scrollMap.push(-1); }
|
||
10 years ago
|
|
||
10 years ago
|
nonEmptyList.push(0);
|
||
|
_scrollMap[0] = 0;
|
||
10 years ago
|
|
||
9 years ago
|
$('.line').each(function (n, el) {
|
||
10 years ago
|
var $el = $(el), t = $el.data('line');
|
||
|
if (t === '') { return; }
|
||
|
t = lineHeightMap[t];
|
||
|
if (t !== 0) { nonEmptyList.push(t); }
|
||
|
_scrollMap[t] = Math.round($el.offset().top + offset);
|
||
|
});
|
||
10 years ago
|
|
||
10 years ago
|
nonEmptyList.push(linesCount);
|
||
|
_scrollMap[linesCount] = $('.result-html')[0].scrollHeight;
|
||
|
|
||
|
pos = 0;
|
||
|
for (i = 1; i < linesCount; i++) {
|
||
|
if (_scrollMap[i] !== -1) {
|
||
|
pos++;
|
||
|
continue;
|
||
10 years ago
|
}
|
||
|
|
||
10 years ago
|
a = nonEmptyList[pos];
|
||
|
b = nonEmptyList[pos + 1];
|
||
|
_scrollMap[i] = Math.round((_scrollMap[b] * (i - a) + _scrollMap[a] * (b - i)) / (b - a));
|
||
10 years ago
|
}
|
||
|
|
||
10 years ago
|
return _scrollMap;
|
||
|
}
|
||
|
|
||
|
// Synchronize scroll position from source to result
|
||
|
var syncResultScroll = _.debounce(function () {
|
||
|
var textarea = $('.source'),
|
||
|
lineHeight = parseFloat(textarea.css('line-height')),
|
||
|
lineNo, posTo;
|
||
|
|
||
|
lineNo = Math.floor(textarea.scrollTop() / lineHeight);
|
||
|
if (!scrollMap) { scrollMap = buildScrollMap(); }
|
||
|
posTo = scrollMap[lineNo];
|
||
|
$('.result-html').stop(true).animate({
|
||
|
scrollTop: posTo
|
||
|
}, 100, 'linear');
|
||
|
}, 50, { maxWait: 50 });
|
||
|
|
||
|
// Synchronize scroll position from result to source
|
||
|
var syncSrcScroll = _.debounce(function () {
|
||
|
var resultHtml = $('.result-html'),
|
||
|
scrollTop = resultHtml.scrollTop(),
|
||
|
textarea = $('.source'),
|
||
|
lineHeight = parseFloat(textarea.css('line-height')),
|
||
|
lines,
|
||
|
i,
|
||
|
line;
|
||
|
|
||
|
if (!scrollMap) { scrollMap = buildScrollMap(); }
|
||
|
|
||
|
lines = Object.keys(scrollMap);
|
||
|
|
||
|
if (lines.length < 1) {
|
||
|
return;
|
||
|
}
|
||
10 years ago
|
|
||
10 years ago
|
line = lines[0];
|
||
10 years ago
|
|
||
10 years ago
|
for (i = 1; i < lines.length; i++) {
|
||
|
if (scrollMap[lines[i]] < scrollTop) {
|
||
|
line = lines[i];
|
||
|
continue;
|
||
10 years ago
|
}
|
||
|
|
||
10 years ago
|
break;
|
||
|
}
|
||
10 years ago
|
|
||
10 years ago
|
textarea.stop(true).animate({
|
||
|
scrollTop: lineHeight * line
|
||
|
}, 100, 'linear');
|
||
|
}, 50, { maxWait: 50 });
|
||
|
|
||
10 years ago
|
|
||
|
function loadPermalink() {
|
||
|
|
||
|
if (!location.hash) { return; }
|
||
|
|
||
|
var cfg, opts;
|
||
|
|
||
|
try {
|
||
|
|
||
|
if (/^#md3=/.test(location.hash)) {
|
||
|
cfg = JSON.parse(mdurl.decode(location.hash.slice(5), mdurl.decode.componentChars));
|
||
|
|
||
|
} else if (/^#md64=/.test(location.hash)) {
|
||
|
cfg = JSON.parse(window.atob(location.hash.slice(6)));
|
||
|
|
||
|
} else if (/^#md=/.test(location.hash)) {
|
||
|
cfg = JSON.parse(decodeURIComponent(location.hash.slice(4)));
|
||
|
|
||
|
} else {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (_.isString(cfg.source)) {
|
||
|
$('.source').val(cfg.source);
|
||
|
}
|
||
|
} catch (__) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
opts = _.isObject(cfg.defaults) ? cfg.defaults : {};
|
||
|
|
||
|
// copy config to defaults, but only if key exists
|
||
|
// and value has the same type
|
||
|
_.forOwn(opts, function (val, key) {
|
||
|
if (!_.has(defaults, key)) { return; }
|
||
|
|
||
|
// Legacy, for old links
|
||
|
if (key === '_src') {
|
||
|
defaults._view = val ? 'src' : 'html';
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if ((_.isBoolean(defaults[key]) && _.isBoolean(val)) ||
|
||
|
(_.isString(defaults[key]) && _.isString(val))) {
|
||
|
defaults[key] = val;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// sanitize for sure
|
||
|
if ([ 'html', 'src', 'debug' ].indexOf(defaults._view) === -1) {
|
||
|
defaults._view = 'html';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
10 years ago
|
//////////////////////////////////////////////////////////////////////////////
|
||
|
// Init on page load
|
||
|
//
|
||
9 years ago
|
$(function () {
|
||
10 years ago
|
// highlight snippet
|
||
|
if (window.hljs) {
|
||
9 years ago
|
$('pre.code-sample code').each(function (i, block) {
|
||
10 years ago
|
window.hljs.highlightBlock(block);
|
||
|
});
|
||
|
}
|
||
10 years ago
|
|
||
10 years ago
|
loadPermalink();
|
||
10 years ago
|
|
||
10 years ago
|
// Activate tooltips
|
||
|
$('._tip').tooltip({ container: 'body' });
|
||
10 years ago
|
|
||
10 years ago
|
// Set default option values and option listeners
|
||
|
_.forOwn(defaults, function (val, key) {
|
||
|
if (key === 'highlight') { return; }
|
||
10 years ago
|
|
||
10 years ago
|
var el = document.getElementById(key);
|
||
10 years ago
|
|
||
10 years ago
|
if (!el) { return; }
|
||
10 years ago
|
|
||
10 years ago
|
var $el = $(el);
|
||
10 years ago
|
|
||
10 years ago
|
if (_.isBoolean(val)) {
|
||
|
$el.prop('checked', val);
|
||
|
$el.on('change', function () {
|
||
|
var value = Boolean($el.prop('checked'));
|
||
|
setOptionClass(key, value);
|
||
|
defaults[key] = value;
|
||
|
mdInit();
|
||
|
updateResult();
|
||
|
});
|
||
|
setOptionClass(key, val);
|
||
|
|
||
|
} else {
|
||
|
$(el).val(val);
|
||
|
$el.on('change update keyup', function () {
|
||
|
defaults[key] = String($(el).val());
|
||
|
mdInit();
|
||
|
updateResult();
|
||
|
});
|
||
|
}
|
||
|
});
|
||
10 years ago
|
|
||
10 years ago
|
setResultView(defaults._view);
|
||
10 years ago
|
|
||
10 years ago
|
mdInit();
|
||
|
permalink = document.getElementById('permalink');
|
||
10 years ago
|
|
||
10 years ago
|
// Setup listeners
|
||
|
$('.source').on('keyup paste cut mouseup', _.debounce(updateResult, 300, { maxWait: 500 }));
|
||
10 years ago
|
|
||
10 years ago
|
$('.source').on('touchstart mouseover', function () {
|
||
|
$('.result-html').off('scroll');
|
||
|
$('.source').on('scroll', syncResultScroll);
|
||
|
});
|
||
10 years ago
|
|
||
10 years ago
|
$('.result-html').on('touchstart mouseover', function () {
|
||
|
$('.source').off('scroll');
|
||
|
$('.result-html').on('scroll', syncSrcScroll);
|
||
|
});
|
||
|
|
||
|
$('.source-clear').on('click', function (event) {
|
||
|
$('.source').val('');
|
||
|
updateResult();
|
||
|
event.preventDefault();
|
||
|
});
|
||
10 years ago
|
|
||
10 years ago
|
$(document).on('click', '[data-result-as]', function (event) {
|
||
|
var view = $(this).data('resultAs');
|
||
|
if (view) {
|
||
|
setResultView(view);
|
||
|
// only to update permalink
|
||
10 years ago
|
updateResult();
|
||
|
event.preventDefault();
|
||
10 years ago
|
}
|
||
|
});
|
||
10 years ago
|
|
||
10 years ago
|
// Need to recalculate line positions on window resize
|
||
|
$(window).on('resize', function () {
|
||
|
scrollMap = null;
|
||
10 years ago
|
});
|
||
10 years ago
|
|
||
|
updateResult();
|
||
|
});
|