okf-catalog 0.1.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 (107) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +10 -0
  4. package/README.md +87 -0
  5. package/dist/bundle/cat-file.js +94 -0
  6. package/dist/bundle/cat-file.js.map +1 -0
  7. package/dist/bundle/contract.js +60 -0
  8. package/dist/bundle/contract.js.map +1 -0
  9. package/dist/bundle/frontmatter.js +106 -0
  10. package/dist/bundle/frontmatter.js.map +1 -0
  11. package/dist/bundle/git-tree.js +109 -0
  12. package/dist/bundle/git-tree.js.map +1 -0
  13. package/dist/bundle/index-file.js +90 -0
  14. package/dist/bundle/index-file.js.map +1 -0
  15. package/dist/bundle/links.js +57 -0
  16. package/dist/bundle/links.js.map +1 -0
  17. package/dist/bundle/load.js +363 -0
  18. package/dist/bundle/load.js.map +1 -0
  19. package/dist/bundle/manifest.js +95 -0
  20. package/dist/bundle/manifest.js.map +1 -0
  21. package/dist/bundle/markdown.js +210 -0
  22. package/dist/bundle/markdown.js.map +1 -0
  23. package/dist/bundle/model.js +7 -0
  24. package/dist/bundle/model.js.map +1 -0
  25. package/dist/bundle/page.js +374 -0
  26. package/dist/bundle/page.js.map +1 -0
  27. package/dist/bundle/paths.js +36 -0
  28. package/dist/bundle/paths.js.map +1 -0
  29. package/dist/bundle/reserved.js +56 -0
  30. package/dist/bundle/reserved.js.map +1 -0
  31. package/dist/bundle/timestamp.js +49 -0
  32. package/dist/bundle/timestamp.js.map +1 -0
  33. package/dist/catalog/model.js +32 -0
  34. package/dist/catalog/model.js.map +1 -0
  35. package/dist/catalog/nearest.js +56 -0
  36. package/dist/catalog/nearest.js.map +1 -0
  37. package/dist/catalog/outputs.js +438 -0
  38. package/dist/catalog/outputs.js.map +1 -0
  39. package/dist/catalog/provenance.js +37 -0
  40. package/dist/catalog/provenance.js.map +1 -0
  41. package/dist/catalog/runtime.js +2 -0
  42. package/dist/catalog/runtime.js.map +1 -0
  43. package/dist/catalog/text.js +140 -0
  44. package/dist/catalog/text.js.map +1 -0
  45. package/dist/cli.js +81 -0
  46. package/dist/cli.js.map +1 -0
  47. package/dist/commands/check.js +155 -0
  48. package/dist/commands/check.js.map +1 -0
  49. package/dist/commands/pack.js +182 -0
  50. package/dist/commands/pack.js.map +1 -0
  51. package/dist/commands/serve.js +452 -0
  52. package/dist/commands/serve.js.map +1 -0
  53. package/dist/config/company-config.js +272 -0
  54. package/dist/config/company-config.js.map +1 -0
  55. package/dist/derive/derived-document.js +42 -0
  56. package/dist/derive/derived-document.js.map +1 -0
  57. package/dist/engine/qmd-render.js +54 -0
  58. package/dist/engine/qmd-render.js.map +1 -0
  59. package/dist/engine/qmd.js +241 -0
  60. package/dist/engine/qmd.js.map +1 -0
  61. package/dist/fs/cache-dir.js +140 -0
  62. package/dist/fs/cache-dir.js.map +1 -0
  63. package/dist/fs/company-lock.js +162 -0
  64. package/dist/fs/company-lock.js.map +1 -0
  65. package/dist/fs/walk.js +168 -0
  66. package/dist/fs/walk.js.map +1 -0
  67. package/dist/log.js +65 -0
  68. package/dist/log.js.map +1 -0
  69. package/dist/mcp/server.js +30 -0
  70. package/dist/mcp/server.js.map +1 -0
  71. package/dist/mcp/stdio.js +45 -0
  72. package/dist/mcp/stdio.js.map +1 -0
  73. package/dist/mcp/tools.js +249 -0
  74. package/dist/mcp/tools.js.map +1 -0
  75. package/dist/report/report.js +43 -0
  76. package/dist/report/report.js.map +1 -0
  77. package/dist/search/engine.js +2 -0
  78. package/dist/search/engine.js.map +1 -0
  79. package/dist/search/query.js +91 -0
  80. package/dist/search/query.js.map +1 -0
  81. package/dist/search/search.js +224 -0
  82. package/dist/search/search.js.map +1 -0
  83. package/dist/search/snippet.js +95 -0
  84. package/dist/search/snippet.js.map +1 -0
  85. package/dist/serve/poller.js +119 -0
  86. package/dist/serve/poller.js.map +1 -0
  87. package/dist/serve/runtime.js +390 -0
  88. package/dist/serve/runtime.js.map +1 -0
  89. package/dist/source/git-runner.js +232 -0
  90. package/dist/source/git-runner.js.map +1 -0
  91. package/dist/source/git.js +457 -0
  92. package/dist/source/git.js.map +1 -0
  93. package/dist/source/local.js +20 -0
  94. package/dist/source/local.js.map +1 -0
  95. package/dist/source/source.js +2 -0
  96. package/dist/source/source.js.map +1 -0
  97. package/npm-shrinkwrap.json +5482 -0
  98. package/package.json +80 -0
  99. package/plugin/claude-code/.claude-plugin/plugin.json +15 -0
  100. package/plugin/claude-code/.mcp.json +10 -0
  101. package/plugin/claude-code/skills/okf-catalog/SKILL.md +15 -0
  102. package/recipes/publish/README.md +32 -0
  103. package/recipes/publish/checkers.lock +435 -0
  104. package/recipes/publish/checkers.txt +5 -0
  105. package/recipes/publish/pack.sh +60 -0
  106. package/recipes/publish/publish.yml +88 -0
  107. package/recipes/publish/push.sh +63 -0
