@stll/folio-core 0.22.2 → 0.22.3
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/controller/layoutPipeline.js +28 -0
- package/dist/docx/imageParser.js +6 -9
- package/dist/docx/server/inspectDocxPackage.js +8 -7
- package/dist/docx/styleParser.js +16 -16
- package/dist/docx/xmlParser.d.ts +7 -3
- package/dist/docx/xmlParser.js +20 -7
- package/dist/layout-engine/measure/measureHelpers.d.ts +2 -1
- package/dist/layout-engine/measure/measureHelpers.js +5 -2
- package/dist/layout-engine/types.d.ts +6 -0
- package/dist/layout-painter/anchoredImagePosition.d.ts +2 -1
- package/dist/layout-painter/anchoredImagePosition.js +14 -6
- package/dist/layout-painter/renderPage.js +11 -1
- package/dist/layout-painter/renderParagraph.js +3 -4
- package/dist/prosemirror/attrs/index.js +6 -5
- package/dist/prosemirror/commands/propertyChangeScope.d.ts +1 -1
- package/dist/prosemirror/commands/propertyChangeScope.js +1 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +13 -3
- package/dist/prosemirror/conversion/toProseDoc.js +1 -0
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +30 -3
- package/dist/prosemirror/extensions/marks/UnderlineExtension.js +9 -7
- package/dist/prosemirror/extensions/marks/markUtils.d.ts +2 -1
- package/dist/prosemirror/extensions/marks/markUtils.js +92 -16
- package/dist/prosemirror/plugins/selectionTracker.js +10 -5
- package/dist/prosemirror/schema/nodes.d.ts +1 -0
- package/dist/prosemirror/selectionMarks.d.ts +11 -0
- package/dist/prosemirror/selectionMarks.js +19 -0
- package/dist/prosemirror/selectionState.js +8 -20
- package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
- package/dist/utils/formatToStyle.js +1 -1
- package/package.json +1 -1
|
@@ -58,6 +58,29 @@ function bodyBlocksClearSectionHeaderFooter(blocks, { authoredMargins, sectionHe
|
|
|
58
58
|
});
|
|
59
59
|
return changed ? nextBlocks : blocks;
|
|
60
60
|
}
|
|
61
|
+
const arePageMarginsEqual = (left, right) => left.top === right.top && left.right === right.right && left.bottom === right.bottom && left.left === right.left && left.header === right.header && left.footer === right.footer;
|
|
62
|
+
const mirrorPageMarginsIfNeeded = (authoredMargins, pageNumber, mirrorMargins) => mirrorMargins && pageNumber % 2 === 0 ? {
|
|
63
|
+
...authoredMargins,
|
|
64
|
+
left: authoredMargins.right,
|
|
65
|
+
right: authoredMargins.left
|
|
66
|
+
} : authoredMargins;
|
|
67
|
+
function attachAuthoredMarginsToLayoutPages(layout, options) {
|
|
68
|
+
let changed = false;
|
|
69
|
+
const pages = layout.pages.map((page) => {
|
|
70
|
+
const sectionProperties = options.sectionPropertiesForMargins[page.sectionIndex ?? 0];
|
|
71
|
+
const authoredMargins = mirrorPageMarginsIfNeeded(sectionProperties ? getMargins(sectionProperties) : options.authoredMargins, page.number, options.mirrorMargins);
|
|
72
|
+
if (page.authoredMargins && arePageMarginsEqual(page.authoredMargins, authoredMargins)) return page;
|
|
73
|
+
changed = true;
|
|
74
|
+
return {
|
|
75
|
+
...page,
|
|
76
|
+
authoredMargins
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
return changed ? {
|
|
80
|
+
...layout,
|
|
81
|
+
pages
|
|
82
|
+
} : layout;
|
|
83
|
+
}
|
|
61
84
|
function runLayoutPipeline(deps, state, options = {}) {
|
|
62
85
|
const { contentWidth, columns, pageSize, margins, pageGap, showMarginGuides, marginGuideColor, syncCoordinator, headerContent, footerContent, firstPageHeaderContent, firstPageFooterContent, headerContentRId, footerContentRId, firstPageHeaderContentRId, firstPageFooterContentRId, sectionHeaderFooterRefs, theme: _theme, sectionProperties, document, defaultTabStop, mirrorMargins, styles, layout, hfPMs, painter, pagesContainer, session, renderHfFromContentOrPm, renderHeaderFooterContentByRId, documentFontsAreLoaded, buildFootnoteRenderItems, describeInvalidHighlightMarks, emptyTemplatePreviewEntries: EMPTY_TEMPLATE_PREVIEW_ENTRIES } = deps;
|
|
63
86
|
let outcome = {};
|
|
@@ -341,6 +364,11 @@ function runLayoutPipeline(deps, state, options = {}) {
|
|
|
341
364
|
};
|
|
342
365
|
let stabilizedFieldValues;
|
|
343
366
|
stabilizeFieldWidths();
|
|
367
|
+
newLayout = attachAuthoredMarginsToLayoutPages(newLayout, {
|
|
368
|
+
authoredMargins: margins,
|
|
369
|
+
mirrorMargins,
|
|
370
|
+
sectionPropertiesForMargins
|
|
371
|
+
});
|
|
344
372
|
const rebuildHeaderFooterForLayout = () => {
|
|
345
373
|
const seqValues = buildSeqValues(newBlocks);
|
|
346
374
|
const bookmarkTextInputs = stabilizedFieldValues === void 0 ? { seqValues } : {
|
package/dist/docx/imageParser.js
CHANGED
|
@@ -4,7 +4,7 @@ import { sanitizeExternalUrl } from "../utils/urlSecurity.js";
|
|
|
4
4
|
import { WRAP_ELEMENT_NAMES, parsePositionH, parsePositionV, parseWrapElement } from "./drawingUtils.js";
|
|
5
5
|
import { resolveTarget } from "./relsParser.js";
|
|
6
6
|
import { isTextBoxDrawing } from "./textBoxParser.js";
|
|
7
|
-
import { findByFullName, findChild, getAttribute, getChildElements, parseNumericAttribute } from "./xmlParser.js";
|
|
7
|
+
import { findByFullName, findChild, getAttribute, getChildElements, parseNumericAttribute, parseOnOffValue } from "./xmlParser.js";
|
|
8
8
|
//#region src/docx/imageParser.ts
|
|
9
9
|
/**
|
|
10
10
|
* Convert rotation value (1/60000 of a degree) to degrees
|
|
@@ -74,7 +74,7 @@ function parseDocProps(docPr) {
|
|
|
74
74
|
const name = getAttribute(docPr, null, "name");
|
|
75
75
|
const descr = getAttribute(docPr, null, "descr");
|
|
76
76
|
const title = getAttribute(docPr, null, "title");
|
|
77
|
-
const decorative = getAttribute(docPr, null, "decorative") ===
|
|
77
|
+
const decorative = parseOnOffValue(getAttribute(docPr, null, "decorative")) === true;
|
|
78
78
|
const hlinkClickEl = findChild(docPr, "a", "hlinkClick");
|
|
79
79
|
const hlinkRId = hlinkClickEl ? getAttribute(hlinkClickEl, "r", "id") : null;
|
|
80
80
|
return {
|
|
@@ -92,8 +92,8 @@ function parseDocProps(docPr) {
|
|
|
92
92
|
function parseTransform(xfrm) {
|
|
93
93
|
if (!xfrm) return;
|
|
94
94
|
const rot = getAttribute(xfrm, null, "rot");
|
|
95
|
-
const flipH = getAttribute(xfrm, null, "flipH") ===
|
|
96
|
-
const flipV = getAttribute(xfrm, null, "flipV") ===
|
|
95
|
+
const flipH = parseOnOffValue(getAttribute(xfrm, null, "flipH")) === true;
|
|
96
|
+
const flipV = parseOnOffValue(getAttribute(xfrm, null, "flipV")) === true;
|
|
97
97
|
const rotation = rotToDegrees(rot);
|
|
98
98
|
if (rotation === void 0 && !flipH && !flipV) return;
|
|
99
99
|
const transform = {};
|
|
@@ -155,10 +155,7 @@ function parseImageCrop(blipFill) {
|
|
|
155
155
|
* spec-defined default.
|
|
156
156
|
*/
|
|
157
157
|
function parseOnOffAttr(element, name) {
|
|
158
|
-
|
|
159
|
-
if (raw === null) return;
|
|
160
|
-
if (raw === "1" || raw === "true" || raw === "on") return true;
|
|
161
|
-
if (raw === "0" || raw === "false" || raw === "off") return false;
|
|
158
|
+
return parseOnOffValue(getAttribute(element, null, name));
|
|
162
159
|
}
|
|
163
160
|
/**
|
|
164
161
|
* Parse `<a:alphaModFix amt="..."/>` inside the `a:blip` element. The
|
|
@@ -358,7 +355,7 @@ function parseAnchor(anchorEl, rels, media) {
|
|
|
358
355
|
const size = parseExtent(findByFullName(anchorEl, "wp:extent"));
|
|
359
356
|
const padding = parseEffectExtent(findByFullName(anchorEl, "wp:effectExtent"));
|
|
360
357
|
const props = parseDocProps(findByFullName(anchorEl, "wp:docPr"));
|
|
361
|
-
const behindDoc = getAttribute(anchorEl, null, "behindDoc") ===
|
|
358
|
+
const behindDoc = parseOnOffValue(getAttribute(anchorEl, null, "behindDoc")) === true;
|
|
362
359
|
const layoutInCell = parseOnOffAttr(anchorEl, "layoutInCell");
|
|
363
360
|
const allowOverlap = parseOnOffAttr(anchorEl, "allowOverlap");
|
|
364
361
|
const anchorDistT = parseNumericAttribute(anchorEl, null, "distT");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getAttributeAnyPrefix, getLocalName, parseXmlDocument } from "../xmlParser.js";
|
|
2
2
|
import { DocxArchiveError, loadDocxArchive } from "./boundedArchive.js";
|
|
3
|
-
import { TaggedError } from "better-result";
|
|
3
|
+
import { Result, TaggedError } from "better-result";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
//#region src/docx/server/inspectDocxPackage.ts
|
|
6
6
|
const FOLIO_DOCX_PACKAGE_INSPECTION_VERSION = 1;
|
|
@@ -32,16 +32,17 @@ const decodeXml = (bytes, part) => {
|
|
|
32
32
|
let encoding = "utf-8";
|
|
33
33
|
if (bytes.length >= 2 && (bytes[0] === 255 && bytes[1] === 254 || bytes[0] === 60 && bytes[1] === 0)) encoding = "utf-16le";
|
|
34
34
|
else if (bytes.length >= 2 && (bytes[0] === 254 && bytes[1] === 255 || bytes[0] === 0 && bytes[1] === 60)) encoding = "utf-16be";
|
|
35
|
-
try
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
throw new FolioDocxPackageInspectionError({
|
|
35
|
+
const decoded = Result.try({
|
|
36
|
+
try: () => new TextDecoder(encoding, { fatal: true }).decode(bytes),
|
|
37
|
+
catch: (cause) => new FolioDocxPackageInspectionError({
|
|
39
38
|
message: `Failed to decode XML package part "${part}"`,
|
|
40
39
|
code: "xml-decode-failed",
|
|
41
40
|
part,
|
|
42
41
|
cause
|
|
43
|
-
})
|
|
44
|
-
}
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
if (decoded.isOk()) return decoded.value;
|
|
45
|
+
throw decoded.error;
|
|
45
46
|
};
|
|
46
47
|
const parseContentTypes = (xml) => {
|
|
47
48
|
const declarations = {
|
package/dist/docx/styleParser.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mergeParagraphFormatting } from "../utils/paragraphFormattingMerge.js";
|
|
|
2
2
|
import { mergeTextFormatting } from "../utils/textFormattingMerge.js";
|
|
3
3
|
import { BorderStyleSchema, ConditionalStyleTypeSchema, EmphasisMarkSchema, FontHintSchema, FontThemeSchema, HighlightColorSchema, LineSpacingRuleSchema, ParagraphAlignmentSchema, ShadingPatternSchema, StyleTypeSchema, TabLeaderSchema, TabStopAlignmentSchema, TableCellTextDirectionSchema, TableRowHeightRuleSchema, TableWidthTypeSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
|
|
4
4
|
import { resolveThemeFontRef } from "./themeParser.js";
|
|
5
|
-
import { findChild, findChildren, getAttribute, getLocalName, parseBooleanElement, parseNumberingLevelAttribute, parseNumericAttribute, parseTableMeasurementValue, parseXmlDocument } from "./xmlParser.js";
|
|
5
|
+
import { findChild, findChildren, getAttribute, getLocalName, parseBooleanElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXmlDocument } from "./xmlParser.js";
|
|
6
6
|
//#region src/docx/styleParser.ts
|
|
7
7
|
/**
|
|
8
8
|
* Parse text formatting properties (w:rPr)
|
|
@@ -224,9 +224,9 @@ function parseBorderSpec(border) {
|
|
|
224
224
|
const space = parseNumericAttribute(border, "w", "space");
|
|
225
225
|
if (space !== void 0) spec.space = space;
|
|
226
226
|
const shadowAttr = getAttribute(border, "w", "shadow");
|
|
227
|
-
if (shadowAttr) spec.shadow = shadowAttr
|
|
227
|
+
if (shadowAttr) spec.shadow = parseOnOffValue(shadowAttr) ?? false;
|
|
228
228
|
const frame = getAttribute(border, "w", "frame");
|
|
229
|
-
if (frame) spec.frame = frame
|
|
229
|
+
if (frame) spec.frame = parseOnOffValue(frame) ?? false;
|
|
230
230
|
return spec;
|
|
231
231
|
}
|
|
232
232
|
/**
|
|
@@ -278,9 +278,9 @@ function parseParagraphProperties(pPr, theme) {
|
|
|
278
278
|
const lineRule = narrowEnum(getAttribute(spacing, "w", "lineRule"), LineSpacingRuleSchema);
|
|
279
279
|
if (lineRule) formatting.lineSpacingRule = lineRule;
|
|
280
280
|
const beforeAuto = getAttribute(spacing, "w", "beforeAutospacing");
|
|
281
|
-
if (beforeAuto) formatting.beforeAutospacing = beforeAuto
|
|
281
|
+
if (beforeAuto) formatting.beforeAutospacing = parseOnOffValue(beforeAuto) ?? false;
|
|
282
282
|
const afterAuto = getAttribute(spacing, "w", "afterAutospacing");
|
|
283
|
-
if (afterAuto) formatting.afterAutospacing = afterAuto
|
|
283
|
+
if (afterAuto) formatting.afterAutospacing = parseOnOffValue(afterAuto) ?? false;
|
|
284
284
|
}
|
|
285
285
|
const ind = findChild(pPr, "w", "ind");
|
|
286
286
|
if (ind) {
|
|
@@ -452,17 +452,17 @@ function parseTableLook(tblLook) {
|
|
|
452
452
|
}
|
|
453
453
|
}
|
|
454
454
|
const firstColumn = getAttribute(tblLook, "w", "firstColumn");
|
|
455
|
-
if (firstColumn) look.firstColumn = firstColumn
|
|
455
|
+
if (firstColumn) look.firstColumn = parseOnOffValue(firstColumn) ?? false;
|
|
456
456
|
const firstRow = getAttribute(tblLook, "w", "firstRow");
|
|
457
|
-
if (firstRow) look.firstRow = firstRow
|
|
457
|
+
if (firstRow) look.firstRow = parseOnOffValue(firstRow) ?? false;
|
|
458
458
|
const lastColumn = getAttribute(tblLook, "w", "lastColumn");
|
|
459
|
-
if (lastColumn) look.lastColumn = lastColumn
|
|
459
|
+
if (lastColumn) look.lastColumn = parseOnOffValue(lastColumn) ?? false;
|
|
460
460
|
const lastRow = getAttribute(tblLook, "w", "lastRow");
|
|
461
|
-
if (lastRow) look.lastRow = lastRow
|
|
461
|
+
if (lastRow) look.lastRow = parseOnOffValue(lastRow) ?? false;
|
|
462
462
|
const noHBand = getAttribute(tblLook, "w", "noHBand");
|
|
463
|
-
if (noHBand) look.noHBand = noHBand
|
|
463
|
+
if (noHBand) look.noHBand = parseOnOffValue(noHBand) ?? false;
|
|
464
464
|
const noVBand = getAttribute(tblLook, "w", "noVBand");
|
|
465
|
-
if (noVBand) look.noVBand = noVBand
|
|
465
|
+
if (noVBand) look.noVBand = parseOnOffValue(noVBand) ?? false;
|
|
466
466
|
return Object.keys(look).length > 0 ? look : void 0;
|
|
467
467
|
}
|
|
468
468
|
/**
|
|
@@ -669,7 +669,7 @@ function parseStyle(styleEl, theme) {
|
|
|
669
669
|
type: narrowEnum(rawType, StyleTypeSchema) ?? "paragraph"
|
|
670
670
|
};
|
|
671
671
|
const defaultAttr = getAttribute(styleEl, "w", "default");
|
|
672
|
-
if (defaultAttr) style.default = defaultAttr
|
|
672
|
+
if (defaultAttr) style.default = parseOnOffValue(defaultAttr) ?? false;
|
|
673
673
|
const children = collectStyleChildren(styleEl);
|
|
674
674
|
const nameEl = children.name;
|
|
675
675
|
if (nameEl) {
|
|
@@ -871,10 +871,10 @@ function parseStyleDefinitionsFromDocument(doc, theme, resolvedStyles) {
|
|
|
871
871
|
const latentStylesEl = findChild(doc, "w", "latentStyles");
|
|
872
872
|
if (latentStylesEl) {
|
|
873
873
|
const latentStyles = {
|
|
874
|
-
defLockedState: getAttribute(latentStylesEl, "w", "defLockedState")
|
|
875
|
-
defSemiHidden: getAttribute(latentStylesEl, "w", "defSemiHidden")
|
|
876
|
-
defUnhideWhenUsed: getAttribute(latentStylesEl, "w", "defUnhideWhenUsed")
|
|
877
|
-
defQFormat: getAttribute(latentStylesEl, "w", "defQFormat")
|
|
874
|
+
defLockedState: parseOnOffValue(getAttribute(latentStylesEl, "w", "defLockedState")) ?? false,
|
|
875
|
+
defSemiHidden: parseOnOffValue(getAttribute(latentStylesEl, "w", "defSemiHidden")) ?? false,
|
|
876
|
+
defUnhideWhenUsed: parseOnOffValue(getAttribute(latentStylesEl, "w", "defUnhideWhenUsed")) ?? false,
|
|
877
|
+
defQFormat: parseOnOffValue(getAttribute(latentStylesEl, "w", "defQFormat")) ?? false
|
|
878
878
|
};
|
|
879
879
|
const defUIPriority = parseNumericAttribute(latentStylesEl, "w", "defUIPriority");
|
|
880
880
|
if (defUIPriority !== void 0) latentStyles.defUIPriority = defUIPriority;
|
package/dist/docx/xmlParser.d.ts
CHANGED
|
@@ -272,13 +272,17 @@ declare function parseNumberingLevelAttribute(element: XmlElement | null | undef
|
|
|
272
272
|
* (`5000`); normalize those to the ECMA-376 unit the layout engine expects.
|
|
273
273
|
*/
|
|
274
274
|
declare function parseTableMeasurementValue(element: XmlElement | null | undefined, widthType: string): number | undefined;
|
|
275
|
+
/**
|
|
276
|
+
* Parse an OOXML `ST_OnOff` lexical value.
|
|
277
|
+
*/
|
|
278
|
+
declare function parseOnOffValue(value: string | null | undefined): boolean | undefined;
|
|
275
279
|
/**
|
|
276
280
|
* Parse a boolean value from an attribute or element presence
|
|
277
281
|
*
|
|
278
282
|
* OOXML boolean conventions:
|
|
279
283
|
* - Element presence with no val attribute = true
|
|
280
|
-
* - w:val="true" or w:val="
|
|
281
|
-
* - w:val="false" or w:val="
|
|
284
|
+
* - w:val="true", w:val="1", or w:val="on" = true
|
|
285
|
+
* - w:val="false", w:val="0", or w:val="off" = false
|
|
282
286
|
*
|
|
283
287
|
* @param element - Element to check
|
|
284
288
|
* @param namespace - Namespace for val attribute
|
|
@@ -334,4 +338,4 @@ declare function mergeXmlnsDeclarations(inherited: Record<string, string>, eleme
|
|
|
334
338
|
*/
|
|
335
339
|
declare function cloneWithXmlnsDeclarations(element: XmlElement, xmlnsDecls: Record<string, string>): XmlElement;
|
|
336
340
|
//#endregion
|
|
337
|
-
export { NAMESPACES, WORDPROCESSINGML_NAMESPACE_URIS, XmlElement, XmlNamespaceScope, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseTableMeasurementValue, parseXml, parseXmlDocument };
|
|
341
|
+
export { NAMESPACES, WORDPROCESSINGML_NAMESPACE_URIS, XmlElement, XmlNamespaceScope, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXml, parseXmlDocument };
|
package/dist/docx/xmlParser.js
CHANGED
|
@@ -438,8 +438,7 @@ function getTextContent(element) {
|
|
|
438
438
|
function hasFlag(element, namespace, name) {
|
|
439
439
|
const value = getAttribute(element, namespace, name);
|
|
440
440
|
if (value === null) return false;
|
|
441
|
-
|
|
442
|
-
return true;
|
|
441
|
+
return parseOnOffValue(value) ?? true;
|
|
443
442
|
}
|
|
444
443
|
/**
|
|
445
444
|
* Check if a child element exists (used for boolean flags in OOXML)
|
|
@@ -515,12 +514,27 @@ function parseTableMeasurementValue(element, widthType) {
|
|
|
515
514
|
return Number.isNaN(num) ? void 0 : num;
|
|
516
515
|
}
|
|
517
516
|
/**
|
|
517
|
+
* Parse an OOXML `ST_OnOff` lexical value.
|
|
518
|
+
*/
|
|
519
|
+
function parseOnOffValue(value) {
|
|
520
|
+
if (value === null || value === void 0) return;
|
|
521
|
+
switch (value) {
|
|
522
|
+
case "1":
|
|
523
|
+
case "true":
|
|
524
|
+
case "on": return true;
|
|
525
|
+
case "0":
|
|
526
|
+
case "false":
|
|
527
|
+
case "off": return false;
|
|
528
|
+
default: return;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
518
532
|
* Parse a boolean value from an attribute or element presence
|
|
519
533
|
*
|
|
520
534
|
* OOXML boolean conventions:
|
|
521
535
|
* - Element presence with no val attribute = true
|
|
522
|
-
* - w:val="true" or w:val="
|
|
523
|
-
* - w:val="false" or w:val="
|
|
536
|
+
* - w:val="true", w:val="1", or w:val="on" = true
|
|
537
|
+
* - w:val="false", w:val="0", or w:val="off" = false
|
|
524
538
|
*
|
|
525
539
|
* @param element - Element to check
|
|
526
540
|
* @param namespace - Namespace for val attribute
|
|
@@ -541,8 +555,7 @@ function parseBooleanElement(element, namespace = "w") {
|
|
|
541
555
|
}
|
|
542
556
|
}
|
|
543
557
|
if (val === null) return true;
|
|
544
|
-
|
|
545
|
-
return true;
|
|
558
|
+
return parseOnOffValue(val) ?? true;
|
|
546
559
|
}
|
|
547
560
|
/**
|
|
548
561
|
* Deep find - search recursively for an element
|
|
@@ -699,4 +712,4 @@ function cloneWithXmlnsDeclarations(element, xmlnsDecls) {
|
|
|
699
712
|
return element;
|
|
700
713
|
}
|
|
701
714
|
//#endregion
|
|
702
|
-
export { NAMESPACES, WORDPROCESSINGML_NAMESPACE_URIS, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseTableMeasurementValue, parseXml, parseXmlDocument };
|
|
715
|
+
export { NAMESPACES, WORDPROCESSINGML_NAMESPACE_URIS, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXml, parseXmlDocument };
|
|
@@ -3,6 +3,7 @@ import { FontStyle } from "./measureTypes.js";
|
|
|
3
3
|
//#region src/layout-engine/measure/measureHelpers.d.ts
|
|
4
4
|
declare const DEFAULT_FONT_SIZE = 11;
|
|
5
5
|
declare const DEFAULT_FONT_FAMILY = "Calibri";
|
|
6
|
+
declare const DOCX_SCRIPT_FONT_SCALE = 0.75;
|
|
6
7
|
/**
|
|
7
8
|
* Build a measurement `FontStyle` from a run's formatting. Single source of
|
|
8
9
|
* truth for run → FontStyle so every measurement path (layout line-breaking,
|
|
@@ -87,4 +88,4 @@ declare function halfPtToPx(halfPt: number): number;
|
|
|
87
88
|
*/
|
|
88
89
|
declare function pxToHalfPt(px: number): number;
|
|
89
90
|
//#endregion
|
|
90
|
-
export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, buildFontString, buildRunFontStyle, clearFontResolvedCache, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
|
|
91
|
+
export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, DOCX_SCRIPT_FONT_SCALE, buildFontString, buildRunFontStyle, clearFontResolvedCache, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
|
|
@@ -9,12 +9,14 @@ import { FONT_KERNING_MODE, getRunFontKerningMode } from "./textMeasurementPolic
|
|
|
9
9
|
* char-offset geometry. The layout engine imports these directly so it never
|
|
10
10
|
* transitively pulls the canvas measurement backend; the canvas implementation
|
|
11
11
|
* (`measureContainer.ts`) consumes them too.
|
|
12
|
+
* Script sizing: eigenpal/docx-editor@585413d0 (Apache-2.0), modified for Folio.
|
|
12
13
|
*/
|
|
13
14
|
const TWIPS_PER_INCH = 1440;
|
|
14
15
|
const PX_PER_INCH = 96;
|
|
15
16
|
const TWIPS_PER_PX = TWIPS_PER_INCH / PX_PER_INCH;
|
|
16
17
|
const DEFAULT_FONT_SIZE = 11;
|
|
17
18
|
const DEFAULT_FONT_FAMILY = "Calibri";
|
|
19
|
+
const DOCX_SCRIPT_FONT_SCALE = .75;
|
|
18
20
|
/**
|
|
19
21
|
* Build a measurement `FontStyle` from a run's formatting. Single source of
|
|
20
22
|
* truth for run → FontStyle so every measurement path (layout line-breaking,
|
|
@@ -24,7 +26,8 @@ const DEFAULT_FONT_FAMILY = "Calibri";
|
|
|
24
26
|
* and size fallbacks for runs that declare neither.
|
|
25
27
|
*/
|
|
26
28
|
function buildRunFontStyle(run, fallbackFontFamily, fallbackFontSize) {
|
|
27
|
-
const
|
|
29
|
+
const baseFontSize = run.fontSize ?? fallbackFontSize;
|
|
30
|
+
const fontSize = run.superscript || run.subscript ? baseFontSize * DOCX_SCRIPT_FONT_SCALE : baseFontSize;
|
|
28
31
|
return {
|
|
29
32
|
fontFamily: run.fontFamily ?? fallbackFontFamily,
|
|
30
33
|
...run.eastAsiaFontFamily !== void 0 ? { eastAsiaFontFamily: run.eastAsiaFontFamily } : {},
|
|
@@ -168,4 +171,4 @@ function pxToHalfPt(px) {
|
|
|
168
171
|
return pxToPt(px) * 2;
|
|
169
172
|
}
|
|
170
173
|
//#endregion
|
|
171
|
-
export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, buildFontString, buildRunFontStyle, clearFontResolvedCache, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
|
|
174
|
+
export { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, DOCX_SCRIPT_FONT_SCALE, buildFontString, buildRunFontStyle, clearFontResolvedCache, findCharacterAtX, getResolvedData, getXForCharacter, halfPtToPx, ptToPx, pxToHalfPt, pxToPt, pxToTwips, twipsToPx };
|
|
@@ -1063,6 +1063,12 @@ type Page = {
|
|
|
1063
1063
|
fragments: Fragment[];
|
|
1064
1064
|
/** Page margins. */
|
|
1065
1065
|
margins: PageMargins;
|
|
1066
|
+
/**
|
|
1067
|
+
* Authored section margins before header/footer clearance expands the body
|
|
1068
|
+
* content box insets. Page-margin-relative anchors use these page-setup
|
|
1069
|
+
* landmarks instead of furniture-expanded content insets.
|
|
1070
|
+
*/
|
|
1071
|
+
authoredMargins?: PageMargins;
|
|
1066
1072
|
/** Page size (width, height). */
|
|
1067
1073
|
size: {
|
|
1068
1074
|
w: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ImageRun } from "../layout-engine/types.js";
|
|
1
|
+
import { ImageRun, PageMargins } from "../layout-engine/types.js";
|
|
2
2
|
//#region src/layout-painter/anchoredImagePosition.d.ts
|
|
3
3
|
type PageGeometry = {
|
|
4
4
|
pageWidth: number;
|
|
@@ -7,6 +7,7 @@ type PageGeometry = {
|
|
|
7
7
|
marginTop: number;
|
|
8
8
|
marginRight: number;
|
|
9
9
|
marginBottom: number;
|
|
10
|
+
authoredMargins?: PageMargins;
|
|
10
11
|
contentWidth: number;
|
|
11
12
|
contentHeight: number;
|
|
12
13
|
};
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { emuToPixels } from "./renderUtils.js";
|
|
2
2
|
//#region src/layout-painter/anchoredImagePosition.ts
|
|
3
|
+
const authoredMargins = (geometry) => geometry.authoredMargins ?? {
|
|
4
|
+
top: geometry.marginTop,
|
|
5
|
+
right: geometry.marginRight,
|
|
6
|
+
bottom: geometry.marginBottom,
|
|
7
|
+
left: geometry.marginLeft
|
|
8
|
+
};
|
|
3
9
|
function resolveHorizontalBand(relativeTo, geometry) {
|
|
10
|
+
const margins = authoredMargins(geometry);
|
|
4
11
|
switch (relativeTo) {
|
|
5
12
|
case "page": return {
|
|
6
13
|
baseX: -geometry.marginLeft,
|
|
@@ -9,12 +16,12 @@ function resolveHorizontalBand(relativeTo, geometry) {
|
|
|
9
16
|
case "leftMargin":
|
|
10
17
|
case "insideMargin": return {
|
|
11
18
|
baseX: -geometry.marginLeft,
|
|
12
|
-
bandWidth:
|
|
19
|
+
bandWidth: margins.left
|
|
13
20
|
};
|
|
14
21
|
case "rightMargin":
|
|
15
22
|
case "outsideMargin": return {
|
|
16
|
-
baseX: geometry.
|
|
17
|
-
bandWidth:
|
|
23
|
+
baseX: geometry.pageWidth - geometry.marginLeft - margins.right,
|
|
24
|
+
bandWidth: margins.right
|
|
18
25
|
};
|
|
19
26
|
case "character": return {
|
|
20
27
|
baseX: 0,
|
|
@@ -27,6 +34,7 @@ function resolveHorizontalBand(relativeTo, geometry) {
|
|
|
27
34
|
}
|
|
28
35
|
}
|
|
29
36
|
function resolveVerticalBand(relativeTo, fragmentY, geometry) {
|
|
37
|
+
const margins = authoredMargins(geometry);
|
|
30
38
|
switch (relativeTo) {
|
|
31
39
|
case "page": return {
|
|
32
40
|
baseY: -geometry.marginTop,
|
|
@@ -34,11 +42,11 @@ function resolveVerticalBand(relativeTo, fragmentY, geometry) {
|
|
|
34
42
|
};
|
|
35
43
|
case "topMargin": return {
|
|
36
44
|
baseY: -geometry.marginTop,
|
|
37
|
-
bandHeight:
|
|
45
|
+
bandHeight: margins.top
|
|
38
46
|
};
|
|
39
47
|
case "bottomMargin": return {
|
|
40
|
-
baseY: geometry.
|
|
41
|
-
bandHeight:
|
|
48
|
+
baseY: geometry.pageHeight - geometry.marginTop - margins.bottom,
|
|
49
|
+
bandHeight: margins.bottom
|
|
42
50
|
};
|
|
43
51
|
case "paragraph":
|
|
44
52
|
case "line": return {
|
|
@@ -797,6 +797,7 @@ function renderPage(page, context, options = {}) {
|
|
|
797
797
|
marginTop: page.margins.top,
|
|
798
798
|
marginRight: page.margins.right,
|
|
799
799
|
marginBottom: page.margins.bottom,
|
|
800
|
+
...page.authoredMargins ? { authoredMargins: page.authoredMargins } : {},
|
|
800
801
|
contentWidth,
|
|
801
802
|
contentHeight: page.size.h - page.margins.top - page.margins.bottom
|
|
802
803
|
};
|
|
@@ -1209,10 +1210,19 @@ function computePageFingerprint(page, blockLookup) {
|
|
|
1209
1210
|
function computePageRenderFingerprint(page, blockLookup) {
|
|
1210
1211
|
return computePageFingerprintInternal(page, blockLookup, { includePmPositions: false });
|
|
1211
1212
|
}
|
|
1213
|
+
const pageMarginsFingerprint = (margins) => [
|
|
1214
|
+
margins.top,
|
|
1215
|
+
margins.right,
|
|
1216
|
+
margins.bottom,
|
|
1217
|
+
margins.left,
|
|
1218
|
+
margins.header ?? "",
|
|
1219
|
+
margins.footer ?? ""
|
|
1220
|
+
].join(",");
|
|
1212
1221
|
function computePageFingerprintInternal(page, blockLookup, options) {
|
|
1213
1222
|
const parts = [];
|
|
1214
1223
|
parts.push(`s:${page.size.w},${page.size.h}`);
|
|
1215
|
-
parts.push(`m:${page.margins
|
|
1224
|
+
parts.push(`m:${pageMarginsFingerprint(page.margins)}`);
|
|
1225
|
+
if (page.authoredMargins) parts.push(`am:${pageMarginsFingerprint(page.authoredMargins)}`);
|
|
1216
1226
|
parts.push(`n:${page.number}`);
|
|
1217
1227
|
if (page.sectionIndex !== void 0) parts.push(`si:${page.sectionIndex}`);
|
|
1218
1228
|
if (page.sectionPageNumber !== void 0) parts.push(`sp:${page.sectionPageNumber}`);
|
|
@@ -2,7 +2,7 @@ import { ommlToMathml } from "../docx/mathToMathml.js";
|
|
|
2
2
|
import { parseXmlDocument } from "../docx/xmlParser.js";
|
|
3
3
|
import { evaluateFieldInstruction } from "../fields/evaluateField.js";
|
|
4
4
|
import { getListMarkerInlineWidth, getListMarkerVisualOffset } from "../layout-engine/measure/listMarkerWidth.js";
|
|
5
|
-
import "../layout-engine/measure/measureHelpers.js";
|
|
5
|
+
import { DOCX_SCRIPT_FONT_SCALE } from "../layout-engine/measure/measureHelpers.js";
|
|
6
6
|
import { FONT_KERNING_MODE, countCompressibleSpaces, getFontKerningMode, getRunFontKerningMode, toPaintedText } from "../layout-engine/measure/textMeasurementPolicy.js";
|
|
7
7
|
import { isFloatingImageRun } from "../layout-engine/types.js";
|
|
8
8
|
import { calculateTabWidth } from "../prosemirror/utils/tabCalculator.js";
|
|
@@ -76,7 +76,6 @@ function isMathRun(run) {
|
|
|
76
76
|
}
|
|
77
77
|
const AUTOMATIC_TEXT_COLOR_VALUES = /* @__PURE__ */ new Set(["auto", "windowtext"]);
|
|
78
78
|
const DEFAULT_BLACK_TEXT_COLOR_VALUES = /* @__PURE__ */ new Set(["000000", "000"]);
|
|
79
|
-
const DOCX_SUPERSCRIPT_SCALE = .75;
|
|
80
79
|
const SUGGESTION_COLOR_CSS = "var(--suggestion-color, #6d3bd6)";
|
|
81
80
|
const SUGGESTION_TINT_CSS = "var(--suggestion-bg, color-mix(in oklch, #6d3bd6 12%, transparent))";
|
|
82
81
|
const SUGGESTION_TINT_LAYER_CSS = `linear-gradient(${SUGGESTION_TINT_CSS}, ${SUGGESTION_TINT_CSS})`;
|
|
@@ -109,8 +108,8 @@ function fontSizePtToPx(fontSizePt) {
|
|
|
109
108
|
return fontSizePt * 96 / 72;
|
|
110
109
|
}
|
|
111
110
|
function getRaisedRunFontSize(run) {
|
|
112
|
-
if (run.fontSize) return `${fontSizePtToPx(run.fontSize) *
|
|
113
|
-
return `${
|
|
111
|
+
if (run.fontSize) return `${fontSizePtToPx(run.fontSize) * DOCX_SCRIPT_FONT_SCALE}px`;
|
|
112
|
+
return `${DOCX_SCRIPT_FONT_SCALE}em`;
|
|
114
113
|
}
|
|
115
114
|
/**
|
|
116
115
|
* Apply text run styles to an element
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FIELD_TYPE_VALUES, HIGHLIGHT_COLOR_VALUES, IMAGE_HORIZONTAL_ALIGNMENT_VALUES, IMAGE_HORIZONTAL_RELATIVE_TO_VALUES, IMAGE_VERTICAL_ALIGNMENT_VALUES, IMAGE_VERTICAL_RELATIVE_TO_VALUES, IMAGE_WRAP_TEXT_VALUES, IMAGE_WRAP_TYPE_VALUES, LINE_SPACING_RULE_VALUES, OUTLINE_STYLE_ATTR_VALUES, PARAGRAPH_ALIGNMENT_VALUES, POSITIONAL_TAB_ALIGNMENT_VALUES, POSITIONAL_TAB_LEADER_VALUES, POSITIONAL_TAB_RELATIVE_TO_VALUES, SDT_LOCK_VALUES, SDT_TYPE_VALUES, SHADING_PATTERN_VALUES, TABLE_CELL_TEXT_DIRECTION_VALUES, TABLE_CELL_VERTICAL_ALIGNMENT_VALUES, TABLE_JUSTIFICATION_VALUES, TABLE_ROW_HEIGHT_RULE_VALUES, TABLE_WIDTH_TYPE_VALUES, TAB_LEADER_VALUES, TAB_STOP_ALIGNMENT_VALUES, TEXT_EFFECT_VALUES, THEME_COLOR_SLOT_VALUES, UNDERLINE_STYLE_VALUES } from "../../types/documentEnumValues.js";
|
|
1
|
+
import { FIELD_TYPE_VALUES, FONT_THEME_VALUES, HIGHLIGHT_COLOR_VALUES, IMAGE_HORIZONTAL_ALIGNMENT_VALUES, IMAGE_HORIZONTAL_RELATIVE_TO_VALUES, IMAGE_VERTICAL_ALIGNMENT_VALUES, IMAGE_VERTICAL_RELATIVE_TO_VALUES, IMAGE_WRAP_TEXT_VALUES, IMAGE_WRAP_TYPE_VALUES, LINE_SPACING_RULE_VALUES, OUTLINE_STYLE_ATTR_VALUES, PARAGRAPH_ALIGNMENT_VALUES, POSITIONAL_TAB_ALIGNMENT_VALUES, POSITIONAL_TAB_LEADER_VALUES, POSITIONAL_TAB_RELATIVE_TO_VALUES, SDT_LOCK_VALUES, SDT_TYPE_VALUES, SHADING_PATTERN_VALUES, TABLE_CELL_TEXT_DIRECTION_VALUES, TABLE_CELL_VERTICAL_ALIGNMENT_VALUES, TABLE_JUSTIFICATION_VALUES, TABLE_ROW_HEIGHT_RULE_VALUES, TABLE_WIDTH_TYPE_VALUES, TAB_LEADER_VALUES, TAB_STOP_ALIGNMENT_VALUES, TEXT_EFFECT_VALUES, THEME_COLOR_SLOT_VALUES, UNDERLINE_STYLE_VALUES } from "../../types/documentEnumValues.js";
|
|
2
2
|
import { isParagraphDirection } from "../paragraphDirection.js";
|
|
3
3
|
import { TRACKED_CHANGE_PROVENANCE_VALUES } from "../schema/marks.js";
|
|
4
4
|
import { panic } from "better-result";
|
|
@@ -163,6 +163,7 @@ const readParagraphAttrs = (node) => {
|
|
|
163
163
|
optionalNumber(attrs, "spaceBefore", "paragraph.attrs.spaceBefore", issues);
|
|
164
164
|
optionalNumber(attrs, "spaceAfter", "paragraph.attrs.spaceAfter", issues);
|
|
165
165
|
optionalNumber(attrs, "lineSpacing", "paragraph.attrs.lineSpacing", issues);
|
|
166
|
+
optionalBoolean(attrs, "lineSpacingExplicit", "paragraph.attrs.lineSpacingExplicit", issues);
|
|
166
167
|
optionalBoolean(attrs, "snapToGrid", "paragraph.attrs.snapToGrid", issues);
|
|
167
168
|
optionalOneOf(attrs, "lineSpacingRule", "paragraph.attrs.lineSpacingRule", issues, LINE_SPACING_RULE_VALUES);
|
|
168
169
|
optionalNumber(attrs, "indentLeft", "paragraph.attrs.indentLeft", issues);
|
|
@@ -571,10 +572,10 @@ const readFontFamilyMarkAttrs = (mark) => {
|
|
|
571
572
|
optionalString(attrs, "eastAsia", "fontFamily.attrs.eastAsia", issues);
|
|
572
573
|
optionalString(attrs, "cs", "fontFamily.attrs.cs", issues);
|
|
573
574
|
optionalString(attrs, "hint", "fontFamily.attrs.hint", issues);
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
575
|
+
optionalOneOf(attrs, "asciiTheme", "fontFamily.attrs.asciiTheme", issues, FONT_THEME_VALUES);
|
|
576
|
+
optionalOneOf(attrs, "hAnsiTheme", "fontFamily.attrs.hAnsiTheme", issues, FONT_THEME_VALUES);
|
|
577
|
+
optionalOneOf(attrs, "eastAsiaTheme", "fontFamily.attrs.eastAsiaTheme", issues, FONT_THEME_VALUES);
|
|
578
|
+
optionalOneOf(attrs, "csTheme", "fontFamily.attrs.csTheme", issues, FONT_THEME_VALUES);
|
|
578
579
|
return attrsResult(attrs, issues);
|
|
579
580
|
};
|
|
580
581
|
const expectFontFamilyMarkAttrs = (mark) => expectCachedMarkAttrs(mark, fontFamilyAttrsCache, readFontFamilyMarkAttrs, "font family attrs");
|
|
@@ -19,7 +19,7 @@ type AttrPatch = Record<string, unknown>;
|
|
|
19
19
|
* without a style resolver: `numPrFromStyle`, the `list*` rendering attrs,
|
|
20
20
|
* `spacingFromDocDefaults`, `spacingFromImplicitDefaultStyle`
|
|
21
21
|
*/
|
|
22
|
-
declare const PPR_CHANGE_SCOPED_ATTR_KEYS: readonly ["styleId", "numPr", "alignment", "spaceBefore", "spaceAfter", "lineSpacing", "lineSpacingRule", "snapToGrid", "spacingExplicit", "indentLeft", "indentRight", "indentFirstLine", "hangingIndent", "borders", "shading", "tabs", "pageBreakBefore", "keepNext", "keepLines", "widowControl", "contextualSpacing", "outlineLevel", "direction", "_autospacingBase"];
|
|
22
|
+
declare const PPR_CHANGE_SCOPED_ATTR_KEYS: readonly ["styleId", "numPr", "alignment", "spaceBefore", "spaceAfter", "lineSpacing", "lineSpacingRule", "lineSpacingExplicit", "snapToGrid", "spacingExplicit", "indentLeft", "indentRight", "indentFirstLine", "hangingIndent", "borders", "shading", "tabs", "pageBreakBefore", "keepNext", "keepLines", "widowControl", "contextualSpacing", "outlineLevel", "direction", "_autospacingBase"];
|
|
23
23
|
/**
|
|
24
24
|
* Build the attr patch that rejecting one pPrChange applies to a paragraph:
|
|
25
25
|
* every in-scope key set to the stored previous value, or reset to `null`
|
|
@@ -543,6 +543,7 @@ function paragraphAttrsToFormatting(attrs) {
|
|
|
543
543
|
const afterAutospacingEdited = afterHasAutospacingBase ? !autospacingMatchesBase(attrs._autospacingBase, "after", spaceAfter) : afterOriginalAutospacing && attrs._autospacingBase == null;
|
|
544
544
|
const shouldSerializeSpaceBefore = typeof spaceBefore === "number" && (!beforeHasAutospacingBase || beforeAutospacingEdited);
|
|
545
545
|
const shouldSerializeSpaceAfter = typeof spaceAfter === "number" && (!afterHasAutospacingBase || afterAutospacingEdited);
|
|
546
|
+
const hasDirectLineSpacing = attrs.lineSpacingExplicit === true;
|
|
546
547
|
if (attrs._originalFormatting) {
|
|
547
548
|
const orig = attrs._originalFormatting;
|
|
548
549
|
const result = { ...orig };
|
|
@@ -556,6 +557,15 @@ function paragraphAttrsToFormatting(attrs) {
|
|
|
556
557
|
if (typeof spaceAfter === "number") result.spaceAfter = spaceAfter;
|
|
557
558
|
else delete result.spaceAfter;
|
|
558
559
|
}
|
|
560
|
+
if (attrs.spacingExplicit?.before && typeof spaceBefore === "number") result.spaceBefore = spaceBefore;
|
|
561
|
+
if (attrs.spacingExplicit?.after && typeof spaceAfter === "number") result.spaceAfter = spaceAfter;
|
|
562
|
+
const originalHasDirectLineSpacing = orig.lineSpacing !== void 0 || orig.lineSpacingRule !== void 0;
|
|
563
|
+
if (hasDirectLineSpacing || originalHasDirectLineSpacing) {
|
|
564
|
+
if (typeof attrs.lineSpacing === "number") result.lineSpacing = attrs.lineSpacing;
|
|
565
|
+
else Reflect.deleteProperty(result, "lineSpacing");
|
|
566
|
+
if (attrs.lineSpacingRule) result.lineSpacingRule = attrs.lineSpacingRule;
|
|
567
|
+
else Reflect.deleteProperty(result, "lineSpacingRule");
|
|
568
|
+
}
|
|
559
569
|
if (attrs.alignment !== (orig.alignment ?? void 0)) if (attrs.alignment) result.alignment = attrs.alignment;
|
|
560
570
|
else delete result.alignment;
|
|
561
571
|
if (isStyleSourcedNumPr(attrs)) {
|
|
@@ -583,15 +593,15 @@ function paragraphAttrsToFormatting(attrs) {
|
|
|
583
593
|
}
|
|
584
594
|
const outlineLevel = Reflect.get(attrs, "outlineLevel");
|
|
585
595
|
const bidi = directionToBidi(attrs.direction);
|
|
586
|
-
if (!(attrs.alignment || shouldSerializeSpaceBefore || shouldSerializeSpaceAfter || beforeAutospacingEdited || afterAutospacingEdited ||
|
|
596
|
+
if (!(attrs.alignment || shouldSerializeSpaceBefore || shouldSerializeSpaceAfter || beforeAutospacingEdited || afterAutospacingEdited || hasDirectLineSpacing || attrs.snapToGrid != null || attrs.indentLeft || attrs.indentRight || attrs.indentFirstLine || attrs.numPr || attrs.styleId || attrs.borders || attrs.shading || attrs.tabs || typeof outlineLevel === "number" || attrs.contextualSpacing || attrs.spacingExplicit || bidi != null || attrs.pageBreakBefore != null || attrs.widowControl != null || attrs.kinsoku != null || attrs.overflowPunctuation != null || attrs.suppressAutoHyphens != null)) return;
|
|
587
597
|
const f = {};
|
|
588
598
|
if (attrs.alignment) f.alignment = attrs.alignment;
|
|
589
599
|
if (shouldSerializeSpaceBefore) f.spaceBefore = spaceBefore;
|
|
590
600
|
if (beforeAutospacingEdited) f.beforeAutospacing = false;
|
|
591
601
|
if (shouldSerializeSpaceAfter) f.spaceAfter = spaceAfter;
|
|
592
602
|
if (afterAutospacingEdited) f.afterAutospacing = false;
|
|
593
|
-
if (attrs.lineSpacing) f.lineSpacing = attrs.lineSpacing;
|
|
594
|
-
if (attrs.lineSpacingRule) f.lineSpacingRule = attrs.lineSpacingRule;
|
|
603
|
+
if (hasDirectLineSpacing && typeof attrs.lineSpacing === "number") f.lineSpacing = attrs.lineSpacing;
|
|
604
|
+
if (hasDirectLineSpacing && attrs.lineSpacingRule) f.lineSpacingRule = attrs.lineSpacingRule;
|
|
595
605
|
if (attrs.snapToGrid != null) f.snapToGrid = attrs.snapToGrid;
|
|
596
606
|
if (attrs.spacingExplicit) f.spacingExplicit = attrs.spacingExplicit;
|
|
597
607
|
if (attrs.indentLeft) f.indentLeft = attrs.indentLeft;
|
|
@@ -319,6 +319,7 @@ function paragraphFormattingToAttrs(paragraph, styleResolver, tableParagraphOver
|
|
|
319
319
|
set("spaceAfter", formatting?.spaceAfter ?? stylePpr?.spaceAfter);
|
|
320
320
|
set("lineSpacing", formatting?.lineSpacing ?? stylePpr?.lineSpacing);
|
|
321
321
|
set("lineSpacingRule", formatting?.lineSpacingRule ?? stylePpr?.lineSpacingRule);
|
|
322
|
+
set("lineSpacingExplicit", formatting?.lineSpacing !== void 0 || formatting?.lineSpacingRule !== void 0 ? true : void 0);
|
|
322
323
|
set("snapToGrid", formatting?.snapToGrid ?? stylePpr?.snapToGrid);
|
|
323
324
|
set("spacingExplicit", formatting?.spacingExplicit);
|
|
324
325
|
const paragraphStyle = styleId ? styleResolver.getStyle(styleId) ?? styleResolver.getDefaultParagraphStyle() : styleResolver.getDefaultParagraphStyle();
|
|
@@ -161,6 +161,7 @@ function extractParagraphAttrsFromStyle(element) {
|
|
|
161
161
|
if (spacing) {
|
|
162
162
|
attrs.lineSpacing = spacing.lineSpacing;
|
|
163
163
|
attrs.lineSpacingRule = spacing.lineSpacingRule;
|
|
164
|
+
attrs.lineSpacingExplicit = true;
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
if (style.marginTop) {
|
|
@@ -187,6 +188,7 @@ const paragraphNodeSpec = {
|
|
|
187
188
|
spaceAfter: { default: null },
|
|
188
189
|
lineSpacing: { default: null },
|
|
189
190
|
lineSpacingRule: { default: null },
|
|
191
|
+
lineSpacingExplicit: { default: null },
|
|
190
192
|
snapToGrid: { default: null },
|
|
191
193
|
spacingExplicit: { default: null },
|
|
192
194
|
spacingFromDocDefaults: { default: null },
|
|
@@ -323,6 +325,30 @@ function setParagraphAttr(attr, value) {
|
|
|
323
325
|
return true;
|
|
324
326
|
};
|
|
325
327
|
}
|
|
328
|
+
function setParagraphSpacingAttr(side, twips) {
|
|
329
|
+
return (state, dispatch) => {
|
|
330
|
+
const { $from, $to } = state.selection;
|
|
331
|
+
if (!dispatch) return true;
|
|
332
|
+
let tr = state.tr;
|
|
333
|
+
const seen = /* @__PURE__ */ new Set();
|
|
334
|
+
state.doc.nodesBetween($from.pos, $to.pos, (node, pos) => {
|
|
335
|
+
if (node.type.name === "paragraph" && !seen.has(pos)) {
|
|
336
|
+
seen.add(pos);
|
|
337
|
+
const spacingExplicit = {
|
|
338
|
+
...node.attrs["spacingExplicit"],
|
|
339
|
+
[side]: true
|
|
340
|
+
};
|
|
341
|
+
tr = tr.setNodeMarkup(pos, void 0, {
|
|
342
|
+
...node.attrs,
|
|
343
|
+
[side === "before" ? "spaceBefore" : "spaceAfter"]: twips,
|
|
344
|
+
spacingExplicit
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
dispatch(tr.scrollIntoView());
|
|
349
|
+
return true;
|
|
350
|
+
};
|
|
351
|
+
}
|
|
326
352
|
function setParagraphAttrsCmd(attrs) {
|
|
327
353
|
return (state, dispatch) => {
|
|
328
354
|
const { $from, $to } = state.selection;
|
|
@@ -348,7 +374,8 @@ function makeSetAlignment(alignment) {
|
|
|
348
374
|
function makeSetLineSpacing(value, rule = "auto") {
|
|
349
375
|
return (state, dispatch) => setParagraphAttrsCmd({
|
|
350
376
|
lineSpacing: value,
|
|
351
|
-
lineSpacingRule: rule
|
|
377
|
+
lineSpacingRule: rule,
|
|
378
|
+
lineSpacingExplicit: true
|
|
352
379
|
})(state, dispatch);
|
|
353
380
|
}
|
|
354
381
|
function makeIncreaseIndent(amount = 720) {
|
|
@@ -498,8 +525,8 @@ const ParagraphExtension = createNodeExtension({
|
|
|
498
525
|
singleSpacing: () => makeSetLineSpacing(240),
|
|
499
526
|
oneAndHalfSpacing: () => makeSetLineSpacing(360),
|
|
500
527
|
doubleSpacing: () => makeSetLineSpacing(480),
|
|
501
|
-
setSpaceBefore: (twips) =>
|
|
502
|
-
setSpaceAfter: (twips) =>
|
|
528
|
+
setSpaceBefore: (twips) => setParagraphSpacingAttr("before", twips),
|
|
529
|
+
setSpaceAfter: (twips) => setParagraphSpacingAttr("after", twips),
|
|
503
530
|
increaseIndent: (amount) => makeIncreaseIndent(amount),
|
|
504
531
|
decreaseIndent: (amount) => makeDecreaseIndent(amount),
|
|
505
532
|
setIndentLeft: (twips) => setParagraphAttr("indentLeft", twips > 0 ? twips : null),
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { expectUnderlineMarkAttrs } from "../../attrs/index.js";
|
|
2
2
|
import { createMarkExtension } from "../create.js";
|
|
3
|
-
import { setMark } from "./markUtils.js";
|
|
3
|
+
import { setMark, toggleUnderlineMark } from "./markUtils.js";
|
|
4
4
|
import { panic } from "better-result";
|
|
5
|
-
import { toggleMark } from "prosemirror-commands";
|
|
6
5
|
//#region src/prosemirror/extensions/marks/UnderlineExtension.ts
|
|
7
6
|
/**
|
|
8
7
|
* Underline Mark Extension
|
|
@@ -17,14 +16,17 @@ const UnderlineExtension = createMarkExtension({
|
|
|
17
16
|
},
|
|
18
17
|
parseDOM: [{ tag: "u" }, {
|
|
19
18
|
style: "text-decoration",
|
|
20
|
-
getAttrs: (value) =>
|
|
19
|
+
getAttrs: (value) => {
|
|
20
|
+
if (value.includes("underline")) return {};
|
|
21
|
+
return value.includes("none") ? { style: "none" } : false;
|
|
22
|
+
}
|
|
21
23
|
}],
|
|
22
24
|
toDOM(mark) {
|
|
23
25
|
const attrs = expectUnderlineMarkAttrs(mark);
|
|
24
26
|
const style = attrs.style;
|
|
25
27
|
const colorRgb = attrs.color?.rgb;
|
|
26
|
-
const cssStyle = [
|
|
27
|
-
if (style && style !== "single") {
|
|
28
|
+
const cssStyle = [`text-decoration: ${style === "none" ? "none" : "underline"}`];
|
|
29
|
+
if (style && style !== "single" && style !== "none") {
|
|
28
30
|
const cssDecorationStyle = {
|
|
29
31
|
double: "double",
|
|
30
32
|
dotted: "dotted",
|
|
@@ -46,13 +48,13 @@ const UnderlineExtension = createMarkExtension({
|
|
|
46
48
|
if (!underlineType) panic("Missing mark type: underline");
|
|
47
49
|
return {
|
|
48
50
|
commands: {
|
|
49
|
-
toggleUnderline: () =>
|
|
51
|
+
toggleUnderline: () => toggleUnderlineMark(underlineType),
|
|
50
52
|
setUnderlineStyle: (style, color) => setMark(underlineType, {
|
|
51
53
|
style,
|
|
52
54
|
color
|
|
53
55
|
})
|
|
54
56
|
},
|
|
55
|
-
keyboardShortcuts: { "Mod-u":
|
|
57
|
+
keyboardShortcuts: { "Mod-u": toggleUnderlineMark(underlineType) }
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
60
|
});
|
|
@@ -4,6 +4,7 @@ import { Mark, MarkType, Schema } from "prosemirror-model";
|
|
|
4
4
|
//#region src/prosemirror/extensions/marks/markUtils.d.ts
|
|
5
5
|
type MarkAttrs = Record<string, unknown>;
|
|
6
6
|
declare function setMark(markType: MarkType, attrs: MarkAttrs): Command;
|
|
7
|
+
declare function toggleUnderlineMark(markType: MarkType): Command;
|
|
7
8
|
declare function removeMark(markType: MarkType): Command;
|
|
8
9
|
/**
|
|
9
10
|
* Check if a mark is active in the current selection
|
|
@@ -30,4 +31,4 @@ declare function createSetMarkCommand(markType: MarkType, attrs?: Record<string,
|
|
|
30
31
|
*/
|
|
31
32
|
declare function createRemoveMarkCommand(markType: MarkType): Command;
|
|
32
33
|
//#endregion
|
|
33
|
-
export { clearFormatting, createRemoveMarkCommand, createSetMarkCommand, getMarkAttr, isMarkActive, removeMark, setMark, textFormattingToMarks };
|
|
34
|
+
export { clearFormatting, createRemoveMarkCommand, createSetMarkCommand, getMarkAttr, isMarkActive, removeMark, setMark, textFormattingToMarks, toggleUnderlineMark };
|
|
@@ -1,5 +1,20 @@
|
|
|
1
|
+
import { FONT_THEME_VALUES } from "../../../types/documentEnumValues.js";
|
|
2
|
+
import { mergeFontFamily } from "../../../utils/fontFamilyMerge.js";
|
|
3
|
+
import { expectFontFamilyMarkAttrs } from "../../attrs/index.js";
|
|
1
4
|
import { applyRunFormattingOverrideMark, buildRunFormattingOverrideAttrs } from "./RunFormattingOverrideExtension.js";
|
|
2
5
|
//#region src/prosemirror/extensions/marks/markUtils.ts
|
|
6
|
+
const isFontTheme = (value) => value !== void 0 && FONT_THEME_VALUES.some((theme) => theme === value);
|
|
7
|
+
const fontFamilyAttrsToFormatting = ({ ascii, hAnsi, eastAsia, cs, hint, asciiTheme, hAnsiTheme, eastAsiaTheme, csTheme }) => ({
|
|
8
|
+
...ascii !== void 0 ? { ascii } : {},
|
|
9
|
+
...hAnsi !== void 0 ? { hAnsi } : {},
|
|
10
|
+
...eastAsia !== void 0 ? { eastAsia } : {},
|
|
11
|
+
...cs !== void 0 ? { cs } : {},
|
|
12
|
+
...hint !== void 0 ? { hint } : {},
|
|
13
|
+
...isFontTheme(asciiTheme) ? { asciiTheme } : {},
|
|
14
|
+
...hAnsiTheme !== void 0 ? { hAnsiTheme } : {},
|
|
15
|
+
...eastAsiaTheme !== void 0 ? { eastAsiaTheme } : {},
|
|
16
|
+
...csTheme !== void 0 ? { csTheme } : {}
|
|
17
|
+
});
|
|
3
18
|
function marksToTextFormatting(marks) {
|
|
4
19
|
const formatting = {};
|
|
5
20
|
for (const mark of marks) switch (mark.type.name) {
|
|
@@ -10,7 +25,10 @@ function marksToTextFormatting(marks) {
|
|
|
10
25
|
formatting.italic = true;
|
|
11
26
|
break;
|
|
12
27
|
case "underline":
|
|
13
|
-
formatting.underline = {
|
|
28
|
+
formatting.underline = {
|
|
29
|
+
style: typeof mark.attrs["style"] === "string" ? mark.attrs["style"] : "single",
|
|
30
|
+
...mark.attrs["color"] !== null && mark.attrs["color"] !== void 0 ? { color: mark.attrs["color"] } : {}
|
|
31
|
+
};
|
|
14
32
|
break;
|
|
15
33
|
case "strike":
|
|
16
34
|
formatting.strike = true;
|
|
@@ -34,17 +52,9 @@ function marksToTextFormatting(marks) {
|
|
|
34
52
|
case "fontSize":
|
|
35
53
|
formatting.fontSize = Number(mark.attrs["size"]);
|
|
36
54
|
break;
|
|
37
|
-
case "fontFamily":
|
|
38
|
-
|
|
39
|
-
const hAnsi = mark.attrs["hAnsi"] !== null && mark.attrs["hAnsi"] !== void 0 ? String(mark.attrs["hAnsi"]) : void 0;
|
|
40
|
-
const hint = mark.attrs["hint"];
|
|
41
|
-
formatting.fontFamily = {
|
|
42
|
-
...ascii !== void 0 ? { ascii } : {},
|
|
43
|
-
...hAnsi !== void 0 ? { hAnsi } : {},
|
|
44
|
-
...hint === "default" || hint === "eastAsia" || hint === "cs" ? { hint } : {}
|
|
45
|
-
};
|
|
55
|
+
case "fontFamily":
|
|
56
|
+
formatting.fontFamily = fontFamilyAttrsToFormatting(expectFontFamilyMarkAttrs(mark));
|
|
46
57
|
break;
|
|
47
|
-
}
|
|
48
58
|
case "language": {
|
|
49
59
|
const val = mark.attrs["val"];
|
|
50
60
|
const eastAsia = mark.attrs["eastAsia"];
|
|
@@ -93,21 +103,82 @@ function dispatchStoredMarks(state, dispatch, marks) {
|
|
|
93
103
|
tr.setStoredMarks(marks);
|
|
94
104
|
dispatch(tr);
|
|
95
105
|
}
|
|
106
|
+
function compactAttrs(attrs) {
|
|
107
|
+
if (!attrs) return {};
|
|
108
|
+
const result = {};
|
|
109
|
+
for (const [key, value] of Object.entries(attrs)) if (value !== null && value !== void 0) result[key] = value;
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
function mergeMarkAttrs(markType, currentMark, nextAttrs) {
|
|
113
|
+
const next = compactAttrs(nextAttrs);
|
|
114
|
+
switch (markType.name) {
|
|
115
|
+
case "fontFamily": return mergeFontFamily(currentMark ? fontFamilyAttrsToFormatting(expectFontFamilyMarkAttrs(currentMark)) : void 0, fontFamilyAttrsToFormatting(expectFontFamilyMarkAttrs(markType.create(next))));
|
|
116
|
+
case "underline": return {
|
|
117
|
+
...compactAttrs(currentMark?.attrs),
|
|
118
|
+
...next
|
|
119
|
+
};
|
|
120
|
+
default: return nextAttrs;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function markRequiresAttrMerge(markType) {
|
|
124
|
+
return markType.name === "fontFamily" || markType.name === "underline";
|
|
125
|
+
}
|
|
126
|
+
function createMarkWithMergedAttrs(markType, currentMark, nextAttrs) {
|
|
127
|
+
if (!markRequiresAttrMerge(markType)) return markType.create(nextAttrs);
|
|
128
|
+
return markType.create(mergeMarkAttrs(markType, currentMark, nextAttrs));
|
|
129
|
+
}
|
|
96
130
|
function setMark(markType, attrs) {
|
|
97
131
|
return (state, dispatch) => {
|
|
98
132
|
const { from, to, empty } = state.selection;
|
|
99
|
-
const mark = markType.create(attrs);
|
|
100
133
|
if (empty) {
|
|
101
134
|
if (dispatch) {
|
|
102
135
|
const current = state.storedMarks ?? state.selection.$from.marks();
|
|
103
|
-
|
|
136
|
+
const currentMark = markType.isInSet(current);
|
|
137
|
+
const marks = markType.isInSet(current) ? current.filter((m) => m.type !== markType) : current;
|
|
138
|
+
const mark = createMarkWithMergedAttrs(markType, currentMark, attrs);
|
|
139
|
+
dispatchStoredMarks(state, dispatch, [...marks, mark]);
|
|
104
140
|
}
|
|
105
141
|
return true;
|
|
106
142
|
}
|
|
107
|
-
if (dispatch)
|
|
143
|
+
if (dispatch) {
|
|
144
|
+
if (!markRequiresAttrMerge(markType)) {
|
|
145
|
+
dispatch(state.tr.addMark(from, to, markType.create(attrs)).scrollIntoView());
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
let tr = state.tr;
|
|
149
|
+
state.doc.nodesBetween(from, to, (node, pos) => {
|
|
150
|
+
if (!node.isText) return;
|
|
151
|
+
const start = Math.max(from, pos);
|
|
152
|
+
const end = Math.min(to, pos + node.nodeSize);
|
|
153
|
+
const mark = createMarkWithMergedAttrs(markType, markType.isInSet(node.marks), attrs);
|
|
154
|
+
tr = tr.addMark(start, end, mark);
|
|
155
|
+
});
|
|
156
|
+
dispatch(tr.scrollIntoView());
|
|
157
|
+
}
|
|
108
158
|
return true;
|
|
109
159
|
};
|
|
110
160
|
}
|
|
161
|
+
function selectionHasVisibleUnderline(state, markType) {
|
|
162
|
+
const { from, to, empty, $from } = state.selection;
|
|
163
|
+
if (empty) {
|
|
164
|
+
const mark = markType.isInSet(state.storedMarks ?? $from.marks());
|
|
165
|
+
return mark !== void 0 && mark.attrs["style"] !== "none";
|
|
166
|
+
}
|
|
167
|
+
let hasVisibleUnderline = false;
|
|
168
|
+
state.doc.nodesBetween(from, to, (node) => {
|
|
169
|
+
if (!node.isText) return true;
|
|
170
|
+
const mark = markType.isInSet(node.marks);
|
|
171
|
+
if (mark && mark.attrs["style"] !== "none") {
|
|
172
|
+
hasVisibleUnderline = true;
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
});
|
|
177
|
+
return hasVisibleUnderline;
|
|
178
|
+
}
|
|
179
|
+
function toggleUnderlineMark(markType) {
|
|
180
|
+
return (state, dispatch) => setMark(markType, { style: selectionHasVisibleUnderline(state, markType) ? "none" : "single" })(state, dispatch);
|
|
181
|
+
}
|
|
111
182
|
function removeMark(markType) {
|
|
112
183
|
return (state, dispatch) => {
|
|
113
184
|
const { from, to, empty } = state.selection;
|
|
@@ -197,8 +268,13 @@ function textFormattingToMarks(formatting, schema) {
|
|
|
197
268
|
if (formatting.fontFamily && schema.marks["fontFamily"]) marks.push(schema.marks["fontFamily"].create({
|
|
198
269
|
ascii: formatting.fontFamily.ascii,
|
|
199
270
|
hAnsi: formatting.fontFamily.hAnsi,
|
|
271
|
+
eastAsia: formatting.fontFamily.eastAsia,
|
|
272
|
+
cs: formatting.fontFamily.cs,
|
|
200
273
|
hint: formatting.fontFamily.hint,
|
|
201
|
-
asciiTheme: formatting.fontFamily.asciiTheme
|
|
274
|
+
asciiTheme: formatting.fontFamily.asciiTheme,
|
|
275
|
+
hAnsiTheme: formatting.fontFamily.hAnsiTheme,
|
|
276
|
+
eastAsiaTheme: formatting.fontFamily.eastAsiaTheme,
|
|
277
|
+
csTheme: formatting.fontFamily.csTheme
|
|
202
278
|
}));
|
|
203
279
|
if (formatting.language && schema.marks["language"]) marks.push(schema.marks["language"].create(formatting.language));
|
|
204
280
|
if (formatting.vertAlign === "superscript" && schema.marks["superscript"]) marks.push(schema.marks["superscript"].create());
|
|
@@ -245,4 +321,4 @@ function createRemoveMarkCommand(markType) {
|
|
|
245
321
|
return removeMark(markType);
|
|
246
322
|
}
|
|
247
323
|
//#endregion
|
|
248
|
-
export { clearFormatting, createRemoveMarkCommand, createSetMarkCommand, getMarkAttr, isMarkActive, removeMark, setMark, textFormattingToMarks };
|
|
324
|
+
export { clearFormatting, createRemoveMarkCommand, createSetMarkCommand, getMarkAttr, isMarkActive, removeMark, setMark, textFormattingToMarks, toggleUnderlineMark };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectMarksInRange } from "../selectionMarks.js";
|
|
1
2
|
import { Plugin, PluginKey } from "prosemirror-state";
|
|
2
3
|
//#region src/prosemirror/plugins/selectionTracker.ts
|
|
3
4
|
/**
|
|
@@ -32,7 +33,7 @@ function extractSelectionContext(state) {
|
|
|
32
33
|
const paragraphFormatting = {};
|
|
33
34
|
if (paragraph.type.name === "paragraph") {
|
|
34
35
|
if (paragraph.attrs["alignment"]) paragraphFormatting.alignment = paragraph.attrs["alignment"];
|
|
35
|
-
if (paragraph.attrs["lineSpacing"]) {
|
|
36
|
+
if (typeof paragraph.attrs["lineSpacing"] === "number") {
|
|
36
37
|
paragraphFormatting.lineSpacing = paragraph.attrs["lineSpacing"];
|
|
37
38
|
paragraphFormatting.lineSpacingRule = paragraph.attrs["lineSpacingRule"];
|
|
38
39
|
}
|
|
@@ -79,9 +80,13 @@ function extractSelectionContext(state) {
|
|
|
79
80
|
* Extract text formatting from current selection/cursor marks
|
|
80
81
|
*/
|
|
81
82
|
function extractTextFormatting(state) {
|
|
82
|
-
const { selection } = state;
|
|
83
|
-
const { empty, $from } = selection;
|
|
84
|
-
const marks = state.storedMarks ||
|
|
83
|
+
const { selection, doc } = state;
|
|
84
|
+
const { from, to, empty, $from } = selection;
|
|
85
|
+
const marks = empty ? state.storedMarks || $from.marks() : collectMarksInRange({
|
|
86
|
+
doc,
|
|
87
|
+
from,
|
|
88
|
+
to
|
|
89
|
+
});
|
|
85
90
|
const formatting = {};
|
|
86
91
|
for (const mark of marks) switch (mark.type.name) {
|
|
87
92
|
case "bold":
|
|
@@ -91,7 +96,7 @@ function extractTextFormatting(state) {
|
|
|
91
96
|
formatting.italic = true;
|
|
92
97
|
break;
|
|
93
98
|
case "underline":
|
|
94
|
-
formatting.underline = {
|
|
99
|
+
if (mark.attrs["style"] !== "none") formatting.underline = {
|
|
95
100
|
style: mark.attrs["style"] || "single",
|
|
96
101
|
color: mark.attrs["color"]
|
|
97
102
|
};
|
|
@@ -32,6 +32,7 @@ type ParagraphAttrs = {
|
|
|
32
32
|
spaceAfter?: number;
|
|
33
33
|
lineSpacing?: number;
|
|
34
34
|
lineSpacingRule?: document_d_exports.LineSpacingRule;
|
|
35
|
+
lineSpacingExplicit?: boolean;
|
|
35
36
|
snapToGrid?: boolean;
|
|
36
37
|
spacingExplicit?: SpacingExplicit;
|
|
37
38
|
/** Layout provenance: document defaults survive on empty paragraphs. */
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Mark, Node } from "prosemirror-model";
|
|
2
|
+
//#region src/prosemirror/selectionMarks.d.ts
|
|
3
|
+
type CollectMarksInRangeOptions = {
|
|
4
|
+
doc: Node;
|
|
5
|
+
from: number;
|
|
6
|
+
to: number;
|
|
7
|
+
};
|
|
8
|
+
/** Collect one representative per mark type, preferring visible underline state. */
|
|
9
|
+
declare const collectMarksInRange: ({ doc, from, to }: CollectMarksInRangeOptions) => Mark[];
|
|
10
|
+
//#endregion
|
|
11
|
+
export { collectMarksInRange };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/prosemirror/selectionMarks.ts
|
|
2
|
+
const UNDERLINE_MARK = "underline";
|
|
3
|
+
const HIDDEN_UNDERLINE_STYLE = "none";
|
|
4
|
+
const shouldReplaceMark = (current, incoming) => current.type.name === UNDERLINE_MARK && current.attrs["style"] === HIDDEN_UNDERLINE_STYLE && incoming.attrs["style"] !== HIDDEN_UNDERLINE_STYLE;
|
|
5
|
+
/** Collect one representative per mark type, preferring visible underline state. */
|
|
6
|
+
const collectMarksInRange = ({ doc, from, to }) => {
|
|
7
|
+
const seen = /* @__PURE__ */ new Map();
|
|
8
|
+
doc.nodesBetween(from, to, (node) => {
|
|
9
|
+
if (!node.isText) return;
|
|
10
|
+
for (const mark of node.marks) {
|
|
11
|
+
const name = mark.type.name;
|
|
12
|
+
const current = seen.get(name);
|
|
13
|
+
if (!current || shouldReplaceMark(current, mark)) seen.set(name, mark);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
return Array.from(seen.values());
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { collectMarksInRange };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { directionIsRtl } from "./paragraphDirection.js";
|
|
2
|
+
import { collectMarksInRange } from "./selectionMarks.js";
|
|
2
3
|
//#region src/prosemirror/selectionState.ts
|
|
3
4
|
/**
|
|
4
5
|
* Extract selection state from editor state.
|
|
@@ -18,7 +19,11 @@ function extractSelectionState(state) {
|
|
|
18
19
|
const paragraph = $from.parent;
|
|
19
20
|
const isEmptyParagraph = paragraph.type.name === "paragraph" && paragraph.textContent.length === 0;
|
|
20
21
|
const paragraphDefaultFormatting = paragraph.attrs["defaultTextFormatting"];
|
|
21
|
-
const marks = empty ? state.storedMarks || selection.$from.marks() : collectMarksInRange(
|
|
22
|
+
const marks = empty ? state.storedMarks || selection.$from.marks() : collectMarksInRange({
|
|
23
|
+
doc,
|
|
24
|
+
from,
|
|
25
|
+
to
|
|
26
|
+
});
|
|
22
27
|
if (isEmptyParagraph && marks.length === 0 && paragraphDefaultFormatting) textFormatting = { ...paragraphDefaultFormatting };
|
|
23
28
|
for (const mark of marks) switch (mark.type.name) {
|
|
24
29
|
case "bold":
|
|
@@ -28,7 +33,7 @@ function extractSelectionState(state) {
|
|
|
28
33
|
textFormatting.italic = true;
|
|
29
34
|
break;
|
|
30
35
|
case "underline":
|
|
31
|
-
textFormatting.underline = {
|
|
36
|
+
if (mark.attrs["style"] !== "none") textFormatting.underline = {
|
|
32
37
|
style: mark.attrs["style"] || "single",
|
|
33
38
|
color: mark.attrs["color"]
|
|
34
39
|
};
|
|
@@ -67,7 +72,7 @@ function extractSelectionState(state) {
|
|
|
67
72
|
let styleId = null;
|
|
68
73
|
if (paragraph.type.name === "paragraph") {
|
|
69
74
|
if (paragraph.attrs["alignment"]) paragraphFormatting.alignment = paragraph.attrs["alignment"];
|
|
70
|
-
if (paragraph.attrs["lineSpacing"]) {
|
|
75
|
+
if (typeof paragraph.attrs["lineSpacing"] === "number") {
|
|
71
76
|
paragraphFormatting.lineSpacing = paragraph.attrs["lineSpacing"];
|
|
72
77
|
paragraphFormatting.lineSpacingRule = paragraph.attrs["lineSpacingRule"];
|
|
73
78
|
}
|
|
@@ -91,22 +96,5 @@ function extractSelectionState(state) {
|
|
|
91
96
|
endParagraphIndex
|
|
92
97
|
};
|
|
93
98
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Collect the first occurrence of each mark type found on any text node within
|
|
96
|
-
* the range. Mirrors the "any inline child has this mark" semantics that
|
|
97
|
-
* prosemirror-commands' toggleMark uses to decide add-vs-remove, so the
|
|
98
|
-
* toolbar's active state stays consistent with what a toggle click will do.
|
|
99
|
-
*/
|
|
100
|
-
function collectMarksInRange(doc, from, to) {
|
|
101
|
-
const seen = /* @__PURE__ */ new Map();
|
|
102
|
-
doc.nodesBetween(from, to, (node) => {
|
|
103
|
-
if (!node.isText) return;
|
|
104
|
-
for (const mark of node.marks) {
|
|
105
|
-
const name = mark.type.name;
|
|
106
|
-
if (!seen.has(name)) seen.set(name, mark);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
return Array.from(seen.values());
|
|
110
|
-
}
|
|
111
99
|
//#endregion
|
|
112
100
|
export { extractSelectionState };
|
|
@@ -27,6 +27,7 @@ function paragraphAttrsFromResolvedStyle(resolved) {
|
|
|
27
27
|
spaceAfter: ppr?.spaceAfter ?? null,
|
|
28
28
|
lineSpacing: ppr?.lineSpacing ?? null,
|
|
29
29
|
lineSpacingRule: ppr?.lineSpacingRule ?? null,
|
|
30
|
+
lineSpacingExplicit: null,
|
|
30
31
|
snapToGrid: ppr?.snapToGrid ?? null,
|
|
31
32
|
indentLeft: ppr?.indentLeft ?? null,
|
|
32
33
|
indentRight: ppr?.indentRight ?? null,
|
|
@@ -108,7 +108,7 @@ function paragraphToStyle(formatting, theme) {
|
|
|
108
108
|
}
|
|
109
109
|
if (formatting.indentLeft !== void 0) style.marginLeft = formatPx(twipsToPixels(formatting.indentLeft));
|
|
110
110
|
if (formatting.indentRight !== void 0) style.marginRight = formatPx(twipsToPixels(formatting.indentRight));
|
|
111
|
-
if (formatting.indentFirstLine !== void 0) style.textIndent = formatPx(twipsToPixels(formatting.indentFirstLine));
|
|
111
|
+
if (formatting.indentFirstLine !== void 0) style.textIndent = formatPx(twipsToPixels(formatting.hangingIndent ? -Math.abs(formatting.indentFirstLine) : formatting.indentFirstLine));
|
|
112
112
|
if (formatting.borders) {
|
|
113
113
|
if (formatting.borders.top) Object.assign(style, borderToStyle(formatting.borders.top, "Top", theme));
|
|
114
114
|
if (formatting.borders.bottom) Object.assign(style, borderToStyle(formatting.borders.bottom, "Bottom", theme));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/folio-core",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.3",
|
|
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",
|