@stll/folio-core 0.11.0 → 0.13.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.
- package/dist/ai-edits/apply.d.ts +2 -1
- package/dist/ai-edits/apply.js +693 -63
- package/dist/ai-edits/block-identity.d.ts +4 -0
- package/dist/ai-edits/block-identity.js +9 -0
- package/dist/ai-edits/blockRange.d.ts +28 -1
- package/dist/ai-edits/blockRange.js +68 -2
- package/dist/ai-edits/clean-text.js +2 -1
- package/dist/ai-edits/headless.d.ts +4 -4
- package/dist/ai-edits/headless.js +9 -9
- package/dist/ai-edits/index.d.ts +2 -2
- package/dist/ai-edits/index.js +2 -2
- package/dist/ai-edits/read.d.ts +7 -7
- package/dist/ai-edits/read.js +110 -3
- package/dist/ai-edits/snapshot.js +21 -3
- package/dist/ai-edits/table-cell-mutations.d.ts +48 -0
- package/dist/ai-edits/table-cell-mutations.js +238 -0
- package/dist/ai-edits/table-mutation-plan.d.ts +38 -0
- package/dist/ai-edits/table-mutation-plan.js +90 -0
- package/dist/ai-edits/table-row-column-mutations.d.ts +108 -0
- package/dist/ai-edits/table-row-column-mutations.js +300 -0
- package/dist/ai-edits/table-targets.d.ts +32 -0
- package/dist/ai-edits/table-targets.js +70 -0
- package/dist/ai-edits/types.d.ts +62 -1
- package/dist/ai-edits/word-diff.js +21 -0
- package/dist/compat/eigenpal.d.ts +3 -3
- package/dist/compat/eigenpal.js +3 -3
- package/dist/content-controls/mutateContentControls.js +1 -1
- package/dist/controller/activeEditorStory.d.ts +36 -0
- package/dist/controller/activeEditorStory.js +30 -0
- package/dist/controller/contentControlWidgetController.d.ts +55 -0
- package/dist/controller/contentControlWidgetController.js +141 -0
- package/dist/controller/fontReadiness.js +2 -0
- package/dist/controller/headerFooterEditorManager.js +2 -2
- package/dist/controller/hiddenEditorApi.js +2 -2
- package/dist/controller/hiddenEditorManager.d.ts +12 -1
- package/dist/controller/hiddenEditorManager.js +44 -12
- package/dist/controller/layoutPipeline.d.ts +2 -0
- package/dist/controller/layoutPipeline.js +17 -2
- package/dist/controller/noteEditorManager.d.ts +36 -0
- package/dist/controller/noteEditorManager.js +210 -0
- package/dist/document-operations.d.ts +43 -11
- package/dist/document-operations.js +186 -18
- package/dist/docx/blockContentParser.js +21 -7
- package/dist/docx/commentParser.js +6 -1
- package/dist/docx/commentRangeIntegrity.d.ts +6 -0
- package/dist/docx/commentRangeIntegrity.js +186 -0
- package/dist/docx/conformance.js +62 -1
- package/dist/docx/documentGrid.d.ts +8 -0
- package/dist/docx/documentGrid.js +8 -0
- package/dist/docx/drawingUtils.js +1 -1
- package/dist/docx/encryption/agileDecryption.js +12 -2
- package/dist/docx/encryption/compoundFile.js +6 -1
- package/dist/docx/encryption/encryptionInfo.d.ts +9 -1
- package/dist/docx/encryption/encryptionInfo.js +18 -2
- package/dist/docx/encryption/openEncryptedDocx.js +1 -1
- package/dist/docx/ensureParaIds.js +8 -7
- package/dist/docx/fieldParser.js +1 -1
- package/dist/docx/groupDrawingParser.js +10 -5
- package/dist/docx/hyperlinkParser.js +1 -1
- package/dist/docx/imageParser.js +10 -6
- package/dist/docx/metadataPrivacy.js +17 -3
- package/dist/docx/notePropertiesParser.js +1 -1
- package/dist/docx/numberingParser.js +1 -1
- package/dist/docx/paragraphParser.js +70 -61
- package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
- package/dist/docx/paragraphTextBoxEnrichment.js +67 -13
- package/dist/docx/paragraphTraversal.js +1 -1
- package/dist/docx/parser.js +3 -3
- package/dist/docx/rezip.js +20 -9
- package/dist/docx/runParser.js +17 -6
- package/dist/docx/sdtProperties.js +1 -1
- package/dist/docx/sectionParser.js +18 -5
- package/dist/docx/selectiveSave.js +5 -1
- package/dist/docx/selectiveXmlPatch.d.ts +22 -1
- package/dist/docx/selectiveXmlPatch.js +61 -1
- package/dist/docx/serializer/blockSdtSerializer.js +3 -2
- package/dist/docx/serializer/commentSerializer.js +5 -6
- package/dist/docx/serializer/documentSerializer.js +2 -2
- package/dist/docx/serializer/headerFooterSerializer.js +2 -2
- package/dist/docx/serializer/noteSerializer.js +2 -2
- package/dist/docx/serializer/paragraphSerializer.js +23 -15
- package/dist/docx/serializer/runSerializer.js +25 -21
- package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -1
- package/dist/docx/serializer/stylesSerializer.js +1 -1
- package/dist/docx/serializer/tableSerializer.d.ts +5 -4
- package/dist/docx/serializer/tableSerializer.js +26 -20
- package/dist/docx/serializer/xmlUtils.d.ts +13 -1
- package/dist/docx/serializer/xmlUtils.js +27 -1
- package/dist/docx/server/applyDocxXmlPatchProposal.d.ts +87 -0
- package/dist/docx/server/applyDocxXmlPatchProposal.js +204 -0
- package/dist/docx/server/boundedArchive.d.ts +14 -3
- package/dist/docx/server/boundedArchive.js +56 -11
- package/dist/docx/server/evaluateDocxXmlPatchProposal.d.ts +85 -0
- package/dist/docx/server/evaluateDocxXmlPatchProposal.js +361 -0
- package/dist/docx/server/extractDocxText.js +55 -8
- package/dist/docx/server/inspectDocxPackage.d.ts +70 -0
- package/dist/docx/server/inspectDocxPackage.js +217 -0
- package/dist/docx/server/validateDocxConformance.d.ts +41 -0
- package/dist/docx/server/validateDocxConformance.js +336 -0
- package/dist/docx/settingsParser.js +26 -3
- package/dist/docx/shapeParser.js +1 -1
- package/dist/docx/styleParser.js +4 -2
- package/dist/docx/tableParser.d.ts +8 -1
- package/dist/docx/tableParser.js +31 -10
- package/dist/docx/textBoxParser.d.ts +3 -3
- package/dist/docx/textBoxParser.js +16 -12
- package/dist/docx/unzip.js +1 -0
- package/dist/docx/vmlImageParser.js +11 -3
- package/dist/docx/xmlParser.js +11 -1
- package/dist/docx/xmlSafety.d.ts +5 -0
- package/dist/docx/xmlSafety.js +9 -0
- package/dist/fields/resolveFieldValues.js +1 -3
- package/dist/fonts/embeddedFonts.d.ts +33 -5
- package/dist/fonts/embeddedFonts.js +37 -8
- package/dist/i18n/messages/catalogs.gen.d.ts +17 -0
- package/dist/i18n/messages/catalogs.gen.js +17 -0
- package/dist/i18n/messages/messages.gen.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/footnoteLayout.js +47 -40
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.js +3 -0
- package/dist/layout-bridge/convert/templatePreviewFlow.js +3 -3
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -2
- package/dist/layout-bridge/convert/toFlowBlocks.js +151 -20
- package/dist/layout-bridge/dom/noteStoryDom.d.ts +6 -0
- package/dist/layout-bridge/dom/noteStoryDom.js +17 -0
- package/dist/layout-bridge/engine/clickToPosition.js +2 -2
- package/dist/layout-engine/index.d.ts +2 -2
- package/dist/layout-engine/index.js +58 -30
- package/dist/layout-engine/justificationCompatibility.d.ts +6 -0
- package/dist/layout-engine/justificationCompatibility.js +8 -0
- package/dist/layout-engine/keep-together.js +11 -1
- package/dist/layout-engine/measure/cache.js +23 -5
- package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +8 -2
- package/dist/layout-engine/measure/lineBreakProvider.d.ts +2 -2
- package/dist/layout-engine/measure/lineBreakProvider.js +56 -37
- package/dist/layout-engine/measure/measureBlocks.js +15 -4
- package/dist/layout-engine/measure/measureContainer.js +1 -1
- package/dist/layout-engine/measure/measureHelpers.d.ts +11 -2
- package/dist/layout-engine/measure/measureHelpers.js +15 -4
- package/dist/layout-engine/measure/measureParagraph.js +85 -25
- package/dist/layout-engine/measure/measureProvider.js +4 -3
- package/dist/layout-engine/measure/tableCellFlow.js +5 -2
- package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +9 -7
- package/dist/layout-engine/measure/textBoxParagraphLayout.js +22 -6
- package/dist/layout-engine/measure/textMeasurementPolicy.d.ts +3 -1
- package/dist/layout-engine/measure/textMeasurementPolicy.js +7 -1
- package/dist/layout-engine/paginator.js +5 -1
- package/dist/layout-engine/paragraphSpacing.d.ts +28 -4
- package/dist/layout-engine/paragraphSpacing.js +107 -13
- package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
- package/dist/layout-engine/types.d.ts +48 -15
- package/dist/layout-painter/registry/modules/textBox.js +5 -1
- package/dist/layout-painter/renderImage.d.ts +12 -1
- package/dist/layout-painter/renderImage.js +21 -4
- package/dist/layout-painter/renderPage.d.ts +5 -2
- package/dist/layout-painter/renderPage.js +59 -14
- package/dist/layout-painter/renderParagraph.js +55 -15
- package/dist/layout-painter/renderTable.d.ts +5 -1
- package/dist/layout-painter/renderTable.js +7 -3
- package/dist/layout-painter/renderTextBox.d.ts +2 -1
- package/dist/layout-painter/renderTextBox.js +25 -14
- package/dist/layout-painter/renderWatermark.d.ts +1 -1
- package/dist/layout-painter/renderWatermark.js +2 -1
- package/dist/managers/DocumentLoaderManager.js +1 -1
- package/dist/markdown/fromMarkdown.js +1 -1
- package/dist/model.js +1 -1
- package/dist/prosemirror/attrs/index.d.ts +4 -2
- package/dist/prosemirror/attrs/index.js +175 -18
- package/dist/prosemirror/commands/comments.d.ts +79 -4
- package/dist/prosemirror/commands/comments.js +622 -13
- package/dist/prosemirror/commands/contentControls.js +1 -1
- package/dist/prosemirror/commands/hyperlink.js +15 -1
- package/dist/prosemirror/commands/image.js +7 -1
- package/dist/prosemirror/commands/index.d.ts +2 -2
- package/dist/prosemirror/commands/index.js +2 -2
- package/dist/prosemirror/commands/pageBreak.js +1 -1
- package/dist/prosemirror/commands/propertyChangeScope.d.ts +2 -2
- package/dist/prosemirror/commands/propertyChangeScope.js +3 -2
- package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +9 -0
- package/dist/prosemirror/commands/tableCellMergeResolution.js +181 -0
- package/dist/prosemirror/commentIdAllocator.js +13 -3
- package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +404 -62
- package/dist/prosemirror/conversion/index.js +1 -1
- package/dist/prosemirror/conversion/sdtAttrs.d.ts +9 -0
- package/dist/prosemirror/conversion/sdtAttrs.js +49 -0
- package/dist/prosemirror/conversion/toProseDoc.d.ts +2 -1
- package/dist/prosemirror/conversion/toProseDoc.js +364 -103
- package/dist/prosemirror/extensions/ExtensionManager.js +1 -1
- package/dist/prosemirror/extensions/StarterKit.js +6 -1
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +6 -2
- package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +1 -0
- package/dist/prosemirror/extensions/features/ImagePasteExtension.js +10 -2
- package/dist/prosemirror/extensions/features/ListExtension.js +1 -1
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +10 -1
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +93 -2
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +2 -1
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +6 -2
- package/dist/prosemirror/extensions/features/pasteCleanup.js +15 -2
- package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +1 -1
- package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +30 -4
- package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.d.ts +9 -1
- package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +70 -11
- package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
- package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -0
- package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +22 -0
- package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
- package/dist/prosemirror/extensions/nodes/TableExtension.js +14 -6
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +13 -0
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +41 -0
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +5 -2
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +4 -1
- package/dist/prosemirror/index.d.ts +1 -1
- package/dist/prosemirror/index.js +1 -1
- package/dist/prosemirror/plugins/contentControlWidgets.d.ts +36 -10
- package/dist/prosemirror/plugins/contentControlWidgets.js +128 -94
- package/dist/prosemirror/plugins/revisionIds.d.ts +23 -0
- package/dist/prosemirror/plugins/revisionIds.js +57 -0
- package/dist/prosemirror/plugins/selectionTracker.js +1 -0
- package/dist/prosemirror/plugins/suggestionMode.js +5 -4
- package/dist/prosemirror/runFormattingMarkNames.d.ts +14 -0
- package/dist/prosemirror/runFormattingMarkNames.js +40 -0
- package/dist/prosemirror/schema/index.d.ts +2 -2
- package/dist/prosemirror/schema/marks.d.ts +30 -3
- package/dist/prosemirror/schema/marks.js +15 -0
- package/dist/prosemirror/schema/nodes.d.ts +111 -6
- package/dist/prosemirror/selectionState.js +1 -0
- package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
- package/dist/prosemirror/tableCellMergeRevision.d.ts +9 -0
- package/dist/prosemirror/tableCellMergeRevision.js +22 -0
- package/dist/prosemirror/textBoxAnchorAttrs.d.ts +9 -0
- package/dist/prosemirror/textBoxAnchorAttrs.js +33 -0
- package/dist/prosemirror/utils/extractTrackedChanges.d.ts +1 -1
- package/dist/prosemirror/utils/extractTrackedChanges.js +62 -14
- package/dist/prosemirror/validation.js +9 -1
- package/dist/render-dom/RemoteSelectionOverlay.js +2 -2
- package/dist/server.d.ts +7 -3
- package/dist/server.js +7 -3
- package/dist/utils/clipboard.d.ts +10 -1
- package/dist/utils/clipboard.js +33 -6
- package/dist/utils/colorResolver.d.ts +8 -1
- package/dist/utils/colorResolver.js +35 -5
- package/dist/utils/fontFamilyMerge.js +9 -11
- package/dist/utils/fontLoader.js +3 -2
- package/dist/utils/fontResolver.d.ts +2 -1
- package/dist/utils/fontResolver.js +31 -7
- package/dist/utils/fontWeights.d.ts +1 -1
- package/dist/utils/fontWeights.js +1 -1
- package/dist/utils/hexId.d.ts +11 -1
- package/dist/utils/hexId.js +11 -1
- package/dist/utils/paragraphFormattingMerge.js +1 -0
- package/dist/utils/sanitizeImageSrc.d.ts +21 -0
- package/dist/utils/sanitizeImageSrc.js +30 -0
- package/dist/utils/stripXmlDeclarations.d.ts +11 -0
- package/dist/utils/stripXmlDeclarations.js +35 -0
- package/dist/version-comparison.d.ts +22 -2
- package/dist/version-comparison.js +41 -12
- package/dist/watermark/index.d.ts +11 -1
- package/dist/watermark/index.js +22 -1
- package/package.json +3 -2
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
import { clearFontResolvedCache } from "./measureHelpers.js";
|
|
1
2
|
import { getLineBreakProviderGeneration } from "./lineBreakProvider.js";
|
|
2
3
|
import { lineBreakPolicyCacheParts } from "./effectiveLineBreakPolicy.js";
|
|
3
4
|
//#region src/layout-engine/measure/cache.ts
|
|
4
5
|
/**
|
|
6
|
+
* Default max entries for text width cache
|
|
7
|
+
* Large documents (30+ pages) can generate 20,000+ unique text measurements.
|
|
8
|
+
* A generous default avoids cache thrashing on big docs.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_TEXT_CACHE_SIZE = 2e4;
|
|
11
|
+
const TEXT_CACHE_REFRESH_AFTER_HITS = 16;
|
|
12
|
+
/**
|
|
5
13
|
* Current max size for text width cache
|
|
6
14
|
*/
|
|
7
|
-
let textCacheMaxSize =
|
|
15
|
+
let textCacheMaxSize = DEFAULT_TEXT_CACHE_SIZE;
|
|
8
16
|
/**
|
|
9
17
|
* LRU cache for text width measurements
|
|
10
18
|
* Key format: "text|font|letterSpacing"
|
|
@@ -34,8 +42,12 @@ function getCachedTextWidth(text, font, letterSpacing = 0) {
|
|
|
34
42
|
const key = makeTextKey(text, font, letterSpacing);
|
|
35
43
|
const entry = textWidthCache.get(key);
|
|
36
44
|
if (entry !== void 0) {
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
entry.hitsSinceRefresh += 1;
|
|
46
|
+
if (entry.hitsSinceRefresh >= TEXT_CACHE_REFRESH_AFTER_HITS) {
|
|
47
|
+
entry.hitsSinceRefresh = 0;
|
|
48
|
+
textWidthCache.delete(key);
|
|
49
|
+
textWidthCache.set(key, entry);
|
|
50
|
+
}
|
|
39
51
|
return entry.width;
|
|
40
52
|
}
|
|
41
53
|
}
|
|
@@ -44,8 +56,11 @@ function getCachedTextWidth(text, font, letterSpacing = 0) {
|
|
|
44
56
|
*/
|
|
45
57
|
function setCachedTextWidth(text, font, letterSpacing, width) {
|
|
46
58
|
const key = makeTextKey(text, font, letterSpacing);
|
|
47
|
-
textWidthCache.set(key, {
|
|
48
|
-
|
|
59
|
+
textWidthCache.set(key, {
|
|
60
|
+
hitsSinceRefresh: 0,
|
|
61
|
+
width
|
|
62
|
+
});
|
|
63
|
+
if (textWidthCache.size > textCacheMaxSize) evictTextEntries();
|
|
49
64
|
}
|
|
50
65
|
/**
|
|
51
66
|
* Clear the text width cache
|
|
@@ -169,6 +184,8 @@ function hashParagraphBlock(block) {
|
|
|
169
184
|
if (attrs.defaultFontFamily != null) parts.push(`dff:${attrs.defaultFontFamily}`);
|
|
170
185
|
if (attrs.suppressEmptyParagraphHeight) parts.push("sup");
|
|
171
186
|
if (attrs.reserveEmptyOutlineHeight) parts.push("outline-empty-reserve");
|
|
187
|
+
if (attrs.documentGridLinePitch !== void 0) parts.push(`documentGrid:${attrs.documentGridLinePitch}|${attrs.snapToGrid !== false}`);
|
|
188
|
+
if (attrs.justificationCompatibility) parts.push(`justify-compat:${attrs.justificationCompatibility.type}`);
|
|
172
189
|
parts.push(...lineBreakPolicyCacheParts(attrs));
|
|
173
190
|
const borders = attrs.borders;
|
|
174
191
|
if (borders) {
|
|
@@ -239,6 +256,7 @@ function clearAllCaches() {
|
|
|
239
256
|
clearTextWidthCache();
|
|
240
257
|
clearFontMetricsCache();
|
|
241
258
|
clearParagraphMeasureCache();
|
|
259
|
+
clearFontResolvedCache();
|
|
242
260
|
}
|
|
243
261
|
/**
|
|
244
262
|
* Get total size of all caches
|
|
@@ -12,8 +12,8 @@ const resolveEffectiveLineBreakPolicy = ({ attrs, run }) => {
|
|
|
12
12
|
provider: {
|
|
13
13
|
...locale ? { locale } : {},
|
|
14
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 } : {},
|
|
15
|
+
...before && languageMatches(before.language, locale) ? { noLineBreaksBefore: toCodePointSet(before.characters) } : {},
|
|
16
|
+
...after && languageMatches(after.language, locale) ? { noLineBreaksAfter: toCodePointSet(after.characters) } : {},
|
|
17
17
|
...rules?.useLegacyEthiopicAmharicRules ? { useLegacyEthiopicAmharicRules: true } : {},
|
|
18
18
|
...automaticHyphenation?.doNotHyphenateCaps !== void 0 ? { doNotHyphenateCaps: automaticHyphenation.doNotHyphenateCaps } : {},
|
|
19
19
|
...run.allCaps === true ? { renderedAllCaps: true } : {}
|
|
@@ -56,5 +56,11 @@ const languageMatches = (ruleLanguage, locale) => {
|
|
|
56
56
|
const active = locale.toLowerCase();
|
|
57
57
|
return active === rule || active.startsWith(`${rule}-`) || rule.startsWith(`${active}-`);
|
|
58
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* Split an already length-capped kinsoku character list (settingsParser.ts)
|
|
61
|
+
* into a code-point Set so line-edge membership checks are O(1) instead of
|
|
62
|
+
* an O(n) `String.includes` scan per character measured.
|
|
63
|
+
*/
|
|
64
|
+
const toCodePointSet = (characters) => new Set(Array.from(characters));
|
|
59
65
|
//#endregion
|
|
60
66
|
export { lineBreakPolicyCacheParts, resolveEffectiveLineBreakPolicy };
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
type LineBreakPolicy = {
|
|
11
11
|
/** BCP-47 language tag resolved from `w:lang` for this run. */locale?: string; /** Apply East Asian first/last-character restrictions (`w:kinsoku`). */
|
|
12
12
|
kinsoku?: boolean; /** Document replacement list: characters that may not begin a line. */
|
|
13
|
-
noLineBreaksBefore?: string
|
|
14
|
-
noLineBreaksAfter?: string
|
|
13
|
+
noLineBreaksBefore?: ReadonlySet<string>; /** Document replacement list: characters that may not end a line. */
|
|
14
|
+
noLineBreaksAfter?: ReadonlySet<string>; /** Use the legacy Ethiopic/Amharic compatibility behavior. */
|
|
15
15
|
useLegacyEthiopicAmharicRules?: boolean; /** Keep words made entirely of capital letters unhyphenated. */
|
|
16
16
|
doNotHyphenateCaps?: boolean; /** The run renders with the DOCX all-caps transform. */
|
|
17
17
|
renderedAllCaps?: boolean;
|
|
@@ -27,6 +27,13 @@ const segmenterLocale = (locale) => {
|
|
|
27
27
|
};
|
|
28
28
|
const DICTIONARY_BREAK_SCRIPT = /[\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Myanmar}]/u;
|
|
29
29
|
const CJK_SCRIPT = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
|
|
30
|
+
const WORD_HANGING_PUNCTUATION_LANGUAGES = /* @__PURE__ */ new Set([
|
|
31
|
+
"ii",
|
|
32
|
+
"ja",
|
|
33
|
+
"ko",
|
|
34
|
+
"zh"
|
|
35
|
+
]);
|
|
36
|
+
const SIMPLE_BREAK_TEXT = /^[\p{Script=Latin}\p{Script=Cyrillic}\p{Script=Greek}\p{Number}\p{Punctuation}\p{White_Space}]*$/u;
|
|
30
37
|
const BREAK_AFTER_CHARACTER = /* @__PURE__ */ new Set([
|
|
31
38
|
"-",
|
|
32
39
|
"֊",
|
|
@@ -42,13 +49,10 @@ const BREAK_AFTER_CHARACTER = /* @__PURE__ */ new Set([
|
|
|
42
49
|
"",
|
|
43
50
|
""
|
|
44
51
|
]);
|
|
45
|
-
const
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"u",
|
|
50
|
-
"v",
|
|
51
|
-
"z"
|
|
52
|
+
const NONBREAKING_SPACES = /* @__PURE__ */ new Set([
|
|
53
|
+
"\xA0",
|
|
54
|
+
" ",
|
|
55
|
+
" "
|
|
52
56
|
]);
|
|
53
57
|
const DEFAULT_PROHIBITED_LINE_START = /* @__PURE__ */ new Set([
|
|
54
58
|
"!",
|
|
@@ -124,38 +128,14 @@ const previousCodePoint = (text, index) => {
|
|
|
124
128
|
if (trailing >= 56320 && trailing <= 57343 && index >= 2) return text.slice(index - 2, index);
|
|
125
129
|
return text[index - 1];
|
|
126
130
|
};
|
|
127
|
-
const isProhibitedLineStart = (character, policy) => character !== void 0 && policy?.kinsoku !== false && (policy?.noLineBreaksBefore !== void 0 ? policy.noLineBreaksBefore.
|
|
128
|
-
const isProhibitedLineEnd = (character, policy) => character !== void 0 && policy?.kinsoku !== false && (policy?.noLineBreaksAfter !== void 0 ? policy.noLineBreaksAfter.
|
|
129
|
-
const isCjkLineBreakParticipant = (character, policy) => character !== void 0 && (CJK_SCRIPT.test(character) || DEFAULT_PROHIBITED_LINE_START.has(character) || DEFAULT_PROHIBITED_LINE_END.has(character) || policy?.noLineBreaksBefore?.
|
|
131
|
+
const isProhibitedLineStart = (character, policy) => character !== void 0 && policy?.kinsoku !== false && (policy?.noLineBreaksBefore !== void 0 ? policy.noLineBreaksBefore.has(character) : DEFAULT_PROHIBITED_LINE_START.has(character));
|
|
132
|
+
const isProhibitedLineEnd = (character, policy) => character !== void 0 && policy?.kinsoku !== false && (policy?.noLineBreaksAfter !== void 0 ? policy.noLineBreaksAfter.has(character) : DEFAULT_PROHIBITED_LINE_END.has(character));
|
|
133
|
+
const isCjkLineBreakParticipant = (character, policy) => character !== void 0 && (CJK_SCRIPT.test(character) || policy?.kinsoku !== false && (DEFAULT_PROHIBITED_LINE_START.has(character) || DEFAULT_PROHIBITED_LINE_END.has(character) || policy?.noLineBreaksBefore?.has(character) === true || policy?.noLineBreaksAfter?.has(character) === true));
|
|
130
134
|
const isLegacyEthiopicBreakCharacter = (character, policy) => {
|
|
131
135
|
if (!policy?.useLegacyEthiopicAmharicRules || character === void 0) return false;
|
|
132
136
|
const codePoint = character.codePointAt(0);
|
|
133
137
|
return codePoint !== void 0 && codePoint >= 4961 && codePoint <= 4968;
|
|
134
138
|
};
|
|
135
|
-
const czechProtectedBreaks = (text, policy) => {
|
|
136
|
-
const breaks = /* @__PURE__ */ new Set();
|
|
137
|
-
if (segmenterLocale(policy?.locale) !== "cs") return breaks;
|
|
138
|
-
let tokenStart = 0;
|
|
139
|
-
let index = 0;
|
|
140
|
-
while (index < text.length) {
|
|
141
|
-
const character = firstCodePoint(text, index);
|
|
142
|
-
if (character === void 0) break;
|
|
143
|
-
if (!/\s/u.test(character)) {
|
|
144
|
-
index += character.length;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
const token = text.slice(tokenStart, index);
|
|
148
|
-
const protectsNextWord = token.length === 1 && CZECH_ONE_LETTER_PREPOSITIONS.has(token.toLocaleLowerCase("cs"));
|
|
149
|
-
while (index < text.length) {
|
|
150
|
-
const whitespace = firstCodePoint(text, index);
|
|
151
|
-
if (whitespace === void 0 || !/\s/u.test(whitespace)) break;
|
|
152
|
-
index += whitespace.length;
|
|
153
|
-
if (protectsNextWord) breaks.add(index);
|
|
154
|
-
}
|
|
155
|
-
tokenStart = index;
|
|
156
|
-
}
|
|
157
|
-
return breaks;
|
|
158
|
-
};
|
|
159
139
|
const allowsBreak = (text, index, policy) => {
|
|
160
140
|
const previous = previousCodePoint(text, index);
|
|
161
141
|
const next = firstCodePoint(text, index);
|
|
@@ -165,7 +145,20 @@ const pushBreak = (breaks, text, index, policy) => {
|
|
|
165
145
|
if (index <= 0 || index > text.length || breaks.at(-1) === index) return;
|
|
166
146
|
if (allowsBreak(text, index, policy)) breaks.push(index);
|
|
167
147
|
};
|
|
148
|
+
const findSimpleGraphemeBreaks = (text) => {
|
|
149
|
+
if (!SIMPLE_BREAK_TEXT.test(text)) return;
|
|
150
|
+
const breaks = [];
|
|
151
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
152
|
+
const codeUnit = text.charCodeAt(index);
|
|
153
|
+
if (codeUnit >= 55296 && codeUnit <= 57343) return;
|
|
154
|
+
if (codeUnit === 13 && text.charCodeAt(index + 1) === 10) index += 1;
|
|
155
|
+
breaks.push(index + 1);
|
|
156
|
+
}
|
|
157
|
+
return breaks;
|
|
158
|
+
};
|
|
168
159
|
const findUnicodeGraphemeBreaks = (text, policy) => {
|
|
160
|
+
const simpleBreaks = findSimpleGraphemeBreaks(text);
|
|
161
|
+
if (simpleBreaks !== void 0) return simpleBreaks;
|
|
169
162
|
const breaks = [];
|
|
170
163
|
for (const segment of getSegmenter(graphemeSegmenters, "grapheme", policy?.locale).segment(text)) {
|
|
171
164
|
const end = segment.index + segment.segment.length;
|
|
@@ -173,13 +166,38 @@ const findUnicodeGraphemeBreaks = (text, policy) => {
|
|
|
173
166
|
}
|
|
174
167
|
return breaks;
|
|
175
168
|
};
|
|
169
|
+
const isSimpleWhitespace = (character, codePoint) => codePoint === 32 || codePoint >= 9 && codePoint <= 13 || codePoint > 127 && /\s/u.test(character);
|
|
170
|
+
/**
|
|
171
|
+
* Find the complete set of Unicode line-break opportunities for simple text
|
|
172
|
+
* without invoking `Intl.Segmenter`.
|
|
173
|
+
*
|
|
174
|
+
* Latin, Cyrillic, and Greek letters plus ordinary numbers, punctuation, and
|
|
175
|
+
* spacing are single grapheme clusters, except CRLF, which Unicode treats as
|
|
176
|
+
* one cluster. Returning `undefined` for combining marks, astral characters,
|
|
177
|
+
* dictionary-segmented scripts, CJK, and other complex input keeps the full
|
|
178
|
+
* Unicode provider responsible for those cases.
|
|
179
|
+
*/
|
|
180
|
+
const findSimpleBreaks = (text, policy) => {
|
|
181
|
+
if (!SIMPLE_BREAK_TEXT.test(text)) return;
|
|
182
|
+
const breaks = [];
|
|
183
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
184
|
+
const codePoint = text.charCodeAt(index);
|
|
185
|
+
if (codePoint >= 55296 && codePoint <= 57343) return;
|
|
186
|
+
if (codePoint === 13 && text.charCodeAt(index + 1) === 10) continue;
|
|
187
|
+
const character = text[index];
|
|
188
|
+
if (isSimpleWhitespace(character, codePoint) && !NONBREAKING_SPACES.has(character) || BREAK_AFTER_CHARACTER.has(character) || isLegacyEthiopicBreakCharacter(character, policy)) pushBreak(breaks, text, index + 1, policy);
|
|
189
|
+
}
|
|
190
|
+
return breaks;
|
|
191
|
+
};
|
|
176
192
|
const findUnicodeBreaks = (text, policy) => {
|
|
177
193
|
if (text.length === 0) return [];
|
|
194
|
+
const simpleBreaks = findSimpleBreaks(text, policy);
|
|
195
|
+
if (simpleBreaks !== void 0) return simpleBreaks;
|
|
178
196
|
const breaks = [];
|
|
179
197
|
const graphemeBreaks = findUnicodeGraphemeBreaks(text, policy);
|
|
180
198
|
for (const index of graphemeBreaks) {
|
|
181
199
|
const previous = previousCodePoint(text, index);
|
|
182
|
-
if (previous !== void 0 && (/\s/u.test(previous) || BREAK_AFTER_CHARACTER.has(previous) || isLegacyEthiopicBreakCharacter(previous, policy))) pushBreak(breaks, text, index, policy);
|
|
200
|
+
if (previous !== void 0 && (/\s/u.test(previous) && !NONBREAKING_SPACES.has(previous) || BREAK_AFTER_CHARACTER.has(previous) || isLegacyEthiopicBreakCharacter(previous, policy))) pushBreak(breaks, text, index, policy);
|
|
183
201
|
}
|
|
184
202
|
const wordSegments = [...getSegmenter(wordSegmenters, "word", policy?.locale).segment(text)];
|
|
185
203
|
for (const [segmentIndex, segment] of wordSegments.entries()) {
|
|
@@ -191,8 +209,7 @@ const findUnicodeBreaks = (text, policy) => {
|
|
|
191
209
|
const previous = previousCodePoint(text, index);
|
|
192
210
|
if (isCjkLineBreakParticipant(previous, policy)) pushBreak(breaks, text, index, policy);
|
|
193
211
|
}
|
|
194
|
-
|
|
195
|
-
return [...new Set(breaks)].filter((index) => !protectedBreaks.has(index)).sort((left, right) => left - right);
|
|
212
|
+
return [...new Set(breaks)].sort((left, right) => left - right);
|
|
196
213
|
};
|
|
197
214
|
const hyphenatorFor = (locale) => {
|
|
198
215
|
const normalized = locale?.trim().replaceAll("_", "-").toLowerCase();
|
|
@@ -226,6 +243,8 @@ const findPatternHyphenationBreaks = (text, policy) => {
|
|
|
226
243
|
return sourceOffset === text.length ? breaks : [];
|
|
227
244
|
};
|
|
228
245
|
const isDefaultHangingPunctuation = (text, policy) => {
|
|
246
|
+
const language = segmenterLocale(policy?.locale);
|
|
247
|
+
if (!language || !WORD_HANGING_PUNCTUATION_LANGUAGES.has(language)) return false;
|
|
229
248
|
const characters = [...text];
|
|
230
249
|
return characters.length > 0 && characters.every((character) => isProhibitedLineStart(character, policy));
|
|
231
250
|
};
|
|
@@ -2,7 +2,7 @@ import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand, floatingTextBoxWr
|
|
|
2
2
|
import { findClearLineY, measureParagraph } from "./measureParagraph.js";
|
|
3
3
|
import { getCachedParagraphMeasure, setCachedParagraphMeasure } from "./cache.js";
|
|
4
4
|
import { createTableCellFlowState, finishTableCellFlow, placeTableCellBlock } from "./tableCellFlow.js";
|
|
5
|
-
import {
|
|
5
|
+
import { layoutTextBoxContent } from "./textBoxParagraphLayout.js";
|
|
6
6
|
import { getTextBoxGroupId } from "../textBoxGroup.js";
|
|
7
7
|
import { isParagraphFrameTextBox } from "../paragraphFrame.js";
|
|
8
8
|
import { recordMeasureBlock, recordMeasureBlockError } from "../layoutInstrumentation.js";
|
|
@@ -17,6 +17,14 @@ import { buildTableCellGrid, getFirstAvailableColumn, getTableCellVerticalBorder
|
|
|
17
17
|
*/
|
|
18
18
|
const NO_WRAP_MEASURE_WIDTH = 1e6;
|
|
19
19
|
/**
|
|
20
|
+
* Sanity cap on the table column count derived from summed cell colSpans
|
|
21
|
+
* when a table has no `tblGrid`/explicit column widths. Mirrors the
|
|
22
|
+
* `MAX_TABLE_COLUMNS` clamp applied to `w:gridSpan` at parse time
|
|
23
|
+
* (`docx/tableParser.ts`); enforced again here since a hostile colSpan sum
|
|
24
|
+
* would otherwise size the fallback column array and cell grid unbounded.
|
|
25
|
+
*/
|
|
26
|
+
const MAX_TABLE_COLUMNS = 63;
|
|
27
|
+
/**
|
|
20
28
|
* Check if an image run is a *text-wrapping* floating image — it
|
|
21
29
|
* occupies an exclusion zone the body text should flow around.
|
|
22
30
|
*
|
|
@@ -59,7 +67,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
|
|
|
59
67
|
let columnWidths = tableBlock.columnWidths ?? [];
|
|
60
68
|
const explicitWidthPx = resolveTableWidthPx(tableBlock.width, tableBlock.widthType, contentWidth);
|
|
61
69
|
if (columnWidths.length === 0 && tableBlock.rows.length > 0) {
|
|
62
|
-
const colCount = tableBlock.rows[0].cells.reduce((sum, cell) => sum + (cell.colSpan ?? 1), 0);
|
|
70
|
+
const colCount = Math.min(tableBlock.rows[0].cells.reduce((sum, cell) => sum + (cell.colSpan ?? 1), 0), MAX_TABLE_COLUMNS);
|
|
63
71
|
const equalWidth = (explicitWidthPx ?? contentWidth) / Math.max(1, colCount);
|
|
64
72
|
columnWidths = Array.from({ length: colCount }, () => equalWidth);
|
|
65
73
|
} else if (columnWidths.length > 0 && explicitWidthPx) {
|
|
@@ -439,8 +447,11 @@ function measureBlock(block, contentWidth, floatingZones, cumulativeY, fieldValu
|
|
|
439
447
|
function measureTextBoxBlock(tb, fieldValues) {
|
|
440
448
|
const margins = tb.margins ?? DEFAULT_TEXTBOX_MARGINS;
|
|
441
449
|
const innerWidth = tb.width - margins.left - margins.right;
|
|
442
|
-
const innerMeasures = tb.content.map((
|
|
443
|
-
|
|
450
|
+
const innerMeasures = tb.content.map((block) => {
|
|
451
|
+
if (block.kind === "table") return measureTableBlock(block, innerWidth, fieldValues);
|
|
452
|
+
return measureParagraph(block, innerWidth, fieldValues ? { fieldValues } : void 0);
|
|
453
|
+
});
|
|
454
|
+
const contentBoxHeight = layoutTextBoxContent(tb.content, innerMeasures).totalHeight + margins.top + margins.bottom;
|
|
444
455
|
const totalHeight = tb.autoFit === "shape" ? Math.max(tb.height ?? 0, contentBoxHeight) : tb.height ?? contentBoxHeight;
|
|
445
456
|
return {
|
|
446
457
|
kind: "textBox",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { setMeasureProvider } from "./measureProvider.js";
|
|
2
1
|
import { getFontKerningMode } from "./textMeasurementPolicy.js";
|
|
2
|
+
import { setMeasureProvider } from "./measureProvider.js";
|
|
3
3
|
import { buildFontString, getResolvedData, ptToPx } from "./measureHelpers.js";
|
|
4
4
|
import { hasCjk, isCjkCodePoint, segmentByScript } from "../../utils/scriptSegments.js";
|
|
5
5
|
import { getCachedFontMetrics, getCachedTextWidth, getTextWidthCacheGeneration, setCachedFontMetrics, setCachedTextWidth } from "./cache.js";
|
|
@@ -22,6 +22,15 @@ type ResolvedFontCache = {
|
|
|
22
22
|
* Get the resolved font data for a font family, with caching.
|
|
23
23
|
*/
|
|
24
24
|
declare function getResolvedData(fontFamily: string): ResolvedFontCache;
|
|
25
|
+
/**
|
|
26
|
+
* Clear the resolved-font-family cache. `resolveFontFamily`'s output for a
|
|
27
|
+
* given key can change even when the raw font names don't — e.g. when the
|
|
28
|
+
* active document's embedded-font family map changes (see
|
|
29
|
+
* `utils/fontResolver.ts`'s `setEmbeddedFontFamilyMap`) — so this must be
|
|
30
|
+
* cleared whenever that happens, not just on font-name changes. Folded into
|
|
31
|
+
* `clearAllCaches` (`./cache.ts`) so callers get it for free.
|
|
32
|
+
*/
|
|
33
|
+
declare function clearFontResolvedCache(): void;
|
|
25
34
|
/**
|
|
26
35
|
* Build a CSS font string from styling properties
|
|
27
36
|
*
|
|
@@ -34,7 +43,7 @@ declare function getResolvedData(fontFamily: string): ResolvedFontCache;
|
|
|
34
43
|
*
|
|
35
44
|
* @example
|
|
36
45
|
* buildFontString({ fontFamily: "Arial", fontSize: 12, bold: true })
|
|
37
|
-
* // Returns: "
|
|
46
|
+
* // Returns: "700 16px Arial, Arimo, Helvetica, sans-serif" (12pt = 16px)
|
|
38
47
|
*/
|
|
39
48
|
declare function buildFontString(style: FontStyle): string;
|
|
40
49
|
/**
|
|
@@ -79,4 +88,4 @@ declare function halfPtToPx(halfPt: number): number;
|
|
|
79
88
|
*/
|
|
80
89
|
declare function pxToHalfPt(px: number): number;
|
|
81
90
|
//#endregion
|
|
82
|
-
export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, buildFontString, buildRunFontStyle, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
|
|
91
|
+
export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, buildFontString, buildRunFontStyle, clearFontResolvedCache, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveFontFamily } from "../../utils/fontResolver.js";
|
|
2
|
-
import "../../utils/fontWeights.js";
|
|
3
2
|
import { FONT_KERNING_MODE, getRunFontKerningMode } from "./textMeasurementPolicy.js";
|
|
3
|
+
import "../../utils/fontWeights.js";
|
|
4
4
|
//#region src/layout-engine/measure/measureHelpers.ts
|
|
5
5
|
/**
|
|
6
6
|
* Pure measurement helpers — no canvas, no DOM, no worker.
|
|
@@ -62,6 +62,17 @@ function getResolvedFallback(fontFamily) {
|
|
|
62
62
|
return getResolvedData(fontFamily).cssFallback;
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
+
* Clear the resolved-font-family cache. `resolveFontFamily`'s output for a
|
|
66
|
+
* given key can change even when the raw font names don't — e.g. when the
|
|
67
|
+
* active document's embedded-font family map changes (see
|
|
68
|
+
* `utils/fontResolver.ts`'s `setEmbeddedFontFamilyMap`) — so this must be
|
|
69
|
+
* cleared whenever that happens, not just on font-name changes. Folded into
|
|
70
|
+
* `clearAllCaches` (`./cache.ts`) so callers get it for free.
|
|
71
|
+
*/
|
|
72
|
+
function clearFontResolvedCache() {
|
|
73
|
+
fontResolvedCache.clear();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
65
76
|
* Build a CSS font string from styling properties
|
|
66
77
|
*
|
|
67
78
|
* Font sizes are in points and need to be converted to pixels for canvas.
|
|
@@ -73,13 +84,13 @@ function getResolvedFallback(fontFamily) {
|
|
|
73
84
|
*
|
|
74
85
|
* @example
|
|
75
86
|
* buildFontString({ fontFamily: "Arial", fontSize: 12, bold: true })
|
|
76
|
-
* // Returns: "
|
|
87
|
+
* // Returns: "700 16px Arial, Arimo, Helvetica, sans-serif" (12pt = 16px)
|
|
77
88
|
*/
|
|
78
89
|
function buildFontString(style) {
|
|
79
90
|
const parts = [];
|
|
80
91
|
if (style.italic) parts.push("italic");
|
|
81
92
|
if (style.fontVariant) parts.push(style.fontVariant);
|
|
82
|
-
if (style.bold) parts.push("
|
|
93
|
+
if (style.bold) parts.push("700");
|
|
83
94
|
const fontSizePx = ptToPx(style.fontSize ?? 11);
|
|
84
95
|
parts.push(`${fontSizePx}px`);
|
|
85
96
|
const fontFamily = style.fontFamily ?? "Calibri";
|
|
@@ -156,4 +167,4 @@ function pxToHalfPt(px) {
|
|
|
156
167
|
return pxToPt(px) * 2;
|
|
157
168
|
}
|
|
158
169
|
//#endregion
|
|
159
|
-
export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, buildFontString, buildRunFontStyle, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
|
|
170
|
+
export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, buildFontString, buildRunFontStyle, clearFontResolvedCache, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { CJK_FALLBACK_FONT_FAMILY, isCjkFont } from "../../utils/fontResolver.js";
|
|
2
|
-
import { getFontMetrics, measureRun, measureTextWidth } from "./measureProvider.js";
|
|
3
2
|
import { countCompressibleSpaces } from "./textMeasurementPolicy.js";
|
|
3
|
+
import { getFontMetrics, measureRun, measureTextWidth } from "./measureProvider.js";
|
|
4
4
|
import { buildRunFontStyle, ptToPx, twipsToPx } from "./measureHelpers.js";
|
|
5
|
-
import "./lineBreakProvider.js";
|
|
5
|
+
import { defaultLineBreakProvider, getLineBreakProvider } from "./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";
|
|
@@ -27,7 +27,9 @@ const WIDTH_TOLERANCE = .5;
|
|
|
27
27
|
const JUSTIFY_SHRINK_TOLERANCE_RATIO = .016;
|
|
28
28
|
const JUSTIFY_SPACE_CONTRACTION_RATIO = .075;
|
|
29
29
|
const JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO = .195;
|
|
30
|
-
const
|
|
30
|
+
const JUSTIFY_DEEP_HANGING_LIST_MARKER_SHRINK_TOLERANCE_RATIO = .022;
|
|
31
|
+
const JUSTIFY_LIST_CONTINUATION_SPACE_CONTRACTION_RATIO = .32;
|
|
32
|
+
const JUSTIFY_LIST_CONTINUATION_MAX_SHRINK_TOLERANCE_RATIO = .015;
|
|
31
33
|
const JUSTIFY_INSET_LIST_SHRINK_TOLERANCE_RATIO = .015;
|
|
32
34
|
const JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO = .017;
|
|
33
35
|
const JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO = .021;
|
|
@@ -137,7 +139,7 @@ const WORD_SINGLE_LINE_FLOOR = 1.15;
|
|
|
137
139
|
* `auto`/`atLeast` line rules; `exact` means exact (per OOXML §17.3.1.33)
|
|
138
140
|
* and stays untouched.
|
|
139
141
|
*/
|
|
140
|
-
function calculateEmptyParagraphMetrics(fontSize, spacing, fontFamily) {
|
|
142
|
+
function calculateEmptyParagraphMetrics(fontSize, spacing, fontFamily, attrs) {
|
|
141
143
|
const result = calculateTypographyMetrics(fontSize, spacing, getFontMetrics({
|
|
142
144
|
fontSize,
|
|
143
145
|
fontFamily: fontFamily ?? DEFAULT_FONT_FAMILY
|
|
@@ -145,12 +147,22 @@ function calculateEmptyParagraphMetrics(fontSize, spacing, fontFamily) {
|
|
|
145
147
|
const lineRule = spacing?.lineRule ?? "auto";
|
|
146
148
|
if (lineRule === "auto" || lineRule === "atLeast") {
|
|
147
149
|
const floor = ptToPx(fontSize) * WORD_SINGLE_LINE_FLOOR;
|
|
148
|
-
if (result.lineHeight < floor) return {
|
|
150
|
+
if (result.lineHeight < floor) return applyDocumentGrid({
|
|
149
151
|
...result,
|
|
150
152
|
lineHeight: floor
|
|
151
|
-
};
|
|
153
|
+
}, attrs, spacing);
|
|
152
154
|
}
|
|
153
|
-
return result;
|
|
155
|
+
return applyDocumentGrid(result, attrs, spacing);
|
|
156
|
+
}
|
|
157
|
+
function applyDocumentGrid(typography, attrs, spacing) {
|
|
158
|
+
const pitch = attrs?.documentGridLinePitch;
|
|
159
|
+
if (pitch === void 0 || pitch <= 0 || attrs?.snapToGrid === false) return typography;
|
|
160
|
+
if (spacing?.lineRule === "exact") return typography;
|
|
161
|
+
const lineHeight = Math.ceil(typography.lineHeight / pitch - 1e-9) * pitch;
|
|
162
|
+
return lineHeight === typography.lineHeight ? typography : {
|
|
163
|
+
...typography,
|
|
164
|
+
lineHeight
|
|
165
|
+
};
|
|
154
166
|
}
|
|
155
167
|
/**
|
|
156
168
|
* Check if a run is a text run
|
|
@@ -344,6 +356,7 @@ function uppercaseLetterRatio(text) {
|
|
|
344
356
|
return letters === 0 ? 0 : uppercase / letters;
|
|
345
357
|
}
|
|
346
358
|
function resolveJustifyFitStrategy(block, isFirstLine, profile) {
|
|
359
|
+
if (block.attrs?.justificationCompatibility?.type === "legacy") return { type: "strict" };
|
|
347
360
|
if (isShallowFullHangingListContinuation(block, isFirstLine)) return { type: "rounding" };
|
|
348
361
|
if (block.attrs?.listMarker !== void 0) {
|
|
349
362
|
const hanging = block.attrs.indent?.hanging ?? 0;
|
|
@@ -352,7 +365,7 @@ function resolveJustifyFitStrategy(block, isFirstLine, profile) {
|
|
|
352
365
|
ratio: JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO
|
|
353
366
|
} : {
|
|
354
367
|
type: "width",
|
|
355
|
-
ratio:
|
|
368
|
+
ratio: JUSTIFY_DEEP_HANGING_LIST_MARKER_SHRINK_TOLERANCE_RATIO
|
|
356
369
|
};
|
|
357
370
|
const left = block.attrs.indent?.left ?? 0;
|
|
358
371
|
if (hanging > 0 && left > hanging) return {
|
|
@@ -361,7 +374,8 @@ function resolveJustifyFitStrategy(block, isFirstLine, profile) {
|
|
|
361
374
|
};
|
|
362
375
|
return {
|
|
363
376
|
type: "space",
|
|
364
|
-
ratio: JUSTIFY_LIST_CONTINUATION_SPACE_CONTRACTION_RATIO
|
|
377
|
+
ratio: JUSTIFY_LIST_CONTINUATION_SPACE_CONTRACTION_RATIO,
|
|
378
|
+
maxWidthRatio: JUSTIFY_LIST_CONTINUATION_MAX_SHRINK_TOLERANCE_RATIO
|
|
365
379
|
};
|
|
366
380
|
}
|
|
367
381
|
const hasTabStops = (block.attrs?.tabs?.length ?? 0) > 0;
|
|
@@ -391,9 +405,12 @@ function compressibleSpaceWidth(text, style) {
|
|
|
391
405
|
return count === 0 ? 0 : count * measureTextWidth(" ", style);
|
|
392
406
|
}
|
|
393
407
|
function justifyFitTolerance(line, strategy, candidateSpaceWidth) {
|
|
408
|
+
if (strategy.type === "strict") return 0;
|
|
394
409
|
if (strategy.type === "rounding") return WIDTH_TOLERANCE;
|
|
395
410
|
if (strategy.type === "width") return Math.max(WIDTH_TOLERANCE, line.availableWidth * strategy.ratio);
|
|
396
|
-
|
|
411
|
+
const spaceTolerance = (line.regularSpaceWidth + candidateSpaceWidth) * strategy.ratio;
|
|
412
|
+
const boundedTolerance = strategy.maxWidthRatio === void 0 ? spaceTolerance : Math.min(spaceTolerance, line.availableWidth * strategy.maxWidthRatio);
|
|
413
|
+
return Math.max(WIDTH_TOLERANCE, boundedTolerance);
|
|
397
414
|
}
|
|
398
415
|
function isShallowFullHangingListContinuation(block, isFirstLine) {
|
|
399
416
|
if (isFirstLine || block.attrs?.listMarker === void 0) return false;
|
|
@@ -410,8 +427,20 @@ function trimTrailingSpacesAndTabs(text) {
|
|
|
410
427
|
}
|
|
411
428
|
return text.slice(0, end);
|
|
412
429
|
}
|
|
430
|
+
function trailingCodePoint(text) {
|
|
431
|
+
if (text.length === 0) return;
|
|
432
|
+
const trailing = text.charCodeAt(text.length - 1);
|
|
433
|
+
const preceding = text.charCodeAt(text.length - 2);
|
|
434
|
+
const start = trailing >= 56320 && trailing <= 57343 && preceding >= 55296 && preceding <= 56319 ? text.length - 2 : text.length - 1;
|
|
435
|
+
return text.slice(start);
|
|
436
|
+
}
|
|
437
|
+
const usesDefaultHangingPunctuationClassifier = () => {
|
|
438
|
+
return (getLineBreakProvider().isHangingPunctuation ?? defaultLineBreakProvider.isHangingPunctuation) === defaultLineBreakProvider.isHangingPunctuation;
|
|
439
|
+
};
|
|
413
440
|
function trailingHangingPunctuationWidth(text, style, policy, enabled) {
|
|
414
441
|
if (!enabled || text.length === 0) return 0;
|
|
442
|
+
const trailing = trailingCodePoint(text);
|
|
443
|
+
if (usesDefaultHangingPunctuationClassifier() && !isHangingPunctuation(trailing ?? "", policy)) return 0;
|
|
415
444
|
const lastStart = findGraphemeBreaks(text, policy).at(-2) ?? 0;
|
|
416
445
|
const lastGrapheme = text.slice(lastStart);
|
|
417
446
|
if (!isHangingPunctuation(lastGrapheme, policy)) return 0;
|
|
@@ -448,13 +477,14 @@ function computeProtectedCrossRunGlueWidths(block) {
|
|
|
448
477
|
for (let index = 0; index < block.runs.length; index++) {
|
|
449
478
|
const run = block.runs[index];
|
|
450
479
|
if (!run || !isTextRun(run)) continue;
|
|
451
|
-
const trailing = /(\S+)(\s*)$/u.exec(run.text ?? "");
|
|
452
|
-
const token = trailing?.[1];
|
|
453
480
|
const policy = resolveEffectiveLineBreakPolicy({
|
|
454
481
|
attrs: block.attrs,
|
|
455
482
|
run
|
|
456
483
|
}).provider;
|
|
457
|
-
if (!
|
|
484
|
+
if (!policy.locale?.toLocaleLowerCase().startsWith("cs")) continue;
|
|
485
|
+
const trailing = /(\S+)(\s*)$/u.exec(run.text ?? "");
|
|
486
|
+
const token = trailing?.[1];
|
|
487
|
+
if (!token || token.length !== 1) continue;
|
|
458
488
|
let separator = trailing[2] ?? "";
|
|
459
489
|
let glueWidth = separator.length > 0 ? measureTextWidth(separator, runToFontStyle(run)) : 0;
|
|
460
490
|
let followingWord = "";
|
|
@@ -485,12 +515,20 @@ function computeProtectedCrossRunGlueWidths(block) {
|
|
|
485
515
|
}
|
|
486
516
|
return widths;
|
|
487
517
|
}
|
|
518
|
+
/**
|
|
519
|
+
* Cap on the number of runs `collectCrossRunWord` traverses per call. A
|
|
520
|
+
* paragraph split into many tiny (including empty) runs would otherwise let
|
|
521
|
+
* the search walk the full run list for every word-start position it is
|
|
522
|
+
* invoked from, without the per-word text-length budget ever kicking in.
|
|
523
|
+
*/
|
|
524
|
+
const MAX_CROSS_RUN_SEGMENTS = 64;
|
|
488
525
|
/** Collect one lexical word continued through adjacent formatting runs. */
|
|
489
526
|
function collectCrossRunWord({ block, startRunIndex, startChar }) {
|
|
490
527
|
const segments = [];
|
|
491
528
|
let text = "";
|
|
492
529
|
let width = 0;
|
|
493
530
|
for (let runIndex = startRunIndex; runIndex < block.runs.length; runIndex++) {
|
|
531
|
+
if (runIndex - startRunIndex >= MAX_CROSS_RUN_SEGMENTS) return;
|
|
494
532
|
const run = block.runs[runIndex];
|
|
495
533
|
if (!run || !isTextRun(run)) break;
|
|
496
534
|
const start = runIndex === startRunIndex ? startChar : 0;
|
|
@@ -655,7 +693,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
655
693
|
totalHeight: 0
|
|
656
694
|
};
|
|
657
695
|
}
|
|
658
|
-
const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY);
|
|
696
|
+
const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY, attrs);
|
|
659
697
|
const outlineLineHeight = attrs?.reserveEmptyOutlineHeight ? emptyMetrics.lineHeight * 2 : emptyMetrics.lineHeight;
|
|
660
698
|
lines.push({
|
|
661
699
|
fromRun: 0,
|
|
@@ -675,9 +713,8 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
675
713
|
totalHeight
|
|
676
714
|
};
|
|
677
715
|
}
|
|
678
|
-
if (runs.length
|
|
679
|
-
const
|
|
680
|
-
const emptyMetrics = calculateEmptyParagraphMetrics(run.fontSize ?? attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, run.fontFamily ?? attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY);
|
|
716
|
+
if (runs.length > 0 && runs.every((run) => isTextRun(run) && isEmptyTextRun(run))) {
|
|
717
|
+
const emptyMetrics = calculateEmptyParagraphMetrics(attrs?.defaultFontSize ?? DEFAULT_FONT_SIZE, spacing, attrs?.defaultFontFamily ?? DEFAULT_FONT_FAMILY, attrs);
|
|
681
718
|
lines.push({
|
|
682
719
|
fromRun: 0,
|
|
683
720
|
fromChar: 0,
|
|
@@ -733,7 +770,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
733
770
|
finalTypography.lineHeight = objectHeight;
|
|
734
771
|
finalTypography.ascent = objectHeight;
|
|
735
772
|
finalTypography.descent = 0;
|
|
736
|
-
return finalTypography;
|
|
773
|
+
return applyDocumentGrid(finalTypography, attrs, spacing);
|
|
737
774
|
}
|
|
738
775
|
if (soleRun && isImageRun(soleRun)) {
|
|
739
776
|
finalTypography.lineHeight = objectHeight;
|
|
@@ -744,7 +781,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
744
781
|
finalTypography.ascent = objectHeight;
|
|
745
782
|
}
|
|
746
783
|
}
|
|
747
|
-
return finalTypography;
|
|
784
|
+
return applyDocumentGrid(finalTypography, attrs, spacing);
|
|
748
785
|
};
|
|
749
786
|
const getPostWrapAvailableWidth = () => {
|
|
750
787
|
if (!floatingZones || floatingZones.length === 0) return bodyContentWidth;
|
|
@@ -769,7 +806,8 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
769
806
|
toChar: currentLine.toChar,
|
|
770
807
|
width: Math.max(0, currentLine.width - currentLine.trailingWhitespaceWidth),
|
|
771
808
|
...finalTypography,
|
|
772
|
-
...currentLine.discretionaryHyphen ? { discretionaryHyphen: currentLine.discretionaryHyphen } : {}
|
|
809
|
+
...currentLine.discretionaryHyphen ? { discretionaryHyphen: currentLine.discretionaryHyphen } : {},
|
|
810
|
+
...currentLine.renderedPageBreakBefore ? { renderedPageBreakBefore: true } : {}
|
|
773
811
|
};
|
|
774
812
|
if (currentLine.leftOffset > 0) line.leftOffset = currentLine.leftOffset;
|
|
775
813
|
if (currentLine.rightOffset > 0) line.rightOffset = currentLine.rightOffset;
|
|
@@ -827,6 +865,22 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
827
865
|
for (let runIndex = 0; runIndex < runs.length; runIndex++) {
|
|
828
866
|
if (crossRunResume && runIndex < crossRunResume.runIndex) continue;
|
|
829
867
|
const run = runs[runIndex];
|
|
868
|
+
if (run.kind === "renderedPageBreak") {
|
|
869
|
+
if (!runs.slice(runIndex + 1).some((followingRun) => followingRun.kind !== "renderedPageBreak")) {
|
|
870
|
+
currentLine.toRun = runIndex;
|
|
871
|
+
currentLine.toChar = 0;
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (currentLine.width > 0 || currentLine.maxImageHeightPx > 0 || currentLine.maxMathHeightPx > 0) startNewLine(runIndex + 1, 0);
|
|
875
|
+
else {
|
|
876
|
+
currentLine.fromRun = runIndex + 1;
|
|
877
|
+
currentLine.toRun = runIndex + 1;
|
|
878
|
+
currentLine.fromChar = 0;
|
|
879
|
+
currentLine.toChar = 0;
|
|
880
|
+
}
|
|
881
|
+
currentLine.renderedPageBreakBefore = true;
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
830
884
|
if (isLineBreakRun(run)) {
|
|
831
885
|
currentLine.toRun = runIndex;
|
|
832
886
|
currentLine.toChar = 0;
|
|
@@ -952,13 +1006,11 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
952
1006
|
const wordBreaks = findWordBreaks(text, breakPolicy);
|
|
953
1007
|
let charIndex = crossRunResume?.runIndex === runIndex ? crossRunResume.charIndex : 0;
|
|
954
1008
|
if (crossRunResume?.runIndex === runIndex) crossRunResume = void 0;
|
|
1009
|
+
let wordBreakIndex = 0;
|
|
955
1010
|
let activeHyphenationWord;
|
|
956
1011
|
while (charIndex < text.length) {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
nextBreak = breakPoint;
|
|
960
|
-
break;
|
|
961
|
-
}
|
|
1012
|
+
while ((wordBreaks[wordBreakIndex] ?? Number.POSITIVE_INFINITY) <= charIndex) wordBreakIndex += 1;
|
|
1013
|
+
const nextBreak = wordBreaks[wordBreakIndex] ?? text.length;
|
|
962
1014
|
const word = text.slice(charIndex, nextBreak);
|
|
963
1015
|
const measuredWord = trimTrailingSpacesAndTabs(word);
|
|
964
1016
|
const wordWidth = measureTextWidth(measuredWord, style);
|
|
@@ -1094,6 +1146,14 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
1094
1146
|
}
|
|
1095
1147
|
}
|
|
1096
1148
|
}
|
|
1149
|
+
const listParagraphMarkFontSize = attrs?.listParagraphMarkFontSize;
|
|
1150
|
+
if (listParagraphMarkFontSize !== void 0 && listParagraphMarkFontSize > currentLine.maxFontSize) {
|
|
1151
|
+
const fontFamily = currentLine.maxFontMetrics?.fontFamily ?? attrs?.defaultFontFamily;
|
|
1152
|
+
updateMaxFont({
|
|
1153
|
+
fontSize: listParagraphMarkFontSize,
|
|
1154
|
+
...fontFamily === void 0 ? {} : { fontFamily }
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1097
1157
|
finalizeLine();
|
|
1098
1158
|
let totalWithSpacing = lines.reduce((sum, line) => sum + measuredLineAdvance(line), 0);
|
|
1099
1159
|
if (spacing?.before) totalWithSpacing += spacing.before;
|