@@ -0,0 +1,109 @@
1
+ import { collisionKey, isSafeRelativePath } from "./paths.js";
2
+ const RECORD_HEAD = /^([0-7]{6}) (blob|tree|commit) ([0-9a-f]{40,64}) +(\d+|-)$/;
3
+ const utf8 = new TextDecoder("utf-8", { fatal: true });
4
+ /**
5
+ * Parses the NUL-terminated records of `ls-tree -z` with `-l`: `<mode> <type> <sha> <size padded>\t<path>`. The
6
+ * path is verbatim, so only the first tab separates it. A record that does not fit is an error, never a guess.
7
+ */
8
+ export function parseLsTree(bytes) {
9
+ const entries = [];
10
+ let start = 0;
11
+ for (let i = 0; i <= bytes.length; i++) {
12
+ if (i < bytes.length && bytes[i] !== 0)
13
+ continue;
14
+ if (i === start) {
15
+ start = i + 1;
16
+ continue;
17
+ }
18
+ let record;
19
+ try {
20
+ record = utf8.decode(bytes.subarray(start, i));
21
+ }
22
+ catch {
23
+ throw new Error("ls-tree printed a path that is not valid UTF-8");
24
+ }
25
+ const tab = record.indexOf("\t");
26
+ const head = tab === -1 ? record : record.slice(0, tab);
27
+ const match = RECORD_HEAD.exec(head);
28
+ if (tab === -1 || match === null)
29
+ throw new Error(`ls-tree printed a record this reader cannot parse`);
30
+ const [, mode, type, sha, size] = match;
31
+ const entry = { mode, type, sha, path: record.slice(tab + 1) };
32
+ if (size !== "-")
33
+ entry.size = Number(size);
34
+ entries.push(entry);
35
+ start = i + 1;
36
+ }
37
+ return entries;
38
+ }
39
+ /** Code points git's HFS+ protection ignores when it compares a name with `.git`. */
40
+ const HFS_IGNORABLE = /[‌-‏‪-‮-]/gu;
41
+ /** Whether a path segment is `.git`, or one of the spellings a file system would fold onto it. */
42
+ export function isDotGitSegment(segment) {
43
+ // NTFS reads a name up to its first colon (a stream name follows), and ignores trailing dots and spaces.
44
+ const colon = segment.indexOf(":");
45
+ const lowered = (colon === -1 ? segment : segment.slice(0, colon)).normalize("NFC").toLowerCase();
46
+ if (/^\.git[. ]*$/.test(lowered))
47
+ return true;
48
+ if (/^git~1[. ]*$/.test(lowered))
49
+ return true;
50
+ return lowered.replace(HFS_IGNORABLE, "") === ".git";
51
+ }
52
+ const refuse = (path, rule, detail) => ({
53
+ path,
54
+ rule,
55
+ detail,
56
+ });
57
+ const utf8Length = (text) => new TextEncoder().encode(text).length;
58
+ /** The shortest platform limit on a whole path the server runs on (macOS); a longer one fails at extraction. */
59
+ const PATH_BYTES_CAP = 1024;
60
+ /**
61
+ * Judges a fetched commit's tree before anything is written (D43): a symbolic link or a gitlink, a blob over the
62
+ * file cap, more entries than the file cap, more blob bytes than the tree cap, a path the manifest rule would
63
+ * refuse or that a file system would fold onto `.git`, a segment over 255 bytes, a name not in normalisation form
64
+ * C, two names the engine's key folds together, or a bundle path that names no tree, each refuse the whole commit.
65
+ */
66
+ export function validateTree(entries, caps, bundlePath) {
67
+ const seen = new Map();
68
+ let total = 0;
69
+ let bundleFound = bundlePath === ".";
70
+ if (entries.length > caps.files)
71
+ return refuse("", "too-many-files", `${entries.length} tree entries, over the cap of ${caps.files}`);
72
+ for (const entry of entries) {
73
+ if (entry.mode === "120000")
74
+ return refuse(entry.path, "symlink", "a symbolic link in the published tree");
75
+ if (entry.type === "commit" || entry.mode === "160000")
76
+ return refuse(entry.path, "gitlink", "a submodule in the published tree");
77
+ if (!isSafeRelativePath(entry.path))
78
+ return refuse(entry.path, "path-escape", "not a safe bundle-relative path");
79
+ if (entry.path !== entry.path.normalize("NFC"))
80
+ return refuse(entry.path, "path-escape", "not in Unicode normalisation form C");
81
+ if (utf8Length(entry.path) > PATH_BYTES_CAP)
82
+ return refuse(entry.path, "path-escape", `a path over ${PATH_BYTES_CAP} bytes`);
83
+ for (const segment of entry.path.split("/")) {
84
+ if (isDotGitSegment(segment))
85
+ return refuse(entry.path, "path-escape", "a segment a file system would read as .git");
86
+ if (utf8Length(segment) > 255)
87
+ return refuse(entry.path, "path-escape", "a segment over 255 bytes");
88
+ }
89
+ const key = collisionKey(entry.path);
90
+ const other = seen.get(key);
91
+ if (other !== undefined)
92
+ return refuse(entry.path, "path-escape", `collides with ${other} under case folding or normalisation`);
93
+ seen.set(key, entry.path);
94
+ if (entry.type === "blob") {
95
+ const size = entry.size ?? 0;
96
+ if (size > caps.fileBytes)
97
+ return refuse(entry.path, "oversize", `${size} bytes, over the cap of ${caps.fileBytes}`);
98
+ total += size;
99
+ if (total > caps.treeBytes)
100
+ return refuse(entry.path, "tree-too-large", `the blobs total more than ${caps.treeBytes} bytes`);
101
+ }
102
+ else if (entry.path === bundlePath)
103
+ bundleFound = true;
104
+ }
105
+ if (!bundleFound)
106
+ return refuse(bundlePath, "bundle-path-missing", "the configured bundle path is not a folder of the published tree");
107
+ return undefined;
108
+ }
109
+ //# sourceMappingURL=git-tree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-tree.js","sourceRoot":"","sources":["../../src/bundle/git-tree.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Caps, Refusal } from \"./model.js\";\nimport { collisionKey, isSafeRelativePath } from \"./paths.js\";\n\n/** One record of `git ls-tree -r -t -l -z --full-tree <commit>`. */\nexport interface TreeEntry {\n mode: string;\n type: \"blob\" | \"tree\" | \"commit\";\n sha: string;\n /** Blob size in bytes; absent for a tree or a commit, which ls-tree prints as `-`. */\n size?: number;\n path: string;\n}\n\nconst RECORD_HEAD = /^([0-7]{6}) (blob|tree|commit) ([0-9a-f]{40,64}) +(\\d+|-)$/;\nconst utf8 = new TextDecoder(\"utf-8\", { fatal: true });\n\n/**\n * Parses the NUL-terminated records of `ls-tree -z` with `-l`: `<mode> <type> <sha> <size padded>\\t<path>`. The\n * path is verbatim, so only the first tab separates it. A record that does not fit is an error, never a guess.\n */\nexport function parseLsTree(bytes: Uint8Array): TreeEntry[] {\n const entries: TreeEntry[] = [];\n let start = 0;\n for (let i = 0; i <= bytes.length; i++) {\n if (i < bytes.length && bytes[i] !== 0) continue;\n if (i === start) {\n start = i + 1;\n continue;\n }\n let record: string;\n try {\n record = utf8.decode(bytes.subarray(start, i));\n } catch {\n throw new Error(\"ls-tree printed a path that is not valid UTF-8\");\n }\n const tab = record.indexOf(\"\\t\");\n const head = tab === -1 ? record : record.slice(0, tab);\n const match = RECORD_HEAD.exec(head);\n if (tab === -1 || match === null)\n throw new Error(`ls-tree printed a record this reader cannot parse`);\n const [, mode, type, sha, size] = match as unknown as [\n string,\n string,\n TreeEntry[\"type\"],\n string,\n string,\n ];\n const entry: TreeEntry = { mode, type, sha, path: record.slice(tab + 1) };\n if (size !== \"-\") entry.size = Number(size);\n entries.push(entry);\n start = i + 1;\n }\n return entries;\n}\n\n/** Code points git's HFS+ protection ignores when it compares a name with `.git`. */\nconst HFS_IGNORABLE = /[‌-‏‪-‮-]/gu;\n\n/** Whether a path segment is `.git`, or one of the spellings a file system would fold onto it. */\nexport function isDotGitSegment(segment: string): boolean {\n // NTFS reads a name up to its first colon (a stream name follows), and ignores trailing dots and spaces.\n const colon = segment.indexOf(\":\");\n const lowered = (colon === -1 ? segment : segment.slice(0, colon)).normalize(\"NFC\").toLowerCase();\n if (/^\\.git[. ]*$/.test(lowered)) return true;\n if (/^git~1[. ]*$/.test(lowered)) return true;\n return lowered.replace(HFS_IGNORABLE, \"\") === \".git\";\n}\n\nconst refuse = (path: string, rule: Refusal[\"rule\"], detail: string): Refusal => ({\n path,\n rule,\n detail,\n});\nconst utf8Length = (text: string): number => new TextEncoder().encode(text).length;\n/** The shortest platform limit on a whole path the server runs on (macOS); a longer one fails at extraction. */\nconst PATH_BYTES_CAP = 1024;\n\n/**\n * Judges a fetched commit's tree before anything is written (D43): a symbolic link or a gitlink, a blob over the\n * file cap, more entries than the file cap, more blob bytes than the tree cap, a path the manifest rule would\n * refuse or that a file system would fold onto `.git`, a segment over 255 bytes, a name not in normalisation form\n * C, two names the engine's key folds together, or a bundle path that names no tree, each refuse the whole commit.\n */\nexport function validateTree(\n entries: readonly TreeEntry[],\n caps: Caps,\n bundlePath: string,\n): Refusal | undefined {\n const seen = new Map<string, string>();\n let total = 0;\n let bundleFound = bundlePath === \".\";\n if (entries.length > caps.files)\n return refuse(\n \"\",\n \"too-many-files\",\n `${entries.length} tree entries, over the cap of ${caps.files}`,\n );\n for (const entry of entries) {\n if (entry.mode === \"120000\")\n return refuse(entry.path, \"symlink\", \"a symbolic link in the published tree\");\n if (entry.type === \"commit\" || entry.mode === \"160000\")\n return refuse(entry.path, \"gitlink\", \"a submodule in the published tree\");\n if (!isSafeRelativePath(entry.path))\n return refuse(entry.path, \"path-escape\", \"not a safe bundle-relative path\");\n if (entry.path !== entry.path.normalize(\"NFC\"))\n return refuse(entry.path, \"path-escape\", \"not in Unicode normalisation form C\");\n if (utf8Length(entry.path) > PATH_BYTES_CAP)\n return refuse(entry.path, \"path-escape\", `a path over ${PATH_BYTES_CAP} bytes`);\n for (const segment of entry.path.split(\"/\")) {\n if (isDotGitSegment(segment))\n return refuse(entry.path, \"path-escape\", \"a segment a file system would read as .git\");\n if (utf8Length(segment) > 255)\n return refuse(entry.path, \"path-escape\", \"a segment over 255 bytes\");\n }\n const key = collisionKey(entry.path);\n const other = seen.get(key);\n if (other !== undefined)\n return refuse(\n entry.path,\n \"path-escape\",\n `collides with ${other} under case folding or normalisation`,\n );\n seen.set(key, entry.path);\n if (entry.type === \"blob\") {\n const size = entry.size ?? 0;\n if (size > caps.fileBytes)\n return refuse(entry.path, \"oversize\", `${size} bytes, over the cap of ${caps.fileBytes}`);\n total += size;\n if (total > caps.treeBytes)\n return refuse(\n entry.path,\n \"tree-too-large\",\n `the blobs total more than ${caps.treeBytes} bytes`,\n );\n } else if (entry.path === bundlePath) bundleFound = true;\n }\n if (!bundleFound)\n return refuse(\n bundlePath,\n \"bundle-path-missing\",\n \"the configured bundle path is not a folder of the published tree\",\n );\n return undefined;\n}\n"]}
@@ -0,0 +1,90 @@
1
+ import { fromMarkdown } from "mdast-util-from-markdown";
2
+ import { toString as mdastToString } from "mdast-util-to-string";
3
+ import { byCodeUnit } from "./paths.js";
4
+ const SEPARATOR = /^\s*[-–—]\s*/;
5
+ function entryOf(item) {
6
+ const paragraph = item.children.find((c) => c.type === "paragraph");
7
+ if (paragraph === undefined)
8
+ return undefined;
9
+ const children = paragraph.children;
10
+ const at = children.findIndex((c) => c.type === "link");
11
+ const link = children[at];
12
+ if (link === undefined || link.type !== "link")
13
+ return undefined;
14
+ const rest = children
15
+ .slice(at + 1)
16
+ .map((c) => mdastToString(c))
17
+ .join("")
18
+ .replace(SEPARATOR, "")
19
+ .trim();
20
+ const entry = { title: mdastToString(link).trim(), href: link.url };
21
+ if (rest.length > 0)
22
+ entry.description = rest;
23
+ return entry;
24
+ }
25
+ /** Reads a §8 index body: headings open sections, list items with a link are entries, `- description` follows the link. */
26
+ export function parseIndex(body) {
27
+ const tree = fromMarkdown(body);
28
+ const sections = [];
29
+ let current;
30
+ const open = (heading) => {
31
+ const section = { heading, entries: [] };
32
+ sections.push(section);
33
+ return section;
34
+ };
35
+ for (const node of tree.children) {
36
+ if (node.type === "heading") {
37
+ current = open(mdastToString(node).trim());
38
+ }
39
+ else if (node.type === "list") {
40
+ for (const item of node.children) {
41
+ const entry = entryOf(item);
42
+ if (entry === undefined)
43
+ continue;
44
+ if (current === undefined)
45
+ current = open("");
46
+ current.entries.push(entry);
47
+ }
48
+ }
49
+ }
50
+ return sections;
51
+ }
52
+ /** Text safe inside a link's square brackets or after it: whitespace collapsed, brackets and backslashes escaped, markup disarmed. */
53
+ function prose(s) {
54
+ return s
55
+ .replace(/\s+/g, " ")
56
+ .trim()
57
+ .replace(/[\\[\]]/g, (c) => `\\${c}`)
58
+ .replace(/</g, "&lt;")
59
+ .replace(/>/g, "&gt;");
60
+ }
61
+ /** A relative link target with each segment percent-encoded, so a space or a parenthesis cannot break the link. */
62
+ const href = (relative) => relative.split("/").map(encodeURIComponent).join("/");
63
+ /** Writes a §8 index for one folder: a Pages section with relative links, then a Subfolders section; empty sections are omitted. */
64
+ export function generateIndex(folder, pages, subfolders) {
65
+ const prefix = folder === "" ? "" : `${folder}/`;
66
+ const lines = [];
67
+ const own = pages
68
+ .filter((p) => p.path.startsWith(prefix) && !p.path.slice(prefix.length).includes("/"))
69
+ .sort((a, b) => byCodeUnit(a.path, b.path));
70
+ if (own.length > 0) {
71
+ lines.push("# Pages", "");
72
+ for (const page of own) {
73
+ const target = href(page.path.slice(prefix.length));
74
+ const description = page.description === undefined ? "" : prose(page.description);
75
+ lines.push(description.length > 0
76
+ ? `* [${prose(page.title)}](${target}) - ${description}`
77
+ : `* [${prose(page.title)}](${target})`);
78
+ }
79
+ lines.push("");
80
+ }
81
+ const subs = [...subfolders].sort(byCodeUnit);
82
+ if (subs.length > 0) {
83
+ lines.push("# Subfolders", "");
84
+ for (const sub of subs)
85
+ lines.push(`* [${prose(sub)}](${href(sub)}/)`);
86
+ lines.push("");
87
+ }
88
+ return lines.join("\n");
89
+ }
90
+ //# sourceMappingURL=index-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-file.js","sourceRoot":"","sources":["../../src/bundle/index-file.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ListItem, Nodes, Parent, PhrasingContent } from \"mdast\";\nimport { fromMarkdown } from \"mdast-util-from-markdown\";\nimport { toString as mdastToString } from \"mdast-util-to-string\";\nimport { byCodeUnit } from \"./paths.js\";\n\nexport interface IndexEntry {\n title: string;\n href: string;\n description?: string;\n}\n\nexport interface IndexSection {\n heading: string;\n entries: IndexEntry[];\n}\n\nconst SEPARATOR = /^\\s*[-–—]\\s*/;\n\nfunction entryOf(item: ListItem): IndexEntry | undefined {\n const paragraph = item.children.find((c) => c.type === \"paragraph\");\n if (paragraph === undefined) return undefined;\n const children = paragraph.children as PhrasingContent[];\n const at = children.findIndex((c) => c.type === \"link\");\n const link = children[at];\n if (link === undefined || link.type !== \"link\") return undefined;\n const rest = children\n .slice(at + 1)\n .map((c) => mdastToString(c))\n .join(\"\")\n .replace(SEPARATOR, \"\")\n .trim();\n const entry: IndexEntry = { title: mdastToString(link).trim(), href: link.url };\n if (rest.length > 0) entry.description = rest;\n return entry;\n}\n\n/** Reads a §8 index body: headings open sections, list items with a link are entries, `- description` follows the link. */\nexport function parseIndex(body: string): IndexSection[] {\n const tree = fromMarkdown(body);\n const sections: IndexSection[] = [];\n let current: IndexSection | undefined;\n const open = (heading: string): IndexSection => {\n const section: IndexSection = { heading, entries: [] };\n sections.push(section);\n return section;\n };\n for (const node of (tree as Parent).children as Nodes[]) {\n if (node.type === \"heading\") {\n current = open(mdastToString(node).trim());\n } else if (node.type === \"list\") {\n for (const item of node.children) {\n const entry = entryOf(item);\n if (entry === undefined) continue;\n if (current === undefined) current = open(\"\");\n current.entries.push(entry);\n }\n }\n }\n return sections;\n}\n\nexport interface IndexPage {\n path: string;\n title: string;\n description?: string;\n}\n\n/** Text safe inside a link's square brackets or after it: whitespace collapsed, brackets and backslashes escaped, markup disarmed. */\nfunction prose(s: string): string {\n return s\n .replace(/\\s+/g, \" \")\n .trim()\n .replace(/[\\\\[\\]]/g, (c) => `\\\\${c}`)\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n}\n\n/** A relative link target with each segment percent-encoded, so a space or a parenthesis cannot break the link. */\nconst href = (relative: string): string => relative.split(\"/\").map(encodeURIComponent).join(\"/\");\n\n/** Writes a §8 index for one folder: a Pages section with relative links, then a Subfolders section; empty sections are omitted. */\nexport function generateIndex(folder: string, pages: IndexPage[], subfolders: string[]): string {\n const prefix = folder === \"\" ? \"\" : `${folder}/`;\n const lines: string[] = [];\n const own = pages\n .filter((p) => p.path.startsWith(prefix) && !p.path.slice(prefix.length).includes(\"/\"))\n .sort((a, b) => byCodeUnit(a.path, b.path));\n if (own.length > 0) {\n lines.push(\"# Pages\", \"\");\n for (const page of own) {\n const target = href(page.path.slice(prefix.length));\n const description = page.description === undefined ? \"\" : prose(page.description);\n lines.push(\n description.length > 0\n ? `* [${prose(page.title)}](${target}) - ${description}`\n : `* [${prose(page.title)}](${target})`,\n );\n }\n lines.push(\"\");\n }\n const subs = [...subfolders].sort(byCodeUnit);\n if (subs.length > 0) {\n lines.push(\"# Subfolders\", \"\");\n for (const sub of subs) lines.push(`* [${prose(sub)}](${href(sub)}/)`);\n lines.push(\"\");\n }\n return lines.join(\"\\n\");\n}\n"]}
@@ -0,0 +1,57 @@
1
+ const SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
2
+ /**
3
+ * Resolves a Markdown link against the bundle. Bundle-absolute links start with a slash; relative links
4
+ * resolve against the page's folder. Fragments and queries are dropped, segments are percent-decoded, and
5
+ * a path that climbs above the root is broken. The result says what the link points at: a page, a folder,
6
+ * a reserved file, an attachment, a same-page anchor, something external, or nothing.
7
+ */
8
+ export function resolveLink(url, fromPath, index) {
9
+ const trimmed = url.trim();
10
+ if (trimmed.startsWith("#"))
11
+ return { kind: "anchor" };
12
+ if (SCHEME.test(trimmed) || trimmed.startsWith("//"))
13
+ return { kind: "external" };
14
+ const withoutFragment = trimmed.split("#")[0]?.split("?")[0] ?? "";
15
+ if (withoutFragment.length === 0)
16
+ return { kind: "broken" };
17
+ const folderHint = withoutFragment.endsWith("/");
18
+ const absolute = withoutFragment.startsWith("/");
19
+ const base = absolute ? [] : fromPath.split("/").slice(0, -1);
20
+ const stack = [...base];
21
+ for (const rawSegment of withoutFragment.split("/")) {
22
+ let segment;
23
+ try {
24
+ segment = decodeURIComponent(rawSegment);
25
+ }
26
+ catch {
27
+ return { kind: "broken" };
28
+ }
29
+ if (segment === "" || segment === ".")
30
+ continue;
31
+ if (segment === "..") {
32
+ if (stack.length === 0)
33
+ return { kind: "broken" };
34
+ stack.pop();
35
+ continue;
36
+ }
37
+ if (/[/\\\0]/.test(segment))
38
+ return { kind: "broken" };
39
+ stack.push(segment);
40
+ }
41
+ const target = stack.join("/");
42
+ if (folderHint) {
43
+ if (index.folders.has(target) || index.reserved.has(`${target}/index.md`))
44
+ return { kind: "folder", target };
45
+ return { kind: "broken" };
46
+ }
47
+ if (index.pages.has(target))
48
+ return { kind: "page", target };
49
+ if (index.reserved.has(target))
50
+ return { kind: "reserved", target };
51
+ if (index.attachments.has(target))
52
+ return { kind: "attachment", target };
53
+ if (index.folders.has(target))
54
+ return { kind: "folder", target };
55
+ return { kind: "broken" };
56
+ }
57
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.js","sourceRoot":"","sources":["../../src/bundle/links.ts"],"names":[],"mappings":"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","sourcesContent":["import type { LinkKind } from \"./model.js\";\n\nexport interface LinkIndex {\n pages: Set<string>;\n reserved: Set<string>;\n attachments: Set<string>;\n folders: Set<string>;\n}\n\nexport type ResolvedLink = { kind: LinkKind; target?: string };\n\nconst SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;\n\n/**\n * Resolves a Markdown link against the bundle. Bundle-absolute links start with a slash; relative links\n * resolve against the page's folder. Fragments and queries are dropped, segments are percent-decoded, and\n * a path that climbs above the root is broken. The result says what the link points at: a page, a folder,\n * a reserved file, an attachment, a same-page anchor, something external, or nothing.\n */\nexport function resolveLink(url: string, fromPath: string, index: LinkIndex): ResolvedLink {\n const trimmed = url.trim();\n if (trimmed.startsWith(\"#\")) return { kind: \"anchor\" };\n if (SCHEME.test(trimmed) || trimmed.startsWith(\"//\")) return { kind: \"external\" };\n const withoutFragment = trimmed.split(\"#\")[0]?.split(\"?\")[0] ?? \"\";\n if (withoutFragment.length === 0) return { kind: \"broken\" };\n const folderHint = withoutFragment.endsWith(\"/\");\n const absolute = withoutFragment.startsWith(\"/\");\n const base = absolute ? [] : fromPath.split(\"/\").slice(0, -1);\n const stack = [...base];\n for (const rawSegment of withoutFragment.split(\"/\")) {\n let segment: string;\n try {\n segment = decodeURIComponent(rawSegment);\n } catch {\n return { kind: \"broken\" };\n }\n if (segment === \"\" || segment === \".\") continue;\n if (segment === \"..\") {\n if (stack.length === 0) return { kind: \"broken\" };\n stack.pop();\n continue;\n }\n if (/[/\\\\\\0]/.test(segment)) return { kind: \"broken\" };\n stack.push(segment);\n }\n const target = stack.join(\"/\");\n if (folderHint) {\n if (index.folders.has(target) || index.reserved.has(`${target}/index.md`))\n return { kind: \"folder\", target };\n return { kind: \"broken\" };\n }\n if (index.pages.has(target)) return { kind: \"page\", target };\n if (index.reserved.has(target)) return { kind: \"reserved\", target };\n if (index.attachments.has(target)) return { kind: \"attachment\", target };\n if (index.folders.has(target)) return { kind: \"folder\", target };\n return { kind: \"broken\" };\n}\n"]}