define([ 'jquery', '/api/config', '/bower_components/marked/marked.min.js', '/common/common-hash.js', '/common/common-util.js', '/common/hyperscript.js', '/common/inner/common-mediatag.js', '/common/media-tag.js', '/customize/messages.js', '/lib/less.min.js', '/customize/pages.js', '/lib/highlight/highlight.pack.js', '/lib/diff-dom/diffDOM.js', '/bower_components/tweetnacl/nacl-fast.min.js', 'css!/lib/highlight/styles/'+ (window.CryptPad_theme === 'dark' ? 'dark.css' : 'github.css') ],function ($, ApiConfig, Marked, Hash, Util, h, MT, MediaTag, Messages, Less, Pages) { var DiffMd = {}; var Highlight = window.hljs; var DiffDOM = window.diffDOM; var renderer = new Marked.Renderer(); var restrictedRenderer = new Marked.Renderer(); var pluginLoaded = Util.mkEvent(); DiffMd.onPluginLoaded = pluginLoaded.reg; var mermaidThemeCSS = //".node rect { fill: #DDD; stroke: #AAA; } " + "rect.task, rect.task0, rect.task2 { stroke-width: 1 !important; rx: 0 !important; } " + "g.grid g.tick line { opacity: 0.25; }" + "g.today line { stroke: red; stroke-width: 1; stroke-dasharray: 3; opacity: 0.5; }"; var Mermaid = { __stubbed: true, init: function () { require([ 'mermaid', //'css!/code/mermaid-new.css' // XXX ], function (_Mermaid) { console.debug("loaded mermaid"); if (Mermaid.__stubbed) { Mermaid = _Mermaid; Mermaid.initialize({ gantt: { axisFormat: '%m-%d', }, flowchart: { htmlLabels: false, }, theme: (window.CryptPad_theme === 'dark') ? 'dark' : 'default', "themeCSS": mermaidThemeCSS, }); } pluginLoaded.fire(); }); } }; var Mathjax = { __stubbed: true, tex2svg: function (a, b) { require([ '/bower_components/MathJax/es5/tex-svg.js', ], function () { console.debug("Loaded mathjax"); if (Mathjax.__stubbed) { Mathjax = window.MathJax; } Mathjax.tex2svg(a, b); pluginLoaded.fire(); }); } }; var drawMarkmap; var MarkMapTransform; var Markmap; var markmapLoaded = false; var loadMarkmap = function ($el) { require([ '/lib/markmap/transform.min.js', '/lib/markmap/view.min.js', ], function (_Transform, _View) { if (!markmapLoaded) { console.debug("Loaded markmap"); MarkMapTransform = _Transform; Markmap = _View; markmapLoaded = true; } drawMarkmap($el); pluginLoaded.fire(); }); }; var sfCommon; var fixMarkmapClickables = function ($svg) { // find all links in the tree and do the following for each one var onClick = function (e) { e.preventDefault(); e.stopImmediatePropagation(); var $el = $(e.target); // Open links only from the preview modal if (!sfCommon) { return void console.error('No sfCommon'); } var href = $el.attr('href'); if (!href || !/^(https?:\/\/|\/)/.test(href)) { return; } if (/^http/.test(href)) { sfCommon.openUnsafeURL(href); return; } sfCommon.openURL(href); }; $svg.find('a').click(onClick); // make sure the links added later by collapsing/expading the map are also safe var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'childList') { var n; for (var i = 0; i < mutation.addedNodes.length; i++) { n = mutation.addedNodes[i]; if (n.nodeName === "A") { return void n.addEventListener('click', onClick); } $(n).find('a').click(onClick); } } }); }); observer.observe($svg[0], { childList: true, subtree: true }); }; drawMarkmap = function ($el) { if (!markmapLoaded) { return void loadMarkmap($el); } if (!$el) { return console.error("no element provided"); } var data = MarkMapTransform.transform($el[0].getAttribute("markmap-source")); $el[0].innerHTML = ""; Markmap.markmap($el[0].firstChild, data); fixMarkmapClickables($el); }; var highlighter = function () { return function(code, lang) { if (lang) { try { return Highlight.highlight(lang, code).value; } catch (e) { return code; } } return code; }; }; Marked.setOptions({ //sanitize: true, // Disable HTML renderer: renderer, highlight: highlighter(), }); var toc = []; var getTOC = function () { var content = [h('h2', Messages.markdown_toc)]; toc.forEach(function (obj) { // Only include level 2 headings var level = obj.level - 1; if (level < 1) { return; } var a = h('a.cp-md-toc-link', { href: '#', 'data-href': obj.id, }); a.innerHTML = obj.title; content.push(h('p.cp-md-toc-'+level, ['• ', a])); }); return h('div.cp-md-toc', content).outerHTML; }; var noHeadingId = false; DiffMd.render = function (md, sanitize, restrictedMd, noId) { Marked.setOptions({ renderer: restrictedMd ? restrictedRenderer : renderer, }); noHeadingId = noId; var r = Marked(md, { sanitize: sanitize, headerIds: !noId, gfm: true, }); // Add Table of Content if (!restrictedMd) { r = r.replace(/
'+Util.fixHTML(code)+''; } else if (language === 'markmap') { return '
'+Util.fixHTML(code)+''; } else if (language === 'mathjax') { return '
'+Util.fixHTML(code)+''; } else { return defaultCode.apply(renderer, arguments); } }; restrictedRenderer.code = renderer.code; var _heading = renderer.heading; renderer.heading = function (text, level) { if (noHeadingId) { return _heading.apply(this, arguments); } var i = 0; var safeText = text.toLowerCase().replace(/[^\w]+/g, '-'); var getId = function () { return 'cp-md-' + i + '-' + safeText; }; var id = getId(); var isAlreadyUsed = function (obj) { return obj.id === id; }; while (toc.some(isAlreadyUsed)) { i++; id = getId(); } toc.push({ level: level, id: id, title: Util.stripTags(text) }); return "
)?\[[xX]\](<\/p>)?\s*/; var uncheckedTaskItemPtn = /^\s*(
)?\[ ?\](<\/p>)?\s*/; var bogusCheckPtn = //; var bogusUncheckPtn = //; renderer.listitem = function (text) { var isCheckedTaskItem = checkedTaskItemPtn.test(text); var isUncheckedTaskItem = uncheckedTaskItemPtn.test(text); var hasBogusCheckedInput = bogusCheckPtn.test(text); var hasBogusUncheckedInput = bogusUncheckPtn.test(text); var isCheckbox = true; if (isCheckedTaskItem) { text = text.replace(checkedTaskItemPtn, '') + '\n'; } else if (isUncheckedTaskItem) { text = text.replace(uncheckedTaskItemPtn, '') + '\n'; } else if (hasBogusCheckedInput) { text = text.replace(bogusCheckPtn, '') + '\n'; } else if (hasBogusUncheckedInput) { text = text.replace(bogusUncheckPtn, '') + '\n'; } else { isCheckbox = false; } var cls = (isCheckbox) ? ' class="todo-list-item"' : ''; return '
' + p + '
\n'; }; renderer.paragraph = function (p) { if (p === '[TOC]') { return ''; } return renderParagraph(p); }; restrictedRenderer.paragraph = function (p) { return renderParagraph(p); }; // Note: iframe, video and audio are used in mediatags and are allowed in rich text pads. var forbiddenTags = [ 'SCRIPT', //'IFRAME', 'OBJECT', 'APPLET', //'VIDEO', // privacy implications of videos are the same as images //'AUDIO', // same with audio 'SOURCE' ]; var restrictedTags = [ 'IFRAME', 'VIDEO', 'AUDIO' ]; var unsafeTag = function (info) { /*if (info.node && $(info.node).parents('media-tag').length) { // Do not remove elements inside a media-tag return true; }*/ if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) { if (/^on/i.test(info.diff.name)) { console.log("Rejecting forbidden element attribute with name", info.diff.name); return true; } } if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) { var msg = "Rejecting forbidden tag of type (%s)"; if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName.toUpperCase()) !== -1) { console.log(msg, info.diff.element.nodeName); return true; } else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeName.toUpperCase()) !== -1) { console.log("Replacing restricted element type (%s) with PRE", info.diff.newValue.nodeName); info.diff.newValue.nodeName = 'PRE'; } } }; var slice = function (coll) { return Array.prototype.slice.call(coll); }; var removeNode = function (node) { if (!(node && node.parentElement)) { return; } var parent = node.parentElement; if (!parent) { return; } console.debug('removing %s tag', node.nodeName); parent.removeChild(node); }; // Only allow iframe, video and audio with local source var checkSrc = function (root) { if (restrictedTags.indexOf(root.nodeName.toUpperCase()) === -1) { return true; } return root.getAttribute && /^(blob\:|\/lib\/pdfjs)/.test(root.getAttribute('src')); }; var removeForbiddenTags = function (root) { if (!root) { return; } if (forbiddenTags.indexOf(root.nodeName.toUpperCase()) !== -1) { removeNode(root); } if (!checkSrc(root)) { removeNode(root); } slice(root.children).forEach(removeForbiddenTags); }; /* remove listeners from the DOM */ var removeListeners = function (root) { if (!root) { return; } slice(root.attributes).map(function (attr) { if (/^on/i.test(attr.name)) { console.log('removing attribute', attr.name, root.attributes[attr.name]); root.attributes.removeNamedItem(attr.name); } }); // all the way down slice(root.children).forEach(removeListeners); }; var domFromHTML = function (html) { var Dom = new DOMParser().parseFromString(html, "text/html"); Dom.normalize(); removeForbiddenTags(Dom.body); removeListeners(Dom.body); return Dom; }; var DD = new DiffDOM({ preDiffApply: function (info) { if (unsafeTag(info)) { return true; } }, }); var makeDiff = function (A, B, id) { var Err; var Els = [A, B].map(function (frag) { if (typeof(frag) === 'object') { if (!frag || (frag && !frag.body)) { Err = "No body"; return; } var els = frag.body.querySelectorAll('#'+id); if (els.length) { return els[0]; } } Err = 'No candidate found'; }); if (Err) { return Err; } var patch = DD.diff(Els[0], Els[1]); return patch; }; var plugins = {}; var removeMermaidClickables = function ($el) { // find all links in the tree and do the following for each one $el.find('a').each(function (index, a) { var parent = a.parentElement; if (!parent) { return; } // iterate over the links' children and transform them into preceding children // to preserve their visible ordering slice(a.children).forEach(function (child) { parent.insertBefore(child, a); }); // remove the link once it has been emptied $(a).remove(); }); // finally, find all 'clickable' items and remove the class $el.find('.clickable').removeClass('clickable'); }; plugins.mermaid = { name: 'mermaid', attr: 'mermaid-source', render: function ($el) { Mermaid.init(undefined, $el); // clickable elements in mermaid don't work well with our sandboxing setup // the function below strips clickable elements but still leaves behind some artifacts // tippy tooltips might still be useful, so they're not removed. It would be // preferable to just support links, but this covers up a rough edge in the meantime removeMermaidClickables($el); } }; plugins.markmap = { name: 'markmap', attr: 'markmap-source', render: function ($el) { drawMarkmap($el); } }; plugins.mathjax = { name: 'mathjax', attr: 'mathjax-source', render: function renderMathjax ($el) { var el = $el[0]; if (!el) { return; } var code = el.getAttribute("mathjax-source"); var svg = Mathjax.tex2svg(code, {display: true}); if (!svg) { return; } svg.innerHTML = svg.innerHTML.replace(/xlink:href/g, "href"); var wrapper = document.createElement('span'); wrapper.innerHTML = svg.innerHTML; el.innerHTML = wrapper.outerHTML; } }; var applyCSS = function (el, css) { var style = h('style'); style.appendChild(document.createTextNode(css)); el.innerText = ''; el.appendChild(style); }; // trim non-functional text from less input so that // the compiler is only triggered when there has been a functional change var canonicalizeLess = function (source) { return (source || '') // leading and trailing spaces are irrelevant .trim() // line comments are easy to disregard .replace(/\/\/[^\n]*/g, '') // lines with nothing but spaces and tabs can be ignored .replace(/^[ \t]*$/g, '') // consecutive newlines make no difference .replace(/\n+/g, ''); }; var rendered_less = {}; var getRenderedLess = (function () { var timeouts = {}; return function (src) { if (!rendered_less[src]) { return; } if (timeouts[src]) { clearTimeout(timeouts[src]); } // avoid memory leaks by deleting cached content // 15s after it was last accessed timeouts[src] = setTimeout(function () { delete rendered_less[src]; delete timeouts[src]; }, 15000); return rendered_less[src]; }; }()); plugins.less = { name: 'less', attr: 'less-src', render: function renderLess ($el, opt) { var src = canonicalizeLess($el.text()); if (!src) { return; } var el = $el[0]; var rendered = getRenderedLess(src); if (rendered) { return void applyCSS(el, rendered); } var scope = opt.scope.attr('id') || 'cp-app-code-preview-content'; var scoped_src = '#' + scope + ' { ' + src + '}'; //console.error("RENDERING LESS"); Less.render(scoped_src, {}, function (err, result) { // the console is the only feedback for users to know that they did something wrong // but less rendering isn't intended so much as a feature but a useful tool to avoid // leaking styles from the preview into the rest of the DOM. This is an improvement. if (err) { // we assume the compiler is deterministic. Something that returns an error once // will do it again, so avoid successive calls by caching a truthy // but non-functional string to block them. rendered_less[src] = ' '; return void console.error(err); } var css = rendered_less[src] = result.css; applyCSS(el, css); }); }, }; var getAvailableCachedElement = function ($content, cache, src) { var cached = cache[src]; if (!Array.isArray(cached)) { return; } var root = $content[0]; var l = cached.length; for (var i = 0; i < l; i++) { if (!root.contains(cached[i])) { return cached[i]; } } }; var cacheRenderedElement = function (cache, src, el) { if (Array.isArray(cache[src])) { cache[src].push(el); } else { cache[src] = [ el ]; } }; // remove elements from the cache that are not embedded in the dom var clearUnusedCacheEntries = function ($content, plugins) { var root = $content[0]; Object.keys(plugins).forEach(function (name) { var plugin = plugins[name]; var cache = plugin.cache; Object.keys(cache).forEach(function (key) { var list = cache[key]; if (!Array.isArray(list)) { return; } cache[key] = list.filter(function (el) { return root.contains(el); }); }); }); }; DiffMd.apply = function (newHtml, $content, common) { if (!sfCommon) { sfCommon = common; } var contextMenu = common.importMediaTagMenu(); var id = $content.attr('id'); if (!id) { throw new Error("The element must have a valid id"); } var pattern = /(