'use strict'; /*eslint-env browser*/ /*global $, _*/ var mdurl = require('mdurl'); var hljs = require('highlight.js/lib/core'); hljs.registerLanguage('actionscript', require('highlight.js/lib/languages/actionscript')); hljs.registerLanguage('apache', require('highlight.js/lib/languages/apache')); hljs.registerLanguage('armasm', require('highlight.js/lib/languages/armasm')); hljs.registerLanguage('xml', require('highlight.js/lib/languages/xml')); hljs.registerLanguage('asciidoc', require('highlight.js/lib//languages/asciidoc')); hljs.registerLanguage('avrasm', require('highlight.js/lib/languages/avrasm')); hljs.registerLanguage('bash', require('highlight.js/lib/languages/bash')); hljs.registerLanguage('clojure', require('highlight.js/lib/languages/clojure')); hljs.registerLanguage('cmake', require('highlight.js/lib/languages/cmake')); hljs.registerLanguage('coffeescript', require('highlight.js/lib/languages/coffeescript')); hljs.registerLanguage('c-like', require('highlight.js/lib/languages/c-like')); hljs.registerLanguage('c', require('highlight.js/lib/languages/c')); hljs.registerLanguage('cpp', require('highlight.js/lib/languages/cpp')); hljs.registerLanguage('arduino', require('highlight.js/lib/languages/arduino')); hljs.registerLanguage('css', require('highlight.js/lib/languages/css')); hljs.registerLanguage('diff', require('highlight.js/lib/languages/diff')); hljs.registerLanguage('django', require('highlight.js/lib/languages/django')); hljs.registerLanguage('dockerfile', require('highlight.js/lib/languages/dockerfile')); hljs.registerLanguage('ruby', require('highlight.js/lib/languages/ruby')); hljs.registerLanguage('fortran', require('highlight.js/lib/languages/fortran')); hljs.registerLanguage('glsl', require('highlight.js/lib/languages/glsl')); hljs.registerLanguage('go', require('highlight.js/lib/languages/go')); hljs.registerLanguage('groovy', require('highlight.js/lib/languages/groovy')); hljs.registerLanguage('handlebars', require('highlight.js/lib/languages/handlebars')); hljs.registerLanguage('haskell', require('highlight.js/lib/languages/haskell')); hljs.registerLanguage('ini', require('highlight.js/lib/languages/ini')); hljs.registerLanguage('java', require('highlight.js/lib/languages/java')); hljs.registerLanguage('javascript', require('highlight.js/lib/languages/javascript')); hljs.registerLanguage('json', require('highlight.js/lib/languages/json')); hljs.registerLanguage('latex', require('highlight.js/lib/languages/latex')); hljs.registerLanguage('less', require('highlight.js/lib/languages/less')); hljs.registerLanguage('lisp', require('highlight.js/lib/languages/lisp')); hljs.registerLanguage('livescript', require('highlight.js/lib/languages/livescript')); hljs.registerLanguage('lua', require('highlight.js/lib/languages/lua')); hljs.registerLanguage('makefile', require('highlight.js/lib/languages/makefile')); hljs.registerLanguage('matlab', require('highlight.js/lib/languages/matlab')); hljs.registerLanguage('mipsasm', require('highlight.js/lib/languages/mipsasm')); hljs.registerLanguage('perl', require('highlight.js/lib/languages/perl')); hljs.registerLanguage('nginx', require('highlight.js/lib/languages/nginx')); hljs.registerLanguage('objectivec', require('highlight.js/lib/languages/objectivec')); hljs.registerLanguage('php', require('highlight.js/lib/languages/php')); hljs.registerLanguage('python', require('highlight.js/lib/languages/python')); hljs.registerLanguage('rust', require('highlight.js/lib/languages/rust')); hljs.registerLanguage('scala', require('highlight.js/lib/languages/scala')); hljs.registerLanguage('scheme', require('highlight.js/lib/languages/scheme')); hljs.registerLanguage('scss', require('highlight.js/lib/languages/scss')); hljs.registerLanguage('smalltalk', require('highlight.js/lib/languages/smalltalk')); hljs.registerLanguage('stylus', require('highlight.js/lib/languages/stylus')); hljs.registerLanguage('swift', require('highlight.js/lib/languages/swift')); hljs.registerLanguage('tcl', require('highlight.js/lib/languages/tcl')); hljs.registerLanguage('typescript', require('highlight.js/lib/languages/typescript')); hljs.registerLanguage('verilog', require('highlight.js/lib/languages/verilog')); hljs.registerLanguage('vhdl', require('highlight.js/lib/languages/vhdl')); hljs.registerLanguage('yaml', require('highlight.js/lib/languages/yaml')); var mdHtml, mdSrc, permalink, scrollMap; var defaults = { html: false, // Enable HTML tags in source xhtmlOut: false, // Use '/' to close single tags (
) breaks: false, // Convert '\n' in paragraphs into
langPrefix: 'language-', // CSS language prefix for fenced blocks linkify: true, // autoconvert URL-like texts to links typographer: true, // Enable smartypants and other sweet transforms // options below are for demo only _highlight: true, _strict: false, _view: 'html' // html / src / debug }; defaults.highlight = function (str, lang) { var esc = mdHtml.utils.escapeHtml; try { if (!defaults._highlight) { throw 'highlighting disabled'; } if (lang && lang !== 'auto' && hljs.getLanguage(lang)) { return '
' +
             hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
             '
