blog/src/plugins/rehype.js

117 lines
3.6 KiB
JavaScript
Raw Normal View History

2023-08-19 19:45:02 +00:00
import { visit, CONTINUE, EXIT, SKIP, } from 'unist-util-visit';
import { find } from 'unist-util-find';
import { toText } from 'hast-util-to-text';
import { makeSlug } from '../lib/utils.js';
2023-12-27 04:30:09 +00:00
import {writeFileSync} from 'node:fs';
import {toHtml} from 'hast-util-to-html';
2023-08-19 19:45:02 +00:00
2023-12-27 04:30:09 +00:00
export function localRehype() {
2023-08-19 19:45:02 +00:00
return (tree, vfile) => {
const needsDropcap = vfile.data.fm.dropcap !== false
let dropcapAdded = false;
let sidenotesCount = 0;
2023-08-19 19:45:02 +00:00
let moduleScript;
2023-08-20 23:12:04 +00:00
let imports = new Set();
2023-08-19 19:45:02 +00:00
if (needsDropcap) {
2023-08-20 23:12:04 +00:00
imports.add("import Dropcap from '$lib/Dropcap.svelte';");
2023-08-19 19:45:02 +00:00
}
visit(tree, node => {
// add slugs to headings
if (isHeading(node)) {
processHeading(node);
2023-08-20 23:12:04 +00:00
imports.add("import Heading from '$lib/Heading.svelte';");
2023-08-19 19:45:02 +00:00
return SKIP;
}
// mdsvex adds a <script context="module"> so we just hijack that for our own purposes
if (isModuleScript(node)) {
moduleScript = node;
}
// convert first letter/word of first paragraph to <Dropcap word="{whatever}">
if (needsDropcap && !dropcapAdded && isParagraph(node)) {
addDropcap(node);
dropcapAdded = true;
}
// add `count` prop to each <Sidenote> component
if (isSidenote(node)) {
// increment the counter first so that the count starts at 1
sidenotesCount += 1;
addSidenoteCount(node, sidenotesCount);
2023-08-19 19:45:02 +00:00
}
});
// insert our imports at the top of the `<script context="module">` tag
2023-08-20 23:12:04 +00:00
if (imports.size > 0) {
2023-08-19 19:45:02 +00:00
const script = moduleScript.value;
// split the script where the opening tag ends
const i = script.indexOf('>');
const openingTag = script.slice(0, i + 1);
const remainder = script.slice(i + 1);
// mdvsex uses tabs so we will as well
2023-08-20 23:12:04 +00:00
const importScript = Array.from(imports).join('\n\t');
2023-08-19 19:45:02 +00:00
2023-08-20 23:12:04 +00:00
moduleScript.value = `${openingTag}\n\t${importScript}${remainder}`;
2023-08-19 19:45:02 +00:00
}
// const name = vfile.filename.split('/').findLast(() => true);
// writeFileSync(`scratch/${name}.json`, JSON.stringify(tree, undefined, 4));
2023-08-19 19:45:02 +00:00
}
}
function processHeading(node) {
2023-08-20 23:12:04 +00:00
const level = node.tagName.slice(1);
node.tagName = 'Heading';
node.properties.level = level;
2023-08-19 19:45:02 +00:00
node.properties.id = makeSlug(toText(node));
}
function addDropcap(par) {
let txtNode = find(par, {type: 'text'});
const i = txtNode.value.search(/\s/);
const firstWord = txtNode.value.slice(0, i);
const remainder = txtNode.value.slice(i);
par.children.unshift({
type: 'raw',
value: `<Dropcap word="${firstWord}" />`,
});
txtNode.value = remainder;
}
function addSidenoteCount(node, count) {
// get the index of the closing >
const i = node.value.search(/>\s*$/);
if (i < 0) {
throw new Error('Failed to add counter to element, closing angle bracket not found.');
}
// splice in the count prop
node.value = `${node.value.slice(0, i)} count={${count}}>`;
}
2023-08-19 19:45:02 +00:00
function isHeading(node) {
return node.type === 'element' && node.tagName.match(/h[1-6]/);
}
function isModuleScript(node) {
return node.type === 'raw' && node.value.match(/^<script context="module">/);
}
function isParagraph(node) {
return node.type === 'element' && node.tagName === 'p';
}
function isSidenote(node) {
return node.type === 'raw' && node.value.match(/<\s*Sidenote/);
}