@cairn-tool/cairn 3.2.0 → 3.3.1

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 (78) hide show
  1. package/README.md +73 -29
  2. package/dist/agent/audit/index.d.ts +6 -6
  3. package/dist/agent/audit/index.js +6 -17
  4. package/dist/agent/audit/index.js.map +1 -1
  5. package/dist/atomic-write.d.ts +22 -0
  6. package/dist/atomic-write.js +42 -0
  7. package/dist/atomic-write.js.map +1 -0
  8. package/dist/binary-kind.d.ts +13 -0
  9. package/dist/binary-kind.js +25 -0
  10. package/dist/binary-kind.js.map +1 -0
  11. package/dist/cli.js +543 -250
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/jira.js +1 -21
  14. package/dist/commands/jira.js.map +1 -1
  15. package/dist/commands/pdf.d.ts +29 -0
  16. package/dist/commands/pdf.js +345 -0
  17. package/dist/commands/pdf.js.map +1 -0
  18. package/dist/contract/registry.js +64 -0
  19. package/dist/contract/registry.js.map +1 -1
  20. package/dist/contract/schemas/index.js +2 -0
  21. package/dist/contract/schemas/index.js.map +1 -1
  22. package/dist/contract/schemas/pdf.d.ts +2 -0
  23. package/dist/contract/schemas/pdf.js +292 -0
  24. package/dist/contract/schemas/pdf.js.map +1 -0
  25. package/dist/jira/adf/to-markdown.js +2 -32
  26. package/dist/jira/adf/to-markdown.js.map +1 -1
  27. package/dist/markdown-stringify.d.ts +35 -0
  28. package/dist/markdown-stringify.js +44 -0
  29. package/dist/markdown-stringify.js.map +1 -0
  30. package/dist/pdf/attachments.d.ts +50 -0
  31. package/dist/pdf/attachments.js +229 -0
  32. package/dist/pdf/attachments.js.map +1 -0
  33. package/dist/pdf/diagnostics.d.ts +110 -0
  34. package/dist/pdf/diagnostics.js +137 -0
  35. package/dist/pdf/diagnostics.js.map +1 -0
  36. package/dist/pdf/document.d.ts +159 -0
  37. package/dist/pdf/document.js +272 -0
  38. package/dist/pdf/document.js.map +1 -0
  39. package/dist/pdf/forms.d.ts +9 -0
  40. package/dist/pdf/forms.js +101 -0
  41. package/dist/pdf/forms.js.map +1 -0
  42. package/dist/pdf/inspect.d.ts +42 -0
  43. package/dist/pdf/inspect.js +232 -0
  44. package/dist/pdf/inspect.js.map +1 -0
  45. package/dist/pdf/layout.d.ts +78 -0
  46. package/dist/pdf/layout.js +524 -0
  47. package/dist/pdf/layout.js.map +1 -0
  48. package/dist/pdf/outline.d.ts +30 -0
  49. package/dist/pdf/outline.js +127 -0
  50. package/dist/pdf/outline.js.map +1 -0
  51. package/dist/pdf/read.d.ts +62 -0
  52. package/dist/pdf/read.js +193 -0
  53. package/dist/pdf/read.js.map +1 -0
  54. package/dist/pdf/struct.d.ts +35 -0
  55. package/dist/pdf/struct.js +226 -0
  56. package/dist/pdf/struct.js.map +1 -0
  57. package/dist/pdf/text.d.ts +71 -0
  58. package/dist/pdf/text.js +166 -0
  59. package/dist/pdf/text.js.map +1 -0
  60. package/dist/pdf/to-markdown.d.ts +22 -0
  61. package/dist/pdf/to-markdown.js +289 -0
  62. package/dist/pdf/to-markdown.js.map +1 -0
  63. package/dist/pdf/types.d.ts +205 -0
  64. package/dist/pdf/types.js +2 -0
  65. package/dist/pdf/types.js.map +1 -0
  66. package/dist/pdf/validate.d.ts +51 -0
  67. package/dist/pdf/validate.js +136 -0
  68. package/dist/pdf/validate.js.map +1 -0
  69. package/dist/serve/pdf-tools.d.ts +2 -0
  70. package/dist/serve/pdf-tools.js +196 -0
  71. package/dist/serve/pdf-tools.js.map +1 -0
  72. package/dist/serve/tools.d.ts +2 -17
  73. package/dist/serve/tools.js +4 -0
  74. package/dist/serve/tools.js.map +1 -1
  75. package/dist/serve/types.d.ts +22 -0
  76. package/dist/serve/types.js +2 -0
  77. package/dist/serve/types.js.map +1 -0
  78. package/package.json +2 -1
