@heroiclands/package-build 0.6.1 → 3.0.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.
Files changed (127) hide show
  1. package/CHANGELOG-content-build.md +965 -0
  2. package/CHANGELOG.md +58 -0
  3. package/CONTENT.md +824 -0
  4. package/MIGRATING.md +127 -0
  5. package/README.md +29 -17
  6. package/bin/content-build.mjs +1082 -0
  7. package/bin/package-build.mjs +2 -2
  8. package/bin/report.mjs +2 -2
  9. package/config.mjs +21 -18
  10. package/content-config.mjs +1424 -0
  11. package/coverage.mjs +1 -1
  12. package/e2e.mjs +1 -1
  13. package/engine/abbreviations.mjs +0 -0
  14. package/engine/base-compiler.mjs +608 -0
  15. package/engine/code-fences.mjs +311 -0
  16. package/engine/compendiums.mjs +353 -0
  17. package/engine/content-address.mjs +141 -0
  18. package/engine/content-links.mjs +438 -0
  19. package/engine/content-lint.mjs +204 -0
  20. package/engine/content-package.mjs +67 -0
  21. package/engine/content-slug.mjs +169 -0
  22. package/engine/content-tables.mjs +1216 -0
  23. package/engine/content-tree.mjs +56 -0
  24. package/engine/diagnostics.mjs +270 -0
  25. package/engine/field-reference.mjs +242 -0
  26. package/engine/field-spec.mjs +218 -0
  27. package/engine/foreign-catalog.mjs +450 -0
  28. package/engine/foreign-manifests.mjs +129 -0
  29. package/engine/frontmatter-lint.mjs +343 -0
  30. package/engine/frontmatter.mjs +258 -0
  31. package/engine/generate.mjs +305 -0
  32. package/engine/helpers.mjs +809 -0
  33. package/engine/ids.mjs +192 -0
  34. package/engine/index.mjs +142 -0
  35. package/engine/item-docs.mjs +153 -0
  36. package/engine/item-registry.mjs +129 -0
  37. package/engine/journals.mjs +374 -0
  38. package/engine/kb-manifest.mjs +473 -0
  39. package/engine/macros.mjs +333 -0
  40. package/engine/manifest-emit.mjs +339 -0
  41. package/engine/map-notes.mjs +1302 -0
  42. package/engine/pack-config.mjs +479 -0
  43. package/engine/pack-router.mjs +254 -0
  44. package/engine/prose-config.mjs +238 -0
  45. package/engine/prose-lint.mjs +294 -0
  46. package/engine/region-events.mjs +79 -0
  47. package/engine/scene-levels.mjs +420 -0
  48. package/engine/scenes.mjs +528 -0
  49. package/engine/site-build.mjs +778 -0
  50. package/engine/site-index.mjs +332 -0
  51. package/engine/web-wikilinks.mjs +330 -0
  52. package/engine/wikilink-syntax.mjs +105 -0
  53. package/engine/wikilinks.mjs +645 -0
  54. package/index.mjs +42 -25
  55. package/lang.mjs +1 -1
  56. package/manifest.mjs +79 -5
  57. package/markdownlint-config.mjs +45 -0
  58. package/package.json +78 -35
  59. package/prettier-config.mjs +34 -0
  60. package/sohl/actors.mjs +581 -0
  61. package/sohl/affiliation-standings.mjs +44 -0
  62. package/sohl/being-info.mjs +173 -0
  63. package/sohl/default-item-art.mjs +82 -0
  64. package/sohl/index.mjs +55 -0
  65. package/sohl/item-builders.mjs +114 -0
  66. package/sohl/item-fields.mjs +867 -0
  67. package/sohl/items.mjs +197 -0
  68. package/sohl/kb-passes.mjs +207 -0
  69. package/sohl/note-schemas.mjs +322 -0
  70. package/templates.mjs +1 -1
  71. package/types/config.d.mts +1 -1
  72. package/types/content-config.d.mts +571 -0
  73. package/types/engine/abbreviations.d.mts +44 -0
  74. package/types/engine/base-compiler.d.mts +320 -0
  75. package/types/engine/code-fences.d.mts +102 -0
  76. package/types/engine/compendiums.d.mts +77 -0
  77. package/types/engine/content-address.d.mts +61 -0
  78. package/types/engine/content-links.d.mts +87 -0
  79. package/types/engine/content-lint.d.mts +47 -0
  80. package/types/engine/content-package.d.mts +36 -0
  81. package/types/engine/content-slug.d.mts +59 -0
  82. package/types/engine/content-tables.d.mts +131 -0
  83. package/types/engine/content-tree.d.mts +12 -0
  84. package/types/engine/diagnostics.d.mts +152 -0
  85. package/types/engine/field-reference.d.mts +20 -0
  86. package/types/engine/field-spec.d.mts +181 -0
  87. package/types/engine/foreign-catalog.d.mts +103 -0
  88. package/types/engine/foreign-manifests.d.mts +43 -0
  89. package/types/engine/frontmatter-lint.d.mts +74 -0
  90. package/types/engine/frontmatter.d.mts +99 -0
  91. package/types/engine/generate.d.mts +56 -0
  92. package/types/engine/helpers.d.mts +348 -0
  93. package/types/engine/ids.d.mts +126 -0
  94. package/types/engine/index.d.mts +33 -0
  95. package/types/engine/item-docs.d.mts +73 -0
  96. package/types/engine/item-registry.d.mts +57 -0
  97. package/types/engine/journals.d.mts +121 -0
  98. package/types/engine/kb-manifest.d.mts +214 -0
  99. package/types/engine/macros.d.mts +201 -0
  100. package/types/engine/manifest-emit.d.mts +137 -0
  101. package/types/engine/map-notes.d.mts +283 -0
  102. package/types/engine/pack-config.d.mts +77 -0
  103. package/types/engine/pack-router.d.mts +53 -0
  104. package/types/engine/prose-config.d.mts +121 -0
  105. package/types/engine/prose-lint.d.mts +59 -0
  106. package/types/engine/region-events.d.mts +43 -0
  107. package/types/engine/scene-levels.d.mts +36 -0
  108. package/types/engine/scenes.d.mts +48 -0
  109. package/types/engine/site-build.d.mts +214 -0
  110. package/types/engine/site-index.d.mts +141 -0
  111. package/types/engine/web-wikilinks.d.mts +67 -0
  112. package/types/engine/wikilink-syntax.d.mts +101 -0
  113. package/types/engine/wikilinks.d.mts +172 -0
  114. package/types/index.d.mts +3 -0
  115. package/types/manifest.d.mts +52 -4
  116. package/types/markdownlint-config.d.mts +8 -0
  117. package/types/prettier-config.d.mts +1 -0
  118. package/types/sohl/actors.d.mts +65 -0
  119. package/types/sohl/affiliation-standings.d.mts +22 -0
  120. package/types/sohl/being-info.d.mts +78 -0
  121. package/types/sohl/default-item-art.d.mts +28 -0
  122. package/types/sohl/index.d.mts +7 -0
  123. package/types/sohl/item-builders.d.mts +11 -0
  124. package/types/sohl/item-fields.d.mts +20 -0
  125. package/types/sohl/items.d.mts +19 -0
  126. package/types/sohl/kb-passes.d.mts +71 -0
  127. package/types/sohl/note-schemas.d.mts +7 -0
