@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21

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 (126) hide show
  1. package/README.md +28 -62
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-assembler/SKILL.md +25 -6
  6. package/skills/c64-assembler/scripts/assemble.ts +45 -10
  7. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  8. package/skills/c64-basic/SKILL.md +15 -7
  9. package/skills/c64-basic/scripts/basic.ts +4 -4
  10. package/skills/c64-disk/SKILL.md +34 -11
  11. package/skills/c64-disk/scripts/disk.ts +65 -10
  12. package/skills/c64-disk/scripts/lib/containers.ts +124 -0
  13. package/skills/c64-docs/SKILL.md +138 -0
  14. package/skills/c64-docs/references/page-types.md +141 -0
  15. package/skills/c64-docs/references/style.md +43 -0
  16. package/skills/c64-docs/scripts/generate.ts +102 -0
  17. package/skills/c64-docs/scripts/lib/links.ts +155 -0
  18. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  19. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  20. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  21. package/skills/c64-docs/scripts/link.ts +207 -0
  22. package/skills/c64-emulator/SKILL.md +29 -8
  23. package/skills/c64-knowledge/SKILL.md +201 -15
  24. package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
  25. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  26. package/skills/c64-memory-map/SKILL.md +14 -2
  27. package/skills/c64-memory-map/scripts/memmap.ts +15 -4
  28. package/skills/c64-provenance/SKILL.md +100 -11
  29. package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
  30. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  31. package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
  32. package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
  33. package/skills/c64-provenance/scripts/provenance.ts +477 -24
  34. package/skills/c64-reverse-engineering/SKILL.md +90 -11
  35. package/skills/c64-source/SKILL.md +174 -0
  36. package/skills/c64-source/scripts/generate.ts +788 -0
  37. package/skills/c64-source/scripts/lib/loader.ts +630 -0
  38. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  39. package/skills/c64-static-analysis/SKILL.md +53 -13
  40. package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
  41. package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
  42. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
  43. package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
  44. package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
  45. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  46. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  47. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  48. package/skills/c64-testing/SKILL.md +57 -4
  49. package/skills/c64-testing/references/scenario-format.md +50 -6
  50. package/skills/c64-testing/scripts/checklist.ts +3 -3
  51. package/skills/c64-testing/scripts/diff.ts +167 -0
  52. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  53. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
  54. package/skills/c64-testing/scripts/run.ts +241 -0
  55. package/skills/c64-testing/scripts/test.ts +20 -10
  56. package/skills/c64-unpacker/SKILL.md +88 -21
  57. package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
  58. package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
  59. package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
  60. package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
  61. package/skills/c64-unpacker/scripts/unpack.ts +545 -61
  62. package/src/c64.ts +240 -12
  63. package/src/cli/cleanup.ts +1 -1
  64. package/src/cli/main.ts +14 -8
  65. package/src/cli/status.ts +105 -11
  66. package/src/graphics.ts +158 -0
  67. package/src/host/main.ts +57 -31
  68. package/src/host/server.ts +30 -47
  69. package/src/host/tools/c1541.ts +34 -5
  70. package/src/host/tools/index.ts +6 -2
  71. package/src/host/tools/petcat.ts +10 -3
  72. package/src/host/tools/status.ts +23 -6
  73. package/src/host/vice/adapter.ts +64 -20
  74. package/src/host/vice/chips.ts +11 -1
  75. package/src/host/vice/process.ts +19 -8
  76. package/src/host/vice/screen.ts +4 -44
  77. package/src/host/vice/session.ts +343 -60
  78. package/src/host-client/autostart.ts +55 -0
  79. package/src/host-client/connect.ts +14 -14
  80. package/src/host-client/coverage.ts +36 -0
  81. package/src/host-client/image.ts +63 -0
  82. package/src/host-client/steps.ts +175 -0
  83. package/src/host-client/tools.ts +7 -6
  84. package/src/host-client/transfer.ts +60 -10
  85. package/src/host-client/vice-session.ts +107 -28
  86. package/src/knowledge/conflicts.ts +161 -0
  87. package/src/knowledge/database.ts +150 -38
  88. package/src/knowledge/expression.ts +209 -0
  89. package/src/knowledge/graph.ts +104 -0
  90. package/src/knowledge/history.ts +105 -27
  91. package/src/knowledge/import.ts +141 -85
  92. package/src/knowledge/join.ts +211 -0
  93. package/src/knowledge/program.ts +258 -0
  94. package/src/knowledge/read.ts +445 -63
  95. package/src/knowledge/schema.ts +277 -65
  96. package/src/knowledge/text.ts +415 -0
  97. package/src/knowledge/write.ts +1145 -150
  98. package/src/mcp/main.ts +44 -19
  99. package/src/mcp/output.ts +39 -0
  100. package/src/mcp/server.ts +15 -4
  101. package/src/mcp/tools/debug.ts +202 -31
  102. package/src/mcp/tools/execution.ts +39 -5
  103. package/src/mcp/tools/input.ts +7 -4
  104. package/src/mcp/tools/machine.ts +14 -2
  105. package/src/mcp/tools/media.ts +44 -16
  106. package/src/mcp/tools/memory.ts +68 -12
  107. package/src/mcp/tools/video.ts +20 -9
  108. package/src/native/discover.ts +44 -6
  109. package/src/native/ghidra/analyze.ts +2 -2
  110. package/src/native/ghidra/index.ts +19 -3
  111. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  112. package/src/native/ghidra/scripts/C64Export.java +33 -1
  113. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  114. package/src/native/status.ts +15 -3
  115. package/src/native/types.ts +43 -8
  116. package/src/opcodes.ts +133 -0
  117. package/src/package.ts +21 -0
  118. package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
  119. package/src/project.ts +166 -3
  120. package/src/protocol/messages.ts +17 -10
  121. package/src/protocol/tools.ts +27 -22
  122. package/src/protocol/vice.ts +255 -33
  123. package/src/script.ts +49 -9
  124. package/src/trace.ts +55 -23
  125. package/skills/c64-provenance/scripts/compare.ts +0 -92
  126. package/skills/c64-unpacker/scripts/evidence.ts +0 -135
