// Parsing routine that can be dropped in to message parsing // and formats code blocks // // This supports arbitrarily nested code blocks as well as // auto-completing code blocks missing a trailing close. // See backend fenced_code.py:71 for associated regexp const fencestr = "^(~{3,}|`{3,})" + // Opening Fence "[ ]*" + // Spaces "(" + "\\{?\\.?" + "([a-zA-Z0-9_+-./#]*)" + // Language "\\}?" + ")" + "[ ]*" + // Spaces "(" + "\\{?\\.?" + "([^~`]*)" + // Header (see fenced_code.py) "\\}?" + ")" + "$"; const fence_re = new RegExp(fencestr); // Default stashing function does nothing let stash_func = function (text) { return text; }; exports.wrap_code = function (code) { // Trim trailing \n until there's just one left // This mirrors how pygments handles code input return '
' +
        _.escape(code.replace(/^\n+|\n+$/g, '')) +
        '\n
\n'; }; function wrap_quote(text) { const paragraphs = text.split('\n\n'); const quoted_paragraphs = []; // Prefix each quoted paragraph with > at the // beginning of each line for (const paragraph of paragraphs) { const lines = paragraph.split('\n'); quoted_paragraphs.push(lines.filter((line) => line !== '').map((line) => '> ' + line).join('\n')); } return quoted_paragraphs.join('\n\n'); } function wrap_tex(tex) { try { return katex.renderToString(tex, { displayMode: true, }); } catch (ex) { return '' + _.escape(tex) + ''; } } function wrap_spoiler(header, text, stash_func) { const output = []; const header_div_open_html = '
'; const end_header_start_content_html = '
'; output.push(stash_func(header_div_open_html)); output.push(header); output.push(stash_func(end_header_start_content_html)); output.push(text); output.push(stash_func(footer_html)); return output.join("\n\n"); } exports.set_stash_func = function (stash_handler) { stash_func = stash_handler; }; exports.process_fenced_code = function (content) { const input = content.split('\n'); const output = []; const handler_stack = []; let consume_line; function handler_for_fence(output_lines, fence, lang, header) { // lang is ignored except for 'quote', as we // don't do syntax highlighting yet return (function () { const lines = []; if (lang === 'quote') { return { handle_line: function (line) { if (line === fence) { this.done(); } else { consume_line(lines, line); } }, done: function () { const text = wrap_quote(lines.join('\n')); output_lines.push(''); output_lines.push(text); output_lines.push(''); handler_stack.pop(); }, }; } if (lang === 'math') { return { handle_line: function (line) { if (line === fence) { this.done(); } else { lines.push(line); } }, done: function () { const text = wrap_tex(lines.join('\n')); const placeholder = stash_func(text, true); output_lines.push(''); output_lines.push(placeholder); output_lines.push(''); handler_stack.pop(); }, }; } if (lang === 'spoiler') { return { handle_line: function (line) { if (line === fence) { this.done(); } else { lines.push(line); } }, done: function () { const text = wrap_spoiler(header, lines.join('\n'), stash_func); output_lines.push(''); output_lines.push(text); output_lines.push(''); handler_stack.pop(); }, }; } return { handle_line: function (line) { if (line === fence) { this.done(); } else { lines.push(line.trimRight()); } }, done: function () { const text = exports.wrap_code(lines.join('\n')); // insert safe HTML that is passed through the parsing const placeholder = stash_func(text, true); output_lines.push(''); output_lines.push(placeholder); output_lines.push(''); handler_stack.pop(); }, }; }()); } function default_hander() { return { handle_line: function (line) { consume_line(output, line); }, done: function () { handler_stack.pop(); }, }; } consume_line = function consume_line(output_lines, line) { const match = fence_re.exec(line); if (match) { const fence = match[1]; const lang = match[3]; const header = match[5]; const handler = handler_for_fence(output_lines, fence, lang, header); handler_stack.push(handler); } else { output_lines.push(line); } }; const current_handler = default_hander(); handler_stack.push(current_handler); for (const line of input) { const handler = handler_stack[handler_stack.length - 1]; handler.handle_line(line); } // Clean up all trailing blocks by letting them // insert closing fences while (handler_stack.length !== 0) { const handler = handler_stack[handler_stack.length - 1]; handler.done(); } if (output.length > 2 && output[output.length - 2] !== '') { output.push(''); } return output.join('\n'); }; const fence_length_re = /^ {0,3}(`{3,})/gm; exports.get_unused_fence = (content) => { // we only return ``` fences, not ~~~. let length = 3; let match; fence_length_re.lastIndex = 0; while ((match = fence_length_re.exec(content)) !== null) { length = Math.max(length, match[1].length + 1); } return '`'.repeat(length); }; window.fenced_code = exports;