narrarium-astro-reader 0.1.20 → 0.1.22
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/README.md +1 -0
- package/cli-dist/lib/book.d.ts +1 -0
- package/cli-dist/lib/book.d.ts.map +1 -1
- package/cli-dist/lib/book.js +10 -1
- package/cli-dist/lib/book.js.map +1 -1
- package/cli-dist/lib/canon-mentions.d.ts +8 -0
- package/cli-dist/lib/canon-mentions.d.ts.map +1 -0
- package/cli-dist/lib/canon-mentions.js +86 -0
- package/cli-dist/lib/canon-mentions.js.map +1 -0
- package/cli-dist/lib/workshop.d.ts +49 -0
- package/cli-dist/lib/workshop.d.ts.map +1 -0
- package/cli-dist/lib/workshop.js +113 -0
- package/cli-dist/lib/workshop.js.map +1 -0
- package/package.json +2 -2
- package/src/components/ReaderRuntime.astro +43 -15
- package/src/layouts/BaseLayout.astro +2 -0
- package/src/lib/book.ts +11 -1
- package/src/lib/canon-mentions.ts +105 -0
- package/src/lib/workshop.ts +159 -0
- package/src/pages/index.astro +11 -1
- package/src/pages/workshop/index.astro +138 -0
- package/src/styles/global.css +36 -0
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ In public mode:
|
|
|
43
43
|
- secrets stay hidden from the public atlas and nav
|
|
44
44
|
- direct canon pages fall back to teaser or locked views when `known_from` or `reveal_in` say a dossier is not safe yet
|
|
45
45
|
- search, canon popups, and backlinks follow the same thresholds
|
|
46
|
+
- chapter and scene prose should keep canon names as plain text; the reader upgrades visible mentions into canon popups and also rewrites legacy internal canon links at runtime
|
|
46
47
|
|
|
47
48
|
If you want an author-only or spoiler-friendly deployment, enable full canon mode:
|
|
48
49
|
|
package/cli-dist/lib/book.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"book.d.ts","sourceRoot":"","sources":["../../src/lib/book.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"book.d.ts","sourceRoot":"","sources":["../../src/lib/book.ts"],"names":[],"mappings":"AAcA,KAAK,gBAAgB,GACjB,WAAW,GACX,UAAU,GACV,SAAS,GACT,MAAM,GACN,QAAQ,GACR,gBAAgB,CAAC;AAErB,wBAAgB,WAAW,IAAI,MAAM,CAKpC;AAED,wBAAsB,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CrC;AAQD,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM;;;;;;;;GAG5D;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,gBAAgB;;;;;;;;;GAa/D;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM;;;;;GAG5E;AAED,wBAAsB,oBAAoB;;;;;;;;;;;;;GAoBzC"}
|
package/cli-dist/lib/book.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
2
3
|
import { listChapters, listEntities, pathExists, readBook, readChapter, readEntity, readTimelineMain, } from "narrarium";
|
|
3
4
|
import { defaultBookRoot } from "./book-config.js";
|
|
4
5
|
import { readReaderBookRootEnv, resolveReaderBookRootCandidate } from "./env.js";
|
|
@@ -18,6 +19,7 @@ export async function loadHomePageData() {
|
|
|
18
19
|
root,
|
|
19
20
|
book: null,
|
|
20
21
|
chapters: [],
|
|
22
|
+
draftChapterCount: 0,
|
|
21
23
|
characters: [],
|
|
22
24
|
locations: [],
|
|
23
25
|
factions: [],
|
|
@@ -26,9 +28,10 @@ export async function loadHomePageData() {
|
|
|
26
28
|
timelineEvents: [],
|
|
27
29
|
};
|
|
28
30
|
}
|
|
29
|
-
const [book, chapters, characters, locations, factions, items, secrets, timelineEvents] = await Promise.all([
|
|
31
|
+
const [book, chapters, draftChapterCount, characters, locations, factions, items, secrets, timelineEvents] = await Promise.all([
|
|
30
32
|
readBook(root),
|
|
31
33
|
listChapters(root),
|
|
34
|
+
countDraftChapters(root),
|
|
32
35
|
listEntities(root, "character"),
|
|
33
36
|
listEntities(root, "location"),
|
|
34
37
|
listEntities(root, "faction"),
|
|
@@ -41,6 +44,7 @@ export async function loadHomePageData() {
|
|
|
41
44
|
root,
|
|
42
45
|
book,
|
|
43
46
|
chapters,
|
|
47
|
+
draftChapterCount,
|
|
44
48
|
characters,
|
|
45
49
|
locations,
|
|
46
50
|
factions,
|
|
@@ -49,6 +53,11 @@ export async function loadHomePageData() {
|
|
|
49
53
|
timelineEvents,
|
|
50
54
|
};
|
|
51
55
|
}
|
|
56
|
+
async function countDraftChapters(root) {
|
|
57
|
+
const draftsRoot = path.join(root, "drafts");
|
|
58
|
+
const entries = await readdir(draftsRoot, { withFileTypes: true }).catch(() => []);
|
|
59
|
+
return entries.filter((entry) => entry.isDirectory()).length;
|
|
60
|
+
}
|
|
52
61
|
export async function loadChapterPageData(chapterSlug) {
|
|
53
62
|
const root = getBookRoot();
|
|
54
63
|
return readChapter(root, chapterSlug);
|
package/cli-dist/lib/book.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"book.js","sourceRoot":"","sources":["../../src/lib/book.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,WAAW,EACX,UAAU,EACV,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,8BAA8B,EAAE,MAAM,UAAU,CAAC;AAUjF,MAAM,UAAU,WAAW;IACzB,MAAM,UAAU,GAAG,qBAAqB,EAAE,CAAC;IAC3C,MAAM,kBAAkB,GAAG,8BAA8B,CAAC,UAAU,CAAC,CAAC;IACtE,IAAI,kBAAkB;QAAE,OAAO,kBAAkB,CAAC;IAClD,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,eAAe,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,MAAM,IAAI,GAAG,WAAW,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC;IAE7D,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,IAAI;YACJ,IAAI,EAAE,IAAI;YACV,QAAQ,EAAE,EAAE;YACZ,UAAU,EAAE,EAAE;YACd,SAAS,EAAE,EAAE;YACb,QAAQ,EAAE,EAAE;YACZ,KAAK,EAAE,EAAE;YACT,OAAO,EAAE,EAAE;YACX,cAAc,EAAE,EAAE;SACnB,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,cAAc,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;
|
|
1
|
+
{"version":3,"file":"book.js","sourceRoot":"","sources":["../../src/lib/book.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,WAAW,EACX,UAAU,EACV,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,8BAA8B,EAAE,MAAM,UAAU,CAAC;AAUjF,MAAM,UAAU,WAAW;IACzB,MAAM,UAAU,GAAG,qBAAqB,EAAE,CAAC;IAC3C,MAAM,kBAAkB,GAAG,8BAA8B,CAAC,UAAU,CAAC,CAAC;IACtE,IAAI,kBAAkB;QAAE,OAAO,kBAAkB,CAAC;IAClD,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,eAAe,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,MAAM,IAAI,GAAG,WAAW,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC;IAE7D,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,IAAI;YACJ,IAAI,EAAE,IAAI;YACV,QAAQ,EAAE,EAAE;YACZ,iBAAiB,EAAE,CAAC;YACpB,UAAU,EAAE,EAAE;YACd,SAAS,EAAE,EAAE;YACb,QAAQ,EAAE,EAAE;YACZ,KAAK,EAAE,EAAE;YACT,OAAO,EAAE,EAAE;YACX,cAAc,EAAE,EAAE;SACnB,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,iBAAiB,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,cAAc,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC7H,QAAQ,CAAC,IAAI,CAAC;QACd,YAAY,CAAC,IAAI,CAAC;QAClB,kBAAkB,CAAC,IAAI,CAAC;QACxB,YAAY,CAAC,IAAI,EAAE,WAAW,CAAC;QAC/B,YAAY,CAAC,IAAI,EAAE,UAAU,CAAC;QAC9B,YAAY,CAAC,IAAI,EAAE,SAAS,CAAC;QAC7B,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC;QAC1B,YAAY,CAAC,IAAI,EAAE,QAAQ,CAAC;QAC5B,YAAY,CAAC,IAAI,EAAE,gBAAgB,CAAC;KACrC,CAAC,CAAC;IAEH,OAAO;QACL,KAAK,EAAE,IAAI;QACX,IAAI;QACJ,IAAI;QACJ,QAAQ;QACR,iBAAiB;QACjB,UAAU;QACV,SAAS;QACT,QAAQ;QACR,KAAK;QACL,OAAO;QACP,cAAc;KACf,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,kBAAkB,CAAC,IAAY;IAC5C,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IACnF,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;AAC/D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,WAAmB;IAC3D,MAAM,IAAI,GAAG,WAAW,EAAE,CAAC;IAC3B,OAAO,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,IAAsB;IAC9D,MAAM,IAAI,GAAG,WAAW,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC;IAE3D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAC9C,CAAC;IAED,OAAO;QACL,KAAK,EAAE,IAAI;QACX,IAAI;QACJ,QAAQ,EAAE,MAAM,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC;KACzC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,IAAsB,EAAE,IAAY;IAC3E,MAAM,IAAI,GAAG,WAAW,EAAE,CAAC;IAC3B,OAAO,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB;IACxC,MAAM,IAAI,GAAG,WAAW,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC;IAE3D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,IAAI;YACJ,IAAI,EAAE,IAAI;YACV,MAAM,EAAE,EAAE;SACX,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IACzG,OAAO;QACL,KAAK,EAAE,IAAI;QACX,IAAI;QACJ,IAAI;QACJ,MAAM;KACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type CanonMentionEntry = {
|
|
2
|
+
id: string;
|
|
3
|
+
href: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function buildCanonHrefIndex(entries: CanonMentionEntry[]): Map<string, string>;
|
|
6
|
+
export declare function resolveCanonEntryIdFromHref(href: string, entries: CanonMentionEntry[]): string | null;
|
|
7
|
+
export declare function normalizeCanonEntityHref(href: string): string | null;
|
|
8
|
+
//# sourceMappingURL=canon-mentions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canon-mentions.d.ts","sourceRoot":"","sources":["../../src/lib/canon-mentions.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAarF;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,EAAE,GAAG,MAAM,GAAG,IAAI,CAOrG;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyCpE"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export function buildCanonHrefIndex(entries) {
|
|
2
|
+
const index = new Map();
|
|
3
|
+
for (const entry of entries) {
|
|
4
|
+
const normalizedHref = normalizeCanonEntityHref(entry.href);
|
|
5
|
+
if (!normalizedHref) {
|
|
6
|
+
continue;
|
|
7
|
+
}
|
|
8
|
+
index.set(normalizedHref, entry.id);
|
|
9
|
+
}
|
|
10
|
+
return index;
|
|
11
|
+
}
|
|
12
|
+
export function resolveCanonEntryIdFromHref(href, entries) {
|
|
13
|
+
const normalizedHref = normalizeCanonEntityHref(href);
|
|
14
|
+
if (!normalizedHref) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return buildCanonHrefIndex(entries).get(normalizedHref) ?? null;
|
|
18
|
+
}
|
|
19
|
+
export function normalizeCanonEntityHref(href) {
|
|
20
|
+
const strippedOrigin = href
|
|
21
|
+
.trim()
|
|
22
|
+
.replace(/^[a-z]+:\/\/[^/]+/i, "")
|
|
23
|
+
.split(/[?#]/, 1)[0]
|
|
24
|
+
.replace(/\\/g, "/");
|
|
25
|
+
if (!strippedOrigin) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const segments = strippedOrigin
|
|
29
|
+
.split("/")
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.filter((segment) => segment !== "." && segment !== "..");
|
|
32
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
33
|
+
const current = segments[index]?.toLowerCase();
|
|
34
|
+
if (!current) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (current === "timelines" && segments[index + 1]?.toLowerCase() === "events") {
|
|
38
|
+
const slug = normalizeSlugSegment(segments[index + 2]);
|
|
39
|
+
if (slug) {
|
|
40
|
+
return `timeline/${slug}/`;
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const section = normalizeCanonSection(current);
|
|
45
|
+
if (!section) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const slug = normalizeSlugSegment(segments[index + 1]);
|
|
49
|
+
if (slug) {
|
|
50
|
+
return `${section}/${slug}/`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function normalizeCanonSection(segment) {
|
|
56
|
+
switch (segment.toLowerCase()) {
|
|
57
|
+
case "character":
|
|
58
|
+
case "characters":
|
|
59
|
+
return "characters";
|
|
60
|
+
case "location":
|
|
61
|
+
case "locations":
|
|
62
|
+
return "locations";
|
|
63
|
+
case "faction":
|
|
64
|
+
case "factions":
|
|
65
|
+
return "factions";
|
|
66
|
+
case "item":
|
|
67
|
+
case "items":
|
|
68
|
+
return "items";
|
|
69
|
+
case "secret":
|
|
70
|
+
case "secrets":
|
|
71
|
+
return "secrets";
|
|
72
|
+
case "timeline":
|
|
73
|
+
case "timeline-event":
|
|
74
|
+
return "timeline";
|
|
75
|
+
default:
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function normalizeSlugSegment(segment) {
|
|
80
|
+
if (!segment) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const normalized = segment.replace(/\.md$/i, "").trim().toLowerCase();
|
|
84
|
+
return /^[a-z0-9-]+$/.test(normalized) ? normalized : null;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=canon-mentions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canon-mentions.js","sourceRoot":"","sources":["../../src/lib/canon-mentions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type WorkshopEntry = {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
body: string;
|
|
5
|
+
status: string;
|
|
6
|
+
tags: string[];
|
|
7
|
+
sourceKind?: string;
|
|
8
|
+
promotedTo?: string;
|
|
9
|
+
};
|
|
10
|
+
export type WorkshopDocument = {
|
|
11
|
+
path: string;
|
|
12
|
+
title: string;
|
|
13
|
+
bucket: string;
|
|
14
|
+
bodyHtml: string;
|
|
15
|
+
entries: WorkshopEntry[];
|
|
16
|
+
};
|
|
17
|
+
export type WorkshopDraftChapter = {
|
|
18
|
+
slug: string;
|
|
19
|
+
title: string;
|
|
20
|
+
summary: string;
|
|
21
|
+
bodyHtml: string;
|
|
22
|
+
paragraphs: Array<{
|
|
23
|
+
slug: string;
|
|
24
|
+
title: string;
|
|
25
|
+
summary: string;
|
|
26
|
+
}>;
|
|
27
|
+
ideas: WorkshopDocument | null;
|
|
28
|
+
notes: WorkshopDocument | null;
|
|
29
|
+
promoted: WorkshopDocument | null;
|
|
30
|
+
};
|
|
31
|
+
export declare function loadWorkshopPageData(): Promise<{
|
|
32
|
+
ready: boolean;
|
|
33
|
+
root: string;
|
|
34
|
+
global: null;
|
|
35
|
+
draftChapters: never[];
|
|
36
|
+
} | {
|
|
37
|
+
ready: boolean;
|
|
38
|
+
root: string;
|
|
39
|
+
global: {
|
|
40
|
+
context: WorkshopDocument | null;
|
|
41
|
+
ideas: WorkshopDocument | null;
|
|
42
|
+
notes: WorkshopDocument | null;
|
|
43
|
+
storyDesign: WorkshopDocument | null;
|
|
44
|
+
promoted: WorkshopDocument | null;
|
|
45
|
+
};
|
|
46
|
+
draftChapters: WorkshopDraftChapter[];
|
|
47
|
+
}>;
|
|
48
|
+
export declare function countDraftChapters(root: string): Promise<number>;
|
|
49
|
+
//# sourceMappingURL=workshop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workshop.d.ts","sourceRoot":"","sources":["../../src/lib/workshop.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC/B,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC/B,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACnC,CAAC;AAEF,wBAAsB,oBAAoB;;;;;;;;;;;;;;;;GAkCzC;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEtE"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { marked } from "marked";
|
|
4
|
+
import { parseNarrariumMarkdownDocument, pathExists, readChapterDraft } from "narrarium";
|
|
5
|
+
import { getBookRoot } from "./book.js";
|
|
6
|
+
export async function loadWorkshopPageData() {
|
|
7
|
+
const root = getBookRoot();
|
|
8
|
+
const ready = await pathExists(path.join(root, "book.md"));
|
|
9
|
+
if (!ready) {
|
|
10
|
+
return {
|
|
11
|
+
ready: false,
|
|
12
|
+
root,
|
|
13
|
+
global: null,
|
|
14
|
+
draftChapters: [],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const [context, ideas, notes, storyDesign, promoted, draftChapters] = await Promise.all([
|
|
18
|
+
readWorkshopDocument(root, "context.md"),
|
|
19
|
+
readWorkshopDocument(root, "ideas.md"),
|
|
20
|
+
readWorkshopDocument(root, "notes.md"),
|
|
21
|
+
readWorkshopDocument(root, "story-design.md"),
|
|
22
|
+
readWorkshopDocument(root, "promoted.md"),
|
|
23
|
+
listDraftChapters(root),
|
|
24
|
+
]);
|
|
25
|
+
return {
|
|
26
|
+
ready: true,
|
|
27
|
+
root,
|
|
28
|
+
global: {
|
|
29
|
+
context,
|
|
30
|
+
ideas,
|
|
31
|
+
notes,
|
|
32
|
+
storyDesign,
|
|
33
|
+
promoted,
|
|
34
|
+
},
|
|
35
|
+
draftChapters,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export async function countDraftChapters(root) {
|
|
39
|
+
return (await listDraftChapters(root)).length;
|
|
40
|
+
}
|
|
41
|
+
async function listDraftChapters(root) {
|
|
42
|
+
const draftsRoot = path.join(root, "drafts");
|
|
43
|
+
const entries = await readdir(draftsRoot, { withFileTypes: true }).catch(() => []);
|
|
44
|
+
const slugs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
45
|
+
const chapters = await Promise.all(slugs.map(async (slug) => {
|
|
46
|
+
const chapterFile = path.join(draftsRoot, slug, "chapter.md");
|
|
47
|
+
if (!(await pathExists(chapterFile))) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const chapter = await readChapterDraft(root, slug);
|
|
51
|
+
const [ideas, notes, promoted] = await Promise.all([
|
|
52
|
+
readWorkshopDocument(root, path.posix.join("drafts", slug, "ideas.md")),
|
|
53
|
+
readWorkshopDocument(root, path.posix.join("drafts", slug, "notes.md")),
|
|
54
|
+
readWorkshopDocument(root, path.posix.join("drafts", slug, "promoted.md")),
|
|
55
|
+
]);
|
|
56
|
+
return {
|
|
57
|
+
slug,
|
|
58
|
+
title: chapter.metadata.title,
|
|
59
|
+
summary: chapter.metadata.summary ?? "Draft chapter in progress.",
|
|
60
|
+
bodyHtml: await toHtml(chapter.body || "No chapter draft body yet."),
|
|
61
|
+
paragraphs: chapter.paragraphs.map((paragraph) => ({
|
|
62
|
+
slug: path.basename(paragraph.path, ".md"),
|
|
63
|
+
title: paragraph.metadata.title,
|
|
64
|
+
summary: paragraph.metadata.summary ?? "Draft scene.",
|
|
65
|
+
})),
|
|
66
|
+
ideas,
|
|
67
|
+
notes,
|
|
68
|
+
promoted,
|
|
69
|
+
};
|
|
70
|
+
}));
|
|
71
|
+
return chapters.filter((chapter) => Boolean(chapter));
|
|
72
|
+
}
|
|
73
|
+
async function readWorkshopDocument(root, relativePath) {
|
|
74
|
+
const absolutePath = path.join(root, relativePath);
|
|
75
|
+
const raw = await readFile(absolutePath, "utf8").catch(() => null);
|
|
76
|
+
if (!raw) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const document = parseNarrariumMarkdownDocument(relativePath, raw);
|
|
80
|
+
const frontmatter = document.frontmatter;
|
|
81
|
+
const title = typeof frontmatter.title === "string" && frontmatter.title.trim() ? frontmatter.title : relativePath;
|
|
82
|
+
const bucket = typeof frontmatter.bucket === "string" ? frontmatter.bucket : document.kind;
|
|
83
|
+
return {
|
|
84
|
+
path: relativePath,
|
|
85
|
+
title,
|
|
86
|
+
bucket,
|
|
87
|
+
bodyHtml: await toHtml(document.body || "No content yet."),
|
|
88
|
+
entries: readWorkshopEntries(frontmatter),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function readWorkshopEntries(frontmatter) {
|
|
92
|
+
if (!Array.isArray(frontmatter.entries)) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
return frontmatter.entries
|
|
96
|
+
.filter((entry) => Boolean(entry && typeof entry === "object"))
|
|
97
|
+
.map((entry) => ({
|
|
98
|
+
id: typeof entry.id === "string" ? entry.id : "",
|
|
99
|
+
title: typeof entry.title === "string" ? entry.title : "Untitled",
|
|
100
|
+
body: typeof entry.body === "string" ? entry.body : "",
|
|
101
|
+
status: typeof entry.status === "string" ? entry.status : "active",
|
|
102
|
+
tags: Array.isArray(entry.tags) ? entry.tags.filter((tag) => typeof tag === "string") : [],
|
|
103
|
+
sourceKind: typeof entry.source_kind === "string" ? entry.source_kind : undefined,
|
|
104
|
+
promotedTo: typeof entry.promoted_to === "string" ? entry.promoted_to : undefined,
|
|
105
|
+
}))
|
|
106
|
+
.filter((entry) => entry.id.length > 0)
|
|
107
|
+
.sort((left, right) => left.title.localeCompare(right.title));
|
|
108
|
+
}
|
|
109
|
+
async function toHtml(markdown) {
|
|
110
|
+
const rendered = await marked.parse(markdown);
|
|
111
|
+
return typeof rendered === "string" ? rendered : String(rendered);
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=workshop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workshop.js","sourceRoot":"","sources":["../../src/lib/workshop.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "narrarium-astro-reader",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Astro reader and scaffolding CLI for Narrarium book repositories.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"test": "npm run build:cli && node --test test/**/*.test.mjs"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"narrarium": "^0.1.
|
|
53
|
+
"narrarium": "^0.1.22",
|
|
54
54
|
"astro": "^5.14.1",
|
|
55
55
|
"chokidar": "^4.0.3",
|
|
56
56
|
"marked": "^16.3.0"
|
|
@@ -144,7 +144,9 @@ const chaptersJson = JSON.stringify(chapters).replace(/</g, "\\u003c");
|
|
|
144
144
|
<script type="application/json" id="canon-glossary-data" set:html={glossaryJson}></script>
|
|
145
145
|
<script type="application/json" id="reader-chapter-data" set:html={chaptersJson}></script>
|
|
146
146
|
|
|
147
|
-
<script
|
|
147
|
+
<script>
|
|
148
|
+
import { buildCanonHrefIndex, normalizeCanonEntityHref } from "../lib/canon-mentions.js";
|
|
149
|
+
|
|
148
150
|
const themeStorageKey = "narrarium-reader-theme";
|
|
149
151
|
const canonModeStorageKey = "narrarium-reader-canon";
|
|
150
152
|
const progressStorageKey = "narrarium-reader-progress";
|
|
@@ -354,6 +356,7 @@ const chaptersJson = JSON.stringify(chapters).replace(/</g, "\\u003c");
|
|
|
354
356
|
const limit = getSelectedSpoilerLimit();
|
|
355
357
|
const visibleEntries = glossaryEntries.filter((entry) => isEntryVisible(entry, limit));
|
|
356
358
|
const glossaryById = new Map(visibleEntries.map((entry) => [entry.id, entry]));
|
|
359
|
+
const hrefIndex = buildCanonHrefIndex(visibleEntries);
|
|
357
360
|
const termMap = new Map();
|
|
358
361
|
|
|
359
362
|
for (const entry of visibleEntries) {
|
|
@@ -368,15 +371,15 @@ const chaptersJson = JSON.stringify(chapters).replace(/</g, "\\u003c");
|
|
|
368
371
|
}
|
|
369
372
|
|
|
370
373
|
const terms = [...termMap.keys()].sort((left, right) => right.length - left.length);
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
+
const pattern = terms.length > 0
|
|
375
|
+
? new RegExp(`(^|[^\\p{L}\\p{N}])(${terms.map(escapeRegex).join("|")})(?=$|[^\\p{L}\\p{N}])`, "giu")
|
|
376
|
+
: null;
|
|
374
377
|
|
|
375
|
-
enhanceCanonRoot(document, pattern, termMap);
|
|
376
|
-
initializeCanonOverlay(pattern, termMap, glossaryById, limit);
|
|
378
|
+
enhanceCanonRoot(document, pattern, termMap, hrefIndex);
|
|
379
|
+
initializeCanonOverlay(pattern, termMap, hrefIndex, glossaryById, limit);
|
|
377
380
|
}
|
|
378
381
|
|
|
379
|
-
function enhanceCanonRoot(root, pattern, termMap) {
|
|
382
|
+
function enhanceCanonRoot(root, pattern, termMap, hrefIndex) {
|
|
380
383
|
const containers = root instanceof Document
|
|
381
384
|
? root.querySelectorAll(".prose p, .prose li, .prose blockquote")
|
|
382
385
|
: root.matches?.(".prose")
|
|
@@ -385,7 +388,11 @@ const chaptersJson = JSON.stringify(chapters).replace(/</g, "\\u003c");
|
|
|
385
388
|
|
|
386
389
|
containers.forEach((container) => {
|
|
387
390
|
if (container.dataset.canonEnhanced === "true") return;
|
|
391
|
+
rewriteLegacyCanonLinks(container, hrefIndex);
|
|
388
392
|
container.dataset.canonEnhanced = "true";
|
|
393
|
+
if (!pattern) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
389
396
|
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
390
397
|
const textNodes = [];
|
|
391
398
|
while (walker.nextNode()) {
|
|
@@ -430,12 +437,7 @@ const chaptersJson = JSON.stringify(chapters).replace(/</g, "\\u003c");
|
|
|
430
437
|
}
|
|
431
438
|
|
|
432
439
|
if (entryId) {
|
|
433
|
-
|
|
434
|
-
button.type = "button";
|
|
435
|
-
button.className = "canon-mention";
|
|
436
|
-
button.dataset.canonId = entryId;
|
|
437
|
-
button.textContent = term;
|
|
438
|
-
fragment.append(button);
|
|
440
|
+
fragment.append(createCanonMentionButton(entryId, term));
|
|
439
441
|
} else {
|
|
440
442
|
fragment.append(term);
|
|
441
443
|
}
|
|
@@ -450,7 +452,33 @@ const chaptersJson = JSON.stringify(chapters).replace(/</g, "\\u003c");
|
|
|
450
452
|
textNode.replaceWith(fragment);
|
|
451
453
|
}
|
|
452
454
|
|
|
453
|
-
function
|
|
455
|
+
function rewriteLegacyCanonLinks(container, hrefIndex) {
|
|
456
|
+
container.querySelectorAll("a[href]").forEach((anchor) => {
|
|
457
|
+
if (!(anchor instanceof HTMLAnchorElement) || anchor.closest("[data-no-canon]")) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const normalizedHref = normalizeCanonEntityHref(anchor.getAttribute("href") || "");
|
|
462
|
+
if (!normalizedHref) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const label = anchor.textContent || "";
|
|
467
|
+
const entryId = hrefIndex.get(normalizedHref);
|
|
468
|
+
anchor.replaceWith(entryId ? createCanonMentionButton(entryId, label) : document.createTextNode(label));
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function createCanonMentionButton(entryId, label) {
|
|
473
|
+
const button = document.createElement("button");
|
|
474
|
+
button.type = "button";
|
|
475
|
+
button.className = "canon-mention";
|
|
476
|
+
button.dataset.canonId = entryId;
|
|
477
|
+
button.textContent = label;
|
|
478
|
+
return button;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function initializeCanonOverlay(pattern, termMap, hrefIndex, glossaryById, limit) {
|
|
454
482
|
const overlay = document.querySelector("[data-canon-overlay]");
|
|
455
483
|
if (!overlay || overlay.dataset.bound === "true") return;
|
|
456
484
|
overlay.dataset.bound = "true";
|
|
@@ -482,7 +510,7 @@ const chaptersJson = JSON.stringify(chapters).replace(/</g, "\\u003c");
|
|
|
482
510
|
link.href = entry.href;
|
|
483
511
|
link.hidden = !revealed;
|
|
484
512
|
body.innerHTML = revealed ? entry.bodyHtml || "" : "";
|
|
485
|
-
enhanceCanonRoot(body, pattern, termMap);
|
|
513
|
+
enhanceCanonRoot(body, pattern, termMap, hrefIndex);
|
|
486
514
|
metadata.replaceChildren(...(entry.metadataEntries || []).map((item) => {
|
|
487
515
|
const row = document.createElement("div");
|
|
488
516
|
row.className = "meta-row";
|
|
@@ -14,6 +14,7 @@ interface Props {
|
|
|
14
14
|
activeSection?:
|
|
15
15
|
| "home"
|
|
16
16
|
| "chapters"
|
|
17
|
+
| "workshop"
|
|
17
18
|
| "characters"
|
|
18
19
|
| "locations"
|
|
19
20
|
| "factions"
|
|
@@ -75,6 +76,7 @@ const isChapterPage = Number.isFinite(currentChapterNumber);
|
|
|
75
76
|
<nav class="site-nav">
|
|
76
77
|
<a class:list={["nav-link", activeSection === "home" && "is-active"]} href="./">Book</a>
|
|
77
78
|
<a class:list={["nav-link", activeSection === "chapters" && "is-active"]} href="./#chapters">Chapters</a>
|
|
79
|
+
{fullCanonMode && <a class:list={["nav-link", activeSection === "workshop" && "is-active"]} href="workshop/">Workshop</a>}
|
|
78
80
|
<a class:list={["nav-link", activeSection === "characters" && "is-active"]} href="characters/">Characters</a>
|
|
79
81
|
<a class:list={["nav-link", activeSection === "locations" && "is-active"]} href="locations/">Locations</a>
|
|
80
82
|
<a class:list={["nav-link", activeSection === "factions" && "is-active"]} href="factions/">Factions</a>
|
package/src/lib/book.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
2
3
|
import {
|
|
3
4
|
listChapters,
|
|
4
5
|
listEntities,
|
|
@@ -36,6 +37,7 @@ export async function loadHomePageData() {
|
|
|
36
37
|
root,
|
|
37
38
|
book: null,
|
|
38
39
|
chapters: [],
|
|
40
|
+
draftChapterCount: 0,
|
|
39
41
|
characters: [],
|
|
40
42
|
locations: [],
|
|
41
43
|
factions: [],
|
|
@@ -45,9 +47,10 @@ export async function loadHomePageData() {
|
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
const [book, chapters, characters, locations, factions, items, secrets, timelineEvents] = await Promise.all([
|
|
50
|
+
const [book, chapters, draftChapterCount, characters, locations, factions, items, secrets, timelineEvents] = await Promise.all([
|
|
49
51
|
readBook(root),
|
|
50
52
|
listChapters(root),
|
|
53
|
+
countDraftChapters(root),
|
|
51
54
|
listEntities(root, "character"),
|
|
52
55
|
listEntities(root, "location"),
|
|
53
56
|
listEntities(root, "faction"),
|
|
@@ -61,6 +64,7 @@ export async function loadHomePageData() {
|
|
|
61
64
|
root,
|
|
62
65
|
book,
|
|
63
66
|
chapters,
|
|
67
|
+
draftChapterCount,
|
|
64
68
|
characters,
|
|
65
69
|
locations,
|
|
66
70
|
factions,
|
|
@@ -70,6 +74,12 @@ export async function loadHomePageData() {
|
|
|
70
74
|
};
|
|
71
75
|
}
|
|
72
76
|
|
|
77
|
+
async function countDraftChapters(root: string): Promise<number> {
|
|
78
|
+
const draftsRoot = path.join(root, "drafts");
|
|
79
|
+
const entries = await readdir(draftsRoot, { withFileTypes: true }).catch(() => []);
|
|
80
|
+
return entries.filter((entry) => entry.isDirectory()).length;
|
|
81
|
+
}
|
|
82
|
+
|
|
73
83
|
export async function loadChapterPageData(chapterSlug: string) {
|
|
74
84
|
const root = getBookRoot();
|
|
75
85
|
return readChapter(root, chapterSlug);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export type CanonMentionEntry = {
|
|
2
|
+
id: string;
|
|
3
|
+
href: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function buildCanonHrefIndex(entries: CanonMentionEntry[]): Map<string, string> {
|
|
7
|
+
const index = new Map<string, string>();
|
|
8
|
+
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
const normalizedHref = normalizeCanonEntityHref(entry.href);
|
|
11
|
+
if (!normalizedHref) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
index.set(normalizedHref, entry.id);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return index;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveCanonEntryIdFromHref(href: string, entries: CanonMentionEntry[]): string | null {
|
|
22
|
+
const normalizedHref = normalizeCanonEntityHref(href);
|
|
23
|
+
if (!normalizedHref) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return buildCanonHrefIndex(entries).get(normalizedHref) ?? null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeCanonEntityHref(href: string): string | null {
|
|
31
|
+
const strippedOrigin = href
|
|
32
|
+
.trim()
|
|
33
|
+
.replace(/^[a-z]+:\/\/[^/]+/i, "")
|
|
34
|
+
.split(/[?#]/, 1)[0]
|
|
35
|
+
.replace(/\\/g, "/");
|
|
36
|
+
if (!strippedOrigin) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const segments = strippedOrigin
|
|
41
|
+
.split("/")
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.filter((segment) => segment !== "." && segment !== "..");
|
|
44
|
+
|
|
45
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
46
|
+
const current = segments[index]?.toLowerCase();
|
|
47
|
+
if (!current) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (current === "timelines" && segments[index + 1]?.toLowerCase() === "events") {
|
|
52
|
+
const slug = normalizeSlugSegment(segments[index + 2]);
|
|
53
|
+
if (slug) {
|
|
54
|
+
return `timeline/${slug}/`;
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const section = normalizeCanonSection(current);
|
|
60
|
+
if (!section) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const slug = normalizeSlugSegment(segments[index + 1]);
|
|
65
|
+
if (slug) {
|
|
66
|
+
return `${section}/${slug}/`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeCanonSection(segment: string): string | null {
|
|
74
|
+
switch (segment.toLowerCase()) {
|
|
75
|
+
case "character":
|
|
76
|
+
case "characters":
|
|
77
|
+
return "characters";
|
|
78
|
+
case "location":
|
|
79
|
+
case "locations":
|
|
80
|
+
return "locations";
|
|
81
|
+
case "faction":
|
|
82
|
+
case "factions":
|
|
83
|
+
return "factions";
|
|
84
|
+
case "item":
|
|
85
|
+
case "items":
|
|
86
|
+
return "items";
|
|
87
|
+
case "secret":
|
|
88
|
+
case "secrets":
|
|
89
|
+
return "secrets";
|
|
90
|
+
case "timeline":
|
|
91
|
+
case "timeline-event":
|
|
92
|
+
return "timeline";
|
|
93
|
+
default:
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeSlugSegment(segment: string | undefined): string | null {
|
|
99
|
+
if (!segment) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const normalized = segment.replace(/\.md$/i, "").trim().toLowerCase();
|
|
104
|
+
return /^[a-z0-9-]+$/.test(normalized) ? normalized : null;
|
|
105
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { marked } from "marked";
|
|
4
|
+
import { parseNarrariumMarkdownDocument, pathExists, readChapterDraft } from "narrarium";
|
|
5
|
+
import { getBookRoot } from "./book.js";
|
|
6
|
+
|
|
7
|
+
export type WorkshopEntry = {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
body: string;
|
|
11
|
+
status: string;
|
|
12
|
+
tags: string[];
|
|
13
|
+
sourceKind?: string;
|
|
14
|
+
promotedTo?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type WorkshopDocument = {
|
|
18
|
+
path: string;
|
|
19
|
+
title: string;
|
|
20
|
+
bucket: string;
|
|
21
|
+
bodyHtml: string;
|
|
22
|
+
entries: WorkshopEntry[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type WorkshopDraftChapter = {
|
|
26
|
+
slug: string;
|
|
27
|
+
title: string;
|
|
28
|
+
summary: string;
|
|
29
|
+
bodyHtml: string;
|
|
30
|
+
paragraphs: Array<{ slug: string; title: string; summary: string }>;
|
|
31
|
+
ideas: WorkshopDocument | null;
|
|
32
|
+
notes: WorkshopDocument | null;
|
|
33
|
+
promoted: WorkshopDocument | null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export async function loadWorkshopPageData() {
|
|
37
|
+
const root = getBookRoot();
|
|
38
|
+
const ready = await pathExists(path.join(root, "book.md"));
|
|
39
|
+
|
|
40
|
+
if (!ready) {
|
|
41
|
+
return {
|
|
42
|
+
ready: false,
|
|
43
|
+
root,
|
|
44
|
+
global: null,
|
|
45
|
+
draftChapters: [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const [context, ideas, notes, storyDesign, promoted, draftChapters] = await Promise.all([
|
|
50
|
+
readWorkshopDocument(root, "context.md"),
|
|
51
|
+
readWorkshopDocument(root, "ideas.md"),
|
|
52
|
+
readWorkshopDocument(root, "notes.md"),
|
|
53
|
+
readWorkshopDocument(root, "story-design.md"),
|
|
54
|
+
readWorkshopDocument(root, "promoted.md"),
|
|
55
|
+
listDraftChapters(root),
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
ready: true,
|
|
60
|
+
root,
|
|
61
|
+
global: {
|
|
62
|
+
context,
|
|
63
|
+
ideas,
|
|
64
|
+
notes,
|
|
65
|
+
storyDesign,
|
|
66
|
+
promoted,
|
|
67
|
+
},
|
|
68
|
+
draftChapters,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function countDraftChapters(root: string): Promise<number> {
|
|
73
|
+
return (await listDraftChapters(root)).length;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function listDraftChapters(root: string): Promise<WorkshopDraftChapter[]> {
|
|
77
|
+
const draftsRoot = path.join(root, "drafts");
|
|
78
|
+
const entries = await readdir(draftsRoot, { withFileTypes: true }).catch(() => []);
|
|
79
|
+
const slugs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
80
|
+
|
|
81
|
+
const chapters = await Promise.all(
|
|
82
|
+
slugs.map(async (slug) => {
|
|
83
|
+
const chapterFile = path.join(draftsRoot, slug, "chapter.md");
|
|
84
|
+
if (!(await pathExists(chapterFile))) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const chapter = await readChapterDraft(root, slug);
|
|
89
|
+
const [ideas, notes, promoted] = await Promise.all([
|
|
90
|
+
readWorkshopDocument(root, path.posix.join("drafts", slug, "ideas.md")),
|
|
91
|
+
readWorkshopDocument(root, path.posix.join("drafts", slug, "notes.md")),
|
|
92
|
+
readWorkshopDocument(root, path.posix.join("drafts", slug, "promoted.md")),
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
slug,
|
|
97
|
+
title: chapter.metadata.title,
|
|
98
|
+
summary: chapter.metadata.summary ?? "Draft chapter in progress.",
|
|
99
|
+
bodyHtml: await toHtml(chapter.body || "No chapter draft body yet."),
|
|
100
|
+
paragraphs: chapter.paragraphs.map((paragraph) => ({
|
|
101
|
+
slug: path.basename(paragraph.path, ".md"),
|
|
102
|
+
title: paragraph.metadata.title,
|
|
103
|
+
summary: paragraph.metadata.summary ?? "Draft scene.",
|
|
104
|
+
})),
|
|
105
|
+
ideas,
|
|
106
|
+
notes,
|
|
107
|
+
promoted,
|
|
108
|
+
} satisfies WorkshopDraftChapter;
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return chapters.filter((chapter): chapter is WorkshopDraftChapter => Boolean(chapter));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function readWorkshopDocument(root: string, relativePath: string): Promise<WorkshopDocument | null> {
|
|
116
|
+
const absolutePath = path.join(root, relativePath);
|
|
117
|
+
const raw = await readFile(absolutePath, "utf8").catch(() => null);
|
|
118
|
+
if (!raw) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const document = parseNarrariumMarkdownDocument(relativePath, raw);
|
|
123
|
+
const frontmatter = document.frontmatter as Record<string, unknown>;
|
|
124
|
+
const title = typeof frontmatter.title === "string" && frontmatter.title.trim() ? frontmatter.title : relativePath;
|
|
125
|
+
const bucket = typeof frontmatter.bucket === "string" ? frontmatter.bucket : document.kind;
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
path: relativePath,
|
|
129
|
+
title,
|
|
130
|
+
bucket,
|
|
131
|
+
bodyHtml: await toHtml(document.body || "No content yet."),
|
|
132
|
+
entries: readWorkshopEntries(frontmatter),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readWorkshopEntries(frontmatter: Record<string, unknown>): WorkshopEntry[] {
|
|
137
|
+
if (!Array.isArray(frontmatter.entries)) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return frontmatter.entries
|
|
142
|
+
.filter((entry): entry is Record<string, unknown> => Boolean(entry && typeof entry === "object"))
|
|
143
|
+
.map((entry) => ({
|
|
144
|
+
id: typeof entry.id === "string" ? entry.id : "",
|
|
145
|
+
title: typeof entry.title === "string" ? entry.title : "Untitled",
|
|
146
|
+
body: typeof entry.body === "string" ? entry.body : "",
|
|
147
|
+
status: typeof entry.status === "string" ? entry.status : "active",
|
|
148
|
+
tags: Array.isArray(entry.tags) ? entry.tags.filter((tag): tag is string => typeof tag === "string") : [],
|
|
149
|
+
sourceKind: typeof entry.source_kind === "string" ? entry.source_kind : undefined,
|
|
150
|
+
promotedTo: typeof entry.promoted_to === "string" ? entry.promoted_to : undefined,
|
|
151
|
+
}))
|
|
152
|
+
.filter((entry) => entry.id.length > 0)
|
|
153
|
+
.sort((left, right) => left.title.localeCompare(right.title));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function toHtml(markdown: string): Promise<string> {
|
|
157
|
+
const rendered = await marked.parse(markdown);
|
|
158
|
+
return typeof rendered === "string" ? rendered : String(rendered);
|
|
159
|
+
}
|
package/src/pages/index.astro
CHANGED
|
@@ -6,7 +6,7 @@ import { loadAssetFigure } from "../lib/assets";
|
|
|
6
6
|
import { countPublicCanonEntries } from "../lib/public-canon";
|
|
7
7
|
import { isFullCanonMode } from "../lib/reader-mode";
|
|
8
8
|
|
|
9
|
-
const { ready, root, book, chapters, characters, locations, factions, items, secrets, timelineEvents } = await loadHomePageData();
|
|
9
|
+
const { ready, root, book, chapters, draftChapterCount, characters, locations, factions, items, secrets, timelineEvents } = await loadHomePageData();
|
|
10
10
|
const title = book?.frontmatter.title ?? "Narrarium Reader";
|
|
11
11
|
const coverFigure = await loadAssetFigure("book", `${title} cover`);
|
|
12
12
|
const fullCanonMode = isFullCanonMode();
|
|
@@ -129,6 +129,16 @@ const chapterCards = await Promise.all(
|
|
|
129
129
|
<div class="chip-row"><span class="chip">{visibleTimelineCount} visible</span></div>
|
|
130
130
|
</a>
|
|
131
131
|
</article>
|
|
132
|
+
{fullCanonMode && (
|
|
133
|
+
<article class="catalog-card">
|
|
134
|
+
<a href="workshop/">
|
|
135
|
+
<span class="browse-card-icon">🧭</span>
|
|
136
|
+
<h3 class="chapter-title">Workshop</h3>
|
|
137
|
+
<div class="chapter-meta">Inspect drafts, ideas, notes, story design, and promoted items.</div>
|
|
138
|
+
<div class="chip-row"><span class="chip">{draftChapterCount} draft chapters</span></div>
|
|
139
|
+
</a>
|
|
140
|
+
</article>
|
|
141
|
+
)}
|
|
132
142
|
</div>
|
|
133
143
|
</section>
|
|
134
144
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BaseLayout from "../../layouts/BaseLayout.astro";
|
|
3
|
+
import { isFullCanonMode } from "../../lib/reader-mode";
|
|
4
|
+
import { loadWorkshopPageData } from "../../lib/workshop";
|
|
5
|
+
|
|
6
|
+
const fullCanonMode = isFullCanonMode();
|
|
7
|
+
const { ready, global, draftChapters } = await loadWorkshopPageData();
|
|
8
|
+
|
|
9
|
+
const globalDocuments = global
|
|
10
|
+
? [
|
|
11
|
+
{ label: "Context", document: global.context },
|
|
12
|
+
{ label: "Story Design", document: global.storyDesign },
|
|
13
|
+
{ label: "Ideas", document: global.ideas },
|
|
14
|
+
{ label: "Notes", document: global.notes },
|
|
15
|
+
{ label: "Promoted", document: global.promoted },
|
|
16
|
+
]
|
|
17
|
+
: [];
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<BaseLayout title="Workshop" description="Inspect drafts, ideas, notes, and story design." activeSection="workshop">
|
|
21
|
+
<section class="hero">
|
|
22
|
+
<p class="eyebrow">Workshop</p>
|
|
23
|
+
<h1>Drafts, Ideas, and Notes</h1>
|
|
24
|
+
<p class="lede">
|
|
25
|
+
{fullCanonMode
|
|
26
|
+
? "Review global planning material, draft chapters, and promoted work items from one author-facing workspace."
|
|
27
|
+
: "Workshop content is available only in full canon mode."}
|
|
28
|
+
</p>
|
|
29
|
+
</section>
|
|
30
|
+
|
|
31
|
+
{!ready ? (
|
|
32
|
+
<div class="empty">No Narrarium book repository is connected yet.</div>
|
|
33
|
+
) : !fullCanonMode ? (
|
|
34
|
+
<div class="empty">Switch the reader to full canon mode to inspect workshop material such as drafts, ideas, notes, and story design.</div>
|
|
35
|
+
) : (
|
|
36
|
+
<>
|
|
37
|
+
<section class="section">
|
|
38
|
+
<h2>Global Workspace</h2>
|
|
39
|
+
<div class="chapter-list">
|
|
40
|
+
{globalDocuments.map(({ label, document }) => (
|
|
41
|
+
<article class="chapter-card workspace-card">
|
|
42
|
+
<div class="chapter-number">{label}</div>
|
|
43
|
+
<h3 class="chapter-title">{document?.title ?? `${label} unavailable`}</h3>
|
|
44
|
+
<div class="chapter-meta">{document ? document.path : "File not present in this book yet."}</div>
|
|
45
|
+
{document ? (
|
|
46
|
+
<>
|
|
47
|
+
<div class="chip-row"><span class="chip">{document.entries.length} entries</span></div>
|
|
48
|
+
<div class="prose" set:html={document.bodyHtml}></div>
|
|
49
|
+
{document.entries.length > 0 && (
|
|
50
|
+
<div class="workspace-entry-list">
|
|
51
|
+
{document.entries.map((entry) => (
|
|
52
|
+
<article class="workspace-entry">
|
|
53
|
+
<div class="chip-row">
|
|
54
|
+
<span class="chip">{entry.status}</span>
|
|
55
|
+
{entry.tags.map((tag) => (
|
|
56
|
+
<span class="chip">{tag}</span>
|
|
57
|
+
))}
|
|
58
|
+
{entry.promotedTo && <span class="chip">to {entry.promotedTo}</span>}
|
|
59
|
+
</div>
|
|
60
|
+
<h4>{entry.title}</h4>
|
|
61
|
+
<p>{entry.body || "No details yet."}</p>
|
|
62
|
+
</article>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</>
|
|
67
|
+
) : null}
|
|
68
|
+
</article>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
</section>
|
|
72
|
+
|
|
73
|
+
<section class="section">
|
|
74
|
+
<h2>Draft Chapters</h2>
|
|
75
|
+
{draftChapters.length > 0 ? (
|
|
76
|
+
<div class="chapter-list">
|
|
77
|
+
{draftChapters.map((chapter) => (
|
|
78
|
+
<article class="chapter-card workspace-card">
|
|
79
|
+
<div class="chapter-number">Draft</div>
|
|
80
|
+
<h3 class="chapter-title">{chapter.title}</h3>
|
|
81
|
+
<div class="chapter-meta">{chapter.summary}</div>
|
|
82
|
+
<div class="chip-row">
|
|
83
|
+
<span class="chip">{chapter.paragraphs.length} scenes</span>
|
|
84
|
+
<span class="chip">{chapter.ideas?.entries.length ?? 0} ideas</span>
|
|
85
|
+
<span class="chip">{chapter.notes?.entries.length ?? 0} notes</span>
|
|
86
|
+
<span class="chip">{chapter.promoted?.entries.length ?? 0} promoted</span>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="prose" set:html={chapter.bodyHtml}></div>
|
|
89
|
+
{chapter.paragraphs.length > 0 && (
|
|
90
|
+
<div class="workspace-subsection">
|
|
91
|
+
<h4>Draft Scenes</h4>
|
|
92
|
+
<div class="workspace-entry-list">
|
|
93
|
+
{chapter.paragraphs.map((paragraph) => (
|
|
94
|
+
<article class="workspace-entry">
|
|
95
|
+
<h5>{paragraph.title}</h5>
|
|
96
|
+
<p>{paragraph.summary}</p>
|
|
97
|
+
</article>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
{[chapter.ideas, chapter.notes, chapter.promoted].map(
|
|
104
|
+
(document, index) =>
|
|
105
|
+
document && (
|
|
106
|
+
<div class="workspace-subsection">
|
|
107
|
+
<h4>{index === 0 ? "Ideas" : index === 1 ? "Notes" : "Promoted"}</h4>
|
|
108
|
+
<div class="prose" set:html={document.bodyHtml}></div>
|
|
109
|
+
{document.entries.length > 0 && (
|
|
110
|
+
<div class="workspace-entry-list">
|
|
111
|
+
{document.entries.map((entry) => (
|
|
112
|
+
<article class="workspace-entry">
|
|
113
|
+
<div class="chip-row">
|
|
114
|
+
<span class="chip">{entry.status}</span>
|
|
115
|
+
{entry.tags.map((tag) => (
|
|
116
|
+
<span class="chip">{tag}</span>
|
|
117
|
+
))}
|
|
118
|
+
{entry.promotedTo && <span class="chip">to {entry.promotedTo}</span>}
|
|
119
|
+
</div>
|
|
120
|
+
<h5>{entry.title}</h5>
|
|
121
|
+
<p>{entry.body || "No details yet."}</p>
|
|
122
|
+
</article>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
),
|
|
128
|
+
)}
|
|
129
|
+
</article>
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
) : (
|
|
133
|
+
<div class="empty">No chapter drafts exist yet.</div>
|
|
134
|
+
)}
|
|
135
|
+
</section>
|
|
136
|
+
</>
|
|
137
|
+
)}
|
|
138
|
+
</BaseLayout>
|
package/src/styles/global.css
CHANGED
|
@@ -248,6 +248,42 @@ body.tts-enabled .shell {
|
|
|
248
248
|
flex-wrap: wrap;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
.workspace-card {
|
|
252
|
+
width: 100%;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.workspace-entry-list {
|
|
256
|
+
display: grid;
|
|
257
|
+
gap: 0.85rem;
|
|
258
|
+
margin-top: 1rem;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.workspace-entry {
|
|
262
|
+
padding: 1rem 1.1rem;
|
|
263
|
+
border: 1px solid var(--line);
|
|
264
|
+
border-radius: 14px;
|
|
265
|
+
background: var(--surface-muted);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.workspace-entry h4,
|
|
269
|
+
.workspace-entry h5 {
|
|
270
|
+
margin: 0.35rem 0 0.45rem;
|
|
271
|
+
font-size: 1rem;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.workspace-entry p {
|
|
275
|
+
margin: 0;
|
|
276
|
+
color: var(--text-muted);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.workspace-subsection {
|
|
280
|
+
margin-top: 1.4rem;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.workspace-subsection h4 {
|
|
284
|
+
margin: 0 0 0.75rem;
|
|
285
|
+
}
|
|
286
|
+
|
|
251
287
|
/* ─── Icon buttons (masthead controls) ──────────────────────── */
|
|
252
288
|
.masthead-btn {
|
|
253
289
|
display: inline-flex;
|