@stll/folio-core 0.37.2 → 0.37.3

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 (51) hide show
  1. package/dist/ai-edits/table-cell-mutations.js +10 -5
  2. package/dist/ai-edits/table-template.js +19 -5
  3. package/dist/controller/hiddenEditorManager.js +11 -0
  4. package/dist/controller/layoutPipeline.js +1 -1
  5. package/dist/display-list/build/textBoxPrimitives.js +34 -1
  6. package/dist/display-list/dom/renderDisplayListToDom.js +5 -2
  7. package/dist/display-list/types.d.ts +8 -4
  8. package/dist/docx/paragraphPropertySource.d.ts +82 -2
  9. package/dist/docx/paragraphPropertySource.js +569 -3
  10. package/dist/docx/parser.js +9 -0
  11. package/dist/docx/server/materializeYjsDocx.d.ts +1 -1
  12. package/dist/docx/server/materializeYjsDocx.js +21 -2
  13. package/dist/headless-layout.js +1 -1
  14. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +7 -1
  15. package/dist/layout-bridge/convert/headerFooterLayout.js +20 -3
  16. package/dist/layout-bridge/convert/toFlowBlocks.js +43 -12
  17. package/dist/layout-engine/index.js +20 -10
  18. package/dist/layout-engine/measure/measureBlocks.d.ts +7 -3
  19. package/dist/layout-engine/measure/measureBlocks.js +10 -7
  20. package/dist/layout-engine/measure/measureParagraph.d.ts +2 -0
  21. package/dist/layout-engine/measure/measureParagraph.js +1 -1
  22. package/dist/layout-engine/paginator.d.ts +1 -1
  23. package/dist/layout-engine/paginator.js +41 -4
  24. package/dist/layout-engine/textBoxFlow.d.ts +2 -0
  25. package/dist/layout-engine/textBoxFlow.js +9 -5
  26. package/dist/layout-engine/types.d.ts +6 -0
  27. package/dist/layout-painter/documentColors.d.ts +10 -1
  28. package/dist/layout-painter/documentColors.js +16 -1
  29. package/dist/layout-painter/renderParagraph.js +9 -24
  30. package/dist/layout-painter/renderTable.js +3 -3
  31. package/dist/layout-painter/renderTextBox.js +2 -1
  32. package/dist/pdf/pageSpace.d.ts +12 -1
  33. package/dist/pdf/pageSpace.js +24 -1
  34. package/dist/pdf/paint.js +2 -2
  35. package/dist/prosemirror/attrs/index.js +19 -8
  36. package/dist/prosemirror/commands/comments.js +12 -3
  37. package/dist/prosemirror/commands/tableCellMergeResolution.js +18 -11
  38. package/dist/prosemirror/conversion/fromProseDoc.js +94 -21
  39. package/dist/prosemirror/conversion/toProseDoc.js +100 -8
  40. package/dist/prosemirror/extensions/core/DocExtension.js +2 -0
  41. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -0
  42. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +5 -1
  43. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +132 -41
  44. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +2 -0
  45. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +7 -0
  46. package/dist/prosemirror/schema/nodes.d.ts +3 -1
  47. package/dist/prosemirror/yjsParagraphSourceContract.d.ts +9 -0
  48. package/dist/prosemirror/yjsParagraphSourceContract.js +26 -0
  49. package/dist/utils/rotationBoundingBox.d.ts +5 -1
  50. package/dist/utils/rotationBoundingBox.js +9 -1
  51. package/package.json +2 -2
@@ -1,27 +1,151 @@
1
1
  import { visitDocxParagraphs } from "./paragraphTraversal.js";
2
- import { panic } from "better-result";
2
+ import { TaggedError, panic } from "better-result";
3
+ import { PluginKey } from "prosemirror-state";
3
4
  //#region src/docx/paragraphPropertySource.ts
4
5
  const paragraphPropertySources = /* @__PURE__ */ new WeakMap();
5
6
  const paragraphPropertySourceOwners = /* @__PURE__ */ new WeakMap();
6
7
  const proseParagraphSourceOwners = /* @__PURE__ */ new WeakMap();
7
8
  const paragraphPropertySourceCandidates = /* @__PURE__ */ new WeakMap();
8
9
  const paragraphPropertySourceTransferIds = /* @__PURE__ */ new WeakMap();