package/sohl/items.mjs ADDED
@@ -0,0 +1,197 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * Items pack compiler — produces JSON pack files for the single "items"
16
+ * Foundry compendium from markdown notes in the `assets/content/` tree.
17
+ *
18
+ * The content root (`contentBase`) is walked recursively; any `.md` file whose
19
+ * frontmatter declares a recognized `type:` is compiled into one JSON entry.
20
+ * Files outside the whitelist (blog posts, rules text, templates) are
21
+ * silently skipped.
22
+ *
23
+ * Type-specific `system.*` fields come from the nested `sohl:` block in
24
+ * vault frontmatter, read via `sohlField()`. The body is **not** rendered
25
+ * here: it compiles into the item's doc in the journals pack, and
26
+ * `system.docHtml` becomes a pointer to it (see `item-docs.mjs`). Folder
27
+ * assignment is deferred — every item currently emits `folder: null`.
28
+ *
29
+ * Not a standalone script — exports the `Items` compiler class, imported and
30
+ * driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
31
+ *
32
+ * The walk itself — filtering by package and type, skipping drafts,
33
+ * expanding tables, converting wikilinks, writing the JSON and counting
34
+ * errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
35
+ * states only what makes this pass its own (#1509).
36
+ */
37
+
38
+ import log from "loglevel";
39
+
40
+ import {
41
+ sohlField,
42
+ resolveName,
43
+ resolveImg,
44
+ defaultStats,
45
+ withArchetypeFlag,
46
+ } from "../engine/helpers.mjs";
47
+ import { BasePackCompiler } from "../engine/base-compiler.mjs";
48
+ // Per-type default art lives in one framework-free module shared with the
49
+ // runtime (`SohlItem.getDefaultArtwork`), so the two can't drift — see #932.
50
+ // It lives in the build package, not `src/`: a relative path out of the package
51
+ // resolves to garbage once this pipeline runs from `node_modules` (#1510).
52
+ import { journalPageId, splitPages } from "../engine/journals.mjs";
53
+ import { foundryPackageId } from "../engine/content-package.mjs";
54
+ import { itemDocEntryId, itemDocPointer } from "../engine/item-docs.mjs";
55
+ // The whitelist and the per-type `system` builders both come from the resolved
56
+ // configuration, so the types this pass claims and the builders it compiles
57
+ // them with are one table — the consuming repository's, not this package's
58
+ // (#1504/#1563).
59
+ import { itemTypes, itemBuilder, itemArt } from "../engine/item-registry.mjs";
60
+
61
+ /**
62
+ * The description an item carries: a pointer to its **item doc**, the
63
+ * JournalEntry the journals pass compiles this same body into (#1348).
64
+ *
65
+ * The prose is not rendered here at all. Carrying it would duplicate it onto
66
+ * every actor holding the item — 7.59 MB of copies across the actors pack, of
67
+ * which 133 KB was distinct — where a link is 60 bytes and always current. The
68
+ * two passes derive the target from the note's own id, so neither has to see
69
+ * the other's output; both split the *converted* markdown, so an H1 carrying a
70
+ * wikilink names the same page on both sides.
71
+ *
72
+ * An item with no prose points at nothing, exactly as the journals pass writes
73
+ * no entry for it.
74
+ *
75
+ * @param {string} markdown - The note body, tables expanded and wikilinks
76
+ * resolved.
77
+ * @param {object} fm - The note's frontmatter.
78
+ * @param {string} name - The item's name.
79
+ * @returns {string} The pointer, or "" for a note with no body.
80
+ */
81
+ function itemDescription(markdown, fm, name) {
82
+ if (!String(markdown).trim()) return "";
83
+ const [leadPage] = splitPages(markdown, name);
84
+ const pageId = journalPageId(itemDocEntryId(fm.id), leadPage, 0);
85
+ return itemDocPointer(foundryPackageId(), fm.id, name, pageId);
86
+ }
87
+
88
+ /**
89
+ * Build the `system.*` fields shared by every item type:
90
+ * shortcode, actionDefs, notes, docHtml.
91
+ */
92
+ function commonSystem(fm, description) {
93
+ return {
94
+ shortcode: fm.shortcode,
95
+ actionDefs: Array.isArray(fm.actionDefs) ? fm.actionDefs : [],
96
+ notes: "",
97
+ docHtml: description || "",
98
+ };
99
+ }
100
+
101
+ /* -------------------------------------------------------------------- */
102
+ /* Synthesized Active Effects */
103
+ /* -------------------------------------------------------------------- */
104
+
105
+ /* -------------------------------------------------------------------- */
106
+ /* Compiler */
107
+ /* -------------------------------------------------------------------- */
108
+
109
+ export class Items extends BasePackCompiler {
110
+ static id = "items";
111
+ static label = "item";
112
+
113
+ /**
114
+ * How many of each item type this pass wrote, for the summary. Every type
115
+ * is present from the start so the tally reads as a census of the
116
+ * whitelist rather than of what happened to compile.
117
+ *
118
+ * @type {Record<string, number>}
119
+ */
120
+ counts = Object.fromEntries([...itemTypes()].map((t) => [t, 0]));
121
+
122
+ /**
123
+ * Every content type that compiles into an item.
124
+ *
125
+ * @param {object} fm - The note's frontmatter.
126
+ * @returns {boolean} True for a whitelisted item type.
127
+ */
128
+ selects(fm) {
129
+ return Boolean(fm.type) && itemTypes().has(fm.type);
130
+ }
131
+
132
+ /** An item is named by its own type in the log, not by "item". */
133
+ noteLabel(fm) {
134
+ return fm.type;
135
+ }
136
+
137
+ /**
138
+ * Construct the full compendium envelope for one item, including
139
+ * synthesized Active Effects where applicable.
140
+ *
141
+ * @param {object} fm - The note's frontmatter.
142
+ * @param {string} markdown - The body, tables expanded and wikilinks
143
+ * resolved.
144
+ * @returns {object} The item document, keyed for the pack.
145
+ */
146
+ buildEntry(fm, markdown) {
147
+ const type = fm.type;
148
+ const name = resolveName(fm);
149
+ const description = itemDescription(markdown, fm, name);
150
+ const id = fm.id;
151
+ const system = {
152
+ ...commonSystem(fm, description),
153
+ ...itemBuilder(type)(fm),
154
+ };
155
+
156
+ const effects = Array.isArray(fm.effects) ? [...fm.effects] : [];
157
+
158
+ const folderId = sohlField(fm, "folder", null);
159
+ const folder = this.folderResolver(folderId);
160
+
161
+ return {
162
+ name,
163
+ type,
164
+ img: resolveImg(fm.img) || itemArt(type),
165
+ _id: id,
166
+ system,
167
+ effects,
168
+ // `sohl.archetype` (required nullable number) drives
169
+ // `flags.sohl.docArchetype` (#640 / archetype contract #604).
170
+ flags: withArchetypeFlag(fm, fm.flags, `item "${name}"`),
171
+ _stats: defaultStats(),
172
+ ownership: { default: 0 },
173
+ folder,
174
+ _key: `!items!${id}`,
175
+ };
176
+ }
177
+
178
+ /** @inheritdoc */
179
+ onCompiled(fm) {
180
+ this.counts[fm.type]++;
181
+ }
182
+
183
+ /** @inheritdoc */
184
+ reportCompiled(stats) {
185
+ log.info(`Compiled ${stats.compiled} items:`);
186
+ for (const [t, n] of Object.entries(this.counts)) {
187
+ if (n > 0) log.info(` ${t}: ${n}`);
188
+ }
189
+ }
190
+
191
+ /** @inheritdoc */
192
+ reportDetail(stats) {
193
+ log.debug(
194
+ `Skipped ${stats.skippedOther} non-item file(s) (no recognized type)`,
195
+ );
196
+ }
197
+ }
@@ -0,0 +1,207 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * The `sohl` knowledgebase's own body passes (#63).
16
+ *
17
+ * `content-build site` publishes a content tree as a website, and almost all of
18
+ * that job is the same for every package. These two rewrites are not: they are
19
+ * driven by a TypeDoc symbol map and a repository layout only this package has,
20
+ * and #63 rules them explicitly per-consumer.
21
+ *
22
+ * They live here rather than in a script in the consuming repository for the
23
+ * same reason `sohl/item-builders.mjs` and `sohl/being-info.mjs` do: a
24
+ * configuration is data and cannot hold a function, so a package-specific table
25
+ * of code is **named** from configuration and resolved from a registry. What is
26
+ * package-specific is the code; what is repository-specific — where the symbol
27
+ * map sits, what the API is served at, which GitHub tree to link into — is
28
+ * options, supplied beside the name.
29
+ *
30
+ * Neither pass ever fails a build. A `{@link}` the map does not know degrades to
31
+ * a code span, and a relative link that resolves outside the documentation tree
32
+ * becomes a GitHub blob URL. Both are legible to a reader; a broken link or a
33
+ * failed build for a syntax example in prose would not be.
34
+ *
35
+ * @module
36
+ */
37
+
38
+ import fs from "node:fs";
39
+ import path from "node:path";
40
+
41
+ /**
42
+ * Reads the TypeDoc symbol map, or an empty one.
43
+ *
44
+ * Absent is not an error: the map is generated by `npm run docs`, and a
45
+ * knowledgebase build that runs before it — or in a checkout that has never run
46
+ * it — should publish prose with `{@link}` tags degraded to code spans rather
47
+ * than refuse to build.
48
+ *
49
+ * @param {string|undefined} file - Path to the map, if configured.
50
+ * @returns {Record<string, string>} Qualified name → API page path.
51
+ */
52
+ function readSymbolMap(file) {
53
+ if (!file) return {};
54
+ try {
55
+ return JSON.parse(fs.readFileSync(file, "utf8"));
56
+ } catch {
57
+ return {};
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Resolves inline TypeDoc `{@link}` / `{@linkcode}` / `{@linkplain}` tags
63
+ * against the API symbol map.
64
+ *
65
+ * - A known target becomes a Markdown link to its page on the API site;
66
+ * `@linkcode` renders the text as code.
67
+ * - Optional display text (`{@link Target | text}` or `{@link Target text}`)
68
+ * wins; otherwise the last dotted segment is the link text.
69
+ * - An external URL target becomes a plain Markdown link.
70
+ * - Anything the map does not know — `{@link Symbol.member}` written as a syntax
71
+ * example, say — degrades to a code span. Never a broken link, never a build
72
+ * failure.
73
+ *
74
+ * @param {string} body - The markdown body.
75
+ * @param {Record<string, string>} symbols - The symbol map.
76
+ * @param {string} apiBase - Where the generated API documentation is served.
77
+ * @returns {string} The body with every tag resolved.
78
+ */
79
+ export function resolveApiLinks(body, symbols, apiBase) {
80
+ return body.replace(
81
+ /\{@link(code|plain)?\s+([^}]+)\}/g,
82
+ (_m, kind, inner) => {
83
+ inner = inner.trim();
84
+ let target, text;
85
+ const pipe = inner.indexOf("|");
86
+ const space = inner.search(/\s/);
87
+ if (pipe !== -1) {
88
+ target = inner.slice(0, pipe).trim();
89
+ text = inner.slice(pipe + 1).trim();
90
+ } else if (space !== -1) {
91
+ target = inner.slice(0, space);
92
+ text = inner.slice(space + 1).trim();
93
+ } else {
94
+ target = inner;
95
+ text = "";
96
+ }
97
+
98
+ if (/^https?:\/\//.test(target)) {
99
+ return `[${text || target}](${target})`;
100
+ }
101
+
102
+ const url = symbols[target];
103
+ const display = text || target.split(".").pop();
104
+ if (url) {
105
+ const href = apiBase + url;
106
+ return kind === "code" ?
107
+ `[\`${display}\`](${href})`
108
+ : `[${display}](${href})`;
109
+ }
110
+ return kind === "plain" ? display : `\`${display}\``;
111
+ },
112
+ );
113
+ }
114
+
115
+ /**
116
+ * Rewrites the relative links in a developer-doc body so they resolve on the
117
+ * published site.
118
+ *
119
+ * Developer docs are authored to link one another and the source tree with
120
+ * repository-relative paths, and neither target exists at the same path once
121
+ * rendered. Each link is resolved against the doc's own location:
122
+ *
123
+ * - a `*.md` link landing inside the documentation tree becomes the published
124
+ * route, preserving any `#anchor`; a `README` is its directory's landing, so
125
+ * that segment is dropped.
126
+ * - anything else — source, templates, a repository-root `*.md` — becomes its
127
+ * GitHub blob URL.
128
+ *
129
+ * Absolute URLs, anchor-only links, `mailto:` and site-root links are untouched.
130
+ *
131
+ * @param {string} body - The markdown body.
132
+ * @param {string} docRel - The doc's path relative to the documentation tree.
133
+ * @param {object} options - `{ repoRoot, docsSrc, docsRel, route, blob }`.
134
+ * @returns {string} The body with every relative link rewritten.
135
+ */
136
+ export function rewriteRepoLinks(body, docRel, options) {
137
+ const { repoRoot, docsSrc, docsRel, route, blob } = options;
138
+ const docDir = path.dirname(docRel);
139
+ return body.replace(/\]\(([^)]+)\)/g, (whole, raw) => {
140
+ // Peel an optional link title: [text](url "title").
141
+ const sp = raw.search(/\s/);
142
+ const href = sp === -1 ? raw : raw.slice(0, sp);
143
+ const title = sp === -1 ? "" : raw.slice(sp);
144
+ if (/^(https?:|mailto:|tel:|#|\/)/.test(href)) return whole;
145
+
146
+ const hash = href.indexOf("#");
147
+ const filePart = hash === -1 ? href : href.slice(0, hash);
148
+ const anchor = hash === -1 ? "" : href.slice(hash);
149
+ if (!filePart) return whole;
150
+
151
+ const repoRel = path
152
+ .relative(repoRoot, path.resolve(docsSrc, docDir, filePart))
153
+ .replace(/\\/g, "/");
154
+
155
+ let out;
156
+ if (repoRel.startsWith(`${docsRel}/`) && repoRel.endsWith(".md")) {
157
+ const rel2 = repoRel.slice(docsRel.length + 1, -3).toLowerCase();
158
+ const devPath =
159
+ path.basename(rel2) === "readme" ?
160
+ path.posix.dirname(rel2)
161
+ : rel2;
162
+ out = `${route}${devPath === "." ? "" : `${devPath}/`}${anchor}`;
163
+ } else {
164
+ out = `${blob}${repoRel}${anchor}`;
165
+ }
166
+ return `](${out}${title})`;
167
+ });
168
+ }
169
+
170
+ /**
171
+ * The `sohl` knowledgebase pass bundle, built from its options.
172
+ *
173
+ * A pass bundle is two optional hooks the page renderer calls around wikilink
174
+ * resolution, and the order matters:
175
+ *
176
+ * - `beforeLinks` runs on every page, before wikilinks resolve, because a
177
+ * `{@link}` tag may sit inside prose a wikilink also touches.
178
+ * - `afterLinks` runs only on pages from an **extra tree** — the documentation
179
+ * tree — because repository-relative links are a property of how those pages
180
+ * are authored, not of content notes.
181
+ *
182
+ * Both run inside code-fence protection, so neither can rewrite a fenced example.
183
+ *
184
+ * @param {object} options - Resolved from `site.passOptions`.
185
+ * @param {string} [options.symbolMap] - Path to the TypeDoc symbol map.
186
+ * @param {string} [options.apiBase] - Where the API documentation is served.
187
+ * @param {string} [options.blob] - GitHub blob base for repository files.
188
+ * @param {string} options.repoRoot - The repository root, for relative paths.
189
+ * @returns {{beforeLinks: Function, afterLinks: Function}} The bundle.
190
+ */
191
+ export function sohlKbPass(options) {
192
+ const symbols = readSymbolMap(options.symbolMap);
193
+ const apiBase = options.apiBase ?? "";
194
+ return {
195
+ beforeLinks: (text) => resolveApiLinks(text, symbols, apiBase),
196
+ afterLinks: (text, page) =>
197
+ page.tree ?
198
+ rewriteRepoLinks(text, page.rel, {
199
+ repoRoot: options.repoRoot,
200
+ docsSrc: page.tree.from,
201
+ docsRel: page.tree.rel,
202
+ route: page.tree.route,
203
+ blob: options.blob ?? "",
204
+ })
205
+ : text,
206
+ };
207
+ }