in pxtlib/docsrender.ts [465:714]
export function renderMarkdown(opts: RenderOptions): string {
let hasPubInfo = true
if (!opts.pubinfo) {
hasPubInfo = false
opts.pubinfo = {}
}
let pubinfo = opts.pubinfo
if (!opts.theme) opts.theme = {}
delete opts.pubinfo["private"] // just in case
if (pubinfo["time"]) {
let tm = parseInt(pubinfo["time"])
if (!pubinfo["timems"])
pubinfo["timems"] = 1000 * tm + ""
if (!pubinfo["humantime"])
pubinfo["humantime"] = U.isoTime(tm)
}
if (pubinfo["name"]) {
pubinfo["dirname"] = pubinfo["name"].replace(/[^A-Za-z0-9_]/g, "-")
pubinfo["title"] = pubinfo["name"]
}
if (hasPubInfo) {
pubinfo["JSON"] = JSON.stringify(pubinfo, null, 4).replace(/</g, "\\u003c")
}
let template = opts.template
template = template
.replace(/<!--\s*@include\s+(\S+)\s*-->/g,
(full, fn) => {
let cont = (opts.theme.htmlDocIncludes || {})[fn] || ""
return "<!-- include " + fn + " -->\n" + cont + "\n<!-- end include -->\n"
})
template = renderConditionalMacros(template, pubinfo);
if (opts.locale)
template = translate(template, opts.locale).text
let d: RenderData = {
html: template,
theme: opts.theme,
filepath: opts.filepath,
versionPath: opts.versionPath,
ghEditURLs: opts.ghEditURLs,
params: pubinfo,
TOC: opts.TOC
}
prepTemplate(d)
if (!markedInstance) {
markedInstance = requireMarked();
}
// We have to re-create the renderer every time to avoid the link() function's closure capturing the opts
let renderer = new markedInstance.Renderer()
setupRenderer(renderer);
const linkRenderer = renderer.link;
renderer.link = function (href: string, title: string, text: string) {
const relative = new RegExp('^[/#]').test(href);
const target = !relative ? '_blank' : '';
if (relative && d.versionPath) href = `/${d.versionPath}${href}`;
const html = linkRenderer.call(renderer, href, title, text);
return html.replace(/^<a /, `<a ${target ? `target="${target}"` : ''} rel="nofollow noopener" `);
};
let sanitizer = requireDOMSanitizer();
markedInstance.setOptions({
renderer: renderer,
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: true,
sanitizer: sanitizer,
smartLists: true,
smartypants: true
});
let markdown = opts.markdown
// append repo info if any
if (opts.repo)
markdown += `
\`\`\`package
${opts.repo.name.replace(/^pxt-/, '')}=github:${opts.repo.fullName}#${opts.repo.tag || "master"}
\`\`\`
`;
//Uses the CmdLink definitions to replace links to YouTube and Vimeo (limited at the moment)
markdown = markdown.replace(/^\s*https?:\/\/(\S+)\s*$/mg, (f, lnk) => {
for (let ent of links) {
let m = ent.rx.exec(lnk)
if (m) {
return ent.cmd.replace(/\$(\d+)/g, (f, k) => {
return m[parseInt(k)] || ""
}) + "\n"
}
}
return f
})
// replace pre-template in markdown
markdown = markdown.replace(/@([a-z]+)@/ig, (m, param) => {
let macro = pubinfo[param];
if (!macro && opts.throwOnError)
U.userError(`unknown macro ${param}`);
return macro || 'unknown macro'
});
let html = markedInstance(markdown)
// support for breaks which somehow don't work out of the box
html = html.replace(/<br\s*\/>/ig, "<br/>");
// github will render images if referenced as 
// we require /static/foo.png
html = html.replace(/(<img [^>]* src=")\/docs\/static\/([^">]+)"/g,
(f, pref, addr) => pref + '/static/' + addr + '"')
let endBox = ""
let boxSize = 0;
function appendEndBox(size: number, box: string, html: string): string {
let r = html;
if (size <= boxSize) {
r = endBox + r;
endBox = "";
boxSize = 0;
}
return r;
}
html = html.replace(/<h(\d)[^>]+>\s*([~@])?\s*(.*?)<\/h\d>/g, (f, lvl, tp, body) => {
let m = /^(\w+)\s+(.*)/.exec(body)
let cmd = m ? m[1] : body
let args = m ? m[2] : ""
let rawArgs = args
args = html2Quote(args)
cmd = html2Quote(cmd)
lvl = parseInt(lvl);
if (!tp) {
return appendEndBox(lvl, endBox, f);
} else if (tp == "@") {
let expansion = U.lookup(d.settings, cmd)
if (expansion != null) {
pubinfo[cmd] = args
} else {
expansion = U.lookup(d.macros, cmd)
if (expansion == null) {
if (opts.throwOnError)
U.userError(`Unknown command: @${cmd}`);
return error(`Unknown command: @${cmd}`)
}
}
let ivars: Map<string> = {
ARGS: args,
CMD: cmd
}
return appendEndBox(lvl, endBox, injectHtml(expansion, ivars, ["ARGS", "CMD"]))
} else {
if (!cmd) {
let r = endBox
endBox = ""
return r
}
let box = U.lookup(d.boxes, cmd)
if (box) {
let parts = box.split("@BODY@")
let r = appendEndBox(lvl, endBox, parts[0].replace("@ARGS@", args));
endBox = parts[1];
let attrs = box.match(/data-[^>\s]+/ig);
if (attrs && attrs.indexOf('data-inferred') >= 0) {
boxSize = lvl;
}
return r;
} else {
if (opts.throwOnError)
U.userError(`Unknown box: ~ ${cmd}`);
return error(`Unknown box: ~ ${cmd}`)
}
}
})
if (endBox) html = html + endBox;
if (!pubinfo["title"]) {
let titleM = /<h1[^<>]*>([^<>]+)<\/h1>/.exec(html)
if (titleM)
pubinfo["title"] = html2Quote(titleM[1])
}
if (!pubinfo["description"]) {
let descM = /<p>([^]+?)<\/p>/.exec(html)
if (descM)
pubinfo["description"] = html2Quote(descM[1])
}
// try getting a better custom image for twitter
const imgM = /<div class="ui embed mdvid"[^<>]+?data-placeholder="([^"]+)"[^>]*\/?>/i.exec(html)
|| /<img class="ui [^"]*image" src="([^"]+)"[^>]*\/?>/i.exec(html);
if (imgM)
pubinfo["cardLogo"] = html2Quote(imgM[1]);
pubinfo["twitter"] = html2Quote(opts.theme.twitter || "@msmakecode");
let registers: Map<string> = {}
registers["main"] = "" // first
html = html.replace(/<!-- BEGIN-ASIDE (\S+) -->([^]*?)<!-- END-ASIDE -->/g, (f, nam, cont) => {
let s = U.lookup(registers, nam)
registers[nam] = (s || "") + cont
return "<!-- aside -->"
})
// fix up spourious newlines at the end of code blocks
html = html.replace(/\n<\/code>/g, "</code>")
registers["main"] = html
let injectBody = (tmpl: string, body: string) =>
injectHtml(d.boxes[tmpl] || "@BODY@", { BODY: body }, ["BODY"])
html = ""
for (let k of Object.keys(registers)) {
html += injectBody(k + "-container", registers[k])
}
pubinfo["body"] = html
// don't mangle target name in title, it is already in the sitename
pubinfo["name"] = pubinfo["title"] || ""
for (let k of Object.keys(opts.theme)) {
let v = (opts.theme as any)[k]
if (typeof v == "string")
pubinfo["theme_" + k] = v
}
return d.finish()
}