@stll/folio-core 0.48.0 → 0.50.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.js +565 -121
- package/dist/ai-edits/headless.d.ts +37 -1
- package/dist/ai-edits/headless.js +47 -16
- package/dist/ai-edits/minimal-replacement.d.ts +64 -0
- package/dist/ai-edits/minimal-replacement.js +282 -0
- package/dist/controller/layoutPipeline.js +11 -1
- package/dist/display-list/build/paragraphPrimitives.js +63 -7
- package/dist/display-list/build/textBoxPrimitives.js +1 -1
- package/dist/display-list/build/unsupported.d.ts +2 -2
- package/dist/display-list/build/unsupported.js +0 -0
- package/dist/display-list/dom/renderDisplayListToDom.d.ts +10 -1
- package/dist/display-list/dom/renderDisplayListToDom.js +8 -8
- package/dist/display-list/html/renderDisplayListToHtml.d.ts +21 -0
- package/dist/display-list/html/renderDisplayListToHtml.js +126 -0
- package/dist/display-list/selectDisplayPages.d.ts +6 -0
- package/dist/display-list/selectDisplayPages.js +44 -0
- package/dist/docx/blockContentParser.js +2 -2
- package/dist/docx/bulletMarkers.d.ts +18 -2
- package/dist/docx/bulletMarkers.js +27 -2
- package/dist/docx/drawingGroupChildren.d.ts +60 -0
- package/dist/docx/drawingGroupChildren.js +176 -0
- package/dist/docx/drawingUtils.js +3 -0
- package/dist/docx/groupDrawingParser.d.ts +7 -1
- package/dist/docx/groupDrawingParser.js +123 -32
- package/dist/docx/packageParts.d.ts +45 -1
- package/dist/docx/packageParts.js +93 -2
- package/dist/docx/paraIdAttribute.d.ts +3 -2
- package/dist/docx/paragraphParser.js +91 -7
- package/dist/docx/paragraphTextBoxEnrichment.js +126 -4
- package/dist/docx/runParser.js +10 -0
- package/dist/docx/selectiveSaveFlags.d.ts +10 -3
- package/dist/docx/selectiveSaveFlags.js +1 -1
- package/dist/docx/selectiveXmlPatch.d.ts +56 -34
- package/dist/docx/selectiveXmlPatch.js +222 -115
- package/dist/docx/serializer/groupTextBoxWriteBack.d.ts +12 -0
- package/dist/docx/serializer/groupTextBoxWriteBack.js +101 -0
- package/dist/docx/serializer/paragraphSerializer.js +5 -4
- package/dist/docx/serializer/runSerializer.d.ts +5 -1
- package/dist/docx/serializer/runSerializer.js +4 -3
- package/dist/docx/shapeAlternateContent.d.ts +25 -0
- package/dist/docx/shapeAlternateContent.js +52 -0
- package/dist/docx/textBoxParser.d.ts +10 -2
- package/dist/docx/textBoxParser.js +9 -3
- package/dist/export-pdf.d.ts +35 -2
- package/dist/export-pdf.js +51 -20
- package/dist/fonts/fontsourceFaces.d.ts +47 -0
- package/dist/fonts/fontsourceFaces.js +230 -0
- package/dist/internal/headlessRevisionResolution.js +50 -13
- package/dist/layout-bridge/convert/flowBlockPostprocessing.d.ts +8 -0
- package/dist/layout-bridge/convert/flowBlockPostprocessing.js +106 -0
- package/dist/layout-bridge/convert/flowBorders.d.ts +17 -0
- package/dist/layout-bridge/convert/flowBorders.js +31 -0
- package/dist/layout-bridge/convert/flowConversionShared.d.ts +111 -0
- package/dist/layout-bridge/convert/flowConversionShared.js +23 -0
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -1
- package/dist/layout-bridge/convert/footnoteLayout.js +2 -1
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +2 -1
- package/dist/layout-bridge/convert/imageConversion.d.ts +33 -0
- package/dist/layout-bridge/convert/imageConversion.js +118 -0
- package/dist/layout-bridge/convert/listMarkers.d.ts +28 -0
- package/dist/layout-bridge/convert/listMarkers.js +98 -0
- package/dist/layout-bridge/convert/noteReferences.d.ts +14 -0
- package/dist/layout-bridge/convert/noteReferences.js +29 -0
- package/dist/layout-bridge/convert/pageBreakSplitting.d.ts +16 -0
- package/dist/layout-bridge/convert/pageBreakSplitting.js +140 -0
- package/dist/layout-bridge/convert/paragraphAttrs.d.ts +20 -0
- package/dist/layout-bridge/convert/paragraphAttrs.js +234 -0
- package/dist/layout-bridge/convert/paragraphConversion.d.ts +12 -0
- package/dist/layout-bridge/convert/paragraphConversion.js +112 -0
- package/dist/layout-bridge/convert/paragraphRuns.d.ts +15 -0
- package/dist/layout-bridge/convert/paragraphRuns.js +281 -0
- package/dist/layout-bridge/convert/runFormattingMerge.d.ts +19 -0
- package/dist/layout-bridge/convert/runFormattingMerge.js +167 -0
- package/dist/layout-bridge/convert/runMarkFormatting.d.ts +11 -0
- package/dist/layout-bridge/convert/runMarkFormatting.js +291 -0
- package/dist/layout-bridge/convert/sectionBoundaries.d.ts +35 -0
- package/dist/layout-bridge/convert/sectionBoundaries.js +114 -0
- package/dist/layout-bridge/convert/tableConversion.d.ts +12 -0
- package/dist/layout-bridge/convert/tableConversion.js +303 -0
- package/dist/layout-bridge/convert/textBoxFill.d.ts +20 -0
- package/dist/layout-bridge/convert/textBoxFill.js +42 -0
- package/dist/layout-bridge/convert/textFormattingConversion.d.ts +22 -0
- package/dist/layout-bridge/convert/textFormattingConversion.js +120 -0
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +5 -113
- package/dist/layout-bridge/convert/toFlowBlocks.js +11 -2060
- package/dist/layout-bridge/engine/hitTest.js +1 -1
- package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
- package/dist/layout-bridge/engine/measuring/index.js +2 -1
- package/dist/layout-bridge/engine/selectionRects.js +1 -1
- package/dist/layout-engine/imageLayout.d.ts +9 -0
- package/dist/layout-engine/imageLayout.js +48 -0
- package/dist/layout-engine/index.d.ts +4 -19
- package/dist/layout-engine/index.js +14 -910
- package/dist/layout-engine/keep-together.d.ts +5 -2
- package/dist/layout-engine/keep-together.js +5 -1
- package/dist/layout-engine/layoutFlowShared.d.ts +6 -0
- package/dist/layout-engine/layoutFlowShared.js +8 -0
- package/dist/layout-engine/measure/advanceComposition.js +49 -3
- package/dist/layout-engine/measure/crossRunWords.d.ts +45 -0
- package/dist/layout-engine/measure/crossRunWords.js +172 -0
- package/dist/layout-engine/measure/floatingLineClearance.d.ts +24 -0
- package/dist/layout-engine/measure/floatingLineClearance.js +44 -0
- package/dist/layout-engine/measure/inlineObjectMetrics.d.ts +6 -0
- package/dist/layout-engine/measure/inlineObjectMetrics.js +52 -0
- package/dist/layout-engine/measure/justification.d.ts +32 -0
- package/dist/layout-engine/measure/justification.js +83 -0
- package/dist/layout-engine/measure/lineFitting.d.ts +48 -0
- package/dist/layout-engine/measure/lineFitting.js +145 -0
- package/dist/layout-engine/measure/lineTypography.d.ts +44 -0
- package/dist/layout-engine/measure/lineTypography.js +124 -0
- package/dist/layout-engine/measure/measureBlocks.js +7 -4
- package/dist/layout-engine/measure/measureContainer.js +46 -3
- package/dist/layout-engine/measure/measureHelpers.js +13 -3
- package/dist/layout-engine/measure/measureParagraph.d.ts +1 -20
- package/dist/layout-engine/measure/measureParagraph.js +41 -780
- package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +109 -0
- package/dist/layout-engine/measure/paragraphMeasureShared.js +82 -0
- package/dist/layout-engine/measure/paragraphTabs.d.ts +18 -0
- package/dist/layout-engine/measure/paragraphTabs.js +78 -0
- package/dist/layout-engine/measure/smallCapsCasing.d.ts +56 -0
- package/dist/layout-engine/measure/smallCapsCasing.js +88 -0
- package/dist/layout-engine/paginator.d.ts +2 -0
- package/dist/layout-engine/paginator.js +5 -1
- package/dist/layout-engine/paragraphLayout.d.ts +23 -0
- package/dist/layout-engine/paragraphLayout.js +204 -0
- package/dist/layout-engine/paragraphSpacing.d.ts +4 -2
- package/dist/layout-engine/paragraphSpacing.js +2 -1
- package/dist/layout-engine/sectionLayout.d.ts +45 -0
- package/dist/layout-engine/sectionLayout.js +216 -0
- package/dist/layout-engine/tableLayout.d.ts +17 -0
- package/dist/layout-engine/tableLayout.js +369 -0
- package/dist/layout-engine/tableRowBreak.d.ts +8 -1
- package/dist/layout-engine/tableRowBreak.js +22 -1
- package/dist/layout-engine/textBoxLayout.d.ts +15 -0
- package/dist/layout-engine/textBoxLayout.js +137 -0
- package/dist/layout-engine/types.d.ts +21 -2
- package/dist/layout-painter/renderPage.js +1 -0
- package/dist/layout-painter/renderParagraph.js +41 -4
- package/dist/layout-painter/renderTextBox.d.ts +12 -2
- package/dist/layout-painter/renderTextBox.js +26 -2
- package/dist/paged-layout/sectionBlockWidths.d.ts +2 -1
- package/dist/paged-layout/sectionBlockWidths.js +1 -1
- package/dist/prosemirror/alternateContentAttrs.d.ts +9 -0
- package/dist/prosemirror/alternateContentAttrs.js +35 -0
- package/dist/prosemirror/attrs/index.js +90 -0
- package/dist/prosemirror/commands/comments.js +51 -41
- package/dist/prosemirror/contentControlRevisions.d.ts +43 -0
- package/dist/prosemirror/contentControlRevisions.js +84 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +46 -22
- package/dist/prosemirror/conversion/toProseDoc.js +42 -19
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +7 -7
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -7
- package/dist/prosemirror/extensions/nodes/FieldExtension.js +6 -3
- package/dist/prosemirror/extensions/nodes/SdtExtension.js +9 -2
- package/dist/prosemirror/extensions/nodes/ShapeExtension.js +1 -0
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +3 -0
- package/dist/prosemirror/listMarker.js +2 -2
- package/dist/prosemirror/paragraphDirection.d.ts +29 -2
- package/dist/prosemirror/paragraphDirection.js +21 -2
- package/dist/prosemirror/plugins/suggestionMode.js +37 -8
- package/dist/prosemirror/rejoinRunCarriers.d.ts +27 -0
- package/dist/prosemirror/rejoinRunCarriers.js +66 -0
- package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +10 -0
- package/dist/prosemirror/runIdentityAcrossRevisions.js +90 -0
- package/dist/prosemirror/schema/nodes.d.ts +37 -5
- package/dist/server.d.ts +3 -2
- package/dist/types/content.d.ts +2 -2
- package/package.json +2 -2
- package/dist/layout-engine/justifiedLineFit.d.ts +0 -7
- package/dist/layout-engine/justifiedLineFit.js +0 -6
|
@@ -35,6 +35,34 @@ type FolioApplyOperationsOptions = {
|
|
|
35
35
|
};
|
|
36
36
|
/** Options for {@link FolioDocxReviewer.applyDocumentOperations}. */
|
|
37
37
|
type FolioApplyDocumentOperationsOptions = Omit<FolioApplyOperationsOptions, "mode">;
|
|
38
|
+
/**
|
|
39
|
+
* Why a save rewrote the whole package instead of patching the edited
|
|
40
|
+
* paragraphs into the original: `structuralChange` (paragraphs added or
|
|
41
|
+
* removed, or styles, headers, footers or final section properties staged),
|
|
42
|
+
* `untrackedChange` (an edit the change tracker cannot key to a paragraph), or
|
|
43
|
+
* `selectiveDeclined` (the patch could not express the edit safely).
|
|
44
|
+
*/
|
|
45
|
+
type FolioReviewerRepackReason = "structuralChange" | "untrackedChange" | "selectiveDeclined";
|
|
46
|
+
/** Options for {@link FolioDocxReviewer.save}. */
|
|
47
|
+
type FolioReviewerSaveOptions = {
|
|
48
|
+
/**
|
|
49
|
+
* `"allow"` (default) falls back to a full repack when the selective patch
|
|
50
|
+
* does not apply; `"refuse"` reports the reason and writes nothing.
|
|
51
|
+
*/
|
|
52
|
+
repack?: "allow" | "refuse";
|
|
53
|
+
};
|
|
54
|
+
/** Result of {@link FolioDocxReviewer.save}. */
|
|
55
|
+
type FolioReviewerSaveResult = {
|
|
56
|
+
type: "selective";
|
|
57
|
+
buffer: ArrayBuffer;
|
|
58
|
+
} | {
|
|
59
|
+
type: "full-repack";
|
|
60
|
+
reason: FolioReviewerRepackReason;
|
|
61
|
+
buffer: ArrayBuffer;
|
|
62
|
+
} | {
|
|
63
|
+
type: "repackRefused";
|
|
64
|
+
reason: FolioReviewerRepackReason;
|
|
65
|
+
};
|
|
38
66
|
/** Options for {@link FolioDocxReviewer.getContentAsText}. */
|
|
39
67
|
type FolioGetContentAsTextOptions = {
|
|
40
68
|
/**
|
|
@@ -248,6 +276,8 @@ type FolioReviewReplyInput = {
|
|
|
248
276
|
/** Reply author; defaults to the reviewer's author. */
|
|
249
277
|
author?: string;
|
|
250
278
|
initials?: string;
|
|
279
|
+
/** ISO-8601 reply date; defaults to the wall clock. */
|
|
280
|
+
date?: string;
|
|
251
281
|
};
|
|
252
282
|
/** A comment thread discovered in the document. */
|
|
253
283
|
type FolioReviewComment = {
|
|
@@ -506,6 +536,12 @@ declare class FolioDocxReviewer {
|
|
|
506
536
|
* structural edits — the same two-tier path the editor's save uses.
|
|
507
537
|
*/
|
|
508
538
|
toBuffer(): Promise<ArrayBuffer>;
|
|
539
|
+
/**
|
|
540
|
+
* Serialise like {@link toBuffer} and report which path wrote the package.
|
|
541
|
+
* With `repack: "refuse"`, a save the selective patch cannot express
|
|
542
|
+
* returns the reason instead of rewriting every part.
|
|
543
|
+
*/
|
|
544
|
+
save({ repack: repackPolicy }?: FolioReviewerSaveOptions): Promise<FolioReviewerSaveResult>;
|
|
509
545
|
private assertResolvedStoriesSerialized;
|
|
510
546
|
/**
|
|
511
547
|
* Attempt the selective patch, treating a throw the same as a decline. Odd
|
|
@@ -580,4 +616,4 @@ type ApplyFolioAIEditsToBufferResult = FolioAIEditApplyResult & {
|
|
|
580
616
|
*/
|
|
581
617
|
declare const applyFolioAIEditsToBuffer: (buffer: ArrayBuffer, operations: FolioAIEditOperation[], options?: ApplyFolioAIEditsToBufferOptions) => Promise<ApplyFolioAIEditsToBufferResult>;
|
|
582
618
|
//#endregion
|
|
583
|
-
export { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FolioApplyDocumentOperationsOptions, FolioApplyDocumentOperationsToStoryOptions, FolioApplyOperationsOptions, FolioDocumentStory, FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, FolioDocxReviewer, FolioDocxReviewerOptions, FolioEditableDocumentStoryHandle, FolioGetContentAsTextOptions, FolioMatchStoryTableGeometryOptions, FolioNumberingLevel, FolioReadReviewedStoryOptions, FolioResolveReviewedStoryOptions, FolioResolvedReviewedView, type FolioReviewChange, FolioReviewChangeFilter, type FolioReviewChangeKind, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, FolioReviewedStory, FolioReviewedView, type FolioRevisionStamp, UnsupportedFolioReviewedViewError, applyFolioAIEditsToBuffer, getFolioDocxComparisonAccess, isFolioResolvedReviewedView, isFolioReviewedView };
|
|
619
|
+
export { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FolioApplyDocumentOperationsOptions, FolioApplyDocumentOperationsToStoryOptions, FolioApplyOperationsOptions, FolioDocumentStory, FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, FolioDocxReviewer, FolioDocxReviewerOptions, FolioEditableDocumentStoryHandle, FolioGetContentAsTextOptions, FolioMatchStoryTableGeometryOptions, FolioNumberingLevel, FolioReadReviewedStoryOptions, FolioResolveReviewedStoryOptions, FolioResolvedReviewedView, type FolioReviewChange, FolioReviewChangeFilter, type FolioReviewChangeKind, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, FolioReviewedStory, FolioReviewedView, FolioReviewerRepackReason, FolioReviewerSaveOptions, FolioReviewerSaveResult, type FolioRevisionStamp, UnsupportedFolioReviewedViewError, applyFolioAIEditsToBuffer, getFolioDocxComparisonAccess, isFolioResolvedReviewedView, isFolioReviewedView };
|
|
@@ -68,16 +68,10 @@ import JSZip from "jszip";
|
|
|
68
68
|
*/
|
|
69
69
|
let commentIdCursor = Date.now();
|
|
70
70
|
let undoHandleCursor = Date.now();
|
|
71
|
-
|
|
72
|
-
* Build the note-free comment thread the apply layer references by id.
|
|
73
|
-
* Mirrors `commentsHelpers.createComment` (a pure object literal there) so a
|
|
74
|
-
* headless `commentOnBlock` / `comment` op serialises the same `comments.xml`
|
|
75
|
-
* shape the editor produces.
|
|
76
|
-
*/
|
|
77
|
-
const createReviewerComment = (id, text, author) => ({
|
|
71
|
+
const createReviewerComment = ({ id, text, author, date }) => ({
|
|
78
72
|
id,
|
|
79
73
|
author,
|
|
80
|
-
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
74
|
+
date: date ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
81
75
|
content: [{
|
|
82
76
|
type: "paragraph",
|
|
83
77
|
formatting: {},
|
|
@@ -848,7 +842,12 @@ var FolioDocxReviewer = class FolioDocxReviewer {
|
|
|
848
842
|
...tableTemplates !== void 0 && { tableTemplates },
|
|
849
843
|
...replacementBackground !== void 0 && { replacementBackground },
|
|
850
844
|
createCommentId: (text) => {
|
|
851
|
-
const comment = createReviewerComment(
|
|
845
|
+
const comment = createReviewerComment({
|
|
846
|
+
id: this.nextCommentId(),
|
|
847
|
+
text,
|
|
848
|
+
author: this.author,
|
|
849
|
+
date: revisionStamp?.date
|
|
850
|
+
});
|
|
852
851
|
this.createdComments.push(comment);
|
|
853
852
|
return comment.id;
|
|
854
853
|
},
|
|
@@ -1080,7 +1079,8 @@ var FolioDocxReviewer = class FolioDocxReviewer {
|
|
|
1080
1079
|
const reply = createReply([...this.baseDocument.package.document.comments ?? [], ...this.createdComments], parentId, {
|
|
1081
1080
|
author: input.author ?? this.author,
|
|
1082
1081
|
text: input.text,
|
|
1083
|
-
...input.initials !== void 0 ? { initials: input.initials } : {}
|
|
1082
|
+
...input.initials !== void 0 ? { initials: input.initials } : {},
|
|
1083
|
+
...input.date !== void 0 && { date: input.date }
|
|
1084
1084
|
});
|
|
1085
1085
|
if (!reply) return null;
|
|
1086
1086
|
this.createdComments.push(reply);
|
|
@@ -1344,12 +1344,21 @@ var FolioDocxReviewer = class FolioDocxReviewer {
|
|
|
1344
1344
|
structuralChange ||= hasStructuralChanges(entry.state);
|
|
1345
1345
|
untrackedChanges ||= hasUntrackedChanges(entry.state);
|
|
1346
1346
|
}
|
|
1347
|
+
let path = {
|
|
1348
|
+
type: "selective-first",
|
|
1349
|
+
changedParaIds
|
|
1350
|
+
};
|
|
1351
|
+
if (structuralChange) path = {
|
|
1352
|
+
type: "full-repack",
|
|
1353
|
+
reason: "structuralChange"
|
|
1354
|
+
};
|
|
1355
|
+
else if (untrackedChanges) path = {
|
|
1356
|
+
type: "full-repack",
|
|
1357
|
+
reason: "untrackedChange"
|
|
1358
|
+
};
|
|
1347
1359
|
return {
|
|
1348
1360
|
document: this.documentFromStateSnapshot(snapshot),
|
|
1349
|
-
path
|
|
1350
|
-
type: "selective-first",
|
|
1351
|
-
changedParaIds
|
|
1352
|
-
},
|
|
1361
|
+
path,
|
|
1353
1362
|
changedNoteParaIds,
|
|
1354
1363
|
sectionReferenceRemovals: snapshot.sectionReferenceRemovals,
|
|
1355
1364
|
sectionEndpointRemoval: getTrackedSectionEndpointRemoval(snapshot.mainState),
|
|
@@ -1363,12 +1372,30 @@ var FolioDocxReviewer = class FolioDocxReviewer {
|
|
|
1363
1372
|
* structural edits — the same two-tier path the editor's save uses.
|
|
1364
1373
|
*/
|
|
1365
1374
|
async toBuffer() {
|
|
1375
|
+
const result = await this.save();
|
|
1376
|
+
if (result.type === "repackRefused") return panic("A save that allows a full repack refused to repack", { result });
|
|
1377
|
+
return result.buffer;
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Serialise like {@link toBuffer} and report which path wrote the package.
|
|
1381
|
+
* With `repack: "refuse"`, a save the selective patch cannot express
|
|
1382
|
+
* returns the reason instead of rewriting every part.
|
|
1383
|
+
*/
|
|
1384
|
+
async save({ repack: repackPolicy = "allow" } = {}) {
|
|
1366
1385
|
const save = this.captureSaveSnapshot();
|
|
1367
1386
|
const selective = save.path.type === "selective-first" ? await this.trySelectiveSave(save.document, save.path.changedParaIds) : null;
|
|
1368
1387
|
if (selective) {
|
|
1369
1388
|
await this.assertResolvedStoriesSerialized(selective, save.resolvedStoryExpectations);
|
|
1370
|
-
return
|
|
1389
|
+
return {
|
|
1390
|
+
type: "selective",
|
|
1391
|
+
buffer: selective
|
|
1392
|
+
};
|
|
1371
1393
|
}
|
|
1394
|
+
const reason = save.path.type === "full-repack" ? save.path.reason : "selectiveDeclined";
|
|
1395
|
+
if (repackPolicy === "refuse") return {
|
|
1396
|
+
type: "repackRefused",
|
|
1397
|
+
reason
|
|
1398
|
+
};
|
|
1372
1399
|
const repackDocument = {
|
|
1373
1400
|
...save.document,
|
|
1374
1401
|
originalBuffer: this.originalBuffer
|
|
@@ -1385,7 +1412,11 @@ var FolioDocxReviewer = class FolioDocxReviewer {
|
|
|
1385
1412
|
repack: repackReferences
|
|
1386
1413
|
}) : await repackReferences();
|
|
1387
1414
|
await this.assertResolvedStoriesSerialized(buffer, save.resolvedStoryExpectations);
|
|
1388
|
-
return
|
|
1415
|
+
return {
|
|
1416
|
+
type: "full-repack",
|
|
1417
|
+
reason,
|
|
1418
|
+
buffer
|
|
1419
|
+
};
|
|
1389
1420
|
}
|
|
1390
1421
|
async assertResolvedStoriesSerialized(buffer, expectations) {
|
|
1391
1422
|
if (expectations.length === 0) return;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { WordDiffSegment } from "./word-diff.js";
|
|
2
|
+
//#region src/ai-edits/minimal-replacement.d.ts
|
|
3
|
+
/** Replace `source.slice(start, end)` with `text`; offsets index the source span. */
|
|
4
|
+
type TextChange = {
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
text: string;
|
|
8
|
+
};
|
|
9
|
+
/** A clean-text span that can only be replaced whole (a field result). */
|
|
10
|
+
type AtomicTextSpan = {
|
|
11
|
+
offset: number;
|
|
12
|
+
length: number;
|
|
13
|
+
};
|
|
14
|
+
/** Common prefix length that never ends between the halves of a surrogate pair. */
|
|
15
|
+
declare const commonPrefixLength: (a: string, b: string) => number;
|
|
16
|
+
/** Common suffix length that never starts between the halves of a surrogate pair. */
|
|
17
|
+
declare const commonSuffixLength: (a: string, b: string) => number;
|
|
18
|
+
/**
|
|
19
|
+
* Edits beyond which a replacement is treated as one rewrite. Bounds the diff
|
|
20
|
+
* at O((N + M) * D) time and O(D^2) memory; a rewrite that large keeps
|
|
21
|
+
* nothing of its source worth aligning to anyway.
|
|
22
|
+
*/
|
|
23
|
+
declare const MAX_TOKEN_EDITS = 1024;
|
|
24
|
+
/**
|
|
25
|
+
* The shortest edit script between two token sequences (Myers, 1986), as
|
|
26
|
+
* segments of joined token text, or `null` past `maxEdits` edits.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately the shortest one. The redline diff in `word-diff.ts` gives up
|
|
29
|
+
* short matches between changes so a reader sees whole phrases replaced; an
|
|
30
|
+
* edit applied directly has no reader to serve, and every token it gives up is
|
|
31
|
+
* text rewritten with someone else's formatting.
|
|
32
|
+
*/
|
|
33
|
+
declare const shortestTokenDiff: (before: readonly string[], after: readonly string[], maxEdits?: number) => WordDiffSegment[] | null;
|
|
34
|
+
/**
|
|
35
|
+
* The changes from `source` to `replacement`, in source order and disjoint.
|
|
36
|
+
*
|
|
37
|
+
* Words (with their leading whitespace, and edge punctuation on its own; see
|
|
38
|
+
* `tokenizeWords`) are aligned by the shortest edit script, and each changed
|
|
39
|
+
* stretch is then trimmed to the characters that actually differ, so
|
|
40
|
+
* appending a letter to a word that spans two runs leaves both runs alone. A
|
|
41
|
+
* rewrite past {@link MAX_TOKEN_EDITS}, or a diff that does not reconstruct
|
|
42
|
+
* both strings exactly (it never should), is one change over the whole span,
|
|
43
|
+
* trimmed the same way.
|
|
44
|
+
*/
|
|
45
|
+
declare const planTextChanges: (source: string, replacement: string) => TextChange[];
|
|
46
|
+
/**
|
|
47
|
+
* The changes a diff's segments describe, in source order and disjoint: each
|
|
48
|
+
* run of `del` and `ins` segments between two `equal` ones is one change,
|
|
49
|
+
* exactly as wide as the segments say. A redline's segments come cut to the
|
|
50
|
+
* granularity its reader asked for, so they are not trimmed further.
|
|
51
|
+
*/
|
|
52
|
+
declare const changesFromSegments: (segments: readonly WordDiffSegment[]) => TextChange[];
|
|
53
|
+
/**
|
|
54
|
+
* `changes` widened so none cuts into an atomic span, then merged where the
|
|
55
|
+
* widening made two overlap. A widened change re-emits the source text it
|
|
56
|
+
* absorbed, so applying the result still yields exactly the replacement.
|
|
57
|
+
*
|
|
58
|
+
* `spans` are in the same coordinates as the changes (the source span's).
|
|
59
|
+
*/
|
|
60
|
+
declare const widenChangesToAtomicSpans: (source: string, changes: readonly TextChange[], spans: readonly AtomicTextSpan[]) => TextChange[];
|
|
61
|
+
/** `source` with `changes` applied; the invariant the planner owes. */
|
|
62
|
+
declare const applyTextChanges: (source: string, changes: readonly TextChange[]) => string;
|
|
63
|
+
//#endregion
|
|
64
|
+
export { AtomicTextSpan, MAX_TOKEN_EDITS, TextChange, applyTextChanges, changesFromSegments, commonPrefixLength, commonSuffixLength, planTextChanges, shortestTokenDiff, widenChangesToAtomicSpans };
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { tokenizeWords } from "./word-diff.js";
|
|
2
|
+
//#region src/ai-edits/minimal-replacement.ts
|
|
3
|
+
/**
|
|
4
|
+
* The smallest set of text changes that turns one clean-text span into another.
|
|
5
|
+
*
|
|
6
|
+
* A direct-mode replacement (`replaceInBlock`, `replaceRange`, `replaceBlock`)
|
|
7
|
+
* names the text it matched and the text it wants. Swapping the whole match
|
|
8
|
+
* gives every character of the result the formatting of the first one, drops
|
|
9
|
+
* the inline content controls, note references and fields the match crossed,
|
|
10
|
+
* and makes a one-character edit rewrite the paragraph. The edit is the
|
|
11
|
+
* difference between the two strings, so that is all the applier writes:
|
|
12
|
+
*
|
|
13
|
+
* - Text outside a change is not touched: its runs, marks and every inline node
|
|
14
|
+
* between its characters stay exactly as they were.
|
|
15
|
+
* - A change deletes only the characters it removes (text, tabs, breaks and
|
|
16
|
+
* whole field results). Non-text inline content between those characters —
|
|
17
|
+
* bookmarks, comment anchors, drawings, content-control boundaries, rendered
|
|
18
|
+
* page breaks — stays where it was; the new text is written where the first
|
|
19
|
+
* removed character stood and takes its formatting.
|
|
20
|
+
* - A pure insertion follows the character before it, with the formatting that
|
|
21
|
+
* character has, the way typed text does. At the start of the matched span
|
|
22
|
+
* there is no such character inside the match, so it takes the formatting of
|
|
23
|
+
* the first matched character. A note reference's number lends no
|
|
24
|
+
* formatting: prose beside it is not superscript.
|
|
25
|
+
* - An insertion at the edge of an inline content control (`w:sdt`), a link or
|
|
26
|
+
* a comment range stays inside it only when the whole match lies inside it.
|
|
27
|
+
* Matching a control's or a link's text edits it; appending to a paragraph
|
|
28
|
+
* that ends in a checkbox or a link writes after it.
|
|
29
|
+
* - A field result is one unit: a change that would cut into it is widened to
|
|
30
|
+
* the whole result, which the new text then replaces as plain text (a result
|
|
31
|
+
* cannot be partly edited; it is regenerated from the field code). A field
|
|
32
|
+
* whose displayed text the replacement keeps is never touched. A match that
|
|
33
|
+
* itself starts or ends inside a field result is refused as before
|
|
34
|
+
* (`unsupportedBlock`).
|
|
35
|
+
*
|
|
36
|
+
* A replacement carrying inline emphasis markup (`**bold**`) states its own
|
|
37
|
+
* formatting and still replaces the match whole in direct mode.
|
|
38
|
+
*
|
|
39
|
+
* Tracked-changes and suggested modes cut their changes from the redline diff
|
|
40
|
+
* instead ({@link changesFromSegments}: word or character granularity, as the
|
|
41
|
+
* caller asked), and map them onto the document by the same rules. A change
|
|
42
|
+
* marks exactly the characters it removes as a deletion, which keeps their
|
|
43
|
+
* runs, and writes its text as an insertion after them, formatted as the
|
|
44
|
+
* first removed character that is not a note reference; a pure insertion is
|
|
45
|
+
* placed and formatted as above. Nothing outside a change is wrapped in a
|
|
46
|
+
* revision.
|
|
47
|
+
*
|
|
48
|
+
* This module is the pure half: clean-text offsets in, changes out. The
|
|
49
|
+
* applier maps each change onto the document.
|
|
50
|
+
*/
|
|
51
|
+
const isHighSurrogate = (code) => code >= 55296 && code <= 56319;
|
|
52
|
+
const isLowSurrogate = (code) => code >= 56320 && code <= 57343;
|
|
53
|
+
/** Common prefix length that never ends between the halves of a surrogate pair. */
|
|
54
|
+
const commonPrefixLength = (a, b) => {
|
|
55
|
+
const limit = Math.min(a.length, b.length);
|
|
56
|
+
let index = 0;
|
|
57
|
+
while (index < limit && a.charCodeAt(index) === b.charCodeAt(index)) index++;
|
|
58
|
+
return index > 0 && isHighSurrogate(a.charCodeAt(index - 1)) ? index - 1 : index;
|
|
59
|
+
};
|
|
60
|
+
/** Common suffix length that never starts between the halves of a surrogate pair. */
|
|
61
|
+
const commonSuffixLength = (a, b) => {
|
|
62
|
+
const limit = Math.min(a.length, b.length);
|
|
63
|
+
let length = 0;
|
|
64
|
+
while (length < limit && a.charCodeAt(a.length - 1 - length) === b.charCodeAt(b.length - 1 - length)) length++;
|
|
65
|
+
return length > 0 && isLowSurrogate(a.charCodeAt(a.length - length)) ? length - 1 : length;
|
|
66
|
+
};
|
|
67
|
+
/** `change` without the characters its deleted and inserted text share at either end. */
|
|
68
|
+
const trimChange = (source, change) => {
|
|
69
|
+
const removed = source.slice(change.start, change.end);
|
|
70
|
+
const prefix = commonPrefixLength(removed, change.text);
|
|
71
|
+
const suffix = commonSuffixLength(removed.slice(prefix), change.text.slice(prefix));
|
|
72
|
+
const trimmed = {
|
|
73
|
+
start: change.start + prefix,
|
|
74
|
+
end: change.end - suffix,
|
|
75
|
+
text: change.text.slice(prefix, change.text.length - suffix)
|
|
76
|
+
};
|
|
77
|
+
return trimmed.start === trimmed.end && trimmed.text.length === 0 ? null : trimmed;
|
|
78
|
+
};
|
|
79
|
+
const reconstructs = (segments, source, replacement) => {
|
|
80
|
+
let before = "";
|
|
81
|
+
let after = "";
|
|
82
|
+
for (const segment of segments) {
|
|
83
|
+
if (segment.type !== "ins") before += segment.text;
|
|
84
|
+
if (segment.type !== "del") after += segment.text;
|
|
85
|
+
}
|
|
86
|
+
return before === source && after === replacement;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Edits beyond which a replacement is treated as one rewrite. Bounds the diff
|
|
90
|
+
* at O((N + M) * D) time and O(D^2) memory; a rewrite that large keeps
|
|
91
|
+
* nothing of its source worth aligning to anyway.
|
|
92
|
+
*/
|
|
93
|
+
const MAX_TOKEN_EDITS = 1024;
|
|
94
|
+
/**
|
|
95
|
+
* The shortest edit script between two token sequences (Myers, 1986), as
|
|
96
|
+
* segments of joined token text, or `null` past `maxEdits` edits.
|
|
97
|
+
*
|
|
98
|
+
* Deliberately the shortest one. The redline diff in `word-diff.ts` gives up
|
|
99
|
+
* short matches between changes so a reader sees whole phrases replaced; an
|
|
100
|
+
* edit applied directly has no reader to serve, and every token it gives up is
|
|
101
|
+
* text rewritten with someone else's formatting.
|
|
102
|
+
*/
|
|
103
|
+
const shortestTokenDiff = (before, after, maxEdits = MAX_TOKEN_EDITS) => {
|
|
104
|
+
const n = before.length;
|
|
105
|
+
const m = after.length;
|
|
106
|
+
const limit = Math.min(n + m, maxEdits);
|
|
107
|
+
const offset = limit + 1;
|
|
108
|
+
const frontier = new Int32Array(2 * limit + 3);
|
|
109
|
+
const trace = [];
|
|
110
|
+
let edits = -1;
|
|
111
|
+
search: for (let d = 0; d <= limit; d++) {
|
|
112
|
+
trace.push(frontier.slice(offset - d, offset + d + 1));
|
|
113
|
+
for (let k = -d; k <= d; k += 2) {
|
|
114
|
+
let x = k === -d || k !== d && frontier[offset + k - 1] < frontier[offset + k + 1] ? frontier[offset + k + 1] : frontier[offset + k - 1] + 1;
|
|
115
|
+
let y = x - k;
|
|
116
|
+
while (x < n && y < m && before[x] === after[y]) {
|
|
117
|
+
x++;
|
|
118
|
+
y++;
|
|
119
|
+
}
|
|
120
|
+
frontier[offset + k] = x;
|
|
121
|
+
if (x >= n && y >= m) {
|
|
122
|
+
edits = d;
|
|
123
|
+
break search;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (edits < 0) return null;
|
|
128
|
+
const reversed = [];
|
|
129
|
+
const push = (type, text) => {
|
|
130
|
+
const last = reversed.at(-1);
|
|
131
|
+
if (last?.type === type) last.text = text + last.text;
|
|
132
|
+
else reversed.push({
|
|
133
|
+
type,
|
|
134
|
+
text
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
let x = n;
|
|
138
|
+
let y = m;
|
|
139
|
+
for (let d = edits; d > 0; d--) {
|
|
140
|
+
const previous = trace[d];
|
|
141
|
+
const at = (diagonal) => previous[diagonal + d];
|
|
142
|
+
const k = x - y;
|
|
143
|
+
const down = k === -d || k !== d && at(k - 1) < at(k + 1);
|
|
144
|
+
const previousK = down ? k + 1 : k - 1;
|
|
145
|
+
const previousX = at(previousK);
|
|
146
|
+
const previousY = previousX - previousK;
|
|
147
|
+
while (x > previousX && y > previousY) {
|
|
148
|
+
x--;
|
|
149
|
+
y--;
|
|
150
|
+
push("equal", before[x]);
|
|
151
|
+
}
|
|
152
|
+
if (down) {
|
|
153
|
+
y--;
|
|
154
|
+
push("ins", after[y]);
|
|
155
|
+
} else {
|
|
156
|
+
x--;
|
|
157
|
+
push("del", before[x]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
while (x > 0 && y > 0) {
|
|
161
|
+
x--;
|
|
162
|
+
y--;
|
|
163
|
+
push("equal", before[x]);
|
|
164
|
+
}
|
|
165
|
+
return reversed.toReversed();
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* The changes from `source` to `replacement`, in source order and disjoint.
|
|
169
|
+
*
|
|
170
|
+
* Words (with their leading whitespace, and edge punctuation on its own; see
|
|
171
|
+
* `tokenizeWords`) are aligned by the shortest edit script, and each changed
|
|
172
|
+
* stretch is then trimmed to the characters that actually differ, so
|
|
173
|
+
* appending a letter to a word that spans two runs leaves both runs alone. A
|
|
174
|
+
* rewrite past {@link MAX_TOKEN_EDITS}, or a diff that does not reconstruct
|
|
175
|
+
* both strings exactly (it never should), is one change over the whole span,
|
|
176
|
+
* trimmed the same way.
|
|
177
|
+
*/
|
|
178
|
+
const planTextChanges = (source, replacement) => {
|
|
179
|
+
if (source === replacement) return [];
|
|
180
|
+
const diffed = shortestTokenDiff(tokenizeWords(source), tokenizeWords(replacement));
|
|
181
|
+
const segments = diffed !== null && reconstructs(diffed, source, replacement) ? diffed : [{
|
|
182
|
+
type: "del",
|
|
183
|
+
text: source
|
|
184
|
+
}, {
|
|
185
|
+
type: "ins",
|
|
186
|
+
text: replacement
|
|
187
|
+
}];
|
|
188
|
+
return changesFromSegments(segments).flatMap((change) => trimChange(source, change) ?? []);
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* The changes a diff's segments describe, in source order and disjoint: each
|
|
192
|
+
* run of `del` and `ins` segments between two `equal` ones is one change,
|
|
193
|
+
* exactly as wide as the segments say. A redline's segments come cut to the
|
|
194
|
+
* granularity its reader asked for, so they are not trimmed further.
|
|
195
|
+
*/
|
|
196
|
+
const changesFromSegments = (segments) => {
|
|
197
|
+
const changes = [];
|
|
198
|
+
let cursor = 0;
|
|
199
|
+
let pending = null;
|
|
200
|
+
for (const segment of segments) {
|
|
201
|
+
if (segment.type === "equal") {
|
|
202
|
+
if (pending !== null) {
|
|
203
|
+
changes.push(pending);
|
|
204
|
+
pending = null;
|
|
205
|
+
}
|
|
206
|
+
cursor += segment.text.length;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
pending ??= {
|
|
210
|
+
start: cursor,
|
|
211
|
+
end: cursor,
|
|
212
|
+
text: ""
|
|
213
|
+
};
|
|
214
|
+
if (segment.type === "del") {
|
|
215
|
+
cursor += segment.text.length;
|
|
216
|
+
pending.end = cursor;
|
|
217
|
+
} else pending.text += segment.text;
|
|
218
|
+
}
|
|
219
|
+
if (pending !== null) changes.push(pending);
|
|
220
|
+
return changes;
|
|
221
|
+
};
|
|
222
|
+
const cutsInto = ({ offset, length }, position) => position > offset && position < offset + length;
|
|
223
|
+
/**
|
|
224
|
+
* `changes` widened so none cuts into an atomic span, then merged where the
|
|
225
|
+
* widening made two overlap. A widened change re-emits the source text it
|
|
226
|
+
* absorbed, so applying the result still yields exactly the replacement.
|
|
227
|
+
*
|
|
228
|
+
* `spans` are in the same coordinates as the changes (the source span's).
|
|
229
|
+
*/
|
|
230
|
+
const widenChangesToAtomicSpans = (source, changes, spans) => {
|
|
231
|
+
if (spans.length === 0 || changes.length === 0) return [...changes];
|
|
232
|
+
const windows = changes.map(({ start, end }) => {
|
|
233
|
+
let from = start;
|
|
234
|
+
let to = end;
|
|
235
|
+
for (const span of spans) {
|
|
236
|
+
if (cutsInto(span, from)) from = span.offset;
|
|
237
|
+
if (cutsInto(span, to)) to = span.offset + span.length;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
from,
|
|
241
|
+
to
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
const merged = [];
|
|
245
|
+
let index = 0;
|
|
246
|
+
while (index < changes.length) {
|
|
247
|
+
const { from } = windows[index];
|
|
248
|
+
let { to } = windows[index];
|
|
249
|
+
const group = [changes[index]];
|
|
250
|
+
index++;
|
|
251
|
+
while (index < changes.length && windows[index].from < to) {
|
|
252
|
+
to = Math.max(to, windows[index].to);
|
|
253
|
+
group.push(changes[index]);
|
|
254
|
+
index++;
|
|
255
|
+
}
|
|
256
|
+
let text = "";
|
|
257
|
+
let cursor = from;
|
|
258
|
+
for (const change of group) {
|
|
259
|
+
text += source.slice(cursor, change.start) + change.text;
|
|
260
|
+
cursor = change.end;
|
|
261
|
+
}
|
|
262
|
+
text += source.slice(cursor, to);
|
|
263
|
+
merged.push({
|
|
264
|
+
start: from,
|
|
265
|
+
end: to,
|
|
266
|
+
text
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return merged;
|
|
270
|
+
};
|
|
271
|
+
/** `source` with `changes` applied; the invariant the planner owes. */
|
|
272
|
+
const applyTextChanges = (source, changes) => {
|
|
273
|
+
let result = "";
|
|
274
|
+
let cursor = 0;
|
|
275
|
+
for (const change of changes) {
|
|
276
|
+
result += source.slice(cursor, change.start) + change.text;
|
|
277
|
+
cursor = change.end;
|
|
278
|
+
}
|
|
279
|
+
return result + source.slice(cursor);
|
|
280
|
+
};
|
|
281
|
+
//#endregion
|
|
282
|
+
export { MAX_TOKEN_EDITS, applyTextChanges, changesFromSegments, commonPrefixLength, commonSuffixLength, planTextChanges, shortestTokenDiff, widenChangesToAtomicSpans };
|
|
@@ -230,6 +230,15 @@ function runLayoutPipelineMeasured(deps, state, options) {
|
|
|
230
230
|
preparedFooter: refs.footerEven ? footerContentByRId?.get(refs.footerEven) : void 0
|
|
231
231
|
});
|
|
232
232
|
});
|
|
233
|
+
const sectionFirstPageMargins = sectionHeaderFooterRefs?.map((refs, index) => {
|
|
234
|
+
if (refs.titlePg !== true) return;
|
|
235
|
+
const properties = sectionPropertiesForMargins[index];
|
|
236
|
+
return bodyMarginsClearHeaderFooter({
|
|
237
|
+
authoredMargins: properties ? getMargins(properties) : margins,
|
|
238
|
+
preparedHeader: refs.headerFirst ? headerContentByRId?.get(refs.headerFirst) : void 0,
|
|
239
|
+
preparedFooter: refs.footerFirst ? footerContentByRId?.get(refs.footerFirst) : void 0
|
|
240
|
+
});
|
|
241
|
+
});
|
|
233
242
|
newBlocks = bodyBlocksClearSectionHeaderFooter(newBlocks, {
|
|
234
243
|
authoredMargins: margins,
|
|
235
244
|
sectionHeaderFooterRefs,
|
|
@@ -358,7 +367,8 @@ function runLayoutPipelineMeasured(deps, state, options) {
|
|
|
358
367
|
};
|
|
359
368
|
if (document?.package.document.sections !== void 0) nextLayoutOpts.sectionVerticalAlignments = document.package.document.sections.map(({ properties }) => properties.verticalAlign);
|
|
360
369
|
else if (sectionProperties !== null && sectionProperties !== void 0) nextLayoutOpts.sectionVerticalAlignments = [sectionProperties.verticalAlign];
|
|
361
|
-
if (
|
|
370
|
+
if (sectionFirstPageMargins !== void 0) nextLayoutOpts.sectionFirstPageMargins = sectionFirstPageMargins;
|
|
371
|
+
else if (hasTitlePg) nextLayoutOpts.firstPageMargins = bodyMarginsClearHeaderFooter({
|
|
362
372
|
authoredMargins: margins,
|
|
363
373
|
preparedHeader: firstPageHeaderForRender,
|
|
364
374
|
preparedFooter: firstPageFooterForRender
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getListMarkerInlineWidth, getListMarkerVisualOffset, resolveListMarkerFont } from "../../layout-engine/measure/listMarkerWidth.js";
|
|
2
2
|
import { DEFAULT_FONT_FAMILY, buildRunFontStyle, ptToPx } from "../../layout-engine/measure/measureHelpers.js";
|
|
3
3
|
import { getFontMetrics, measureTextWidth } from "../../layout-engine/measure/measureProvider.js";
|
|
4
|
+
import { SMALL_CAPS_SCALE, smallCapsSegments } from "../../layout-engine/measure/smallCapsCasing.js";
|
|
4
5
|
import { calculateTabWidth, pixelsToTwips } from "../../layout-engine/measure/tabCalculator.js";
|
|
5
6
|
import { countCompressibleSpaces, toPaintedText } from "../../layout-engine/measure/textMeasurementPolicy.js";
|
|
6
7
|
import { isFloatingImageRun } from "../../layout-engine/types.js";
|
|
@@ -315,6 +316,39 @@ const runGlyphDirection = (run, paragraphIsRtl) => run.bidiWrapper?.direction ??
|
|
|
315
316
|
*/
|
|
316
317
|
const runUnicodeBidi = (run) => run.bidiWrapper === void 0 ? void 0 : UNICODE_BIDI_BY_WRAPPER_CONTROL[run.bidiWrapper.control];
|
|
317
318
|
/**
|
|
319
|
+
* `glyphs` split into same-size stretches for a `w:smallCaps` run, or
|
|
320
|
+
* `undefined` when it has no lowercase letter (a heading typed in caps, a run
|
|
321
|
+
* of digits and punctuation): that run paints as the single glyph run it
|
|
322
|
+
* always did, no backend work spent on a split it does not need.
|
|
323
|
+
*
|
|
324
|
+
* Every backend draws one glyph size per `glyphRun` (a PDF's `Tf` sets the
|
|
325
|
+
* whole text-showing operation's size; a DOM span sets one `font-size`), so a
|
|
326
|
+
* mixed-size run becomes a short run of primitives instead of one primitive
|
|
327
|
+
* with glyphs a backend cannot all draw at the size it is told. `advancesPx`
|
|
328
|
+
* is sliced from the already-measured run — the widths line breaking decided
|
|
329
|
+
* on — never re-measured, so the split cannot disagree with the width the
|
|
330
|
+
* line was fitted at.
|
|
331
|
+
*/
|
|
332
|
+
const smallCapsGlyphRunSegments = (glyphs, fontSizePx) => {
|
|
333
|
+
const segments = smallCapsSegments(glyphs.text);
|
|
334
|
+
if (!segments.some((segment) => segment.small)) return;
|
|
335
|
+
const smallFontSizePx = fontSizePx * SMALL_CAPS_SCALE;
|
|
336
|
+
const result = [];
|
|
337
|
+
let advanceIndex = 0;
|
|
338
|
+
for (const segment of segments) {
|
|
339
|
+
const codePointCount = [...segment.text].length;
|
|
340
|
+
const advancesPx = glyphs.advancesPx.slice(advanceIndex, advanceIndex + codePointCount);
|
|
341
|
+
advanceIndex += codePointCount;
|
|
342
|
+
result.push({
|
|
343
|
+
text: segment.text,
|
|
344
|
+
fontSizePx: segment.small ? smallFontSizePx : fontSizePx,
|
|
345
|
+
advancesPx,
|
|
346
|
+
widthPx: advancesPx.reduce((sum, advance) => sum + advance, 0)
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return result;
|
|
350
|
+
};
|
|
351
|
+
/**
|
|
318
352
|
* One `glyphRun` plus everything painted around it: the run's background rect
|
|
319
353
|
* first, then the glyphs, then the decorations whose geometry CSS would have
|
|
320
354
|
* derived from the font.
|
|
@@ -380,20 +414,43 @@ const emitGlyphRun = ({ sink, context, run, glyphs, style, paintXPx, baselineYPx
|
|
|
380
414
|
pattern: "solid"
|
|
381
415
|
} : void 0;
|
|
382
416
|
const unicodeBidi = runUnicodeBidi(run);
|
|
383
|
-
|
|
384
|
-
|
|
417
|
+
const direction = runGlyphDirection(run, isRtl);
|
|
418
|
+
const commonFields = {
|
|
385
419
|
font,
|
|
386
|
-
fontSizePx,
|
|
387
420
|
color,
|
|
388
|
-
xPx: paintXPx,
|
|
389
421
|
baselineYPx: runBaselineYPx,
|
|
390
|
-
|
|
391
|
-
direction: runGlyphDirection(run, isRtl),
|
|
422
|
+
direction,
|
|
392
423
|
...unicodeBidi === void 0 ? {} : { unicodeBidi },
|
|
393
424
|
...stroke === void 0 ? {} : { stroke },
|
|
394
425
|
...pmRange === void 0 ? {} : { pmRange },
|
|
395
426
|
...collapsedEdge === void 0 ? {} : { collapsedEdge }
|
|
427
|
+
};
|
|
428
|
+
const smallCapsRuns = style.fontVariant === "small-caps" && direction === "ltr" ? smallCapsGlyphRunSegments(glyphs, fontSizePx) : void 0;
|
|
429
|
+
if (smallCapsRuns === void 0) sink.glyphs.push({
|
|
430
|
+
kind: "glyphRun",
|
|
431
|
+
fontSizePx,
|
|
432
|
+
xPx: paintXPx,
|
|
433
|
+
...glyphRunText(glyphs),
|
|
434
|
+
...commonFields
|
|
396
435
|
});
|
|
436
|
+
else {
|
|
437
|
+
const adjustments = glyphs.adjustments === void 0 ? {} : { adjustments: glyphs.adjustments };
|
|
438
|
+
let segmentXPx = paintXPx;
|
|
439
|
+
for (const segment of smallCapsRuns) {
|
|
440
|
+
sink.glyphs.push({
|
|
441
|
+
kind: "glyphRun",
|
|
442
|
+
fontSizePx: segment.fontSizePx,
|
|
443
|
+
xPx: segmentXPx,
|
|
444
|
+
text: segment.text,
|
|
445
|
+
advancesPx: segment.advancesPx,
|
|
446
|
+
kerning: glyphs.kerning,
|
|
447
|
+
smallCaps: true,
|
|
448
|
+
...adjustments,
|
|
449
|
+
...commonFields
|
|
450
|
+
});
|
|
451
|
+
segmentXPx += segment.widthPx;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
397
454
|
emitDecorations({
|
|
398
455
|
sink,
|
|
399
456
|
run,
|
|
@@ -506,7 +563,6 @@ const emitDecorations = ({ sink, run, color, context, xPx, glyphs, baselineYPx,
|
|
|
506
563
|
*/
|
|
507
564
|
const reportRunEffects = (run, context) => {
|
|
508
565
|
const { unsupported, pageIndex } = context;
|
|
509
|
-
if (run.smallCaps) unsupported.report(UNSUPPORTED_CONSTRUCT.smallCaps, pageIndex, "w:smallCaps is carried on the run and painted by the DOM backend; the PDF backend paints full-size glyphs for it");
|
|
510
566
|
if (getHorizontalScaleFactor(run.horizontalScale) !== 1) unsupported.report(UNSUPPORTED_CONSTRUCT.horizontalScale, pageIndex, `w:w ${String(run.horizontalScale)}% is carried on the run and painted by the DOM backend; the PDF backend paints unnarrowed glyphs at the measured advances`);
|
|
511
567
|
if (run.textEffect) unsupported.report(UNSUPPORTED_CONSTRUCT.textEffect, pageIndex, `w:effect ${run.textEffect}`);
|
|
512
568
|
if (run.emphasisMark) unsupported.report(UNSUPPORTED_CONSTRUCT.emphasisMark, pageIndex, `w:em ${run.emphasisMark}`);
|
|
@@ -64,7 +64,7 @@ const paintUnrotatedTextBoxFragment = ({ composer, fragment, block, measure, con
|
|
|
64
64
|
fill
|
|
65
65
|
});
|
|
66
66
|
else context.unsupported.report(UNSUPPORTED_CONSTRUCT.unresolvedColor, context.pageIndex, `text box fill ${block.fillColor}`);
|
|
67
|
-
}
|
|
67
|
+
} else if (block.fillGradient) context.unsupported.report(UNSUPPORTED_CONSTRUCT.textBoxGradientFill, context.pageIndex, "text box gradient fill");
|
|
68
68
|
let outlineWidthPx = 0;
|
|
69
69
|
const outlineDash = presetDashForOutlineAttr(block.outlineStyle) ?? "solid";
|
|
70
70
|
if (block.outlineWidth !== void 0 && block.outlineWidth > 0 && outlineDash !== "none") {
|