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 `${spans}`;
}
/**
* 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
}