@@ -0,0 +1,102 @@
1
+ // c64-docs generate.ts (D80): the reference pages of a program from the
2
+ // knowledge, the source map of c64-source and the program in the knowledge. The pages are
3
+ // generated and never edited; the pages of a person link to them.
4
+
5
+ import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
6
+ import { join, posix } from "node:path";
7
+ import { parseArgs } from "node:util";
8
+
9
+ import { WireFailure } from "#src/host-client/tools.ts";
10
+ import { writeProjectFile } from "#src/host-client/transfer.ts";
11
+ import { conflicts } from "#src/knowledge/conflicts.ts";
12
+ import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
13
+ import { currentRevision, listComments, listModules, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
14
+ import { resolveProjectPath } from "#src/project.ts";
15
+ import { runScript, UsageError } from "#src/script.ts";
16
+ import { buildReference, GENERATED_MARK } from "./lib/reference.ts";
17
+ import { readSourceMap } from "./lib/source-map.ts";
18
+ import { programImage, type ProgramImage } from "#src/knowledge/program.ts";
19
+
20
+ const USAGE = `generate.ts --source <dir> --out <dir>
21
+
22
+ --source <dir> the source directory of c64-source, with its source-map.json,
23
+ relative to the project folder
24
+ --out <dir> the directory for the reference pages, relative to the project
25
+ folder, for example docs/reference; generate.ts writes only
26
+ there and refuses a page in it that it did not write
27
+ When the knowledge has the program of the source (knowledge.ts image of the
28
+ c64-knowledge skill, D83), the reference decodes the tables and shows the
29
+ graphics. The result is one JSON object.`;
30
+
31
+ /** The files of an earlier run, or a refusal that names a file of a person. */
32
+ function oldFiles(out: string): string[] {
33
+ const directory = resolveProjectPath(out);
34
+ if (!existsSync(directory)) return [];
35
+ const found: string[] = [];
36
+ const walk = (relative: string) => {
37
+ for (const entry of readdirSync(join(directory, relative), { withFileTypes: true })) {
38
+ const path = posix.join(relative, entry.name);
39
+ if (entry.isDirectory()) {
40
+ walk(path);
41
+ continue;
42
+ }
43
+ const generated = path.endsWith(".md") ? readFileSync(join(directory, path), "utf8").startsWith(GENERATED_MARK) : path.startsWith("images/") && path.endsWith(".png");
44
+ if (!generated) throw new WireFailure("invalid-input", `${posix.join(out, path)} is not a page of generate.ts. Nothing was written. Give --out a directory that holds only the reference, and keep the pages of a person outside it.`);
45
+ found.push(path);
46
+ }
47
+ };
48
+ walk("");
49
+ return found;
50
+ }
51
+
52
+ function run(argv: string[]): Record<string, unknown> {
53
+ const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { source: { type: "string" }, out: { type: "string" }, help: { type: "boolean" } } });
54
+ if (values.help) throw new UsageError("");
55
+ if (positionals.length > 0) throw new UsageError(`generate.ts takes no positional arguments, not ${positionals[0]}`);
56
+ if (values.source === undefined) throw new UsageError("give --source <dir>: the source directory of c64-source");
57
+ if (values.out === undefined) throw new UsageError("give --out <dir>: the directory for the reference pages");
58
+ for (const [option, path] of [["--source", values.source], ["--out", values.out]] as const) {
59
+ try {
60
+ resolveProjectPath(path);
61
+ } catch {
62
+ throw new UsageError(`${option} must be a path relative to the project folder that stays inside it, not ${path}`);
63
+ }
64
+ }
65
+ const map = readSourceMap(values.source);
66
+ const db = openForRead();
67
+ let knowledge;
68
+ let image: ProgramImage | undefined;
69
+ try {
70
+ image = programImage(db, map.space);
71
+ const found = conflicts(db).length;
72
+ if (found > 0) throw new KnowledgeError("conflict", `The knowledge has ${found} conflict${found === 1 ? "" : "s"} from a git merge. Nothing was written. Run c64-knowledge's knowledge.ts merge, then generate the source and the reference again.`);
73
+ const revision = currentRevision(db);
74
+ // A source of another revision has other lines: the links would point to the wrong code.
75
+ if (map.revision !== revision) {
76
+ throw new WireFailure("invalid-input", `${map.pathOf("source-map.json")} is from knowledge revision ${map.revision}, and the knowledge is at revision ${revision}. Nothing was written. Generate the source again with c64-source, then run generate.ts.`);
77
+ }
78
+ knowledge = {
79
+ revision,
80
+ symbols: listSymbols(db, undefined, undefined, undefined, map.space),
81
+ regions: listRegions(db, undefined, undefined, undefined, map.space),
82
+ comments: listComments(db, undefined, undefined, map.space),
83
+ references: listReferences(db, undefined, undefined, undefined, map.space),
84
+ modules: listModules(db, undefined, undefined, map.space),
85
+ };
86
+ } finally {
87
+ db?.close();
88
+ }
89
+ const old = oldFiles(values.out);
90
+ const built = buildReference({
91
+ out: posix.normalize(values.out),
92
+ map,
93
+ ...knowledge,
94
+ ...(image === undefined ? {} : { image: { byteAt: image.byteAt, inImage: (address: number) => image.segments.some((segment) => address >= segment.start && address <= segment.end) } }),
95
+ });
96
+ const directory = resolveProjectPath(values.out);
97
+ for (const path of old) rmSync(join(directory, path), { force: true });
98
+ for (const file of built.files) writeProjectFile(file.path, file.bytes, { what: "--out" });
99
+ return { out: values.out, revision: knowledge.revision, index: posix.join(posix.normalize(values.out), "index.md"), ...built.counts, program: image === undefined ? "none: put the program into the knowledge with knowledge.ts image to decode the tables and show the graphics" : "the knowledge" };
100
+ }
101
+
102
+ await runScript(run, { usage: USAGE, refusals: [KnowledgeError] });
@@ -0,0 +1,155 @@
1
+ // The links of a page written by a person (D80). A name in backticks links to
2
+ // its line in the source; the address after it links to the entry of the name
3
+ // in the reference, or, for an address inside the routine or the data of the
4
+ // name ("draw_room $0AAF", a place in draw_room), to the line of that address. A link that points into the source or the reference is
5
+ // managed: link.ts writes it again when a line moves. Other links stay as the
6
+ // person wrote them; link.ts only checks that their targets exist.
7
+
8
+ import { posix } from "node:path";
9
+
10
+ import { anchorsOf, linksOf, proseLines } from "./markdown.ts";
11
+
12
+ export interface LinkContext {
13
+ /** The page, relative to the project folder. */
14
+ page: string;
15
+ /** The address of a name of the knowledge or of the source; undefined for a word that is no name. */
16
+ addressOf: (name: string) => number | undefined;
17
+ /** The project path and line of the label of a name, as path#Lnn. */
18
+ codeOf: (name: string) => string | undefined;
19
+ /** The project path and anchor of the entry of a name, as path#name. */
20
+ entryOf: (name: string) => string | undefined;
21
+ /** The last address of the routine or data of a name: an address up to it is a place inside the name. */
22
+ bodyEnd: (name: string) => number | undefined;
23
+ /** The project path and line that writes an address, as path#Lnn. */
24
+ lineOf: (address: number) => string | undefined;
25
+ /** The name of the routine or data that holds an address, for a message. */
26
+ placeOf: (address: number) => string | undefined;
27
+ /** True for a project path that link.ts manages: a file of the source or a page of the reference. */
28
+ managed: (path: string) => boolean;
29
+ }
30
+
31
+ export type ProblemKind = "unknown-name" | "wrong-address" | "missing-file" | "missing-anchor" | "missing-line" | "outside-project" | "stale-reference";
32
+
33
+ export interface Problem {
34
+ page: string;
35
+ /** 1 is the first line. */
36
+ line: number;
37
+ kind: ProblemKind;
38
+ message: string;
39
+ }
40
+
41
+ const NAME = String.raw`([A-Za-z_][A-Za-z0-9_]*)(\+(?:\$[0-9A-Fa-f]{1,4}|[0-9]+))?`;
42
+ const ADDRESS = String.raw`\$([0-9A-Fa-f]{2,4})(?:-\$[0-9A-Fa-f]{2,4})?`;
43
+ const TICK = "`";
44
+ /**
45
+ * A name in backticks, linked or not, with an optional address in backticks
46
+ * after it, linked or not. Groups: 1 the [ of a linked name, 2 the name, 3 its
47
+ * offset, 4 the target of the name, 5 the space or bracket between them, 6 the
48
+ * address text, 7 its start digits, 8 the target of the address.
49
+ */
50
+ const MENTION = new RegExp(
51
+ String.raw`(\[)?${TICK}${NAME}${TICK}(?:\]\(([^)\s]*)\))?(?:(\s*\(\s*|\s+)\[?${TICK}(${ADDRESS})${TICK}(?:\]\(([^)\s]*)\))?)?`,
52
+ "g",
53
+ );
54
+
55
+ const relative = (page: string, target: string) => {
56
+ const [path, fragment] = target.split("#");
57
+ const shown = posix.relative(posix.dirname(page), path!) || posix.basename(path!);
58
+ return fragment === undefined ? shown : `${shown}#${fragment}`;
59
+ };
60
+ /** The project path of a link target, without its fragment; undefined for a link out of the project (a URL, or a page anchor only). */
61
+ export function targetPath(page: string, target: string): string | undefined {
62
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("#")) return undefined;
63
+ return posix.normalize(posix.join(posix.dirname(page), decodeURIComponent(target.split("#")[0]!)));
64
+ }
65
+
66
+ /** Writes the links of the name mentions of a page; returns the new text, the number of links changed and the problems. */
67
+ export function linkPage(text: string, context: LinkContext): { text: string; changed: number; problems: Problem[] } {
68
+ const problems: Problem[] = [];
69
+ let changed = 0;
70
+ const lines = proseLines(text).map((line, index) => {
71
+ if (!line.prose) return line.text;
72
+ return line.text.replace(MENTION, (whole: string, open: string | undefined, name: string, offset: string | undefined, nameTarget: string | undefined, space: string | undefined, addressText: string | undefined, digits: string | undefined, addressTarget: string | undefined, at: number, all: string) => {
73
+ // "[`x`]" without a target is no link of ours: leave it.
74
+ if (open !== undefined && nameTarget === undefined) return whole;
75
+ const ownLink = (target: string | undefined) => target !== undefined && !context.managed(targetPath(context.page, target) ?? "");
76
+ const address = context.addressOf(name);
77
+ const report = (kind: ProblemKind, message: string) => problems.push({ page: context.page, line: index + 1, kind, message });
78
+ // In a list "`a`/`b` `$1000`/`$1010`" the address belongs to another name: the name gets its link, the address stays as it is.
79
+ const namePart = open === undefined ? `${TICK}${name}${offset ?? ""}${TICK}` : `[${TICK}${name}${offset ?? ""}${TICK}](${nameTarget})`;
80
+ const listed = addressText !== undefined && (all[at - 1] === "/" || all[at + whole.length] === "/");
81
+ const rest = listed ? whole.slice(namePart.length) : "";
82
+ const claimed = addressText !== undefined;
83
+ if (listed) addressText = digits = addressTarget = undefined;
84
+ if (address === undefined) {
85
+ // A word in backticks is no claim of a name; with an address after it, or with a link of ours, it is.
86
+ if (claimed || (nameTarget !== undefined && !ownLink(nameTarget))) {
87
+ report("unknown-name", `\`${name}\` is no name of the knowledge or the source. Apply the name to the knowledge, or correct the page.`);
88
+ }
89
+ return whole;
90
+ }
91
+ const delta = offset === undefined ? 0 : offset.startsWith("+$") ? Number.parseInt(offset.slice(2), 16) : Number.parseInt(offset.slice(1), 10);
92
+ const given = digits === undefined ? undefined : Number.parseInt(digits, 16);
93
+ const place = given !== undefined && given !== address + delta && given > address && given <= (context.bodyEnd(name) ?? address);
94
+ if (given !== undefined && given !== address + delta && !place) {
95
+ const expected = `$${(address + delta).toString(16).toUpperCase().padStart(4, "0")}`;
96
+ const holder = context.placeOf(given!);
97
+ report("wrong-address", `\`${name}${offset ?? ""}\` is ${expected} in the knowledge, and the page gives ${addressText}${holder === undefined || holder === name ? "" : `, a place in \`${holder}\``}. Correct the name or the address.${digits!.length <= 2 ? ` For a value, write \`${name}\` = \`${addressText}\`.` : ""}`);
98
+ }
99
+ const shownName = `${TICK}${name}${offset ?? ""}${TICK}`;
100
+ const code = context.codeOf(name);
101
+ const nameLink = ownLink(nameTarget) ? `[${shownName}](${nameTarget})` : code === undefined ? shownName : `[${shownName}](${relative(context.page, code)})`;
102
+ let result = nameLink;
103
+ if (addressText !== undefined) {
104
+ const entry = place ? context.lineOf(given!) : context.entryOf(name);
105
+ const shownAddress = `${TICK}${addressText}${TICK}`;
106
+ result += space! + (ownLink(addressTarget) ? `[${shownAddress}](${addressTarget})` : entry === undefined ? shownAddress : `[${shownAddress}](${relative(context.page, entry)})`);
107
+ }
108
+ result += rest;
109
+ if (result !== whole) changed++;
110
+ return result;
111
+ });
112
+ });
113
+ return { text: lines.join("\n"), changed, problems };
114
+ }
115
+
116
+ /** The files that a check reads: their text, or undefined when the file does not exist. */
117
+ export interface Files {
118
+ exists: (path: string) => boolean;
119
+ /** The text of a project file, or undefined when it is no file. */
120
+ text: (path: string) => string | undefined;
121
+ inside: (path: string) => boolean;
122
+ }
123
+
124
+ /** Checks that each link and image of a page reaches a file, and its anchor or line. */
125
+ export function checkLinks(page: string, text: string, files: Files): Problem[] {
126
+ const problems: Problem[] = [];
127
+ const anchors = new Map<string, Set<string>>();
128
+ const anchorsAt = (path: string) => {
129
+ if (!anchors.has(path)) anchors.set(path, anchorsOf(path === page ? text : (files.text(path) ?? "")));
130
+ return anchors.get(path)!;
131
+ };
132
+ for (const link of linksOf(text)) {
133
+ const report = (kind: ProblemKind, message: string) => problems.push({ page, line: link.line, kind, message });
134
+ const fragment = link.target.includes("#") ? link.target.slice(link.target.indexOf("#") + 1) : undefined;
135
+ if (/^[a-z][a-z0-9+.-]*:/i.test(link.target)) continue;
136
+ const path = link.target.startsWith("#") ? page : targetPath(page, link.target)!;
137
+ if (!files.inside(path)) {
138
+ report("outside-project", `${link.target} leaves the project folder.`);
139
+ continue;
140
+ }
141
+ if (!files.exists(path)) {
142
+ report("missing-file", `${link.target}: there is no ${path}.`);
143
+ continue;
144
+ }
145
+ if (fragment === undefined || fragment === "") continue;
146
+ const line = /^L(\d+)(?:-L?\d+)?$/.exec(fragment);
147
+ if (path.endsWith(".md")) {
148
+ if (!anchorsAt(path).has(fragment)) report("missing-anchor", `${link.target}: ${path} has no anchor ${fragment}.`);
149
+ } else if (line !== null) {
150
+ const count = (files.text(path) ?? "").split("\n").length;
151
+ if (Number(line[1]) > count) report("missing-line", `${link.target}: ${path} has ${count} lines.`);
152
+ }
153
+ }
154
+ return problems;
155
+ }
@@ -0,0 +1,79 @@
1
+ // What the documentation scripts read in a Markdown page: the lines outside
2
+ // code blocks and HTML comments, the anchors that a link can name, and the
3
+ // links and images with their targets.
4
+
5
+ /** The lines of a page and, for each one, whether it is prose: outside fenced code (also mermaid) and outside HTML comments. */
6
+ export function proseLines(text: string): Array<{ text: string; prose: boolean }> {
7
+ const lines = text.split("\n");
8
+ let fence: string | undefined;
9
+ let comment = false;
10
+ return lines.map((line) => {
11
+ const opening = /^\s*(`{3,}|~{3,})/.exec(line);
12
+ if (fence !== undefined) {
13
+ if (opening !== null && opening[1]!.startsWith(fence[0]!) && opening[1]!.length >= fence.length && line.trim() === opening[1]) fence = undefined;
14
+ return { text: line, prose: false };
15
+ }
16
+ if (opening !== null) {
17
+ fence = opening[1]!;
18
+ return { text: line, prose: false };
19
+ }
20
+ if (comment) {
21
+ if (line.includes("-->")) comment = false;
22
+ return { text: line, prose: false };
23
+ }
24
+ if (line.trimStart().startsWith("<!--")) {
25
+ comment = !line.includes("-->");
26
+ return { text: line, prose: false };
27
+ }
28
+ return { text: line, prose: true };
29
+ });
30
+ }
31
+
32
+ /** The id that GitHub and VS Code give a heading: lower case, without punctuation, spaces as hyphens. */
33
+ export function headingSlug(heading: string): string {
34
+ const text = heading
35
+ .replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
36
+ .replace(/<[^>]+>/g, "")
37
+ .replace(/`/g, "")
38
+ .trim()
39
+ .toLowerCase();
40
+ return text.replace(/[^\p{L}\p{N}\s_-]/gu, "").replace(/\s/g, "-");
41
+ }
42
+
43
+ /** The anchors of a page: each <a id> or <a name>, and each heading slug, with -1, -2 for a slug that comes again. */
44
+ export function anchorsOf(text: string): Set<string> {
45
+ const anchors = new Set<string>();
46
+ const seen = new Map<string, number>();
47
+ for (const line of proseLines(text)) {
48
+ for (const match of line.text.matchAll(/<a\s+(?:id|name)="([^"]+)"/g)) anchors.add(match[1]!);
49
+ if (!line.prose) continue;
50
+ const heading = /^#{1,6}\s+(.*?)\s*#*\s*$/.exec(line.text);
51
+ if (heading === null) continue;
52
+ const slug = headingSlug(heading[1]!);
53
+ const count = seen.get(slug) ?? 0;
54
+ seen.set(slug, count + 1);
55
+ anchors.add(count === 0 ? slug : `${slug}-${count}`);
56
+ }
57
+ return anchors;
58
+ }
59
+
60
+ export interface Link {
61
+ /** 1 is the first line. */
62
+ line: number;
63
+ image: boolean;
64
+ target: string;
65
+ }
66
+
67
+ /** The links and images of the prose lines; links inside inline code are not links. */
68
+ export function linksOf(text: string): Link[] {
69
+ const links: Link[] = [];
70
+ proseLines(text).forEach((line, index) => {
71
+ if (!line.prose) return;
72
+ // Inline code that holds no link text: `[x](y)` inside one pair of backticks is code, not a link.
73
+ const outside = line.text.replace(/`[^`]*`/g, (code) => (code.includes("](") ? " ".repeat(code.length) : code));
74
+ for (const match of outside.matchAll(/(!?)\[(?:[^\]`]|`[^`]*`)*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)) {
75
+ links.push({ line: index + 1, image: match[1] === "!", target: match[2]! });
76
+ }
77
+ });
78
+ return links;
79
+ }