From e08452770146382654817176856a629553e48b20 Mon Sep 17 00:00:00 2001 From: Romain Courteaud <romain@nexedi.com> Date: Fri, 1 Mar 2019 08:48:27 +0000 Subject: [PATCH] [erp5_notebook] Implement a JSMD viewer This is not a notebook (as it does not provide a "live" editing functionnality). It is only used to trigger the calculation and view the result. Except for the JSMD format, this code is not related to iodide. Python plugins are not supported for now. --- .../erp5_notebook/gadget_jsmd_eval.html.html | 19 + .../erp5_notebook/gadget_jsmd_eval.html.xml | 28 + .../erp5_notebook/gadget_jsmd_eval.js.js | 309 +++ .../erp5_notebook/gadget_jsmd_eval.js.xml | 28 + .../erp5_notebook/gadget_jsmd_viewer.css.css | 34 + .../erp5_notebook/gadget_jsmd_viewer.css.xml | 28 + .../gadget_jsmd_viewer.html.html | 20 + .../erp5_notebook/gadget_jsmd_viewer.html.xml | 28 + .../erp5_notebook/gadget_jsmd_viewer.js.js | 85 + .../erp5_notebook/gadget_jsmd_viewer.js.xml | 28 + .../portal_skins/erp5_notebook/marked.js.js | 1690 +++++++++++++++++ .../portal_skins/erp5_notebook/marked.js.xml | 28 + .../erp5_core/gadget_editor.js.js | 16 +- 13 files changed, 2337 insertions(+), 4 deletions(-) create mode 100644 bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.html.html create mode 100644 bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.html.xml create mode 100644 bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.js.js create mode 100644 bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.js.xml create mode 100644 bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.css.css create mode 100644 bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.css.xml create mode 100644 bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.html.html create mode 100644 bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.html.xml create mode 100644 bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.js.js create mode 100644 bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.js.xml create mode 100644 bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/marked.js.js create mode 100644 bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/marked.js.xml diff --git a/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.html.html b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.html.html new file mode 100644 index 0000000000..6897c1c58f --- /dev/null +++ b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.html.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Security-Policy" + content="script-src 'unsafe-inline' 'unsafe-eval' *; style-src 'unsafe-inline' *"> + + <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> + <meta name="viewport" content="width=device-width" /> + + <title>JSMD Eval Gadget</title> + + <script src="rsvp.js" type="text/javascript"></script> + <script src="marked.js" type="text/javascript"></script> + <script src="gadget_jsmd_eval.js" type="text/javascript"></script> + </head> + + <body class="pane-content"> + </body> +</html> diff --git a/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.html.xml b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.html.xml new file mode 100644 index 0000000000..b30a9eb6e1 --- /dev/null +++ b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.html.xml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="File" module="OFS.Image"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>__name__</string> </key> + <value> <string>gadget_jsmd_eval.html</string> </value> + </item> + <item> + <key> <string>content_type</string> </key> + <value> <string>text/html</string> </value> + </item> + <item> + <key> <string>precondition</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string></string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.js.js b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.js.js new file mode 100644 index 0000000000..3e8228b9d1 --- /dev/null +++ b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.js.js @@ -0,0 +1,309 @@ +/*global window, console, RSVP, document, URL, eval, XMLHttpRequest, marked */ +/*jslint nomen: true, indent: 2, maxerr: 3 */ +(function (window) { + "use strict"; + + var IODide = function createIODide() { + return; + }, + JSMDCell = function createJSMDCell(type, line_list) { + this._type = type; + this._line_list = line_list; + }, + split_line_regex = /[\r\n|\n|\r]/, + cell_type_regexp = /^\%\% (\w+)$/; + + window.iodide = new IODide(); + + IODide.prototype.addOutputHandler = function () { + return; + }; + + // Copied from jio + function ajax(param) { + var xhr = new XMLHttpRequest(); + return new RSVP.Promise(function (resolve, reject) { + var k; + xhr.open(param.type || "GET", param.url, true); + xhr.responseType = param.dataType || ""; + if (typeof param.headers === 'object' && param.headers !== null) { + for (k in param.headers) { + if (param.headers.hasOwnProperty(k)) { + xhr.setRequestHeader(k, param.headers[k]); + } + } + } + xhr.addEventListener("load", function (e) { + if (e.target.status >= 400) { + return reject(e); + } + resolve(e); + }); + xhr.addEventListener("error", reject); + if (typeof param.xhrFields === 'object' && param.xhrFields !== null) { + for (k in param.xhrFields) { + if (param.xhrFields.hasOwnProperty(k)) { + xhr[k] = param.xhrFields[k]; + } + } + } + if (param.timeout !== undefined && param.timeout !== 0) { + xhr.timeout = param.timeout; + xhr.ontimeout = function () { + return reject(new Error("Gateway Timeout")); + }; + } + if (typeof param.beforeSend === 'function') { + param.beforeSend(xhr); + } + xhr.send(param.data); + }, function () { + xhr.abort(); + }); + } + + function parseJSMDCellList(jsmd) { + // Split the text into a list of Iodide cells + var line_list = jsmd.split(split_line_regex), + i, + len = line_list.length, + current_line, + current_type, + current_text_list, + next_type, + cell_list = []; + + function pushNewCell() { + if (current_type !== undefined) { + cell_list.push(new JSMDCell(current_type[1], + current_text_list)); + } + } + + for (i = 0; i < len; i += 1) { + current_line = line_list[i]; + next_type = current_line.match(cell_type_regexp); + if (next_type) { + // New type detexted + pushNewCell(); + current_type = next_type; + current_text_list = []; + } else if (current_text_list !== undefined) { + current_text_list.push(current_line); + } + } + // Push last cell + pushNewCell(); + return cell_list; + } + + function executeUnknownCellType(cell) { + throw new Error('Unsupported cell: ' + cell._type); + } + + function executeJSCell(line_list) { + // console.info('eval', line_list); + var text = line_list.join('\n'), + pre, + br, + code; + try { + return eval.call(window, text); + } catch (e) { + console.error(e); + pre = document.createElement('pre'); + pre.textContent = e.message; + + br = document.createElement('br'); + pre.appendChild(br); + + code = document.createElement('code'); + code.textContent = text; + pre.appendChild(code); + document.body.appendChild(pre); + throw e; + } + } + + function executeCssCell(line_list) { + var style = document.createElement('style'); + style.textContent = line_list.join('\n'); + document.head.appendChild(style); + } + + function loadJSResource(url) { + // Copied from renderJS + return new RSVP.Promise( + function waitForJSLoadEvent(resolve, reject) { + var newScript; + newScript = document.createElement('script'); + newScript.async = false; + newScript.type = 'text/javascript'; + newScript.onload = function (evt) { + resolve(evt.target.value); + }; + newScript.onerror = function (error) { + console.warn(error); + reject(error); + }; + newScript.src = url; + document.head.appendChild(newScript); + } + ); + } + + function deferJSResourceLoading(url) { + return function () { + return loadJSResource(url); + }; + } + + function loadCSSResource(url) { + // Copied from renderJS + return new RSVP.Promise( + function waitForCSSLoadEvent(resolve, reject) { + var link; + link = document.createElement('link'); + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = url; + link.onload = resolve; + link.onerror = reject; + document.head.appendChild(link); + } + ); + } + + function deferCSSResourceLoading(url) { + return function () { + return loadCSSResource(url); + }; + } + + function loadTextResource(line) { + var line_split = line.split('=', 2), + variable = line_split[0], + url = line_split[1]; + console.log(line_split); + return new RSVP.Queue() + .push(function () { + return ajax({url: url}); + }) + .push(function (evt) { + window[variable] = evt.target.responseText; + }); + } + + function deferTextResourceLoading(line) { + return function () { + return loadTextResource(line); + }; + } + + function executeResourceCell(line_list) { + var queue = new RSVP.Queue(), + len = line_list.length, + i; + for (i = 0; i < len; i += 1) { + if (line_list[i]) { + queue.push(deferJSResourceLoading(line_list[i])); + } + } + return queue; + } + + function executeFetchCell(line_list) { + var queue = new RSVP.Queue(), + len = line_list.length, + i, + line; + for (i = 0; i < len; i += 1) { + line = line_list[i]; + if (line) { + if (line.startsWith('js: ')) { + queue.push(deferJSResourceLoading(line.slice(4))); + } else if (line.startsWith('css: ')) { + queue.push(deferCSSResourceLoading(line.slice(5))); + } else if (line.startsWith('text: ')) { + queue.push(deferTextResourceLoading(line.slice(6))); + } else { + queue.cancel(); + throw new Error('Unsupported fetch type: ' + line); + } + } + } + return queue; + } + + function executeMarkdownCell(line_list) { + var renderer = new marked.Renderer(); + return new RSVP.Promise(function (resolve, reject) { + marked(line_list.join('\n'), + {renderer: renderer}, + function (err, content) { + if (err) { + reject(err); + } + var div = document.createElement('div'); + div.classList.add('user-markdown'); + div.innerHTML = content; + document.body.appendChild(div); + resolve(); + }); + }); + } + + function executeCell(cell) { + if (cell._type === 'raw') { + // Do nothing... + return; + } + if (cell._type === 'js') { + return executeJSCell(cell._line_list); + } + if (cell._type === 'resource') { + return executeResourceCell(cell._line_list); + } + if (cell._type === 'fetch') { + return executeFetchCell(cell._line_list); + } + if (cell._type === 'md') { + return executeMarkdownCell(cell._line_list); + } + if (cell._type === 'css') { + return executeCssCell(cell._line_list); + } + return executeUnknownCellType(cell); + } + + function deferCellExecution(cell) { + return function () { + return executeCell(cell); + }; + } + + + document.addEventListener('DOMContentLoaded', function () { + + var jsmd = document.querySelector('[type="text/x-jsmd"]').textContent, + cell_list = parseJSMDCellList(jsmd), + len = cell_list.length, + i, + queue = new RSVP.Queue(); + + for (i = 0; i < len; i += 1) { + queue.push(deferCellExecution(cell_list[i])); + } + return queue + .push(function () { + console.info('JSMD executed.'); + }, function (error) { + console.error(error); + var pre = document.createElement('pre'); + pre.textContent = error; + document.body.appendChild(pre); + }); + + }, false); + +}(window)); \ No newline at end of file diff --git a/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.js.xml b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.js.xml new file mode 100644 index 0000000000..35ac36d687 --- /dev/null +++ b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_eval.js.xml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="File" module="OFS.Image"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>__name__</string> </key> + <value> <string>gadget_jsmd_eval.js</string> </value> + </item> + <item> + <key> <string>content_type</string> </key> + <value> <string>application/javascript</string> </value> + </item> + <item> + <key> <string>precondition</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string></string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.css.css b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.css.css new file mode 100644 index 0000000000..2b4653ad96 --- /dev/null +++ b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.css.css @@ -0,0 +1,34 @@ +html { + height: 100%; + width: 100%; + display: block; + + box-sizing: border-box; + margin: 0; + padding: 0; + +} + +*, *:before, *:after { + box-sizing: inherit; +} + +body { + height: 100%; + width: 100%; + display: block; + + word-wrap: break-word; + margin: 0; + padding: 0; +} + +iframe { + max-width: 100%; + max-height: 100vh; + width: 100%; + height: 25em; + border: none; + margin: 0; + padding: 0; +} diff --git a/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.css.xml b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.css.xml new file mode 100644 index 0000000000..393f137992 --- /dev/null +++ b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.css.xml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="File" module="OFS.Image"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>__name__</string> </key> + <value> <string>gadget_jsmd_viewer.css</string> </value> + </item> + <item> + <key> <string>content_type</string> </key> + <value> <string>text/css</string> </value> + </item> + <item> + <key> <string>precondition</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string></string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.html.html b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.html.html new file mode 100644 index 0000000000..af003ac29f --- /dev/null +++ b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.html.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> + <meta name="viewport" content="width=device-width" /> + + <title>JSMD Editor Gadget</title> + <link rel="stylesheet" type="text/css" href="gadget_jsmd_viewer.css" /> + + <script src="rsvp.js" type="text/javascript"></script> + <script src="renderjs.js" type="text/javascript"></script> + <script src="jiodev.js" type="text/javascript"></script> + <script src="gadget_global.js" type="text/javascript"></script> + <script src="gadget_jsmd_viewer.js" type="text/javascript"></script> + </head> + + <body> + <div></div> + </body> +</html> diff --git a/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.html.xml b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.html.xml new file mode 100644 index 0000000000..a36ccda15b --- /dev/null +++ b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.html.xml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="File" module="OFS.Image"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>__name__</string> </key> + <value> <string>gadget_jsmd_viewer.html</string> </value> + </item> + <item> + <key> <string>content_type</string> </key> + <value> <string>text/html</string> </value> + </item> + <item> + <key> <string>precondition</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string></string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.js.js b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.js.js new file mode 100644 index 0000000000..c2b9d1cd62 --- /dev/null +++ b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.js.js @@ -0,0 +1,85 @@ +/*global window, rJS, console, RSVP, jIO, DOMParser, Blob, document, + URL, loopEventListener */ +/*jslint nomen: true, indent: 2, maxerr: 3 */ +(function (window, rJS, RSVP, jIO, DOMParser, document, URL, + loopEventListener) { + "use strict"; + + function fetchHTML(url, base_url) { + url = new URL(url, base_url).href; + return new RSVP.Queue() + .push(function () { + return jIO.util.ajax({url: url}); + }) + .push(function (evt) { + // Insert a "base" element, in order to resolve all relative links + // which could get broken with a data url + var doc = (new DOMParser()).parseFromString(evt.target.responseText, + 'text/html'), + base = doc.createElement('base'); + base.href = url; + doc.head.insertBefore(base, doc.head.firstChild); + return doc; + }); + } + + rJS(window) + ///////////////////////////////////////////////////////////////// + // declared methods + ///////////////////////////////////////////////////////////////// + + .declareMethod("render", function (options) { + return this.changeState(options); + }) + + .onStateChange(function () { + // Reset everything if something change + + var gadget = this, + base_url = document.location.toString(), + doc; + + return fetchHTML("gadget_jsmd_eval.html", base_url) + .push(function (result) { + doc = result; + // Insert text + doc.body.textContent = ''; + + // Insert the JSMD value inside the HTML + var script = document.createElement('script'), + iframe = document.createElement("iframe"); + + script.setAttribute('type', 'text/x-jsmd'); + script.setAttribute('id', 'jsmd-source'); + script.textContent = gadget.state.value; + doc.head.appendChild(script); + +/* + blob = new Blob([doc.documentElement.outerHTML], + {type: "text/html;charset=UTF-8"}); + return jIO.util.readBlobAsDataURL(blob); + }) + .push(function (evt) { +*/ + // XXX Insecure + iframe.setAttribute("sandbox", "allow-scripts allow-same-origin"); + // iframe.setAttribute("csp", "default-src *; script-src * 'unsafe-inline';"); + // iframe.setAttribute("src", evt.target.result); + iframe.setAttribute("srcdoc", doc.documentElement.outerHTML); + gadget.element.innerHTML = iframe.outerHTML; + gadget.listenResize(); + }); + }) + + .declareJob('listenResize', function () { + var gadget = this; + function resize() { + gadget.element.querySelector("iframe").style.height = + (window.innerHeight - + gadget.element.querySelector("iframe").offsetTop) + "px"; + } + resize(); + return loopEventListener(window, 'resize', false, resize); + }); + +}(window, rJS, RSVP, jIO, DOMParser, document, URL, loopEventListener)); \ No newline at end of file diff --git a/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.js.xml b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.js.xml new file mode 100644 index 0000000000..37b65a09bf --- /dev/null +++ b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/gadget_jsmd_viewer.js.xml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="File" module="OFS.Image"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>__name__</string> </key> + <value> <string>gadget_jsmd_viewer.js</string> </value> + </item> + <item> + <key> <string>content_type</string> </key> + <value> <string>application/javascript</string> </value> + </item> + <item> + <key> <string>precondition</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string></string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/marked.js.js b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/marked.js.js new file mode 100644 index 0000000000..db76720d95 --- /dev/null +++ b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/marked.js.js @@ -0,0 +1,1690 @@ +/** + * marked - a markdown parser + * Copyright (c) 2011-2018, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ + +;(function(root) { +'use strict'; + +/** + * Block-Level Grammar + */ + +var block = { + newline: /^\n+/, + code: /^( {4}[^\n]+\n*)+/, + fences: noop, + hr: /^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/, + heading: /^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/, + nptable: noop, + blockquote: /^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/, + list: /^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, + html: '^ {0,3}(?:' // optional indentation + + '<(script|pre|style)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)' // (1) + + '|comment[^\\n]*(\\n+|$)' // (2) + + '|<\\?[\\s\\S]*?\\?>\\n*' // (3) + + '|<![A-Z][\\s\\S]*?>\\n*' // (4) + + '|<!\\[CDATA\\[[\\s\\S]*?\\]\\]>\\n*' // (5) + + '|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:\\n{2,}|$)' // (6) + + '|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$)' // (7) open tag + + '|</(?!script|pre|style)[a-z][\\w-]*\\s*>(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$)' // (7) closing tag + + ')', + def: /^ {0,3}\[(label)\]: *\n? *<?([^\s>]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/, + table: noop, + lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, + paragraph: /^([^\n]+(?:\n(?!hr|heading|lheading| {0,3}>|<\/?(?:tag)(?: +|\n|\/?>)|<(?:script|pre|style|!--))[^\n]+)*)/, + text: /^[^\n]+/ +}; + +block._label = /(?!\s*\])(?:\\[\[\]]|[^\[\]])+/; +block._title = /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/; +block.def = edit(block.def) + .replace('label', block._label) + .replace('title', block._title) + .getRegex(); + +block.bullet = /(?:[*+-]|\d{1,9}\.)/; +block.item = /^( *)(bull) ?[^\n]*(?:\n(?!\1bull ?)[^\n]*)*/; +block.item = edit(block.item, 'gm') + .replace(/bull/g, block.bullet) + .getRegex(); + +block.list = edit(block.list) + .replace(/bull/g, block.bullet) + .replace('hr', '\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))') + .replace('def', '\\n+(?=' + block.def.source + ')') + .getRegex(); + +block._tag = 'address|article|aside|base|basefont|blockquote|body|caption' + + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption' + + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe' + + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' + + '|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr' + + '|track|ul'; +block._comment = /<!--(?!-?>)[\s\S]*?-->/; +block.html = edit(block.html, 'i') + .replace('comment', block._comment) + .replace('tag', block._tag) + .replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/) + .getRegex(); + +block.paragraph = edit(block.paragraph) + .replace('hr', block.hr) + .replace('heading', block.heading) + .replace('lheading', block.lheading) + .replace('tag', block._tag) // pars can be interrupted by type (6) html blocks + .getRegex(); + +block.blockquote = edit(block.blockquote) + .replace('paragraph', block.paragraph) + .getRegex(); + +/** + * Normal Block Grammar + */ + +block.normal = merge({}, block); + +/** + * GFM Block Grammar + */ + +block.gfm = merge({}, block.normal, { + fences: /^ {0,3}(`{3,}|~{3,})([^`\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/, + paragraph: /^/, + heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/ +}); + +block.gfm.paragraph = edit(block.paragraph) + .replace('(?!', '(?!' + + block.gfm.fences.source.replace('\\1', '\\2') + '|' + + block.list.source.replace('\\1', '\\3') + '|') + .getRegex(); + +/** + * GFM + Tables Block Grammar + */ + +block.tables = merge({}, block.gfm, { + nptable: /^ *([^|\n ].*\|.*)\n *([-:]+ *\|[-| :]*)(?:\n((?:.*[^>\n ].*(?:\n|$))*)\n*|$)/, + table: /^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/ +}); + +/** + * Pedantic grammar + */ + +block.pedantic = merge({}, block.normal, { + html: edit( + '^ *(?:comment *(?:\\n|\\s*$)' + + '|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)' // closed tag + + '|<tag(?:"[^"]*"|\'[^\']*\'|\\s[^\'"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))') + .replace('comment', block._comment) + .replace(/tag/g, '(?!(?:' + + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' + + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' + + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b') + .getRegex(), + def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/ +}); + +/** + * Block Lexer + */ + +function Lexer(options) { + this.tokens = []; + this.tokens.links = Object.create(null); + this.options = options || marked.defaults; + this.rules = block.normal; + + if (this.options.pedantic) { + this.rules = block.pedantic; + } else if (this.options.gfm) { + if (this.options.tables) { + this.rules = block.tables; + } else { + this.rules = block.gfm; + } + } +} + +/** + * Expose Block Rules + */ + +Lexer.rules = block; + +/** + * Static Lex Method + */ + +Lexer.lex = function(src, options) { + var lexer = new Lexer(options); + return lexer.lex(src); +}; + +/** + * Preprocessing + */ + +Lexer.prototype.lex = function(src) { + src = src + .replace(/\r\n|\r/g, '\n') + .replace(/\t/g, ' ') + .replace(/\u00a0/g, ' ') + .replace(/\u2424/g, '\n'); + + return this.token(src, true); +}; + +/** + * Lexing + */ + +Lexer.prototype.token = function(src, top) { + src = src.replace(/^ +$/gm, ''); + var next, + loose, + cap, + bull, + b, + item, + listStart, + listItems, + t, + space, + i, + tag, + l, + isordered, + istask, + ischecked; + + while (src) { + // newline + if (cap = this.rules.newline.exec(src)) { + src = src.substring(cap[0].length); + if (cap[0].length > 1) { + this.tokens.push({ + type: 'space' + }); + } + } + + // code + if (cap = this.rules.code.exec(src)) { + src = src.substring(cap[0].length); + cap = cap[0].replace(/^ {4}/gm, ''); + this.tokens.push({ + type: 'code', + text: !this.options.pedantic + ? rtrim(cap, '\n') + : cap + }); + continue; + } + + // fences (gfm) + if (cap = this.rules.fences.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'code', + lang: cap[2] ? cap[2].trim() : cap[2], + text: cap[3] || '' + }); + continue; + } + + // heading + if (cap = this.rules.heading.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'heading', + depth: cap[1].length, + text: cap[2] + }); + continue; + } + + // table no leading pipe (gfm) + if (top && (cap = this.rules.nptable.exec(src))) { + item = { + type: 'table', + header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')), + align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + cells: cap[3] ? cap[3].replace(/\n$/, '').split('\n') : [] + }; + + if (item.header.length === item.align.length) { + src = src.substring(cap[0].length); + + for (i = 0; i < item.align.length; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = 'right'; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = 'center'; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = 'left'; + } else { + item.align[i] = null; + } + } + + for (i = 0; i < item.cells.length; i++) { + item.cells[i] = splitCells(item.cells[i], item.header.length); + } + + this.tokens.push(item); + + continue; + } + } + + // hr + if (cap = this.rules.hr.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'hr' + }); + continue; + } + + // blockquote + if (cap = this.rules.blockquote.exec(src)) { + src = src.substring(cap[0].length); + + this.tokens.push({ + type: 'blockquote_start' + }); + + cap = cap[0].replace(/^ *> ?/gm, ''); + + // Pass `top` to keep the current + // "toplevel" state. This is exactly + // how markdown.pl works. + this.token(cap, top); + + this.tokens.push({ + type: 'blockquote_end' + }); + + continue; + } + + // list + if (cap = this.rules.list.exec(src)) { + src = src.substring(cap[0].length); + bull = cap[2]; + isordered = bull.length > 1; + + listStart = { + type: 'list_start', + ordered: isordered, + start: isordered ? +bull : '', + loose: false + }; + + this.tokens.push(listStart); + + // Get each top-level item. + cap = cap[0].match(this.rules.item); + + listItems = []; + next = false; + l = cap.length; + i = 0; + + for (; i < l; i++) { + item = cap[i]; + + // Remove the list item's bullet + // so it is seen as the next token. + space = item.length; + item = item.replace(/^ *([*+-]|\d+\.) */, ''); + + // Outdent whatever the + // list item contains. Hacky. + if (~item.indexOf('\n ')) { + space -= item.length; + item = !this.options.pedantic + ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') + : item.replace(/^ {1,4}/gm, ''); + } + + // Determine whether the next list item belongs here. + // Backpedal if it does not belong in this list. + if (i !== l - 1) { + b = block.bullet.exec(cap[i + 1])[0]; + if (bull.length > 1 ? b.length === 1 + : (b.length > 1 || (this.options.smartLists && b !== bull))) { + src = cap.slice(i + 1).join('\n') + src; + i = l - 1; + } + } + + // Determine whether item is loose or not. + // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ + // for discount behavior. + loose = next || /\n\n(?!\s*$)/.test(item); + if (i !== l - 1) { + next = item.charAt(item.length - 1) === '\n'; + if (!loose) loose = next; + } + + if (loose) { + listStart.loose = true; + } + + // Check for task list items + istask = /^\[[ xX]\] /.test(item); + ischecked = undefined; + if (istask) { + ischecked = item[1] !== ' '; + item = item.replace(/^\[[ xX]\] +/, ''); + } + + t = { + type: 'list_item_start', + task: istask, + checked: ischecked, + loose: loose + }; + + listItems.push(t); + this.tokens.push(t); + + // Recurse. + this.token(item, false); + + this.tokens.push({ + type: 'list_item_end' + }); + } + + if (listStart.loose) { + l = listItems.length; + i = 0; + for (; i < l; i++) { + listItems[i].loose = true; + } + } + + this.tokens.push({ + type: 'list_end' + }); + + continue; + } + + // html + if (cap = this.rules.html.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: this.options.sanitize + ? 'paragraph' + : 'html', + pre: !this.options.sanitizer + && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'), + text: cap[0] + }); + continue; + } + + // def + if (top && (cap = this.rules.def.exec(src))) { + src = src.substring(cap[0].length); + if (cap[3]) cap[3] = cap[3].substring(1, cap[3].length - 1); + tag = cap[1].toLowerCase().replace(/\s+/g, ' '); + if (!this.tokens.links[tag]) { + this.tokens.links[tag] = { + href: cap[2], + title: cap[3] + }; + } + continue; + } + + // table (gfm) + if (top && (cap = this.rules.table.exec(src))) { + item = { + type: 'table', + header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')), + align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + cells: cap[3] ? cap[3].replace(/(?: *\| *)?\n$/, '').split('\n') : [] + }; + + if (item.header.length === item.align.length) { + src = src.substring(cap[0].length); + + for (i = 0; i < item.align.length; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = 'right'; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = 'center'; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = 'left'; + } else { + item.align[i] = null; + } + } + + for (i = 0; i < item.cells.length; i++) { + item.cells[i] = splitCells( + item.cells[i].replace(/^ *\| *| *\| *$/g, ''), + item.header.length); + } + + this.tokens.push(item); + + continue; + } + } + + // lheading + if (cap = this.rules.lheading.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'heading', + depth: cap[2] === '=' ? 1 : 2, + text: cap[1] + }); + continue; + } + + // top-level paragraph + if (top && (cap = this.rules.paragraph.exec(src))) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'paragraph', + text: cap[1].charAt(cap[1].length - 1) === '\n' + ? cap[1].slice(0, -1) + : cap[1] + }); + continue; + } + + // text + if (cap = this.rules.text.exec(src)) { + // Top-level should never reach here. + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'text', + text: cap[0] + }); + continue; + } + + if (src) { + throw new Error('Infinite loop on byte: ' + src.charCodeAt(0)); + } + } + + return this.tokens; +}; + +/** + * Inline-Level Grammar + */ + +var inline = { + escape: /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/, + autolink: /^<(scheme:[^\s\x00-\x1f<>]*|email)>/, + url: noop, + tag: '^comment' + + '|^</[a-zA-Z][\\w:-]*\\s*>' // self-closing tag + + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag + + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. <?php ?> + + '|^<![a-zA-Z]+\\s[\\s\\S]*?>' // declaration, e.g. <!DOCTYPE html> + + '|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>', // CDATA section + link: /^!?\[(label)\]\(href(?:\s+(title))?\s*\)/, + reflink: /^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/, + nolink: /^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/, + strong: /^__([^\s_])__(?!_)|^\*\*([^\s*])\*\*(?!\*)|^__([^\s][\s\S]*?[^\s])__(?!_)|^\*\*([^\s][\s\S]*?[^\s])\*\*(?!\*)/, + em: /^_([^\s_])_(?!_)|^\*([^\s*"<\[])\*(?!\*)|^_([^\s][\s\S]*?[^\s_])_(?!_|[^\spunctuation])|^_([^\s_][\s\S]*?[^\s])_(?!_|[^\spunctuation])|^\*([^\s"<\[][\s\S]*?[^\s*])\*(?!\*)|^\*([^\s*"<\[][\s\S]*?[^\s])\*(?!\*)/, + code: /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/, + br: /^( {2,}|\\)\n(?!\s*$)/, + del: noop, + text: /^(`+|[^`])[\s\S]*?(?=[\\<!\[`*]|\b_| {2,}\n|$)/ +}; + +// list of punctuation marks from common mark spec +// without ` and ] to workaround Rule 17 (inline code blocks/links) +inline._punctuation = '!"#$%&\'()*+,\\-./:;<=>?@\\[^_{|}~'; +inline.em = edit(inline.em).replace(/punctuation/g, inline._punctuation).getRegex(); + +inline._escapes = /\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g; + +inline._scheme = /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/; +inline._email = /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/; +inline.autolink = edit(inline.autolink) + .replace('scheme', inline._scheme) + .replace('email', inline._email) + .getRegex(); + +inline._attribute = /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/; + +inline.tag = edit(inline.tag) + .replace('comment', block._comment) + .replace('attribute', inline._attribute) + .getRegex(); + +inline._label = /(?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|`(?!`)|[^\[\]\\`])*?/; +inline._href = /\s*(<(?:\\[<>]?|[^\s<>\\])*>|[^\s\x00-\x1f]*)/; +inline._title = /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/; + +inline.link = edit(inline.link) + .replace('label', inline._label) + .replace('href', inline._href) + .replace('title', inline._title) + .getRegex(); + +inline.reflink = edit(inline.reflink) + .replace('label', inline._label) + .getRegex(); + +/** + * Normal Inline Grammar + */ + +inline.normal = merge({}, inline); + +/** + * Pedantic Inline Grammar + */ + +inline.pedantic = merge({}, inline.normal, { + strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, + em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/, + link: edit(/^!?\[(label)\]\((.*?)\)/) + .replace('label', inline._label) + .getRegex(), + reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/) + .replace('label', inline._label) + .getRegex() +}); + +/** + * GFM Inline Grammar + */ + +inline.gfm = merge({}, inline.normal, { + escape: edit(inline.escape).replace('])', '~|])').getRegex(), + _extended_email: /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/, + url: /^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/, + _backpedal: /(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/, + del: /^~+(?=\S)([\s\S]*?\S)~+/, + text: edit(inline.text) + .replace(']|', '~]|') + .replace('|$', '|https?://|ftp://|www\\.|[a-zA-Z0-9.!#$%&\'*+/=?^_`{\\|}~-]+@|$') + .getRegex() +}); + +inline.gfm.url = edit(inline.gfm.url, 'i') + .replace('email', inline.gfm._extended_email) + .getRegex(); +/** + * GFM + Line Breaks Inline Grammar + */ + +inline.breaks = merge({}, inline.gfm, { + br: edit(inline.br).replace('{2,}', '*').getRegex(), + text: edit(inline.gfm.text).replace('{2,}', '*').getRegex() +}); + +/** + * Inline Lexer & Compiler + */ + +function InlineLexer(links, options) { + this.options = options || marked.defaults; + this.links = links; + this.rules = inline.normal; + this.renderer = this.options.renderer || new Renderer(); + this.renderer.options = this.options; + + if (!this.links) { + throw new Error('Tokens array requires a `links` property.'); + } + + if (this.options.pedantic) { + this.rules = inline.pedantic; + } else if (this.options.gfm) { + if (this.options.breaks) { + this.rules = inline.breaks; + } else { + this.rules = inline.gfm; + } + } +} + +/** + * Expose Inline Rules + */ + +InlineLexer.rules = inline; + +/** + * Static Lexing/Compiling Method + */ + +InlineLexer.output = function(src, links, options) { + var inline = new InlineLexer(links, options); + return inline.output(src); +}; + +/** + * Lexing/Compiling + */ + +InlineLexer.prototype.output = function(src) { + var out = '', + link, + text, + href, + title, + cap, + prevCapZero; + + while (src) { + // escape + if (cap = this.rules.escape.exec(src)) { + src = src.substring(cap[0].length); + out += escape(cap[1]); + continue; + } + + // tag + if (cap = this.rules.tag.exec(src)) { + if (!this.inLink && /^<a /i.test(cap[0])) { + this.inLink = true; + } else if (this.inLink && /^<\/a>/i.test(cap[0])) { + this.inLink = false; + } + if (!this.inRawBlock && /^<(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + this.inRawBlock = true; + } else if (this.inRawBlock && /^<\/(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + this.inRawBlock = false; + } + + src = src.substring(cap[0].length); + out += this.options.sanitize + ? this.options.sanitizer + ? this.options.sanitizer(cap[0]) + : escape(cap[0]) + : cap[0]; + continue; + } + + // link + if (cap = this.rules.link.exec(src)) { + var lastParenIndex = findClosingBracket(cap[2], '()'); + if (lastParenIndex > -1) { + var removeChars = cap[2].length - lastParenIndex; + cap[2] = cap[2].substring(0, lastParenIndex); + cap[0] = cap[0].substring(0, cap[0].length - removeChars); + } + src = src.substring(cap[0].length); + this.inLink = true; + href = cap[2]; + if (this.options.pedantic) { + link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(href); + + if (link) { + href = link[1]; + title = link[3]; + } else { + title = ''; + } + } else { + title = cap[3] ? cap[3].slice(1, -1) : ''; + } + href = href.trim().replace(/^<([\s\S]*)>$/, '$1'); + out += this.outputLink(cap, { + href: InlineLexer.escapes(href), + title: InlineLexer.escapes(title) + }); + this.inLink = false; + continue; + } + + // reflink, nolink + if ((cap = this.rules.reflink.exec(src)) + || (cap = this.rules.nolink.exec(src))) { + src = src.substring(cap[0].length); + link = (cap[2] || cap[1]).replace(/\s+/g, ' '); + link = this.links[link.toLowerCase()]; + if (!link || !link.href) { + out += cap[0].charAt(0); + src = cap[0].substring(1) + src; + continue; + } + this.inLink = true; + out += this.outputLink(cap, link); + this.inLink = false; + continue; + } + + // strong + if (cap = this.rules.strong.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.strong(this.output(cap[4] || cap[3] || cap[2] || cap[1])); + continue; + } + + // em + if (cap = this.rules.em.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.em(this.output(cap[6] || cap[5] || cap[4] || cap[3] || cap[2] || cap[1])); + continue; + } + + // code + if (cap = this.rules.code.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.codespan(escape(cap[2].trim(), true)); + continue; + } + + // br + if (cap = this.rules.br.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.br(); + continue; + } + + // del (gfm) + if (cap = this.rules.del.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.del(this.output(cap[1])); + continue; + } + + // autolink + if (cap = this.rules.autolink.exec(src)) { + src = src.substring(cap[0].length); + if (cap[2] === '@') { + text = escape(this.mangle(cap[1])); + href = 'mailto:' + text; + } else { + text = escape(cap[1]); + href = text; + } + out += this.renderer.link(href, null, text); + continue; + } + + // url (gfm) + if (!this.inLink && (cap = this.rules.url.exec(src))) { + if (cap[2] === '@') { + text = escape(cap[0]); + href = 'mailto:' + text; + } else { + // do extended autolink path validation + do { + prevCapZero = cap[0]; + cap[0] = this.rules._backpedal.exec(cap[0])[0]; + } while (prevCapZero !== cap[0]); + text = escape(cap[0]); + if (cap[1] === 'www.') { + href = 'http://' + text; + } else { + href = text; + } + } + src = src.substring(cap[0].length); + out += this.renderer.link(href, null, text); + continue; + } + + // text + if (cap = this.rules.text.exec(src)) { + src = src.substring(cap[0].length); + if (this.inRawBlock) { + out += this.renderer.text(cap[0]); + } else { + out += this.renderer.text(escape(this.smartypants(cap[0]))); + } + continue; + } + + if (src) { + throw new Error('Infinite loop on byte: ' + src.charCodeAt(0)); + } + } + + return out; +}; + +InlineLexer.escapes = function(text) { + return text ? text.replace(InlineLexer.rules._escapes, '$1') : text; +}; + +/** + * Compile Link + */ + +InlineLexer.prototype.outputLink = function(cap, link) { + var href = link.href, + title = link.title ? escape(link.title) : null; + + return cap[0].charAt(0) !== '!' + ? this.renderer.link(href, title, this.output(cap[1])) + : this.renderer.image(href, title, escape(cap[1])); +}; + +/** + * Smartypants Transformations + */ + +InlineLexer.prototype.smartypants = function(text) { + if (!this.options.smartypants) return text; + return text + // em-dashes + .replace(/---/g, '\u2014') + // en-dashes + .replace(/--/g, '\u2013') + // opening singles + .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018') + // closing singles & apostrophes + .replace(/'/g, '\u2019') + // opening doubles + .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c') + // closing doubles + .replace(/"/g, '\u201d') + // ellipses + .replace(/\.{3}/g, '\u2026'); +}; + +/** + * Mangle Links + */ + +InlineLexer.prototype.mangle = function(text) { + if (!this.options.mangle) return text; + var out = '', + l = text.length, + i = 0, + ch; + + for (; i < l; i++) { + ch = text.charCodeAt(i); + if (Math.random() > 0.5) { + ch = 'x' + ch.toString(16); + } + out += '&#' + ch + ';'; + } + + return out; +}; + +/** + * Renderer + */ + +function Renderer(options) { + this.options = options || marked.defaults; +} + +Renderer.prototype.code = function(code, infostring, escaped) { + var lang = (infostring || '').match(/\S*/)[0]; + if (this.options.highlight) { + var out = this.options.highlight(code, lang); + if (out != null && out !== code) { + escaped = true; + code = out; + } + } + + if (!lang) { + return '<pre><code>' + + (escaped ? code : escape(code, true)) + + '</code></pre>'; + } + + return '<pre><code class="' + + this.options.langPrefix + + escape(lang, true) + + '">' + + (escaped ? code : escape(code, true)) + + '</code></pre>\n'; +}; + +Renderer.prototype.blockquote = function(quote) { + return '<blockquote>\n' + quote + '</blockquote>\n'; +}; + +Renderer.prototype.html = function(html) { + return html; +}; + +Renderer.prototype.heading = function(text, level, raw, slugger) { + if (this.options.headerIds) { + return '<h' + + level + + ' id="' + + this.options.headerPrefix + + slugger.slug(raw) + + '">' + + text + + '</h' + + level + + '>\n'; + } + // ignore IDs + return '<h' + level + '>' + text + '</h' + level + '>\n'; +}; + +Renderer.prototype.hr = function() { + return this.options.xhtml ? '<hr/>\n' : '<hr>\n'; +}; + +Renderer.prototype.list = function(body, ordered, start) { + var type = ordered ? 'ol' : 'ul', + startatt = (ordered && start !== 1) ? (' start="' + start + '"') : ''; + return '<' + type + startatt + '>\n' + body + '</' + type + '>\n'; +}; + +Renderer.prototype.listitem = function(text) { + return '<li>' + text + '</li>\n'; +}; + +Renderer.prototype.checkbox = function(checked) { + return '<input ' + + (checked ? 'checked="" ' : '') + + 'disabled="" type="checkbox"' + + (this.options.xhtml ? ' /' : '') + + '> '; +}; + +Renderer.prototype.paragraph = function(text) { + return '<p>' + text + '</p>\n'; +}; + +Renderer.prototype.table = function(header, body) { + if (body) body = '<tbody>' + body + '</tbody>'; + + return '<table>\n' + + '<thead>\n' + + header + + '</thead>\n' + + body + + '</table>\n'; +}; + +Renderer.prototype.tablerow = function(content) { + return '<tr>\n' + content + '</tr>\n'; +}; + +Renderer.prototype.tablecell = function(content, flags) { + var type = flags.header ? 'th' : 'td'; + var tag = flags.align + ? '<' + type + ' align="' + flags.align + '">' + : '<' + type + '>'; + return tag + content + '</' + type + '>\n'; +}; + +// span level renderer +Renderer.prototype.strong = function(text) { + return '<strong>' + text + '</strong>'; +}; + +Renderer.prototype.em = function(text) { + return '<em>' + text + '</em>'; +}; + +Renderer.prototype.codespan = function(text) { + return '<code>' + text + '</code>'; +}; + +Renderer.prototype.br = function() { + return this.options.xhtml ? '<br/>' : '<br>'; +}; + +Renderer.prototype.del = function(text) { + return '<del>' + text + '</del>'; +}; + +Renderer.prototype.link = function(href, title, text) { + href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); + if (href === null) { + return text; + } + var out = '<a href="' + escape(href) + '"'; + if (title) { + out += ' title="' + title + '"'; + } + out += '>' + text + '</a>'; + return out; +}; + +Renderer.prototype.image = function(href, title, text) { + href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); + if (href === null) { + return text; + } + + var out = '<img src="' + href + '" alt="' + text + '"'; + if (title) { + out += ' title="' + title + '"'; + } + out += this.options.xhtml ? '/>' : '>'; + return out; +}; + +Renderer.prototype.text = function(text) { + return text; +}; + +/** + * TextRenderer + * returns only the textual part of the token + */ + +function TextRenderer() {} + +// no need for block level renderers + +TextRenderer.prototype.strong = +TextRenderer.prototype.em = +TextRenderer.prototype.codespan = +TextRenderer.prototype.del = +TextRenderer.prototype.text = function (text) { + return text; +}; + +TextRenderer.prototype.link = +TextRenderer.prototype.image = function(href, title, text) { + return '' + text; +}; + +TextRenderer.prototype.br = function() { + return ''; +}; + +/** + * Parsing & Compiling + */ + +function Parser(options) { + this.tokens = []; + this.token = null; + this.options = options || marked.defaults; + this.options.renderer = this.options.renderer || new Renderer(); + this.renderer = this.options.renderer; + this.renderer.options = this.options; + this.slugger = new Slugger(); +} + +/** + * Static Parse Method + */ + +Parser.parse = function(src, options) { + var parser = new Parser(options); + return parser.parse(src); +}; + +/** + * Parse Loop + */ + +Parser.prototype.parse = function(src) { + this.inline = new InlineLexer(src.links, this.options); + // use an InlineLexer with a TextRenderer to extract pure text + this.inlineText = new InlineLexer( + src.links, + merge({}, this.options, {renderer: new TextRenderer()}) + ); + this.tokens = src.reverse(); + + var out = ''; + while (this.next()) { + out += this.tok(); + } + + return out; +}; + +/** + * Next Token + */ + +Parser.prototype.next = function() { + return this.token = this.tokens.pop(); +}; + +/** + * Preview Next Token + */ + +Parser.prototype.peek = function() { + return this.tokens[this.tokens.length - 1] || 0; +}; + +/** + * Parse Text Tokens + */ + +Parser.prototype.parseText = function() { + var body = this.token.text; + + while (this.peek().type === 'text') { + body += '\n' + this.next().text; + } + + return this.inline.output(body); +}; + +/** + * Parse Current Token + */ + +Parser.prototype.tok = function() { + switch (this.token.type) { + case 'space': { + return ''; + } + case 'hr': { + return this.renderer.hr(); + } + case 'heading': { + return this.renderer.heading( + this.inline.output(this.token.text), + this.token.depth, + unescape(this.inlineText.output(this.token.text)), + this.slugger); + } + case 'code': { + return this.renderer.code(this.token.text, + this.token.lang, + this.token.escaped); + } + case 'table': { + var header = '', + body = '', + i, + row, + cell, + j; + + // header + cell = ''; + for (i = 0; i < this.token.header.length; i++) { + cell += this.renderer.tablecell( + this.inline.output(this.token.header[i]), + { header: true, align: this.token.align[i] } + ); + } + header += this.renderer.tablerow(cell); + + for (i = 0; i < this.token.cells.length; i++) { + row = this.token.cells[i]; + + cell = ''; + for (j = 0; j < row.length; j++) { + cell += this.renderer.tablecell( + this.inline.output(row[j]), + { header: false, align: this.token.align[j] } + ); + } + + body += this.renderer.tablerow(cell); + } + return this.renderer.table(header, body); + } + case 'blockquote_start': { + body = ''; + + while (this.next().type !== 'blockquote_end') { + body += this.tok(); + } + + return this.renderer.blockquote(body); + } + case 'list_start': { + body = ''; + var ordered = this.token.ordered, + start = this.token.start; + + while (this.next().type !== 'list_end') { + body += this.tok(); + } + + return this.renderer.list(body, ordered, start); + } + case 'list_item_start': { + body = ''; + var loose = this.token.loose; + + if (this.token.task) { + body += this.renderer.checkbox(this.token.checked); + } + + while (this.next().type !== 'list_item_end') { + body += !loose && this.token.type === 'text' + ? this.parseText() + : this.tok(); + } + + return this.renderer.listitem(body); + } + case 'html': { + // TODO parse inline content if parameter markdown=1 + return this.renderer.html(this.token.text); + } + case 'paragraph': { + return this.renderer.paragraph(this.inline.output(this.token.text)); + } + case 'text': { + return this.renderer.paragraph(this.parseText()); + } + default: { + var errMsg = 'Token with "' + this.token.type + '" type was not found.'; + if (this.options.silent) { + console.log(errMsg); + } else { + throw new Error(errMsg); + } + } + } +}; + +/** + * Slugger generates header id + */ + +function Slugger () { + this.seen = {}; +} + +/** + * Convert string to unique id + */ + +Slugger.prototype.slug = function (value) { + var slug = value + .toLowerCase() + .trim() + .replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g, '') + .replace(/\s/g, '-'); + + if (this.seen.hasOwnProperty(slug)) { + var originalSlug = slug; + do { + this.seen[originalSlug]++; + slug = originalSlug + '-' + this.seen[originalSlug]; + } while (this.seen.hasOwnProperty(slug)); + } + this.seen[slug] = 0; + + return slug; +}; + +/** + * Helpers + */ + +function escape(html, encode) { + if (encode) { + if (escape.escapeTest.test(html)) { + return html.replace(escape.escapeReplace, function (ch) { return escape.replacements[ch]; }); + } + } else { + if (escape.escapeTestNoEncode.test(html)) { + return html.replace(escape.escapeReplaceNoEncode, function (ch) { return escape.replacements[ch]; }); + } + } + + return html; +} + +escape.escapeTest = /[&<>"']/; +escape.escapeReplace = /[&<>"']/g; +escape.replacements = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' +}; + +escape.escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/; +escape.escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g; + +function unescape(html) { + // explicitly match decimal, hex, and named HTML entities + return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, function(_, n) { + n = n.toLowerCase(); + if (n === 'colon') return ':'; + if (n.charAt(0) === '#') { + return n.charAt(1) === 'x' + ? String.fromCharCode(parseInt(n.substring(2), 16)) + : String.fromCharCode(+n.substring(1)); + } + return ''; + }); +} + +function edit(regex, opt) { + regex = regex.source || regex; + opt = opt || ''; + return { + replace: function(name, val) { + val = val.source || val; + val = val.replace(/(^|[^\[])\^/g, '$1'); + regex = regex.replace(name, val); + return this; + }, + getRegex: function() { + return new RegExp(regex, opt); + } + }; +} + +function cleanUrl(sanitize, base, href) { + if (sanitize) { + try { + var prot = decodeURIComponent(unescape(href)) + .replace(/[^\w:]/g, '') + .toLowerCase(); + } catch (e) { + return null; + } + if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { + return null; + } + } + if (base && !originIndependentUrl.test(href)) { + href = resolveUrl(base, href); + } + try { + href = encodeURI(href).replace(/%25/g, '%'); + } catch (e) { + return null; + } + return href; +} + +function resolveUrl(base, href) { + if (!baseUrls[' ' + base]) { + // we can ignore everything in base after the last slash of its path component, + // but we might need to add _that_ + // https://tools.ietf.org/html/rfc3986#section-3 + if (/^[^:]+:\/*[^/]*$/.test(base)) { + baseUrls[' ' + base] = base + '/'; + } else { + baseUrls[' ' + base] = rtrim(base, '/', true); + } + } + base = baseUrls[' ' + base]; + + if (href.slice(0, 2) === '//') { + return base.replace(/:[\s\S]*/, ':') + href; + } else if (href.charAt(0) === '/') { + return base.replace(/(:\/*[^/]*)[\s\S]*/, '$1') + href; + } else { + return base + href; + } +} +var baseUrls = {}; +var originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i; + +function noop() {} +noop.exec = noop; + +function merge(obj) { + var i = 1, + target, + key; + + for (; i < arguments.length; i++) { + target = arguments[i]; + for (key in target) { + if (Object.prototype.hasOwnProperty.call(target, key)) { + obj[key] = target[key]; + } + } + } + + return obj; +} + +function splitCells(tableRow, count) { + // ensure that every cell-delimiting pipe has a space + // before it to distinguish it from an escaped pipe + var row = tableRow.replace(/\|/g, function (match, offset, str) { + var escaped = false, + curr = offset; + while (--curr >= 0 && str[curr] === '\\') escaped = !escaped; + if (escaped) { + // odd number of slashes means | is escaped + // so we leave it alone + return '|'; + } else { + // add space before unescaped | + return ' |'; + } + }), + cells = row.split(/ \|/), + i = 0; + + if (cells.length > count) { + cells.splice(count); + } else { + while (cells.length < count) cells.push(''); + } + + for (; i < cells.length; i++) { + // leading or trailing whitespace is ignored per the gfm spec + cells[i] = cells[i].trim().replace(/\\\|/g, '|'); + } + return cells; +} + +// Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). +// /c*$/ is vulnerable to REDOS. +// invert: Remove suffix of non-c chars instead. Default falsey. +function rtrim(str, c, invert) { + if (str.length === 0) { + return ''; + } + + // Length of suffix matching the invert condition. + var suffLen = 0; + + // Step left until we fail to match the invert condition. + while (suffLen < str.length) { + var currChar = str.charAt(str.length - suffLen - 1); + if (currChar === c && !invert) { + suffLen++; + } else if (currChar !== c && invert) { + suffLen++; + } else { + break; + } + } + + return str.substr(0, str.length - suffLen); +} + +function findClosingBracket(str, b) { + if (str.indexOf(b[1]) === -1) { + return -1; + } + var level = 0; + for (var i = 0; i < str.length; i++) { + if (str[i] === '\\') { + i++; + } else if (str[i] === b[0]) { + level++; + } else if (str[i] === b[1]) { + level--; + if (level < 0) { + return i; + } + } + } + return -1; +} + +/** + * Marked + */ + +function marked(src, opt, callback) { + // throw error in case of non string input + if (typeof src === 'undefined' || src === null) { + throw new Error('marked(): input parameter is undefined or null'); + } + if (typeof src !== 'string') { + throw new Error('marked(): input parameter is of type ' + + Object.prototype.toString.call(src) + ', string expected'); + } + + if (callback || typeof opt === 'function') { + if (!callback) { + callback = opt; + opt = null; + } + + opt = merge({}, marked.defaults, opt || {}); + + var highlight = opt.highlight, + tokens, + pending, + i = 0; + + try { + tokens = Lexer.lex(src, opt); + } catch (e) { + return callback(e); + } + + pending = tokens.length; + + var done = function(err) { + if (err) { + opt.highlight = highlight; + return callback(err); + } + + var out; + + try { + out = Parser.parse(tokens, opt); + } catch (e) { + err = e; + } + + opt.highlight = highlight; + + return err + ? callback(err) + : callback(null, out); + }; + + if (!highlight || highlight.length < 3) { + return done(); + } + + delete opt.highlight; + + if (!pending) return done(); + + for (; i < tokens.length; i++) { + (function(token) { + if (token.type !== 'code') { + return --pending || done(); + } + return highlight(token.text, token.lang, function(err, code) { + if (err) return done(err); + if (code == null || code === token.text) { + return --pending || done(); + } + token.text = code; + token.escaped = true; + --pending || done(); + }); + })(tokens[i]); + } + + return; + } + try { + if (opt) opt = merge({}, marked.defaults, opt); + return Parser.parse(Lexer.lex(src, opt), opt); + } catch (e) { + e.message += '\nPlease report this to https://github.com/markedjs/marked.'; + if ((opt || marked.defaults).silent) { + return '<p>An error occurred:</p><pre>' + + escape(e.message + '', true) + + '</pre>'; + } + throw e; + } +} + +/** + * Options + */ + +marked.options = +marked.setOptions = function(opt) { + merge(marked.defaults, opt); + return marked; +}; + +marked.getDefaults = function () { + return { + baseUrl: null, + breaks: false, + gfm: true, + headerIds: true, + headerPrefix: '', + highlight: null, + langPrefix: 'language-', + mangle: true, + pedantic: false, + renderer: new Renderer(), + sanitize: false, + sanitizer: null, + silent: false, + smartLists: false, + smartypants: false, + tables: true, + xhtml: false + }; +}; + +marked.defaults = marked.getDefaults(); + +/** + * Expose + */ + +marked.Parser = Parser; +marked.parser = Parser.parse; + +marked.Renderer = Renderer; +marked.TextRenderer = TextRenderer; + +marked.Lexer = Lexer; +marked.lexer = Lexer.lex; + +marked.InlineLexer = InlineLexer; +marked.inlineLexer = InlineLexer.output; + +marked.Slugger = Slugger; + +marked.parse = marked; + +if (typeof module !== 'undefined' && typeof exports === 'object') { + module.exports = marked; +} else if (typeof define === 'function' && define.amd) { + define(function() { return marked; }); +} else { + root.marked = marked; +} +})(this || (typeof window !== 'undefined' ? window : global)); \ No newline at end of file diff --git a/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/marked.js.xml b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/marked.js.xml new file mode 100644 index 0000000000..a2f85b0e54 --- /dev/null +++ b/bt5/erp5_notebook/SkinTemplateItem/portal_skins/erp5_notebook/marked.js.xml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="File" module="OFS.Image"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>__name__</string> </key> + <value> <string>marked.js</string> </value> + </item> + <item> + <key> <string>content_type</string> </key> + <value> <string>application/javascript</string> </value> + </item> + <item> + <key> <string>precondition</string> </key> + <value> <string></string> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string></string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/product/ERP5/bootstrap/erp5_core/SkinTemplateItem/portal_skins/erp5_core/gadget_editor.js.js b/product/ERP5/bootstrap/erp5_core/SkinTemplateItem/portal_skins/erp5_core/gadget_editor.js.js index 9cbf92faea..0b1598f9a5 100644 --- a/product/ERP5/bootstrap/erp5_core/SkinTemplateItem/portal_skins/erp5_core/gadget_editor.js.js +++ b/product/ERP5/bootstrap/erp5_core/SkinTemplateItem/portal_skins/erp5_core/gadget_editor.js.js @@ -13,7 +13,8 @@ lockGadgetInQueue, unlockGadgetInQueue, unlockGadgetInFailedQueue*/ "minipaint": {"url": "minipaint.gadget.html"}, "jquery-sheets": {"url": "jquery-sheets.gadget.html"}, "pdf": {"url": "pdf_js/pdfjs.gadget.html"}, - "notebook_editor": {"url": "gadget_notebook.html"} + "notebook_editor": {"url": "gadget_notebook.html"}, + "jsmd_editor": {"url": "gadget_jsmd_viewer.html"} }; @@ -73,15 +74,16 @@ lockGadgetInQueue, unlockGadgetInQueue, unlockGadgetInFailedQueue*/ if ((modification_dict.hasOwnProperty('editable')) || (modification_dict.hasOwnProperty('editor')) || - (gadget.state.editor == 'notebook_editor')) { + (gadget.state.editor === 'notebook_editor')) { // Clear first to DOM, append after to reduce flickering/manip while (element.firstChild) { element.removeChild(element.firstChild); } if (modification_dict.hasOwnProperty('maximize') || - (gadget.state.editor == 'notebook_editor')) { + (gadget.state.editor === 'notebook_editor')) { // for fck_editor fields, we want to be able to maximize also in non editable if ((gadget.state.maximize && gadget.state.editable) || + (gadget.state.maximize && gadget.state.editor === 'jsmd_editor') || (gadget.state.maximize && gadget.state.editor === 'fck_editor')) { element.appendChild(div_max); queue @@ -108,11 +110,16 @@ lockGadgetInQueue, unlockGadgetInQueue, unlockGadgetInFailedQueue*/ if ((gadget.state.editable && (editor_dict.hasOwnProperty(gadget.state.editor))) || (!gadget.state.editable && gadget.state.editor === 'fck_editor') || + (!gadget.state.editable && gadget.state.editor === 'jsmd_editor') || (gadget.state.editor === 'pdf')) { queue .push(function () { + var url = editor_dict[gadget.state.editor].url; + if (gadget.state.editable && (gadget.state.editor === 'jsmd_editor')) { + url = editor_dict.codemirror.url; + } return gadget.declareGadget( - editor_dict[gadget.state.editor].url, + url, { scope: 'editor', sandbox: 'iframe', @@ -134,6 +141,7 @@ lockGadgetInQueue, unlockGadgetInQueue, unlockGadgetInFailedQueue*/ if ((gadget.state.editable && (editor_dict.hasOwnProperty(gadget.state.editor))) || (!gadget.state.editable && gadget.state.editor === 'fck_editor') || + (!gadget.state.editable && gadget.state.editor === 'jsmd_editor') || (gadget.state.editor === 'pdf')) { queue .push(function () { -- 2.30.9