import { Remarkable } from "remarkable"; import assert from "assert"; // Node is absolutely stupid. The remarkable project has an esm setting which // specifies the dist/esm directory, but it completely ignores it and complains. import { linkify } from "remarkable/dist/cjs/linkify.js"; import Prism from "../static/js/prism.cjs"; import _ from "lodash"; import slugify from "slugify"; import fs from "fs"; import PATH from "path"; import child_process from "child_process"; _.templateSettings.interpolate = /{{([\s\S]+?)}}/g; class Ork { constructor(base, metadata) { // base needs to go away, but for now it's where the root of the db is if(metadata.code) { this.code_path = `${base}${metadata.code}`; } else { this.code_path = false; } } js(file='code.js') { assert(this.code_path, "You are using ork but no code path set in metadata."); return `\`\`\`javascript\n${fs.readFileSync(`${this.code_path}/${file}`).toString().trim()}\n\`\`\`` } node(file='code.js', args='') { assert(this.code_path, "You are using ork but no code path set."); let cmd = `node "${file}" ${args}`; let result = child_process.execSync(cmd, {cwd: this.code_path}); return `\`\`\`shell-session\n$ ${cmd}\n${result.toString().trim()}\n\`\`\`` } } const count_lines = (str) => { let match = str.match(/\n(?!$)/g); return match ? match.length + 1 : 1; } const line_number_spans = (str) => { let count = count_lines(str); let spans = new Array(count + 1).join(''); return ``; } /** * Runs Prism on the string, defaulting to the "markup" * language which will encode most code correctly even * if you don't specify a language after the ``` **/ const run_prism = (str, lang="markup") => { assert(lang !== undefined, "Language can't be undefined."); // do the check again since the language might not exist if(Prism.languages[lang]) { return Prism.highlight(str, Prism.languages[lang], lang); } else { console.error("Unknown language", lang); return Prism.highlight(str, Prism.languages["markup"], "markup"); } } export const highlight = (str, lang) => { return run_prism(str, lang) + line_number_spans(str); } /* A separate renderer just for the titles that doesn't need anything else. */ export const title_render = new Remarkable('full').use(rem => { let pass = (tokens, idx) => ''; rem.renderer.rules.paragraph_open = pass; rem.renderer.rules.paragraph_close = pass; }); export const split = (raw_md) => { let [metadata, ...body] = raw_md.split('------'); metadata = JSON.parse(metadata); body = body.join('------'); return [metadata, body]; } const null_cb = (metadata, body) => body; export const create_renderer = (toc) => { const renderer = new Remarkable('full', { html: true, highlight }); renderer.use(linkify); renderer.use(rem => { rem.renderer.rules.heading_open = (tokens, idx) => { let level = tokens[idx].hLevel; let content = tokens[idx + 1].content; let slug = slugify(content, {lower: true, strict: true}); if(toc) toc.push({level, content, slug}); return ``; } rem.renderer.rules.heading_close = (tokens, idx) => { return `\n`; } }); renderer.use(rem => { const orig_open = rem.renderer.rules.link_open; const orig_close = rem.renderer.rules.link_close; rem.renderer.rules.link_open = (tokens, idx, options) => { return orig_open(tokens, idx, options); } rem.renderer.rules.link_close = (tokens, idx, options) => { return orig_close(tokens, idx, options); } }); return renderer; } export const render = (raw_md, cb=null_cb) => { let toc = []; let [metadata, body] = split(raw_md); const renderer = create_renderer(toc); let content = renderer.render(cb(metadata, body)); // now we can use the TOC to figure out a title metadata.title = toc[0].content; try { // finally, run the renderer on all of the TOC toc.forEach(t => t.html = title_render.render(t.content)); } catch(error) { console.error(error); } metadata.original_md = body; return {toc, content, metadata}; } export const render_with_code = (source_dir, md_file) => { // get the file without path or extension const tail_no_ext = PATH.basename(md_file, PATH.extname(md_file)); // this is a closure that is passed to render to run Ork for the code wrapping const load_code = (metadata, body) => { return _.template(body)({ fs, ork: new Ork(source_dir, metadata)}); } // load the .md file contents let raw_md = fs.readFileSync(md_file); let { toc, content, metadata } = render(raw_md.toString(), load_code); metadata.slug = slugify(tail_no_ext, {lower: true, strict: true}); return {content, toc, metadata}; } export default { render, split, render_with_code, create_renderer }