So a lot of progress has happened since the end of post 12 there. That initial nav renderer is in a much better place now.
function buildNav(curriedParent: BlogPost, page: BlogPost, idx: number, arr: BlogPost[]): void {
const prev = Math.max(0, idx - 1);
const next = Math.min(arr.length - 1, idx + 1);
const navInfo: NavInfo = {
self: page.path,
up: curriedParent.path,
};
if (prev !== idx) navInfo.prev = arr[prev].path;
if (next !== idx) navInfo.next = arr[next].path;
if (page.children) {
navInfo.children = page.children.map(c => c.path);
}
// console.log('buildNav on parent:', curriedParent.path, 'for', page.path, 'idx', idx, navInfo);
page.nav = navInfo;
}
function renderNav(navInfo: NavInfo): string {
let nav = `<nav><div class="self">${navInfo.self}</div><ul>`;
if (navInfo.prev) nav += `\n<li class="left">${navInfo.prev}</li>`;
nav += `\n<li class="up">${navInfo.up}</li>`;
if (navInfo.next) nav += `\n<li class="right">${navInfo.next}</li>`;
if (navInfo.children) {
nav += `\n<li class="children"><ul>${navInfo.children.map(cp => `<li>${cp}</li>`).join('\n')}</ul></li>`;
}
nav += `\n</ul></nav>`;
return nav;
}
Hooray for separation of concerns. Gosh, I just love that these LLMs make it so easy now to refactor for high level improvements to code like this. We never would have guessed they'd get this good this quickly.
Along similar lines, one of the hardest problems in computer science, naming things, you can request with such little effort, sure you might not get back something that's an improvement, but quite often it will be. What's so great is that we don't have to really handhold them much about "basic but hard" things like "what is a variable" and "what variable names currently really suck" and the only fly in the ointment we have (a huge one, of course) is that we have zero guarantees that it will properly apply basic logic or do things like invent references to variables that don't exist, and certainly a very common occurrence is making questionable assumptions about design and acting on them to your dismay.
Just scratching the surface of why I think it will be very fruitful to explore how to combine the strengths of ML and traditional techniques.
Right, so anyway, where I'm at with this currently is I do have correct nav generation in place now:
Blog Structure: {
path: 'blog',
isFile: false,
children: [
{
path: 'blog/airtags.md',
isFile: true,
nav: { self: 'blog/airtags.md', up: 'blog', next: 'blog/blog-engine' }
},
{
path: 'blog/blog-engine',
isFile: false,
children: [
{
path: 'blog/blog-engine/index.md',
isFile: true,
phantom: true
},
{
path: 'blog/blog-engine/1.deep.md',
isFile: true,
nav: {
self: 'blog/blog-engine/1.deep.md',
up: 'blog/blog-engine',
next: 'blog/blog-engine/2.deep.md'
}
},
{
path: 'blog/blog-engine/2.deep.md',
isFile: true,
nav: {
self: 'blog/blog-engine/2.deep.md',
up: 'blog/blog-engine',
prev: 'blog/blog-engine/1.deep.md',
next: 'blog/blog-engine/3.deep.md'
}
},
...
{
path: 'blog/blog-engine/13.md',
isFile: true,
nav: {
self: 'blog/blog-engine/13.md',
up: 'blog/blog-engine',
prev: 'blog/blog-engine/12.md'
}
}
],
nav: {
self: 'blog/blog-engine',
up: 'blog',
prev: 'blog/airtags.md',
next: 'blog/code-as-graphs.md',
children: [
'blog/blog-engine/index.md',
'blog/blog-engine/1.deep.md',
'blog/blog-engine/2.deep.md',
'blog/blog-engine/3.deep.md',
'blog/blog-engine/4.deep.md',
'blog/blog-engine/5.deep.md',
'blog/blog-engine/6.deep.md',
'blog/blog-engine/7.md',
'blog/blog-engine/7.1.deep.md',
'blog/blog-engine/8.md',
'blog/blog-engine/9.md',
'blog/blog-engine/10.md',
'blog/blog-engine/11.md',
'blog/blog-engine/12.md',
'blog/blog-engine/13.md'
]
}
},
...
]
}
I am constructing missing index.md
files in the root of the saga dirs now, and i mark these with a phantom
prop to indicate that I intend to generate index.html
s for these items, but their index.md
post files don't actually exist as I will generally not bother to author them until much later.
In addition, what I need to do with these is inherit the nav that were recursively computed for the nodes that represent
their parent directories; those nodes do not render to HTML files so I need a mechanism to obtain this nav structure to
generate for rendering into those index.html
s.
The conundrum I'm working out right now is with this code:
// structurally iterate pages to render them in. Concurrency starts to matter here?
const proms: Promise<void>[] = [];
const processFile = async (node: BlogPost, targetPath: string): Promise<void> => {
let processedContent: string;
if (node.path.endsWith('.deep.md')) {
processedContent =
`<meta charset="UTF-8">`
+ `<link rel="stylesheet" href="/resources/markdeep/journal.css">`
+ process_markdeep(await fsp.readFile(node.path, 'utf8'));
} else if (!node.phantom) {
processedContent =
`<meta charset="UTF-8">`
+ `<link rel="stylesheet" type="text/css" href="/resources/hljs/default.min.css" />`
+ await marked.parse(await fsp.readFile(node.path, 'utf8'));
} else {
console.assert(node.phantom, `not node.phantom`);
console.assert(node.path.endsWith('/index.md'), 'phantom node not an "index.md"');
// want to insert parent node's nav into processedContent here
}
if (node.nav) {
processedContent += renderNav(node.nav);
}
const targetFile = targetPath.replace(/\.deep\.md$/, '.html').replace(/\.md$/, '.html');
await fsp.mkdir(path.dirname(targetFile), { recursive: true });
await fsp.writeFile(targetFile, processedContent);
};
const iterate = (node: BlogPost, currentPath: string = '') => {
const relativePath = path.relative('blog', node.path);
const targetPath = path.join(targetDir, 'blog', relativePath);
// console.log('iterate node:', node, 'currentPath:', currentPath);
if (node.isFile) {
proms.push(processFile(node, targetPath));
} else if (node.children) {
for (const child of node.children) {
iterate(child, path.join(currentPath, path.basename(node.path)));
}
}
};
iterate(blogStructure);
await Promise.all(proms).then(() => console.log('All files processed successfully'))
.catch(error => console.error('Error processing files:', error));
You see it has already been structured so that the flow of processing/rendering each post file is clear and separated
from the recursion used to iterate the nested data structure. This is great but I now need to reach into the parents of
each of these nodes in order to grab their nav
properties to additionally render as nav footers in these
index.html
s I'm adding now.
That's a bit awkward because I will need to modify the signature of both iterate()
and processFile()
to insert that.
So I think I am going to attempt a different angle on it, I will augment the data structure, making it more of a graph with cycles than the current tree, by adding backreferences to each node's parent so I can directly access this value.
I already know that this should not brick the debug prints for that main blog tree structure because node.js
(util.inspect
) is sweet like that. I'll just see some extra [Circular *n]
reference entries when I log it out, no biggie.
} else {
console.assert(node.phantom, `not node.phantom`);
console.assert(node.path.endsWith('/index.md'), 'phantom node not an "index.md"');
// phantom entries have no nav, just because only real files got navs earlier
processedContent = renderNav(node.parent?.nav);
}
Great, that trick worked, and actually what's great about node's object inspection is that circular references like I've
just made so many of with the parent pointer here are rendered out by util.inspect
with <ref *n>
indexed labels and
corresponding [Circular *n]
pointers, so it's really easy to see where the pointers are pointing.
Example:
<ref *4> {
path: 'blog/test2',
isFile: false,
children: [
{
path: 'blog/test2/index.md',
isFile: true,
phantom: true,
parent: [Circular *4]
},
{
path: 'blog/test2/aa.md',
isFile: true,
parent: [Circular *4],
nav: { self: 'blog/test2/aa.md', up: 'blog/test2' }
}
],
parent: [Circular *1],
nav: {
self: 'blog/test2',
up: 'blog',
prev: 'blog/test',
children: [ 'blog/test2/index.md', 'blog/test2/aa.md' ]
}
}
Something that occurs to me is I could actually use this rendering to build cool diagrams (...perhaps with mermaid, though I had decent results recently with a tool called Diagon, which has better embeddability due to using unicode text based output). I think this may be a good candidate for something to add to my typescript utilities repo next.
Now I've got proper nav getting rendered into pages. They look like this in leaf markdown post pages:
<nav>
<div class="self">blog/blog-engine/13.md</div>
<ul>
<li class="left">blog/blog-engine/12.md</li>
<li class="up">blog/blog-engine</li>
</ul>
</nav>
And my genned index.html
pages now just have a body that is composed of:
<nav>
<div class="self">blog/blog-engine</div>
<ul>
<li class="left">blog/airtags.md</li>
<li class="up">blog</li>
<li class="right">blog/code-as-graphs.md</li>
<li class="children">
<ul>
<li>blog/blog-engine/index.md</li>
<li>blog/blog-engine/1.deep.md</li>
<li>blog/blog-engine/2.deep.md</li>
<li>blog/blog-engine/3.deep.md</li>
<li>blog/blog-engine/4.deep.md</li>
<li>blog/blog-engine/5.deep.md</li>
<li>blog/blog-engine/6.deep.md</li>
<li>blog/blog-engine/7.md</li>
<li>blog/blog-engine/7.1.deep.md</li>
<li>blog/blog-engine/8.md</li>
<li>blog/blog-engine/9.md</li>
<li>blog/blog-engine/10.md</li>
<li>blog/blog-engine/11.md</li>
<li>blog/blog-engine/12.md</li>
<li>blog/blog-engine/13.md</li>
</ul>
</li>
</ul>
</nav>
... which means I am finally ready to whip up some CSS to make this look presentable. I'm actually still just inserting CSS as part of the SSR:
const css_tweaks = /* css */ `
/* allow code blocks to wrap so that they become more readable when resized horizontally. Plan to expose/play with later */
code { white-space: pre-wrap; }
/* Incredibly silly, making hovertext spans visible */
span[title] {
cursor: help;
border-radius: 0.5em;
background-color: color-mix(in srgb, purple 10%, transparent);
transition: cursor 0s linear;
transition-delay: 1s;
}
span[title]:hover {
border: 1px purple solid;
}
/* --> */
`;
// just use some defaults for now but incorporating more for themability is exciting
const resourcesDir = path.join(targetDir, 'resources');
const hljsDir = path.join(resourcesDir, 'hljs');
fs.mkdirSync(hljsDir, { recursive: true });
fs.copyFileSync(path.join(__dirname, ...'../node_modules/highlight.js/styles/default.min.css'.split('/')), path.join(hljsDir, 'default.min.css'));
fs.appendFileSync(path.join(hljsDir, 'default.min.css'), css_tweaks);
Quite hacky, but really simple for now and I will modularize that once I come to do CSS themes.