@kenjura/ursa 0.81.3 → 0.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/src/helper/__test__/frontmatterTable.test.js +88 -0
- package/src/helper/frontmatterTable.js +21 -2
- package/src/jobs/generate.js +19 -2
- package/src/serve.js +32 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
# 0.82.0
|
|
2
|
+
2026-05-06
|
|
3
|
+
|
|
4
|
+
- **Frontmatter table is now opt-in**: The HTML frontmatter table that was previously injected into every Markdown/MDX document after the first H1 is now only rendered when the document's frontmatter sets `render-frontmatter: true` (boolean `true` or string `"true"`). Documents without the flag (or with it set to `false`) no longer have the table injected. The `render-frontmatter` key itself is excluded from the rendered table.
|
|
5
|
+
|
|
6
|
+
# 0.81.4
|
|
7
|
+
2026-05-04
|
|
8
|
+
|
|
9
|
+
- bug fix: directory index html files were never being written...impossibly, but there it is
|
|
10
|
+
|
|
1
11
|
# 0.81.3
|
|
2
12
|
2026-04-14
|
|
3
13
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
metadataToTable,
|
|
3
|
+
injectFrontmatterTable,
|
|
4
|
+
isRenderFrontmatterEnabled,
|
|
5
|
+
} from "../frontmatterTable.js";
|
|
6
|
+
|
|
7
|
+
describe("isRenderFrontmatterEnabled", () => {
|
|
8
|
+
it("returns false for missing metadata", () => {
|
|
9
|
+
expect(isRenderFrontmatterEnabled(null)).toBe(false);
|
|
10
|
+
expect(isRenderFrontmatterEnabled(undefined)).toBe(false);
|
|
11
|
+
expect(isRenderFrontmatterEnabled({})).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
it("returns false when flag is missing", () => {
|
|
14
|
+
expect(isRenderFrontmatterEnabled({ title: "Foo" })).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
it("returns false when flag is false", () => {
|
|
17
|
+
expect(isRenderFrontmatterEnabled({ "render-frontmatter": false })).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
it("returns true when flag is boolean true", () => {
|
|
20
|
+
expect(isRenderFrontmatterEnabled({ "render-frontmatter": true })).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
it("returns true when flag is the string 'true'", () => {
|
|
23
|
+
expect(isRenderFrontmatterEnabled({ "render-frontmatter": "true" })).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("injectFrontmatterTable", () => {
|
|
28
|
+
const body = "<h1>Title</h1>\n<p>Body</p>";
|
|
29
|
+
|
|
30
|
+
it("does not inject when render-frontmatter flag is missing", () => {
|
|
31
|
+
const result = injectFrontmatterTable(body, { type: "power", cost: 3 });
|
|
32
|
+
expect(result).toBe(body);
|
|
33
|
+
expect(result).not.toContain("frontmatter-table");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("does not inject when render-frontmatter is false", () => {
|
|
37
|
+
const result = injectFrontmatterTable(body, {
|
|
38
|
+
"render-frontmatter": false,
|
|
39
|
+
type: "power",
|
|
40
|
+
});
|
|
41
|
+
expect(result).toBe(body);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("injects table after first H1 when render-frontmatter is true", () => {
|
|
45
|
+
const result = injectFrontmatterTable(body, {
|
|
46
|
+
"render-frontmatter": true,
|
|
47
|
+
type: "power",
|
|
48
|
+
cost: 3,
|
|
49
|
+
});
|
|
50
|
+
expect(result).toContain('<table class="frontmatter-table">');
|
|
51
|
+
expect(result).toContain("<th>Type</th>");
|
|
52
|
+
expect(result).toContain("<td>power</td>");
|
|
53
|
+
// Table appears after the </h1>
|
|
54
|
+
expect(result.indexOf("</h1>")).toBeLessThan(result.indexOf("frontmatter-table"));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("accepts string 'true' for the flag", () => {
|
|
58
|
+
const result = injectFrontmatterTable(body, {
|
|
59
|
+
"render-frontmatter": "true",
|
|
60
|
+
type: "power",
|
|
61
|
+
});
|
|
62
|
+
expect(result).toContain('<table class="frontmatter-table">');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("does not include render-frontmatter itself as a row", () => {
|
|
66
|
+
const result = injectFrontmatterTable(body, {
|
|
67
|
+
"render-frontmatter": true,
|
|
68
|
+
type: "power",
|
|
69
|
+
});
|
|
70
|
+
expect(result).not.toContain(">Render Frontmatter<");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("returns body unchanged when no displayable entries exist", () => {
|
|
74
|
+
const result = injectFrontmatterTable(body, {
|
|
75
|
+
"render-frontmatter": true,
|
|
76
|
+
title: "ignored",
|
|
77
|
+
});
|
|
78
|
+
expect(result).toBe(body);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("metadataToTable", () => {
|
|
83
|
+
it("still produces a table when called directly (used by other consumers)", () => {
|
|
84
|
+
const html = metadataToTable({ type: "power", cost: 3 });
|
|
85
|
+
expect(html).toContain('<table class="frontmatter-table">');
|
|
86
|
+
expect(html).toContain("<th>Type</th>");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
* and inject it into the document body after the first H1
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Determine whether the document's frontmatter opts in to rendering
|
|
8
|
+
* the frontmatter table. Accepts boolean true or the string "true".
|
|
9
|
+
* @param {Object} metadata
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function isRenderFrontmatterEnabled(metadata) {
|
|
13
|
+
if (!metadata || typeof metadata !== 'object') return false;
|
|
14
|
+
const value = metadata['render-frontmatter'];
|
|
15
|
+
return value === true || value === 'true';
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
/**
|
|
7
19
|
* Convert a metadata value to a displayable string
|
|
8
20
|
* @param {any} value - The value to convert
|
|
@@ -75,7 +87,8 @@ export function metadataToTable(metadata) {
|
|
|
75
87
|
'generate-auto-index', // Auto-indexing control
|
|
76
88
|
'auto-index-depth', // Auto-indexing depth
|
|
77
89
|
'auto-index-position', // Auto-indexing position
|
|
78
|
-
'hydrate'
|
|
90
|
+
'hydrate', // MDX hydration control
|
|
91
|
+
'render-frontmatter' // Opt-in flag controlling whether this table is rendered
|
|
79
92
|
];
|
|
80
93
|
const entries = Object.entries(metadata).filter(
|
|
81
94
|
([key]) => !excludeKeys.includes(key.toLowerCase())
|
|
@@ -109,8 +122,14 @@ ${rows}
|
|
|
109
122
|
* @returns {string} The body HTML with the frontmatter table injected
|
|
110
123
|
*/
|
|
111
124
|
export function injectFrontmatterTable(bodyHtml, metadata) {
|
|
125
|
+
// Opt-in: only render the frontmatter table when the document explicitly
|
|
126
|
+
// sets `render-frontmatter: true` in its frontmatter.
|
|
127
|
+
if (!metadata || !isRenderFrontmatterEnabled(metadata)) {
|
|
128
|
+
return bodyHtml;
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
const table = metadataToTable(metadata);
|
|
113
|
-
|
|
132
|
+
|
|
114
133
|
if (!table) {
|
|
115
134
|
return bodyHtml;
|
|
116
135
|
}
|
package/src/jobs/generate.js
CHANGED
|
@@ -1025,7 +1025,7 @@ export async function generate({
|
|
|
1025
1025
|
|
|
1026
1026
|
// html
|
|
1027
1027
|
const htmlOutputFilename = dirPath.replace(source, output) + ".html";
|
|
1028
|
-
const indexAlreadyExists = fileExists(htmlOutputFilename);
|
|
1028
|
+
const indexAlreadyExists = await fileExists(htmlOutputFilename);
|
|
1029
1029
|
if (!indexAlreadyExists) {
|
|
1030
1030
|
const template = templates["default-template"];
|
|
1031
1031
|
const indexHtml = `<ul>${pathsInThisDirectory
|
|
@@ -1583,7 +1583,24 @@ export async function regenerateSingleFile(changedFile, {
|
|
|
1583
1583
|
const xmlOutputFilename = outputFilename.replace(".html", ".xml");
|
|
1584
1584
|
const xml = `<article>${o2x(jsonObject)}</article>`;
|
|
1585
1585
|
await outputFile(xmlOutputFilename, xml);
|
|
1586
|
-
|
|
1586
|
+
|
|
1587
|
+
// Folder-named index promotion (mirrors autoIndex behavior on full builds):
|
|
1588
|
+
// If the file is named like its parent folder (e.g. aletheia/aletheia.md) and
|
|
1589
|
+
// no explicit index.{md,mdx,txt,yml,html} exists alongside it, also write the
|
|
1590
|
+
// same outputs to <dir>/index.html|json|xml so the canonical URL stays fresh.
|
|
1591
|
+
const sourceDirOfFile = dirname(changedFile);
|
|
1592
|
+
const parentFolderName = basename(sourceDirOfFile);
|
|
1593
|
+
if (base && parentFolderName && base === parentFolderName) {
|
|
1594
|
+
const hasExplicitIndex = ['index.md', 'index.mdx', 'index.txt', 'index.yml', 'index.html']
|
|
1595
|
+
.some(name => existsSync(join(sourceDirOfFile, name)));
|
|
1596
|
+
if (!hasExplicitIndex) {
|
|
1597
|
+
const outDirOfFile = dirname(outputFilename);
|
|
1598
|
+
await outputFile(join(outDirOfFile, 'index.html'), finalHtml);
|
|
1599
|
+
await outputFile(join(outDirOfFile, 'index.json'), json);
|
|
1600
|
+
await outputFile(join(outDirOfFile, 'index.xml'), xml);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1587
1604
|
// Update hash cache
|
|
1588
1605
|
updateHash(changedFile, rawBody, hashCache);
|
|
1589
1606
|
|
package/src/serve.js
CHANGED
|
@@ -69,6 +69,30 @@ function docPathToUrl(docPath, sourceDir) {
|
|
|
69
69
|
return normalizeUrl(rawUrl);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Return all URL paths a source file maps to. Most files map to a single URL,
|
|
74
|
+
* but folder-named files (e.g. aletheia/aletheia.md) are promoted to
|
|
75
|
+
* <folder>/index.html during the build, so they also serve the folder URL.
|
|
76
|
+
* @param {string} docPath - Absolute source path
|
|
77
|
+
* @param {string} sourceDir - Absolute source directory (with trailing slash)
|
|
78
|
+
* @returns {string[]} Normalized URL paths
|
|
79
|
+
*/
|
|
80
|
+
function docPathToUrls(docPath, sourceDir) {
|
|
81
|
+
const urls = [docPathToUrl(docPath, sourceDir)];
|
|
82
|
+
const ext = docPath.match(/\.(md|mdx|txt|yml|yaml)$/);
|
|
83
|
+
if (ext) {
|
|
84
|
+
const base = basename(docPath, ext[0]);
|
|
85
|
+
const parent = basename(dirname(docPath));
|
|
86
|
+
if (base && parent && base === parent) {
|
|
87
|
+
// Folder-named file → also serves the folder's index URL
|
|
88
|
+
const folderUrl = normalizeUrl('/' + dirname(docPath).replace(
|
|
89
|
+
sourceDir.endsWith('/') ? sourceDir : sourceDir + '/', '') + '/');
|
|
90
|
+
if (!urls.includes(folderUrl)) urls.push(folderUrl);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return urls;
|
|
94
|
+
}
|
|
95
|
+
|
|
72
96
|
/**
|
|
73
97
|
* Broadcast a message to all connected WebSocket clients.
|
|
74
98
|
* @param {object} messageObj - Object to JSON.stringify and send
|
|
@@ -573,10 +597,12 @@ export async function serve({
|
|
|
573
597
|
const affectedUrlSet = new Set();
|
|
574
598
|
|
|
575
599
|
for (const docPath of docPathsArray) {
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
priorityPaths.
|
|
600
|
+
const urls = docPathToUrls(docPath, sourceDir + '/');
|
|
601
|
+
for (const url of urls) {
|
|
602
|
+
affectedUrlSet.add(url);
|
|
603
|
+
if (viewedUrls.includes(url) && !priorityPaths.includes(docPath)) {
|
|
604
|
+
priorityPaths.push(docPath);
|
|
605
|
+
}
|
|
580
606
|
}
|
|
581
607
|
}
|
|
582
608
|
|
|
@@ -599,7 +625,7 @@ export async function serve({
|
|
|
599
625
|
onPriorityComplete: ({ regenerated, failed, priorityDocs }) => {
|
|
600
626
|
if (regenerated > 0) {
|
|
601
627
|
// Immediately reload clients whose pages are now ready
|
|
602
|
-
const readyUrls = new Set(priorityDocs.
|
|
628
|
+
const readyUrls = new Set(priorityDocs.flatMap(p => docPathToUrls(p, sourceDir + '/')));
|
|
603
629
|
console.log(`⚡ Priority complete: ${regenerated} OK, ${failed} failed → reloading clients`);
|
|
604
630
|
reloadAffectedClients(readyUrls, uniqueNames[0]);
|
|
605
631
|
} else if (failed > 0) {
|
|
@@ -610,7 +636,7 @@ export async function serve({
|
|
|
610
636
|
|
|
611
637
|
// After all remaining docs are done, reload any remaining affected clients
|
|
612
638
|
// (non-priority clients that weren't reloaded during onPriorityComplete)
|
|
613
|
-
const priorityUrlSet = new Set(priorityPaths.
|
|
639
|
+
const priorityUrlSet = new Set(priorityPaths.flatMap(p => docPathToUrls(p, sourceDir + '/')));
|
|
614
640
|
const remainingUrls = new Set([...affectedUrlSet].filter(u => !priorityUrlSet.has(u)));
|
|
615
641
|
if (remainingUrls.size > 0) {
|
|
616
642
|
reloadAffectedClients(remainingUrls, uniqueNames[0]);
|