10
+ const paragraphPropertySourceTokens = /* @__PURE__ */ new WeakMap();
11
+ const documentParagraphPropertySourceBindingBrand = Symbol("documentParagraphPropertySourceBinding");
12
+ const documentParagraphPropertySourceBinding = Symbol("paragraphPropertySourceBinding");
13
+ const PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR = "_docxParagraphSourceToken";
14
+ const PROSE_PARAGRAPH_SOURCE_CONTRACT_ATTR = "_docxParagraphSourceContract";
15
+ /** Durable identity nested inside opaque collapsed-cell ProseMirror attrs; stripped on restore. */
16
+ const TABLE_CELL_PARAGRAPH_SOURCE_BINDING_ATTR = "_docxParagraphSourceBinding";
17
+ const PARAGRAPH_SOURCE_TOKEN_VERSION = "folio-ppr-v1";
18
+ const PARAGRAPH_SOURCE_TOKEN_PREFIX = "p1d";
19
+ const SHA256_HEX = /^[0-9a-f]{64}$/;
20
+ const SOURCE_TOKEN = /^p1d:([0-9a-f]{32}):(0|[1-9a-z][0-9a-z]*)$/;
21
+ const PARAGRAPH_PROPERTY_SOURCE_VALIDATION_CODES = [
22
+ "contract_mismatch",
23
+ "duplicate_token",
24
+ "invalid_token",
25
+ "unknown_token"
26
+ ];
27
+ const TABLE_CELL_PARAGRAPH_SOURCE_PAYLOAD_ERROR_CLASSIFICATIONS = [
28
+ "complexity_limit",
29
+ "cyclic_or_aliased_graph",
30
+ "depth_limit",
31
+ "expected_array",
32
+ "expected_block",
33
+ "expected_cell",
34
+ "expected_inline_content",
35
+ "expected_paragraph",
36
+ "expected_record",
37
+ "expected_row",
38
+ "invalid_binding",
39
+ "invalid_block_type",
40
+ "invalid_cell_type",
41
+ "invalid_graph_value",
42
+ "invalid_inline_content_type",
43
+ "invalid_paragraph_type",
44
+ "invalid_row_type",
45
+ "malformed_source_token"
46
+ ];
47
+ var ParagraphPropertySourceValidationError = class extends TaggedError("ParagraphPropertySourceValidationError") {};
48
+ const contractForDigest = (sourceDigest) => {
49
+ if (!SHA256_HEX.test(sourceDigest)) panic("Paragraph-property source digest must be lowercase SHA-256 hex");
50
+ return `${PARAGRAPH_SOURCE_TOKEN_VERSION}:${sourceDigest}`;
51
+ };
52
+ const fingerprintFromContract = (contract) => {
53
+ const prefix = `${PARAGRAPH_SOURCE_TOKEN_VERSION}:`;
54
+ const digest = contract.startsWith(prefix) ? contract.slice(prefix.length) : "";
55
+ return SHA256_HEX.test(digest) ? digest.slice(0, 32) : null;
56
+ };
57
+ const tokenForOrdinal = (contract, ordinal) => {
58
+ const fingerprint = fingerprintFromContract(contract);
59
+ if (!fingerprint) panic("Cannot mint a paragraph-property token for an invalid source contract");
60
+ return `${PARAGRAPH_SOURCE_TOKEN_PREFIX}:${fingerprint}:${ordinal.toString(36)}`;
61
+ };
62
+ const isDocumentParagraphPropertySourceBinding = (value) => typeof value === "object" && value !== null && documentParagraphPropertySourceBindingBrand in value && value[documentParagraphPropertySourceBindingBrand] === true && "contract" in value && typeof value.contract === "string" && "sourceOwners" in value && value.sourceOwners instanceof Set && "sources" in value && value.sources instanceof Map;
63
+ const setDocumentParagraphPropertySourceBinding = (document, binding) => {
64
+ if (!fingerprintFromContract(binding.contract)) panic("Cannot attach an invalid paragraph-property source contract");
65
+ if ((Object.hasOwn(document, documentParagraphPropertySourceBinding) ? Reflect.get(document, documentParagraphPropertySourceBinding) : void 0) === binding) return;
66
+ if (!Reflect.defineProperty(document, documentParagraphPropertySourceBinding, {
67
+ enumerable: true,
68
+ value: binding
69
+ })) panic("Cannot attach the paragraph-property source contract to the document");
70
+ };
71
+ const isParagraphPropertySourceToken = (token) => typeof token === "string" && SOURCE_TOKEN.test(token);
72
+ /** True only for a body-story token derived from the exact source contract. */
73
+ const paragraphPropertySourceTokenMatchesContract = (token, contract) => {
74
+ if (!isParagraphPropertySourceToken(token)) return false;
75
+ const match = SOURCE_TOKEN.exec(token);
76
+ const fingerprint = fingerprintFromContract(contract);
77
+ return match !== null && fingerprint !== null && match[1] === fingerprint;
78
+ };
79
+ /** Visit the v1 provenance scope in canonical OOXML body-story order. */
80
+ const visitDocumentStoryParagraphs = (content, visit) => {
81
+ visitDocxParagraphs({ documentBody: { content } }, visit);
82
+ };
9
83
  const assignParagraphPropertySource = (paragraph, source) => {
10
84
  paragraphPropertySources.set(paragraph, source);
11
85
  paragraphPropertySourceOwners.set(paragraph, paragraph);
12
86
  };
13
87
  const getParagraphPropertySource = (paragraph) => paragraphPropertySources.get(paragraph);
