aubade
Version:
filesystem-based content processor
103 lines (102 loc) • 3.87 kB
JavaScript
import { engrave } from '../artisan/index.js';
import { parse as manifest } from '../manifest/index.js';
import { uhi } from '../utils.js';
export function parse(source) {
const match = /---\r?\n([\s\S]+?)\r?\n---/.exec(source);
if (!match)
return { body: source };
const crude = source.slice(match.index + match[0].length);
const memory = manifest(match[1].trim());
const stuffed = inject(crude, memory);
return {
body: stuffed.trim(),
frontmatter: Object.assign(memory, {
/** estimated reading time */
get estimate() {
const paragraphs = stuffed.split('\n').filter((p) => !!p && !/^[!*]/.test(p));
const words = paragraphs.reduce((total, line) => {
if (/^[\t\s]*<.+>/.test(line.trim()))
return total + 1;
const accumulated = line.split(' ').filter((w) => !!w && /\w|\d/.test(w) && w.length > 1);
return total + accumulated.length;
}, 0);
const images = stuffed.match(/!\[.+\]\(.+\)/g);
const total = words + (images || []).length * 12;
return Math.round(total / 240) || 1;
},
/** table of contents */
get table() {
const table = [];
for (const line of stuffed.replace(/<!--[\s\S]+?-->/g, '').split('\n')) {
const match = line.trim().match(/^(#{2,4}) (.+)/);
if (!match)
continue;
const [, hashes, title] = match;
const [delimited] = title.match(/\$\(.*?\)/) || [''];
table.push({
id: uhi(delimited.slice(2, -1) || title),
title: title.replace(delimited, delimited.slice(2, -1)),
level: hashes.length,
});
}
let parents = ['', ''];
for (let i = 0; i < table.length; i++) {
const { id, level } = table[i];
if (level === 2)
parents = [id];
if (level === 3)
parents = [parents[0], id];
if (level === 4)
parents[2] = id;
table[i].id = parents.filter((p) => p).join('-');
}
return table;
},
}),
};
}
function inject(source, metadata) {
const plane = compress(metadata);
return source.replace(/!{(.+)}/g, (s, c) => (c && plane[c]) || s);
}
function compress(metadata, parent = '') {
const memo = {};
const prefix = parent ? `${parent}:` : '';
for (const [k, v] of Object.entries(metadata)) {
if (typeof v !== 'object')
memo[prefix + k] = v;
else
Object.assign(memo, compress(v, k));
}
return memo;
}
export function assemble(source) {
const match = /---\r?\n([\s\S]+?)\r?\n---/.exec(source);
if (!match) {
const document = engrave(source);
const meta = { body: source, words: words(document.tokens) };
return { md: document, meta };
}
const body = source.slice(match.index + match[0].length);
const document = engrave(body);
return {
manifest: manifest(match[1].trim()),
md: document,
meta: { body, words: words(document.tokens) },
};
}
function words(tokens) {
let count = 0;
for (const token of tokens) {
switch (token.type) {
case 'inline:text':
count += token.text.split(/\s+/).length;
break;
case 'parent:paragraph':
count += words(token.children);
default:
break;
}
}
return count;
}