@stll/folio-core 0.47.1 → 0.47.2

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.
@@ -52,7 +52,11 @@ function parseFootnoteProperties(element) {
52
52
  const numFmtEl = findChild(element, "w", "numFmt");
53
53
  if (numFmtEl) {
54
54
  const numFmt = parseNumberFormat(getAttribute(numFmtEl, "w", "val"));
55
- if (numFmt !== void 0) props.numFmt = numFmt;
55
+ if (numFmt !== void 0) {
56
+ props.numFmt = numFmt;
57
+ const format = getAttribute(numFmtEl, "w", "format");
58
+ if (format !== null) props.numFmtFormat = format;
59
+ }
56
60
  }
57
61
  const numStartEl = findChild(element, "w", "numStart");
58
62
  if (numStartEl) {
@@ -81,7 +85,11 @@ function parseEndnoteProperties(element) {
81
85
  const numFmtEl = findChild(element, "w", "numFmt");
82
86
  if (numFmtEl) {
83
87
  const numFmt = parseNumberFormat(getAttribute(numFmtEl, "w", "val"));
84
- if (numFmt !== void 0) props.numFmt = numFmt;
88
+ if (numFmt !== void 0) {
89
+ props.numFmt = numFmt;
90
+ const format = getAttribute(numFmtEl, "w", "format");
91
+ if (format !== null) props.numFmtFormat = format;
92
+ }
85
93
  }
86
94
  const numStartEl = findChild(element, "w", "numStart");
87
95
  if (numStartEl) {
@@ -24,7 +24,7 @@ type NumberingMap = {
24
24
  * @returns NumberingMap with definitions and helper functions
25
25
  */
26
26
  declare function parseNumbering(numberingXml: string | null): NumberingMap;
27
- /** A `w:numFmt` as the model holds it: the token, plus what `custom` defers to. */
27
+ /** A `w:numFmt` as the model holds it: its token and authored format metadata. */
28
28
  type ResolvedNumFmt = Pick<document_d_exports.ListLevel, "numFmt" | "numFmtFormat">;
29
29
  /**
30
30
  * The width a `custom` format's zero-padded first token counts to, or
@@ -35,14 +35,6 @@ type ResolvedNumFmt = Pick<document_d_exports.ListLevel, "numFmt" | "numFmtForma
35
35
  * way; the width is the digit count of the first token, clamped to five.
36
36
  */
37
37
  declare const customNumberFormatPadWidth: (format: string | undefined) => number | undefined;
38
- /**
39
- * The `@w:format` a level's `w:numFmt` writes, which only `custom` carries.
40
- *
41
- * `custom` is the reserved member that defers to another attribute, so the
42
- * comparison lives here, in the module the reserved-value registry names as
43
- * its owner, rather than in the serializer that needs the answer.
44
- */
45
- declare const customNumberFormatOf: (level: ResolvedNumFmt) => string | undefined;
46
38
  /** What the marker renderer counts a level in; `custom` decides by pad width. */
47
39
  declare const counterFormatOf: (level: ResolvedNumFmt) => document_d_exports.CounterFormat;
48
40
  /** {@link MARKER_ALIGNMENT} for a level that may not state a justification. */
@@ -92,4 +84,4 @@ declare function isBulletLevel(level: document_d_exports.ListLevel): boolean;
92
84
  /** Whether a numbering level reserves horizontal space for a visible marker. */
93
85
  declare const numberingLevelHasMarkerSlot: (level: Pick<document_d_exports.ListLevel, "numFmt">) => boolean;
94
86
  //#endregion
95
- export { NumberingMap, computeListRendering, counterFormatOf, createNumberingMap, customNumberFormatOf, customNumberFormatPadWidth, formatOoxmlCounter as formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, markerAlignmentForLevel, markerFormattingFromLevel, numberingLevelHasMarkerSlot, padDecimal, parseNumbering, renderListMarker };
87
+ export { NumberingMap, computeListRendering, counterFormatOf, createNumberingMap, customNumberFormatPadWidth, formatOoxmlCounter as formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, markerAlignmentForLevel, markerFormattingFromLevel, numberingLevelHasMarkerSlot, padDecimal, parseNumbering, renderListMarker };
@@ -248,16 +248,16 @@ function parseListLevel(element) {
248
248
  * Resolve a `<w:numFmt>` element, or null when it is absent or carries a token
249
249
  * outside `ST_NumberFormat` (an mc:Fallback can then supply the rendering).
250
250
  *
251
- * `custom` stays `custom`, with its `@w:format` beside it. It used to be
252
- * decoded here into a synthetic `decimalZero{3,4,5}` the enumeration does not
253
- * declare, which the serializer wrote back as a `w:val` no consumer can read.
254
- * The pad width belongs to the renderer, not to the model.
251
+ * `@w:format` stays beside every admitted token exactly as authored. `custom`
252
+ * used to be decoded here into a synthetic `decimalZero{3,4,5}` the
253
+ * enumeration does not declare, which the serializer wrote back as a `w:val`
254
+ * no consumer can read. The pad-width interpretation belongs to the renderer,
255
+ * not to the model; preservation belongs to every schema-valid `CT_NumFmt`.
255
256
  */
256
257
  function resolveNumFmt(numFmtEl) {
257
258
  if (!numFmtEl) return null;
258
259
  const numFmt = narrowEnum(getAttribute(numFmtEl, "w", "val"), NumberFormatSchema);
259
260
  if (numFmt === void 0) return null;
260
- if (numFmt !== "custom") return { numFmt };
261
261
  const format = getAttribute(numFmtEl, "w", "format");
262
262
  return format === null ? { numFmt } : {
263
263
  numFmt,
@@ -293,14 +293,6 @@ const customNumberFormatPadWidth = (format) => {
293
293
  if (!/^0+1$/u.test(firstToken)) return;
294
294
  return Math.min(firstToken.length, 5);
295
295
  };
296
- /**
297
- * The `@w:format` a level's `w:numFmt` writes, which only `custom` carries.
298
- *
299
- * `custom` is the reserved member that defers to another attribute, so the
300
- * comparison lives here, in the module the reserved-value registry names as
301
- * its owner, rather than in the serializer that needs the answer.
302
- */
303
- const customNumberFormatOf = (level) => level.numFmt === "custom" ? level.numFmtFormat : void 0;
304
296
  /** What the marker renderer counts a level in; `custom` decides by pad width. */
305
297
  const counterFormatOf = (level) => {
306
298
  if (level.numFmt !== "custom") return level.numFmt;
@@ -511,4 +503,4 @@ function isBulletLevel(level) {
511
503
  /** Whether a numbering level reserves horizontal space for a visible marker. */
512
504
  const numberingLevelHasMarkerSlot = (level) => level.numFmt !== "none";
513
505
  //#endregion
514
- export { computeListRendering, counterFormatOf, createNumberingMap, customNumberFormatOf, customNumberFormatPadWidth, formatOoxmlCounter as formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, markerAlignmentForLevel, markerFormattingFromLevel, numberingLevelHasMarkerSlot, padDecimal, parseNumbering, renderListMarker };
506
+ export { computeListRendering, counterFormatOf, createNumberingMap, customNumberFormatPadWidth, formatOoxmlCounter as formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, markerAlignmentForLevel, markerFormattingFromLevel, numberingLevelHasMarkerSlot, padDecimal, parseNumbering, renderListMarker };
@@ -16,7 +16,7 @@ import { resolveThemeFontRef } from "./themeParser.js";
16
16
  import { parsePropertyChangeInfo } from "./trackedChangeInfo.js";
17
17
  import { captureVerbatimXml } from "./verbatimCapture.js";
18
18
  import { parseVmlImageContent, shouldPreserveRawVmlPict } from "./vmlImageParser.js";
19
- import { WORDPROCESSINGML_NAMESPACE_URIS, cloneWithXmlnsDeclarations, findAllDeep, findChild, findChildren, getAttribute, getChildElements, getLocalName, getNamespaceUri, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute, selectAlternateContentBranch } from "./xmlParser.js";
19
+ import { WORDPROCESSINGML_NAMESPACE_URIS, cloneWithXmlnsDeclarations, findAllDeep, findChild, findChildren, getAttribute, getChildElements, getLocalName, getNamespaceUri, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute, parseOnOffAttribute, selectAlternateContentBranch } from "./xmlParser.js";
20
20
  import { DRAWING_RAW_XML_MODES } from "@stll/docx-core/model";
21
21
  //#region src/docx/runParser.ts
22
22
  /**
@@ -486,19 +486,25 @@ function parseSymbolContent(element) {
486
486
  * Parse footnote reference (w:footnoteReference)
487
487
  */
488
488
  function parseFootnoteReference(element) {
489
- return {
489
+ const reference = {
490
490
  type: "footnoteRef",
491
491
  id: parseNumericAttribute(element, "w", "id") ?? 0
492
492
  };
493
+ const customMarkFollows = parseOnOffAttribute(element, "w", "customMarkFollows");
494
+ if (customMarkFollows !== void 0) reference.customMarkFollows = customMarkFollows;
495
+ return reference;
493
496
  }
494
497
  /**
495
498
  * Parse endnote reference (w:endnoteReference)
496
499
  */
497
500
  function parseEndnoteReference(element) {
498
- return {
501
+ const reference = {
499
502
  type: "endnoteRef",
500
503
  id: parseNumericAttribute(element, "w", "id") ?? 0
501
504
  };
505
+ const customMarkFollows = parseOnOffAttribute(element, "w", "customMarkFollows");
506
+ if (customMarkFollows !== void 0) reference.customMarkFollows = customMarkFollows;
507
+ return reference;
502
508
  }
503
509
  /**
504
510
  * Parse field character (w:fldChar)
@@ -670,7 +676,7 @@ function parseRunContents(runElement, rels, media, rootXmlns = {}) {
670
676
  case "cr":
671
677
  contents.push({
672
678
  type: "break",
673
- breakType: "textWrapping"
679
+ sourceElement: "cr"
674
680
  });
675
681
  break;
676
682
  case "AlternateContent": {
@@ -1,6 +1,5 @@
1
1
  import { serializeParagraphPropertySet } from "../../internal/paragraphFormattingSerialization.js";
2
2
  import { serializePreservedAttributes } from "../attributeRemainder.js";
3
- import { customNumberFormatOf } from "../numberingParser.js";
4
3
  import { serializePartElement } from "./partNamespaces.js";
5
4
  import { serializeTextFormatting } from "./textFormattingSerializer.js";
6
5
  import { intAttr } from "./xmlUtils.js";
@@ -9,8 +8,8 @@ import { serializeSequenceChildren } from "@stll/docx-core/schema";
9
8
  //#region src/docx/serializer/numberingSerializer.ts
10
9
  /** Serialize one `w:lvl`. */
11
10
  function serializeLevel(level) {
12
- const customFormat = customNumberFormatOf(level);
13
- const numFmtFormat = customFormat === void 0 ? "" : ` w:format="${escapeXmlAttribute(customFormat)}"`;
11
+ let numFmtFormat = "";
12
+ if (level.numFmtFormat !== void 0) numFmtFormat = ` w:format="${escapeXmlAttribute(level.numFmtFormat)}"`;
14
13
  const pPr = level.pPr === void 0 ? "" : serializeParagraphPropertySet({ formatting: level.pPr }) || "<w:pPr/>";
15
14
  const rPr = level.rPr === void 0 ? "" : serializeTextFormatting(level.rPr) || "<w:rPr/>";
16
15
  const lvlTextAttrs = [`w:val="${escapeXmlAttribute(level.lvlText)}"`];
@@ -76,6 +76,7 @@ function serializeTabContent(content) {
76
76
  * Serialize break content (w:br)
77
77
  */
78
78
  function serializeBreakContent(content) {
79
+ if (content.sourceElement === "cr") return "<w:cr/>";
79
80
  const attrs = [];
80
81
  if (content.breakType === "page") attrs.push("w:type=\"page\"");
81
82
  else if (content.breakType === "column") attrs.push("w:type=\"column\"");
@@ -94,8 +95,10 @@ function serializeSymbolContent(content) {
94
95
  * Serialize footnote/endnote reference
95
96
  */
96
97
  function serializeNoteReference(content) {
97
- if (content.type === "footnoteRef") return `<w:footnoteReference w:id="${content.id}"/>`;
98
- return `<w:endnoteReference w:id="${content.id}"/>`;
98
+ let customMarkFollows = "";
99
+ if (content.customMarkFollows !== void 0) customMarkFollows = ` w:customMarkFollows="${content.customMarkFollows ? "1" : "0"}"`;
100
+ if (content.type === "footnoteRef") return `<w:footnoteReference w:id="${content.id}"${customMarkFollows}/>`;
101
+ return `<w:endnoteReference w:id="${content.id}"${customMarkFollows}/>`;
99
102
  }
100
103
  /**
101
104
  * Serialize field character (w:fldChar)
@@ -13,7 +13,11 @@ function serializeFootnoteProperties(props) {
13
13
  if (!props) return "";
14
14
  const parts = [];
15
15
  if (props.position) parts.push(`<w:pos w:val="${props.position}"/>`);
16
- if (props.numFmt) parts.push(`<w:numFmt w:val="${props.numFmt}"/>`);
16
+ if (props.numFmt) {
17
+ let format = "";
18
+ if (props.numFmtFormat !== void 0) format = ` w:format="${escapeXmlAttribute(props.numFmtFormat)}"`;
19
+ parts.push(`<w:numFmt w:val="${props.numFmt}"${format}/>`);
20
+ }
17
21
  if (props.numStart !== void 0) parts.push(`<w:numStart w:val="${intAttr(props.numStart)}"/>`);
18
22
  if (props.numRestart) parts.push(`<w:numRestart w:val="${props.numRestart}"/>`);
19
23
  return parts.length > 0 ? `<w:footnotePr>${parts.join("")}</w:footnotePr>` : "";
@@ -25,7 +29,11 @@ function serializeEndnoteProperties(props) {
25
29
  if (!props) return "";
26
30
  const parts = [];
27
31
  if (props.position) parts.push(`<w:pos w:val="${props.position}"/>`);
28
- if (props.numFmt) parts.push(`<w:numFmt w:val="${props.numFmt}"/>`);
32
+ if (props.numFmt) {
33
+ let format = "";
34
+ if (props.numFmtFormat !== void 0) format = ` w:format="${escapeXmlAttribute(props.numFmtFormat)}"`;
35
+ parts.push(`<w:numFmt w:val="${props.numFmt}"${format}/>`);
36
+ }
29
37
  if (props.numStart !== void 0) parts.push(`<w:numStart w:val="${intAttr(props.numStart)}"/>`);
30
38
  if (props.numRestart) parts.push(`<w:numRestart w:val="${props.numRestart}"/>`);
31
39
  return parts.length > 0 ? `<w:endnotePr>${parts.join("")}</w:endnotePr>` : "";
@@ -529,10 +529,11 @@ function serializeCellContent(content, serializeParagraph) {
529
529
  */
530
530
  function serializeTableCell(cell, serializeParagraph) {
531
531
  const parts = [];
532
+ const id = cell.id === void 0 ? "" : ` w:id="${escapeXmlAttribute(cell.id)}"`;
532
533
  const tcPrXml = serializeTableCellFormatting(cell.formatting, cell.propertyChanges, cell.structuralChange);
533
534
  if (tcPrXml) parts.push(tcPrXml);
534
535
  parts.push(serializeCellContent(cell.content, serializeParagraph));
535
- return `<w:tc>${parts.join("")}</w:tc>`;
536
+ return `<w:tc${id}>${parts.join("")}</w:tc>`;
536
537
  }
537
538
  /**
538
539
  * A container's verbatim sink and its bookmark markers, as one positioned list.
@@ -12,7 +12,7 @@ import { TABLE_LOOK_FLAGS } from "./tableLook.js";
12
12
  import { parsePropertyChangeInfo, parseTrackedChangeInfo } from "./trackedChangeInfo.js";
13
13
  import { percentageSpelling, transitionalSlotEncoding } from "./transitionalSpelling.js";
14
14
  import { captureVerbatimXml } from "./verbatimCapture.js";
15
- import { cloneElement, findChild, findChildren, findWordprocessingChild, getAttribute, getLocalName, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute, parseOnOffAttribute, parseTableMeasurementValue, selectAlternateContentBranch } from "./xmlParser.js";
15
+ import { WORDPROCESSINGML_NAMESPACE_URIS, cloneElement, findChild, findChildren, findWordprocessingChild, getAttribute, getAttributeByNamespaceUri, getLocalName, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute, parseOnOffAttribute, parseTableMeasurementValue, selectAlternateContentBranch } from "./xmlParser.js";
16
16
  //#region src/docx/tableParser.ts
17
17
  /**
18
18
  * Sanity cap on `w:gridSpan` (and the derived table column count). Word's
@@ -858,6 +858,8 @@ function parseTableCell(tcElement, styles, theme, numbering, rels, media, option
858
858
  type: "tableCell",
859
859
  content: []
860
860
  };
861
+ const id = getAttributeByNamespaceUri(tcElement, WORDPROCESSINGML_NAMESPACE_URIS, "id");
862
+ if (id !== null) cell.id = id;
861
863
  const tcPrElement = findChild(tcElement, "w", "tcPr");
862
864
  const formatting = parseTableCellProperties(tcPrElement);
863
865
  if (formatting) cell.formatting = formatting;
@@ -89,6 +89,7 @@ const TEXT_BOX_TRACKED_CHANGE_TYPES = [
89
89
  "moveTo"
90
90
  ];
91
91
  const HARD_BREAK_TYPES = ["column", "textWrapping"];
92
+ const HARD_BREAK_SOURCE_ELEMENTS = ["br", "cr"];
92
93
  const PAGE_BREAK_CLEAR_VALUES = [
93
94
  "none",
94
95
  "left",
@@ -269,8 +270,19 @@ const readHardBreakAttrs = (node) => {
269
270
  const attrs = attrsRecord(node.attrs);
270
271
  const issues = [];
271
272
  expectNodeType(node, "hardBreak", issues);
273
+ optionalOneOf(attrs, "sourceElement", "hardBreak.attrs.sourceElement", issues, HARD_BREAK_SOURCE_ELEMENTS);
272
274
  optionalOneOf(attrs, "breakType", "hardBreak.attrs.breakType", issues, HARD_BREAK_TYPES);
273
275
  optionalOneOf(attrs, "clear", "hardBreak.attrs.clear", issues, PAGE_BREAK_CLEAR_VALUES);
276
+ if (attrs["sourceElement"] === "cr") {
277
+ if (attrs["breakType"] !== void 0 && attrs["breakType"] !== null) issues.push({
278
+ path: "hardBreak.attrs.breakType",
279
+ message: "Not allowed on a carriage-return element."
280
+ });
281
+ if (attrs["clear"] !== void 0 && attrs["clear"] !== null) issues.push({
282
+ path: "hardBreak.attrs.clear",
283
+ message: "Not allowed on a carriage-return element."
284
+ });
285
+ }
274
286
  return attrsResult(attrs, issues);
275
287
  };
276
288
  const expectHardBreakAttrs = (node) => expectCachedNodeAttrs(node, hardBreakAttrsCache, readHardBreakAttrs, "hard break attrs");
@@ -386,6 +398,7 @@ const readTableCellAttrs = (node) => {
386
398
  const attrs = attrsRecord(node.attrs);
387
399
  const issues = [];
388
400
  expectNodeTypeOneOf(node, ["tableCell", "tableHeader"], issues);
401
+ optionalString(attrs, "_docxCellId", "tableCell.attrs._docxCellId", issues);
389
402
  requiredNumber(attrs, "colspan", "tableCell.attrs.colspan", issues);
390
403
  requiredNumber(attrs, "rowspan", "tableCell.attrs.rowspan", issues);
391
404
  optionalOneOf(attrs, "_omittedGridSlot", "tableCell.attrs._omittedGridSlot", issues, OMITTED_GRID_SLOT_VALUES);
@@ -767,6 +780,7 @@ const readFootnoteRefMarkAttrs = (mark) => {
767
780
  requiredStringOrNumber(attrs, "id", "footnoteRef.attrs.id", issues);
768
781
  optionalOneOf(attrs, "noteType", "footnoteRef.attrs.noteType", issues, NOTE_TYPES);
769
782
  optionalOneOf(attrs, "vertAlign", "footnoteRef.attrs.vertAlign", issues, NOTE_REF_VERT_ALIGNS);
783
+ optionalBoolean(attrs, "customMarkFollows", "footnoteRef.attrs.customMarkFollows", issues);
770
784
  return attrsResult(attrs, issues);
771
785
  };
772
786
  const expectFootnoteRefMarkAttrs = (mark) => expectCachedMarkAttrs(mark, footnoteRefAttrsCache, readFootnoteRefMarkAttrs, "footnote reference attrs");
@@ -1018,6 +1032,9 @@ const readHyperlinkMarkAttrs = (mark) => {
1018
1032
  requiredString(attrs, "href", "hyperlink.attrs.href", issues);
1019
1033
  optionalString(attrs, "tooltip", "hyperlink.attrs.tooltip", issues);
1020
1034
  optionalString(attrs, "rId", "hyperlink.attrs.rId", issues);
1035
+ optionalString(attrs, "target", "hyperlink.attrs.target", issues);
1036
+ optionalBoolean(attrs, "history", "hyperlink.attrs.history", issues);
1037
+ optionalString(attrs, "docLocation", "hyperlink.attrs.docLocation", issues);
1021
1038
  validateNonNegativeInteger(attrs["_docxHyperlinkIndex"], "hyperlink.attrs._docxHyperlinkIndex", issues);
1022
1039
  return attrsResult(attrs, issues);
1023
1040
  };
@@ -2612,6 +2629,9 @@ const optionalEmptyHyperlinkArray = (value, issues) => {
2612
2629
  optionalString(item, "anchor", `${itemPath}.anchor`, issues);
2613
2630
  optionalString(item, "tooltip", `${itemPath}.tooltip`, issues);
2614
2631
  optionalString(item, "rId", `${itemPath}.rId`, issues);
2632
+ optionalString(item, "target", `${itemPath}.target`, issues);
2633
+ optionalBoolean(item, "history", `${itemPath}.history`, issues);
2634
+ optionalString(item, "docLocation", `${itemPath}.docLocation`, issues);
2615
2635
  }
2616
2636
  };
2617
2637
  //#endregion
@@ -1643,6 +1643,9 @@ function createEmptyHyperlink(attrs) {
1643
1643
  if (attrs.anchor !== void 0) hyperlink.anchor = attrs.anchor;
1644
1644
  if (attrs.tooltip !== void 0) hyperlink.tooltip = attrs.tooltip;
1645
1645
  if (attrs.rId !== void 0) hyperlink.rId = attrs.rId;
1646
+ if (attrs.target !== void 0) hyperlink.target = attrs.target;
1647
+ if (attrs.history !== void 0) hyperlink.history = attrs.history;
1648
+ if (attrs.docLocation !== void 0) hyperlink.docLocation = attrs.docLocation;
1646
1649
  return hyperlink;
1647
1650
  }
1648
1651
  function getCommentMarkIds(marks) {
@@ -1684,6 +1687,9 @@ function getLinkKey(mark) {
1684
1687
  attrs.href,
1685
1688
  attrs.rId ?? "",
1686
1689
  attrs.tooltip ?? "",
1690
+ attrs.target ?? "",
1691
+ attrs.history === void 0 ? "" : String(attrs.history),
1692
+ attrs.docLocation ?? "",
1687
1693
  attrs._docxHyperlinkIndex ?? ""
1688
1694
  ].join("\0");
1689
1695
  }
@@ -1701,21 +1707,24 @@ function getMarksKey(marks) {
1701
1707
  function createHyperlink(linkMark) {
1702
1708
  const attrs = expectHyperlinkMarkAttrs(linkMark);
1703
1709
  const href = attrs.href;
1704
- if (href.startsWith("#")) {
1705
- const hyperlink = {
1706
- type: "hyperlink",
1707
- anchor: href.slice(1),
1708
- children: []
1709
- };
1710
- if (attrs.tooltip) hyperlink.tooltip = attrs.tooltip;
1711
- return hyperlink;
1712
- }
1710
+ const metadata = {
1711
+ ...attrs.tooltip === void 0 ? {} : { tooltip: attrs.tooltip },
1712
+ ...attrs.target === void 0 ? {} : { target: attrs.target },
1713
+ ...attrs.history === void 0 ? {} : { history: attrs.history },
1714
+ ...attrs.docLocation === void 0 ? {} : { docLocation: attrs.docLocation }
1715
+ };
1716
+ if (href.startsWith("#")) return {
1717
+ type: "hyperlink",
1718
+ anchor: href.slice(1),
1719
+ ...metadata,
1720
+ children: []
1721
+ };
1713
1722
  const hyperlink = {
1714
1723
  type: "hyperlink",
1715
1724
  href,
1725
+ ...metadata,
1716
1726
  children: []
1717
1727
  };
1718
- if (attrs.tooltip) hyperlink.tooltip = attrs.tooltip;
1719
1728
  if (attrs.rId) hyperlink.rId = attrs.rId;
1720
1729
  return hyperlink;
1721
1730
  }
@@ -1831,12 +1840,14 @@ function getNoteReferenceVertAlign(noteAttrs, marks) {
1831
1840
  }
1832
1841
  function createNoteReferenceRun(noteRefMark, marks, formattingContext) {
1833
1842
  const noteAttrs = expectFootnoteRefMarkAttrs(noteRefMark);
1843
+ const noteRef = {
1844
+ type: noteAttrs.noteType === "endnote" ? "endnoteRef" : "footnoteRef",
1845
+ id: typeof noteAttrs.id === "string" ? Number.parseInt(noteAttrs.id, 10) || 0 : noteAttrs.id
1846
+ };
1847
+ if (noteAttrs.customMarkFollows !== void 0) noteRef.customMarkFollows = noteAttrs.customMarkFollows;
1834
1848
  const run = {
1835
1849
  type: "run",
1836
- content: [{
1837
- type: noteAttrs.noteType === "endnote" ? "endnoteRef" : "footnoteRef",
1838
- id: typeof noteAttrs.id === "string" ? Number.parseInt(noteAttrs.id, 10) || 0 : noteAttrs.id
1839
- }]
1850
+ content: [noteRef]
1840
1851
  };
1841
1852
  const formatting = getAtomRunFormattingFromMarks(marks, formattingContext);
1842
1853
  const vertAlign = getNoteReferenceVertAlign(noteAttrs, marks);
@@ -2045,8 +2056,12 @@ function createPageBreakCarrierRun({ baseParagraphFormatting, clear, inheritedFo
2045
2056
  function createBreakRun(attrs, marks, formattingContext) {
2046
2057
  const run = {
2047
2058
  type: "run",
2048
- content: [{
2059
+ content: [attrs.sourceElement === "cr" ? {
2060
+ type: "break",
2061
+ sourceElement: "cr"
2062
+ } : {
2049
2063
  type: "break",
2064
+ ...attrs.sourceElement !== void 0 ? { sourceElement: attrs.sourceElement } : {},
2050
2065
  ...attrs.breakType !== void 0 ? { breakType: attrs.breakType } : {},
2051
2066
  ...attrs.clear !== void 0 ? { clear: attrs.clear } : {}
2052
2067
  }]
@@ -3305,6 +3320,7 @@ function convertPMTableCell(node, documentCounts, styleResolver = null) {
3305
3320
  type: "tableCell",
3306
3321
  content
3307
3322
  };
3323
+ if (attrs._docxCellId !== void 0) cell.id = attrs._docxCellId;
3308
3324
  const cellFormatting = tableCellAttrsToFormatting(attrs);
3309
3325
  if (cellFormatting) cell.formatting = cellFormatting;
3310
3326
  if (Array.isArray(attrs.tcPrChange) && attrs.tcPrChange.length > 0) cell.propertyChanges = [...attrs.tcPrChange];
@@ -492,7 +492,10 @@ function convertParagraph(paragraph, styleResolver, context, extraRunFormatting,
492
492
  ...content.href !== void 0 ? { href: content.href } : {},
493
493
  ...content.anchor !== void 0 ? { anchor: content.anchor } : {},
494
494
  ...content.tooltip !== void 0 ? { tooltip: content.tooltip } : {},
495
- ...content.rId !== void 0 ? { rId: content.rId } : {}
495
+ ...content.rId !== void 0 ? { rId: content.rId } : {},
496
+ ...content.target !== void 0 ? { target: content.target } : {},
497
+ ...content.history !== void 0 ? { history: content.history } : {},
498
+ ...content.docLocation !== void 0 ? { docLocation: content.docLocation } : {}
496
499
  });
497
500
  break;
498
501
  }
@@ -1325,6 +1328,7 @@ function convertTableCell({ cell, styleResolver, context, isHeader, gridWidthPer
1325
1328
  colspan: formatting?.gridSpan ?? 1,
1326
1329
  rowspan
1327
1330
  };
1331
+ if (cell.id !== void 0) attrs._docxCellId = cell.id;
1328
1332
  if (effectiveFormatting.width.type === "value") {
1329
1333
  attrs.width = effectiveFormatting.width.value;
1330
1334
  if (effectiveFormatting.width.widthType) attrs.widthType = effectiveFormatting.width.widthType;
@@ -2064,6 +2068,7 @@ function convertRunContent(content, marks, formatting, textBoxAnchors, authoredF
2064
2068
  case "break":
2065
2069
  if (content.breakType === "textWrapping" || !content.breakType) {
2066
2070
  const attrs = {
2071
+ ...content.sourceElement !== void 0 ? { sourceElement: content.sourceElement } : {},
2067
2072
  ...content.breakType !== void 0 ? { breakType: content.breakType } : {},
2068
2073
  ...content.clear !== void 0 ? { clear: content.clear } : {}
2069
2074
  };
@@ -2095,7 +2100,8 @@ function convertRunContent(content, marks, formatting, textBoxAnchors, authoredF
2095
2100
  const footnoteMark = schema.mark("footnoteRef", {
2096
2101
  id: content.id.toString(),
2097
2102
  noteType: "footnote",
2098
- vertAlign: noteReferenceVertAlign(formatting?.vertAlign)
2103
+ vertAlign: noteReferenceVertAlign(formatting?.vertAlign),
2104
+ customMarkFollows: content.customMarkFollows
2099
2105
  });
2100
2106
  return [schema.text(content.id.toString(), [...marks, footnoteMark])];
2101
2107
  }
@@ -2103,7 +2109,8 @@ function convertRunContent(content, marks, formatting, textBoxAnchors, authoredF
2103
2109
  const endnoteMark = schema.mark("footnoteRef", {
2104
2110
  id: content.id.toString(),
2105
2111
  noteType: "endnote",
2106
- vertAlign: noteReferenceVertAlign(formatting?.vertAlign)
2112
+ vertAlign: noteReferenceVertAlign(formatting?.vertAlign),
2113
+ customMarkFollows: content.customMarkFollows
2107
2114
  });
2108
2115
  return [schema.text(content.id.toString(), [...marks, endnoteMark])];
2109
2116
  }
@@ -2284,6 +2291,9 @@ function convertHyperlink(hyperlink, { getInheritedRunFormatting, styleResolver,
2284
2291
  href,
2285
2292
  tooltip: hyperlink.tooltip,
2286
2293
  rId: hyperlink.rId,
2294
+ target: hyperlink.target,
2295
+ history: hyperlink.history,
2296
+ docLocation: hyperlink.docLocation,
2287
2297
  _docxHyperlinkIndex: hyperlinkIndex
2288
2298
  });
2289
2299
  for (const { content: child, stack } of withInlineWrapperStacks(hyperlink.children)) {
@@ -20,7 +20,8 @@ const FootnoteRefExtension = createMarkExtension({
20
20
  attrs: {
21
21
  id: {},
22
22
  noteType: { default: "footnote" },
23
- vertAlign: { default: null }
23
+ vertAlign: { default: null },
24
+ customMarkFollows: { default: null }
24
25
  },
25
26
  parseDOM: [
26
27
  {
@@ -82,6 +82,9 @@ const HyperlinkExtension = createMarkExtension({
82
82
  href: {},
83
83
  tooltip: { default: null },
84
84
  rId: { default: null },
85
+ target: { default: null },
86
+ history: { default: null },
87
+ docLocation: { default: null },
85
88
  _docxHyperlinkIndex: { default: null }
86
89
  },
87
90
  inclusive: false,
@@ -89,14 +92,15 @@ const HyperlinkExtension = createMarkExtension({
89
92
  tag: "a[href]",
90
93
  getAttrs: (dom) => ({
91
94
  href: sanitizeStoredHref(dom.getAttribute("href") ?? void 0),
92
- tooltip: dom.getAttribute("title") ?? void 0
95
+ tooltip: dom.getAttribute("title") ?? void 0,
96
+ target: dom.getAttribute("target") ?? void 0
93
97
  })
94
98
  }],
95
99
  toDOM(mark) {
96
- const { href, tooltip } = expectHyperlinkMarkAttrs(mark);
100
+ const { href, target, tooltip } = expectHyperlinkMarkAttrs(mark);
97
101
  const domAttrs = {
98
102
  href: sanitizeStoredHref(href),
99
- ...anchorTargetAttrs(void 0)
103
+ ...anchorTargetAttrs(target)
100
104
  };
101
105
  if (tooltip) domAttrs["title"] = tooltip;
102
106
  return [
@@ -12,6 +12,7 @@ const HardBreakExtension = createNodeExtension({
12
12
  inline: true,
13
13
  group: "inline",
14
14
  attrs: {
15
+ sourceElement: { default: null },
15
16
  breakType: { default: null },
16
17
  clear: { default: null }
17
18
  },
@@ -22,15 +23,18 @@ const HardBreakExtension = createNodeExtension({
22
23
  if (!(node instanceof HTMLElement)) return null;
23
24
  const breakType = node.dataset["docxBreakType"];
24
25
  const clear = node.dataset["docxBreakClear"];
26
+ const sourceElement = node.dataset["docxSourceElement"];
25
27
  const attrs = {};
28
+ if (sourceElement === "br" || sourceElement === "cr") attrs["sourceElement"] = sourceElement;
26
29
  if (breakType === "column" || breakType === "textWrapping") attrs["breakType"] = breakType;
27
30
  if (clear === "none" || clear === "left" || clear === "right" || clear === "all") attrs["clear"] = clear;
28
31
  return Object.keys(attrs).length > 0 ? attrs : null;
29
32
  }
30
33
  }],
31
34
  toDOM(node) {
32
- const { breakType, clear } = expectHardBreakAttrs(node);
35
+ const { sourceElement, breakType, clear } = expectHardBreakAttrs(node);
33
36
  const attrs = {};
37
+ if (sourceElement !== void 0) attrs["data-docx-source-element"] = sourceElement;
34
38
  if (breakType !== void 0) attrs["data-docx-break-type"] = breakType;
35
39
  if (clear !== void 0) attrs["data-docx-break-clear"] = clear;
36
40
  return Object.keys(attrs).length > 0 ? ["br", attrs] : ["br"];
@@ -39,6 +39,43 @@ const preserveOmittedGridSlots = (command) => (state, dispatch, view) => {
39
39
  if (table && hasOmittedGridSlots(table)) return false;
40
40
  return command(state, dispatch, view);
41
41
  };
42
+ const preserveSourceCellIdentity = (command) => (state, dispatch, view) => {
43
+ if (!command(state, void 0, view)) return false;
44
+ if (!dispatch) return true;
45
+ const rect = selectedRect(state);
46
+ const sourceRelativePosition = rect.map.map.at(rect.top * rect.map.width + rect.left);
47
+ if (sourceRelativePosition === void 0) return panic("The selected table cell is absent from its table map");
48
+ const sourceCell = rect.table.nodeAt(sourceRelativePosition);
49
+ if (!sourceCell) return panic("The selected table cell is absent from its table map");
50
+ const sourceId = expectTableCellAttrs(sourceCell)._docxCellId;
51
+ return command(state, (tr) => {
52
+ if (sourceId === void 0 || sourceId === null) {
53
+ dispatch(tr);
54
+ return;
55
+ }
56
+ const mappedTablePosition = tr.mapping.map(rect.tableStart - 1, -1);
57
+ const table = tr.doc.nodeAt(mappedTablePosition);
58
+ if (!table) return panic("The split-cell transaction lost its table");
59
+ const tableStart = mappedTablePosition + 1;
60
+ const map = TableMap.get(table);
61
+ const survivingRelativePosition = map.map.at(rect.top * map.width + rect.left);
62
+ if (survivingRelativePosition === void 0) return panic("The surviving table cell is absent from its table map");
63
+ const cleared = /* @__PURE__ */ new Set();
64
+ for (let row = rect.top; row < rect.bottom; row++) for (let column = rect.left; column < rect.right; column++) {
65
+ const relativePosition = map.map.at(row * map.width + column);
66
+ if (relativePosition === void 0) return panic("A split table cell is absent from its table map");
67
+ if (relativePosition === survivingRelativePosition || cleared.has(relativePosition)) continue;
68
+ cleared.add(relativePosition);
69
+ const cell = table.nodeAt(relativePosition);
70
+ if (!cell) return panic("A split table cell is absent from its table map");
71
+ tr.setNodeMarkup(tableStart + relativePosition, null, {
72
+ ...cell.attrs,
73
+ _docxCellId: null
74
+ });
75
+ }
76
+ dispatch(tr);
77
+ }, view);
78
+ };
42
79
  const isOneOf = (value, values) => value !== null && value !== void 0 && values.some((allowed) => allowed === value);
43
80
  /** Map CSS border-style to OOXML border style. */
44
81
  function cssBorderStyleToOoxml(cssStyle) {
@@ -334,6 +371,7 @@ const tableCellSpec = {
334
371
  tableRole: "cell",
335
372
  isolating: true,
336
373
  attrs: {
374
+ _docxCellId: { default: null },
337
375
  colspan: { default: 1 },
338
376
  rowspan: { default: 1 },
339
377
  _omittedGridSlot: { default: null },
@@ -402,6 +440,7 @@ const tableHeaderSpec = {
402
440
  tableRole: "header_cell",
403
441
  isolating: true,
404
442
  attrs: {
443
+ _docxCellId: { default: null },
405
444
  colspan: { default: 1 },
406
445
  rowspan: { default: 1 },
407
446
  _omittedGridSlot: { default: null },
@@ -2004,7 +2043,7 @@ const TablePluginExtension = createExtension({
2004
2043
  selectRow: () => selectRow,
2005
2044
  selectColumn: () => selectColumn,
2006
2045
  mergeCells: () => preserveOmittedGridSlots(mergeCells),
2007
- splitCell: () => preserveOmittedGridSlots(splitCell),
2046
+ splitCell: () => preserveOmittedGridSlots(preserveSourceCellIdentity(splitCell)),
2008
2047
  setCellBorder: (side, spec, clearOthers) => setCellBorder(side, spec, clearOthers),
2009
2048
  setTableBorders: (preset, borderSpec) => setTableBorders(preset, borderSpec),
2010
2049
  setTableBorderPreset: (preset) => setTableBorderPreset(preset),
@@ -1,5 +1,6 @@
1
1
  import { decodeOoxmlSymbolCharacter } from "../utils/ooxmlSymbol.js";
2
2
  import { expectFieldAttrs, expectPreservedXmlAttrs, expectSymbolAttrs } from "./attrs/index.js";
3
+ import { PRESERVED_XML_LEVELS } from "./schema/nodes.js";
3
4
  import { panic } from "better-result";
4
5
  import { Mark } from "prosemirror-model";
5
6
  //#region src/prosemirror/runFormattingInlineCarriers.ts
@@ -31,6 +32,22 @@ const RUN_FORMATTING_INLINE_ATOM_DISPOSITIONS = Object.freeze({
31
32
  textBoxAnchor: "not-a-run"
32
33
  });
33
34
  const dispositionsByName = new Map(Object.entries(RUN_FORMATTING_INLINE_ATOM_DISPOSITIONS));
35
+ /**
36
+ * One preserved capture is a `w:r` child and the other is a `w:p` child, and
37
+ * the map above can only name the node type.
38
+ *
39
+ * Only the run-level capture serializes inside a `w:r`, so only it has a
40
+ * `w:rPr` to carry; the save path already branches on the same level. A
41
+ * paragraph-level capture is the markup Word writes BETWEEN runs — `w:proofErr`
42
+ * above all — and treating it as a carrier makes every consumer of this map
43
+ * wrong in the same way: formatting marks put on it are dropped on save, and a
44
+ * comparison is asked to line up the base's proofing annotations with the
45
+ * revised document's own, which no redline can do.
46
+ */
47
+ const PRESERVED_XML_DISPOSITIONS = {
48
+ [PRESERVED_XML_LEVELS.run]: "preserved-xml-run",
49
+ [PRESERVED_XML_LEVELS.inline]: "not-a-run"
50
+ };
34
51
  const CONTROL_CHARACTER_BY_DISPOSITION = Object.freeze({
35
52
  "break-run": "\n",
36
53
  "field-run": null,
@@ -73,7 +90,7 @@ const runFormattingInlineAtomDisposition = (node) => {
73
90
  if (!node.isInline || !node.isAtom) return null;
74
91
  const disposition = dispositionsByName.get(node.type.name);
75
92
  if (!disposition) return panic(`Inline atom ${JSON.stringify(node.type.name)} has no run-formatting disposition`);
76
- return disposition;
93
+ return disposition === "preserved-xml-run" ? PRESERVED_XML_DISPOSITIONS[expectPreservedXmlAttrs(node).level] : disposition;
77
94
  };
78
95
  /** Apply a mark set without changing a carrier's text or serialized ownership. */
79
96
  const applyMarksToRunFormattingRepresentation = ({ tr, representation, marks }) => {
@@ -81,6 +81,7 @@ type FootnoteRefAttrs = {
81
81
  id: string | number;
82
82
  noteType?: "footnote" | "endnote";
83
83
  vertAlign?: "baseline" | "superscript";
84
+ customMarkFollows?: document_d_exports.NoteReferenceContent["customMarkFollows"];
84
85
  };
85
86
  type CommentAttrs = {
86
87
  commentId: number;
@@ -271,6 +272,9 @@ type HyperlinkAttrs = {
271
272
  href: string;
272
273
  tooltip?: string;
273
274
  rId?: string;
275
+ target?: document_d_exports.Hyperlink["target"];
276
+ history?: document_d_exports.Hyperlink["history"];
277
+ docLocation?: document_d_exports.Hyperlink["docLocation"];
274
278
  _docxHyperlinkIndex?: number;
275
279
  };
276
280
  //#endregion
@@ -7,6 +7,12 @@ import { ParagraphNumberingAttr } from "../numberingAttr.js";
7
7
  import { ParagraphDirection } from "../paragraphDirection.js";
8
8
  //#region src/prosemirror/schema/nodes.d.ts
9
9
  type HardBreakAttrs = {
10
+ sourceElement: "cr";
11
+ breakType?: never;
12
+ clear?: never;
13
+ } | {
14
+ /** Absent and `br` both serialize as the standard break element. */
15
+ sourceElement?: "br";
10
16
  /** Absent means the source `w:br` omitted `w:type`. */
11
17
  breakType?: "column" | "textWrapping";
12
18
  clear?: "none" | "left" | "right" | "all";
@@ -216,6 +222,9 @@ type ParagraphAttrs = {
216
222
  anchor?: document_d_exports.Hyperlink["anchor"];
217
223
  tooltip?: document_d_exports.Hyperlink["tooltip"];
218
224
  rId?: document_d_exports.Hyperlink["rId"];
225
+ target?: document_d_exports.Hyperlink["target"];
226
+ history?: document_d_exports.Hyperlink["history"];
227
+ docLocation?: document_d_exports.Hyperlink["docLocation"];
219
228
  }[];
220
229
  /**
221
230
  * Run-in heading flag: this paragraph's mark carries
@@ -1056,6 +1065,8 @@ type SuggestedStructuralMarker = {
1056
1065
  * Table cell attributes
1057
1066
  */
1058
1067
  type TableCellAttrs = {
1068
+ /** Authored OOXML cell identifier. */
1069
+ _docxCellId?: document_d_exports.TableCell["id"];
1059
1070
  /** Column span */
1060
1071
  colspan: number;
1061
1072
  /** Row span */
@@ -5,7 +5,7 @@ type TablePropertiesCommand = {
5
5
  widthType?: document_d_exports.TableWidthType | null;
6
6
  justification?: document_d_exports.TableAlignment | null;
7
7
  };
8
- declare const TABLE_PROPERTY_JUSTIFICATIONS: readonly ("start" | "center" | "end" | "left" | "right")[];
8
+ declare const TABLE_PROPERTY_JUSTIFICATIONS: readonly document_d_exports.TableAlignment[];
9
9
  declare const toTablePropertyJustification: (value: string | null | undefined) => NonNullable<TablePropertiesCommand["justification"]>;
10
10
  type TableAction = "addRowAbove" | "addRowBelow" | "addColumnLeft" | "addColumnRight" | "deleteRow" | "deleteColumn" | "mergeCells" | "splitCell" | "deleteTable" | "selectTable" | "selectRow" | "selectColumn" | "borderAll" | "borderOutside" | "borderInside" | "borderNone" | "borderTop" | "borderBottom" | "borderLeft" | "borderRight" | {
11
11
  type: "cellFillColor";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.47.1",
3
+ "version": "0.47.2",
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",
@@ -128,7 +128,7 @@
128
128
  "perf": "bun scripts/profile-editor.ts"
129
129
  },
130
130
  "dependencies": {
131
- "@stll/docx-core": "^0.25.0",
131
+ "@stll/docx-core": "^0.25.1",
132
132
  "@stll/docx-utils": "^0.1.0",
133
133
  "@stll/template-conditions": ">=0.4.0 <1.0.0",
134
134
  "better-result": "3.0.1",