// 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 `${escapedContent}\n`; } export function setup(helper) { helper.registerOptions((opts, siteSettings) => { opts.defaultCodeLang = siteSettings.default_code_lang; opts.acceptableCodeClasses = (siteSettings.highlighted_languages || "") .split("|") .filter(Boolean) .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); }); }