@stll/folio-core 0.9.0 → 0.11.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 (69) hide show
  1. package/dist/ai-edits/headless.d.ts +52 -12
  2. package/dist/ai-edits/headless.js +181 -55
  3. package/dist/ai-edits/index.d.ts +2 -2
  4. package/dist/ai-edits/index.js +2 -2
  5. package/dist/controller/collaborationModules.d.ts +7 -0
  6. package/dist/controller/collaborationModules.js +15 -0
  7. package/dist/controller/folioEditor.d.ts +1 -1
  8. package/dist/controller/headerFooterEditorManager.d.ts +41 -0
  9. package/dist/controller/headerFooterEditorManager.js +182 -0
  10. package/dist/controller/hiddenEditorManager.d.ts +1 -1
  11. package/dist/controller/layoutPipeline.js +2 -1
  12. package/dist/document-operations.d.ts +11 -2
  13. package/dist/document-operations.js +7 -1
  14. package/dist/document-stories.d.ts +10 -0
  15. package/dist/document-stories.js +27 -0
  16. package/dist/docx/corePropertiesParser.d.ts +8 -0
  17. package/dist/docx/corePropertiesParser.js +53 -0
  18. package/dist/docx/index.d.ts +2 -1
  19. package/dist/docx/index.js +2 -1
  20. package/dist/docx/metadataPrivacy.d.ts +40 -0
  21. package/dist/docx/metadataPrivacy.js +131 -0
  22. package/dist/docx/parser.js +4 -1
  23. package/dist/docx/rezip.d.ts +5 -4
  24. package/dist/docx/rezip.js +6 -8
  25. package/dist/layout-bridge/convert/toFlowBlocks.js +1 -0
  26. package/dist/layout-engine/measure/cache.js +2 -7
  27. package/dist/layout-engine/measure/effectiveLineBreakPolicy.d.ts +29 -0
  28. package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +60 -0
  29. package/dist/layout-engine/measure/measureParagraph.js +32 -36
  30. package/dist/layout-engine/measure/tableCellFloating.js +39 -33
  31. package/dist/layout-engine/types.d.ts +5 -3
  32. package/dist/managers/FindReplaceManager.d.ts +8 -0
  33. package/dist/managers/FindReplaceManager.js +18 -0
  34. package/dist/managers/TableSelectionManager.d.ts +6 -0
  35. package/dist/managers/TableSelectionManager.js +14 -0
  36. package/dist/prosemirror/attrs/index.js +1 -0
  37. package/dist/prosemirror/conversion/effectiveTableCellFormatting.d.ts +62 -0
  38. package/dist/prosemirror/conversion/effectiveTableCellFormatting.js +131 -0
  39. package/dist/prosemirror/conversion/fromProseDoc.js +1 -0
  40. package/dist/prosemirror/conversion/toProseDoc.js +40 -68
  41. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -1
  42. package/dist/prosemirror/extensions/marks/HighlightExtension.js +1 -1
  43. package/dist/prosemirror/extensions/marks/RunShadingExtension.js +1 -1
  44. package/dist/prosemirror/extensions/marks/TextColorExtension.js +1 -1
  45. package/dist/prosemirror/extensions/nodes/ImageExtension.js +1 -0
  46. package/dist/prosemirror/extensions/nodes/TableExtension.js +1 -1
  47. package/dist/prosemirror/findReplaceSelection.d.ts +6 -1
  48. package/dist/prosemirror/findReplaceSelection.js +57 -13
  49. package/dist/prosemirror/schema/nodes.d.ts +2 -1
  50. package/dist/redline.d.ts +26 -11
  51. package/dist/redline.js +95 -62
  52. package/dist/render-dom/BodySelectionOverlay.d.ts +31 -0
  53. package/dist/render-dom/BodySelectionOverlay.js +76 -0
  54. package/dist/render-dom/HeaderFooterSelectionOverlay.d.ts +28 -0
  55. package/dist/render-dom/HeaderFooterSelectionOverlay.js +121 -0
  56. package/dist/render-dom/RemoteSelectionOverlay.d.ts +30 -0
  57. package/dist/render-dom/RemoteSelectionOverlay.js +67 -0
  58. package/dist/render-dom/RenderedDomContext.d.ts +40 -0
  59. package/dist/render-dom/RenderedDomContext.js +126 -0
  60. package/dist/render-dom/resolveSidebarItemPositions.d.ts +33 -0
  61. package/dist/render-dom/resolveSidebarItemPositions.js +45 -0
  62. package/dist/server.d.ts +5 -4
  63. package/dist/server.js +5 -4
  64. package/dist/utils/findReplace.d.ts +32 -0
  65. package/dist/utils/findReplace.js +118 -0
  66. package/dist/utils/headerFooter.js +24 -1
  67. package/dist/version-comparison.d.ts +65 -11
  68. package/dist/version-comparison.js +187 -29
  69. package/package.json +1 -1
