@stll/folio-core 0.58.0 → 0.58.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.
@@ -22,7 +22,7 @@ import { expectCharacterStyleMarkAttrs, expectFootnoteRefMarkAttrs } from "../pr
22
22
  import { acceptAIEditRevision, acceptSuggestion, getSuggestions, rejectAIEditRevision, rejectAllSuggestions, rejectSuggestion, resolveAllChangesInHeadlessState, suggestionIdOfRevision } from "../prosemirror/commands/comments.js";
23
23
  import { sectionRejectProperties } from "../prosemirror/commands/propertyChangeScope.js";
24
24
  import { allocateCommentId, createCommentIdAllocator, seedCommentAllocator } from "../prosemirror/commentIdAllocator.js";
25
- import { proseDocToBlocks, updateDocumentContent } from "../prosemirror/conversion/fromProseDoc.js";
25
+ import { fromProseDoc, proseDocToBlocks } from "../prosemirror/conversion/fromProseDoc.js";
26
26
  import { footnoteToProseDoc, headerFooterToProseDoc, toProseDoc } from "../prosemirror/conversion/toProseDoc.js";
27
27
  import { ensureBaseDirectionInState } from "../prosemirror/extensions/features/AutoBidiDetectionExtension.js";
28
28
  import { getChangedParagraphIds, getTrackedSectionEndpointRemoval, hasStructuralChanges, hasUntrackedChanges } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
@@ -1680,13 +1680,17 @@ var FolioDocxReviewer = class FolioDocxReviewer {
1680
1680
  };
1681
1681
  }
