zulip/static/js/fenced_code.js

189 lines
5.8 KiB
JavaScript

// 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
")$";
const fence_re = new RegExp(fencestr);
// Default stashing function does nothing
let stash_func = function (text) {
return text;
};
let escape_func = function (text) {
return text;
};
function wrap_code(code) {
// Trim trailing \n until there's just one left
// This mirrors how pygments handles code input
code += '\n';
while (code.length > 2 && code.substr(code.length - 2) === '\n\n') {
code = code.substring(0, code.length - 1);
}
return '<div class="codehilite"><pre><span></span>' + escape_func(code) + '</pre></div>\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
_.each(paragraphs, function (paragraph) {
const lines = paragraph.split('\n');
quoted_paragraphs.push(_.map(
_.reject(lines, function (line) { return line === ''; }),
function (line) { return '> ' + line; }).join('\n'));
});
return quoted_paragraphs.join('\n\n');
}
function wrap_tex(tex) {
try {
return katex.renderToString(tex, {
displayMode: true,
});
} catch (ex) {
return '<span class="tex-error">' + escape_func(tex) + '</span>';
}
}
exports.set_stash_func = function (stash_handler) {
stash_func = stash_handler;
};
exports.set_escape_func = function (escape) {
escape_func = escape;
};
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) {
// 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' || lang === 'tex' || lang === 'latex') {
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();
},
};
}
return {
handle_line: function (line) {
if (line === fence) {
this.done();
} else {
lines.push(util.rtrim(line));
}
},
done: function () {
const text = 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 handler = handler_for_fence(output_lines, fence, lang);
handler_stack.push(handler);
} else {
output_lines.push(line);
}
};
const current_handler = default_hander();
handler_stack.push(current_handler);
_.each(input, function (line) {
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');
};
window.fenced_code = exports;