@stll/folio-core 0.33.2 → 0.34.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 +137 -23
- package/dist/ai-edits/snapshot.js +24 -2
- package/dist/ai-edits/types.d.ts +6 -6
- package/dist/compare/formatting.d.ts +1 -1
- package/dist/compare/formatting.js +38 -9
- package/dist/compare/verification.js +18 -1
- package/dist/controller/headerFooterEditorManager.js +11 -9
- package/dist/controller/layoutPipeline.js +24 -3
- package/dist/display-list/build/watermarkPrimitives.js +15 -3
- package/dist/document-operations.js +30 -3
- package/dist/docx/headerFooterParser.js +8 -14
- package/dist/docx/paragraphParser.js +45 -0
- package/dist/docx/serializer/headerFooterSerializer.js +21 -2
- package/dist/docx/serializer/paragraphSerializer.d.ts +1 -1
- package/dist/docx/serializer/paragraphSerializer.js +19 -9
- package/dist/docx/settingsParser.js +3 -0
- package/dist/docx/watermarkParser.d.ts +2 -4
- package/dist/docx/watermarkParser.js +4 -6
- package/dist/headless-layout.js +14 -2
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -2
- package/dist/layout-bridge/convert/footnoteLayout.js +23 -10
- package/dist/layout-bridge/convert/headerFooterLayout.js +13 -2
- package/dist/layout-bridge/convert/toFlowBlocks.js +96 -13
- package/dist/layout-engine/index.js +11 -4
- package/dist/layout-engine/justifiedLineFit.d.ts +4 -4
- package/dist/layout-engine/justifiedLineFit.js +4 -4
- package/dist/layout-engine/measure/lineBreakProvider.js +1 -0
- package/dist/layout-engine/measure/measureBlocks.js +1 -1
- package/dist/layout-engine/measure/measureParagraph.js +29 -29
- package/dist/layout-painter/renderPage.js +6 -1
- package/dist/layout-painter/renderParagraph.js +18 -8
- package/dist/layout-painter/renderWatermark.js +11 -4
- package/dist/prosemirror/attrs/index.js +11 -0
- package/dist/prosemirror/commands/comments.js +22 -2
- package/dist/prosemirror/conversion/fromProseDoc.js +32 -5
- package/dist/prosemirror/conversion/toProseDoc.js +49 -11
- package/dist/prosemirror/extensions/core/DocExtension.js +7 -1
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
- package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +1 -0
- package/dist/prosemirror/schema/marks.d.ts +2 -8
- package/dist/utils/fontResolver.js +51 -0
- package/dist/utils/formatToStyle.js +41 -1
- package/dist/watermark/index.js +7 -0
- package/package.json +2 -2
package/dist/ai-edits/apply.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js";
|
|
1
|
+
import { expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js";
|
|
2
2
|
import { PPR_CHANGE_SCOPED_ATTR_KEYS } from "../prosemirror/commands/propertyChangeScope.js";
|
|
3
3
|
import { addedBreakCarrierBefore, finalParagraphsOf, paragraphEndsItsContainer } from "../prosemirror/containerFinalParagraph.js";
|
|
4
4
|
import { marksToTextFormatting } from "../prosemirror/conversion/fromProseDoc.js";
|
|
@@ -154,24 +154,132 @@ const REPLACEMENT_BACKGROUND_CLEAR_FORMATTING = {
|
|
|
154
154
|
highlight: false,
|
|
155
155
|
runShading: false
|
|
156
156
|
};
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
const INLINE_FORMATTING_MARK_NAMES = {
|
|
158
|
+
bold: "bold",
|
|
159
|
+
italic: "italic",
|
|
160
|
+
underline: "underline",
|
|
161
|
+
strike: "strike",
|
|
162
|
+
fontFamily: "fontFamily",
|
|
163
|
+
fontSizePt: "fontSize",
|
|
164
|
+
color: "textColor",
|
|
165
|
+
highlight: "highlight",
|
|
166
|
+
runShading: "runShading"
|
|
167
|
+
};
|
|
168
|
+
const DIRECT_FONT_PROPERTIES = [
|
|
169
|
+
"fontFamily",
|
|
170
|
+
"fontSize",
|
|
171
|
+
"color"
|
|
172
|
+
];
|
|
173
|
+
const formattingMarkName = (property) => {
|
|
174
|
+
if (!Object.hasOwn(INLINE_FORMATTING_MARK_NAMES, property)) return null;
|
|
175
|
+
const name = Reflect.get(INLINE_FORMATTING_MARK_NAMES, property);
|
|
176
|
+
return typeof name === "string" ? name : null;
|
|
177
|
+
};
|
|
178
|
+
const formattingMarkAttrs = (property, value) => {
|
|
179
|
+
switch (property) {
|
|
180
|
+
case "underline": return { style: "single" };
|
|
181
|
+
case "fontFamily": return typeof value === "string" ? {
|
|
182
|
+
ascii: value,
|
|
183
|
+
hAnsi: value
|
|
184
|
+
} : null;
|
|
185
|
+
case "fontSizePt": return typeof value === "number" ? { size: value * 2 } : null;
|
|
186
|
+
case "color": return typeof value === "string" ? { rgb: value.replace(/^#/u, "").toUpperCase() } : null;
|
|
187
|
+
default: return {};
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
const applyInlineFormatting = ({ tr, schema, from, to, formatting, includedProperties }) => {
|
|
191
|
+
for (const [property, value] of Object.entries(formatting)) {
|
|
192
|
+
if (includedProperties && !includedProperties.includes(property)) continue;
|
|
193
|
+
const markName = formattingMarkName(property);
|
|
194
|
+
const markType = markName ? schema.marks[markName] : void 0;
|
|
160
195
|
if (!markType) continue;
|
|
161
|
-
if (
|
|
162
|
-
|
|
196
|
+
if (value !== false && value !== null) {
|
|
197
|
+
const attrs = formattingMarkAttrs(property, value);
|
|
198
|
+
if (attrs) tr.addMark(from, to, markType.create(attrs));
|
|
163
199
|
continue;
|
|
164
200
|
}
|
|
165
201
|
tr.removeMark(from, to, markType);
|
|
166
202
|
}
|
|
203
|
+
applyDirectFontProvenance({
|
|
204
|
+
tr,
|
|
205
|
+
schema,
|
|
206
|
+
from,
|
|
207
|
+
to,
|
|
208
|
+
formatting,
|
|
209
|
+
...includedProperties ? { includedProperties } : {}
|
|
210
|
+
});
|
|
167
211
|
return tr;
|
|
168
212
|
};
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
213
|
+
const applyDirectFontProvenance = ({ tr, schema, from, to, formatting, includedProperties }) => {
|
|
214
|
+
if ("highlight" in formatting) return;
|
|
215
|
+
const changed = [
|
|
216
|
+
[
|
|
217
|
+
"fontFamily",
|
|
218
|
+
"fontFamily",
|
|
219
|
+
formatting.fontFamily
|
|
220
|
+
],
|
|
221
|
+
[
|
|
222
|
+
"fontSize",
|
|
223
|
+
"fontSizePt",
|
|
224
|
+
formatting.fontSizePt
|
|
225
|
+
],
|
|
226
|
+
[
|
|
227
|
+
"color",
|
|
228
|
+
"color",
|
|
229
|
+
formatting.color
|
|
230
|
+
]
|
|
231
|
+
].filter(([, inputProperty, value]) => value !== void 0 && (!includedProperties || includedProperties.includes(inputProperty)));
|
|
232
|
+
const markType = schema.marks["runFormattingOverride"];
|
|
233
|
+
if (!markType || changed.length === 0) return;
|
|
234
|
+
tr.doc.nodesBetween(from, to, (node, pos) => {
|
|
235
|
+
if (!node.isText) return;
|
|
236
|
+
const segmentFrom = Math.max(from, pos);
|
|
237
|
+
const segmentTo = Math.min(to, pos + node.nodeSize);
|
|
238
|
+
const existing = node.marks.find((mark) => mark.type === markType);
|
|
239
|
+
const attrs = existing ? expectRunFormattingOverrideMarkAttrs(existing) : {};
|
|
240
|
+
const directFontProperties = new Set(attrs.directFontProperties);
|
|
241
|
+
for (const [property, , value] of changed) if (value === null) directFontProperties.delete(property);
|
|
242
|
+
else directFontProperties.add(property);
|
|
243
|
+
tr.removeMark(segmentFrom, segmentTo, markType);
|
|
244
|
+
const nextAttrs = {
|
|
245
|
+
...attrs,
|
|
246
|
+
directFontProperties: DIRECT_FONT_PROPERTIES.filter((property) => directFontProperties.has(property))
|
|
247
|
+
};
|
|
248
|
+
if (Object.entries(nextAttrs).some(([key, value]) => key === "directFontProperties" ? Array.isArray(value) && value.length > 0 : value !== null && value !== void 0)) tr.addMark(segmentFrom, segmentTo, markType.create(nextAttrs));
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
const directFontPropertyForFormattingProperty = (property) => {
|
|
252
|
+
switch (property) {
|
|
253
|
+
case "fontFamily": return "fontFamily";
|
|
254
|
+
case "fontSizePt": return "fontSize";
|
|
255
|
+
case "color": return "color";
|
|
256
|
+
default: return null;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const formattingPropertyWouldChange = (marks, property, value) => {
|
|
260
|
+
const markName = formattingMarkName(property);
|
|
261
|
+
const mark = marks.find((candidate) => candidate.type.name === markName);
|
|
262
|
+
const directFontProperty = directFontPropertyForFormattingProperty(property);
|
|
263
|
+
if (directFontProperty) {
|
|
264
|
+
const overrideMark = marks.find((candidate) => candidate.type.name === "runFormattingOverride");
|
|
265
|
+
const isDirect = overrideMark ? (expectRunFormattingOverrideMarkAttrs(overrideMark).directFontProperties ?? []).includes(directFontProperty) : false;
|
|
266
|
+
if (value === false || value === null) return isDirect;
|
|
267
|
+
if (!isDirect) return true;
|
|
268
|
+
} else if (value === false || value === null) return mark !== void 0;
|
|
269
|
+
if (property === "underline") return mark?.attrs["style"] !== "single";
|
|
270
|
+
if (property === "fontFamily") return mark?.attrs["ascii"] !== value || mark?.attrs["hAnsi"] !== value;
|
|
271
|
+
if (property === "fontSizePt") return Number(mark?.attrs["size"]) !== Number(value) * 2;
|
|
272
|
+
if (property === "color") {
|
|
273
|
+
const current = mark?.attrs["rgb"];
|
|
274
|
+
return (typeof current === "string" ? current.replace(/^#/u, "").toUpperCase() : null) !== String(value).replace(/^#/u, "").toUpperCase();
|
|
275
|
+
}
|
|
173
276
|
return mark === void 0;
|
|
174
|
-
}
|
|
277
|
+
};
|
|
278
|
+
const formattingChangesForMarks = (marks, formatting) => {
|
|
279
|
+
const changes = [];
|
|
280
|
+
for (const [property, value] of Object.entries(formatting)) if (formattingPropertyWouldChange(marks, property, value)) changes.push(property);
|
|
281
|
+
return changes;
|
|
282
|
+
};
|
|
175
283
|
const clearReplacementBackground = ({ tr, schema, from, to, mode, revisionId, author, date, initials, suggestionId = null }) => {
|
|
176
284
|
if (![schema.marks["highlight"], schema.marks["runShading"]].some((markType) => markType !== void 0 && tr.doc.rangeHasMark(from, to, markType))) return tr;
|
|
177
285
|
if (mode === "direct") return applyInlineFormatting({
|
|
@@ -200,7 +308,9 @@ const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, r
|
|
|
200
308
|
if (!propertyChangeType) return tr;
|
|
201
309
|
const segments = [];
|
|
202
310
|
doc.nodesBetween(from, to, (node, pos) => {
|
|
203
|
-
if (!node.isText
|
|
311
|
+
if (!node.isText) return;
|
|
312
|
+
const includedProperties = formattingChangesForMarks(node.marks, formatting);
|
|
313
|
+
if (includedProperties.length === 0) return;
|
|
204
314
|
const segmentFrom = Math.max(from, pos);
|
|
205
315
|
const segmentTo = Math.min(to, pos + node.nodeSize);
|
|
206
316
|
const previousFormatting = marksToTextFormatting(node.marks);
|
|
@@ -219,6 +329,7 @@ const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, r
|
|
|
219
329
|
segments.push({
|
|
220
330
|
from: segmentFrom,
|
|
221
331
|
to: segmentTo,
|
|
332
|
+
includedProperties,
|
|
222
333
|
changes: [...existingChanges, change]
|
|
223
334
|
});
|
|
224
335
|
});
|
|
@@ -227,17 +338,20 @@ const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, r
|
|
|
227
338
|
provenance: "suggested",
|
|
228
339
|
suggestionId
|
|
229
340
|
};
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
341
|
+
for (const segment of segments) {
|
|
342
|
+
applyInlineFormatting({
|
|
343
|
+
tr,
|
|
344
|
+
schema,
|
|
345
|
+
from: segment.from,
|
|
346
|
+
to: segment.to,
|
|
347
|
+
formatting,
|
|
348
|
+
includedProperties: segment.includedProperties
|
|
349
|
+
});
|
|
350
|
+
tr.addMark(segment.from, segment.to, propertyChangeType.create({
|
|
351
|
+
changes: segment.changes,
|
|
352
|
+
...suggestionAttrs
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
241
355
|
return tr;
|
|
242
356
|
};
|
|
243
357
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { expectRunFormattingOverrideMarkAttrs } from "../prosemirror/attrs/index.js";
|
|
1
2
|
import { deriveBlankBlockId, deriveBlockId } from "../types/block-id.js";
|
|
2
3
|
import { buildCleanBlockText } from "./clean-text.js";
|
|
3
4
|
import { TableMap } from "prosemirror-tables";
|
|
@@ -252,14 +253,16 @@ const getPreviewRuns = (node) => {
|
|
|
252
253
|
if (!child.isText || child.text === void 0) return true;
|
|
253
254
|
if (child.marks.some((mark) => mark.type.name === DELETION_MARK)) return false;
|
|
254
255
|
const style = getPreviewRunStyle(child.marks, defaultStyle);
|
|
256
|
+
const directFormatting = getDirectPreviewRunStyle(child.marks, defaultStyle);
|
|
255
257
|
const previous = runs.at(-1);
|
|
256
|
-
if (previous && samePreviewRunStyle(previous, style)) {
|
|
258
|
+
if (previous && samePreviewRunStyle(previous, style) && sameDirectFormatting(previous.directFormatting, directFormatting)) {
|
|
257
259
|
previous.text += child.text;
|
|
258
260
|
return false;
|
|
259
261
|
}
|
|
260
262
|
runs.push({
|
|
261
263
|
text: child.text,
|
|
262
|
-
...style
|
|
264
|
+
...style,
|
|
265
|
+
...!isEmptyPreviewRunStyle(directFormatting) && { directFormatting }
|
|
263
266
|
});
|
|
264
267
|
return false;
|
|
265
268
|
});
|
|
@@ -310,6 +313,23 @@ const getPreviewRunStyle = (marks, defaultStyle) => {
|
|
|
310
313
|
}
|
|
311
314
|
return style;
|
|
312
315
|
};
|
|
316
|
+
const getDirectPreviewRunStyle = (marks, inheritedStyle) => {
|
|
317
|
+
const markedStyle = getPreviewRunStyle(marks, {});
|
|
318
|
+
const directStyle = {};
|
|
319
|
+
const overrideMark = marks.find(({ type }) => type.name === "runFormattingOverride");
|
|
320
|
+
const hasFormattingProvenance = overrideMark !== void 0 || marks.some(({ type }) => type.name === "characterStyle");
|
|
321
|
+
const directFontProperties = overrideMark ? expectRunFormattingOverrideMarkAttrs(overrideMark).directFontProperties : void 0;
|
|
322
|
+
for (const property of [
|
|
323
|
+
"bold",
|
|
324
|
+
"italic",
|
|
325
|
+
"underline",
|
|
326
|
+
"strike"
|
|
327
|
+
]) if (Boolean(markedStyle[property]) !== Boolean(inheritedStyle[property])) directStyle[property] = Boolean(markedStyle[property]);
|
|
328
|
+
if (markedStyle.fontFamily !== void 0 && (hasFormattingProvenance ? directFontProperties?.includes("fontFamily") : markedStyle.fontFamily !== inheritedStyle.fontFamily)) directStyle.fontFamily = markedStyle.fontFamily;
|
|
329
|
+
if (markedStyle.fontSizePt !== void 0 && (hasFormattingProvenance ? directFontProperties?.includes("fontSize") : markedStyle.fontSizePt !== inheritedStyle.fontSizePt)) directStyle.fontSizePt = markedStyle.fontSizePt;
|
|
330
|
+
if (markedStyle.color !== void 0 && (hasFormattingProvenance ? directFontProperties?.includes("color") : markedStyle.color !== inheritedStyle.color)) directStyle.color = markedStyle.color;
|
|
331
|
+
return directStyle;
|
|
332
|
+
};
|
|
313
333
|
const getBooleanTextFormatting = (formatting) => ({
|
|
314
334
|
...Reflect.get(formatting, "bold") === true && { bold: true },
|
|
315
335
|
...Reflect.get(formatting, "italic") === true && { italic: true },
|
|
@@ -352,6 +372,8 @@ const getColorFromAttrs = (attrs) => {
|
|
|
352
372
|
return `#${rgb}`;
|
|
353
373
|
};
|
|
354
374
|
const samePreviewRunStyle = (run, style) => run.bold === style.bold && run.italic === style.italic && run.underline === style.underline && run.strike === style.strike && run.fontFamily === style.fontFamily && run.fontSizePt === style.fontSizePt && run.color === style.color;
|
|
375
|
+
const isEmptyPreviewRunStyle = ({ bold, italic, underline, strike, fontFamily, fontSizePt, color }) => bold === void 0 && italic === void 0 && underline === void 0 && strike === void 0 && fontFamily === void 0 && fontSizePt === void 0 && color === void 0;
|
|
376
|
+
const sameDirectFormatting = (left, right) => left === void 0 && isEmptyPreviewRunStyle(right) || left !== void 0 && left.bold === right.bold && left.italic === right.italic && left.underline === right.underline && left.strike === right.strike && left.fontFamily === right.fontFamily && left.fontSizePt === right.fontSizePt && left.color === right.color;
|
|
355
377
|
const isUnstyledPreviewRun = ({ bold, italic, underline, strike, fontFamily, fontSizePt, color }) => bold === void 0 && italic === void 0 && underline === void 0 && strike === void 0 && fontFamily === void 0 && fontSizePt === void 0 && color === void 0;
|
|
356
378
|
//#endregion
|
|
357
379
|
export { createFolioAIEditSnapshot, createFolioAITextRangeHandle, folioStoryTables, hashFolioAIBlockText, isFolioAIContentBlock, isHiddenTableRow, normalizeFolioAIBlockText, trailingBodyBlockId };
|
package/dist/ai-edits/types.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
//#region src/ai-edits/types.d.ts
|
|
2
2
|
type FolioAIBlockKind = "heading" | "listItem" | "paragraph";
|
|
3
|
+
type FolioAIInlineFormatting = Partial<Record<"bold" | "italic" | "underline" | "strike", boolean>> & {
|
|
4
|
+
fontFamily?: string | null;
|
|
5
|
+
fontSizePt?: number | null;
|
|
6
|
+
color?: string | null;
|
|
7
|
+
};
|
|
3
8
|
type FolioAIBlockPreviewRun = {
|
|
4
9
|
text: string;
|
|
5
10
|
bold?: boolean;
|
|
@@ -9,6 +14,7 @@ type FolioAIBlockPreviewRun = {
|
|
|
9
14
|
fontFamily?: string;
|
|
10
15
|
fontSizePt?: number;
|
|
11
16
|
color?: string;
|
|
17
|
+
directFormatting?: FolioAIInlineFormatting;
|
|
12
18
|
};
|
|
13
19
|
/**
|
|
14
20
|
* Where a block sits inside its innermost enclosing table. Every index is
|
|
@@ -165,12 +171,6 @@ type FolioDocumentNavigationTarget = {
|
|
|
165
171
|
story: "main";
|
|
166
172
|
blockId: string;
|
|
167
173
|
} | FolioAITextRangeHandle;
|
|
168
|
-
type FolioAIInlineFormatting = {
|
|
169
|
-
bold?: boolean;
|
|
170
|
-
italic?: boolean;
|
|
171
|
-
underline?: boolean;
|
|
172
|
-
strike?: boolean;
|
|
173
|
-
};
|
|
174
174
|
/**
|
|
175
175
|
* A party in an `insertSignatureTable` op. Mirrors the
|
|
176
176
|
* `signatureTable` helper in `docx-core/legal-source/compile.ts`:
|
|
@@ -15,7 +15,7 @@ type InlineFormattingSegmentsOptions = {
|
|
|
15
15
|
maxSegments: number;
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
18
|
-
* Segments where `targetBlock`'s
|
|
18
|
+
* Segments where `targetBlock`'s supported run formatting differs from
|
|
19
19
|
* `baseBlock`'s, for two blocks that carry the same text. Returns `null` only
|
|
20
20
|
* when the diff would exceed `maxSegments`.
|
|
21
21
|
*
|
|
@@ -1,12 +1,41 @@
|
|
|
1
|
+
import { resolveColorToHex } from "../utils/colorResolver.js";
|
|
1
2
|
//#region src/compare/formatting.ts
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
const normalizeInlineFormattingColor = (color) => resolveColorToHex(color === void 0 ? void 0 : { rgb: color }, null);
|
|
4
|
+
const changedStringValue = (baseEffective, targetEffective, baseAuthored, targetAuthored) => {
|
|
5
|
+
if (baseAuthored !== targetAuthored) return targetAuthored ?? null;
|
|
6
|
+
return baseEffective === targetEffective ? void 0 : targetEffective ?? null;
|
|
7
|
+
};
|
|
8
|
+
const changedNumberValue = (baseEffective, targetEffective, baseAuthored, targetAuthored) => {
|
|
9
|
+
if (baseAuthored !== targetAuthored) return targetAuthored ?? null;
|
|
10
|
+
return baseEffective === targetEffective ? void 0 : targetEffective ?? null;
|
|
11
|
+
};
|
|
12
|
+
const changedSupportedFormatting = (base, target) => {
|
|
13
|
+
const baseDirect = base.directFormatting ?? {};
|
|
14
|
+
const targetDirect = target.directFormatting ?? {};
|
|
15
|
+
const formatting = {};
|
|
16
|
+
const changedBoolean = (property) => {
|
|
17
|
+
if (baseDirect[property] !== targetDirect[property]) return targetDirect[property] === true;
|
|
18
|
+
return Boolean(base[property]) === Boolean(target[property]) ? void 0 : Boolean(target[property]);
|
|
19
|
+
};
|
|
20
|
+
for (const property of [
|
|
21
|
+
"bold",
|
|
22
|
+
"italic",
|
|
23
|
+
"underline",
|
|
24
|
+
"strike"
|
|
25
|
+
]) {
|
|
26
|
+
const changed = changedBoolean(property);
|
|
27
|
+
if (changed !== void 0) formatting[property] = changed;
|
|
28
|
+
}
|
|
29
|
+
const fontFamily = changedStringValue(base.fontFamily, target.fontFamily, baseDirect.fontFamily, targetDirect.fontFamily);
|
|
30
|
+
if (fontFamily !== void 0) formatting.fontFamily = fontFamily;
|
|
31
|
+
const fontSizePt = changedNumberValue(base.fontSizePt, target.fontSizePt, baseDirect.fontSizePt, targetDirect.fontSizePt);
|
|
32
|
+
if (fontSizePt !== void 0) formatting.fontSizePt = fontSizePt;
|
|
33
|
+
const color = changedStringValue(normalizeInlineFormattingColor(base.color), normalizeInlineFormattingColor(target.color), normalizeInlineFormattingColor(baseDirect.color ?? void 0), normalizeInlineFormattingColor(targetDirect.color ?? void 0));
|
|
34
|
+
if (color !== void 0) formatting.color = color;
|
|
35
|
+
return formatting;
|
|
36
|
+
};
|
|
37
|
+
const sameInlineFormatting = (left, right) => left.bold === right.bold && left.italic === right.italic && left.underline === right.underline && left.strike === right.strike && left.fontFamily === right.fontFamily && left.fontSizePt === right.fontSizePt && left.color === right.color;
|
|
38
|
+
const hasInlineFormatting = (formatting) => formatting.bold !== void 0 || formatting.italic !== void 0 || formatting.underline !== void 0 || formatting.strike !== void 0 || formatting.fontFamily !== void 0 || formatting.fontSizePt !== void 0 || formatting.color !== void 0;
|
|
10
39
|
/**
|
|
11
40
|
* A block's runs, or `null` when they cannot describe the block's text.
|
|
12
41
|
* Non-text inline content (a field, an image) leaves the concatenated run text
|
|
@@ -18,7 +47,7 @@ const previewRunsForBlock = (block) => {
|
|
|
18
47
|
return runs.map(({ text }) => text).join("") === block.text ? runs : null;
|
|
19
48
|
};
|
|
20
49
|
/**
|
|
21
|
-
* Segments where `targetBlock`'s
|
|
50
|
+
* Segments where `targetBlock`'s supported run formatting differs from
|
|
22
51
|
* `baseBlock`'s, for two blocks that carry the same text. Returns `null` only
|
|
23
52
|
* when the diff would exceed `maxSegments`.
|
|
24
53
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveColorToHex } from "../utils/colorResolver.js";
|
|
1
2
|
import { PARAGRAPH_MARK_CHANGE_KINDS } from "@stll/docx-core/model";
|
|
2
3
|
//#region src/compare/verification.ts
|
|
3
4
|
/**
|
|
@@ -14,6 +15,7 @@ import { PARAGRAPH_MARK_CHANGE_KINDS } from "@stll/docx-core/model";
|
|
|
14
15
|
* Never a phrase of either document, because a caller may log it, put it in a
|
|
15
16
|
* report, or quote it in a review.
|
|
16
17
|
*/
|
|
18
|
+
const normalizeInlineFormattingColor = (color) => resolveColorToHex(color === void 0 ? void 0 : { rgb: color }, null);
|
|
17
19
|
/** The two directions of the round trip, each an invariant of its own. */
|
|
18
20
|
const COMPARE_VERIFICATION_INVARIANTS = Object.freeze(["accept-reproduces-target", "reject-reproduces-base"]);
|
|
19
21
|
/**
|
|
@@ -32,7 +34,22 @@ const COMPARE_VERIFICATION_CAUSES = Object.freeze([
|
|
|
32
34
|
"whitespace",
|
|
33
35
|
"text"
|
|
34
36
|
]);
|
|
35
|
-
const supportedInlineStyle = ({ bold, italic, underline, strike
|
|
37
|
+
const supportedInlineStyle = ({ bold, italic, underline, strike, fontFamily, fontSizePt, color, directFormatting }) => JSON.stringify([
|
|
38
|
+
bold === true,
|
|
39
|
+
italic === true,
|
|
40
|
+
underline === true,
|
|
41
|
+
strike === true,
|
|
42
|
+
fontFamily ?? null,
|
|
43
|
+
fontSizePt ?? null,
|
|
44
|
+
normalizeInlineFormattingColor(color) ?? null,
|
|
45
|
+
directFormatting?.bold === true,
|
|
46
|
+
directFormatting?.italic === true,
|
|
47
|
+
directFormatting?.underline === true,
|
|
48
|
+
directFormatting?.strike === true,
|
|
49
|
+
directFormatting?.fontFamily ?? null,
|
|
50
|
+
directFormatting?.fontSizePt ?? null,
|
|
51
|
+
normalizeInlineFormattingColor(directFormatting?.color ?? void 0) ?? null
|
|
52
|
+
]);
|
|
36
53
|
/** Effective supported formatting with equivalent adjacent runs normalized. */
|
|
37
54
|
const projectSupportedInlineFormatting = ({ text, previewRuns }) => {
|
|
38
55
|
const projected = [];
|
|
@@ -10,19 +10,21 @@ import { schema } from "../prosemirror/schema/index.js";
|
|
|
10
10
|
import { EditorState } from "prosemirror-state";
|
|
11
11
|
import { EditorView } from "prosemirror-view";
|
|
12
12
|
//#region src/controller/headerFooterEditorManager.ts
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
const DETACHED_WATERMARK_HOST = Symbol.for("stll.detachedWatermarkHost");
|
|
14
|
+
const headerFooterToProseDocWithDetachedWatermarkHost = (headerFooter, options) => {
|
|
15
|
+
return headerFooterToProseDoc(headerFooter.content.map((block, blockIndex) => {
|
|
16
|
+
if (blockIndex !== headerFooter.watermarkBlockIndex || block.type !== "paragraph") return block;
|
|
17
|
+
return {
|
|
18
|
+
...block,
|
|
19
|
+
[DETACHED_WATERMARK_HOST]: true
|
|
20
|
+
};
|
|
21
|
+
}), options);
|
|
22
|
+
};
|
|
21
23
|
const buildInitialState = (headerFooter, styles, theme, manager) => {
|
|
22
24
|
const proseDocOptions = {};
|
|
23
25
|
if (styles) proseDocOptions.styles = styles;
|
|
24
26
|
if (theme !== void 0) proseDocOptions.theme = theme;
|
|
25
|
-
const document = headerFooterToProseDoc(headerFooter.content, proseDocOptions);
|
|
27
|
+
const document = headerFooter.watermarkBlockIndex === void 0 ? headerFooterToProseDoc(headerFooter.content, proseDocOptions) : headerFooterToProseDocWithDetachedWatermarkHost(headerFooter, proseDocOptions);
|
|
26
28
|
return ensureBaseDirectionInState(ensureParaIdsInState(EditorState.create({
|
|
27
29
|
doc: document,
|
|
28
30
|
schema,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createDisplayListPagePainter } from "../display-list/editor/displayListPagePainter.js";
|
|
2
2
|
import { PAGE_RENDERER, displayListFurnitureFrom } from "../display-list/editor/pageRenderer.js";
|
|
3
3
|
import { resolveDocumentGridLinePitch } from "../docx/documentGrid.js";
|
|
4
|
+
import { formatOoxmlCounter } from "../docx/ooxmlCounterFormatter.js";
|
|
4
5
|
import { buildBookmarkPageMap } from "../fields/bookmarkPages.js";
|
|
5
6
|
import { buildBookmarkText } from "../fields/bookmarkText.js";
|
|
6
7
|
import { buildHeaderFooterFieldValues, fieldValuesEqual, resolveFieldValues } from "../fields/resolveFieldValues.js";
|
|
@@ -26,6 +27,17 @@ import { getMargins, getPageNumbering, getPageSize, twipsToPixels } from "../pag
|
|
|
26
27
|
import { templatePreviewValuesKey } from "../prosemirror/plugins/templatePreviewValues.js";
|
|
27
28
|
import { getDocumentWatermark } from "../watermark/index.js";
|
|
28
29
|
//#region src/controller/layoutPipeline.ts
|
|
30
|
+
const formatEndnoteTexts = (numbers, formatNumber) => {
|
|
31
|
+
const texts = /* @__PURE__ */ new Map();
|
|
32
|
+
for (const [id, displayNumber] of numbers) texts.set(id, formatNumber(displayNumber));
|
|
33
|
+
return texts;
|
|
34
|
+
};
|
|
35
|
+
/** Resolve only package-owned picture-watermark bytes for the DOM painter. */
|
|
36
|
+
const resolveWatermarkImageSrc = (document, watermark) => {
|
|
37
|
+
if (watermark?.kind !== "picture" || watermark.imageTargetExternal === true) return;
|
|
38
|
+
const target = watermark.imageTarget;
|
|
39
|
+
return target === void 0 ? void 0 : document.package.media?.get(target)?.dataUrl;
|
|
40
|
+
};
|
|
29
41
|
function bodyMarginsClearHeaderFooter({ authoredMargins, preparedHeader, preparedFooter }) {
|
|
30
42
|
const headerBottom = preparedHeader ? (authoredMargins.header ?? 0) + (preparedHeader.marginPushBottom ?? preparedHeader.height) : authoredMargins.top;
|
|
31
43
|
const footerClearance = preparedFooter ? (authoredMargins.footer ?? 0) + (preparedFooter.marginPushBottom ?? preparedFooter.height) : authoredMargins.bottom;
|
|
@@ -123,7 +135,10 @@ function runLayoutPipeline(deps, state, options = {}) {
|
|
|
123
135
|
};
|
|
124
136
|
const finalSectionDocumentGridLinePitchTwips = resolveDocumentGridLinePitch(document?.package.document.sections?.at(-1)?.properties.docGrid);
|
|
125
137
|
if (finalSectionDocumentGridLinePitchTwips !== void 0) flowOpts.finalSectionDocumentGridLinePitchTwips = finalSectionDocumentGridLinePitchTwips;
|
|
126
|
-
let newBlocks = toFlowBlocks(state.doc
|
|
138
|
+
let newBlocks = toFlowBlocks(document === null ? state.doc.type.create({
|
|
139
|
+
...state.doc.attrs,
|
|
140
|
+
_finalSectionStart: sectionProperties?.sectionStart ?? null
|
|
141
|
+
}, state.doc.content, state.doc.marks) : state.doc, flowOpts);
|
|
127
142
|
const previewState = templatePreviewValuesKey.getState(state);
|
|
128
143
|
const previewEntries = previewState?.entries ?? EMPTY_TEMPLATE_PREVIEW_ENTRIES;
|
|
129
144
|
const previewMode = previewState?.preview?.mode ?? "plain";
|
|
@@ -144,9 +159,11 @@ function runLayoutPipeline(deps, state, options = {}) {
|
|
|
144
159
|
const footnoteDisplayNumbers = documentFootnotes ? computeNoteDisplayNumbers(documentFootnotes, footnoteRefs.map((ref) => ref.footnoteId)) : void 0;
|
|
145
160
|
const documentEndnotes = document?.package.endnotes;
|
|
146
161
|
const endnoteDisplayNumbers = documentEndnotes ? computeNoteDisplayNumbers(documentEndnotes, collectEndnoteRefs(newBlocks).map((ref) => ref.endnoteId)) : void 0;
|
|
162
|
+
const endnoteNumberFormat = document?.package.document.sections?.at(-1)?.properties.endnotePr?.numFmt ?? "lowerRoman";
|
|
163
|
+
const endnoteTexts = endnoteDisplayNumbers ? formatEndnoteTexts(endnoteDisplayNumbers, (displayNumber) => formatOoxmlCounter(displayNumber, endnoteNumberFormat)) : void 0;
|
|
147
164
|
newBlocks = remapNoteMarkerText(newBlocks, {
|
|
148
165
|
...footnoteDisplayNumbers ? { footnoteNumbers: footnoteDisplayNumbers } : {},
|
|
149
|
-
...
|
|
166
|
+
...endnoteTexts ? { endnoteTexts } : {}
|
|
150
167
|
});
|
|
151
168
|
outcome.blocks = newBlocks;
|
|
152
169
|
const hfMetricsHeader = {
|
|
@@ -479,12 +496,16 @@ function runLayoutPipeline(deps, state, options = {}) {
|
|
|
479
496
|
if (document) {
|
|
480
497
|
const watermark = getDocumentWatermark(document);
|
|
481
498
|
if (watermark) renderOpts.watermark = watermark;
|
|
499
|
+
let watermarkByHeaderRId;
|
|
482
500
|
const headers = document.package.headers;
|
|
483
501
|
if (headers) {
|
|
484
|
-
|
|
502
|
+
watermarkByHeaderRId = /* @__PURE__ */ new Map();
|
|
485
503
|
for (const [rId, header] of headers) if (header.watermark) watermarkByHeaderRId.set(rId, header.watermark);
|
|
486
504
|
if (watermarkByHeaderRId.size > 0) renderOpts.watermarkByHeaderRId = watermarkByHeaderRId;
|
|
487
505
|
}
|
|
506
|
+
const pictureSources = new Set([watermark, ...watermarkByHeaderRId?.values() ?? []].filter((candidate) => candidate !== void 0).map((candidate) => resolveWatermarkImageSrc(document, candidate)));
|
|
507
|
+
const watermarkImageSrc = pictureSources.size === 1 ? [...pictureSources].at(0) : void 0;
|
|
508
|
+
if (watermarkImageSrc !== void 0) renderOpts.watermarkImageSrc = watermarkImageSrc;
|
|
488
509
|
}
|
|
489
510
|
if (pageRenderer === PAGE_RENDERER.displayList) renderOpts.paintPage = createDisplayListPagePainter({
|
|
490
511
|
layout: newLayout,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pxToPt } from "../../layout-engine/measure/measureHelpers.js";
|
|
1
|
+
import { ptToPx, pxToPt } from "../../layout-engine/measure/measureHelpers.js";
|
|
2
2
|
import { getFontMetrics } from "../../layout-engine/measure/measureProvider.js";
|
|
3
3
|
import { parseDisplayColor } from "./colors.js";
|
|
4
4
|
import { buildGlyphs, glyphRunText } from "./glyphs.js";
|
|
@@ -28,7 +28,7 @@ const TEXT_DEFAULT_COLOR = "#C0C0C0";
|
|
|
28
28
|
const TEXT_DEFAULT_OPACITY = .5;
|
|
29
29
|
const TEXT_DIAGONAL_DEGREES = -45;
|
|
30
30
|
const PICTURE_NATIVE_SCALE = 1;
|
|
31
|
-
const PICTURE_WASHOUT_OPACITY = .
|
|
31
|
+
const PICTURE_WASHOUT_OPACITY = .18;
|
|
32
32
|
const paintTextWatermark = (watermark, page, context) => {
|
|
33
33
|
if (watermark.text.length === 0) return [];
|
|
34
34
|
const style = {
|
|
@@ -98,6 +98,18 @@ const containedRect = (page, scale, pixelWidth, pixelHeight) => {
|
|
|
98
98
|
heightPx
|
|
99
99
|
};
|
|
100
100
|
};
|
|
101
|
+
/** The centred VML shape box, when both authored dimensions survived parsing. */
|
|
102
|
+
const authoredPictureRect = (page, widthPt, heightPt) => {
|
|
103
|
+
if (widthPt === void 0 || heightPt === void 0) return;
|
|
104
|
+
const widthPx = ptToPx(widthPt);
|
|
105
|
+
const heightPx = ptToPx(heightPt);
|
|
106
|
+
return {
|
|
107
|
+
xPx: (page.size.w - widthPx) / 2,
|
|
108
|
+
yPx: (page.size.h - heightPx) / 2,
|
|
109
|
+
widthPx,
|
|
110
|
+
heightPx
|
|
111
|
+
};
|
|
112
|
+
};
|
|
101
113
|
const paintPictureWatermark = (watermark, page, imageSrc, context) => {
|
|
102
114
|
if (imageSrc === void 0) {
|
|
103
115
|
context.unsupported.report(UNSUPPORTED_CONSTRUCT.watermark, context.pageIndex, `picture watermark ${watermark.imageRId} has no resolved image source: the relationship id resolves in the package layer, not in the builder`);
|
|
@@ -113,7 +125,7 @@ const paintPictureWatermark = (watermark, page, imageSrc, context) => {
|
|
|
113
125
|
return [{
|
|
114
126
|
kind: "image",
|
|
115
127
|
image: ref,
|
|
116
|
-
rect: containedRect(page, watermark.scale ?? PICTURE_NATIVE_SCALE, source?.pixelWidth ?? 0, source?.pixelHeight ?? 0),
|
|
128
|
+
rect: authoredPictureRect(page, watermark.widthPt, watermark.heightPt) ?? containedRect(page, watermark.scale ?? PICTURE_NATIVE_SCALE, source?.pixelWidth ?? 0, source?.pixelHeight ?? 0),
|
|
117
129
|
opacity: watermark.washout === false ? 1 : PICTURE_WASHOUT_OPACITY
|
|
118
130
|
}];
|
|
119
131
|
};
|
|
@@ -143,6 +143,24 @@ const readOptionalBoolean = (value, key, path) => {
|
|
|
143
143
|
if (typeof candidate === "boolean") return candidate;
|
|
144
144
|
return invalidBatch(`${path}.${key}`, "expected a boolean when provided");
|
|
145
145
|
};
|
|
146
|
+
const readClearableNonEmptyString = (value, key, path) => {
|
|
147
|
+
const candidate = value[key];
|
|
148
|
+
if (candidate === void 0 || candidate === null) return candidate;
|
|
149
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) return candidate.trim();
|
|
150
|
+
return invalidBatch(`${path}.${key}`, "expected a non-empty string or null when provided");
|
|
151
|
+
};
|
|
152
|
+
const readClearableFontSize = (value, key, path) => {
|
|
153
|
+
const candidate = value[key];
|
|
154
|
+
if (candidate === void 0 || candidate === null) return candidate;
|
|
155
|
+
if (typeof candidate === "number" && candidate > 0 && Number.isInteger(candidate * 2)) return candidate;
|
|
156
|
+
return invalidBatch(`${path}.${key}`, "expected a positive half-point value or null");
|
|
157
|
+
};
|
|
158
|
+
const readClearableRgbColor = (value, key, path) => {
|
|
159
|
+
const candidate = value[key];
|
|
160
|
+
if (candidate === void 0 || candidate === null) return candidate;
|
|
161
|
+
if (typeof candidate === "string" && /^#?[0-9a-fA-F]{6}$/u.test(candidate)) return candidate.replace(/^#/u, "").toUpperCase();
|
|
162
|
+
return invalidBatch(`${path}.${key}`, "expected a six-digit RGB color or null");
|
|
163
|
+
};
|
|
146
164
|
const readOptionalStringArray = (value, key, path) => {
|
|
147
165
|
const candidate = value[key];
|
|
148
166
|
if (candidate === void 0) return;
|
|
@@ -238,18 +256,27 @@ const readInlineFormatting = (value, path) => {
|
|
|
238
256
|
"bold",
|
|
239
257
|
"italic",
|
|
240
258
|
"underline",
|
|
241
|
-
"strike"
|
|
259
|
+
"strike",
|
|
260
|
+
"fontFamily",
|
|
261
|
+
"fontSizePt",
|
|
262
|
+
"color"
|
|
242
263
|
]);
|
|
243
264
|
const bold = readOptionalBoolean(candidate, "bold", formattingPath);
|
|
244
265
|
const italic = readOptionalBoolean(candidate, "italic", formattingPath);
|
|
245
266
|
const underline = readOptionalBoolean(candidate, "underline", formattingPath);
|
|
246
267
|
const strike = readOptionalBoolean(candidate, "strike", formattingPath);
|
|
247
|
-
|
|
268
|
+
const fontFamily = readClearableNonEmptyString(candidate, "fontFamily", formattingPath);
|
|
269
|
+
const fontSizePt = readClearableFontSize(candidate, "fontSizePt", formattingPath);
|
|
270
|
+
const color = readClearableRgbColor(candidate, "color", formattingPath);
|
|
271
|
+
if (bold === void 0 && italic === void 0 && underline === void 0 && strike === void 0 && fontFamily === void 0 && fontSizePt === void 0 && color === void 0) return invalidBatch(formattingPath, "expected at least one formatting property");
|
|
248
272
|
return {
|
|
249
273
|
...bold !== void 0 && { bold },
|
|
250
274
|
...italic !== void 0 && { italic },
|
|
251
275
|
...underline !== void 0 && { underline },
|
|
252
|
-
...strike !== void 0 && { strike }
|
|
276
|
+
...strike !== void 0 && { strike },
|
|
277
|
+
...fontFamily !== void 0 && { fontFamily },
|
|
278
|
+
...fontSizePt !== void 0 && { fontSizePt },
|
|
279
|
+
...color !== void 0 && { color }
|
|
253
280
|
};
|
|
254
281
|
};
|
|
255
282
|
const readOptionalComment = (value, path) => {
|
|
@@ -31,27 +31,21 @@ function parseHeader(headerXml, hdrFtrType = "default", styles = null, theme = n
|
|
|
31
31
|
result.rawWatermarkXml = watermarkResult.rawParagraphXml;
|
|
32
32
|
result.watermarkBlockIndex = watermarkResult.blockIndex;
|
|
33
33
|
}
|
|
34
|
-
result.content = parseBlockContent(
|
|
34
|
+
result.content = parseBlockContent(rootElement, styles, theme, numbering, rels, media, {
|
|
35
35
|
inHeaderFooter: true,
|
|
36
36
|
rootXmlns: collectXmlnsDeclarations(rootElement)
|
|
37
37
|
});
|
|
38
|
+
if (watermarkResult) {
|
|
39
|
+
const host = result.content.at(watermarkResult.blockIndex);
|
|
40
|
+
if (host?.type === "paragraph") result.content[watermarkResult.blockIndex] = {
|
|
41
|
+
...host,
|
|
42
|
+
content: []
|
|
43
|
+
};
|
|
44
|
+
}
|
|
38
45
|
assignHeaderFooterVerbatimXml(result, headerXml);
|
|
39
46
|
return result;
|
|
40
47
|
}
|
|
41
48
|
/**
|
|
42
|
-
* Return a shallow copy of `parent` whose `elements` array omits the
|
|
43
|
-
* single child reference `child`. Used to skip the watermark paragraph
|
|
44
|
-
* when feeding the header into `parseBlockContent` — without this the
|
|
45
|
-
* body parser would emit an empty placeholder paragraph where the
|
|
46
|
-
* watermark sits in the source.
|
|
47
|
-
*/
|
|
48
|
-
function withoutChild(parent, child) {
|
|
49
|
-
return {
|
|
50
|
-
...parent,
|
|
51
|
-
elements: (parent.elements ?? []).filter((el) => el !== child)
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
49
|
* Parse a footer XML file (word/footer*.xml)
|
|
56
50
|
*
|
|
57
51
|
* @param footerXml - The raw XML content of the footer file
|