import { marked } from 'marked'; import { highlight } from './prism'; import katex = require('katex'); import type { KatexOptions } from 'katex'; import render_bytefield = require('bytefield-svg'); import { renderSvg as render_nomnoml } from 'nomnoml'; import { pikchr } from 'pikchr'; import { parse as parse_yaml } from 'yaml'; import { icons } from './icons'; import { strip_svg } from './svg'; import { generate_mecard_qr_code, generate_qr_code } from './qrcode'; import { bind_data_async } from './async-steps'; import { render_vega_spec } from './vega'; import { parse_attributes } from './attrs'; import { MarkdownOptions } from './render'; export function create_renderer(opts: MarkdownOptions) { const renderer = new marked.Renderer(); renderer.heading = heading(renderer, opts); renderer.code = code(renderer, opts); // ... return renderer; } function heading(renderer: marked.Renderer, opts: MarkdownOptions) { return function(orig_text: string, level: 1 | 2 | 3 | 4 | 5 | 6, raw: string, slugger) { let { text, id, html_attrs } = parse_attributes(raw); if (! id) { id = slugger.slug(text); html_attrs.push(`id="${id}"`); } return ` ${text} ${icons.link} Section titled ${text} `; }; } function code(renderer: marked.Renderer, opts: MarkdownOptions) { return function(code: string, infostring: string, is_escaped: boolean) { const args = parse_code_args(infostring); if (! args || ! args[0]) { return `
${escape(code, is_escaped)}
`; } let caption = ''; const flags = new Set(); for (let i = 1; i < args.length; i++) { if (args[i][0] === ':') { flags.add(args[i]); } else { caption = `
${marked.parseInline(args[i], renderer.options)}
`; break; } } const size = flags.has(':small') ? 'small' : flags.has(':medium') ? 'medium' : flags.has(':large') ? 'large' : flags.has(':full') ? 'full' : 'medium'; const figure = (content: string) => `
${content}${caption}
`; if (args[0].startsWith('http:')) { return render_http_with_content(code, args[0].slice(5)); } switch (args[0]) { case 'samp': return figure(`
${escape(code, is_escaped)}
`); case 'bash:samp': { // Find the first newline that is not preceeded by a "\" const end_of_input = /(?${render_prism(code, 'bash')}`); } const input = code.slice(0, end_of_input.index); const rendered_input = render_prism(input, 'bash'); const output = code.slice(end_of_input.index + 1); const rendered_output = `${escape(output, is_escaped)}`; return figure(`
${rendered_input}\n${rendered_output}
`); }; case 'katex': { const katex_opts: KatexOptions = { displayMode: true, // true == "block" output: 'html', macros: opts.katex_macros, }; return figure( (katex as any).renderToString(code, katex_opts) ); }; case 'nomnoml': { const svg = render_nomnoml(code); return figure(post_process_nomnoml_svg(svg)); }; case 'clojure:bytefield': { const svg = render_bytefield(code); return figure(post_process_bytefield_svg(svg)); }; case 'pikchr': { const svg = pikchr(code); return figure(post_process_pikchr_svg(svg)); }; case 'qrcode': { const promise = generate_qr_code(code); const async_binding = bind_data_async(promise); return figure(async_binding); }; case 'yaml:mecard': { const parsed = parse_yaml(code); const promise = generate_mecard_qr_code(parsed); const async_binding = bind_data_async(promise); return figure(async_binding); }; case 'json:vega': { const spec = JSON.parse(code); const promise = render_vega_spec(spec); const binding = bind_data_async(promise); return figure(binding); }; case 'yaml:vega': { const spec = parse_yaml(code); const promise = render_vega_spec(spec); const binding = bind_data_async(promise); return figure(binding); }; default: return figure(`
${render_prism(code, args[0])}
`); } function render_http_with_content(code: string, lang: string) { // Find the first double newline const end_of_header = /(?:\r\n|\r|\n)(?:\r\n|\r|\n)/.exec(code); // If there is no such newline, the whole content is HTTP header if (! end_of_header) { return figure(`
${render_prism(code, 'http')}
`); } const header = code.slice(0, end_of_header.index); const rendered_header = render_prism(header, 'http', true); const content = code.slice(end_of_header.index + 1); const rendered_content = render_prism(content, lang, true); return figure(`
${rendered_header}\n${rendered_content}
`); } function render_prism(code: string, lang: string, include_class = false) { const out = highlight(code, lang); if (out != null && out !== code) { is_escaped = true; code = out; } const classname = include_class ? `class="language-${lang}"` : ''; return `${escape(code, is_escaped)}`; } }; } const arg_pattern = /^(?:[a-zA-Z0-9_:-]+|"(?:[^"\n]|(?<=\\)")*")/; function parse_code_args(text: string) { const args: string[] = [ ]; text = text.trim(); while (text.length) { const match = arg_pattern.exec(text); if (! match) { break; } if (match[0][0] === '"') { args.push(match[0].slice(1, -1)); } else { args.push(match[0]); } text = text.slice(match[0].length).trimStart(); } return args; } function escape(str: string, is_escaped: boolean) { return is_escaped ? str : str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } const svg_text = /\s*\s*<\/path>\s*<\/g>/gi; const svg_nomnoml_unfilled_arrow_head = //gi; // todo: css variables function post_process_nomnoml_svg(svg: string, size?: string) { svg = strip_svg(svg); // nomnoml uses some specific built-in styles for things, which we will be replacing // with variables (referencing the color themes) that fall back to safe defaults for // rendering the svg in a context that has css (like an RSS feed or other embedded // use case) // default text font svg = svg.replace(svg_font_family_helvetica, 'font-family="var(--theme-open-sans, helvetica)"'); // root-level boxes background svg = svg.replace(svg_fill_eee8d5, 'fill="var(--theme-bg-light, transparent)"'); // outlines and relationship lines svg = svg.replace(svg_stroke_33322e, 'stroke="var(--theme-line, currentcolor)"'); // arrow heads svg = svg.replace(svg_nomnoml_filled_arrow_head, ($0, $1) => ``); svg = svg.replace(svg_nomnoml_unfilled_arrow_head, ($0, $1) => ``); // text color svg = svg.replace(svg_fill_33322e, 'fill="var(--theme-text-body, currentcolor)"'); // nested boxes background svg = svg.replace(svg_fill_fdf6e3, 'fill="var(--theme-bg-heavy, transparent)"'); return svg; } const svg_text_fill_rgb_000 = /\b