@@ -0,0 +1,182 @@
1
+ import { clearHeaderFooterVerbatimXml } from "../docx/headerFooterVerbatim.js";
2
+ import { proseDocToBlocks } from "../prosemirror/conversion/fromProseDoc.js";
3
+ import { ExtensionManager } from "../prosemirror/extensions/ExtensionManager.js";
4
+ import { ensureBaseDirectionInState } from "../prosemirror/extensions/features/AutoBidiDetectionExtension.js";
5
+ import { createDocumentStylesPlugin } from "../prosemirror/plugins/documentStyles.js";
6
+ import { ensureParaIdsInState } from "../prosemirror/extensions/features/ParaIdAllocatorExtension.js";
7
+ import { createStarterKit } from "../prosemirror/extensions/StarterKit.js";
8
+ import { schema } from "../prosemirror/schema/index.js";
9
+ import { headerFooterToProseDoc } from "../prosemirror/conversion/toProseDoc.js";
10
+ import { EditorState } from "prosemirror-state";
11
+ import { EditorView } from "prosemirror-view";
12
+ //#region src/controller/headerFooterEditorManager.ts
13
+ /**
14
+ * Framework-neutral lifecycle owner for persistent header/footer EditorViews.
15
+ *
16
+ * Each distinct relationship id owns one off-screen ProseMirror view. Painted
17
+ * header/footer instances that share that relationship therefore share live
18
+ * editor state, and adapters only decide when to synchronize or destroy the
19
+ * manager.
20
+ */
21
+ const buildInitialState = (headerFooter, styles, theme, manager) => {
22
+ const proseDocOptions = {};
23
+ if (styles) proseDocOptions.styles = styles;
24
+ if (theme !== void 0) proseDocOptions.theme = theme;
25
+ const document = headerFooterToProseDoc(headerFooter.content, proseDocOptions);
26
+ return ensureBaseDirectionInState(ensureParaIdsInState(EditorState.create({
27
+ doc: document,
28
+ schema,
29
+ plugins: [...manager.getPlugins(), createDocumentStylesPlugin(styles)]
30
+ })));
31
+ };
32
+ const enumerateHeaderFooterParts = ({ headers, footers }) => {
33
+ const parts = [];
34
+ if (headers) for (const rId of headers.keys()) parts.push({
35
+ kind: "header",
36
+ rId
37
+ });
38
+ if (footers) {
39
+ for (const rId of footers.keys()) if (!headers?.has(rId)) parts.push({
40
+ kind: "footer",
41
+ rId
42
+ });
43
+ }
44
+ return parts;
45
+ };
46
+ const enumerateDocumentHeaderFooterParts = (document) => {
47
+ if (!document?.package) return [];
48
+ return enumerateHeaderFooterParts({
49
+ headers: document.package.headers,
50
+ footers: document.package.footers
51
+ });
52
+ };
53
+ const resolveHeaderFooter = (document, { kind, rId }) => {
54
+ return (kind === "header" ? document?.package.headers : document?.package.footers)?.get(rId) ?? null;
55
+ };
56
+ const createHeaderFooterEditorManager = (deps) => {
57
+ const mounted = /* @__PURE__ */ new Map();
58
+ const destroyMounted = (part) => {
59
+ part.view.destroy();
60
+ part.manager.destroy();
61
+ part.mountNode.remove();
62
+ };
63
+ const destroy = () => {
64
+ for (const part of mounted.values()) destroyMounted(part);
65
+ mounted.clear();
66
+ };
67
+ const sync = () => {
68
+ const host = deps.getHost();
69
+ if (!host) return;
70
+ const document = deps.getDocument();
71
+ const styles = deps.getStyles();
72
+ const theme = deps.getTheme();
73
+ const wanted = new Map(enumerateDocumentHeaderFooterParts(document).map((part) => [part.rId, part]));
74
+ for (const [rId, part] of mounted) {
75
+ if (wanted.has(rId)) continue;
76
+ destroyMounted(part);
77
+ mounted.delete(rId);
78
+ }
79
+ for (const part of wanted.values()) {
80
+ const headerFooter = resolveHeaderFooter(document, part);
81
+ if (!headerFooter) continue;
82
+ const existing = mounted.get(part.rId);
83
+ if (existing) {
84
+ if (existing.mountNode.parentElement !== host) host.append(existing.mountNode);
85
+ const contentIsCurrent = existing.appliedHeaderFooter === headerFooter && existing.appliedContent === headerFooter.content;
86
+ const contextIsCurrent = existing.appliedStyles === styles && existing.appliedTheme === theme;
87
+ if (contentIsCurrent && contextIsCurrent) continue;
88
+ existing.view.updateState(buildInitialState(headerFooter, styles, theme, existing.manager));
89
+ existing.appliedHeaderFooter = headerFooter;
90
+ existing.appliedContent = headerFooter.content;
91
+ existing.appliedStyles = styles;
92
+ existing.appliedTheme = theme;
93
+ existing.dirty = false;
94
+ continue;
95
+ }
96
+ const manager = new ExtensionManager(createStarterKit());
97
+ manager.buildSchema();
98
+ manager.initializeRuntime();
99
+ const mountNode = host.ownerDocument.createElement("div");
100
+ mountNode.dataset["hfRId"] = part.rId;
101
+ mountNode.dataset["hfKind"] = part.kind;
102
+ host.append(mountNode);
103
+ const view = new EditorView(mountNode, {
104
+ state: buildInitialState(headerFooter, styles, theme, manager),
105
+ dispatchTransaction(transaction) {
106
+ const nextState = view.state.apply(transaction);
107
+ view.updateState(nextState);
108
+ const mountedPart = mounted.get(part.rId);
109
+ if (mountedPart && transaction.docChanged) mountedPart.dirty = true;
110
+ deps.onTransaction?.({
111
+ docChanged: transaction.docChanged,
112
+ kind: part.kind,
113
+ rId: part.rId,
114
+ selectionChanged: transaction.selectionSet,
115
+ view
116
+ });
117
+ }
118
+ });
119
+ mounted.set(part.rId, {
120
+ appliedContent: headerFooter.content,
121
+ appliedHeaderFooter: headerFooter,
122
+ appliedStyles: styles,
123
+ appliedTheme: theme,
124
+ dirty: false,
125
+ kind: part.kind,
126
+ manager,
127
+ mountNode,
128
+ rId: part.rId,
129
+ view
130
+ });
131
+ }
132
+ };
133
+ return {
134
+ destroy,
135
+ getView: (rId) => mounted.get(rId)?.view ?? null,
136
+ listSlots: () => [...mounted.values()].map(({ kind, rId }) => ({
137
+ kind,
138
+ rId
139
+ })),
140
+ snapshotDocument: (document) => {
141
+ let headers = document.package.headers;
142
+ let footers = document.package.footers;
143
+ let headersChanged = false;
144
+ let footersChanged = false;
145
+ for (const { dirty, kind, rId, view } of mounted.values()) {
146
+ if (!dirty) continue;
147
+ const source = kind === "header" ? headers : footers;
148
+ const existing = source?.get(rId);
149
+ if (!source || !existing) continue;
150
+ const updated = {
151
+ ...existing,
152
+ content: proseDocToBlocks(view.state.doc)
153
+ };
154
+ clearHeaderFooterVerbatimXml(updated);
155
+ if (kind === "header") {
156
+ if (!headersChanged) {
157
+ headers = new Map(headers);
158
+ headersChanged = true;
159
+ }
160
+ headers?.set(rId, updated);
161
+ } else {
162
+ if (!footersChanged) {
163
+ footers = new Map(footers);
164
+ footersChanged = true;
165
+ }
166
+ footers?.set(rId, updated);
167
+ }
168
+ }
169
+ if (!headersChanged && !footersChanged) return document;
170
+ const packageWithSnapshots = { ...document.package };
171
+ if (headersChanged && headers) packageWithSnapshots.headers = headers;
172
+ if (footersChanged && footers) packageWithSnapshots.footers = footers;
173
+ return {
174
+ ...document,
175
+ package: packageWithSnapshots
176
+ };
177
+ },
178
+ sync
179
+ };
180
+ };
181
+ //#endregion
182
+ export { createHeaderFooterEditorManager, enumerateDocumentHeaderFooterParts, enumerateHeaderFooterParts };
@@ -1,7 +1,7 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
- import { HiddenEditorApi } from "./hiddenEditorApi.js";
3
2
  import { HiddenEditorStateReason } from "../layout-engine/layoutInstrumentation.js";
