2017-07-14 20:27:28 +08:00
|
|
|
// we need a custom renderer for code blocks cause we have a slightly non compliant
|
|
|
|
// format with special handling for text and so on
|
2016-06-15 02:31:51 +08:00
|
|
|
const TEXT_CODE_CLASSES = ["text", "pre", "plain"];
|
|
|
|
|
2022-02-09 18:23:44 +08:00
|
|
|
function extractTokenInfo(info, md) {
|
|
|
|
if (!info) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
info = info.trim();
|
|
|
|
|
|
|
|
const matches = info.match(/(^\s*\S*)\s*(.*)/i);
|
|
|
|
if (!matches) {
|
|
|
|
return;
|
2017-07-14 20:27:28 +08:00
|
|
|
}
|
|
|
|
|
2022-02-09 18:23:44 +08:00
|
|
|
// ensure the token has only valid chars
|
|
|
|
// c++, strucuted-text and p91, are all valid
|
|
|
|
if (!/^[\w+-]*$/i.test(matches[1])) {
|
|
|
|
return;
|
2017-07-14 20:27:28 +08:00
|
|
|
}
|
|
|
|
|
2022-02-09 18:23:44 +08:00
|
|
|
const ASCII_REGEX = /[^\x00-\x7F]/;
|
|
|
|
const tag = md.utils.unescapeAll(matches[1].replace(ASCII_REGEX, ""));
|
|
|
|
const extractedData = { tag, attributes: {} };
|
2017-07-14 20:27:28 +08:00
|
|
|
|
2022-02-09 18:23:44 +08:00
|
|
|
if (matches[2]?.length) {
|
|
|
|
md.utils
|
|
|
|
.unescapeAll(matches[2].replace(ASCII_REGEX, ""))
|
|
|
|
.split(",")
|
|
|
|
.forEach((potentialPair) => {
|
|
|
|
const [key, value] = potentialPair.trim().split(/\s+/g)[0].split("=");
|
|
|
|
|
|
|
|
// invalid pairs would get caught here and not used, eg `foo=`
|
|
|
|
if (key && value) {
|
|
|
|
extractedData.attributes[key] = value;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return extractedData;
|
|
|
|
}
|
|
|
|
|
|
|
|
function render(tokens, idx, options, env, slf, md) {
|
|
|
|
const token = tokens[idx];
|
|
|
|
const escapedContent = md.utils.escapeHtml(token.content);
|
|
|
|
const tokenInfo = extractTokenInfo(token.info, md);
|
|
|
|
const tag = tokenInfo?.tag || md.options.discourse.defaultCodeLang;
|
|
|
|
const attributes = tokenInfo?.attributes || {};
|
|
|
|
|
|
|
|
let className;
|
|
|
|
|
|
|
|
const acceptableCodeClasses =
|
|
|
|
md.options.discourse.acceptableCodeClasses || [];
|
|
|
|
|
|
|
|
if (TEXT_CODE_CLASSES.indexOf(tag) > -1) {
|
|
|
|
className = "lang-nohighlight";
|
|
|
|
} else if (acceptableCodeClasses.indexOf(tag) > -1) {
|
|
|
|
className = `lang-${tag}`;
|
|
|
|
} else {
|
|
|
|
className = "lang-nohighlight";
|
|
|
|
attributes["wrap"] = tag;
|
|
|
|
}
|
|
|
|
|
|
|
|
const dataAttributes = Object.keys(attributes)
|
|
|
|
.map((key) => {
|
|
|
|
const value = md.utils.escapeHtml(attributes[key]);
|
|
|
|
key = md.utils.escapeHtml(key);
|
|
|
|
return `data-code-${key}="${value}"`;
|
|
|
|
})
|
|
|
|
.join(" ");
|
|
|
|
|
|
|
|
return `<pre${dataAttributes ? ` ${dataAttributes}` : ""}><code${
|
|
|
|
className ? ` class="${className}"` : ""
|
|
|
|
}>${escapedContent}</code></pre>\n`;
|
2017-07-14 20:27:28 +08:00
|
|
|
}
|
2017-07-13 06:10:51 +08:00
|
|
|
|
2016-06-15 02:31:51 +08:00
|
|
|
export function setup(helper) {
|
2017-07-14 20:27:28 +08:00
|
|
|
helper.registerOptions((opts, siteSettings) => {
|
|
|
|
opts.defaultCodeLang = siteSettings.default_code_lang;
|
|
|
|
opts.acceptableCodeClasses = (siteSettings.highlighted_languages || "")
|
|
|
|
.split("|")
|
2020-09-29 20:34:46 +08:00
|
|
|
.filter(Boolean)
|
2017-07-14 20:27:28 +08:00
|
|
|
.concat(["auto", "nohighlight"]);
|
|
|
|
});
|
2017-06-09 06:02:30 +08:00
|
|
|
|
2022-02-09 18:23:44 +08:00
|
|
|
helper.allowList(["pre[data-code-*]"]);
|
|
|
|
|
2020-10-28 10:22:06 +08:00
|
|
|
helper.allowList({
|
2016-06-15 02:31:51 +08:00
|
|
|
custom(tag, name, value) {
|
|
|
|
if (tag === "code" && name === "class") {
|
|
|
|
const m = /^lang\-(.+)$/.exec(value);
|
|
|
|
if (m) {
|
|
|
|
return helper.getOptions().acceptableCodeClasses.indexOf(m[1]) !== -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2017-07-14 20:27:28 +08:00
|
|
|
helper.registerPlugin((md) => {
|
|
|
|
md.renderer.rules.fence = (tokens, idx, options, env, slf) =>
|
|
|
|
render(tokens, idx, options, env, slf, md);
|
2016-06-15 02:31:51 +08:00
|
|
|
});
|
|
|
|
}
|