1682
1682
  documentFromStateSnapshot(snapshot) {
1683
- const document = updateDocumentContent(snapshot.importedStyles === void 0 ? this.baseDocument : {
1683
+ const sourceDocument = snapshot.importedStyles === void 0 ? this.baseDocument : {
1684
1684
  ...this.baseDocument,
1685
1685
  package: {
1686
1686
  ...this.baseDocument.package,
1687
1687
  styles: snapshot.importedStyles
1688
1688
  }
1689
- }, snapshot.mainState.doc);
1689
+ };
1690
+ const document = fromProseDoc(snapshot.mainState.doc, sourceDocument, { stylesheetSource: snapshot.importedStyles === void 0 ? { type: "package" } : {
1691
+ type: "supplied",
1692
+ styles: snapshot.importedStyles
1693
+ } });
1690
1694
  if (snapshot.finalSectionPropertiesOverride !== void 0) document.package.document.finalSectionProperties = snapshot.finalSectionPropertiesOverride;
1691
1695
  if (snapshot.importedStyles !== void 0) document.package.styles = snapshot.importedStyles;
1692
1696
  if (snapshot.importedMedia.size > 0) document.package.media = new Map([...document.package.media ?? [], ...snapshot.importedMedia]);
@@ -25,8 +25,20 @@ declare const canReplayHeaderFooterVerbatim: (hf: document_d_exports.HeaderFoote
25
25
  declare const assignHeaderFooterVerbatimXml: (hf: document_d_exports.HeaderFooter, xml: string) => void;
26
26
  declare const refreshHeaderFooterVerbatimFingerprint: (hf: document_d_exports.HeaderFooter) => void;
27
27
  declare const clearHeaderFooterVerbatimXml: (hf: document_d_exports.HeaderFooter) => void;
28
- /** Capture the body in the same trusted package registry as secondary stories. */
29
- declare const captureDocumentSourceBaseline: (document: document_d_exports.Document, xml: string) => void;
28
+ /** Capture parsed resources on the same trusted handle, including an absent main part. */
29
+ declare const captureDocumentSourceBaseline: (document: document_d_exports.Document, xml: string | undefined) => void;
30
+ /** Read the identity-stable, deeply frozen stylesheet without cloning or body comparison. */
31
+ declare const getDocumentSourceStyles: (document: document_d_exports.Document, liveStyles?: document_d_exports.Document["package"]["styles"]) => {
32
+ readonly type: "missing";
33
+ readonly baseline?: never;
34
+ } | {
35
+ readonly type: "mismatch";
36
+ readonly baseline?: never;
37
+ } | {
38
+ readonly type: "captured";
39
+ readonly styles: Readonly<import("@stll/docx-core/model").StyleDefinitions> | undefined;
40
+ readonly additions: import("@stll/docx-core").Style[];
41
+ };
30
42
  declare const getDocumentSourceBaseline: (document: document_d_exports.Document) => {
31
43
  type: "missing";
32
44
  } | {
@@ -44,4 +56,4 @@ declare const getDocumentSourceBaseline: (document: document_d_exports.Document)
44
56
  }>;
45
57
  };
46
58
  //#endregion
47
- export { assignHeaderFooterVerbatimXml, canReplayHeaderFooterBlocks, canReplayHeaderFooterVerbatim, captureDocumentSourceBaseline, captureHeaderFooterPackageBaselines, clearHeaderFooterVerbatimXml, copyHeaderFooterBaselineHandles, copyHeaderFooterPackageBaselines, getDocumentSourceBaseline, getHeaderFooterSourceBaseline, getHeaderFooterVerbatimXml, refreshHeaderFooterVerbatimFingerprint };
59
+ export { assignHeaderFooterVerbatimXml, canReplayHeaderFooterBlocks, canReplayHeaderFooterVerbatim, captureDocumentSourceBaseline, captureHeaderFooterPackageBaselines, clearHeaderFooterVerbatimXml, copyHeaderFooterBaselineHandles, copyHeaderFooterPackageBaselines, getDocumentSourceBaseline, getDocumentSourceStyles, getHeaderFooterSourceBaseline, getHeaderFooterVerbatimXml, refreshHeaderFooterVerbatimFingerprint };
@@ -1,5 +1,5 @@
1
1
  import { canonicalJson } from "../utils/canonicalJson.js";
2
- import { Result } from "better-result";
2
+ import { Result, panic } from "better-result";
3
3
  //#region src/docx/headerFooterVerbatim.ts
4
4
  const fingerprintBaselines = /* @__PURE__ */ new WeakMap();
5
5
  const readFingerprint = (hf) => {
@@ -147,16 +147,33 @@ const clearHeaderFooterVerbatimXml = (hf) => {
147
147
  delete ext.verbatimFingerprint;
148
148
  Reflect.deleteProperty(hf, BASELINE_HANDLE);
149
149
  };
150
- /** Capture the body in the same trusted package registry as secondary stories. */
150
+ /** Freeze the parsed plain-object graph once, retaining shared references. */
151
+ const freezeSourceStyles = (value, visited = /* @__PURE__ */ new WeakSet()) => {
152
+ if (typeof value !== "object" || value === null || visited.has(value)) return;
153
+ visited.add(value);
154
+ for (const child of Object.values(value)) freezeSourceStyles(child, visited);
155
+ Object.freeze(value);
156
+ };
157
+ /** Capture parsed resources on the same trusted handle, including an absent main part. */
151
158
  const captureDocumentSourceBaseline = (document, xml) => {
152
- const body = structuredClone(document.package.document);
153
159
  const handle = {};
154
- contentBaselines.set(handle, {
160
+ const resourceStyles = structuredClone(document.package.styles);
161
+ freezeSourceStyles(resourceStyles);
162
+ const styleIds = new Set(resourceStyles?.styles.map(({ styleId }) => styleId));
163
+ if (xml === void 0) contentBaselines.set(handle, {
164
+ type: "body",
165
+ kind: "noMainPart",
166
+ resourceStyles,
167
+ styleIds
168
+ });
169
+ else contentBaselines.set(handle, {
155
170
  type: "body",
171
+ kind: "mainPart",
156
172
  fingerprint: { type: "pending" },
157
173
  xml,
158
- body,
159
- resourceStyles: structuredClone(document.package.styles),
174
+ body: structuredClone(document.package.document),
175
+ resourceStyles,
176
+ styleIds,
160
177
  resourceRelationships: structuredClone(document.package.relationships),
161
178
  resourceMedia: new Map([...document.package.media ?? []].map(([path, media]) => [path, {
162
179
  data: media.data,
@@ -174,13 +191,38 @@ const captureDocumentSourceBaseline = (document, xml) => {
174
191
  packageBaselines.set(document.originalBuffer, registry);
175
192
  }
176
193
  };
177
- const getDocumentSourceBaseline = (document) => {
194
+ const readDocumentSourceCapture = (document) => {
178
195
  const body = document.package.document;
179
196
  const handle = BASELINE_HANDLE in body ? body[BASELINE_HANDLE] : document.originalBuffer && packageBaselines.get(document.originalBuffer)?.get("word/document.xml")?.get("body");
180
197
  if (handle === void 0) return { type: "missing" };
181
198
  if (typeof handle !== "object" || handle === null) return { type: "mismatch" };
182
199
  const baseline = contentBaselines.get(handle);
183
200
  if (!baseline || baseline.type !== "body") return { type: "mismatch" };
201
+ return {
202
+ type: "captured",
203
+ baseline
204
+ };
205
+ };
206
+ /** Read the identity-stable, deeply frozen stylesheet without cloning or body comparison. */
207
+ const getDocumentSourceStyles = (document, liveStyles) => {
208
+ const source = readDocumentSourceCapture(document);
209
+ if (source.type !== "captured") return source;
210
+ const additions = liveStyles?.styles.filter(({ styleId }) => !source.baseline.styleIds.has(styleId)) ?? [];
211
+ return {
212
+ type: "captured",
213
+ styles: source.baseline.resourceStyles,
214
+ additions
215
+ };
216
+ };
217
+ const getDocumentSourceBaseline = (document) => {
218
+ const source = readDocumentSourceCapture(document);
219
+ if (source.type !== "captured") return source;
220
+ const { baseline } = source;
221
+ switch (baseline.kind) {
222
+ case "noMainPart": return { type: "missing" };
223
+ case "mainPart": break;
224
+ default: return panic(`Unexpected document source capture: ${JSON.stringify(baseline)}`);
225
+ }
184
226
  const fingerprint = JSON.stringify(baseline.body);
185
227
  if (baseline.fingerprint.type === "pending") baseline.fingerprint = {
186
228
  type: "captured",
@@ -198,4 +240,4 @@ const getDocumentSourceBaseline = (document) => {
198
240
  };
199
241
  };
200
242
  //#endregion
201
- export { assignHeaderFooterVerbatimXml, canReplayHeaderFooterBlocks, canReplayHeaderFooterVerbatim, captureDocumentSourceBaseline, captureHeaderFooterPackageBaselines, clearHeaderFooterVerbatimXml, copyHeaderFooterBaselineHandles, copyHeaderFooterPackageBaselines, getDocumentSourceBaseline, getHeaderFooterSourceBaseline, getHeaderFooterVerbatimXml, refreshHeaderFooterVerbatimFingerprint };
243
+ export { assignHeaderFooterVerbatimXml, canReplayHeaderFooterBlocks, canReplayHeaderFooterVerbatim, captureDocumentSourceBaseline, captureHeaderFooterPackageBaselines, clearHeaderFooterVerbatimXml, copyHeaderFooterBaselineHandles, copyHeaderFooterPackageBaselines, getDocumentSourceBaseline, getDocumentSourceStyles, getHeaderFooterSourceBaseline, getHeaderFooterVerbatimXml, refreshHeaderFooterVerbatimFingerprint };
@@ -474,7 +474,7 @@ async function parseDocxWithPreviewBudget(input, options, previewBudget) {
474
474
  }
475
475
  onProgress("Complete", 100);
476
476
  captureHeaderFooterPackageBaselines(document);
477
- if (raw.documentXml) captureDocumentSourceBaseline(document, raw.documentXml);
477
+ captureDocumentSourceBaseline(document, raw.documentXml ?? void 0);
478
478
  return document;
479
479
  } catch (error) {
480
480
  if (error instanceof DocxEncryptionError) throw error;
@@ -16,7 +16,15 @@ import { Node } from "prosemirror-model";
16
16
  type ProjectionReuse = "none" | "matched";
17
17
  /** How the conversion treats records the editor did not change. */
18
18
  type FromProseDocOptions = {
19
- /** The stylesheet authority used to construct the projection. */
19
+ /**
20
+ * The stylesheet authority used to construct the projection. With a parsed package,
21
+ * package source uses its captured definitions; edits to live package.styles do not
22
+ * alter existing definitions. Captured definitions are deeply frozen and shared;
23
+ * package-source saves append live definitions for IDs missing from the capture.
24
+ * Supplied sources follow the same addition rule and must match existing definitions
25
+ * and document defaults. Clone frozen definitions once when authoring a mutable copy.
26
+ * If the parsed package had no styles part, all live definitions are additions.
27
+ */
20
28
  stylesheetSource: {
21
29
  type: "package";
22
30
  } | {
@@ -1,7 +1,7 @@
1
1
  import { joinCommentRangesAcrossParagraphs } from "../../docx/commentRangeJoin.js";
2
2
  import { completeCommentReferences } from "../../docx/commentReferenceCompletion.js";
3
3
  import { normalizeFoldedListNumbers } from "../../docx/foldedListNumberFields.js";
4
- import { getDocumentSourceBaseline } from "../../docx/headerFooterVerbatim.js";
4
+ import { getDocumentSourceStyles } from "../../docx/headerFooterVerbatim.js";
5
5
  import { EDITED_PREVIEW_FINGERPRINT, imageRawXmlFingerprint } from "../../docx/imageRawXml.js";
6
6
  import { isInlineSdtContent, isSimpleFieldContent } from "../../docx/inlineWrapperContent.js";
7
7
  import { paragraphNumberingReferenceId as paragraphNumberingReferenceId$1, sameStatedParagraphNumbering as sameStatedParagraphNumbering$1 } from "../../docx/numberingReference.js";
@@ -272,56 +272,77 @@ const restoreLinkedParagraphPropertySources = (content) => {
272
272
  };
273
273
  };
274
274
  var SaveStylesheetSourceMismatch = class extends TaggedError("SaveStylesheetSourceMismatch") {};
275
- const assertSuppliedStylesMatchSource = (styles, baseDocument) => {
276
- if (!baseDocument?.originalBuffer) return;
277
- const source = getDocumentSourceBaseline(baseDocument);
275
+ const parsedPackageStyles = (document, liveStyles) => {
276
+ const source = getDocumentSourceStyles(document, liveStyles);
278
277
  switch (source.type) {
278
+ case "captured": {
279
+ if (source.styles !== void 0 && source.additions.length === 0) return source.styles;
280
+ const addedIds = /* @__PURE__ */ new Set();
281
+ for (const addition of source.additions) {
282
+ if (addedIds.has(addition.styleId)) throw new SaveStylesheetSourceMismatch({
283
+ message: `Duplicate added style ID: ${addition.styleId}.`,
284
+ divergences: [{
285
+ type: "style",
286
+ styleId: addition.styleId
287
+ }]
288
+ });
289
+ addedIds.add(addition.styleId);
290
+ }
291
+ if (source.styles === void 0) return liveStyles === void 0 ? void 0 : structuredClone(liveStyles);
292
+ return {
293
+ ...source.styles,
294
+ styles: [...source.styles.styles, ...structuredClone(source.additions)]
295
+ };
296
+ }
279
297
  case "missing":
280
298
  case "mismatch": throw new SaveStylesheetSourceMismatch({
281
299
  message: "The parsed package stylesheet source is unavailable.",
282
300
  divergences: [{ type: "sourceUnavailable" }]
283
301
  });
284
- case "captured": {
285
- const sourceStyles = source.resourceStyles;
286
- if (!sourceStyles) return;
287
- const suppliedById = new Map(styles.styles.map((style) => [style.styleId, style]));
288
- const sourceById = new Map(sourceStyles.styles.map((style) => [style.styleId, style]));
289
- const divergentIds = /* @__PURE__ */ new Set();
290
- for (const style of sourceStyles.styles) if (canonicalJson(style) !== canonicalJson(suppliedById.get(style.styleId))) divergentIds.add(style.styleId);
291
- for (const style of styles.styles) if (canonicalJson(style) !== canonicalJson(suppliedById.get(style.styleId))) divergentIds.add(style.styleId);
292
- const persistedStyles = {
293
- ...sourceStyles,
294
- styles: [...sourceStyles.styles, ...styles.styles.filter(({ styleId }) => !sourceById.has(styleId))]
295
- };
296
- const suppliedResolver = createStyleEngine(styles);
297
- const persistedResolver = createStyleEngine(persistedStyles);
298
- for (const [supplied, persisted] of [
299
- [suppliedResolver.getDefaultParagraphStyle(), persistedResolver.getDefaultParagraphStyle()],
300
- [suppliedResolver.getDefaultCharacterStyle(), persistedResolver.getDefaultCharacterStyle()],
301
- [suppliedResolver.getDefaultTableStyle(), persistedResolver.getDefaultTableStyle()]
302
- ]) {
303
- if (supplied?.styleId === persisted?.styleId) continue;
304
- if (supplied) divergentIds.add(supplied.styleId);
305
- if (persisted) divergentIds.add(persisted.styleId);
306
- }
307
- const divergences = [];
308
- for (const styleId of divergentIds) divergences.push({
309
- type: "style",
310
- styleId
311
- });
312
- if (canonicalJson(styles.docDefaults) !== canonicalJson(sourceStyles.docDefaults)) divergences.push({ type: "docDefaults" });
313
- if (divergences.length === 0) return;
314
- throw new SaveStylesheetSourceMismatch({
315
- message: `Supplied stylesheet differs from the parsed package: ${[...divergentIds, ...divergences.some(({ type }) => type === "docDefaults") ? ["docDefaults"] : []].join(", ")}.`,
316
- divergences
317
- });
318
- }
319
- default: return panic(`Unexpected stylesheet source baseline: ${JSON.stringify(source)}`);
302
+ default: return panic(`Unexpected stylesheet source capture: ${JSON.stringify(source)}`);
320
303
  }
321
304
  };
305
+ const assertSuppliedStylesMatchSource = (styles, baseDocument) => {
306
+ if (!baseDocument?.originalBuffer) return;
307
+ const sourceStyles = parsedPackageStyles(baseDocument);
308
+ if (!sourceStyles) return;
309
+ const suppliedById = new Map(styles.styles.map((style) => [style.styleId, style]));
310
+ const sourceById = new Map(sourceStyles.styles.map((style) => [style.styleId, style]));
311
+ const divergentIds = /* @__PURE__ */ new Set();
312
+ for (const style of sourceStyles.styles) if (canonicalJson(style) !== canonicalJson(suppliedById.get(style.styleId))) divergentIds.add(style.styleId);
313
+ for (const style of styles.styles) if (canonicalJson(style) !== canonicalJson(suppliedById.get(style.styleId))) divergentIds.add(style.styleId);
314
+ const persistedStyles = {
315
+ ...sourceStyles,
316
+ styles: [...sourceStyles.styles, ...styles.styles.filter(({ styleId }) => !sourceById.has(styleId))]
317
+ };
318
+ const suppliedResolver = createStyleEngine(styles);
319
+ const persistedResolver = createStyleEngine(persistedStyles);
320
+ for (const [supplied, persisted] of [
321
+ [suppliedResolver.getDefaultParagraphStyle(), persistedResolver.getDefaultParagraphStyle()],
322
+ [suppliedResolver.getDefaultCharacterStyle(), persistedResolver.getDefaultCharacterStyle()],
323
+ [suppliedResolver.getDefaultTableStyle(), persistedResolver.getDefaultTableStyle()]
324
+ ]) {
325
+ if (supplied?.styleId === persisted?.styleId) continue;
326
+ if (supplied) divergentIds.add(supplied.styleId);
327
+ if (persisted) divergentIds.add(persisted.styleId);
328
+ }
329
+ const divergences = [];
330
+ for (const styleId of divergentIds) divergences.push({
331
+ type: "style",
332
+ styleId
333
+ });
334
+ if (canonicalJson(styles.docDefaults) !== canonicalJson(sourceStyles.docDefaults)) divergences.push({ type: "docDefaults" });
335
+ if (divergences.length === 0) return;
336
+ throw new SaveStylesheetSourceMismatch({
337
+ message: `Supplied stylesheet differs from the parsed package: ${[...divergentIds, ...divergences.some(({ type }) => type === "docDefaults") ? ["docDefaults"] : []].join(", ")}.`,
338
+ divergences
339
+ });
340
+ };
322
341
  const saveStylesheet = (source, baseDocument) => {
323
342
  switch (source.type) {
324
- case "package": return baseDocument?.package.styles;
343
+ case "package":
344
+ if (!baseDocument?.originalBuffer) return baseDocument?.package.styles;
345
+ return parsedPackageStyles(baseDocument, baseDocument.package.styles);
325
346
  case "supplied":
326
347
  assertSuppliedStylesMatchSource(source.styles, baseDocument);
327
348
  return source.styles;
@@ -347,13 +368,6 @@ const assertSaveStylesheetAvailable = ({ pmDoc, styles, baseDocument, stylesheet
347
368
  };
348
369
  /** Convert a ProseMirror document to the document model. */
349
370
  function fromProseDoc(pmDoc, baseDocument, { stylesheetSource, reuse = "none" }) {
350
- const styles = saveStylesheet(stylesheetSource, baseDocument);
351
- assertSaveStylesheetAvailable({
352
- pmDoc,
353
- styles,
354
- baseDocument,
355
- stylesheetSource
356
- });
357
371
  assertNoteReferenceOccurrences(pmDoc);
358
372
  switch (reuse) {
359
373
  case "none": break;
@@ -366,6 +380,13 @@ function fromProseDoc(pmDoc, baseDocument, { stylesheetSource, reuse = "none" })
366
380
  const proseContract = getProseDocumentParagraphPropertySourceContract(pmDoc);
367
381
  if (proseContractAttribute !== null && proseContractAttribute !== void 0 && proseContract === null || (baseContract ?? null) !== proseContract) throw sourceValidationError("contract_mismatch", "The ProseMirror document does not match its paragraph-property source document.");
368
382
  const tokenSources = baseContract && proseContract && baseDocument ? validateParagraphPropertySourceTokens(pmDoc, baseDocument, baseContract) : null;
383
+ const styles = saveStylesheet(stylesheetSource, baseDocument);
384
+ assertSaveStylesheetAvailable({
385
+ pmDoc,
386
+ styles,
387
+ baseDocument,
388
+ stylesheetSource
389
+ });
369
390
  const blocks = extractBlocks(pmDoc, "resolve", createStyleEngine(styles));
370
391
  joinCommentRangesAcrossParagraphs(blocks);
371
392
  completeCommentReferences(blocks);
@@ -384,7 +405,7 @@ function fromProseDoc(pmDoc, baseDocument, { stylesheetSource, reuse = "none" })
384
405
  package: {
385
406
  ...baseDocument.package,
386
407
  document: documentBody,
387
- ...styles ? { styles } : {},
408
+ styles,
388
409
  ...numbering ? { numbering } : {}
389
410
  }
390
411
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.58.0",
3
+ "version": "0.58.1",
4
4
  "description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
5
5
  "keywords": [
6
6
  "document-model",
@@ -146,7 +146,7 @@
146
146
  "perf": "bun scripts/profile-editor.ts"
147
147
  },
148
148
  "dependencies": {
149
- "@stll/docx-core": "^0.34.0",
149
+ "@stll/docx-core": "^0.35.0",
150
150
  "@stll/docx-utils": "^0.1.0",
151
151
  "@stll/template-conditions": "^0.6.2",
152
152
  "better-result": "3.0.1",