@@ -0,0 +1,127 @@
1
+ import { CODES, DiagnosticSink } from "./diagnostics.js";
2
+ /**
3
+ * The outline is attacker-shaped structure and the walk is recursive, so it is
4
+ * bounded for the same reason the ADF reader bounds nesting depth: a stack
5
+ * overflow with no diagnostic is the wrong failure for input off a network.
6
+ */
7
+ export const MAX_OUTLINE_DEPTH = 64;
8
+ /**
9
+ * C0 controls and DEL, stripped from every attacker-controlled string.
10
+ *
11
+ * Declared once so the directive stays attached to the pattern: inline, the
12
+ * formatter moves the expression onto its own line and the comment governs the
13
+ * wrong statement.
14
+ */
15
+ // eslint-disable-next-line no-control-regex
16
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/g;
17
+ /** Titles are attacker-controlled; bound them and strip control characters. */
18
+ function cleanTitle(value) {
19
+ if (typeof value !== "string")
20
+ return "";
21
+ return value.replace(CONTROL_CHARACTERS, "").normalize("NFC").trim().slice(0, 512);
22
+ }
23
+ /**
24
+ * Reads the declared outline and resolves each destination to a page.
25
+ *
26
+ * The outline a document declares, never one inferred from its text: a document
27
+ * with no `/Outlines` returns an empty tree and exits 0, which is an answer
28
+ * rather than a failure.
29
+ *
30
+ * URLs are recorded, never followed — no fetch, no HEAD, no DNS. `unsafeUrl` is
31
+ * deliberately not surfaced as `url`: pdf.js populates `url` only when the
32
+ * scheme passed its own validation, and putting a rejected `javascript:` or
33
+ * `file:` URI into a field named as though it were clickable would be a
34
+ * misrepresentation.
35
+ */
36
+ export async function readOutline(handle) {
37
+ const sink = new DiagnosticSink();
38
+ const headingLevels = new Map();
39
+ const { doc } = handle;
40
+ let raw;
41
+ try {
42
+ raw = (await handle.within(() => doc.getOutline()));
43
+ }
44
+ catch {
45
+ // A document with a malformed /Outlines reports no outline rather than
46
+ // failing: the outline is not why the caller opened the document.
47
+ raw = null;
48
+ }
49
+ if (!raw || raw.length === 0)
50
+ return { outline: [], diagnostics: sink.all(), headingLevels };
51
+ // `getPageIndex` walks the page tree, so a 500-entry outline on a long
52
+ // document is quadratic without this.
53
+ const resolved = new Map();
54
+ const pageFor = async (entry, title) => {
55
+ const dest = entry.dest;
56
+ if (dest === null || dest === undefined)
57
+ return null;
58
+ try {
59
+ const target = Array.isArray(dest)
60
+ ? dest
61
+ : typeof dest === "string"
62
+ ? await handle.within(() => doc.getDestination(dest))
63
+ : null;
64
+ const first = Array.isArray(target) ? target[0] : null;
65
+ if (!first || typeof first.num !== "number")
66
+ return null;
67
+ const key = `${first.num}R${first.gen ?? 0}`;
68
+ if (resolved.has(key))
69
+ return resolved.get(key) ?? null;
70
+ const index = await handle.within(() => doc.getPageIndex(first));
71
+ const page = index + 1;
72
+ resolved.set(key, page);
73
+ return page;
74
+ }
75
+ catch {
76
+ sink.add({
77
+ code: CODES.destinationUnresolved,
78
+ quality: "approximate",
79
+ message: `The destination for "${title}" does not resolve to a page`,
80
+ construct: title,
81
+ remediation: "The entry is kept with a null page rather than dropped.",
82
+ });
83
+ return null;
84
+ }
85
+ };
86
+ const walk = async (entries, level) => {
87
+ if (level > MAX_OUTLINE_DEPTH) {
88
+ sink.add({
89
+ code: CODES.outlineTooDeep,
90
+ quality: "unsupported",
91
+ message: `The outline nests deeper than ${MAX_OUTLINE_DEPTH} levels and was truncated`,
92
+ });
93
+ return [];
94
+ }
95
+ const out = [];
96
+ for (const entry of entries) {
97
+ const title = cleanTitle(entry.title);
98
+ const page = await pageFor(entry, title);
99
+ if (title && page !== null && !headingLevels.has(title))
100
+ headingLevels.set(title, level);
101
+ out.push({
102
+ title,
103
+ level,
104
+ page,
105
+ ...(typeof entry.url === "string" ? { url: entry.url } : {}),
106
+ children: await walk(entry.items ?? [], level + 1),
107
+ });
108
+ }
109
+ return out;
110
+ };
111
+ const outline = await walk(raw, 1);
112
+ return { outline, diagnostics: sink.all(), headingLevels };
113
+ }
114
+ /** Renders the tree for the human-facing streams, two spaces per level. */
115
+ export function formatOutline(entries) {
116
+ const lines = [];
117
+ const walk = (nodes) => {
118
+ for (const node of nodes) {
119
+ const indent = " ".repeat(node.level - 1);
120
+ lines.push(`${indent}${node.title} (p. ${node.page ?? "—"})`);
121
+ walk(node.children);
122
+ }
123
+ };
124
+ walk(entries);
125
+ return lines.length ? `${lines.join("\n")}\n` : "";
126
+ }
127
+ //# sourceMappingURL=outline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outline.js","sourceRoot":"","sources":["../../src/pdf/outline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import type { PdfDiagnostic } from "./types.js";
2
+ /**
3
+ * 64 MiB.
4
+ *
5
+ * Not the 256 MB a PDF's own size limits would suggest. The buffer is resident,
6
+ * it is then *transferred* into the parser, and pdf.js's object cache on top of
7
+ * it commonly runs three to five times the file size — so a 256 MB input is a
8
+ * multi-gigabyte process, and the failure is an OOM abort with no diagnostic at
9
+ * all. That is the shape of failure `MAX_DEPTH` exists to prevent in the ADF
10
+ * reader. Real corpora sit far below this: papers and forms are single-digit
11
+ * megabytes, scanned manuals twenty to eighty, and the tail past 64 MiB is
12
+ * image-only scans that carry no text layer to extract anyway.
13
+ *
14
+ * `--max-bytes` raises it, up to {@link MAX_INPUT_CEILING}.
15
+ */
16
+ export declare const MAX_INPUT_BYTES: number;
17
+ /** The highest `--max-bytes` will accept, so the flag cannot ask for an OOM. */
18
+ export declare const MAX_INPUT_CEILING: number;
19
+ /**
20
+ * How far in to look for `%PDF-`.
21
+ *
22
+ * The specification and every real reader tolerate leading bytes before the
23
+ * header, and files served through mail gateways routinely have them.
24
+ */
25
+ export declare const HEADER_SCAN_BYTES = 1024;
26
+ export interface ReadLimits {
27
+ maxBytes: number;
28
+ }
29
+ export type ReadResult = {
30
+ ok: true;
31
+ bytes: Uint8Array;
32
+ notices: PdfDiagnostic[];
33
+ } | {
34
+ ok: false;
35
+ diagnostic: PdfDiagnostic;
36
+ };
37
+ /**
38
+ * Reads a PDF, from a path or from stdin when `source` is `-`.
39
+ *
40
+ * The guards mirror `readSnippetSource` in `src/snippets.ts` and the ADF reader:
41
+ * resolve through symlinks, refuse anything that is not a regular file *on the
42
+ * open descriptor* so it cannot be swapped between the check and the read, open
43
+ * with `O_NONBLOCK` so a FIFO does not block until a writer appears, and cap the
44
+ * size. The NUL-byte rejection does not carry over — a PDF is binary.
45
+ */
46
+ export declare function readInput(source: string, limits: ReadLimits): Promise<ReadResult>;
47
+ export type PageRangeResult = {
48
+ ok: true;
49
+ pages: number[];
50
+ } | {
51
+ ok: false;
52
+ diagnostic: PdfDiagnostic;
53
+ };
54
+ /**
55
+ * Parses `--pages`, e.g. `1,3,5-8`, `-4`, `20-`.
56
+ *
57
+ * Validated against the real page count rather than trusted, and always emitted
58
+ * ascending and deduplicated regardless of the order it was written in —
59
+ * determinism, not convenience: `--pages 5,1` and `--pages 1,5` must produce
60
+ * byte-identical output.
61
+ */
62
+ export declare function parsePageRange(spec: string, pageCount: number): PageRangeResult;
@@ -0,0 +1,193 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { CODES, diagnostic } from "./diagnostics.js";
4
+ /**
5
+ * 64 MiB.
6
+ *
7
+ * Not the 256 MB a PDF's own size limits would suggest. The buffer is resident,
8
+ * it is then *transferred* into the parser, and pdf.js's object cache on top of
9
+ * it commonly runs three to five times the file size — so a 256 MB input is a
10
+ * multi-gigabyte process, and the failure is an OOM abort with no diagnostic at
11
+ * all. That is the shape of failure `MAX_DEPTH` exists to prevent in the ADF
12
+ * reader. Real corpora sit far below this: papers and forms are single-digit
13
+ * megabytes, scanned manuals twenty to eighty, and the tail past 64 MiB is
14
+ * image-only scans that carry no text layer to extract anyway.
15
+ *
16
+ * `--max-bytes` raises it, up to {@link MAX_INPUT_CEILING}.
17
+ */
18
+ export const MAX_INPUT_BYTES = 64 * 1024 * 1024;
19
+ /** The highest `--max-bytes` will accept, so the flag cannot ask for an OOM. */
20
+ export const MAX_INPUT_CEILING = 512 * 1024 * 1024;
21
+ /**
22
+ * How far in to look for `%PDF-`.
23
+ *
24
+ * The specification and every real reader tolerate leading bytes before the
25
+ * header, and files served through mail gateways routinely have them.
26
+ */
27
+ export const HEADER_SCAN_BYTES = 1024;
28
+ const SIGNATURE = Buffer.from("%PDF-", "latin1");
29
+ function refuse(code, message, remediation) {
30
+ return {
31
+ ok: false,
32
+ diagnostic: diagnostic({
33
+ code,
34
+ severity: "error",
35
+ quality: "unsupported",
36
+ message,
37
+ ...(remediation ? { remediation } : {}),
38
+ }),
39
+ };
40
+ }
41
+ /**
42
+ * Reads stdin to completion, refusing past the cap while it streams.
43
+ *
44
+ * Not `fs.readFileSync(0)`: when stdin is a pipe whose writer has not produced
45
+ * anything yet that throws `EAGAIN` rather than waiting, and piping a document
46
+ * in is a documented workflow. Streaming also means the cap is enforced as the
47
+ * bytes arrive — there is no `fstat` size to consult here, so a reader that
48
+ * buffered first and checked after would happily hold the whole oversized input
49
+ * before deciding it was too big.
50
+ */
51
+ async function readStdin(limit) {
52
+ const chunks = [];
53
+ let total = 0;
54
+ for await (const chunk of process.stdin) {
55
+ const buffer = chunk;
56
+ total += buffer.length;
57
+ if (total > limit) {
58
+ process.stdin.destroy();
59
+ return "too-large";
60
+ }
61
+ chunks.push(buffer);
62
+ }
63
+ return own(Buffer.concat(chunks));
64
+ }
65
+ /**
66
+ * Copies into an `ArrayBuffer` this process exclusively owns.
67
+ *
68
+ * `getDocument({ data })` **transfers and detaches** what it is given — verified
69
+ * against 6.3.289, where `byteLength` is 0 after the call. Node `Buffer`s below
70
+ * 8 KiB are views into a shared pooled allocator, so handing one over would
71
+ * detach a buffer other parts of the process are still using. Every path out of
72
+ * this module goes through here.
73
+ */
74
+ function own(source) {
75
+ const copy = new Uint8Array(source.byteLength);
76
+ copy.set(source);
77
+ return copy;
78
+ }
79
+ /** Locates `%PDF-` within the leading bytes; -1 when it is not there. */
80
+ function signatureOffset(bytes) {
81
+ const window = Buffer.from(bytes.buffer, bytes.byteOffset, Math.min(bytes.byteLength, HEADER_SCAN_BYTES + SIGNATURE.length));
82
+ return window.indexOf(SIGNATURE);
83
+ }
84
+ function leadingHex(bytes) {
85
+ return Buffer.from(bytes.subarray(0, 8)).toString("hex").replace(/(..)/g, "$1 ").trim();
86
+ }
87
+ /**
88
+ * Reads a PDF, from a path or from stdin when `source` is `-`.
89
+ *
90
+ * The guards mirror `readSnippetSource` in `src/snippets.ts` and the ADF reader:
91
+ * resolve through symlinks, refuse anything that is not a regular file *on the
92
+ * open descriptor* so it cannot be swapped between the check and the read, open
93
+ * with `O_NONBLOCK` so a FIFO does not block until a writer appears, and cap the
94
+ * size. The NUL-byte rejection does not carry over — a PDF is binary.
95
+ */
96
+ export async function readInput(source, limits) {
97
+ const notices = [];
98
+ let bytes;
99
+ if (source === "-") {
100
+ const streamed = await readStdin(limits.maxBytes);
101
+ if (streamed === "too-large")
102
+ return refuse(CODES.tooLarge, `Input on stdin is larger than ${limits.maxBytes} bytes`, "Raise the bound with --max-bytes, or convert a smaller document.");
103
+ bytes = streamed;
104
+ }
105
+ else {
106
+ const resolved = path.resolve(source);
107
+ let real;
108
+ try {
109
+ real = fs.realpathSync(resolved);
110
+ }
111
+ catch {
112
+ return refuse(CODES.notRegularFile, `Input file not found: ${source}`);
113
+ }
114
+ let descriptor;
115
+ try {
116
+ descriptor = fs.openSync(real, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK);
117
+ }
118
+ catch {
119
+ return refuse(CODES.notRegularFile, `Input file could not be opened: ${source}`);
120
+ }
121
+ try {
122
+ const stat = fs.fstatSync(descriptor);
123
+ if (!stat.isFile())
124
+ return refuse(CODES.notRegularFile, `Input path is not a regular file: ${source}`);
125
+ if (stat.size > limits.maxBytes)
126
+ return refuse(CODES.tooLarge, `Input is ${stat.size} bytes, larger than ${limits.maxBytes}`, "Raise the bound with --max-bytes, or convert a smaller document.");
127
+ bytes = own(fs.readFileSync(descriptor));
128
+ }
129
+ finally {
130
+ fs.closeSync(descriptor);
131
+ }
132
+ }
133
+ if (bytes.byteLength === 0)
134
+ return refuse(CODES.emptyInput, `Input is empty: ${source}`);
135
+ const offset = signatureOffset(bytes);
136
+ if (offset === -1)
137
+ return refuse(CODES.notAPdf, `Input has no %PDF- signature in its first ${HEADER_SCAN_BYTES} bytes ` +
138
+ `(begins ${leadingHex(bytes)})`, "Check that this is a PDF and not an HTML error page, an archive, or another document format.");
139
+ // Accepted, not stripped: the byte offsets in the cross-reference table are
140
+ // measured from the start of the file as it stands, so slicing the prefix off
141
+ // would invalidate every one of them. pdf.js handles the offset itself.
142
+ if (offset > 0)
143
+ notices.push(diagnostic({
144
+ code: CODES.leadingBytesIgnored,
145
+ quality: "exact",
146
+ message: `The %PDF- signature is at byte ${offset}, not at the start of the file`,
147
+ }));
148
+ return { ok: true, bytes, notices };
149
+ }
150
+ /**
151
+ * Parses `--pages`, e.g. `1,3,5-8`, `-4`, `20-`.
152
+ *
153
+ * Validated against the real page count rather than trusted, and always emitted
154
+ * ascending and deduplicated regardless of the order it was written in —
155
+ * determinism, not convenience: `--pages 5,1` and `--pages 1,5` must produce
156
+ * byte-identical output.
157
+ */
158
+ export function parsePageRange(spec, pageCount) {
159
+ const selected = new Set();
160
+ const invalid = (detail) => ({
161
+ ok: false,
162
+ diagnostic: diagnostic({
163
+ code: CODES.pageRangeInvalid,
164
+ severity: "error",
165
+ quality: "unsupported",
166
+ message: `Invalid --pages value: ${detail}`,
167
+ remediation: `This document has ${pageCount} page(s). Use a form like 1,3,5-8.`,
168
+ }),
169
+ });
170
+ for (const part of spec.split(",")) {
171
+ const token = part.trim();
172
+ if (!token)
173
+ return invalid(`empty range in "${spec}"`);
174
+ const match = /^(\d+)?(-)?(\d+)?$/.exec(token);
175
+ if (!match || (!match[1] && !match[3]))
176
+ return invalid(`"${token}" is not a page or a range`);
177
+ const [, rawStart, dash, rawEnd] = match;
178
+ const start = rawStart ? Number(rawStart) : 1;
179
+ const end = dash ? (rawEnd ? Number(rawEnd) : pageCount) : start;
180
+ if (!dash && rawStart && rawEnd)
181
+ return invalid(`"${token}" is not a page or a range`);
182
+ if (start < 1 || end < 1)
183
+ return invalid(`"${token}" names page 0; pages are 1-based`);
184
+ if (start > pageCount || end > pageCount)
185
+ return invalid(`"${token}" is outside 1-${pageCount}`);
186
+ if (start > end)
187
+ return invalid(`"${token}" runs backwards`);
188
+ for (let page = start; page <= end; page += 1)
189
+ selected.add(page);
190
+ }
191
+ return { ok: true, pages: [...selected].sort((a, b) => a - b) };
192
+ }
193
+ //# sourceMappingURL=read.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/pdf/read.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import { DiagnosticSink } from "./diagnostics.js";
2
+ import type { PdfStructNode } from "./document.js";
3
+ import type { PositionedRun } from "./text.js";
4
+ import type { Block, BlockKind, MappingQuality } from "./types.js";
5
+ /**
6
+ * How a structure-tree role maps onto a block.
7
+ *
8
+ * Data, deliberately, in the shape `FIDELITY` takes in
9
+ * `src/jira/adf/profile.ts`: a role a reader can look up beats a switch a reader
10
+ * has to trace. `block: null` means the role contributes nesting but emits
11
+ * nothing itself.
12
+ */
13
+ export interface RoleMapping {
14
+ block: BlockKind | null;
15
+ quality: MappingQuality;
16
+ /** Heading level, for the roles that fix one. */
17
+ level?: number;
18
+ note: string;
19
+ }
20
+ export declare const ROLE_FIDELITY: Record<string, RoleMapping>;
21
+ export interface StructResult {
22
+ blocks: Block[];
23
+ /** False when the page's tree carried nothing usable and geometry should run. */
24
+ usable: boolean;
25
+ }
26
+ /**
27
+ * Converts one page's structure tree into blocks.
28
+ *
29
+ * The pairing is by marked-content id: a `StructTreeContent` leaf's `id` and a
30
+ * `TextMarkedContent` item's `id` are the *same string* (`p5R_mc0`), verified
31
+ * against 6.3.289, and only `beginMarkedContentProps` carries one. `text.ts`
32
+ * has already attributed each run to its innermost id-bearing frame, so this
33
+ * only has to look runs up.
34
+ */
35
+ export declare function blocksFromStruct(tree: PdfStructNode | null, runs: PositionedRun[], sink: DiagnosticSink, page: number): StructResult;
@@ -0,0 +1,226 @@
1
+ import { CODES } from "./diagnostics.js";
2
+ export const ROLE_FIDELITY = {
3
+ Document: { block: null, quality: "exact", note: "A container; contributes nesting only." },
4
+ Part: { block: null, quality: "exact", note: "A container; contributes nesting only." },
5
+ Art: { block: null, quality: "exact", note: "A container; contributes nesting only." },
6
+ Sect: { block: null, quality: "exact", note: "A container; contributes nesting only." },
7
+ Div: { block: null, quality: "exact", note: "A container; contributes nesting only." },
8
+ NonStruct: { block: null, quality: "exact", note: "A container; contributes nesting only." },
9
+ // Real producers mark running heads and page numbers as artifacts, which is
10
+ // why the tagged path gets header and footer removal for free.
11
+ Artifact: { block: null, quality: "exact", note: "Page furniture; dropped, as intended." },
12
+ H1: { block: "heading", quality: "exact", level: 1, note: "A level 1 heading." },
13
+ H2: { block: "heading", quality: "exact", level: 2, note: "A level 2 heading." },
14
+ H3: { block: "heading", quality: "exact", level: 3, note: "A level 3 heading." },
15
+ H4: { block: "heading", quality: "exact", level: 4, note: "A level 4 heading." },
16
+ H5: { block: "heading", quality: "exact", level: 5, note: "A level 5 heading." },
17
+ H6: { block: "heading", quality: "exact", level: 6, note: "A level 6 heading." },
18
+ H: {
19
+ block: "heading",
20
+ quality: "approximate",
21
+ note: "A generic heading; its level is inferred.",
22
+ },
23
+ Title: {
24
+ block: "heading",
25
+ quality: "approximate",
26
+ level: 1,
27
+ note: "Treated as a level 1 heading.",
28
+ },
29
+ P: { block: "paragraph", quality: "exact", note: "A paragraph." },
30
+ L: {
31
+ block: "list",
32
+ quality: "approximate",
33
+ note: "A list; ordered-ness is inferred from its labels.",
34
+ },
35
+ LI: { block: "listItem", quality: "exact", note: "A list item." },
36
+ Lbl: { block: null, quality: "exact", note: "An item label; consumed as the marker." },
37
+ LBody: { block: null, quality: "exact", note: "An item body; its content is the item's." },
38
+ Index: { block: null, quality: "exact", note: "A container; contributes nesting only." },
39
+ Private: { block: null, quality: "exact", note: "A container; contributes nesting only." },
40
+ Aside: { block: null, quality: "exact", note: "A container; contributes nesting only." },
41
+ Table: { block: "table", quality: "exact", note: "A table." },
42
+ // Row groups must be containers, not blocks. Left unmodelled they fall to the
43
+ // AP219 path, which emits one paragraph of everything beneath — collapsing
44
+ // every row of a table head into a single line and losing the table.
45
+ THead: { block: null, quality: "exact", note: "A row group; its rows belong to the table." },
46
+ TBody: { block: null, quality: "exact", note: "A row group; its rows belong to the table." },
47
+ TFoot: { block: null, quality: "exact", note: "A row group; its rows belong to the table." },
48
+ TR: { block: "tableRow", quality: "exact", note: "A table row." },
49
+ TD: { block: "tableCell", quality: "exact", note: "A table cell." },
50
+ TH: { block: "tableCell", quality: "exact", note: "A header cell." },
51
+ BlockQuote: { block: "blockquote", quality: "exact", note: "A block quote." },
52
+ Code: { block: "code", quality: "approximate", note: "A code block; no language is recorded." },
53
+ Caption: {
54
+ block: "caption",
55
+ quality: "approximate",
56
+ note: "A caption, emitted as emphasized text.",
57
+ },
58
+ Figure: { block: "figure", quality: "approximate", note: "A figure; only its text survives." },
59
+ Formula: {
60
+ block: "paragraph",
61
+ quality: "approximate",
62
+ note: "A formula, flattened to its text.",
63
+ },
64
+ TOC: { block: "list", quality: "approximate", note: "A table of contents, flattened to a list." },
65
+ TOCI: { block: "listItem", quality: "approximate", note: "A table-of-contents entry." },
66
+ Span: { block: null, quality: "exact", note: "Inline; folded into the enclosing block." },
67
+ Quote: { block: null, quality: "exact", note: "Inline; folded into the enclosing block." },
68
+ Note: { block: null, quality: "approximate", note: "Inline; folded into the enclosing block." },
69
+ Reference: { block: null, quality: "exact", note: "Inline; folded into the enclosing block." },
70
+ BibEntry: { block: "paragraph", quality: "approximate", note: "A bibliography entry." },
71
+ Link: { block: null, quality: "approximate", note: "Text is kept; the href is not resolved." },
72
+ // Ruby and Warichu annotate East Asian text inline; their bases and
73
+ // annotations flatten into the enclosing block rather than becoming blocks.
74
+ Ruby: { block: null, quality: "approximate", note: "Inline; folded into the enclosing block." },
75
+ RB: { block: null, quality: "exact", note: "Inline; folded into the enclosing block." },
76
+ RT: { block: null, quality: "approximate", note: "Inline; folded into the enclosing block." },
77
+ RP: { block: null, quality: "exact", note: "Inline; folded into the enclosing block." },
78
+ Warichu: {
79
+ block: null,
80
+ quality: "approximate",
81
+ note: "Inline; folded into the enclosing block.",
82
+ },
83
+ WT: { block: null, quality: "exact", note: "Inline; folded into the enclosing block." },
84
+ WP: { block: null, quality: "exact", note: "Inline; folded into the enclosing block." },
85
+ // Interactive elements carry no readable content of their own.
86
+ Form: { block: null, quality: "unsupported", note: "A form field; it carries no text to emit." },
87
+ Annot: { block: null, quality: "unsupported", note: "An annotation; not part of the content." },
88
+ };
89
+ /** Guards against a malformed tree; the walk is recursive over hostile input. */
90
+ const MAX_STRUCT_DEPTH = 128;
91
+ function spansFor(runs) {
92
+ const ordered = [...runs].sort((a, b) => a.page - b.page || a.index - b.index);
93
+ const spans = [];
94
+ for (const run of ordered) {
95
+ const previous = spans[spans.length - 1];
96
+ // Coalesced before emission, or a paragraph split into forty runs renders as
97
+ // `**a****b****c**`.
98
+ if (previous && previous.bold === run.bold && previous.italic === run.italic) {
99
+ const needsSpace = !/\s$/.test(previous.text) && !/^\s/.test(run.text);
100
+ previous.text += needsSpace ? ` ${run.text}` : run.text;
101
+ continue;
102
+ }
103
+ spans.push({ text: run.text, bold: run.bold, italic: run.italic, code: false });
104
+ }
105
+ return spans;
106
+ }
107
+ /**
108
+ * Converts one page's structure tree into blocks.
109
+ *
110
+ * The pairing is by marked-content id: a `StructTreeContent` leaf's `id` and a
111
+ * `TextMarkedContent` item's `id` are the *same string* (`p5R_mc0`), verified
112
+ * against 6.3.289, and only `beginMarkedContentProps` carries one. `text.ts`
113
+ * has already attributed each run to its innermost id-bearing frame, so this
114
+ * only has to look runs up.
115
+ */
116
+ export function blocksFromStruct(tree, runs, sink, page) {
117
+ if (!tree)
118
+ return { blocks: [], usable: false };
119
+ const byMcid = new Map();
120
+ for (const run of runs) {
121
+ if (!run.mcid)
122
+ continue;
123
+ const bucket = byMcid.get(run.mcid);
124
+ if (bucket)
125
+ bucket.push(run);
126
+ else
127
+ byMcid.set(run.mcid, [run]);
128
+ }
129
+ if (byMcid.size === 0)
130
+ return { blocks: [], usable: false };
131
+ const provenance = (quality, role) => ({
132
+ path: "struct",
133
+ quality,
134
+ pages: [page],
135
+ ...(role ? { role } : {}),
136
+ });
137
+ /** Collects every run under a node, in document order. */
138
+ const gather = (node, depth) => {
139
+ if (depth > MAX_STRUCT_DEPTH)
140
+ return [];
141
+ if (node.type === "content" && node.id)
142
+ return byMcid.get(node.id) ?? [];
143
+ return (node.children ?? []).flatMap((child) => gather(child, depth + 1));
144
+ };
145
+ const walk = (node, depth) => {
146
+ if (depth > MAX_STRUCT_DEPTH)
147
+ return [];
148
+ const role = node.role;
149
+ if (!role || role === "Root")
150
+ return (node.children ?? []).flatMap((child) => walk(child, depth + 1));
151
+ const mapping = ROLE_FIDELITY[role];
152
+ if (!mapping) {
153
+ // The AD100 analogue, and the non-negotiable one: an unrecognized role
154
+ // reports and emits its text rather than disappearing. Dropping is the one
155
+ // degradation whose output is indistinguishable from success.
156
+ sink.add({
157
+ code: CODES.unknownRole,
158
+ quality: "unsupported",
159
+ message: `Structure role "${role}" is not modelled; its text was emitted as a paragraph`,
160
+ construct: role,
161
+ page,
162
+ });
163
+ const runsHere = gather(node, depth);
164
+ return runsHere.length
165
+ ? [
166
+ {
167
+ kind: "paragraph",
168
+ spans: spansFor(runsHere),
169
+ children: [],
170
+ provenance: provenance("unsupported", role),
171
+ },
172
+ ]
173
+ : [];
174
+ }
175
+ const children = (node.children ?? []).flatMap((child) => walk(child, depth + 1));
176
+ if (mapping.block === null)
177
+ return children;
178
+ if (mapping.quality === "approximate")
179
+ noteApproximation(role, sink, page);
180
+ const container = mapping.block === "list" ||
181
+ mapping.block === "listItem" ||
182
+ mapping.block === "table" ||
183
+ mapping.block === "tableRow" ||
184
+ mapping.block === "blockquote";
185
+ if (container)
186
+ return [
187
+ {
188
+ kind: mapping.block,
189
+ ...(mapping.block === "list" ? { ordered: false } : {}),
190
+ spans: [],
191
+ children,
192
+ provenance: provenance(mapping.quality, role),
193
+ },
194
+ ];
195
+ const runsHere = gather(node, depth);
196
+ const spans = spansFor(runsHere);
197
+ if (spans.length === 0 && children.length === 0)
198
+ return [];
199
+ return [
200
+ {
201
+ kind: mapping.block,
202
+ ...(mapping.level ? { level: mapping.level } : {}),
203
+ ...(mapping.block === "tableCell" ? { header: role === "TH" } : {}),
204
+ spans,
205
+ children: mapping.block === "tableCell" ? [] : children,
206
+ provenance: provenance(mapping.quality, role),
207
+ },
208
+ ];
209
+ };
210
+ return { blocks: walk(tree, 0), usable: true };
211
+ }
212
+ /** One code per approximating role, so a caller learns what a tag cost. */
213
+ function noteApproximation(role, sink, page) {
214
+ const mapping = ROLE_FIDELITY[role];
215
+ const code = role === "H"
216
+ ? CODES.headingLevelInferred
217
+ : role === "L" || role === "TOC"
218
+ ? CODES.listOrderingInferred
219
+ : role === "Figure"
220
+ ? CODES.figureTextOnly
221
+ : null;
222
+ if (!code)
223
+ return;
224
+ sink.add({ code, quality: "approximate", message: mapping.note, construct: role, page });
225
+ }
226
+ //# sourceMappingURL=struct.js.map