@stll/folio-core 0.9.0 → 0.10.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 (44) 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/layoutPipeline.js +2 -1
  6. package/dist/document-operations.d.ts +11 -2
  7. package/dist/document-operations.js +7 -1
  8. package/dist/document-stories.d.ts +10 -0
  9. package/dist/document-stories.js +27 -0
  10. package/dist/docx/corePropertiesParser.d.ts +8 -0
  11. package/dist/docx/corePropertiesParser.js +53 -0
  12. package/dist/docx/index.d.ts +2 -1
  13. package/dist/docx/index.js +2 -1
  14. package/dist/docx/metadataPrivacy.d.ts +40 -0
  15. package/dist/docx/metadataPrivacy.js +131 -0
  16. package/dist/docx/parser.js +4 -1
  17. package/dist/docx/rezip.d.ts +5 -4
  18. package/dist/docx/rezip.js +6 -8
  19. package/dist/layout-bridge/convert/toFlowBlocks.js +1 -0
  20. package/dist/layout-engine/measure/cache.js +2 -7
  21. package/dist/layout-engine/measure/effectiveLineBreakPolicy.d.ts +29 -0
  22. package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +60 -0
  23. package/dist/layout-engine/measure/measureParagraph.js +32 -36
  24. package/dist/layout-engine/measure/tableCellFloating.js +39 -33
  25. package/dist/layout-engine/types.d.ts +5 -3
  26. package/dist/prosemirror/attrs/index.js +1 -0
  27. package/dist/prosemirror/conversion/effectiveTableCellFormatting.d.ts +62 -0
  28. package/dist/prosemirror/conversion/effectiveTableCellFormatting.js +131 -0
  29. package/dist/prosemirror/conversion/fromProseDoc.js +1 -0
  30. package/dist/prosemirror/conversion/toProseDoc.js +40 -68
  31. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -1
  32. package/dist/prosemirror/extensions/marks/HighlightExtension.js +1 -1
  33. package/dist/prosemirror/extensions/marks/RunShadingExtension.js +1 -1
  34. package/dist/prosemirror/extensions/marks/TextColorExtension.js +1 -1
  35. package/dist/prosemirror/extensions/nodes/ImageExtension.js +1 -0
  36. package/dist/prosemirror/extensions/nodes/TableExtension.js +1 -1
  37. package/dist/prosemirror/schema/nodes.d.ts +2 -1
  38. package/dist/redline.d.ts +26 -11
  39. package/dist/redline.js +95 -62
  40. package/dist/server.d.ts +5 -4
  41. package/dist/server.js +5 -4
  42. package/dist/version-comparison.d.ts +65 -11
  43. package/dist/version-comparison.js +187 -29
  44. package/package.json +1 -1
