@stll/folio-core 0.33.2 → 0.35.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.d.ts +3 -1
- package/dist/ai-edits/snapshot.js +44 -4
- package/dist/ai-edits/types.d.ts +6 -6
- package/dist/compare/compare.js +15 -3
- 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/plugins/templateDirectives.d.ts +14 -7
- package/dist/prosemirror/plugins/templateDirectives.js +26 -19
- package/dist/prosemirror/plugins/templateSlashMenu.js +2 -2
- 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 +3 -3
|
@@ -689,9 +689,9 @@ function createTrackedRunWrapper(type, info, child) {
|
|
|
689
689
|
content
|
|
690
690
|
};
|
|
691
691
|
}
|
|
692
|
-
function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, textBoxAnchorMarkers, skipLeadingRenderedPageBreak = false) {
|
|
692
|
+
function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, textBoxAnchorMarkers, skipLeadingRenderedPageBreak = false, inheritedFormattingOverride) {
|
|
693
693
|
const content = [];
|
|
694
|
-
const inheritedFormatting = paragraph.type.name === "paragraph" ? expectParagraphAttrs(paragraph).defaultTextFormatting ?? void 0 : void 0;
|
|
694
|
+
const inheritedFormatting = inheritedFormattingOverride ?? (paragraph.type.name === "paragraph" ? expectParagraphAttrs(paragraph).defaultTextFormatting ?? void 0 : void 0);
|
|
695
695
|
const sortedEmptyHyperlinks = (emptyHyperlinks ?? []).map((attrs, order) => ({
|
|
696
696
|
attrs,
|
|
697
697
|
order
|
|
@@ -966,7 +966,7 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
|
|
|
966
966
|
}));
|
|
967
967
|
} else if (node.type.name === "sdt") {
|
|
968
968
|
flushCurrentInline();
|
|
969
|
-
content.push(createInlineSdtFromNode(node, textBoxAnchorMarkers));
|
|
969
|
+
content.push(createInlineSdtFromNode(node, textBoxAnchorMarkers, inheritedFormatting));
|
|
970
970
|
} else if (node.type.name === "math") {
|
|
971
971
|
flushCurrentInline();
|
|
972
972
|
content.push(createMathFromNode(node));
|
|
@@ -1369,11 +1369,11 @@ function createMathFromNode(node) {
|
|
|
1369
1369
|
/**
|
|
1370
1370
|
* Create an InlineSdt from a PM sdt node
|
|
1371
1371
|
*/
|
|
1372
|
-
function createInlineSdtFromNode(node, textBoxAnchorMarkers) {
|
|
1372
|
+
function createInlineSdtFromNode(node, textBoxAnchorMarkers, inheritedFormatting) {
|
|
1373
1373
|
return {
|
|
1374
1374
|
type: "inlineSdt",
|
|
1375
1375
|
properties: sdtPropertiesFromAttrs(expectSdtAttrs(node)),
|
|
1376
|
-
content: extractParagraphContent(node, void 0, void 0, textBoxAnchorMarkers).filter((c) => c.type === "run" || c.type === "hyperlink" || c.type === "simpleField" || c.type === "complexField" || c.type === "inlineSdt" || c.type === "insertion" || c.type === "deletion" || c.type === "moveFrom" || c.type === "moveTo" || c.type === "mathEquation")
|
|
1376
|
+
content: extractParagraphContent(node, void 0, void 0, textBoxAnchorMarkers, false, inheritedFormatting).filter((c) => c.type === "run" || c.type === "hyperlink" || c.type === "simpleField" || c.type === "complexField" || c.type === "inlineSdt" || c.type === "insertion" || c.type === "deletion" || c.type === "moveFrom" || c.type === "moveTo" || c.type === "mathEquation")
|
|
1377
1377
|
};
|
|
1378
1378
|
}
|
|
1379
1379
|
/**
|
|
@@ -1524,6 +1524,7 @@ function createShapeRun(node) {
|
|
|
1524
1524
|
function marksToTextFormatting(marks, options) {
|
|
1525
1525
|
const formatting = {};
|
|
1526
1526
|
let directOverrideFormatting;
|
|
1527
|
+
let directFontProperties;
|
|
1527
1528
|
let characterStyleRPr;
|
|
1528
1529
|
let runFormattingOverrideMark;
|
|
1529
1530
|
for (const mark of marks) switch (mark.type.name) {
|
|
@@ -1651,9 +1652,14 @@ function marksToTextFormatting(marks, options) {
|
|
|
1651
1652
|
}
|
|
1652
1653
|
if (runFormattingOverrideMark) {
|
|
1653
1654
|
const overrideAttrs = expectRunFormattingOverrideMarkAttrs(runFormattingOverrideMark);
|
|
1655
|
+
directFontProperties = overrideAttrs.directFontProperties;
|
|
1654
1656
|
applyRunFormattingOverrideAttrs(formatting, overrideAttrs);
|
|
1655
1657
|
directOverrideFormatting = {};
|
|
1656
1658
|
applyRunFormattingOverrideAttrs(directOverrideFormatting, overrideAttrs);
|
|
1659
|
+
for (const property of directFontProperties ?? []) {
|
|
1660
|
+
const value = formatting[property];
|
|
1661
|
+
if (value !== void 0) Reflect.set(directOverrideFormatting, property, value);
|
|
1662
|
+
}
|
|
1657
1663
|
}
|
|
1658
1664
|
if (characterStyleRPr) return subtractCharacterStyleFormatting({
|
|
1659
1665
|
directOverrideFormatting,
|
|
@@ -1661,8 +1667,29 @@ function marksToTextFormatting(marks, options) {
|
|
|
1661
1667
|
inheritedFormatting: options?.inheritedFormatting,
|
|
1662
1668
|
styleRPr: characterStyleRPr
|
|
1663
1669
|
});
|
|
1670
|
+
for (const property of [
|
|
1671
|
+
"fontFamily",
|
|
1672
|
+
"fontSize",
|
|
1673
|
+
"color"
|
|
1674
|
+
]) {
|
|
1675
|
+
if (directFontProperties?.includes(property)) continue;
|
|
1676
|
+
const value = formatting[property];
|
|
1677
|
+
const inheritedValue = options?.inheritedFormatting?.[property];
|
|
1678
|
+
const matchesInherited = property === "fontFamily" ? sameFontFamily(formatting.fontFamily, options?.inheritedFormatting?.fontFamily) : JSON.stringify(value) === JSON.stringify(inheritedValue);
|
|
1679
|
+
if (inheritedValue !== void 0 && matchesInherited) Reflect.deleteProperty(formatting, property);
|
|
1680
|
+
}
|
|
1664
1681
|
return formatting;
|
|
1665
1682
|
}
|
|
1683
|
+
const sameFontFamily = (left, right) => [
|
|
1684
|
+
"ascii",
|
|
1685
|
+
"hAnsi",
|
|
1686
|
+
"eastAsia",
|
|
1687
|
+
"hint",
|
|
1688
|
+
"asciiTheme",
|
|
1689
|
+
"hAnsiTheme",
|
|
1690
|
+
"eastAsiaTheme",
|
|
1691
|
+
"csTheme"
|
|
1692
|
+
].every((property) => left?.[property] === right?.[property]) && (left?.cs ?? left?.ascii) === (right?.cs ?? right?.ascii);
|
|
1666
1693
|
/**
|
|
1667
1694
|
* Negatable boolean run-property keys whose serializer emits an explicit
|
|
1668
1695
|
* `w:val="0"` override when the value is `false` (see `serializeTextFormatting`
|
|
@@ -18,6 +18,7 @@ import { shadingToRunShadingAttrs } from "./runShadingMark.js";
|
|
|
18
18
|
import { sdtAttrsFromProperties } from "./sdtAttrs.js";
|
|
19
19
|
import { panic } from "better-result";
|
|
20
20
|
//#region src/prosemirror/conversion/toProseDoc.ts
|
|
21
|
+
const DETACHED_WATERMARK_HOST = Symbol.for("stll.detachedWatermarkHost");
|
|
21
22
|
/**
|
|
22
23
|
* Build a `nextTextBoxGroupId()` generator salted with a random per-load
|
|
23
24
|
* nonce, so minted text-box anchor ids (`<salt>:<group>:<index>`) are unique
|
|
@@ -139,7 +140,12 @@ function toProseDoc(document, options) {
|
|
|
139
140
|
};
|
|
140
141
|
nodes.push(...convertBodyBlocks(paragraphs));
|
|
141
142
|
if (nodes.length === 0) nodes.push(schema.node("paragraph", {}, []));
|
|
142
|
-
const
|
|
143
|
+
const finalSectionStart = document.package.document.sections?.at(-1)?.properties.sectionStart ?? null;
|
|
144
|
+
const adjustLineHeightInTable = document.package.settings?.adjustLineHeightInTable === true;
|
|
145
|
+
const pmDoc = stampNumberedRefFieldBaselines(schema.node("doc", {
|
|
146
|
+
_finalSectionStart: finalSectionStart,
|
|
147
|
+
_adjustLineHeightInTable: adjustLineHeightInTable
|
|
148
|
+
}, nodes));
|
|
143
149
|
assertValidProseMirrorDocument(pmDoc, "Document conversion produced an invalid ProseMirror document");
|
|
144
150
|
return pmDoc;
|
|
145
151
|
}
|
|
@@ -210,6 +216,7 @@ function convertParagraph(paragraph, styleResolver, nextHyperlinkInstanceIndex,
|
|
|
210
216
|
const paragraphRunFormatting = resolveRunFormattingWithoutDefaults(paragraph.formatting?.runProperties, styleResolver);
|
|
211
217
|
let inheritableParagraphRunFormatting;
|
|
212
218
|
if (paragraphRunFormatting && !isTocParagraph) inheritableParagraphRunFormatting = stripParagraphMarkFormattingForBodyRuns(paragraphRunFormatting);
|
|
219
|
+
const ordinaryStyleFormatting = paragraph.formatting?.styleId === void 0 ? mergeTextFormatting(styleRunFormatting, extraRunFormatting) : mergeTextFormatting(extraRunFormatting, styleRunFormatting);
|
|
213
220
|
const orderedToggleFormatting = cascadeStyleTextFormatting([
|
|
214
221
|
{
|
|
215
222
|
formatting: styleResolver?.getDocDefaults()?.rPr,
|
|
@@ -223,7 +230,7 @@ function convertParagraph(paragraph, styleResolver, nextHyperlinkInstanceIndex,
|
|
|
223
230
|
formatting: paragraphStyleRunFormatting,
|
|
224
231
|
type: "style"
|
|
225
232
|
}
|
|
226
|
-
], { ordinaryFormatting:
|
|
233
|
+
], { ordinaryFormatting: ordinaryStyleFormatting });
|
|
227
234
|
let baseRunFormatting = orderedToggleFormatting.formatting;
|
|
228
235
|
if (paragraphStyleFontFamily) baseRunFormatting = mergeTextFormatting(baseRunFormatting, { fontFamily: paragraphStyleFontFamily });
|
|
229
236
|
const paragraphMarkPrecedesStyle = paragraph.formatting?.styleId !== void 0;
|
|
@@ -1000,10 +1007,10 @@ function convertTableRow(row, styleResolver, context, isHeaderRow, columnWidths,
|
|
|
1000
1007
|
let cellConditionalStyle = conditionalStyles?.wholeTable;
|
|
1001
1008
|
cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, effectiveRowBandStyle);
|
|
1002
1009
|
cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, vertBandStyle);
|
|
1003
|
-
if (cellIsFirstRow && (tableLook?.firstRow || rowCnf?.firstRow || cellCnf?.firstRow)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.firstRow);
|
|
1004
|
-
if (cellIsLastRow && (tableLook?.lastRow || rowCnf?.lastRow || cellCnf?.lastRow)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.lastRow);
|
|
1005
1010
|
if (cellIsFirstCol && (tableLook?.firstColumn || rowCnf?.firstColumn || cellCnf?.firstColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.firstCol);
|
|
1006
1011
|
if (cellIsLastCol && (tableLook?.lastColumn || rowCnf?.lastColumn || cellCnf?.lastColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.lastCol);
|
|
1012
|
+
if (cellIsFirstRow && (tableLook?.firstRow || rowCnf?.firstRow || cellCnf?.firstRow)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.firstRow);
|
|
1013
|
+
if (cellIsLastRow && (tableLook?.lastRow || rowCnf?.lastRow || cellCnf?.lastRow)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.lastRow);
|
|
1007
1014
|
if (cellIsFirstRow && cellIsFirstCol && (tableLook?.firstRow || rowCnf?.firstRow || cellCnf?.firstRow) && (tableLook?.firstColumn || rowCnf?.firstColumn || cellCnf?.firstColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.nwCell);
|
|
1008
1015
|
if (cellIsFirstRow && cellIsLastCol && (tableLook?.firstRow || rowCnf?.firstRow || cellCnf?.firstRow) && (tableLook?.lastColumn || rowCnf?.lastColumn || cellCnf?.lastColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.neCell);
|
|
1009
1016
|
if (cellIsLastRow && cellIsFirstCol && (tableLook?.lastRow || rowCnf?.lastRow || cellCnf?.lastRow) && (tableLook?.firstColumn || rowCnf?.firstColumn || cellCnf?.firstColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.swCell);
|
|
@@ -1265,6 +1272,7 @@ function buildRunMarks(runFormatting, inherited, styleResolver) {
|
|
|
1265
1272
|
hasCharacterStyle: styleId !== void 0,
|
|
1266
1273
|
paragraphMarkOverrides: inherited.paragraphMarkOverrides
|
|
1267
1274
|
}) });
|
|
1275
|
+
addDirectFontProvenance(marks, runFormatting);
|
|
1268
1276
|
if (styleId) {
|
|
1269
1277
|
const styleRPr = characterStyleFormatting ? marksToTextFormatting(textFormattingToMarks(characterStyleFormatting)) : void 0;
|
|
1270
1278
|
marks.push(schema.mark("characterStyle", {
|
|
@@ -1277,6 +1285,24 @@ function buildRunMarks(runFormatting, inherited, styleResolver) {
|
|
|
1277
1285
|
mergedFormatting
|
|
1278
1286
|
};
|
|
1279
1287
|
}
|
|
1288
|
+
const addDirectFontProvenance = (marks, directFormatting) => {
|
|
1289
|
+
const directFontProperties = [];
|
|
1290
|
+
if (directFormatting?.fontFamily !== void 0) directFontProperties.push("fontFamily");
|
|
1291
|
+
if (directFormatting?.fontSize !== void 0) directFontProperties.push("fontSize");
|
|
1292
|
+
if (directFormatting?.color !== void 0) directFontProperties.push("color");
|
|
1293
|
+
if (!directFormatting || !Object.keys(directFormatting).some((property) => property !== "styleId")) return;
|
|
1294
|
+
const index = marks.findIndex(({ type }) => type.name === "runFormattingOverride");
|
|
1295
|
+
const existing = index >= 0 ? marks.at(index) : void 0;
|
|
1296
|
+
const override = schema.mark("runFormattingOverride", {
|
|
1297
|
+
...existing?.attrs,
|
|
1298
|
+
...directFontProperties.length > 0 && { directFontProperties }
|
|
1299
|
+
});
|
|
1300
|
+
if (index >= 0) {
|
|
1301
|
+
marks[index] = override;
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
marks.push(override);
|
|
1305
|
+
};
|
|
1280
1306
|
const ORDINARY_STYLE_TOGGLE_KEYS = [
|
|
1281
1307
|
"bold",
|
|
1282
1308
|
"italic",
|
|
@@ -1658,12 +1684,12 @@ function convertShape(shape) {
|
|
|
1658
1684
|
position
|
|
1659
1685
|
});
|
|
1660
1686
|
}
|
|
1661
|
-
function convertParagraphWithTextBoxes(block, styleResolver, { textBoxGroupId, context, extraRunFormatting, tableParagraphOverlay }) {
|
|
1687
|
+
function convertParagraphWithTextBoxes(block, styleResolver, { textBoxGroupId, context, extraRunFormatting, preserveEmptyWrapper, tableParagraphOverlay }) {
|
|
1662
1688
|
const { textBoxes, textBoxAnchors } = extractTextBoxesFromParagraph(block, textBoxGroupId);
|
|
1663
1689
|
const pmParagraph = convertParagraph(block, styleResolver, context.nextHyperlinkInstanceIndex, context.pairedBookmarkIds, void 0, extraRunFormatting, tableParagraphOverlay, textBoxAnchors);
|
|
1664
1690
|
const nodes = [];
|
|
1665
1691
|
const isEmptyAfterExtraction = textBoxes.length > 0 && !hasContentBesidesTextBoxAnchors(pmParagraph);
|
|
1666
|
-
const keepWrapperParagraph = isEmptyAfterExtraction && hasParagraphBoundaryPayload(block, pmParagraph);
|
|
1692
|
+
const keepWrapperParagraph = isEmptyAfterExtraction && (preserveEmptyWrapper === true || hasParagraphBoundaryPayload(block, pmParagraph));
|
|
1667
1693
|
if (!isEmptyAfterExtraction || keepWrapperParagraph) nodes.push(pmParagraph);
|
|
1668
1694
|
for (const { textBox, anchorId, trackedChange, inlineSdts } of textBoxes) nodes.push(convertTextBox(textBox, styleResolver, {
|
|
1669
1695
|
placement: isEmptyAfterExtraction && !keepWrapperParagraph ? "standalone" : "inlineWithPrevious",
|
|
@@ -1929,11 +1955,23 @@ function headerFooterToProseDoc(content, options) {
|
|
|
1929
1955
|
};
|
|
1930
1956
|
const convertBlocks = (blocks) => {
|
|
1931
1957
|
const out = [];
|
|
1932
|
-
for (const block of blocks) if (block.type === "paragraph")
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1958
|
+
for (const block of blocks) if (block.type === "paragraph") {
|
|
1959
|
+
const isDetachedWatermarkHost = Reflect.get(block, DETACHED_WATERMARK_HOST) === true;
|
|
1960
|
+
const paragraphNodes = convertParagraphWithTextBoxes(block, styleResolver, {
|
|
1961
|
+
textBoxGroupId: nextTextBoxGroupId(),
|
|
1962
|
+
context: conversionContext,
|
|
1963
|
+
preserveEmptyWrapper: isDetachedWatermarkHost
|
|
1964
|
+
});
|
|
1965
|
+
if (isDetachedWatermarkHost) {
|
|
1966
|
+
const paragraphNodeIndex = paragraphNodes.findIndex(({ type }) => type.name === "paragraph");
|
|
1967
|
+
const paragraphNode = paragraphNodes[paragraphNodeIndex];
|
|
1968
|
+
if (paragraphNode) paragraphNodes[paragraphNodeIndex] = paragraphNode.type.create({
|
|
1969
|
+
...paragraphNode.attrs,
|
|
1970
|
+
_detachedWatermarkHost: true
|
|
1971
|
+
}, paragraphNode.content, paragraphNode.marks);
|
|
1972
|
+
}
|
|
1973
|
+
out.push(...paragraphNodes);
|
|
1974
|
+
} else if (block.type === "table") out.push(convertTable(block, styleResolver, conversionContext));
|
|
1937
1975
|
else out.push(convertBlockSdt(block, convertBlocks));
|
|
1938
1976
|
return out;
|
|
1939
1977
|
};
|
|
@@ -6,7 +6,13 @@ import { createNodeExtension } from "../create.js";
|
|
|
6
6
|
const DocExtension = createNodeExtension({
|
|
7
7
|
name: "doc",
|
|
8
8
|
schemaNodeName: "doc",
|
|
9
|
-
nodeSpec: {
|
|
9
|
+
nodeSpec: {
|
|
10
|
+
attrs: {
|
|
11
|
+
_finalSectionStart: { default: null },
|
|
12
|
+
_adjustLineHeightInTable: { default: false }
|
|
13
|
+
},
|
|
14
|
+
content: "(paragraph | horizontalRule | pageBreak | table | textBox | blockSdt)+"
|
|
15
|
+
}
|
|
10
16
|
});
|
|
11
17
|
//#endregion
|
|
12
18
|
export { DocExtension };
|
|
@@ -223,6 +223,7 @@ const paragraphNodeSpec = {
|
|
|
223
223
|
pageBreakBefore: { default: null },
|
|
224
224
|
renderedPageBreakBefore: { default: null },
|
|
225
225
|
_pageBreakCarrier: { default: null },
|
|
226
|
+
_detachedWatermarkHost: { default: null },
|
|
226
227
|
_trailingPageBreak: { default: null },
|
|
227
228
|
keepNext: { default: null },
|
|
228
229
|
keepLines: { default: null },
|
|
@@ -50,6 +50,7 @@ const RunFormattingOverrideExtension = createMarkExtension({
|
|
|
50
50
|
schemaMarkName: "runFormattingOverride",
|
|
51
51
|
markSpec: {
|
|
52
52
|
attrs: {
|
|
53
|
+
directFontProperties: { default: null },
|
|
53
54
|
bold: { default: null },
|
|
54
55
|
italic: { default: null },
|
|
55
56
|
underline: { default: null },
|
|
@@ -9,10 +9,16 @@ type DirectiveRange = {
|
|
|
9
9
|
/** Exclusive PM doc position of the marker end. */
|
|
10
10
|
to: number;
|
|
11
11
|
kind: DirectiveKind;
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Field path, clause name, key, condition, or — for a `for` marker — the
|
|
14
|
+
* array path the loop iterates (`{% for row in items %}` ⇒ `items`).
|
|
15
|
+
*/
|
|
13
16
|
expr: string;
|
|
14
17
|
/** Clause-slot version selector, e.g. "v3" or "latest". */
|
|
15
18
|
clauseVersion?: string;
|
|
19
|
+
/** Loop alias of a `for` marker (`{% for row in items %}` ⇒ `row`); unset
|
|
20
|
+
* for every other kind. */
|
|
21
|
+
alias?: string;
|
|
16
22
|
/** True for block directives that occupy their own paragraph. */
|
|
17
23
|
block: boolean;
|
|
18
24
|
};
|
|
@@ -20,15 +26,16 @@ type DirectiveRange = {
|
|
|
20
26
|
* Nesting depth (0-based) of every block-directive opener, derived purely from
|
|
21
27
|
* the scanned ranges by containment: walk the block openers/closers in document
|
|
22
28
|
* order with a kind-aware stack, and record each opener's depth as the stack size
|
|
23
|
-
* before it is pushed. Only `block:true` if/
|
|
29
|
+
* before it is pushed. Only `block:true` if/for pairs participate (inline markers
|
|
24
30
|
* resolve within a paragraph and get no rail).
|
|
25
31
|
*
|
|
26
32
|
* Matching is kind-aware so a mid-edit / unbalanced template stays sane: a closer
|
|
27
|
-
* pops the nearest opener of the *same family* ({
|
|
28
|
-
* {{
|
|
29
|
-
* matching opener is ignored (never decrements a foreign block's
|
|
30
|
-
* open/close counter would mis-count here: e.g. a stray
|
|
31
|
-
*
|
|
33
|
+
* pops the nearest opener of the *same family* (`{% endif %}` ⇒ `{% if %}`,
|
|
34
|
+
* `{% endfor %}` ⇒ `{% for %}`), dropping any still-open openers nested above it;
|
|
35
|
+
* a closer with no matching opener is ignored (never decrements a foreign block's
|
|
36
|
+
* depth). A blind open/close counter would mis-count here: e.g. a stray
|
|
37
|
+
* `{% endfor %}` between `{% if %}` and a nested `{% for %}` would wrongly pull
|
|
38
|
+
* the inner `{% for %}` back to depth 0.
|
|
32
39
|
*
|
|
33
40
|
* Keyed by the opener's `from` PM position, which is unique per marker, so the
|
|
34
41
|
* overlay can look a band's depth up from its opener range. This is a pure
|
|
@@ -3,41 +3,42 @@ import { collectBlockChunks, joinChunks, offsetToDocPos } from "./pmTextScan.js"
|
|
|
3
3
|
import { PluginKey } from "prosemirror-state";
|
|
4
4
|
import { assertNever, isBlockDirectiveKind, scanMarkers } from "@stll/template-conditions";
|
|
5
5
|
//#region src/prosemirror/plugins/templateDirectives.ts
|
|
6
|
-
/** The display expression for a marker (field path, clause name, key,
|
|
6
|
+
/** The display expression for a marker (field path, clause name, key,
|
|
7
|
+
* condition, loop array path, loop property). */
|
|
7
8
|
const directiveExpr = (meta) => {
|
|
8
9
|
switch (meta.kind) {
|
|
9
10
|
case "placeholder": return meta.expr;
|
|
10
11
|
case "clause": return meta.name;
|
|
11
12
|
case "num":
|
|
12
13
|
case "ref": return meta.key;
|
|
14
|
+
case "loop": return meta.property;
|
|
13
15
|
case "if":
|
|
14
|
-
case "
|
|
15
|
-
case "
|
|
16
|
-
case "index":
|
|
17
|
-
case "count":
|
|
16
|
+
case "elif": return meta.expr;
|
|
17
|
+
case "for": return meta.path;
|
|
18
18
|
case "else":
|
|
19
19
|
case "endif":
|
|
20
|
-
case "
|
|
20
|
+
case "endfor": return "";
|
|
21
21
|
default: return assertNever(meta);
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
|
-
/** Block-directive openers ({
|
|
25
|
-
const BLOCK_OPENER_KINDS = /* @__PURE__ */ new Set(["if", "
|
|
26
|
-
/** Block-directive closers ({
|
|
27
|
-
const BLOCK_CLOSER_KINDS = /* @__PURE__ */ new Set(["endif", "
|
|
24
|
+
/** Block-directive openers (`{% if %}`, `{% for %}`) that start a gutter-rail band. */
|
|
25
|
+
const BLOCK_OPENER_KINDS = /* @__PURE__ */ new Set(["if", "for"]);
|
|
26
|
+
/** Block-directive closers (`{% endif %}`, `{% endfor %}`) that end a gutter-rail band. */
|
|
27
|
+
const BLOCK_CLOSER_KINDS = /* @__PURE__ */ new Set(["endif", "endfor"]);
|
|
28
28
|
/**
|
|
29
29
|
* Nesting depth (0-based) of every block-directive opener, derived purely from
|
|
30
30
|
* the scanned ranges by containment: walk the block openers/closers in document
|
|
31
31
|
* order with a kind-aware stack, and record each opener's depth as the stack size
|
|
32
|
-
* before it is pushed. Only `block:true` if/
|
|
32
|
+
* before it is pushed. Only `block:true` if/for pairs participate (inline markers
|
|
33
33
|
* resolve within a paragraph and get no rail).
|
|
34
34
|
*
|
|
35
35
|
* Matching is kind-aware so a mid-edit / unbalanced template stays sane: a closer
|
|
36
|
-
* pops the nearest opener of the *same family* ({
|
|
37
|
-
* {{
|
|
38
|
-
* matching opener is ignored (never decrements a foreign block's
|
|
39
|
-
* open/close counter would mis-count here: e.g. a stray
|
|
40
|
-
*
|
|
36
|
+
* pops the nearest opener of the *same family* (`{% endif %}` ⇒ `{% if %}`,
|
|
37
|
+
* `{% endfor %}` ⇒ `{% for %}`), dropping any still-open openers nested above it;
|
|
38
|
+
* a closer with no matching opener is ignored (never decrements a foreign block's
|
|
39
|
+
* depth). A blind open/close counter would mis-count here: e.g. a stray
|
|
40
|
+
* `{% endfor %}` between `{% if %}` and a nested `{% for %}` would wrongly pull
|
|
41
|
+
* the inner `{% for %}` back to depth 0.
|
|
41
42
|
*
|
|
42
43
|
* Keyed by the opener's `from` PM position, which is unique per marker, so the
|
|
43
44
|
* overlay can look a band's depth up from its opener range. This is a pure
|
|
@@ -54,12 +55,14 @@ const computeBlockDepths = (ranges) => {
|
|
|
54
55
|
stack.push(range.kind);
|
|
55
56
|
continue;
|
|
56
57
|
}
|
|
57
|
-
const wantOpener = range.kind === "endif" ? "if" : "
|
|
58
|
+
const wantOpener = range.kind === "endif" ? "if" : "for";
|
|
58
59
|
const matchIdx = stack.lastIndexOf(wantOpener);
|
|
59
60
|
if (matchIdx !== -1) stack.length = matchIdx;
|
|
60
61
|
}
|
|
61
62
|
return depths;
|
|
62
63
|
};
|
|
64
|
+
/** The loop alias a `for` marker binds, or undefined for every other kind. */
|
|
65
|
+
const directiveAlias = (meta) => meta.kind === "for" ? meta.alias : void 0;
|
|
63
66
|
const scanDirectives = (doc) => {
|
|
64
67
|
const ranges = [];
|
|
65
68
|
for (const chunks of collectBlockChunks(doc)) {
|
|
@@ -69,24 +72,28 @@ const scanDirectives = (doc) => {
|
|
|
69
72
|
const sole = lineMarkers.length === 1 ? lineMarkers[0] : void 0;
|
|
70
73
|
if (sole && sole.raw === trimmed && isBlockDirectiveKind(sole.meta.kind)) {
|
|
71
74
|
const last = chunks.at(-1);
|
|
75
|
+
const alias = directiveAlias(sole.meta);
|
|
72
76
|
ranges.push({
|
|
73
77
|
from: chunks[0]?.start ?? 0,
|
|
74
78
|
to: last ? last.end ?? last.start + last.text.length : 0,
|
|
75
79
|
kind: sole.meta.kind,
|
|
76
80
|
expr: directiveExpr(sole.meta),
|
|
77
|
-
block: true
|
|
81
|
+
block: true,
|
|
82
|
+
...alias !== void 0 ? { alias } : {}
|
|
78
83
|
});
|
|
79
84
|
continue;
|
|
80
85
|
}
|
|
81
86
|
for (const marker of scanMarkers(joined)) {
|
|
82
87
|
const clauseVersion = marker.meta.kind === "clause" ? marker.meta.version : void 0;
|
|
88
|
+
const alias = directiveAlias(marker.meta);
|
|
83
89
|
ranges.push({
|
|
84
90
|
from: offsetToDocPos(chunks, marker.start),
|
|
85
91
|
to: offsetToDocPos(chunks, marker.end, "end"),
|
|
86
92
|
kind: marker.meta.kind,
|
|
87
93
|
expr: directiveExpr(marker.meta),
|
|
88
94
|
block: false,
|
|
89
|
-
...clauseVersion !== void 0 ? { clauseVersion } : {}
|
|
95
|
+
...clauseVersion !== void 0 ? { clauseVersion } : {},
|
|
96
|
+
...alias !== void 0 ? { alias } : {}
|
|
90
97
|
});
|
|
91
98
|
}
|
|
92
99
|
}
|
|
@@ -24,8 +24,8 @@ const atTriggerBoundary = (state, pos) => {
|
|
|
24
24
|
/** Whether `pos` falls strictly inside an existing template directive. The
|
|
25
25
|
* slash activations insert markers as raw text rather than going through
|
|
26
26
|
* `insertInline`'s overlap guard, so opening here would nest markers — e.g. a
|
|
27
|
-
* `/` typed after
|
|
28
|
-
* `{
|
|
27
|
+
* `/` typed after `if ` inside `{% if condition %}` could produce
|
|
28
|
+
* `{% if {{ field }} %}`, which the scanner/fill grammar cannot interpret.
|
|
29
29
|
* Boundaries are exclusive: a caret right before `{{` or after `}}` is fine. */
|
|
30
30
|
const insideDirective = (state, pos) => getTemplateDirectives(state).some((range) => pos > range.from && pos < range.to);
|
|
31
31
|
/** Whether the `/` that opened the trigger is still present at `from`. */
|
|
@@ -121,17 +121,11 @@ type RunPropertyChangeMarkAttrs = {
|
|
|
121
121
|
provenance: TrackedChangeProvenance;
|
|
122
122
|
suggestionId?: string;
|
|
123
123
|
};
|
|
124
|
-
type RunFormattingOverrideAttrs =
|
|
124
|
+
type RunFormattingOverrideAttrs = Partial<Record<"bold" | "boldCs" | "cs" | "italic" | "italicCs" | "strike" | "allCaps" | "smallCaps" | "hidden" | "emboss" | "imprint" | "shadow" | "outline", boolean>> & {
|
|
125
|
+
directFontProperties?: readonly ("fontFamily" | "fontSize" | "color")[];
|
|
125
126
|
doubleStrike?: false;
|
|
126
127
|
rtl?: false;
|
|
127
|
-
/** Independent complex-script weight (`w:bCs`). */
|
|
128
|
-
boldCs?: boolean;
|
|
129
|
-
/** Force complex-script formatting for the full run (`w:cs`). */
|
|
130
|
-
cs?: boolean;
|
|
131
|
-
/** Independent complex-script size in half-points (`w:szCs`). */
|
|
132
128
|
fontSizeCs?: number;
|
|
133
|
-
/** Independent complex-script slant (`w:iCs`). */
|
|
134
|
-
italicCs?: boolean;
|
|
135
129
|
underline?: "none";
|
|
136
130
|
};
|
|
137
131
|
/**
|
|
@@ -191,6 +191,23 @@ const FONT_MAPPINGS = {
|
|
|
191
191
|
unitsPerEm: 2048
|
|
192
192
|
})
|
|
193
193
|
},
|
|
194
|
+
david: {
|
|
195
|
+
googleFont: "Noto Serif Hebrew",
|
|
196
|
+
category: "serif",
|
|
197
|
+
fallbackStack: [
|
|
198
|
+
"David",
|
|
199
|
+
"Noto Serif Hebrew",
|
|
200
|
+
"Times New Roman",
|
|
201
|
+
"serif"
|
|
202
|
+
],
|
|
203
|
+
singleLineRatio: singleLineRatioOf({
|
|
204
|
+
source: "hhea",
|
|
205
|
+
hheaAscent: 1505,
|
|
206
|
+
hheaDescent: -510,
|
|
207
|
+
hheaLineGap: 0,
|
|
208
|
+
unitsPerEm: 2048
|
|
209
|
+
})
|
|
210
|
+
},
|
|
194
211
|
georgia: {
|
|
195
212
|
googleFont: "Tinos",
|
|
196
213
|
category: "serif",
|
|
@@ -628,6 +645,40 @@ const FONT_MAPPINGS = {
|
|
|
628
645
|
note: "Measured at 11pt against a 15.36pt reference-layout line pitch; the face ships with the office suite, not as a readable font file."
|
|
629
646
|
})
|
|
630
647
|
},
|
|
648
|
+
frankruehl: {
|
|
649
|
+
googleFont: "Frank Ruhl Libre",
|
|
650
|
+
category: "serif",
|
|
651
|
+
fallbackStack: [
|
|
652
|
+
"FrankRuehl",
|
|
653
|
+
"Frank Ruhl Libre",
|
|
654
|
+
"Times New Roman",
|
|
655
|
+
"serif"
|
|
656
|
+
],
|
|
657
|
+
singleLineRatio: singleLineRatioOf({
|
|
658
|
+
source: "hhea",
|
|
659
|
+
hheaAscent: 1462,
|
|
660
|
+
hheaDescent: -442,
|
|
661
|
+
hheaLineGap: 0,
|
|
662
|
+
unitsPerEm: 2048
|
|
663
|
+
})
|
|
664
|
+
},
|
|
665
|
+
miriam: {
|
|
666
|
+
googleFont: "Miriam Libre",
|
|
667
|
+
category: "sans-serif",
|
|
668
|
+
fallbackStack: [
|
|
669
|
+
"Miriam",
|
|
670
|
+
"Miriam Libre",
|
|
671
|
+
"Arial",
|
|
672
|
+
"sans-serif"
|
|
673
|
+
],
|
|
674
|
+
singleLineRatio: singleLineRatioOf({
|
|
675
|
+
source: "hhea",
|
|
676
|
+
hheaAscent: 1546,
|
|
677
|
+
hheaDescent: -512,
|
|
678
|
+
hheaLineGap: 0,
|
|
679
|
+
unitsPerEm: 2048
|
|
680
|
+
})
|
|
681
|
+
},
|
|
631
682
|
"ms mincho": {
|
|
632
683
|
googleFont: "Noto Serif JP",
|
|
633
684
|
category: "serif",
|
|
@@ -151,6 +151,33 @@ function borderToStyle(border, side = "", theme) {
|
|
|
151
151
|
});
|
|
152
152
|
return style;
|
|
153
153
|
}
|
|
154
|
+
const HALF_STEP_SHADING_PATTERNS = /* @__PURE__ */ new Set([
|
|
155
|
+
"pct12",
|
|
156
|
+
"pct37",
|
|
157
|
+
"pct62",
|
|
158
|
+
"pct87"
|
|
159
|
+
]);
|
|
160
|
+
const AUTO_PATTERN_COLOR = "000000";
|
|
161
|
+
const AUTO_PATTERN_BACKGROUND = "FFFFFF";
|
|
162
|
+
function percentageShadingRatio(pattern) {
|
|
163
|
+
if (!pattern?.startsWith("pct")) return;
|
|
164
|
+
const percentage = Number.parseInt(pattern.slice(3), 10);
|
|
165
|
+
if (!Number.isFinite(percentage)) return;
|
|
166
|
+
return (percentage + (HALF_STEP_SHADING_PATTERNS.has(pattern) ? .5 : 0)) / 100;
|
|
167
|
+
}
|
|
168
|
+
function resolvePatternHex({ color, fallback, theme }) {
|
|
169
|
+
const resolved = resolveShadingColor(color, theme);
|
|
170
|
+
return /^#(?<hex>[0-9A-F]{6})$/iu.exec(resolved)?.groups?.["hex"]?.toUpperCase() ?? fallback;
|
|
171
|
+
}
|
|
172
|
+
function blendShadingColors({ foreground, background, ratio }) {
|
|
173
|
+
const channels = [];
|
|
174
|
+
for (let offset = 0; offset < 6; offset += 2) {
|
|
175
|
+
const foregroundChannel = Number.parseInt(foreground.slice(offset, offset + 2), 16);
|
|
176
|
+
const backgroundChannel = Number.parseInt(background.slice(offset, offset + 2), 16);
|
|
177
|
+
channels.push(Math.round(foregroundChannel * ratio + backgroundChannel * (1 - ratio)).toString(16).padStart(2, "0"));
|
|
178
|
+
}
|
|
179
|
+
return `#${channels.join("").toUpperCase()}`;
|
|
180
|
+
}
|
|
154
181
|
/**
|
|
155
182
|
* Convert ShadingProperties to background color
|
|
156
183
|
*
|
|
@@ -161,13 +188,26 @@ function borderToStyle(border, side = "", theme) {
|
|
|
161
188
|
function resolveShadingFill(shading, theme) {
|
|
162
189
|
if (!shading) return "";
|
|
163
190
|
if (shading.pattern === "nil") return "";
|
|
191
|
+
const percentageRatio = percentageShadingRatio(shading.pattern);
|
|
192
|
+
if (percentageRatio !== void 0) return blendShadingColors({
|
|
193
|
+
foreground: resolvePatternHex({
|
|
194
|
+
color: shading.color,
|
|
195
|
+
fallback: AUTO_PATTERN_COLOR,
|
|
196
|
+
theme
|
|
197
|
+
}),
|
|
198
|
+
background: resolvePatternHex({
|
|
199
|
+
color: shading.fill,
|
|
200
|
+
fallback: AUTO_PATTERN_BACKGROUND,
|
|
201
|
+
theme
|
|
202
|
+
}),
|
|
203
|
+
ratio: percentageRatio
|
|
204
|
+
});
|
|
164
205
|
if (shading.fill) {
|
|
165
206
|
if (shading.fill.auto) return "";
|
|
166
207
|
if (shading.fill.rgb === "auto" || shading.fill.rgb === "FFFFFF") return "";
|
|
167
208
|
return resolveShadingColor(shading.fill, theme);
|
|
168
209
|
}
|
|
169
210
|
if (shading.pattern === "solid" && shading.color) return resolveShadingColor(shading.color, theme);
|
|
170
|
-
if (shading.pattern && shading.pattern.startsWith("pct") && shading.color) return resolveShadingColor(shading.color, theme);
|
|
171
211
|
return "";
|
|
172
212
|
}
|
|
173
213
|
/**
|
package/dist/watermark/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isEmptyParagraph } from "../docx/paragraphParser.js";
|
|
1
2
|
//#region src/watermark/index.ts
|
|
2
3
|
/**
|
|
3
4
|
* Schemes allowed for a picture watermark's external image target
|
|
@@ -42,6 +43,12 @@ function setDocumentWatermark(doc, watermark) {
|
|
|
42
43
|
const nextHeaders = /* @__PURE__ */ new Map();
|
|
43
44
|
if (headers) for (const [rId, header] of headers) {
|
|
44
45
|
const next = { ...header };
|
|
46
|
+
const retainedHostIndex = next.watermarkBlockIndex;
|
|
47
|
+
const retainedHost = next.rawWatermarkXml !== void 0 && retainedHostIndex !== void 0 ? next.content.at(retainedHostIndex) : void 0;
|
|
48
|
+
if (retainedHostIndex !== void 0 && retainedHost?.type === "paragraph" && isEmptyParagraph(retainedHost)) {
|
|
49
|
+
next.content = [...next.content];
|
|
50
|
+
next.content.splice(retainedHostIndex, 1);
|
|
51
|
+
}
|
|
45
52
|
if (watermark === void 0) {
|
|
46
53
|
delete next.watermark;
|
|
47
54
|
delete next.watermarkBlockIndex;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/folio-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"document-model",
|
|
@@ -115,9 +115,9 @@
|
|
|
115
115
|
"perf": "bun scripts/profile-editor.ts"
|
|
116
116
|
},
|
|
117
117
|
"dependencies": {
|
|
118
|
-
"@stll/docx-core": "^0.19.
|
|
118
|
+
"@stll/docx-core": "^0.19.2",
|
|
119
119
|
"@stll/docx-utils": "^0.1.0",
|
|
120
|
-
"@stll/template-conditions": "^0.
|
|
120
|
+
"@stll/template-conditions": "^0.4.0",
|
|
121
121
|
"better-result": "3.0.1",
|
|
122
122
|
"csstype": "^3.1.3",
|
|
123
123
|
"dompurify": "^3.4.13",
|