4
3
  import { ExtensionManager } from "../prosemirror/extensions/ExtensionManager.js";
4
+ import { HiddenEditorApi } from "./hiddenEditorApi.js";
5
5
  import { EditorState, Plugin, Transaction } from "prosemirror-state";
6
6
  import { EditorView } from "prosemirror-view";
7
7
  import * as YProseMirror from "y-prosemirror";
@@ -78,7 +78,8 @@ function runLayoutPipeline(deps, state, options = {}) {
78
78
  if (documentSettings?.autoHyphenation === true) flowOpts.automaticHyphenation = {
79
79
  enabled: true,
80
80
  ...documentSettings.doNotHyphenateCaps !== void 0 ? { doNotHyphenateCaps: documentSettings.doNotHyphenateCaps } : {},
81
- ...documentSettings.consecutiveHyphenLimit !== void 0 ? { consecutiveLineLimit: documentSettings.consecutiveHyphenLimit } : {}
81
+ ...documentSettings.consecutiveHyphenLimit !== void 0 ? { consecutiveLineLimit: documentSettings.consecutiveHyphenLimit } : {},
82
+ ...documentSettings.hyphenationZoneTwips !== void 0 ? { hyphenationZoneTwips: documentSettings.hyphenationZoneTwips } : {}
82
83
  };
83
84
  let newBlocks = toFlowBlocks(state.doc, flowOpts);
84
85
  const previewState = templatePreviewValuesKey.getState(state);
@@ -5,7 +5,7 @@ import { FolioAIEditView } from "./ai-edits/apply.js";
5
5
  declare const FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION: 1;
6
6
  declare const FOLIO_DOCUMENT_OPERATION_TYPES: readonly ["replaceInBlock", "replaceRange", "commentOnRange", "formatRange", "insertAfterBlock", "insertBeforeBlock", "replaceBlock", "deleteBlock", "commentOnBlock", "insertSignatureTable"];
7
7
  declare const FOLIO_DOCUMENT_OPERATION_MODES: readonly ["direct", "tracked-changes"];
8
- declare const FOLIO_DOCUMENT_OPERATION_STORIES: readonly ["main"];
8
+ declare const FOLIO_DOCUMENT_OPERATION_STORIES: readonly ["main", "header", "footer", "footnote", "endnote"];
9
9
  declare const FOLIO_DOCUMENT_OPERATION_PRECONDITIONS: readonly ["blockTextHash"];
10
10
  declare const FOLIO_DOCUMENT_OPERATION_BATCH_MODES: readonly ["best-effort", "atomic"];
11
11
  type FolioDocumentOperation = FolioAIEditOperation;
@@ -68,8 +68,17 @@ type FolioDocumentOperationIssue = {
68
68
  recovery: FolioDocumentOperationRecovery;
69
69
  };
70
70
  type FolioDocumentOperationStory = "main" | {
71
- type: "header" | "footer";
71
+ type: "header";
72
72
  relationshipId: string;
73
+ } | {
74
+ type: "footer";
75
+ relationshipId: string;
76
+ } | {
77
+ type: "footnote";
78
+ noteId: number;
79
+ } | {
80
+ type: "endnote";
81
+ noteId: number;
73
82
  };
74
83
  /** One typed target affected by a successfully applied document operation. */
75
84
  type FolioDocumentOperationAffectedTarget = {
@@ -15,7 +15,13 @@ const FOLIO_DOCUMENT_OPERATION_TYPES = Object.freeze([
15
15
  "insertSignatureTable"
16
16
  ]);
17
17
  const FOLIO_DOCUMENT_OPERATION_MODES = Object.freeze(["direct", "tracked-changes"]);
18
- const FOLIO_DOCUMENT_OPERATION_STORIES = Object.freeze(["main"]);
18
+ const FOLIO_DOCUMENT_OPERATION_STORIES = Object.freeze([
19
+ "main",
20
+ "header",
21
+ "footer",
22
+ "footnote",
23
+ "endnote"
24
+ ]);
19
25
  const FOLIO_DOCUMENT_OPERATION_PRECONDITIONS = Object.freeze(["blockTextHash"]);
20
26
  const FOLIO_DOCUMENT_OPERATION_BATCH_MODES = Object.freeze(["best-effort", "atomic"]);
21
27
  const DIRECT_AND_TRACKED_MODES = FOLIO_DOCUMENT_OPERATION_MODES;
@@ -0,0 +1,10 @@
1
+ import { FolioDocumentStoryHandle } from "./ai-edits/headless.js";
2
+
3
+ //#region src/document-stories.d.ts
4
+ type FolioDocumentStoryPair = {
5
+ baseStory: FolioDocumentStoryHandle | null;
6
+ revisedStory: FolioDocumentStoryHandle | null;
7
+ };
8
+ declare const pairFolioDocumentStories: (baseStories: readonly FolioDocumentStoryHandle[], revisedStories: readonly FolioDocumentStoryHandle[]) => FolioDocumentStoryPair[];
9
+ //#endregion
10
+ export { FolioDocumentStoryPair, pairFolioDocumentStories };
@@ -0,0 +1,27 @@
1
+ //#region src/document-stories.ts
2
+ const documentStoryKey = (story) => {
3
+ if (story.type === "main") return story.type;
4
+ if (story.type === "header" || story.type === "footer") return `${story.type}:${story.relationshipId}`;
5
+ return `${story.type}:${String(story.noteId)}`;
6
+ };
7
+ const pairFolioDocumentStories = (baseStories, revisedStories) => {
8
+ const revisedByKey = new Map(revisedStories.map((story) => [documentStoryKey(story), story]));
9
+ const pairedKeys = /* @__PURE__ */ new Set();
10
+ const pairs = [];
11
+ for (const baseStory of baseStories) {
12
+ const key = documentStoryKey(baseStory);
13
+ const revisedStory = revisedByKey.get(key) ?? null;
14
+ pairs.push({
15
+ baseStory,
16
+ revisedStory
17
+ });
18
+ if (revisedStory) pairedKeys.add(key);
19
+ }
20
+ for (const revisedStory of revisedStories) if (!pairedKeys.has(documentStoryKey(revisedStory))) pairs.push({
21
+ baseStory: null,
22
+ revisedStory
23
+ });
24
+ return pairs;
25
+ };
26
+ //#endregion
27
+ export { pairFolioDocumentStories };
@@ -0,0 +1,8 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+
3
+ //#region src/docx/corePropertiesParser.d.ts
4
+ type DocumentProperties = NonNullable<document_d_exports.DocxPackage["properties"]>;
5
+ /** Parse recognized package metadata from `docProps/core.xml`. */
6
+ declare const parseCoreProperties: (xml: string | null) => DocumentProperties | undefined;
7
+ //#endregion
8
+ export { parseCoreProperties };
@@ -0,0 +1,53 @@
1
+ import { findChildByLocalName, getTextContent, parseXmlDocument } from "./xmlParser.js";
2
+ //#region src/docx/corePropertiesParser.ts
3
+ const STRING_PROPERTIES = [
4
+ "title",
5
+ "subject",
6
+ "creator",
7
+ "keywords",
8
+ "description",
9
+ "lastModifiedBy"
10
+ ];
11
+ const parseRevision = (value) => {
12
+ if (!/^\d+$/u.test(value)) return;
13
+ const revision = Number(value);
14
+ return Number.isSafeInteger(revision) ? revision : void 0;
15
+ };
16
+ const parseDate = (value) => {
17
+ const timestamp = Date.parse(value);
18
+ return Number.isNaN(timestamp) ? void 0 : new Date(timestamp);
19
+ };
20
+ /** Parse recognized package metadata from `docProps/core.xml`. */
21
+ const parseCoreProperties = (xml) => {
22
+ if (!xml) return;
23
+ const root = parseXmlDocument(xml);
24
+ if (!root) return;
25
+ const properties = {};
26
+ let recognizedPropertyCount = 0;
27
+ for (const property of STRING_PROPERTIES) {
28
+ const element = findChildByLocalName(root, property);
29
+ if (!element) continue;
30
+ properties[property] = getTextContent(element);
31
+ recognizedPropertyCount++;
32
+ }
33
+ const revisionElement = findChildByLocalName(root, "revision");
34
+ if (revisionElement) {
35
+ const revision = parseRevision(getTextContent(revisionElement));
36
+ if (revision !== void 0) {
37
+ properties.revision = revision;
38
+ recognizedPropertyCount++;
39
+ }
40
+ }
41
+ for (const property of ["created", "modified"]) {
42
+ const element = findChildByLocalName(root, property);
43
+ if (!element) continue;
44
+ const date = parseDate(getTextContent(element));
45
+ if (date !== void 0) {
46
+ properties[property] = date;
47
+ recognizedPropertyCount++;
48
+ }
49
+ }
50
+ return recognizedPropertyCount > 0 ? properties : void 0;
51
+ };
52
+ //#endregion
53
+ export { parseCoreProperties };
@@ -1,4 +1,5 @@
1
1
  import { getCachedNumberingMap } from "./numberingParser.js";
2
2
  import { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError, DocxEncryptionErrorCode, isDocxEncryptionError } from "./encryption/errors.js";
3
3
  import { DecryptDocxOptions, DecryptDocxResult, decryptDocxIfNeeded, openDocxBuffer } from "./encryption/openEncryptedDocx.js";
4
- export { DOCX_ENCRYPTION_ERROR_CODES, type DecryptDocxOptions, type DecryptDocxResult, DocxEncryptionError, type DocxEncryptionErrorCode, decryptDocxIfNeeded, getCachedNumberingMap, isDocxEncryptionError, openDocxBuffer };
4
+ import { FOLIO_DOCUMENT_METADATA_PROPERTIES, FOLIO_DOCUMENT_PRIVACY_TRANSFORMS, FolioDocumentMetadataProperty, FolioDocumentPrivacyArchiveError, FolioDocumentPrivacyOptions, FolioDocumentPrivacyReport, FolioDocumentPrivacyTransform, InvalidFolioDocumentPrivacyOptionsError, RewriteDocxMetadataPrivacyResult, isFolioDocumentPrivacyTransform, rewriteDocxMetadataPrivacy } from "./metadataPrivacy.js";
5
+ export { DOCX_ENCRYPTION_ERROR_CODES, type DecryptDocxOptions, type DecryptDocxResult, DocxEncryptionError, type DocxEncryptionErrorCode, FOLIO_DOCUMENT_METADATA_PROPERTIES, FOLIO_DOCUMENT_PRIVACY_TRANSFORMS, type FolioDocumentMetadataProperty, FolioDocumentPrivacyArchiveError, type FolioDocumentPrivacyOptions, type FolioDocumentPrivacyReport, type FolioDocumentPrivacyTransform, InvalidFolioDocumentPrivacyOptionsError, type RewriteDocxMetadataPrivacyResult, decryptDocxIfNeeded, getCachedNumberingMap, isDocxEncryptionError, isFolioDocumentPrivacyTransform, openDocxBuffer, rewriteDocxMetadataPrivacy };
@@ -1,4 +1,5 @@
1
1
  import { getCachedNumberingMap } from "./numberingParser.js";
2
2
  import { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError, isDocxEncryptionError } from "./encryption/errors.js";
3
3
  import { decryptDocxIfNeeded, openDocxBuffer } from "./encryption/openEncryptedDocx.js";
4
- export { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError, decryptDocxIfNeeded, getCachedNumberingMap, isDocxEncryptionError, openDocxBuffer };
4
+ import { FOLIO_DOCUMENT_METADATA_PROPERTIES, FOLIO_DOCUMENT_PRIVACY_TRANSFORMS, FolioDocumentPrivacyArchiveError, InvalidFolioDocumentPrivacyOptionsError, isFolioDocumentPrivacyTransform, rewriteDocxMetadataPrivacy } from "./metadataPrivacy.js";
5
+ export { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError, FOLIO_DOCUMENT_METADATA_PROPERTIES, FOLIO_DOCUMENT_PRIVACY_TRANSFORMS, FolioDocumentPrivacyArchiveError, InvalidFolioDocumentPrivacyOptionsError, decryptDocxIfNeeded, getCachedNumberingMap, isDocxEncryptionError, isFolioDocumentPrivacyTransform, openDocxBuffer, rewriteDocxMetadataPrivacy };
@@ -0,0 +1,40 @@
1
+ //#region src/docx/metadataPrivacy.d.ts
2
+ declare const FOLIO_DOCUMENT_METADATA_PROPERTIES: readonly ["title", "subject", "creator", "keywords", "description", "lastModifiedBy", "revision", "created", "modified"];
3
+ type FolioDocumentMetadataProperty = (typeof FOLIO_DOCUMENT_METADATA_PROPERTIES)[number];
4
+ declare const FOLIO_DOCUMENT_PRIVACY_TRANSFORMS: readonly ["remove-attribution", "remove-timestamps", "remove-descriptive-metadata"];
5
+ type FolioDocumentPrivacyTransform = (typeof FOLIO_DOCUMENT_PRIVACY_TRANSFORMS)[number];
6
+ declare const isFolioDocumentPrivacyTransform: (value: unknown) => value is FolioDocumentPrivacyTransform;
7
+ type FolioDocumentPrivacyOptions = {
8
+ transforms: readonly FolioDocumentPrivacyTransform[];
9
+ };
10
+ type FolioDocumentPrivacyReport = {
11
+ appliedTransforms: FolioDocumentPrivacyTransform[];
12
+ removedMetadataProperties: FolioDocumentMetadataProperty[];
13
+ };
14
+ type RewriteDocxMetadataPrivacyResult = {
15
+ buffer: ArrayBuffer;
16
+ privacyReport: FolioDocumentPrivacyReport;
17
+ };
18
+ declare const InvalidFolioDocumentPrivacyOptionsError_base: import("better-result").TaggedErrorClass<"InvalidFolioDocumentPrivacyOptionsError", {
19
+ message: string;
20
+ receivedValue: unknown;
21
+ }>;
22
+ declare class InvalidFolioDocumentPrivacyOptionsError extends InvalidFolioDocumentPrivacyOptionsError_base {}
23
+ declare const FolioDocumentPrivacyArchiveError_base: import("better-result").TaggedErrorClass<"FolioDocumentPrivacyArchiveError", {
24
+ message: string;
25
+ reason: "input-too-large" | "load-failed" | "too-many-entries" | "core-properties-too-large";
26
+ cause?: unknown;
27
+ }>;
28
+ declare class FolioDocumentPrivacyArchiveError extends FolioDocumentPrivacyArchiveError_base {}
29
+ declare const PRIVATE_METADATA_PROPERTIES_BY_TRANSFORM: {
30
+ readonly "remove-attribution": readonly ["creator", "lastModifiedBy"];
31
+ readonly "remove-timestamps": readonly ["created", "modified"];
32
+ readonly "remove-descriptive-metadata": readonly ["title", "subject", "keywords", "description"];
33
+ };
34
+ declare const resolveFolioDocumentPrivacyTransforms: (transforms: unknown) => FolioDocumentPrivacyTransform[];
35
+ /** Rewrite selected package metadata fields without changing other package parts. */
36
+ declare const rewriteDocxMetadataPrivacy: (buffer: ArrayBuffer, {
37
+ transforms
38
+ }: FolioDocumentPrivacyOptions) => Promise<RewriteDocxMetadataPrivacyResult>;
39
+ //#endregion
40
+ export { FOLIO_DOCUMENT_METADATA_PROPERTIES, FOLIO_DOCUMENT_PRIVACY_TRANSFORMS, FolioDocumentMetadataProperty, FolioDocumentPrivacyArchiveError, FolioDocumentPrivacyOptions, FolioDocumentPrivacyReport, FolioDocumentPrivacyTransform, InvalidFolioDocumentPrivacyOptionsError, PRIVATE_METADATA_PROPERTIES_BY_TRANSFORM, RewriteDocxMetadataPrivacyResult, isFolioDocumentPrivacyTransform, resolveFolioDocumentPrivacyTransforms, rewriteDocxMetadataPrivacy };
@@ -0,0 +1,131 @@
1
+ import { elementToXml, getLocalName, parseXmlDocument } from "./xmlParser.js";
2
+ import { TaggedError } from "better-result";
3
+ import JSZip from "jszip";
4
+ //#region src/docx/metadataPrivacy.ts
5
+ const FOLIO_DOCUMENT_METADATA_PROPERTIES = Object.freeze([
6
+ "title",
7
+ "subject",
8
+ "creator",
9
+ "keywords",
10
+ "description",
11
+ "lastModifiedBy",
12
+ "revision",
13
+ "created",
14
+ "modified"
15
+ ]);
16
+ const isFolioDocumentMetadataProperty = (value) => FOLIO_DOCUMENT_METADATA_PROPERTIES.some((property) => property === value);
17
+ const FOLIO_DOCUMENT_PRIVACY_TRANSFORMS = Object.freeze([
18
+ "remove-attribution",
19
+ "remove-timestamps",
20
+ "remove-descriptive-metadata"
21
+ ]);
22
+ const isFolioDocumentPrivacyTransform = (value) => FOLIO_DOCUMENT_PRIVACY_TRANSFORMS.some((transform) => transform === value);
23
+ var InvalidFolioDocumentPrivacyOptionsError = class extends TaggedError("InvalidFolioDocumentPrivacyOptionsError")() {};
24
+ var FolioDocumentPrivacyArchiveError = class extends TaggedError("FolioDocumentPrivacyArchiveError")() {};
25
+ const PRIVATE_METADATA_PROPERTIES_BY_TRANSFORM = {
26
+ "remove-attribution": ["creator", "lastModifiedBy"],
27
+ "remove-timestamps": ["created", "modified"],
28
+ "remove-descriptive-metadata": [
29
+ "title",
30
+ "subject",
31
+ "keywords",
32
+ "description"
33
+ ]
34
+ };
35
+ const resolveFolioDocumentPrivacyTransforms = (transforms) => {
36
+ if (!Array.isArray(transforms) || transforms.some((transform) => !isFolioDocumentPrivacyTransform(transform))) throw new InvalidFolioDocumentPrivacyOptionsError({
37
+ message: "Document privacy received an unrecognized transform",
38
+ receivedValue: transforms
39
+ });
40
+ const requested = new Set(transforms);
41
+ return FOLIO_DOCUMENT_PRIVACY_TRANSFORMS.filter((transform) => requested.has(transform));
42
+ };
43
+ const XML_DECLARATION_PATTERN = /^\s*<\?xml[^?]*\?>/u;
44
+ const MAX_INPUT_BYTES = 50 * 1024 * 1024;
45
+ const MAX_ARCHIVE_ENTRIES = 5e3;
46
+ const MAX_CORE_PROPERTIES_BYTES = 1024 * 1024;
47
+ const loadPrivacyArchive = async (buffer) => {
48
+ if (buffer.byteLength > MAX_INPUT_BYTES) throw new FolioDocumentPrivacyArchiveError({
49
+ message: "Document privacy input exceeded the compressed-size limit",
50
+ reason: "input-too-large"
51
+ });
52
+ let zip;
53
+ try {
54
+ zip = await JSZip.loadAsync(buffer);
55
+ } catch (cause) {
56
+ throw new FolioDocumentPrivacyArchiveError({
57
+ message: "Document privacy input is not a readable package",
58
+ reason: "load-failed",
59
+ cause
60
+ });
61
+ }
62
+ if (Object.values(zip.files).length > MAX_ARCHIVE_ENTRIES) throw new FolioDocumentPrivacyArchiveError({
63
+ message: "Document privacy input exceeded the package-entry limit",
64
+ reason: "too-many-entries"
65
+ });
66
+ const coreProperties = zip.file("docProps/core.xml");
67
+ if (!coreProperties) return zip;
68
+ const data = "_data" in coreProperties ? coreProperties._data : void 0;
69
+ const declaredBytes = typeof data === "object" && data !== null && "uncompressedSize" in data ? data.uncompressedSize : void 0;
70
+ if (typeof declaredBytes !== "number" || declaredBytes > MAX_CORE_PROPERTIES_BYTES) throw new FolioDocumentPrivacyArchiveError({
71
+ message: "Document privacy core properties exceeded the part-size limit",
72
+ reason: "core-properties-too-large"
73
+ });
74
+ return zip;
75
+ };
76
+ const rewriteCorePropertiesPrivacy = (xml, transforms) => {
77
+ const root = parseXmlDocument(xml);
78
+ if (!root?.elements) return {
79
+ xml,
80
+ removedMetadataProperties: []
81
+ };
82
+ const propertiesToRemove = /* @__PURE__ */ new Set();
83
+ for (const transform of transforms) for (const property of PRIVATE_METADATA_PROPERTIES_BY_TRANSFORM[transform]) propertiesToRemove.add(property);
84
+ const removedPropertySet = /* @__PURE__ */ new Set();
85
+ root.elements = root.elements.filter((element) => {
86
+ if (element.type !== "element") return true;
87
+ const property = getLocalName(element.name ?? "");
88
+ if (!isFolioDocumentMetadataProperty(property)) return true;
89
+ if (!propertiesToRemove.has(property)) return true;
90
+ removedPropertySet.add(property);
91
+ return false;
92
+ });
93
+ return {
94
+ xml: `${xml.match(XML_DECLARATION_PATTERN)?.at(0) ?? ""}${elementToXml(root)}`,
95
+ removedMetadataProperties: FOLIO_DOCUMENT_METADATA_PROPERTIES.filter((property) => removedPropertySet.has(property))
96
+ };
97
+ };
98
+ /** Rewrite selected package metadata fields without changing other package parts. */
99
+ const rewriteDocxMetadataPrivacy = async (buffer, { transforms }) => {
100
+ const appliedTransforms = resolveFolioDocumentPrivacyTransforms(transforms);
101
+ const zip = await loadPrivacyArchive(buffer);
102
+ const coreProperties = zip.file("docProps/core.xml");
103
+ if (!coreProperties) return {
104
+ buffer,
105
+ privacyReport: {
106
+ appliedTransforms,
107
+ removedMetadataProperties: []
108
+ }
109
+ };
110
+ const rewritten = rewriteCorePropertiesPrivacy(await coreProperties.async("text"), appliedTransforms);
111
+ if (rewritten.removedMetadataProperties.length === 0) return {
112
+ buffer,
113
+ privacyReport: {
114
+ appliedTransforms,
115
+ removedMetadataProperties: []
116
+ }
117
+ };
118
+ zip.file("docProps/core.xml", rewritten.xml);
119
+ return {
120
+ buffer: await zip.generateAsync({
121
+ type: "arraybuffer",
122
+ compression: "DEFLATE"
123
+ }),
124
+ privacyReport: {
125
+ appliedTransforms,
126
+ removedMetadataProperties: rewritten.removedMetadataProperties
127
+ }
128
+ };
129
+ };
130
+ //#endregion
131
+ export { FOLIO_DOCUMENT_METADATA_PROPERTIES, FOLIO_DOCUMENT_PRIVACY_TRANSFORMS, FolioDocumentPrivacyArchiveError, InvalidFolioDocumentPrivacyOptionsError, PRIVATE_METADATA_PROPERTIES_BY_TRANSFORM, isFolioDocumentPrivacyTransform, resolveFolioDocumentPrivacyTransforms, rewriteDocxMetadataPrivacy };
@@ -6,6 +6,7 @@ import { applyThemeFontLang, parseTheme } from "./themeParser.js";
6
6
  import { parseComments } from "./commentParser.js";
7
7
  import { normalizeCommentReferences } from "./commentReferenceNormalization.js";
8
8
  import { detectDocxConformanceClass } from "./conformance.js";
9
+ import { parseCoreProperties } from "./corePropertiesParser.js";
9
10
  import { parseNumbering } from "./numberingParser.js";
10
11
  import { extractAllTemplateVariables, parseDocumentBody } from "./documentParser.js";
11
12
  import { parseEndnotes, parseFootnotes } from "./footnoteParser.js";
@@ -168,6 +169,7 @@ async function parseDocx(input, options = {}) {
168
169
  onProgress("Loaded fonts", 95);
169
170
  } else onProgress("Skipping font loading", 95);
170
171
  onProgress("Assembling document...", 95);
172
+ const properties = timeStage("coreProperties", () => parseCoreProperties(raw.corePropsXml));
171
173
  const document = {
172
174
  package: {
173
175
  conformanceClass: detectDocxConformanceClass(raw.documentXml),
@@ -182,7 +184,8 @@ async function parseDocx(input, options = {}) {
182
184
  ...footnotes !== void 0 ? { footnotes } : {},
183
185
  ...endnotes !== void 0 ? { endnotes } : {},
184
186
  relationships: rels,
185
- media
187
+ media,
188
+ ...properties !== void 0 ? { properties } : {}
186
189
  },
187
190
  originalBuffer: raw.originalBuffer,
188
191
  ...templateVariables !== void 0 ? { templateVariables } : {},
@@ -124,10 +124,11 @@ declare function hasUnmaterializedHeaderFooter(doc: document_d_exports.Document)
124
124
  */
125
125
  declare function hasModelDrivenPictureWatermark(doc: document_d_exports.Document): boolean;
126
126
  declare function collectHeaderFooterUpdates(doc: document_d_exports.Document): Map<string, string>;
127
- /**
128
- * Update core properties XML with new modification date
129
- */
130
- declare function updateCoreProperties(corePropsXml: string, options: {
127
+ /** Update existing core-property values without synthesizing absent metadata. */
128
+ declare function updateCoreProperties(corePropsXml: string, {
129
+ updateModifiedDate,
130
+ modifiedBy
131
+ }: {
131
132
  updateModifiedDate?: boolean;
132
133
  modifiedBy?: string;
133
134
  }): string;
@@ -1022,18 +1022,16 @@ function findNotePartEntry(zip, conventionalLowerPath) {
1022
1022
  for (const [path, file] of Object.entries(zip.files)) if (!file.dir && path.toLowerCase() === conventionalLowerPath) return file;
1023
1023
  return null;
1024
1024
  }
1025
- /**
1026
- * Update core properties XML with new modification date
1027
- */
1028
- function updateCoreProperties(corePropsXml, options) {
1025
+ /** Update existing core-property values without synthesizing absent metadata. */
1026
+ function updateCoreProperties(corePropsXml, { updateModifiedDate, modifiedBy }) {
1029
1027
  let result = corePropsXml;
1030
- if (options.updateModifiedDate) {
1028
+ if (updateModifiedDate) {
1031
1029
  const now = (/* @__PURE__ */ new Date()).toISOString();
1032
1030
  if (result.includes("<dcterms:modified")) result = result.replace(/<dcterms:modified[^<>]*>[^<]*<\/dcterms:modified>/u, `<dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified>`);
1033
- else result = result.replace("</cp:coreProperties>", `<dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified></cp:coreProperties>`);
1034
1031
  }
1035
- if (options.modifiedBy) if (result.includes("<cp:lastModifiedBy")) result = result.replace(/<cp:lastModifiedBy>[^<]*<\/cp:lastModifiedBy>/u, `<cp:lastModifiedBy>${escapeXml(options.modifiedBy)}</cp:lastModifiedBy>`);
1036
- else result = result.replace("</cp:coreProperties>", `<cp:lastModifiedBy>${escapeXml(options.modifiedBy)}</cp:lastModifiedBy></cp:coreProperties>`);
1032
+ if (modifiedBy) {
1033
+ if (result.includes("<cp:lastModifiedBy")) result = result.replace(/<cp:lastModifiedBy>[^<]*<\/cp:lastModifiedBy>/u, `<cp:lastModifiedBy>${escapeXml(modifiedBy)}</cp:lastModifiedBy>`);
1034
+ }
1037
1035
  return result;
1038
1036
  }
1039
1037
  /**
@@ -445,6 +445,7 @@ function buildImageRun(attrs, constrained, pmStart, pmEnd, trackedChange) {
445
445
  if (attrs.cropBottom != null) run.cropBottom = attrs.cropBottom;
446
446
  if (attrs.cropLeft != null) run.cropLeft = attrs.cropLeft;
447
447
  if (attrs.position !== void 0) run.position = attrs.position;
448
+ if (attrs.layoutInCell !== void 0) run.layoutInCell = attrs.layoutInCell;
448
449
  if (trackedChange?.isInsertion) run.isInsertion = true;
449
450
  if (trackedChange?.isDeletion) run.isDeletion = true;
450
451
  if (trackedChange?.changeAuthor !== void 0) run.changeAuthor = trackedChange.changeAuthor;
@@ -1,4 +1,5 @@
1
1
  import { getLineBreakProviderGeneration } from "./lineBreakProvider.js";
2
+ import { lineBreakPolicyCacheParts } from "./effectiveLineBreakPolicy.js";
2
3
  //#region src/layout-engine/measure/cache.ts
3
4
  /**
4
5
  * Current max size for text width cache
@@ -168,13 +169,7 @@ function hashParagraphBlock(block) {
168
169
  if (attrs.defaultFontFamily != null) parts.push(`dff:${attrs.defaultFontFamily}`);
169
170
  if (attrs.suppressEmptyParagraphHeight) parts.push("sup");
170
171
  if (attrs.reserveEmptyOutlineHeight) parts.push("outline-empty-reserve");
171
- if (attrs.kinsoku !== void 0) parts.push(`kinsoku:${attrs.kinsoku}`);
172
- if (attrs.overflowPunctuation !== void 0) parts.push(`overflow-punct:${attrs.overflowPunctuation}`);
173
- if (attrs.suppressAutoHyphens !== void 0) parts.push(`suppress-auto-hyphens:${attrs.suppressAutoHyphens}`);
174
- const automaticHyphenation = attrs.automaticHyphenation;
175
- if (automaticHyphenation) parts.push(`auto-hyphens:${automaticHyphenation.doNotHyphenateCaps}|${automaticHyphenation.consecutiveLineLimit}`);
176
- const lineBreakRules = attrs.lineBreakRules;
177
- if (lineBreakRules) parts.push(`line-break-rules:${lineBreakRules.noLineBreaksBefore?.language}|${lineBreakRules.noLineBreaksBefore?.characters}|${lineBreakRules.noLineBreaksAfter?.language}|${lineBreakRules.noLineBreaksAfter?.characters}|${lineBreakRules.useLegacyEthiopicAmharicRules}`);
172
+ parts.push(...lineBreakPolicyCacheParts(attrs));
178
173
  const borders = attrs.borders;
179
174
  if (borders) {
180
175
  const signature = (border) => border ? `${border.width ?? ""},${border.style ?? ""},${border.color ?? ""}` : "";