'; } else if (lang === 'auto') { var result = hljs.highlightAuto(str); /*eslint-disable no-console*/ console.log('highlight language: ' + result.language + ', relevance: ' + result.relevance); return '
' +
             result.value +
             '
'; } } catch (__) { /**/ } return '
' + esc(str) + '
'; }; 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) .use(require('markdown-it-abbr')) .use(require('markdown-it-container'), 'warning') .use(require('markdown-it-deflist')) .use(require('markdown-it-emoji')) .use(require('markdown-it-footnote')) .use(require('markdown-it-ins')) .use(require('markdown-it-mark')) .use(require('markdown-it-sub')) .use(require('markdown-it-sup')); mdSrc = window.markdownit(defaults) .use(require('markdown-it-abbr')) .use(require('markdown-it-container'), 'warning') .use(require('markdown-it-deflist')) .use(require('markdown-it-emoji')) .use(require('markdown-it-footnote')) .use(require('markdown-it-ins')) .use(require('markdown-it-mark')) .use(require('markdown-it-sub')) .use(require('markdown-it-sup')); } // Beautify output of parser for html content mdHtml.renderer.rules.table_open = function () { return '\n'; }; // Replace emoji codes with images mdHtml.renderer.rules.emoji = function (token, idx) { return window.twemoji.parse(token[idx].content); }; // // 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. function injectLineNumbers(tokens, idx, options, env, slf) { var line; if (tokens[idx].map && tokens[idx].level === 0) { line = tokens[idx].map[0]; tokens[idx].attrJoin('class', 'line'); tokens[idx].attrSet('data-line', String(line)); } return slf.renderToken(tokens, idx, options, env, slf); } mdHtml.renderer.rules.paragraph_open = mdHtml.renderer.rules.heading_open = injectLineNumbers; } function setHighlightedlContent(selector, content, lang) { if (window.hljs) { $(selector).html(window.hljs.highlight(content, { language: lang }).value); } else { $(selector).text(content); } } function updateResult() { var source = $('.source').val(); // 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'); } else if (defaults._view === 'debug') { setHighlightedlContent( '.result-debug-content', JSON.stringify(mdSrc.parse(source, { references: {} }), null, 2), 'json' ); } else { /*defaults._view === 'html'*/ $('.result-html').html(mdHtml.render(source)); } // reset lines mapping cache on content update scrollMap = null; try { if (source) { // serialize state - source and options permalink.href = '#md3=' + mdurl.encode(JSON.stringify({ source: source, defaults: _.omit(defaults, 'highlight') }), '-_.!~', false); } else { permalink.href = ''; } } catch (__) { permalink.href = ''; } } // 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 = $('
').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; textarea.val().split('\n').forEach(function (str) { var h, lh; lineHeightMap.push(acc); if (str.length === 0) { acc++; return; } 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; for (i = 0; i < linesCount; i++) { _scrollMap.push(-1); } nonEmptyList.push(0); _scrollMap[0] = 0; $('.line').each(function (n, el) { 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); }); nonEmptyList.push(linesCount); _scrollMap[linesCount] = $('.result-html')[0].scrollHeight; pos = 0; for (i = 1; i < linesCount; i++) { if (_scrollMap[i] !== -1) { pos++; continue; } a = nonEmptyList[pos]; b = nonEmptyList[pos + 1]; _scrollMap[i] = Math.round((_scrollMap[b] * (i - a) + _scrollMap[a] * (b - i)) / (b - a)); } 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; } line = lines[0]; for (i = 1; i < lines.length; i++) { if (scrollMap[lines[i]] < scrollTop) { line = lines[i]; continue; } break; } textarea.stop(true).animate({ scrollTop: lineHeight * line }, 100, 'linear'); }, 50, { maxWait: 50 }); 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'; } } ////////////////////////////////////////////////////////////////////////////// // Init on page load // $(function () { // highlight snippet if (window.hljs) { $('pre.code-sample code').each(function (i, block) { window.hljs.highlightBlock(block); }); } loadPermalink(); // Activate tooltips $('._tip').tooltip({ container: 'body' }); // Set default option values and option listeners _.forOwn(defaults, function (val, key) { if (key === 'highlight') { return; } var el = document.getElementById(key); if (!el) { return; } var $el = $(el); 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(); }); } }); setResultView(defaults._view); mdInit(); permalink = document.getElementById('permalink'); // Setup listeners $('.source').on('keyup paste cut mouseup', _.debounce(updateResult, 300, { maxWait: 500 })); $('.source').on('touchstart mouseover', function () { $('.result-html').off('scroll'); $('.source').on('scroll', syncResultScroll); }); $('.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(); }); $(document).on('click', '[data-result-as]', function (event) { var view = $(this).data('resultAs'); if (view) { setResultView(view); // only to update permalink updateResult(); event.preventDefault(); } }); // Need to recalculate line positions on window resize $(window).on('resize', function () { scrollMap = null; }); updateResult(); });