14
- const copyParagraphPropertySource = (target, source) => {
88
+ /** Copy the captured `w:pPr` without claiming the source paragraph's durable identity. */
89
+ const copyParagraphPropertyCapture = (target, source) => {
15
90
  const propertySource = paragraphPropertySources.get(source);
16
91
  if (propertySource) {
17
92
  paragraphPropertySources.set(target, { ...propertySource });
18
93
  paragraphPropertySourceOwners.set(target, paragraphPropertySourceOwners.get(source) ?? source);
19
94
  }
95
+ };
96
+ const copyParagraphPropertySource = (target, source) => {
97
+ copyParagraphPropertyCapture(target, source);
20
98
  const transferId = paragraphPropertySourceTransferIds.get(source);
21
99
  if (transferId) paragraphPropertySourceTransferIds.set(target, transferId);
22
100
  const candidate = paragraphPropertySourceCandidates.get(source);
23
101
  if (candidate) paragraphPropertySourceCandidates.set(target, candidate);
102
+ const token = paragraphPropertySourceTokens.get(source);
103
+ if (token) paragraphPropertySourceTokens.set(target, token);
24
104
  };
105
+ /** Bind parsed body paragraphs to one exact source package. */
106
+ const assignDocumentParagraphPropertySourceContract = (document, sourceDigest) => {
107
+ const contract = contractForDigest(sourceDigest);
108
+ const sources = /* @__PURE__ */ new Map();
109
+ let ordinal = 0;
110
+ visitDocumentStoryParagraphs(document.package.document.content, (paragraph) => {
111
+ const token = tokenForOrdinal(contract, ordinal);
112
+ paragraphPropertySourceTokens.set(paragraph, token);
113
+ sources.set(token, paragraph);
114
+ ordinal += 1;
115
+ });
116
+ setDocumentParagraphPropertySourceBinding(document, Object.freeze({
117
+ [documentParagraphPropertySourceBindingBrand]: true,
118
+ contract,
119
+ sourceOwners: new Set(sources.values()),
120
+ sources
121
+ }));
122
+ };
123
+ const getDocumentParagraphPropertySourceBinding = (document) => {
124
+ if (!Object.hasOwn(document, documentParagraphPropertySourceBinding)) return;
125
+ const binding = Reflect.get(document, documentParagraphPropertySourceBinding);
126
+ if (!isDocumentParagraphPropertySourceBinding(binding) || !fingerprintFromContract(binding.contract)) panic("The document carries an invalid paragraph-property source contract");
127
+ return binding;
128
+ };
129
+ const getDocumentParagraphPropertySourceContract = (document) => getDocumentParagraphPropertySourceBinding(document)?.contract;
130
+ const copyDocumentParagraphPropertySources = (document) => {
131
+ const sources = getDocumentParagraphPropertySourceBinding(document)?.sources;
132
+ return sources ? new Map(sources) : void 0;
133
+ };
134
+ const paragraphPropertySourceBelongsToDocument = (paragraph, document) => {
135
+ const owner = paragraphPropertySourceOwners.get(paragraph);
136
+ const binding = getDocumentParagraphPropertySourceBinding(document);
137
+ return owner !== void 0 && binding !== void 0 && binding.sourceOwners.has(owner);
138
+ };
139
+ const getProseDocumentParagraphPropertySourceContract = (document) => {
140
+ const contract = document.attrs[PROSE_PARAGRAPH_SOURCE_CONTRACT_ATTR];
141
+ return typeof contract === "string" && fingerprintFromContract(contract) ? contract : null;
142
+ };
143
+ const getProseParagraphPropertySourceToken = (paragraph) => paragraph.attrs[PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR];
144
+ const copyDocumentParagraphPropertySourceContract = (target, source) => {
145
+ const binding = getDocumentParagraphPropertySourceBinding(source);
146
+ if (binding) setDocumentParagraphPropertySourceBinding(target, binding);
147
+ };
148
+ const getParagraphPropertySourceToken = (paragraph) => paragraphPropertySourceTokens.get(paragraph);
25
149
  /** Clone a paragraph while deliberately retaining its parsed `w:pPr` owner. */
26
150
  const cloneParagraphWithPropertySource = (paragraph, overrides) => {
27
151
  const cloned = {
@@ -62,11 +186,436 @@ const cloneDocumentWithParagraphPropertySources = (document) => {
62
186
  if (!target) panic("The cloned document lost a paragraph owner.");
63
187
  copyParagraphPropertySource(target, source);
64
188
  }
189
+ copyDocumentParagraphPropertySourceContract(cloned, document);
190
+ return cloned;
191
+ };
192
+ const paragraphsInTableCells = (cells) => {
193
+ const paragraphs = [];
194
+ for (const cell of cells) visitDocxParagraphs({ documentBody: { content: cell.content } }, (paragraph) => paragraphs.push(paragraph));
195
+ return paragraphs;
196
+ };
197
+ const isPropertySourceBindingRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
198
+ const inspectTableCellParagraphPropertySourceBinding = (paragraph) => {
199
+ if (!Object.hasOwn(paragraph, "_docxParagraphSourceBinding")) return { status: "absent" };
200
+ const value = Reflect.get(paragraph, TABLE_CELL_PARAGRAPH_SOURCE_BINDING_ATTR);
201
+ if (!isPropertySourceBindingRecord(value)) return { status: "invalid" };
202
+ const keys = Reflect.ownKeys(value);
203
+ if (keys.length === 1 && keys[0] === "type" && Object.hasOwn(value, "type") && value["type"] === "authored") return {
204
+ binding: { type: "authored" },
205
+ status: "authored"
206
+ };
207
+ if (keys.length === 2 && keys.includes("type") && keys.includes("token") && Object.hasOwn(value, "type") && value["type"] === "source" && Object.hasOwn(value, "token") && typeof value["token"] === "string") return {
208
+ binding: {
209
+ token: value["token"],
210
+ type: "source"
211
+ },
212
+ status: "source"
213
+ };
214
+ return { status: "invalid" };
215
+ };
216
+ const TABLE_CELL_PARAGRAPH_SOURCE_MAX_DEPTH = 128;
217
+ const TABLE_CELL_PARAGRAPH_SOURCE_MAX_VALUES = 1e5;
218
+ const ARRAY_INDEX = /^(?:0|[1-9][0-9]*)$/;
219
+ const tableCellParagraphSourcePayloadMessages = {
220
+ complexity_limit: "The hidden table-cell payload exceeds its complexity limit.",
221
+ cyclic_or_aliased_graph: "The hidden table-cell payload contains a cyclic or aliased graph.",
222
+ depth_limit: "The hidden table-cell payload exceeds its nesting limit.",
223
+ expected_array: "The hidden table-cell payload field must be an array.",
224
+ expected_block: "The hidden table-cell payload contains a malformed block.",
225
+ expected_cell: "The hidden table-cell payload contains a malformed cell.",
226
+ expected_inline_content: "The hidden table-cell payload contains malformed inline content.",
227
+ expected_paragraph: "The hidden table-cell payload contains a malformed paragraph.",
228
+ expected_record: "The hidden table-cell payload contains a malformed object.",
229
+ expected_row: "The hidden table-cell payload contains a malformed row.",
230
+ invalid_binding: "A hidden table-cell paragraph has an invalid source binding.",
231
+ invalid_block_type: "The hidden table-cell payload contains an unsupported block type.",
232
+ invalid_cell_type: "The hidden table-cell payload contains an invalid cell type.",
233
+ invalid_graph_value: "The hidden table-cell payload contains an invalid graph value.",
234
+ invalid_inline_content_type: "The hidden table-cell payload contains an unsupported inline content type.",
235
+ invalid_paragraph_type: "The hidden table-cell payload contains an invalid paragraph type.",
236
+ invalid_row_type: "The hidden table-cell payload contains an invalid row type.",
237
+ malformed_source_token: "A hidden table-cell paragraph has a malformed source token."
238
+ };
239
+ const tableCellParagraphSourceValidationErrors = /* @__PURE__ */ new WeakSet();
240
+ const invalidTableCellParagraphSourcePayload = (classification, path) => {
241
+ const error = new ParagraphPropertySourceValidationError({
242
+ code: "invalid_token",
243
+ classification,
244
+ message: tableCellParagraphSourcePayloadMessages[classification],
245
+ path
246
+ });
247
+ tableCellParagraphSourceValidationErrors.add(error);
248
+ throw error;
249
+ };
250
+ const tableCellBlockTraversalByType = {
251
+ blockSdt: "blockSdt",
252
+ paragraph: "paragraph",
253
+ table: "table"
254
+ };
255
+ const tableCellParagraphContentTraversalByType = {
256
+ bookmarkEnd: "leaf",
257
+ bookmarkStart: "leaf",
258
+ commentRangeEnd: "leaf",
259
+ commentRangeStart: "leaf",
260
+ commentReference: "leaf",
261
+ complexField: "complexField",
262
+ deletion: "content",
263
+ hyperlink: "children",
264
+ inlineSdt: "content",
265
+ insertion: "content",
266
+ mathEquation: "leaf",
267
+ moveFrom: "content",
268
+ moveFromRangeEnd: "leaf",
269
+ moveFromRangeStart: "leaf",
270
+ moveTo: "content",
271
+ moveToRangeEnd: "leaf",
272
+ moveToRangeStart: "leaf",
273
+ run: "run",
274
+ simpleField: "content"
275
+ };
276
+ const tableCellRunContentTraversalByType = {
277
+ break: "leaf",
278
+ drawing: "leaf",
279
+ endnoteRef: "leaf",
280
+ fieldChar: "leaf",
281
+ footnoteRef: "leaf",
282
+ instrText: "leaf",
283
+ noBreakHyphen: "leaf",
284
+ renderedPageBreak: "leaf",
285
+ shape: "shape",
286
+ softHyphen: "leaf",
287
+ symbol: "leaf",
288
+ tab: "leaf",
289
+ text: "leaf"
290
+ };
291
+ const isTableCellBlockContentType = (value) => typeof value === "string" && Object.hasOwn(tableCellBlockTraversalByType, value);
292
+ const isTableCellParagraphContentType = (value) => typeof value === "string" && Object.hasOwn(tableCellParagraphContentTraversalByType, value);
293
+ const isTableCellRunContentType = (value) => typeof value === "string" && Object.hasOwn(tableCellRunContentTraversalByType, value);
294
+ const inspectTableCellParagraphSourceGraph = (value, path, depth, context) => {
295
+ if (depth > TABLE_CELL_PARAGRAPH_SOURCE_MAX_DEPTH) invalidTableCellParagraphSourcePayload("depth_limit", path);
296
+ context.values += 1;
297
+ if (context.values > TABLE_CELL_PARAGRAPH_SOURCE_MAX_VALUES) invalidTableCellParagraphSourcePayload("complexity_limit", path);
298
+ if (value === null || value === void 0 || typeof value === "boolean" || typeof value === "number" || typeof value === "string") return;
299
+ if (typeof value !== "object") return invalidTableCellParagraphSourcePayload("invalid_graph_value", path);
300
+ if (context.seen.has(value)) return invalidTableCellParagraphSourcePayload("cyclic_or_aliased_graph", path);
301
+ context.seen.add(value);
302
+ context.objects.push(value);
303
+ const prototype = Object.getPrototypeOf(value);
304
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return invalidTableCellParagraphSourcePayload("invalid_graph_value", path);
305
+ for (const key of Reflect.ownKeys(value)) {
306
+ if (Array.isArray(value) && key === "length") {
307
+ if (value.length > TABLE_CELL_PARAGRAPH_SOURCE_MAX_VALUES) invalidTableCellParagraphSourcePayload("complexity_limit", path);
308
+ continue;
309
+ }
310
+ let childPath = `${path}.*`;
311
+ if (Array.isArray(value)) {
312
+ if (typeof key !== "string" || !ARRAY_INDEX.test(key)) return invalidTableCellParagraphSourcePayload("invalid_graph_value", `${path}[*]`);
313
+ childPath = `${path}[${key}]`;
314
+ }
315
+ if (typeof key !== "string") return invalidTableCellParagraphSourcePayload("invalid_graph_value", childPath);
316
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
317
+ if (descriptor === void 0 || !descriptor.enumerable || !("value" in descriptor)) return invalidTableCellParagraphSourcePayload("invalid_graph_value", childPath);
318
+ inspectTableCellParagraphSourceGraph(descriptor.value, childPath, depth + 1, context);
319
+ }
320
+ };
321
+ const tableCellParagraphSourceArray = (value, path) => {
322
+ if (!Array.isArray(value)) return invalidTableCellParagraphSourcePayload("expected_array", path);
323
+ return value;
324
+ };
325
+ const tableCellParagraphSourceRecord = (value, path, classification) => {
326
+ if (!isPropertySourceBindingRecord(value)) return invalidTableCellParagraphSourcePayload(classification, path);
327
+ return value;
328
+ };
329
+ const isDecodedTableCellParagraph = (value) => isPropertySourceBindingRecord(value) && value["type"] === "paragraph" && Array.isArray(value["content"]);
330
+ const isDecodedTableCell = (value) => isPropertySourceBindingRecord(value) && value["type"] === "tableCell" && Array.isArray(value["content"]);
331
+ const visitDecodedTableCellRunContent = (value, path, context) => {
332
+ const content = tableCellParagraphSourceRecord(value, path, "expected_inline_content");
333
+ const contentType = content["type"];
334
+ if (!isTableCellRunContentType(contentType)) return invalidTableCellParagraphSourcePayload("invalid_inline_content_type", path);
335
+ const traversal = tableCellRunContentTraversalByType[contentType];
336
+ switch (traversal) {
337
+ case "shape": {
338
+ const shapePath = `${path}.shape`;
339
+ const rawTextBody = tableCellParagraphSourceRecord(content["shape"], shapePath, "expected_record")["textBody"];
340
+ if (rawTextBody === void 0 || rawTextBody === null) return;
341
+ const textBodyPath = `${shapePath}.textBody`;
342
+ const textBody = tableCellParagraphSourceRecord(rawTextBody, textBodyPath, "expected_record");
343
+ const blocks = tableCellParagraphSourceArray(textBody["content"], `${textBodyPath}.content`);
344
+ for (const [index, block] of blocks.entries()) visitDecodedTableCellBlock(block, `${textBodyPath}.content[${index}]`, context);
345
+ return;
346
+ }
347
+ case "leaf": return;
348
+ default: return traversal;
349
+ }
350
+ };
351
+ const visitDecodedTableCellRun = (run, path, context) => {
352
+ const content = tableCellParagraphSourceArray(run["content"], `${path}.content`);
353
+ for (const [index, item] of content.entries()) visitDecodedTableCellRunContent(item, `${path}.content[${index}]`, context);
354
+ };
355
+ const visitDecodedTableCellInlineContent = (value, path, context) => {
356
+ const content = tableCellParagraphSourceRecord(value, path, "expected_inline_content");
357
+ const contentType = content["type"];
358
+ if (!isTableCellParagraphContentType(contentType)) return invalidTableCellParagraphSourcePayload("invalid_inline_content_type", path);
359
+ const traversal = tableCellParagraphContentTraversalByType[contentType];
360
+ switch (traversal) {
361
+ case "run":
362
+ visitDecodedTableCellRun(content, path, context);
363
+ return;
364
+ case "children": {
365
+ const children = tableCellParagraphSourceArray(content["children"], `${path}.children`);
366
+ for (const [index, child] of children.entries()) visitDecodedTableCellInlineContent(child, `${path}.children[${index}]`, context);
367
+ return;
368
+ }
369
+ case "content": {
370
+ const children = tableCellParagraphSourceArray(content["content"], `${path}.content`);
371
+ for (const [index, child] of children.entries()) visitDecodedTableCellInlineContent(child, `${path}.content[${index}]`, context);
372
+ return;
373
+ }
374
+ case "complexField":
375
+ for (const field of ["fieldCode", "fieldResult"]) {
376
+ const runs = tableCellParagraphSourceArray(content[field], `${path}.${field}`);
377
+ for (const [index, runValue] of runs.entries()) {
378
+ const runPath = `${path}.${field}[${index}]`;
379
+ const run = tableCellParagraphSourceRecord(runValue, runPath, "expected_inline_content");
380
+ if (run["type"] !== "run") return invalidTableCellParagraphSourcePayload("invalid_inline_content_type", runPath);
381
+ visitDecodedTableCellRun(run, runPath, context);
382
+ }
383
+ }
384
+ return;
385
+ case "leaf": return;
386
+ default: return traversal;
387
+ }
388
+ };
389
+ const visitDecodedTableCellParagraph = (paragraph, path, context) => {
390
+ if (paragraph["type"] !== "paragraph") return invalidTableCellParagraphSourcePayload("invalid_paragraph_type", path);
391
+ if (!isDecodedTableCellParagraph(paragraph)) return invalidTableCellParagraphSourcePayload("expected_paragraph", path);
392
+ const inspection = inspectTableCellParagraphPropertySourceBinding(paragraph);
393
+ switch (inspection.status) {
394
+ case "authored":
395
+ case "source":
396
+ if (inspection.status === "source" && !isParagraphPropertySourceToken(inspection.binding.token)) invalidTableCellParagraphSourcePayload("malformed_source_token", `${path}.${TABLE_CELL_PARAGRAPH_SOURCE_BINDING_ATTR}.token`);
397
+ context.paragraphs.push({
398
+ binding: inspection.binding,
399
+ paragraph,
400
+ path
401
+ });
402
+ break;
403
+ case "absent":
404
+ case "invalid": return invalidTableCellParagraphSourcePayload("invalid_binding", `${path}.${TABLE_CELL_PARAGRAPH_SOURCE_BINDING_ATTR}`);
405
+ default: return inspection;
406
+ }
407
+ const content = tableCellParagraphSourceArray(paragraph.content, `${path}.content`);
408
+ for (const [index, item] of content.entries()) visitDecodedTableCellInlineContent(item, `${path}.content[${index}]`, context);
409
+ };
410
+ const visitDecodedTableCell = (value, path, context) => {
411
+ const cell = tableCellParagraphSourceRecord(value, path, "expected_cell");
412
+ if (cell["type"] !== "tableCell") return invalidTableCellParagraphSourcePayload("invalid_cell_type", path);
413
+ const content = tableCellParagraphSourceArray(cell["content"], `${path}.content`);
414
+ for (const [index, block] of content.entries()) visitDecodedTableCellBlock(block, `${path}.content[${index}]`, context);
415
+ if (!isDecodedTableCell(cell)) return invalidTableCellParagraphSourcePayload("expected_cell", path);
416
+ return cell;
417
+ };
418
+ const visitDecodedTableCellRow = (value, path, context) => {
419
+ const row = tableCellParagraphSourceRecord(value, path, "expected_row");
420
+ if (row["type"] !== "tableRow") return invalidTableCellParagraphSourcePayload("invalid_row_type", path);
421
+ const cells = tableCellParagraphSourceArray(row["cells"], `${path}.cells`);
422
+ for (const [index, cell] of cells.entries()) visitDecodedTableCell(cell, `${path}.cells[${index}]`, context);
423
+ };
424
+ function visitDecodedTableCellBlock(value, path, context) {
425
+ const block = tableCellParagraphSourceRecord(value, path, "expected_block");
426
+ const blockType = block["type"];
427
+ if (!isTableCellBlockContentType(blockType)) return invalidTableCellParagraphSourcePayload("invalid_block_type", path);
428
+ const traversal = tableCellBlockTraversalByType[blockType];
429
+ switch (traversal) {
430
+ case "paragraph":
431
+ visitDecodedTableCellParagraph(block, path, context);
432
+ return;
433
+ case "table": {
434
+ const rows = tableCellParagraphSourceArray(block["rows"], `${path}.rows`);
435
+ for (const [index, row] of rows.entries()) visitDecodedTableCellRow(row, `${path}.rows[${index}]`, context);
436
+ return;
437
+ }
438
+ case "blockSdt": {
439
+ const content = tableCellParagraphSourceArray(block["content"], `${path}.content`);
440
+ for (const [index, child] of content.entries()) visitDecodedTableCellBlock(child, `${path}.content[${index}]`, context);
441
+ return;
442
+ }
443
+ default: return traversal;
444
+ }
445
+ }
446
+ const decodedTableCellParagraphSourcePayloadBrand = Symbol("decodedTableCellParagraphSourcePayload");
447
+ const decodedTableCellParagraphSourcePayloadStates = /* @__PURE__ */ new WeakMap();
448
+ const decodedTableCellParagraphSourcePayloads = /* @__PURE__ */ new WeakMap();
449
+ const decodedTableCellParagraphSourcePayloadState = (payload) => {
450
+ const state = decodedTableCellParagraphSourcePayloadStates.get(payload);
451
+ if (!state) panic("A collapsed-cell paragraph source payload bypassed its decoder.");
452
+ return state;
453
+ };
454
+ const decodeTableCellParagraphSourcePayloadUnchecked = (value) => {
455
+ const graphContext = {
456
+ objects: [],
457
+ seen: /* @__PURE__ */ new WeakSet(),
458
+ values: 0
459
+ };
460
+ inspectTableCellParagraphSourceGraph(value, "continuationCells", 0, graphContext);
461
+ const context = { paragraphs: [] };
462
+ const rawCells = tableCellParagraphSourceArray(value, "continuationCells");
463
+ const cells = [];
464
+ for (const [index, rawCell] of rawCells.entries()) cells.push(visitDecodedTableCell(rawCell, `continuationCells[${index}]`, context));
465
+ let object = graphContext.objects.pop();
466
+ while (object) {
467
+ Object.freeze(object);
468
+ object = graphContext.objects.pop();
469
+ }
470
+ const payload = Object.freeze({
471
+ [decodedTableCellParagraphSourcePayloadBrand]: true,
472
+ cells: Object.freeze(cells)
473
+ });
474
+ decodedTableCellParagraphSourcePayloadStates.set(payload, Object.freeze({ paragraphs: Object.freeze(context.paragraphs.map((entry) => Object.freeze(entry))) }));
475
+ decodedTableCellParagraphSourcePayloads.set(payload.cells, payload);
476
+ return payload;
477
+ };
478
+ /** Decode and freeze the opaque collapsed-cell wire payload before typed use. */
479
+ const decodeTableCellParagraphSourcePayload = (value) => {
480
+ if (typeof value === "object" && value !== null) {
481
+ const decoded = decodedTableCellParagraphSourcePayloads.get(value);
482
+ if (decoded) return decoded;
483
+ }
484
+ try {
485
+ const decoded = decodeTableCellParagraphSourcePayloadUnchecked(value);
486
+ if (typeof value === "object" && value !== null) decodedTableCellParagraphSourcePayloads.set(value, decoded);
487
+ return decoded;
488
+ } catch (error) {
489
+ if (error instanceof ParagraphPropertySourceValidationError && tableCellParagraphSourceValidationErrors.has(error)) throw error;
490
+ return invalidTableCellParagraphSourcePayload("invalid_graph_value", "continuationCells");
491
+ }
492
+ };
493
+ const setTableCellParagraphPropertySourceBinding = (paragraph, binding) => {
494
+ if (!Reflect.set(paragraph, "_docxParagraphSourceBinding", Object.freeze(binding))) panic("Cannot attach paragraph-property transport identity to a table cell.");
495
+ };
496
+ const tableCellParagraphPropertySourceBindingForTransport = (paragraph, inspection) => {
497
+ const directToken = paragraphPropertySourceTokens.get(paragraph);
498
+ if (directToken !== void 0) {
499
+ if (!isParagraphPropertySourceToken(directToken)) panic("Cannot transport a malformed private paragraph-property source token.");
500
+ switch (inspection.status) {
501
+ case "absent": return {
502
+ token: directToken,
503
+ type: "source"
504
+ };
505
+ case "authored": panic("A hidden paragraph-property source binding conflicts with its private owner.");
506
+ case "source":
507
+ if (inspection.binding.token !== directToken) panic("A hidden paragraph-property source binding conflicts with its private owner.");
508
+ return inspection.binding;
509
+ case "invalid": panic("Cannot transport a malformed hidden paragraph-property source binding.");
510
+ default: return inspection;
511
+ }
512
+ }
513
+ switch (inspection.status) {
514
+ case "absent": return { type: "authored" };
515
+ case "authored": return inspection.binding;
516
+ case "source":
517
+ if (!isParagraphPropertySourceToken(inspection.binding.token)) panic("Cannot transport a malformed hidden paragraph-property source token.");
518
+ return inspection.binding;
519
+ case "invalid": panic("Cannot transport a malformed hidden paragraph-property source binding.");
520
+ default: return inspection;
521
+ }
522
+ };
523
+ /** Prepare opaque continuation cells for ProseMirror and Yjs transport. */
524
+ const transportTableCellsWithParagraphPropertySources = (cells) => {
525
+ const cloned = structuredClone(cells);
526
+ const sources = paragraphsInTableCells(cells);
527
+ const targets = paragraphsInTableCells(cloned);
528
+ if (sources.length !== targets.length) panic("The cloned table cells changed paragraph graph ownership.");
529
+ for (const [index, source] of sources.entries()) {
530
+ const target = targets.at(index);
531
+ if (!target) panic("The cloned table cells lost a paragraph owner.");
532
+ copyParagraphPropertyCapture(target, source);
533
+ const inspection = inspectTableCellParagraphPropertySourceBinding(source);
534
+ setTableCellParagraphPropertySourceBinding(target, tableCellParagraphPropertySourceBindingForTransport(source, inspection));
535
+ }
536
+ return cloned;
537
+ };
538
+ /** Restore decoded identities privately and remove them from the document model. */
539
+ const restoreTableCellsWithParagraphPropertySources = (payload) => {
540
+ const sourceEntries = decodedTableCellParagraphSourcePayloadState(payload).paragraphs;
541
+ const cloned = structuredClone([...payload.cells]);
542
+ const targets = paragraphsInTableCells(cloned);
543
+ if (sourceEntries.length !== targets.length) panic("The cloned table cells changed paragraph graph ownership.");
544
+ for (const [index, sourceEntry] of sourceEntries.entries()) {
545
+ const target = targets.at(index);
546
+ if (!target) panic("The cloned table cells lost a paragraph owner.");
547
+ const { paragraph: source } = sourceEntry;
548
+ copyParagraphPropertyCapture(target, source);
549
+ if (!Reflect.deleteProperty(target, "_docxParagraphSourceBinding")) panic("Cannot detach paragraph-property transport identity from a table cell.");
550
+ switch (sourceEntry.binding.type) {
551
+ case "authored": break;
552
+ case "source":
553
+ paragraphPropertySourceTokens.set(target, sourceEntry.binding.token);
554
+ break;
555
+ default: return sourceEntry.binding;
556
+ }
557
+ }
558
+ return cloned;
559
+ };
560
+ /** Visit every transported hidden paragraph in canonical cell-story order. */
561
+ const visitTableCellParagraphPropertySourceBindings = (payload, visit) => {
562
+ for (const { binding, path } of decodedTableCellParagraphSourcePayloadState(payload).paragraphs) visit(binding, path);
563
+ };
564
+ /**
565
+ * Clone package-crossing vertical-merge payloads with their captured `w:pPr`,
566
+ * but without the durable paragraph tokens owned by the source package.
567
+ */
568
+ const cloneTableCellsWithParagraphPropertyCaptures = (payload) => {
569
+ const sourceEntries = decodedTableCellParagraphSourcePayloadState(payload).paragraphs;
570
+ const cloned = structuredClone([...payload.cells]);
571
+ const targets = paragraphsInTableCells(cloned);
572
+ if (sourceEntries.length !== targets.length) panic("The cloned table cells changed paragraph graph ownership.");
573
+ for (const [index, sourceEntry] of sourceEntries.entries()) {
574
+ const target = targets.at(index);
575
+ if (!target) panic("The cloned table cells lost a paragraph owner.");
576
+ copyParagraphPropertyCapture(target, sourceEntry.paragraph);
577
+ setTableCellParagraphPropertySourceBinding(target, { type: "authored" });
578
+ }
65
579
  return cloned;
66
580
  };
67
581
  const linkProseParagraphPropertySource = (proseParagraph, sourceParagraph) => {
68
582
  if (paragraphPropertySources.has(sourceParagraph)) proseParagraphSourceOwners.set(proseParagraph, paragraphPropertySourceOwners.get(sourceParagraph) ?? sourceParagraph);
69
583
  };
584
+ /** Create a parsed paragraph with both its same-process owner and durable body token. */
585
+ const createProseParagraphWithPropertySource = (nodeType, sourceParagraph, options = {}) => {
586
+ if (!nodeType || nodeType.name !== "paragraph") panic("Paragraph-property provenance can only seed a paragraph node");
587
+ const paragraph = nodeType.create({
588
+ ...options.attrs,
589
+ [PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR]: paragraphPropertySourceTokens.get(sourceParagraph) ?? null
590
+ }, options.content, options.marks);
591
+ linkProseParagraphPropertySource(paragraph, sourceParagraph);
592
+ return paragraph;
593
+ };
594
+ const paragraphPropertySourceTransfersKey = new PluginKey("paragraphPropertySourceTransfers");
595
+ /** Join adjacent paragraphs when revision semantics deliberately select the right owner. */
596
+ const joinProseParagraphsWithRightPropertySource = ({ attrs, pos, transaction }) => {
597
+ const $pos = transaction.doc.resolve(pos);
598
+ const left = $pos.nodeBefore;
599
+ const right = $pos.nodeAfter;
600
+ if (!left || !right || left.type.name !== "paragraph" || right.type.name !== "paragraph") panic("Paragraph-property ownership can only join adjacent paragraphs");
601
+ const leftToken = getProseParagraphPropertySourceToken(left);
602
+ const rightToken = getProseParagraphPropertySourceToken(right);
603
+ const leftPos = pos - left.nodeSize;
604
+ transaction.join(pos);
605
+ const joined = transaction.doc.nodeAt(leftPos);
606
+ if (!joined || joined.type.name !== "paragraph") panic("Joining paragraphs did not produce a paragraph");
607
+ transaction.setNodeMarkup(leftPos, void 0, {
608
+ ...attrs,
609
+ [PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR]: rightToken ?? null
610
+ }, joined.marks);
611
+ const transfers = transaction.getMeta(paragraphPropertySourceTransfersKey) ?? [];
612
+ transaction.setMeta(paragraphPropertySourceTransfersKey, [...transfers, {
613
+ displacedToken: typeof leftToken === "string" ? leftToken : null,
614
+ selectedToken: typeof rightToken === "string" ? rightToken : null
615
+ }]);
616
+ return transaction;
617
+ };
618
+ const getExplicitParagraphPropertySourceTransfers = (transaction) => transaction.getMeta(paragraphPropertySourceTransfersKey) ?? [];
70
619
  /** Carry a parser-linked paragraph owner across an immutable PM node rebuild. */
71
620
  const copyProseParagraphPropertySource = (target, source) => {
72
621
  if (target.type.name !== "paragraph" || source.type.name !== "paragraph") return;
@@ -79,6 +628,21 @@ const recreateProseNodeWithParagraphPropertySource = (source, options = {}) => {
79
628
  copyProseParagraphPropertySource(target, source);
80
629
  return target;
81
630
  };
631
+ /**
632
+ * Rebuild a PM node that is crossing from another package: retain its
633
+ * same-process paragraph-property capture, but detach the durable token that
634
+ * can only name a paragraph in the package it came from.
635
+ */
636
+ const recreateProseNodeWithDetachedParagraphPropertySource = (source, options = {}) => {
637
+ const attrs = options.attrs ?? source.attrs;
638
+ return recreateProseNodeWithParagraphPropertySource(source, {
639
+ ...options,
640
+ attrs: source.type.name === "paragraph" ? {
641
+ ...attrs,
642
+ [PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR]: null
643
+ } : attrs
644
+ });
645
+ };
82
646
  /** Replace paragraph markup without losing its private parser-owner link. */
83
647
  const setProseParagraphMarkupWithPropertySource = ({ attrs, ownership, pos, transaction }) => {
84
648
  const source = transaction.doc.nodeAt(pos);
@@ -104,10 +668,12 @@ const transferProseParagraphPropertySource = (target, source, paraId) => {
104
668
  paragraphPropertySourceTransferIds.set(sourceOwner, paraId);
105
669
  };
106
670
  const linkParagraphPropertySourceCandidate = (target, source) => {
671
+ const token = source.attrs[PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR];
672
+ if (typeof token === "string") paragraphPropertySourceTokens.set(target, token);
107
673
  const sourceOwner = proseParagraphSourceOwners.get(source);
108
674
  if (sourceOwner) paragraphPropertySourceCandidates.set(target, sourceOwner);
109
675
  };
110
676
  const getParagraphPropertySourceCandidate = (paragraph) => paragraphPropertySourceCandidates.get(paragraph);
111
677
  const getParagraphPropertySourceTransferId = (paragraph) => paragraphPropertySourceTransferIds.get(paragraph);
112
678
  //#endregion
113
- export { assignParagraphPropertySource, cloneDocumentWithParagraphPropertySources, cloneParagraphWithPropertySource, cloneParagraphWithoutPropertySource, copyParagraphPropertySource, getParagraphPropertySource, getParagraphPropertySourceCandidate, getParagraphPropertySourceTransferId, linkParagraphPropertySourceCandidate, linkProseParagraphPropertySource, recreateProseNodeWithParagraphPropertySource, setProseParagraphMarkupWithPropertySource, transferProseParagraphPropertySource };
679
+ export { PARAGRAPH_PROPERTY_SOURCE_VALIDATION_CODES, PROSE_PARAGRAPH_SOURCE_CONTRACT_ATTR, PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR, ParagraphPropertySourceValidationError, TABLE_CELL_PARAGRAPH_SOURCE_BINDING_ATTR, TABLE_CELL_PARAGRAPH_SOURCE_PAYLOAD_ERROR_CLASSIFICATIONS, assignDocumentParagraphPropertySourceContract, assignParagraphPropertySource, cloneDocumentWithParagraphPropertySources, cloneParagraphWithPropertySource, cloneParagraphWithoutPropertySource, cloneTableCellsWithParagraphPropertyCaptures, copyDocumentParagraphPropertySourceContract, copyDocumentParagraphPropertySources, copyParagraphPropertyCapture, copyParagraphPropertySource, createProseParagraphWithPropertySource, decodeTableCellParagraphSourcePayload, getDocumentParagraphPropertySourceContract, getExplicitParagraphPropertySourceTransfers, getParagraphPropertySource, getParagraphPropertySourceCandidate, getParagraphPropertySourceToken, getParagraphPropertySourceTransferId, getProseDocumentParagraphPropertySourceContract, getProseParagraphPropertySourceToken, isParagraphPropertySourceToken, joinProseParagraphsWithRightPropertySource, linkParagraphPropertySourceCandidate, linkProseParagraphPropertySource, paragraphPropertySourceBelongsToDocument, paragraphPropertySourceTokenMatchesContract, recreateProseNodeWithDetachedParagraphPropertySource, recreateProseNodeWithParagraphPropertySource, restoreTableCellsWithParagraphPropertySources, setProseParagraphMarkupWithPropertySource, transferProseParagraphPropertySource, transportTableCellsWithParagraphPropertySources, visitDocumentStoryParagraphs, visitTableCellParagraphPropertySourceBindings };
@@ -18,6 +18,7 @@ import { renderEmfSvg } from "./metafileSvg.js";
18
18
  import { DocxModelValidationError, formatDocumentModelIssues, validateFolioDocumentModel } from "./modelValidation.js";
19
19
  import { parseNumbering } from "./numberingParser.js";
20
20
  import { normalizeNumberingReferences } from "./numberingReferenceNormalization.js";
21
+ import { assignDocumentParagraphPropertySourceContract } from "./paragraphPropertySource.js";
21
22
  import { RELATIONSHIP_TYPES, parseRelationships, resolveRelativePath } from "./relsParser.js";
22
23
  import { normalizeRenderedPageBreakHints } from "./renderedPageBreakNormalization.js";
23
24
  import { parseSettings } from "./settingsParser.js";
@@ -50,6 +51,12 @@ import { TaggedError } from "better-result";
50
51
  const EMF_MIME_TYPE = "image/x-emf";
51
52
  const SVG_MIME_TYPE = "image/svg+xml";
52
53
  const MAX_PACKAGE_EMF_PREVIEW_BYTES = 8 * 1024 * 1024;
54
+ const sha256Hex = async (buffer) => {
55
+ const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", buffer));
56
+ let digest = "";
57
+ for (const byte of bytes) digest += byte.toString(16).padStart(2, "0");
58
+ return digest;
59
+ };
53
60
  /**
54
61
  * Parse a DOCX file into a complete Document model
55
62
  *
@@ -65,6 +72,7 @@ async function parseDocx(input, options = {}) {
65
72
  try {
66
73
  const timeStage = (_name, fn) => fn();
67
74
  const timeStageAsync = async (_name, fn) => await fn();
75
+ const paragraphPropertySourceDigest = sha256Hex(buffer);
68
76
  onProgress("Extracting DOCX...", 0);
69
77
  const raw = await timeStageAsync("unzip", () => unzipDocx(buffer, {
70
78
  ...unzipLimits,
@@ -216,6 +224,7 @@ async function parseDocx(input, options = {}) {
216
224
  ...templateVariables !== void 0 ? { templateVariables } : {},
217
225
  ...requiredFonts.length > 0 ? { requiredFonts } : {}
218
226
  };
227
+ assignDocumentParagraphPropertySourceContract(document, await paragraphPropertySourceDigest);
219
228
  enforcePackageVmlPreviewBudget(document.package);
220
229
  const validation = validateFolioDocumentModel(document);
221
230
  const parsedCompleteModel = parseHeadersFooters && parseNotes;