@unisim/doc 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 (90) hide show
  1. package/README.md +91 -0
  2. package/dist/convert.d.ts +54 -0
  3. package/dist/convert.d.ts.map +1 -0
  4. package/dist/convert.js +216 -0
  5. package/dist/convert.js.map +1 -0
  6. package/dist/image.d.ts +17 -0
  7. package/dist/image.d.ts.map +1 -0
  8. package/dist/image.js +66 -0
  9. package/dist/image.js.map +1 -0
  10. package/dist/index.d.ts +10 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +51 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/merge.d.ts +14 -0
  15. package/dist/merge.d.ts.map +1 -0
  16. package/dist/merge.js +51 -0
  17. package/dist/merge.js.map +1 -0
  18. package/dist/metrics.d.ts +19 -0
  19. package/dist/metrics.d.ts.map +1 -0
  20. package/dist/metrics.js +151 -0
  21. package/dist/metrics.js.map +1 -0
  22. package/dist/model.d.ts +114 -0
  23. package/dist/model.d.ts.map +1 -0
  24. package/dist/model.js +121 -0
  25. package/dist/model.js.map +1 -0
  26. package/dist/pdfcore.d.ts +118 -0
  27. package/dist/pdfcore.d.ts.map +1 -0
  28. package/dist/pdfcore.js +398 -0
  29. package/dist/pdfcore.js.map +1 -0
  30. package/dist/read/csv.d.ts +17 -0
  31. package/dist/read/csv.d.ts.map +1 -0
  32. package/dist/read/csv.js +167 -0
  33. package/dist/read/csv.js.map +1 -0
  34. package/dist/read/doc.d.ts +3 -0
  35. package/dist/read/doc.d.ts.map +1 -0
  36. package/dist/read/doc.js +360 -0
  37. package/dist/read/doc.js.map +1 -0
  38. package/dist/read/docx.d.ts +3 -0
  39. package/dist/read/docx.d.ts.map +1 -0
  40. package/dist/read/docx.js +455 -0
  41. package/dist/read/docx.js.map +1 -0
  42. package/dist/read/html.d.ts +4 -0
  43. package/dist/read/html.d.ts.map +1 -0
  44. package/dist/read/html.js +236 -0
  45. package/dist/read/html.js.map +1 -0
  46. package/dist/read/json.d.ts +9 -0
  47. package/dist/read/json.d.ts.map +1 -0
  48. package/dist/read/json.js +105 -0
  49. package/dist/read/json.js.map +1 -0
  50. package/dist/read/markdown.d.ts +4 -0
  51. package/dist/read/markdown.d.ts.map +1 -0
  52. package/dist/read/markdown.js +247 -0
  53. package/dist/read/markdown.js.map +1 -0
  54. package/dist/read/odt.d.ts +3 -0
  55. package/dist/read/odt.d.ts.map +1 -0
  56. package/dist/read/odt.js +359 -0
  57. package/dist/read/odt.js.map +1 -0
  58. package/dist/read/rtf.d.ts +3 -0
  59. package/dist/read/rtf.d.ts.map +1 -0
  60. package/dist/read/rtf.js +341 -0
  61. package/dist/read/rtf.js.map +1 -0
  62. package/dist/read/text.d.ts +4 -0
  63. package/dist/read/text.d.ts.map +1 -0
  64. package/dist/read/text.js +62 -0
  65. package/dist/read/text.js.map +1 -0
  66. package/dist/unzip.d.ts +23 -0
  67. package/dist/unzip.d.ts.map +1 -0
  68. package/dist/unzip.js +152 -0
  69. package/dist/unzip.js.map +1 -0
  70. package/dist/write/data.d.ts +16 -0
  71. package/dist/write/data.d.ts.map +1 -0
  72. package/dist/write/data.js +89 -0
  73. package/dist/write/data.js.map +1 -0
  74. package/dist/write/pdf.d.ts +10 -0
  75. package/dist/write/pdf.d.ts.map +1 -0
  76. package/dist/write/pdf.js +696 -0
  77. package/dist/write/pdf.js.map +1 -0
  78. package/dist/write/pdfSettings.d.ts +13 -0
  79. package/dist/write/pdfSettings.d.ts.map +1 -0
  80. package/dist/write/pdfSettings.js +26 -0
  81. package/dist/write/pdfSettings.js.map +1 -0
  82. package/dist/write/text.d.ts +5 -0
  83. package/dist/write/text.d.ts.map +1 -0
  84. package/dist/write/text.js +341 -0
  85. package/dist/write/text.js.map +1 -0
  86. package/dist/xml.d.ts +22 -0
  87. package/dist/xml.d.ts.map +1 -0
  88. package/dist/xml.js +83 -0
  89. package/dist/xml.js.map +1 -0
  90. package/package.json +31 -0
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # @unisim/doc
2
+
3
+ The suite's document stack. Reads Word, OpenDocument, RTF, HTML, Markdown, CSV,
4
+ JSON and plain text into one model, and writes that model back out as PDF, text,
5
+ HTML, Markdown, CSV or JSON.
6
+
7
+ **No runtime dependencies.** That is not incidental — one of its two consumers
8
+ (Universal Converter) is sold on being offline from first visit with nothing
9
+ fetched, so a dependency here would cost that product its pitch.
10
+
11
+ ```ts
12
+ import { readToDoc, convertDocument, DEFAULT_DOC_SETTINGS } from '@unisim/doc'
13
+
14
+ // The model, for a caller that wants to inspect or combine documents
15
+ const doc = await readToDoc(file)
16
+
17
+ // Or straight to an output
18
+ const { blob, ext, notices, pages } = await convertDocument(file, {
19
+ ...DEFAULT_DOC_SETTINGS,
20
+ format: 'pdf',
21
+ })
22
+ ```
23
+
24
+ `convertDocument` hands back the target **extension**, not a filename. Naming
25
+ the output file is the caller's job on purpose: the app knows what the source
26
+ was called and which of its own naming rules apply, and a library offering a
27
+ second opinion is how you end up chasing a `.png.png`.
28
+
29
+ ## Why it exists
30
+
31
+ It existed twice. On 2026-08-13 two sessions that could not see each other
32
+ shipped the same 1,200–1,500 lines on the same afternoon — a ZIP reader, a
33
+ namespace-agnostic XML layer, an OOXML walker, an ODF walker, and a
34
+ tokenise → wrap → paginate layout engine — into Universal PDF and Universal
35
+ Converter. Both files ended up carrying a header saying *"a fix to one is a fix
36
+ to neither."*
37
+
38
+ This is Converter's version, because it was the better one: a model designed as
39
+ a hub (nine readers × six writers) rather than grown out of a Markdown renderer,
40
+ with heading level 4, real image blocks, `underline`/`strike` on a run, and
41
+ `DocNotice` — a channel for reporting what a conversion **lost**, which the
42
+ other had no equivalent of. Universal PDF's model had exactly one thing this
43
+ lacked, `pagesetup`, which was merged in.
44
+
45
+ ## What it does not do
46
+
47
+ It re-typesets a document; it does not reproduce the page. Fonts, columns,
48
+ headers and footers, text boxes and floating shapes do not survive. Anything a
49
+ reader has to drop is reported through `RichDoc.notices` rather than discovered
50
+ later — the failure worth designing against is someone assuming the output is a
51
+ copy.
52
+
53
+ ## Two writers, on purpose
54
+
55
+ The PDF writer here goes through `pdfcore` — base-14 fonts, its own AFM tables,
56
+ no dependency. A consumer that already carries pdf-lib for other reasons
57
+ (Universal PDF does, for signing, redaction and forms) gains nothing by dropping
58
+ it, and can take the readers and the model from here while keeping its own
59
+ writer. The seam is that `write/pdf.ts` depends only on `pdfcore`'s small
60
+ surface — `addPage` / `drawText` / `drawRect` / `drawImage` / `widthOfText`.
61
+
62
+ ## Notes for anyone editing it
63
+
64
+ - **Imports carry a `.js` extension.** Not `.ts` (which needs
65
+ `allowImportingTsExtensions`, only legal with `noEmit`) and not extensionless
66
+ (emitted verbatim, so `dist/index.js` would import `./convert` and Node would
67
+ refuse it). The first build of this package made exactly that mistake.
68
+ - **The model is the intersection of what these formats can express, not the
69
+ union.** A DOCX can hold a text box rotated 3° over a chart; Markdown cannot.
70
+ Anything outside the vocabulary is flattened on the way *in*, by the reader
71
+ that knows what it meant.
72
+ - **`pagesetup` is an instruction, not a property**, because that is how the
73
+ source formats model it — orientation belongs to a section, and a section runs
74
+ until the next break. ⚠️ The two office formats express it in **opposite
75
+ directions**: OOXML's `w:sectPr` sits on the **last** paragraph of the section
76
+ it describes, ODF's `style:master-page-name` on the **first** paragraph of the
77
+ new one. Reading them the same way puts every change one page out.
78
+
79
+ ## Tests
80
+
81
+ ```
82
+ npm test
83
+ ```
84
+
85
+ Covers what is DOM-free: the hand-written PDF byte layer (including that the
86
+ xref offsets actually point at their objects — a reader that repairs a damaged
87
+ table will open a broken file, so "it opened in Chrome" proves nothing), pages
88
+ of differing sizes in one document, and the `setSize` guard rail. The rest is
89
+ DOM-bound and is covered by the consuming apps' browser suites — in particular
90
+ Universal PDF's `e2e/office-import.e2e.mjs`, which drives real `.docx`/`.odt`
91
+ fixtures and asserts on the page sizes pdf.js reads back out.
@@ -0,0 +1,54 @@
1
+ import type { DocNotice, RichDoc } from './model.js';
2
+ import { type PdfSettings } from './write/pdfSettings.js';
3
+ export type { PdfSettings, PaperSize, PageMargin, FontChoice } from './write/pdfSettings.js';
4
+ export { DEFAULT_PDF_SETTINGS } from './write/pdfSettings.js';
5
+ export type { DocNotice, RichDoc } from './model.js';
6
+ export type DocFormat = 'pdf' | 'txt' | 'html' | 'md' | 'csv' | 'json';
7
+ export interface DocSettings {
8
+ format: DocFormat;
9
+ pdf: PdfSettings;
10
+ /** CSV → JSON: read "42" as a number rather than a string. */
11
+ inferTypes: boolean;
12
+ }
13
+ export declare const DEFAULT_DOC_SETTINGS: DocSettings;
14
+ /** What came out, plus anything the person should be told about it. */
15
+ /**
16
+ * What a conversion produced.
17
+ *
18
+ * ⚠️ Carries the target EXTENSION, not a filename. Naming the output file is
19
+ * the caller's job and deliberately not this package's: the app knows what the
20
+ * source was called and which of its own naming rules apply, and a library that
21
+ * guessed would be a second opinion on a question that has already caused a
22
+ * `.png.png` hunt once. Callers pass this to their own `withExtension`.
23
+ */
24
+ export interface DocResult {
25
+ blob: Blob;
26
+ /** Target extension without the dot — 'pdf', 'txt', 'html', 'md', 'csv', 'json'. */
27
+ ext: DocFormat;
28
+ notices: DocNotice[];
29
+ /** Page count, for the formats that have pages. */
30
+ pages?: number;
31
+ }
32
+ /**
33
+ * Input extensions, grouped by the shape of what they produce.
34
+ *
35
+ * `tabular` sources also yield a `Grid`, which is the only thing that can
36
+ * become CSV or JSON. Everything else is a document, and asking for CSV from a
37
+ * Word file is refused by the UI rather than answered with a one-column sheet
38
+ * of its paragraphs.
39
+ */
40
+ export declare const DOC_INPUT_EXTS: readonly ["docx", "doc", "odt", "rtf", "txt", "text", "log", "md", "markdown", "html", "htm", "csv", "tsv", "json"];
41
+ export declare function isTabular(ext: string): boolean;
42
+ /** The targets a given input can actually reach. */
43
+ export declare function targetsFor(ext: string): DocFormat[];
44
+ /** Targets reachable by every file in a mixed queue. */
45
+ export declare function commonTargets(exts: readonly string[]): DocFormat[];
46
+ /**
47
+ * Read a file to a RichDoc, whichever kind it is.
48
+ *
49
+ * The one entry point the "join into one PDF" export needs — it wants the model
50
+ * and not a converted file, so that it can concatenate before writing once.
51
+ */
52
+ export declare function readToDoc(file: File): Promise<RichDoc>;
53
+ export declare function convertDocument(file: File, settings: DocSettings, onProgress?: (fraction: number) => void): Promise<DocResult>;
54
+ //# sourceMappingURL=convert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,SAAS,EAAQ,OAAO,EAAE,MAAM,YAAY,CAAA;AAI1D,OAAO,EAAwB,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAE/E,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAC5F,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEpD,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI,GAAG,KAAK,GAAG,MAAM,CAAA;AAEtE,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,SAAS,CAAA;IACjB,GAAG,EAAE,WAAW,CAAA;IAChB,8DAA8D;IAC9D,UAAU,EAAE,OAAO,CAAA;CACpB;AAED,eAAO,MAAM,oBAAoB,EAAE,WAIlC,CAAA;AAED,uEAAuE;AACvE;;;;;;;;GAQG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,IAAI,CAAA;IACV,oFAAoF;IACpF,GAAG,EAAE,SAAS,CAAA;IACd,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,qHAGjB,CAAA;AAIV,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE9C;AAED,oDAAoD;AACpD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,CAKnD;AAED,wDAAwD;AACxD,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,EAAE,CAKlE;AAOD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAE5D;AAsDD,wBAAsB,eAAe,CACnC,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,WAAW,EACrB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GACtC,OAAO,CAAC,SAAS,CAAC,CAqEpB"}
@@ -0,0 +1,216 @@
1
+ // ---------------------------------------------------------------------------
2
+ // The Files pipeline: read anything → RichDoc (or a Grid) → write anything.
3
+ //
4
+ // This file is the only place that knows which inputs pair with which outputs.
5
+ // Everything either side of it is ignorant of the other, which is what keeps a
6
+ // tenth input format to one new file.
7
+ //
8
+ // EVERY READER IS LAZILY IMPORTED. Together they are a fair amount of code —
9
+ // a CFB reader, an RTF tokeniser, two XML dialects — and somebody who opened
10
+ // the app to convert a JPEG should not download any of it. Dynamic `import()`
11
+ // puts each in its own chunk, fetched the first time a file of that type is
12
+ // converted, and never on the audio, images or video tabs. That is the same
13
+ // bargain LAME strikes, except these are already in the bundle rather than
14
+ // coming from npm, so it works offline: the service worker precaches them.
15
+ //
16
+ // ⚠️ THIS STACK EXISTS TWICE IN THE SUITE. Universal PDF shipped its own office
17
+ // import — `unzip.ts`, `officeXml.ts`, `docxToBlocks.ts`, `odtToBlocks.ts`,
18
+ // `blockPdf.ts` — at 16:24 on 2026-08-13 (commit c1f3faa), the same afternoon
19
+ // this landed, from a parallel session neither could see. The ZIP reader, the
20
+ // XML helpers, the OOXML/ODF walkers and the layout engine all overlap.
21
+ //
22
+ // They are not identical: this side reads .doc, .rtf, .csv, .json, .txt, .md and
23
+ // .html, embeds images, and writes with NO dependency; that side refuses .doc and
24
+ // .rtf and renders through pdf-lib. Consolidating them into @unisim/sdk is an
25
+ // open backlog item ("Universal Converter" in backlog-unisim.md). Until it is
26
+ // done, A FIX HERE IS A FIX TO NEITHER — check the twin.
27
+ // ---------------------------------------------------------------------------
28
+ // ⚠️ From `pdfSettings`, NOT from `write/pdf`. A static import of the renderer
29
+ // here would cancel the dynamic one below and pull the whole layout engine and
30
+ // its font tables into the main bundle — see that file's header.
31
+ import { DEFAULT_PDF_SETTINGS } from './write/pdfSettings.js';
32
+ export { DEFAULT_PDF_SETTINGS } from './write/pdfSettings.js';
33
+ export const DEFAULT_DOC_SETTINGS = {
34
+ format: 'pdf',
35
+ pdf: DEFAULT_PDF_SETTINGS,
36
+ inferTypes: true,
37
+ };
38
+ // ── What reads what ──────────────────────────────────────────────────────────
39
+ /**
40
+ * Input extensions, grouped by the shape of what they produce.
41
+ *
42
+ * `tabular` sources also yield a `Grid`, which is the only thing that can
43
+ * become CSV or JSON. Everything else is a document, and asking for CSV from a
44
+ * Word file is refused by the UI rather than answered with a one-column sheet
45
+ * of its paragraphs.
46
+ */
47
+ export const DOC_INPUT_EXTS = [
48
+ 'docx', 'doc', 'odt', 'rtf', 'txt', 'text', 'log', 'md', 'markdown',
49
+ 'html', 'htm', 'csv', 'tsv', 'json',
50
+ ];
51
+ const TABULAR_EXTS = new Set(['csv', 'tsv', 'json']);
52
+ export function isTabular(ext) {
53
+ return TABULAR_EXTS.has(ext);
54
+ }
55
+ /** The targets a given input can actually reach. */
56
+ export function targetsFor(ext) {
57
+ if (ext === 'csv' || ext === 'tsv')
58
+ return ['pdf', 'json', 'html', 'md', 'txt'];
59
+ if (ext === 'json')
60
+ return ['pdf', 'csv', 'html', 'md', 'txt'];
61
+ // A document has no grid behind it, so CSV and JSON are not on offer.
62
+ return ['pdf', 'txt', 'html', 'md'];
63
+ }
64
+ /** Targets reachable by every file in a mixed queue. */
65
+ export function commonTargets(exts) {
66
+ if (!exts.length)
67
+ return ['pdf', 'txt', 'html', 'md'];
68
+ return exts
69
+ .map(targetsFor)
70
+ .reduce((shared, next) => shared.filter((f) => next.includes(f)));
71
+ }
72
+ /**
73
+ * Read a file to a RichDoc, whichever kind it is.
74
+ *
75
+ * The one entry point the "join into one PDF" export needs — it wants the model
76
+ * and not a converted file, so that it can concatenate before writing once.
77
+ */
78
+ export async function readToDoc(file) {
79
+ return (await read(file, extensionOf(file.name))).doc;
80
+ }
81
+ async function read(file, ext) {
82
+ switch (ext) {
83
+ case 'docx': {
84
+ const { readDocx } = await import('./read/docx.js');
85
+ return { doc: await readDocx(file), grid: null };
86
+ }
87
+ case 'doc': {
88
+ const { readDoc } = await import('./read/doc.js');
89
+ return { doc: await readDoc(file), grid: null };
90
+ }
91
+ case 'odt': {
92
+ const { readOdt } = await import('./read/odt.js');
93
+ return { doc: await readOdt(file), grid: null };
94
+ }
95
+ case 'rtf': {
96
+ const { readRtf } = await import('./read/rtf.js');
97
+ return { doc: await readRtf(file), grid: null };
98
+ }
99
+ case 'md':
100
+ case 'markdown': {
101
+ const { readMarkdown } = await import('./read/markdown.js');
102
+ return { doc: await readMarkdown(file), grid: null };
103
+ }
104
+ case 'html':
105
+ case 'htm': {
106
+ const { readHtml } = await import('./read/html.js');
107
+ return { doc: await readHtml(file), grid: null };
108
+ }
109
+ case 'csv':
110
+ case 'tsv': {
111
+ const { readCsv } = await import('./read/csv.js');
112
+ const { doc, grid } = await readCsv(file);
113
+ return { doc, grid };
114
+ }
115
+ case 'json': {
116
+ const { readJson } = await import('./read/json.js');
117
+ const { doc, grid } = await readJson(file);
118
+ return { doc, grid };
119
+ }
120
+ case 'txt':
121
+ case 'text':
122
+ case 'log': {
123
+ const { readText } = await import('./read/text.js');
124
+ return { doc: await readText(file), grid: null };
125
+ }
126
+ default:
127
+ throw new Error(`${ext ? ext.toUpperCase() : 'That file type'} isn’t a document this can read.`);
128
+ }
129
+ }
130
+ // ── The conversion ───────────────────────────────────────────────────────────
131
+ export async function convertDocument(file, settings, onProgress) {
132
+ const ext = extensionOf(file.name);
133
+ onProgress?.(0.05);
134
+ const { doc, grid } = await read(file, ext);
135
+ // Reading is most of the work for the formats that need unzipping and
136
+ // decoding; writing is fast. The split reflects that rather than pretending
137
+ // to know a percentage it cannot measure.
138
+ onProgress?.(0.65);
139
+ const notices = [...doc.notices];
140
+ switch (settings.format) {
141
+ case 'pdf': {
142
+ const { docToPdf } = await import('./write/pdf.js');
143
+ const result = await docToPdf(doc, settings.pdf);
144
+ if (result.dropped.length) {
145
+ // Named, not counted. "12 characters could not be written" sends
146
+ // somebody hunting; showing them the actual glyphs makes it obvious
147
+ // at a glance that it was the Greek in one footnote.
148
+ const sample = result.dropped.slice(0, 12).join(' ');
149
+ notices.push({
150
+ severity: 'loss',
151
+ message: `Some characters aren’t in the PDF’s built-in fonts and came out as “?”: ${sample}` +
152
+ (result.dropped.length > 12 ? ` and ${result.dropped.length - 12} more.` : '.') +
153
+ ' Those alphabets need a font embedded in the file, which this converter doesn’t do.',
154
+ });
155
+ }
156
+ onProgress?.(1);
157
+ return { blob: result.blob, ext: 'pdf', notices, pages: result.pages };
158
+ }
159
+ case 'txt': {
160
+ const { docToText } = await import('./write/text.js');
161
+ onProgress?.(1);
162
+ return { blob: textBlob(docToText(doc), 'text/plain'), ext: 'txt', notices };
163
+ }
164
+ case 'html': {
165
+ const { docToHtml } = await import('./write/text.js');
166
+ onProgress?.(1);
167
+ return { blob: textBlob(docToHtml(doc), 'text/html'), ext: 'html', notices };
168
+ }
169
+ case 'md': {
170
+ const { docToMarkdown } = await import('./write/text.js');
171
+ onProgress?.(1);
172
+ return { blob: textBlob(docToMarkdown(doc), 'text/markdown'), ext: 'md', notices };
173
+ }
174
+ case 'csv': {
175
+ if (!grid)
176
+ throw new Error('Only a spreadsheet-shaped file can become a CSV.');
177
+ const { gridToCsv } = await import('./write/data.js');
178
+ onProgress?.(1);
179
+ return { blob: csvBlob(gridToCsv(grid)), ext: 'csv', notices };
180
+ }
181
+ case 'json': {
182
+ if (!grid)
183
+ throw new Error('Only a spreadsheet-shaped file can become JSON.');
184
+ const { gridToJson } = await import('./write/data.js');
185
+ onProgress?.(1);
186
+ return {
187
+ blob: textBlob(gridToJson(grid, { inferTypes: settings.inferTypes }), 'application/json'),
188
+ ext: 'json',
189
+ notices,
190
+ };
191
+ }
192
+ }
193
+ }
194
+ function textBlob(text, type) {
195
+ return new Blob([text], { type: `${type};charset=utf-8` });
196
+ }
197
+ /**
198
+ * A CSV, and the ONE place a byte-order mark is written.
199
+ *
200
+ * ⚠️ Excel opens a UTF-8 CSV as Windows-1252 unless the file starts with a BOM,
201
+ * so "Ré" arrives as "Ré" for anybody who double-clicks the result — and
202
+ * double-clicking it is what a CSV is for. The BOM is NOT put on the other text
203
+ * targets, and specifically not on JSON: RFC 8259 forbids one, and `JSON.parse`
204
+ * throws on a string that has it, so the tidy-looking "put it on everything"
205
+ * rule would hand back JSON that will not load.
206
+ */
207
+ function csvBlob(text) {
208
+ return new Blob(['', text], { type: 'text/csv;charset=utf-8' });
209
+ }
210
+ function extensionOf(filename) {
211
+ const dot = filename.lastIndexOf('.');
212
+ if (dot < 1 || dot === filename.length - 1)
213
+ return '';
214
+ return filename.slice(dot + 1).toLowerCase();
215
+ }
216
+ //# sourceMappingURL=convert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convert.js","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { type PdfImage } from './pdfcore.js';
2
+ export type PdfPageSource = PdfImage;
3
+ /**
4
+ * Draw a file to a canvas and read it back as JPEG.
5
+ *
6
+ * The white fill is not cosmetic: a canvas starts transparent, and drawing a
7
+ * PNG with alpha onto it and asking for JPEG gives BLACK where the transparency
8
+ * was on some browsers and white on others. Filling first makes it white
9
+ * everywhere, which is what somebody printing a page expects.
10
+ */
11
+ export declare function imageToJpeg(file: Blob, quality: number, maxEdge: number | null): Promise<PdfPageSource>;
12
+ /**
13
+ * Assemble pages into a PDF — one image per page, laid out at 72 dpi so a pixel
14
+ * is a point and the page is exactly the size of the picture on it.
15
+ */
16
+ export declare function imagesToPdf(pages: readonly PdfPageSource[], title: string): Blob;
17
+ //# sourceMappingURL=image.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAA;AAEzD,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAA;AAEpC;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,CAuB7G;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAQhF"}
package/dist/image.js ADDED
@@ -0,0 +1,66 @@
1
+ // Re-encoding a picture for embedding in a PDF.
2
+ //
3
+ // Lifted out of Universal Converter's `pdf.ts` when the document stack moved
4
+ // here, because the document WRITER needs it (a `{ kind: 'image' }` block) and
5
+ // the rest of that file — one-full-bleed-page-per-image — is a different
6
+ // product feature that stayed in the app.
7
+ //
8
+ // EVERY EMBEDDED IMAGE IS A JPEG, AND THAT IS A REAL TRADE. `/DCTDecode` is the
9
+ // one filter every reader supports and the only one that lets the compressed
10
+ // bytes go in untouched. The cost is that it is LOSSY (a PNG screenshot of text
11
+ // softens slightly) and that TRANSPARENCY IS FLATTENED onto white, because JPEG
12
+ // has no alpha. The alternative, `/FlateDecode` with raw RGB, needs a deflate
13
+ // encoder and would turn a 2 MP photo into a 20 MB page.
14
+ // ⚠️ `.js`, not `.ts` and not extensionless — the TypeScript-ESM convention
15
+ // that @unisim/media already follows, and it is load-bearing twice over. This
16
+ // package EMITS, so `allowImportingTsExtensions` (which needs noEmit) is out;
17
+ // and extensionless specifiers are emitted VERBATIM, so `dist/index.js` would
18
+ // import './convert' and Node would refuse it with ERR_MODULE_NOT_FOUND. That
19
+ // is not hypothetical: it is what the first build of this package did.
20
+ import { PdfDocument } from './pdfcore.js';
21
+ /**
22
+ * Draw a file to a canvas and read it back as JPEG.
23
+ *
24
+ * The white fill is not cosmetic: a canvas starts transparent, and drawing a
25
+ * PNG with alpha onto it and asking for JPEG gives BLACK where the transparency
26
+ * was on some browsers and white on others. Filling first makes it white
27
+ * everywhere, which is what somebody printing a page expects.
28
+ */
29
+ export async function imageToJpeg(file, quality, maxEdge) {
30
+ const bitmap = await createImageBitmap(file);
31
+ let { width, height } = bitmap;
32
+ if (maxEdge && Math.max(width, height) > maxEdge) {
33
+ const scale = maxEdge / Math.max(width, height);
34
+ width = Math.max(1, Math.round(width * scale));
35
+ height = Math.max(1, Math.round(height * scale));
36
+ }
37
+ const canvas = document.createElement('canvas');
38
+ canvas.width = width;
39
+ canvas.height = height;
40
+ const ctx = canvas.getContext('2d');
41
+ if (!ctx)
42
+ throw new Error('This browser would not give us a canvas to draw on.');
43
+ ctx.fillStyle = '#ffffff';
44
+ ctx.fillRect(0, 0, width, height);
45
+ ctx.drawImage(bitmap, 0, 0, width, height);
46
+ bitmap.close?.();
47
+ const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/jpeg', quality));
48
+ if (!blob)
49
+ throw new Error('This browser could not encode a JPEG.');
50
+ return { jpeg: new Uint8Array(await blob.arrayBuffer()), width, height };
51
+ }
52
+ /**
53
+ * Assemble pages into a PDF — one image per page, laid out at 72 dpi so a pixel
54
+ * is a point and the page is exactly the size of the picture on it.
55
+ */
56
+ export function imagesToPdf(pages, title) {
57
+ if (!pages.length)
58
+ throw new Error('A PDF needs at least one page.');
59
+ const pdf = new PdfDocument();
60
+ for (const image of pages) {
61
+ const page = pdf.addPage(image.width, image.height);
62
+ page.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
63
+ }
64
+ return pdf.save({ title });
65
+ }
66
+ //# sourceMappingURL=image.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image.js","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export type { Block, DocNotice, Grid, ListItem, RichDoc, Run, } from './model.js';
2
+ export { DEFAULT_DOC_SETTINGS, DEFAULT_PDF_SETTINGS, DOC_INPUT_EXTS, commonTargets, convertDocument, isTabular, readToDoc, targetsFor, } from './convert.js';
3
+ export type { DocFormat, DocResult, DocSettings, FontChoice, PageMargin, PaperSize, PdfSettings, } from './convert.js';
4
+ export { mergeToPdf, readForMerge } from './merge.js';
5
+ export type { MergePart } from './merge.js';
6
+ export { PdfDocument, PdfPage, rgb, widthOfText } from './pdfcore.js';
7
+ export type { FontId, PdfImage, Rgb } from './pdfcore.js';
8
+ export { imageToJpeg, imagesToPdf } from './image.js';
9
+ export type { PdfPageSource } from './image.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA2CA,YAAY,EACV,KAAK,EACL,SAAS,EACT,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,GAAG,GACJ,MAAM,YAAY,CAAA;AAGnB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,aAAa,EACb,eAAe,EACf,SAAS,EACT,SAAS,EACT,UAAU,GACX,MAAM,cAAc,CAAA;AACrB,YAAY,EACV,SAAS,EACT,SAAS,EACT,WAAW,EACX,UAAU,EACV,UAAU,EACV,SAAS,EACT,WAAW,GACZ,MAAM,cAAc,CAAA;AAGrB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACrD,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAK3C,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AACrE,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACrD,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,51 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @unisim/doc — the suite's document stack.
3
+ //
4
+ // WHY THIS PACKAGE EXISTS
5
+ // -----------------------
6
+ // It existed twice. On 2026-08-13, two sessions that could not see each other
7
+ // shipped the same 1,200–1,500 lines on the same afternoon: Universal PDF's
8
+ // `unzip.ts` / `officeXml.ts` / `docxToBlocks.ts` / `odtToBlocks.ts` /
9
+ // `blockPdf.ts`, and Universal Converter's `doc/` tree. A ZIP reader, a
10
+ // namespace-agnostic XML layer, an OOXML walker, an ODF walker, and a
11
+ // tokenise → wrap → paginate layout engine — twice, and diverging from the day
12
+ // they landed. Both files carried a header saying "a fix to one is a fix to
13
+ // neither".
14
+ //
15
+ // This is Converter's version, because Converter's was ahead on substance: a
16
+ // model designed as a hub (nine readers × six writers) rather than grown out of
17
+ // a Markdown renderer, with heading level 4, real image blocks, `underline` and
18
+ // `strike` on a run, and `DocNotice` — a channel for reporting what a
19
+ // conversion LOST, which the other had no equivalent of. It also fixed defects
20
+ // the original still had: smart quotes and dashes survive (Universal PDF's
21
+ // `sanitize()` flattened '’' to "'" and '—' to '-', though both are WinAnsi and
22
+ // encode fine), and a hard line break inside a paragraph is honoured.
23
+ //
24
+ // Universal PDF's model had exactly one thing this lacked — `pagesetup`, added
25
+ // 2026-08-20 for per-page orientation on import — so the merge is a superset.
26
+ //
27
+ // ONE MODEL AND ONE ENGINE, BUT TWO WRITERS ON PURPOSE
28
+ // ----------------------------------------------------
29
+ // The obvious next step, unifying the PDF writer too, is the wrong one. This
30
+ // package writes PDFs through `pdfcore` — its own base-14 fonts and AFM tables,
31
+ // no dependency — because Universal Converter's whole pitch is
32
+ // offline-from-first-visit with nothing fetched. Universal PDF carries pdf-lib
33
+ // anyway for signing, redaction and forms, so it gains nothing by dropping it.
34
+ // A consumer that would rather render through pdf-lib can take the readers and
35
+ // the model from here and keep its own writer; the seam is that `write/pdf.ts`
36
+ // depends only on `pdfcore`'s small surface (addPage / drawText / drawRect /
37
+ // drawImage / widthOfText), not on the rest of the package.
38
+ //
39
+ // NO RUNTIME DEPENDENCIES, and it should stay that way — that constraint is the
40
+ // product for one of its two consumers.
41
+ // ---------------------------------------------------------------------------
42
+ // ---- Reading, writing, and the orchestration over both ---------------------
43
+ export { DEFAULT_DOC_SETTINGS, DEFAULT_PDF_SETTINGS, DOC_INPUT_EXTS, commonTargets, convertDocument, isTabular, readToDoc, targetsFor, } from './convert.js';
44
+ // ---- Joining several documents into one -----------------------------------
45
+ export { mergeToPdf, readForMerge } from './merge.js';
46
+ // ---- The PDF primitives, for a consumer that wants them directly ----------
47
+ // Universal Converter's "images → one PDF" feature builds pages itself rather
48
+ // than going through the document model, so the writer surface is public.
49
+ export { PdfDocument, PdfPage, rgb, widthOfText } from './pdfcore.js';
50
+ export { imageToJpeg, imagesToPdf } from './image.js';
51
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,4CAA4C;AAC5C,EAAE;AACF,0BAA0B;AAC1B,0BAA0B;AAC1B,8EAA8E;AAC9E,4EAA4E;AAC5E,uEAAuE;AACvE,wEAAwE;AACxE,sEAAsE;AACtE,+EAA+E;AAC/E,4EAA4E;AAC5E,YAAY;AACZ,EAAE;AACF,6EAA6E;AAC7E,gFAAgF;AAChF,gFAAgF;AAChF,sEAAsE;AACtE,+EAA+E;AAC/E,2EAA2E;AAC3E,gFAAgF;AAChF,sEAAsE;AACtE,EAAE;AACF,+EAA+E;AAC/E,8EAA8E;AAC9E,EAAE;AACF,uDAAuD;AACvD,uDAAuD;AACvD,6EAA6E;AAC7E,gFAAgF;AAChF,+DAA+D;AAC/D,+EAA+E;AAC/E,+EAA+E;AAC/E,+EAA+E;AAC/E,+EAA+E;AAC/E,6EAA6E;AAC7E,4DAA4D;AAC5D,EAAE;AACF,gFAAgF;AAChF,wCAAwC;AACxC,8EAA8E;AAY9E,+EAA+E;AAC/E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,aAAa,EACb,eAAe,EACf,SAAS,EACT,SAAS,EACT,UAAU,GACX,MAAM,cAAc,CAAA;AAWrB,8EAA8E;AAC9E,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAGrD,8EAA8E;AAC9E,8EAA8E;AAC9E,0EAA0E;AAC1E,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAErE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,14 @@
1
+ import { type RichDoc } from './model.js';
2
+ import type { PdfSettings } from './write/pdfSettings.js';
3
+ export interface MergePart {
4
+ name: string;
5
+ doc: RichDoc;
6
+ }
7
+ /** Read one file to a RichDoc, whichever kind it is. */
8
+ export declare function readForMerge(file: File): Promise<MergePart>;
9
+ export declare function mergeToPdf(parts: readonly MergePart[], settings: PdfSettings): Promise<{
10
+ blob: Blob;
11
+ pages: number;
12
+ notices: RichDoc['notices'];
13
+ }>;
14
+ //# sourceMappingURL=merge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAeA,OAAO,EAAa,KAAK,OAAO,EAAE,MAAM,YAAY,CAAA;AAEpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAEzD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,OAAO,CAAA;CACb;AAED,wDAAwD;AACxD,wBAAsB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,SAAS,CAAC,CAGjE;AAED,wBAAsB,UAAU,CAC9B,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;CAAE,CAAC,CAgCrE"}
package/dist/merge.js ADDED
@@ -0,0 +1,51 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Several documents → one PDF.
3
+ //
4
+ // Deliberately NOT a second pipeline. Each file goes through the same reader it
5
+ // would on its own, the resulting RichDocs are concatenated into one, and that
6
+ // goes through the same writer with the same settings. The only thing this file
7
+ // adds is the join: a page break between documents and a heading naming each.
8
+ //
9
+ // The alternative — writing each to its own PDF and stitching the byte streams
10
+ // — needs a PDF *parser* to renumber every object, which is the thing
11
+ // `pdfcore.ts` explicitly does not do. Joining at the model level is both less
12
+ // code and a better result: the page numbers run 1..n across the whole thing
13
+ // rather than restarting per document.
14
+ // ---------------------------------------------------------------------------
15
+ import { addNotice } from './model.js';
16
+ import { docToPdf } from './write/pdf.js';
17
+ /** Read one file to a RichDoc, whichever kind it is. */
18
+ export async function readForMerge(file) {
19
+ const { readToDoc } = await import('./index.js');
20
+ return { name: file.name, doc: await readToDoc(file) };
21
+ }
22
+ export async function mergeToPdf(parts, settings) {
23
+ if (!parts.length)
24
+ throw new Error('There are no documents to join.');
25
+ const merged = { blocks: [], notices: [], title: 'Documents' };
26
+ parts.forEach((part, index) => {
27
+ // A page break BEFORE each document except the first: putting one after
28
+ // each instead leaves a blank final page.
29
+ if (index > 0)
30
+ merged.blocks.push({ kind: 'pagebreak' });
31
+ // The filename as a heading. Without it a joined PDF is one undifferentiated
32
+ // run of text and there is no way to tell where one document ended.
33
+ merged.blocks.push({
34
+ kind: 'heading',
35
+ level: 1,
36
+ runs: [{ text: part.name.replace(/\.[^.]+$/, '') }],
37
+ });
38
+ merged.blocks.push(...part.doc.blocks);
39
+ for (const notice of part.doc.notices) {
40
+ // Prefixed with the filename: in a joined batch "only its text could be
41
+ // read" is useless without knowing which of the six it refers to.
42
+ addNotice(merged, `${part.name}: ${notice.message}`, notice.severity);
43
+ }
44
+ });
45
+ const result = await docToPdf(merged, settings);
46
+ if (result.dropped.length) {
47
+ addNotice(merged, `Some characters aren’t in the PDF’s built-in fonts and came out as “?”: ${result.dropped.slice(0, 12).join(' ')}`);
48
+ }
49
+ return { blob: result.blob, pages: result.pages, notices: merged.notices };
50
+ }
51
+ //# sourceMappingURL=merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.js","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,+BAA+B;AAC/B,EAAE;AACF,gFAAgF;AAChF,+EAA+E;AAC/E,gFAAgF;AAChF,8EAA8E;AAC9E,EAAE;AACF,+EAA+E;AAC/E,sEAAsE;AACtE,+EAA+E;AAC/E,6EAA6E;AAC7E,uCAAuC;AACvC,8EAA8E;AAE9E,OAAO,EAAE,SAAS,EAAgB,MAAM,YAAY,CAAA;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAQzC,wDAAwD;AACxD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAU;IAC3C,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAA;IAChD,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,SAAS,CAAC,IAAI,CAAC,EAAE,CAAA;AACxD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,KAA2B,EAC3B,QAAqB;IAErB,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IAErE,MAAM,MAAM,GAAY,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAA;IAEvE,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAC5B,wEAAwE;QACxE,0CAA0C;QAC1C,IAAI,KAAK,GAAG,CAAC;YAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAA;QACxD,6EAA6E;QAC7E,oEAAoE;QACpE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;YACjB,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,CAAC;YACR,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,EAAE,CAAC;SACpD,CAAC,CAAA;QACF,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QACtC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;YACtC,wEAAwE;YACxE,kEAAkE;YAClE,SAAS,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAA;QACvE,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IAC/C,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QAC1B,SAAS,CACP,MAAM,EACN,2EAA2E,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CACnH,CAAA;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAA;AAC5E,CAAC"}