@@ -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 ?? ""}` : "";
@@ -0,0 +1,29 @@
1
+ import { ParagraphAttrs, TextRun } from "../types.js";
2
+ import { LineBreakPolicy } from "./lineBreakProvider.js";
3
+
4
+ //#region src/layout-engine/measure/effectiveLineBreakPolicy.d.ts
5
+ type EffectiveAutomaticHyphenation = Readonly<{
6
+ type: "disabled";
7
+ }> | Readonly<{
8
+ type: "enabled";
9
+ consecutiveLineLimit: number;
10
+ hyphenationZoneTwips: number;
11
+ }>;
12
+ type EffectiveLineBreakPolicy = Readonly<{
13
+ /** Exact provider inputs; omitted OOXML values stay omitted for custom providers. */provider: Readonly<LineBreakPolicy>; /** `w:overflowPunct` defaults to enabled when omitted. */
14
+ hangingPunctuation: boolean; /** Effective document and paragraph automatic-hyphenation state. */
15
+ automaticHyphenation: EffectiveAutomaticHyphenation;
16
+ }>;
17
+ type ResolveEffectiveLineBreakPolicyOptions = {
18
+ attrs: ParagraphAttrs | undefined;
19
+ run: TextRun;
20
+ };
21
+ /** Resolve all authored line-breaking inputs used while measuring one text run. */
22
+ declare const resolveEffectiveLineBreakPolicy: ({
23
+ attrs,
24
+ run
25
+ }: ResolveEffectiveLineBreakPolicyOptions) => EffectiveLineBreakPolicy;
26
+ /** Cache fragments for every paragraph attribute consumed by the resolver. */
27
+ declare const lineBreakPolicyCacheParts: (attrs: ParagraphAttrs) => readonly string[];
28
+ //#endregion
29
+ export { lineBreakPolicyCacheParts, resolveEffectiveLineBreakPolicy };
@@ -0,0 +1,60 @@
1
+ import { hasCjk } from "../../utils/scriptSegments.js";
2
+ //#region src/layout-engine/measure/effectiveLineBreakPolicy.ts
3
+ const DEFAULT_HYPHENATION_ZONE_TWIPS = 360;
4
+ /** Resolve all authored line-breaking inputs used while measuring one text run. */
5
+ const resolveEffectiveLineBreakPolicy = ({ attrs, run }) => {
6
+ const locale = resolveRunLocale(run);
7
+ const rules = attrs?.lineBreakRules;
8
+ const before = rules?.noLineBreaksBefore;
9
+ const after = rules?.noLineBreaksAfter;
10
+ const automaticHyphenation = attrs?.automaticHyphenation;
11
+ return {
12
+ provider: {
13
+ ...locale ? { locale } : {},
14
+ ...attrs?.kinsoku !== void 0 ? { kinsoku: attrs.kinsoku } : {},
15
+ ...before && languageMatches(before.language, locale) ? { noLineBreaksBefore: before.characters } : {},
16
+ ...after && languageMatches(after.language, locale) ? { noLineBreaksAfter: after.characters } : {},
17
+ ...rules?.useLegacyEthiopicAmharicRules ? { useLegacyEthiopicAmharicRules: true } : {},
18
+ ...automaticHyphenation?.doNotHyphenateCaps !== void 0 ? { doNotHyphenateCaps: automaticHyphenation.doNotHyphenateCaps } : {},
19
+ ...run.allCaps === true ? { renderedAllCaps: true } : {}
20
+ },
21
+ hangingPunctuation: attrs?.overflowPunctuation !== false,
22
+ automaticHyphenation: resolveAutomaticHyphenation(attrs)
23
+ };
24
+ };
25
+ /** Cache fragments for every paragraph attribute consumed by the resolver. */
26
+ const lineBreakPolicyCacheParts = (attrs) => {
27
+ const parts = [];
28
+ if (attrs.kinsoku !== void 0) parts.push(`kinsoku:${attrs.kinsoku}`);
29
+ if (attrs.overflowPunctuation !== void 0) parts.push(`overflow-punct:${attrs.overflowPunctuation}`);
30
+ if (attrs.suppressAutoHyphens !== void 0) parts.push(`suppress-auto-hyphens:${attrs.suppressAutoHyphens}`);
31
+ const automaticHyphenation = attrs.automaticHyphenation;
32
+ if (automaticHyphenation) parts.push(`auto-hyphens:${automaticHyphenation.doNotHyphenateCaps}|${automaticHyphenation.consecutiveLineLimit}|${automaticHyphenation.hyphenationZoneTwips}`);
33
+ const rules = attrs.lineBreakRules;
34
+ if (rules) parts.push(`line-break-rules:${rules.noLineBreaksBefore?.language}|${rules.noLineBreaksBefore?.characters}|${rules.noLineBreaksAfter?.language}|${rules.noLineBreaksAfter?.characters}|${rules.useLegacyEthiopicAmharicRules}`);
35
+ return parts;
36
+ };
37
+ const resolveAutomaticHyphenation = (attrs) => {
38
+ const automaticHyphenation = attrs?.automaticHyphenation;
39
+ if (automaticHyphenation?.enabled !== true || attrs?.suppressAutoHyphens === true) return { type: "disabled" };
40
+ return {
41
+ type: "enabled",
42
+ consecutiveLineLimit: automaticHyphenation.consecutiveLineLimit ?? 0,
43
+ hyphenationZoneTwips: automaticHyphenation.hyphenationZoneTwips ?? DEFAULT_HYPHENATION_ZONE_TWIPS
44
+ };
45
+ };
46
+ const resolveRunLocale = (run) => {
47
+ const language = run.language;
48
+ if (hasCjk(run.text)) return language?.eastAsia ?? language?.val;
49
+ if (run.rtl) return language?.bidi ?? language?.val;
50
+ return language?.val;
51
+ };
52
+ const languageMatches = (ruleLanguage, locale) => {
53
+ if (!ruleLanguage) return true;
54
+ if (!locale) return false;
55
+ const rule = ruleLanguage.toLowerCase();
56
+ const active = locale.toLowerCase();
57
+ return active === rule || active.startsWith(`${rule}-`) || rule.startsWith(`${active}-`);
58
+ };
59
+ //#endregion
60
+ export { lineBreakPolicyCacheParts, resolveEffectiveLineBreakPolicy };
@@ -1,13 +1,14 @@
1
1
  import { CJK_FALLBACK_FONT_FAMILY, isCjkFont } from "../../utils/fontResolver.js";
2
2
  import { getFontMetrics, measureRun, measureTextWidth } from "./measureProvider.js";
3
3
  import { countCompressibleSpaces } from "./textMeasurementPolicy.js";
4
- import { buildRunFontStyle, ptToPx } from "./measureHelpers.js";
4
+ import { buildRunFontStyle, ptToPx, twipsToPx } from "./measureHelpers.js";
5
5
  import "./lineBreakProvider.js";
6
6
  import { calculateTabWidth, pixelsToTwips } from "../../prosemirror/utils/tabCalculator.js";
7
7
  import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
8
8
  import { hasCjk } from "../../utils/scriptSegments.js";
9
9
  import { measuredLineAdvance } from "../lineFlow.js";
10
10
  import { isFloatingImageRun } from "../types.js";
11
+ import { resolveEffectiveLineBreakPolicy } from "./effectiveLineBreakPolicy.js";
11
12
  import { clampFloatingWrapMargins } from "./clampFloatingWrapMargins.js";
12
13
  import { getFloatingAvailableWidth, getFloatingMargins } from "./floatingZones.js";
13
14
  import { getListMarkerInlineWidth } from "./listMarkerWidth.js";
@@ -24,7 +25,7 @@ const DEFAULT_FONT_FAMILY = "Calibri";
24
25
  const DEFAULT_LINE_HEIGHT_MULTIPLIER = 1;
25
26
  const WIDTH_TOLERANCE = .5;
26
27
  const JUSTIFY_SHRINK_TOLERANCE_RATIO = .016;
27
- const JUSTIFY_SPACE_CONTRACTION_RATIO = .1;
28
+ const JUSTIFY_SPACE_CONTRACTION_RATIO = .075;
28
29
  const JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO = .195;
29
30
  const JUSTIFY_LIST_CONTINUATION_SPACE_CONTRACTION_RATIO = .23;
30
31
  const JUSTIFY_INSET_LIST_SHRINK_TOLERANCE_RATIO = .015;
@@ -51,6 +52,11 @@ function findMaxFittingLength(text, style, maxWidth, forceMin = false, policy) {
51
52
  }
52
53
  return forceMin && best === 0 ? boundaries.at(0) ?? 0 : best;
53
54
  }
55
+ function exceedsHyphenationZone(line, zoneTwips) {
56
+ if (line.width <= 0) return true;
57
+ const visibleWidth = Math.max(0, line.width - line.trailingWhitespaceWidth);
58
+ return Math.max(0, line.availableWidth - visibleWidth) > twipsToPx(zoneTwips) + WIDTH_TOLERANCE;
59
+ }
54
60
  /**
55
61
  * Extract FontStyle from a run that carries RunFormatting (text, tab, or
56
62
  * field). All three share the same formatting shape, so they measure the
@@ -60,31 +66,6 @@ function findMaxFittingLength(text, style, maxWidth, forceMin = false, policy) {
60
66
  function runToFontStyle(run) {
61
67
  return buildRunFontStyle(run, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE);
62
68
  }
63
- function languageMatches(ruleLanguage, locale) {
64
- if (!ruleLanguage) return true;
65
- if (!locale) return false;
66
- const rule = ruleLanguage.toLowerCase();
67
- const active = locale.toLowerCase();
68
- return active === rule || active.startsWith(`${rule}-`) || rule.startsWith(`${active}-`);
69
- }
70
- function lineBreakPolicy(block, run) {
71
- const language = run.language;
72
- let locale = language?.val;
73
- if (hasCjk(run.text)) locale = language?.eastAsia ?? language?.val;
74
- else if (run.rtl) locale = language?.bidi ?? language?.val;
75
- const rules = block.attrs?.lineBreakRules;
76
- const before = rules?.noLineBreaksBefore;
77
- const after = rules?.noLineBreaksAfter;
78
- return {
79
- ...locale ? { locale } : {},
80
- ...block.attrs?.kinsoku !== void 0 ? { kinsoku: block.attrs.kinsoku } : {},
81
- ...before && languageMatches(before.language, locale) ? { noLineBreaksBefore: before.characters } : {},
82
- ...after && languageMatches(after.language, locale) ? { noLineBreaksAfter: after.characters } : {},
83
- ...rules?.useLegacyEthiopicAmharicRules ? { useLegacyEthiopicAmharicRules: true } : {},
84
- ...block.attrs?.automaticHyphenation?.doNotHyphenateCaps !== void 0 ? { doNotHyphenateCaps: block.attrs.automaticHyphenation.doNotHyphenateCaps } : {},
85
- ...run.allCaps === true ? { renderedAllCaps: true } : {}
86
- };
87
- }
88
69
  /**
89
70
  * Line-HEIGHT style for a text run. Word derives a CJK line's height from an
90
71
  * East-Asian face, not the run's ascii font: real documents routinely put CJK
@@ -454,7 +435,10 @@ function computeTrailingGlueWidths(block) {
454
435
  }
455
436
  if (isSpaceOrTab(text.at(0))) continue;
456
437
  const style = runToFontStyle(nextRun);
457
- const firstBreak = findWordBreaks(text, lineBreakPolicy(block, nextRun)).at(0);
438
+ const firstBreak = findWordBreaks(text, resolveEffectiveLineBreakPolicy({
439
+ attrs: block.attrs,
440
+ run: nextRun
441
+ }).provider).at(0);
458
442
  widths[index] = measureTextWidth(firstBreak === void 0 ? text : trimTrailingSpacesAndTabs(text.slice(0, firstBreak)), style) + (firstBreak === void 0 ? widths[index + 1] ?? 0 : 0);
459
443
  }
460
444
  return widths;
@@ -466,7 +450,10 @@ function computeProtectedCrossRunGlueWidths(block) {
466
450
  if (!run || !isTextRun(run)) continue;
467
451
  const trailing = /(\S+)(\s*)$/u.exec(run.text ?? "");
468
452
  const token = trailing?.[1];
469
- const policy = lineBreakPolicy(block, run);
453
+ const policy = resolveEffectiveLineBreakPolicy({
454
+ attrs: block.attrs,
455
+ run
456
+ }).provider;
470
457
  if (!token || token.length !== 1 || !policy.locale?.toLocaleLowerCase().startsWith("cs")) continue;
471
458
  let separator = trailing[2] ?? "";
472
459
  let glueWidth = separator.length > 0 ? measureTextWidth(separator, runToFontStyle(run)) : 0;
@@ -512,7 +499,10 @@ function collectCrossRunWord({ block, startRunIndex, startChar }) {
512
499
  if (runIndex !== startRunIndex && isSpaceOrTab(remainder[0])) break;
513
500
  const remainingBudget = 256 - text.length;
514
501
  const boundedRemainder = remainder.slice(0, remainingBudget + 1);
515
- const firstBreak = findWordBreaks(boundedRemainder, lineBreakPolicy(block, run)).at(0);
502
+ const firstBreak = findWordBreaks(boundedRemainder, resolveEffectiveLineBreakPolicy({
503
+ attrs: block.attrs,
504
+ run
505
+ }).provider).at(0);
516
506
  const segmentText = trimTrailingSpacesAndTabs(firstBreak === void 0 ? boundedRemainder : boundedRemainder.slice(0, firstBreak));
517
507
  if (segmentText.length === 0) break;
518
508
  if (segmentText.length > remainingBudget) return;
@@ -534,7 +524,10 @@ function collectCrossRunWord({ block, startRunIndex, startChar }) {
534
524
  return {
535
525
  text,
536
526
  width,
537
- breaks: findHyphenationBreaks(text, lineBreakPolicy(block, firstRun)),
527
+ breaks: findHyphenationBreaks(text, resolveEffectiveLineBreakPolicy({
528
+ attrs: block.attrs,
529
+ run: firstRun
530
+ }).provider),
538
531
  segments
539
532
  };
540
533
  }
@@ -944,7 +937,11 @@ function measureParagraph(block, maxWidth, options) {
944
937
  const textRun = run;
945
938
  const text = textRun.text;
946
939
  const style = runToFontStyle(textRun);
947
- const breakPolicy = lineBreakPolicy(block, textRun);
940
+ const effectiveLineBreakPolicy = resolveEffectiveLineBreakPolicy({
941
+ attrs: block.attrs,
942
+ run: textRun
943
+ });
944
+ const breakPolicy = effectiveLineBreakPolicy.provider;
948
945
  const lineHeightStyle = cjkLineHeightStyle(textRun, style);
949
946
  updateMaxFont(lineHeightStyle);
950
947
  if (!text || text.length === 0) {
@@ -966,13 +963,12 @@ function measureParagraph(block, maxWidth, options) {
966
963
  const measuredWord = trimTrailingSpacesAndTabs(word);
967
964
  const wordWidth = measureTextWidth(measuredWord, style);
968
965
  const fullWordWidth = measureTextWidth(word, style);
969
- const hangingPunctuationWidth = trailingHangingPunctuationWidth(measuredWord, style, breakPolicy, block.attrs?.overflowPunctuation);
966
+ const hangingPunctuationWidth = trailingHangingPunctuationWidth(measuredWord, style, breakPolicy, effectiveLineBreakPolicy.hangingPunctuation);
970
967
  const isFirstLine = lines.length === 0;
971
968
  const regularSpaceWidth = compressibleSpaceWidth(measuredWord, style);
972
969
  const widthTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, isFirstLine ? firstLineJustifyFitStrategy : continuationJustifyFitStrategy, regularSpaceWidth) : WIDTH_TOLERANCE;
973
- const automaticHyphenation = block.attrs?.automaticHyphenation;
974
- const consecutiveLineLimit = automaticHyphenation?.consecutiveLineLimit ?? 0;
975
- const mayHyphenateLine = automaticHyphenation?.enabled === true && block.attrs?.suppressAutoHyphens !== true && (consecutiveLineLimit === 0 || consecutiveHyphenatedLines < consecutiveLineLimit);
970
+ const automaticHyphenation = effectiveLineBreakPolicy.automaticHyphenation;
971
+ const mayHyphenateLine = automaticHyphenation.type === "enabled" && (automaticHyphenation.consecutiveLineLimit === 0 || consecutiveHyphenatedLines < automaticHyphenation.consecutiveLineLimit) && exceedsHyphenationZone(currentLine, automaticHyphenation.hyphenationZoneTwips);
976
972
  const isRunTail = nextBreak === text.length;
977
973
  const crossRunWord = mayHyphenateLine && isRunTail && word.length > 0 && !isBreakChar(word.at(-1)) ? collectCrossRunWord({
978
974
  block,