You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
177 lines
5.0 KiB
177 lines
5.0 KiB
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 "prismjs";
|
|
import loadLanguages from "prismjs/components/index.js";
|
|
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('<span></span>');
|
|
return `<span aria-hidden="true" class="line-numbers-rows">${spans}</span>`;
|
|
}
|
|
|
|
/**
|
|
* 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.");
|
|
|
|
if(!Prism.languages[lang]) {
|
|
// try to load the language
|
|
loadLanguages([lang]);
|
|
}
|
|
|
|
// 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});
|
|
toc.push({level, content, slug});
|
|
return `<h${level} id="${slug}">`;
|
|
}
|
|
|
|
rem.renderer.rules.heading_close = (tokens, idx) => {
|
|
return `</h${tokens[idx].hLevel}>\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
|
|
}
|
|
|