discourse/app/assets/javascripts/pretty-text/engines/discourse-markdown/code.js

117 lines
3.2 KiB
JavaScript

import { HLJS_ALIASES } from "pretty-text/highlightjs-aliases";
// we need a custom renderer for code blocks cause we have a slightly non compliant
// format with special handling for text and so on
const TEXT_CODE_CLASSES = ["text", "pre", "plain"];
function extractTokenInfo(info, md) {
if (!info) {
return;
}
info = info.trim();
const matches = info.match(/(^\s*\S*)\s*(.*)/i);
if (!matches) {
return;
}
// ensure the token has only valid chars
// c++, strucuted-text and p91, are all valid
if (!/^[\w+-]*$/i.test(matches[1])) {
return;
}
const ASCII_REGEX = /[^\x00-\x7F]/;
const tag = md.utils.unescapeAll(matches[1].replace(ASCII_REGEX, ""));
const extractedData = { tag, attributes: {} };
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.includes(tag)) {
className = "lang-plaintext";
} else if (acceptableCodeClasses.includes(tag)) {
className = `lang-${tag}`;
} else {
className = "lang-plaintext";
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`;
}
export function setup(helper) {
helper.registerOptions((opts, siteSettings) => {
let languageAliases = [];
const languages = (siteSettings.highlighted_languages || "")
.split("|")
.filter(Boolean);
languages.forEach((lang) => {
if (HLJS_ALIASES[lang]) {
languageAliases = languageAliases.concat(HLJS_ALIASES[lang]);
}
});
opts.defaultCodeLang = siteSettings.default_code_lang;
opts.acceptableCodeClasses = languages
.concat(languageAliases)
.concat(["auto", "plaintext"]);
});
helper.allowList(["pre[data-code-*]"]);
helper.allowList({
custom(tag, name, value) {
if (tag === "code" && name === "class") {
const m = /^lang\-(.+)$/.exec(value);
if (m) {
return helper.getOptions().acceptableCodeClasses.includes(m[1]);
}
}
},
});
helper.registerPlugin((md) => {
md.renderer.rules.fence = (tokens, idx, options, env, slf) =>
render(tokens, idx, options, env, slf, md);
});
}