@stll/folio-core 0.58.3 → 0.58.5

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.
@@ -0,0 +1,10 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { DocumentOp } from "@stll/docx-core/ops";
3
+ //#region src/controller/canonicalListRendering.d.ts
4
+ type CanonicalListNormalization = {
5
+ document: document_d_exports.Document;
6
+ inverse: DocumentOp[];
7
+ };
8
+ declare const normalizeCanonicalListRendering: (document: document_d_exports.Document) => CanonicalListNormalization;
9
+ //#endregion
10
+ export { normalizeCanonicalListRendering };
@@ -0,0 +1,169 @@
1
+ import { foldListNumberFields, planParagraphListNumberFold, unfoldedListNumberContent } from "../docx/foldedListNumberFields.js";
2
+ import { computeListMarker } from "../docx/listMarkerComputation.js";
3
+ import { listRenderingDefinitionsMatch } from "../docx/listRendering.js";
4
+ import { computeListRendering, getCachedNumberingMap } from "../docx/numberingParser.js";
5
+ import { mergeParagraphNumbering, paragraphNumberingReferenceId, sameStatedParagraphNumbering } from "../docx/numberingReference.js";
6
+ import { assignParagraphIndentationProjection, cloneParagraphWithPropertySource, paragraphFormattingWithAuthoredIndentation, sameParagraphIndentationProjection } from "../docx/paragraphPropertySource.js";
7
+ import { paragraphIndentationFromFormatting } from "../prosemirror/paragraphIndentation.js";
8
+ import { listIndentationProvenancePatch } from "../prosemirror/styles/resolvedStyleAttrs.js";
9
+ import { createStyleResolver } from "../prosemirror/styles/styleResolver.js";
10
+ import { canonicalJson } from "../utils/canonicalJson.js";
11
+ import { panic } from "better-result";
12
+ import { DOCUMENT_OP_TYPES, EMPTY_PROPERTY_SETS, OP_STORIES, registerParagraphPropertySnapshot, withBodyContent } from "@stll/docx-core/ops";
13
+ //#region src/controller/canonicalListRendering.ts
14
+ /** Canonical edits retain authored fields; list rendering follows the current numbering. */
15
+ const normalizeCanonicalListRendering = (document) => {
16
+ const inverse = [];
17
+ const styles = createStyleResolver(document.package.styles);
18
+ const numbering = document.package.numbering === void 0 ? null : getCachedNumberingMap(document.package.numbering);
19
+ const normalizeBody = (body, story) => {
20
+ const counters = {
21
+ numbering,
22
+ listCounters: /* @__PURE__ */ new Map(),
23
+ abstractCounters: /* @__PURE__ */ new Map(),
24
+ restartedNumIds: /* @__PURE__ */ new Set(),
25
+ previousList: {
26
+ abstractNumId: null,
27
+ fromStyle: false,
28
+ numId: null
29
+ }
30
+ };
31
+ const content = body.content.map((paragraph) => {
32
+ if (paragraph.type !== "paragraph") return paragraph;
33
+ registerParagraphPropertySnapshot({
34
+ paragraph,
35
+ snapshot: paragraphFormattingWithAuthoredIndentation(paragraph)
36
+ });
37
+ const formatting = paragraph.formatting;
38
+ const numPr = mergeParagraphNumbering(formatting?.numPrFromStyle, formatting?.numPr);
39
+ if (paragraph.listRendering === void 0 && formatting?.numPrFromStyle === void 0 && (numPr?.kind !== "reference" || numbering === null)) {
40
+ computeListMarker(paragraph, counters);
41
+ return paragraph;
42
+ }
43
+ const definition = numPr?.kind === "reference" && numbering !== null ? computeListRendering(numPr, numbering) : null;
44
+ const cached = paragraph.listRendering;
45
+ const rendering = definition !== null && cached !== void 0 && listRenderingDefinitionsMatch(cached, definition) ? { ...cached } : definition;
46
+ const next = cloneParagraphWithPropertySource(paragraph, {});
47
+ if (formatting?.numPrFromStyle !== void 0 && formatting.numPr !== void 0 && paragraphNumberingReferenceId(numPr) !== paragraphNumberingReferenceId(formatting.numPrFromStyle)) {
48
+ next.formatting = { ...formatting };
49
+ delete next.formatting.numPrFromStyle;
50
+ inverse.push({
51
+ type: DOCUMENT_OP_TYPES.SET_PARAGRAPH_PROPS,
52
+ story,
53
+ blockId: paragraph.paraId ?? panic("Canonical numbering normalization requires paragraph identities."),
54
+ patch: { numPrFromStyle: formatting.numPrFromStyle }
55
+ });
56
+ }
57
+ if (formatting?.numPrFromStyle !== void 0 && numPr?.kind === "reference" && !sameStatedParagraphNumbering(formatting.numPr, numPr)) {
58
+ next.formatting = {
59
+ ...next.formatting,
60
+ numPr
61
+ };
62
+ inverse.push({
63
+ type: DOCUMENT_OP_TYPES.SET_PARAGRAPH_PROPS,
64
+ story,
65
+ blockId: paragraph.paraId ?? panic("Canonical numbering normalization requires paragraph identities."),
66
+ patch: { numPr: formatting.numPr ?? null }
67
+ });
68
+ }
69
+ const authoredFormatting = paragraphFormattingWithAuthoredIndentation(next);
70
+ assignParagraphIndentationProjection({
71
+ paragraph: next,
72
+ authored: authoredFormatting,
73
+ inherited: paragraphIndentationFromFormatting(listIndentationProvenancePatch({
74
+ direct: paragraphIndentationFromFormatting(authoredFormatting),
75
+ styleFormatting: styles.resolveParagraphStyle(next.formatting?.styleId).paragraphFormatting,
76
+ numberingSource: next.formatting?.numPrFromStyle === void 0 ? "paragraph" : "style",
77
+ numPr: numPr?.kind === "reference" ? {
78
+ numId: numPr.numId,
79
+ ilvl: numPr.ilvl ?? 0
80
+ } : void 0,
81
+ numbering
82
+ })._resolvedFormatting) ?? {}
83
+ });
84
+ if (canonicalJson(paragraphIndentationFromFormatting(next.formatting)) !== canonicalJson(paragraphIndentationFromFormatting(formatting))) inverse.unshift({
85
+ type: DOCUMENT_OP_TYPES.SET_PARAGRAPH_PROPS,
86
+ story,
87
+ blockId: paragraph.paraId ?? panic("Canonical numbering normalization requires paragraph identities."),
88
+ patch: {
89
+ indentLeft: formatting?.indentLeft ?? null,
90
+ indentRight: formatting?.indentRight ?? null,
91
+ indentFirstLine: formatting?.indentFirstLine ?? null,
92
+ hangingIndent: formatting?.hangingIndent ?? null
93
+ },
94
+ whenEmpty: formatting === void 0 ? EMPTY_PROPERTY_SETS.OMIT : EMPTY_PROPERTY_SETS.KEEP
95
+ });
96
+ if (rendering === null) delete next.listRendering;
97
+ else {
98
+ const fields = foldListNumberFields(paragraph.content.map(unfoldedListNumberContent), () => void 0).fieldCount;
99
+ if (fields === 0) delete rendering.implicitChildLevelAdvances;
100
+ else rendering.implicitChildLevelAdvances = fields;
101
+ const template = rendering.markerTemplate ?? rendering.marker;
102
+ const fold = planParagraphListNumberFold(paragraph.content, !rendering.isBullet && template !== "" && !template.includes(" "));
103
+ if (fold.suffix === void 0) delete rendering.foldedMarkerSuffix;
104
+ else rendering.foldedMarkerSuffix = fold.suffix;
105
+ const child = numbering?.getLevel(rendering.numId, rendering.level + 1)?.pPr;
106
+ if (fold.suffix !== void 0 && child?.hangingIndent === true && child.indentFirstLine !== void 0 && child.indentFirstLine < 0) rendering.markerSecondSlotOffsetTwips = -child.indentFirstLine;
107
+ else delete rendering.markerSecondSlotOffsetTwips;
108
+ rendering.marker = rendering.foldedMarkerSuffix !== void 0 && !template.includes(" ") ? `${template}\t${rendering.foldedMarkerSuffix}` : template;
109
+ next.listRendering = rendering;
110
+ }
111
+ computeListMarker(next, counters);
112
+ registerParagraphPropertySnapshot({
113
+ paragraph: next,
114
+ snapshot: authoredFormatting
115
+ });
116
+ if (sameParagraphIndentationProjection({
117
+ left: next,
118
+ right: paragraph
119
+ }) && canonicalJson(next.formatting) === canonicalJson(formatting) && canonicalJson(next.listRendering) === canonicalJson(cached)) return paragraph;
120
+ return next;
121
+ });
122
+ return content.every((paragraph, index) => paragraph === body.content[index]) ? body : {
123
+ ...body,
124
+ ...withBodyContent(body, content)
125
+ };
126
+ };
127
+ const pkg = document.package;
128
+ const body = normalizeBody(pkg.document, OP_STORIES.MAIN);
129
+ const normalizeParts = (parts, kind) => {
130
+ if (parts === void 0) return parts;
131
+ const normalized = new Map([...parts].map(([id, part]) => [id, normalizeBody(part, {
132
+ kind,
133
+ rId: id
134
+ })]));
135
+ return [...normalized].every(([id, part]) => part === parts.get(id)) ? parts : normalized;
136
+ };
137
+ const normalizeNotes = (notes, kind) => {
138
+ if (notes === void 0) return notes;
139
+ const normalized = notes.map((note) => normalizeBody(note, {
140
+ kind,
141
+ id: note.id
142
+ }));
143
+ return normalized.every((note, index) => note === notes[index]) ? notes : normalized;
144
+ };
145
+ const headers = normalizeParts(pkg.headers, "header");
146
+ const footers = normalizeParts(pkg.footers, "footer");
147
+ const footnotes = normalizeNotes(pkg.footnotes, "footnote");
148
+ const endnotes = normalizeNotes(pkg.endnotes, "endnote");
149
+ if (body === pkg.document && headers === pkg.headers && footers === pkg.footers && footnotes === pkg.footnotes && endnotes === pkg.endnotes) return {
150
+ document,
151
+ inverse
152
+ };
153
+ return {
154
+ inverse,
155
+ document: {
156
+ ...document,
157
+ package: {
158
+ ...pkg,
159
+ document: body,
160
+ ...headers === void 0 ? {} : { headers },
161
+ ...footers === void 0 ? {} : { footers },
162
+ ...footnotes === void 0 ? {} : { footnotes },
163
+ ...endnotes === void 0 ? {} : { endnotes }
164
+ }
165
+ }
166
+ };
167
+ };
168
+ //#endregion
169
+ export { normalizeCanonicalListRendering };
@@ -1,12 +1,13 @@
1
1
  import { splitsGraphemeCluster, splitsSurrogatePair } from "../ai-edits/character-boundaries.js";
2
2
  import { rangedCommentIds, visitCommentMarkers } from "../docx/commentAnchorIndex.js";
3
- import { cloneDocumentWithParagraphPropertySources, copyDocumentParagraphPropertySourceContract, copyParagraphPropertySource } from "../docx/paragraphPropertySource.js";
3
+ import { cloneDocumentWithParagraphPropertySources, copyDocumentParagraphPropertySourceContract, copyParagraphPropertySource, restoreParagraphIndentationProjection } from "../docx/paragraphPropertySource.js";
4
4
  import { canonicalSelectionRange } from "../prosemirror/canonicalSelectionRange.js";
5
5
  import { collectPairedBookmarkIds, footnoteToProseDoc, headerFooterToProseDoc, toProseDoc } from "../prosemirror/conversion/toProseDoc.js";
6
6
  import { markPackageChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
7
7
  import { runFormattingPatchFromMarks } from "../prosemirror/runFormattingFromMarks.js";
8
8
  import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
9
9
  import { canonicalInlineSourceIssue, canonicalNativeCells, projectCanonicalInline } from "./canonicalInlineProjection.js";
10
+ import { normalizeCanonicalListRendering } from "./canonicalListRendering.js";
10
11
  import { restoreCanonicalSelection } from "./canonicalSelection.js";
11
12
  import { mapTocSelection } from "./canonicalTocSelection.js";
12
13
  import { Result, TaggedError, panic } from "better-result";
@@ -237,7 +238,7 @@ const changedStories = ({ document, previous }) => {
237
238
  }
238
239
  return stories;
239
240
  };
240
- const preservePropertySources = ({ target, source, stories = documentStories(source) }) => {
241
+ const preservePropertySources = ({ target, source, stories = documentStories(source), indentation = "derive" }) => {
241
242
  for (const story of stories) {
242
243
  const originals = findStoryBody(source, story)?.content ?? [];
243
244
  const derived = findStoryBody(target, story)?.content ?? [];
@@ -246,7 +247,13 @@ const preservePropertySources = ({ target, source, stories = documentStories(sou
246
247
  for (const [index, original] of originals.entries()) {
247
248
  if (original.type !== "paragraph") continue;
248
249
  const next = original.paraId === void 0 ? derived.at(index) : paragraphs.get(original.paraId);
249
- if (next?.type === "paragraph" && next !== original) copyParagraphPropertySource(next, original);
250
+ if (next?.type === "paragraph" && next !== original) {
251
+ copyParagraphPropertySource(next, original);
252
+ if (indentation === "restore") restoreParagraphIndentationProjection({
253
+ target: next,
254
+ source: original
255
+ });
256
+ }
250
257
  }
251
258
  }
252
259
  copyDocumentParagraphPropertySourceContract(target, source);
@@ -998,6 +1005,14 @@ var CanonicalSession = class {
998
1005
  const applied = stagedApplied === void 0 ? applyDocumentOps(this.currentDocument, ops) : Result.ok(stagedApplied);
999
1006
  if (applied.isErr()) return refuse(applied.error.message);
1000
1007
  if (applied.value.inverse.length === 0) return noChange("The intent makes no document change.");
1008
+ if (propertySourceDocument !== void 0) preservePropertySources({
1009
+ target: applied.value.document,
1010
+ source: propertySourceDocument,
1011
+ indentation: "restore"
1012
+ });
1013
+ const normalized = normalizeCanonicalListRendering(applied.value.document);
1014
+ applied.value.document = normalized.document;
1015
+ applied.value.inverse = normalized.inverse.concat(applied.value.inverse);
1001
1016
  const changed = changedStories({
1002
1017
  document: applied.value.document,
1003
1018
  previous: this.currentDocument
@@ -1138,13 +1153,18 @@ const createCanonicalSession = (document, styles) => {
1138
1153
  const unsupported = unsupportedSeedReason(document);
1139
1154
  if (unsupported !== null) return refuse(unsupported);
1140
1155
  const owned = cloneDocumentWithParagraphPropertySources(document);
1141
- const normalized = normalizeForOps(owned);
1156
+ const identified = normalizeForOps(owned);
1142
1157
  preservePropertySources({
1143
- target: normalized,
1158
+ target: identified,
1144
1159
  source: owned
1145
1160
  });
1146
- const validated = validateOpsDocument(normalized);
1161
+ const validated = validateOpsDocument(identified);
1147
1162
  if (validated.isErr()) return refuse(validated.error.message);
1163
+ const normalized = normalizeCanonicalListRendering(identified).document;
1164
+ preservePropertySources({
1165
+ target: normalized,
1166
+ source: owned
1167
+ });
1148
1168
  const projected = project({
1149
1169
  document: normalized,
1150
1170
  styles
@@ -3,6 +3,7 @@ import { NumberingMap } from "./numberingParser.js";
3
3
  import { ParseContext } from "./parseContext.js";
4
4
  import { XmlElement } from "./xmlParser.js";
5
5
  import { OwnedElsewhere } from "./containerChildren.js";
6
+ import { PreviousListState } from "./listMarkerComputation.js";
6
7
  import { PreviewLedger } from "./previewBudget.js";
7
8
  import { StyleMap } from "./styleParser.js";
8
9
  //#region src/docx/blockContentParser.d.ts
@@ -16,11 +17,6 @@ type ParseBlockContentOptions = {
16
17
  type BlockContentScope = ParseBlockContentOptions & {
17
18
  previews: PreviewLedger;
18
19
  };
19
- type PreviousListState = {
20
- abstractNumId: number | null;
21
- fromStyle: boolean;
22
- numId: number | null;
23
- };
24
20
  type ParseBlockContentState = {
25
21
  listCounters: Map<number, number[]>;
26
22
  abstractCounters: Map<number, number[]>;
@@ -1,9 +1,7 @@
1
1
  import { blockCustomXmlShell } from "./blockCustomXmlShell.js";
2
2
  import { parseBookmarkEnd, parseBookmarkStart } from "./bookmarkParser.js";
3
- import { bulletMarkerFontName, convertBulletToUnicode } from "./bulletMarkers.js";
4
3
  import { CAPTURE, dispatchChildrenWithContext, ownedElsewhere, withPreservedChildren } from "./containerChildren.js";
5
- import { isNumberingReference } from "./numberingReference.js";
6
- import { formatOoxmlCounter } from "./ooxmlCounterFormatter.js";
4
+ import { computeListMarker } from "./listMarkerComputation.js";
7
5
  import { parseParagraph } from "./paragraphParser.js";
8
6
  import { enrichParagraphTextBoxes } from "./paragraphTextBoxEnrichment.js";
9
7
  import { standalonePreviewLedger } from "./previewBudget.js";
@@ -11,77 +9,6 @@ import { captureSdtSiblingMarkers, parseSdtProperties } from "./sdtProperties.js
11
9
  import { parseTable } from "./tableParser.js";
12
10
  import { findWordprocessingChild, getLocalName, mergeXmlnsDeclarations, selectAlternateContentBranch } from "./xmlParser.js";
13
11
  //#region src/docx/blockContentParser.ts
14
- const computeListMarker = (paragraph, { numbering, listCounters, abstractCounters, restartedNumIds, previousList }) => {
15
- const listRendering = paragraph.listRendering;
16
- if (!listRendering || !numbering) {
17
- previousList.abstractNumId = null;
18
- previousList.fromStyle = false;
19
- previousList.numId = null;
20
- return;
21
- }
22
- const { numId, level } = listRendering;
23
- if (!isNumberingReference(numId)) {
24
- previousList.abstractNumId = null;
25
- previousList.fromStyle = false;
26
- previousList.numId = null;
27
- return;
28
- }
29
- const firstEncounter = !listCounters.has(numId);
30
- if (firstEncounter) listCounters.set(numId, Array.from({ length: 9 }).fill(NaN));
31
- let counters = listCounters.get(numId);
32
- if (!counters) return;
33
- const abstractNumId = numbering.getAbstractNumId(numId);
34
- const styleNumbering = paragraph.formatting?.numPrFromStyle;
35
- const resumesRestartedInstance = firstEncounter && listRendering.startOverride === void 0 && abstractNumId !== null && previousList.abstractNumId === abstractNumId && previousList.numId !== null && previousList.numId !== numId && restartedNumIds.has(previousList.numId);
36
- const resumesStyleInstance = firstEncounter && !styleNumbering && listRendering.startOverride === void 0 && abstractNumId !== null && previousList.abstractNumId === abstractNumId && previousList.fromStyle === true;
37
- if (abstractNumId !== null && (styleNumbering || resumesRestartedInstance || resumesStyleInstance)) {
38
- const latestAbstractCounters = abstractCounters.get(abstractNumId);
39
- if (latestAbstractCounters) {
40
- counters = latestAbstractCounters;
41
- listCounters.set(numId, counters);
42
- }
43
- }
44
- if (listRendering.startOverride !== void 0 || resumesRestartedInstance || previousList.numId === numId && restartedNumIds.has(numId)) restartedNumIds.add(numId);
45
- if (abstractNumId !== null && level > 0) {
46
- const latestAbstractCounters = abstractCounters.get(abstractNumId);
47
- if (counters.slice(0, level).every(Number.isNaN)) for (let i = 0; i < level; i += 1) {
48
- const latestCounter = latestAbstractCounters?.[i];
49
- counters[i] = latestCounter !== void 0 && !Number.isNaN(latestCounter) ? latestCounter : numbering.getLevel(numId, i)?.start ?? 1;
50
- }
51
- }
52
- if (Number.isNaN(counters[level])) counters[level] = (numbering.getLevel(numId, level)?.start ?? 1) - 1;
53
- counters[level] = (counters[level] ?? 0) + 1;
54
- for (let i = level + 1; i < counters.length; i += 1) counters[i] = NaN;
55
- const childAdvances = listRendering.implicitChildLevelAdvances ?? 0;
56
- if (childAdvances > 0 && level + 1 < counters.length) {
57
- const childCounter = counters[level + 1];
58
- counters[level + 1] = (childCounter === void 0 || Number.isNaN(childCounter) ? 0 : childCounter) + childAdvances;
59
- }
60
- if (abstractNumId !== null) abstractCounters.set(abstractNumId, counters);
61
- previousList.abstractNumId = abstractNumId;
62
- previousList.fromStyle = Boolean(styleNumbering);
63
- previousList.numId = numId;
64
- const pattern = listRendering.marker;
65
- if (listRendering.isBullet) {
66
- listRendering.marker = convertBulletToUnicode(pattern || "", bulletMarkerFontName(listRendering.markerFormatting));
67
- previousList.abstractNumId = null;
68
- previousList.fromStyle = false;
69
- previousList.numId = null;
70
- return;
71
- }
72
- let computedMarker = pattern;
73
- const useLegalNumbering = numbering.getLevel(numId, level)?.isLgl === true || listRendering.isLegal === true;
74
- for (let lvl = 0; lvl <= level; lvl += 1) {
75
- const placeholder = `%${lvl + 1}`;
76
- if (computedMarker.includes(placeholder)) {
77
- const value = counters[lvl] ?? 0;
78
- const levelInfo = numbering.getLevel(numId, lvl);
79
- const formatted = formatOoxmlCounter(value, useLegalNumbering ? "decimal" : levelInfo?.numFmt || "decimal");
80
- computedMarker = computedMarker.replaceAll(placeholder, formatted);
81
- }
82
- }
83
- listRendering.marker = computedMarker;
84
- };
85
12
  const withContainerXmlns = (state, element) => ({
86
13
  ...state,
87
14
  options: {
@@ -72,6 +72,8 @@ type ListNumberFoldPlan = {
72
72
  * further in is on the line.
73
73
  */
74
74
  declare const planListNumberFold: (items: readonly ListNumberFoldItem[], markerShowsFields: boolean) => ListNumberFoldPlan;
75
+ /** Derive the marker fold from current captures, never from cached rendering. */
76
+ declare const planParagraphListNumberFold: (content: readonly document_d_exports.ParagraphContent[], showsFields: boolean) => ListNumberFoldPlan;
75
77
  /** The item a capture stands for, which shows on the line; any other item is itself. */
76
78
  declare const unfoldedListNumberContent: (content: document_d_exports.ParagraphContent) => document_d_exports.ParagraphContent;
77
79
  /**
@@ -82,4 +84,4 @@ declare const unfoldedListNumberContent: (content: document_d_exports.ParagraphC
82
84
  */
83
85
  declare const normalizeFoldedListNumbers: (paragraph: document_d_exports.Paragraph) => void;
84
86
  //#endregion
85
- export { ListNumberFieldFold, ListNumberFoldItem, ListNumberFoldPlan, foldListNumberFields, foldedListNumberOf, isFoldedListNumber, isFoldedListNumberCapture, isListNumberField, isListNumberGapMarker, isTabOnlyRun, normalizeFoldedListNumbers, planListNumberFold, unfoldedListNumberContent };
87
+ export { ListNumberFieldFold, ListNumberFoldItem, ListNumberFoldPlan, foldListNumberFields, foldedListNumberOf, isFoldedListNumber, isFoldedListNumberCapture, isListNumberField, isListNumberGapMarker, isTabOnlyRun, normalizeFoldedListNumbers, planListNumberFold, planParagraphListNumberFold, unfoldedListNumberContent };
@@ -202,6 +202,8 @@ const foldItemOf = (content) => {
202
202
  } : { kind: "tab" };
203
203
  return isZeroWidth(content) ? { kind: "hidden" } : { kind: "shown" };
204
204
  };
205
+ /** Derive the marker fold from current captures, never from cached rendering. */
206
+ const planParagraphListNumberFold = (content, showsFields) => planListNumberFold(content.map(foldItemOf), showsFields);
205
207
  /** The item a capture stands for, which shows on the line; any other item is itself. */
206
208
  const unfoldedListNumberContent = (content) => {
207
209
  const folded = foldedListNumberOf(content);
@@ -253,7 +255,7 @@ const normalizeFoldedListNumbers = (paragraph) => {
253
255
  });
254
256
  const content = paragraph.content;
255
257
  if (!state.showsFields && !content.some(isFoldedListNumberCapture)) return;
256
- const plan = planListNumberFold(content.map(foldItemOf), state.showsFields);
258
+ const plan = planParagraphListNumberFold(content, state.showsFields);
257
259
  const next = [];
258
260
  for (const index of plan.order) {
259
261
  const item = content[index];
@@ -264,4 +266,4 @@ const normalizeFoldedListNumbers = (paragraph) => {
264
266
  if (rendering && state.showsFields) rendering.marker = listMarkerWithFold(state, plan.suffix);
265
267
  };
266
268
  //#endregion
267
- export { foldListNumberFields, foldedListNumberOf, isFoldedListNumber, isFoldedListNumberCapture, isListNumberField, isListNumberGapMarker, isTabOnlyRun, normalizeFoldedListNumbers, planListNumberFold, unfoldedListNumberContent };
269
+ export { foldListNumberFields, foldedListNumberOf, isFoldedListNumber, isFoldedListNumberCapture, isListNumberField, isListNumberGapMarker, isTabOnlyRun, normalizeFoldedListNumbers, planListNumberFold, planParagraphListNumberFold, unfoldedListNumberContent };
@@ -0,0 +1,18 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { NumberingMap } from "./numberingParser.js";
3
+ //#region src/docx/listMarkerComputation.d.ts
4
+ type PreviousListState = {
5
+ abstractNumId: number | null;
6
+ fromStyle: boolean;
7
+ numId: number | null;
8
+ };
9
+ type ComputeListMarkerOptions = {
10
+ numbering: NumberingMap | null;
11
+ listCounters: Map<number, number[]>;
12
+ abstractCounters: Map<number, number[]>;
13
+ restartedNumIds: Set<number>;
14
+ previousList: PreviousListState;
15
+ };
16
+ declare const computeListMarker: (paragraph: document_d_exports.Paragraph, { numbering, listCounters, abstractCounters, restartedNumIds, previousList }: ComputeListMarkerOptions) => void;
17
+ //#endregion
18
+ export { ComputeListMarkerOptions, PreviousListState, computeListMarker };
@@ -0,0 +1,77 @@
1
+ import { bulletMarkerFontName, convertBulletToUnicode } from "./bulletMarkers.js";
2
+ import { isNumberingReference } from "./numberingReference.js";
3
+ import { formatOoxmlCounter } from "./ooxmlCounterFormatter.js";
4
+ //#region src/docx/listMarkerComputation.ts
5
+ const computeListMarker = (paragraph, { numbering, listCounters, abstractCounters, restartedNumIds, previousList }) => {
6
+ const listRendering = paragraph.listRendering;
7
+ if (!listRendering || !numbering) {
8
+ previousList.abstractNumId = null;
9
+ previousList.fromStyle = false;
10
+ previousList.numId = null;
11
+ return;
12
+ }
13
+ const { numId, level } = listRendering;
14
+ if (!isNumberingReference(numId)) {
15
+ previousList.abstractNumId = null;
16
+ previousList.fromStyle = false;
17
+ previousList.numId = null;
18
+ return;
19
+ }
20
+ const firstEncounter = !listCounters.has(numId);
21
+ if (firstEncounter) listCounters.set(numId, Array.from({ length: 9 }).fill(NaN));
22
+ let counters = listCounters.get(numId);
23
+ if (!counters) return;
24
+ const abstractNumId = numbering.getAbstractNumId(numId);
25
+ const styleNumbering = paragraph.formatting?.numPrFromStyle;
26
+ const resumesRestartedInstance = firstEncounter && listRendering.startOverride === void 0 && abstractNumId !== null && previousList.abstractNumId === abstractNumId && previousList.numId !== null && previousList.numId !== numId && restartedNumIds.has(previousList.numId);
27
+ const resumesStyleInstance = firstEncounter && !styleNumbering && listRendering.startOverride === void 0 && abstractNumId !== null && previousList.abstractNumId === abstractNumId && previousList.fromStyle === true;
28
+ if (abstractNumId !== null && (styleNumbering || resumesRestartedInstance || resumesStyleInstance)) {
29
+ const latestAbstractCounters = abstractCounters.get(abstractNumId);
30
+ if (latestAbstractCounters) {
31
+ counters = latestAbstractCounters;
32
+ listCounters.set(numId, counters);
33
+ }
34
+ }
35
+ if (listRendering.startOverride !== void 0 || resumesRestartedInstance || previousList.numId === numId && restartedNumIds.has(numId)) restartedNumIds.add(numId);
36
+ if (abstractNumId !== null && level > 0) {
37
+ const latestAbstractCounters = abstractCounters.get(abstractNumId);
38
+ if (counters.slice(0, level).every(Number.isNaN)) for (let i = 0; i < level; i += 1) {
39
+ const latestCounter = latestAbstractCounters?.[i];
40
+ counters[i] = latestCounter !== void 0 && !Number.isNaN(latestCounter) ? latestCounter : numbering.getLevel(numId, i)?.start ?? 1;
41
+ }
42
+ }
43
+ if (Number.isNaN(counters[level])) counters[level] = (numbering.getLevel(numId, level)?.start ?? 1) - 1;
44
+ counters[level] = (counters[level] ?? 0) + 1;
45
+ for (let i = level + 1; i < counters.length; i += 1) counters[i] = NaN;
46
+ const childAdvances = listRendering.implicitChildLevelAdvances ?? 0;
47
+ if (childAdvances > 0 && level + 1 < counters.length) {
48
+ const childCounter = counters[level + 1];
49
+ counters[level + 1] = (childCounter === void 0 || Number.isNaN(childCounter) ? 0 : childCounter) + childAdvances;
50
+ }
51
+ if (abstractNumId !== null) abstractCounters.set(abstractNumId, counters);
52
+ previousList.abstractNumId = abstractNumId;
53
+ previousList.fromStyle = Boolean(styleNumbering);
54
+ previousList.numId = numId;
55
+ const pattern = listRendering.marker;
56
+ if (listRendering.isBullet) {
57
+ listRendering.marker = convertBulletToUnicode(pattern || "", bulletMarkerFontName(listRendering.markerFormatting));
58
+ previousList.abstractNumId = null;
59
+ previousList.fromStyle = false;
60
+ previousList.numId = null;
61
+ return;
62
+ }
63
+ let computedMarker = pattern;
64
+ const useLegalNumbering = numbering.getLevel(numId, level)?.isLgl === true || listRendering.isLegal === true;
65
+ for (let lvl = 0; lvl <= level; lvl += 1) {
66
+ const placeholder = `%${lvl + 1}`;
67
+ if (computedMarker.includes(placeholder)) {
68
+ const value = counters[lvl] ?? 0;
69
+ const levelInfo = numbering.getLevel(numId, lvl);
70
+ const formatted = formatOoxmlCounter(value, useLegalNumbering ? "decimal" : levelInfo?.numFmt || "decimal");
71
+ computedMarker = computedMarker.replaceAll(placeholder, formatted);
72
+ }
73
+ }
74
+ listRendering.marker = computedMarker;
75
+ };
76
+ //#endregion
77
+ export { computeListMarker };
@@ -12,12 +12,20 @@ type ParagraphPropertySource = Readonly<{
12
12
  [paragraphPropertySourceIndentationBaseline]: Readonly<Pick<document_d_exports.ParagraphFormatting, (typeof INDENTATION_FIELDS)[number]>>;
13
13
  }>;
14
14
  type IndentationFormatting = Pick<document_d_exports.ParagraphFormatting, (typeof INDENTATION_FIELDS)[number]>;
15
+ type SameParagraphIndentationProjectionOptions = {
16
+ left: document_d_exports.Paragraph;
17
+ right: document_d_exports.Paragraph;
18
+ };
19
+ /** Fixed points retain both authored indentation and its effective baseline. */
20
+ declare const sameParagraphIndentationProjection: ({ left, right }: SameParagraphIndentationProjectionOptions) => boolean;
15
21
  type CopyParagraphIndentationProjectionOptions = {
16
22
  target: document_d_exports.Paragraph;
17
23
  source: document_d_exports.Paragraph;
18
24
  };
19
25
  /** Retain effective-value provenance without copying a durable source identity. */
20
26
  declare const copyParagraphIndentationProjection: ({ target, source }: CopyParagraphIndentationProjectionOptions) => void;
27
+ /** History restores its captured authored/effective baseline before normalization. */
28
+ declare const restoreParagraphIndentationProjection: ({ target, source }: CopyParagraphIndentationProjectionOptions) => void;
21
29
  type AssignParagraphIndentationProjectionOptions = {
22
30
  paragraph: document_d_exports.Paragraph;
23
31
  authored: document_d_exports.ParagraphFormatting | undefined;
@@ -171,4 +179,4 @@ declare const linkParagraphPropertySourceCandidate: (target: document_d_exports.
171
179
  declare const getParagraphPropertySourceCandidate: (paragraph: document_d_exports.Paragraph) => document_d_exports.Paragraph | undefined;
172
180
  declare const getParagraphPropertySourceTransferId: (paragraph: document_d_exports.Paragraph) => string | undefined;
173
181
  //#endregion
174
- export { DecodedTableCellParagraphSourcePayload, PARAGRAPH_PROPERTY_SOURCE_VALIDATION_CODES, PROSE_PARAGRAPH_SOURCE_CONTRACT_ATTR, PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR, ParagraphPropertySourceTransfer, ParagraphPropertySourceValidationCode, ParagraphPropertySourceValidationError, TABLE_CELL_PARAGRAPH_SOURCE_BINDING_ATTR, TABLE_CELL_PARAGRAPH_SOURCE_PAYLOAD_ERROR_CLASSIFICATIONS, TableCellParagraphPropertySourceBinding, TableCellParagraphSourcePayloadErrorClassification, assignDocumentParagraphPropertySourceContract, assignParagraphIndentationProjection, assignParagraphPropertySource, cloneDocumentWithParagraphPropertySources, cloneParagraphWithPropertySource, cloneParagraphWithoutPropertySource, cloneTableCellsWithParagraphPropertyCaptures, copyDocumentParagraphPropertySourceContract, copyDocumentParagraphPropertySources, copyParagraphIndentationProjection, copyParagraphPropertyCapture, copyParagraphPropertySource, createProseParagraphWithPropertySource, decodeTableCellParagraphSourcePayload, getDocumentParagraphPropertySourceContract, getExplicitParagraphPropertySourceTransfers, getParagraphPropertySource, getParagraphPropertySourceCandidate, getParagraphPropertySourceToken, getParagraphPropertySourceTransferId, getProseDocumentParagraphPropertySourceContract, getProseParagraphPropertySourceToken, inspectParagraphSourceIdentity, isParagraphPropertySourceToken, joinProseParagraphsWithRightPropertySource, linkParagraphPropertySourceCandidate, linkProseParagraphPropertySource, markParagraphPropertySourceTransfers, paragraphFormattingWithAuthoredIndentation, paragraphPropertySourceBelongsToDocument, paragraphPropertySourceMatchesAuthoredIndentation, paragraphPropertySourceMatchesEmission, paragraphPropertySourceTokenMatchesContract, proseParagraphAttrsWithoutPropertySource, recreateProseNodeWithDetachedParagraphPropertySource, recreateProseNodeWithParagraphPropertySource, restoreTableCellsWithParagraphPropertySources, setProseParagraphMarkupWithPropertySource, transferProseParagraphPropertySource, transportTableCellsWithParagraphPropertySources, visitDocumentStoryParagraphs, visitTableCellParagraphPropertySourceBindings };
182
+ export { DecodedTableCellParagraphSourcePayload, PARAGRAPH_PROPERTY_SOURCE_VALIDATION_CODES, PROSE_PARAGRAPH_SOURCE_CONTRACT_ATTR, PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR, ParagraphPropertySourceTransfer, ParagraphPropertySourceValidationCode, ParagraphPropertySourceValidationError, TABLE_CELL_PARAGRAPH_SOURCE_BINDING_ATTR, TABLE_CELL_PARAGRAPH_SOURCE_PAYLOAD_ERROR_CLASSIFICATIONS, TableCellParagraphPropertySourceBinding, TableCellParagraphSourcePayloadErrorClassification, assignDocumentParagraphPropertySourceContract, assignParagraphIndentationProjection, assignParagraphPropertySource, cloneDocumentWithParagraphPropertySources, cloneParagraphWithPropertySource, cloneParagraphWithoutPropertySource, cloneTableCellsWithParagraphPropertyCaptures, copyDocumentParagraphPropertySourceContract, copyDocumentParagraphPropertySources, copyParagraphIndentationProjection, copyParagraphPropertyCapture, copyParagraphPropertySource, createProseParagraphWithPropertySource, decodeTableCellParagraphSourcePayload, getDocumentParagraphPropertySourceContract, getExplicitParagraphPropertySourceTransfers, getParagraphPropertySource, getParagraphPropertySourceCandidate, getParagraphPropertySourceToken, getParagraphPropertySourceTransferId, getProseDocumentParagraphPropertySourceContract, getProseParagraphPropertySourceToken, inspectParagraphSourceIdentity, isParagraphPropertySourceToken, joinProseParagraphsWithRightPropertySource, linkParagraphPropertySourceCandidate, linkProseParagraphPropertySource, markParagraphPropertySourceTransfers, paragraphFormattingWithAuthoredIndentation, paragraphPropertySourceBelongsToDocument, paragraphPropertySourceMatchesAuthoredIndentation, paragraphPropertySourceMatchesEmission, paragraphPropertySourceTokenMatchesContract, proseParagraphAttrsWithoutPropertySource, recreateProseNodeWithDetachedParagraphPropertySource, recreateProseNodeWithParagraphPropertySource, restoreParagraphIndentationProjection, restoreTableCellsWithParagraphPropertySources, sameParagraphIndentationProjection, setProseParagraphMarkupWithPropertySource, transferProseParagraphPropertySource, transportTableCellsWithParagraphPropertySources, visitDocumentStoryParagraphs, visitTableCellParagraphPropertySourceBindings };
@@ -18,16 +18,34 @@ const INDENTATION_FIELDS = [
18
18
  const paragraphPropertyCapture = Symbol("paragraphPropertyCapture");
19
19
  const paragraphIndentationProjection = Symbol("paragraphIndentationProjection");
20
20
  const indentationProjections = /* @__PURE__ */ new WeakMap();
21
+ const assignedIndentationProjections = /* @__PURE__ */ new WeakSet();
21
22
  const indentationProjectionFor = (paragraph) => {
22
23
  const handle = Object.getOwnPropertyDescriptor(paragraph, paragraphIndentationProjection)?.value;
23
24
  return typeof handle === "object" && handle !== null ? indentationProjections.get(handle) : void 0;
24
25
  };
26
+ /** Fixed points retain both authored indentation and its effective baseline. */
27
+ const sameParagraphIndentationProjection = ({ left, right }) => {
28
+ const a = indentationProjectionFor(left);
29
+ const b = indentationProjectionFor(right);
30
+ if (a === void 0 || b === void 0) return a === b;
31
+ return INDENTATION_FIELDS.every((key) => a.effective[key] === b.effective[key] && a.authored[key] === b.authored[key]);
32
+ };
25
33
  /** Retain effective-value provenance without copying a durable source identity. */
26
34
  const copyParagraphIndentationProjection = ({ target, source }) => {
27
- if (Object.hasOwn(target, paragraphIndentationProjection)) return;
35
+ if (assignedIndentationProjections.has(target) || Object.hasOwn(target, paragraphIndentationProjection)) return;
28
36
  const descriptor = Object.getOwnPropertyDescriptor(source, paragraphIndentationProjection);
29
37
  if (descriptor && indentationProjectionFor(source)) Object.defineProperty(target, paragraphIndentationProjection, descriptor);
30
38
  };
39
+ /** History restores its captured authored/effective baseline before normalization. */
40
+ const restoreParagraphIndentationProjection = ({ target, source }) => {
41
+ assignedIndentationProjections.delete(target);
42
+ Reflect.deleteProperty(target, paragraphIndentationProjection);
43
+ copyParagraphIndentationProjection({
44
+ target,
45
+ source
46
+ });
47
+ assignedIndentationProjections.add(target);
48
+ };
31
49
  const indentationFields = (formatting) => ({
32
50
  indentLeft: formatting?.indentLeft,
33
51
  indentRight: formatting?.indentRight,
@@ -36,6 +54,7 @@ const indentationFields = (formatting) => ({
36
54
  });
37
55
  /** Keep the model's effective indentation without making inherited values authored. */
38
56
  const assignParagraphIndentationProjection = ({ paragraph, authored, inherited }) => {
57
+ assignedIndentationProjections.add(paragraph);
39
58
  const effective = {
40
59
  ...inherited,
41
60
  ...indentationFields(authored)
@@ -50,6 +69,11 @@ const assignParagraphIndentationProjection = ({ paragraph, authored, inherited }
50
69
  ...authored,
51
70
  ...effective
52
71
  };
72
+ if (Object.keys(paragraph.formatting).length === 0) delete paragraph.formatting;
73
+ if (Object.values(effective).every((value) => value === void 0) || getParagraphPropertySource(paragraph) === void 0 && INDENTATION_FIELDS.every((key) => effective[key] === authored?.[key])) {
74
+ Reflect.deleteProperty(paragraph, paragraphIndentationProjection);
75
+ return;
76
+ }
53
77
  const handle = Object.freeze({});
54
78
  indentationProjections.set(handle, Object.freeze({
55
79
  effective: Object.freeze(indentationFields(paragraph.formatting)),
@@ -872,4 +896,4 @@ const linkParagraphPropertySourceCandidate = (target, source) => {
872
896
  const getParagraphPropertySourceCandidate = (paragraph) => paragraphPropertySourceCandidates.get(paragraph);
873
897
  const getParagraphPropertySourceTransferId = (paragraph) => paragraphPropertySourceTransferIds.get(paragraph);
874
898
  //#endregion
875
- export { PARAGRAPH_PROPERTY_SOURCE_VALIDATION_CODES, PROSE_PARAGRAPH_SOURCE_CONTRACT_ATTR, PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR, ParagraphPropertySourceValidationError, TABLE_CELL_PARAGRAPH_SOURCE_BINDING_ATTR, TABLE_CELL_PARAGRAPH_SOURCE_PAYLOAD_ERROR_CLASSIFICATIONS, assignDocumentParagraphPropertySourceContract, assignParagraphIndentationProjection, assignParagraphPropertySource, cloneDocumentWithParagraphPropertySources, cloneParagraphWithPropertySource, cloneParagraphWithoutPropertySource, cloneTableCellsWithParagraphPropertyCaptures, copyDocumentParagraphPropertySourceContract, copyDocumentParagraphPropertySources, copyParagraphIndentationProjection, copyParagraphPropertyCapture, copyParagraphPropertySource, createProseParagraphWithPropertySource, decodeTableCellParagraphSourcePayload, getDocumentParagraphPropertySourceContract, getExplicitParagraphPropertySourceTransfers, getParagraphPropertySource, getParagraphPropertySourceCandidate, getParagraphPropertySourceToken, getParagraphPropertySourceTransferId, getProseDocumentParagraphPropertySourceContract, getProseParagraphPropertySourceToken, inspectParagraphSourceIdentity, isParagraphPropertySourceToken, joinProseParagraphsWithRightPropertySource, linkParagraphPropertySourceCandidate, linkProseParagraphPropertySource, markParagraphPropertySourceTransfers, paragraphFormattingWithAuthoredIndentation, paragraphPropertySourceBelongsToDocument, paragraphPropertySourceMatchesAuthoredIndentation, paragraphPropertySourceMatchesEmission, paragraphPropertySourceTokenMatchesContract, proseParagraphAttrsWithoutPropertySource, recreateProseNodeWithDetachedParagraphPropertySource, recreateProseNodeWithParagraphPropertySource, restoreTableCellsWithParagraphPropertySources, setProseParagraphMarkupWithPropertySource, transferProseParagraphPropertySource, transportTableCellsWithParagraphPropertySources, visitDocumentStoryParagraphs, visitTableCellParagraphPropertySourceBindings };
899
+ export { PARAGRAPH_PROPERTY_SOURCE_VALIDATION_CODES, PROSE_PARAGRAPH_SOURCE_CONTRACT_ATTR, PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR, ParagraphPropertySourceValidationError, TABLE_CELL_PARAGRAPH_SOURCE_BINDING_ATTR, TABLE_CELL_PARAGRAPH_SOURCE_PAYLOAD_ERROR_CLASSIFICATIONS, assignDocumentParagraphPropertySourceContract, assignParagraphIndentationProjection, assignParagraphPropertySource, cloneDocumentWithParagraphPropertySources, cloneParagraphWithPropertySource, cloneParagraphWithoutPropertySource, cloneTableCellsWithParagraphPropertyCaptures, copyDocumentParagraphPropertySourceContract, copyDocumentParagraphPropertySources, copyParagraphIndentationProjection, copyParagraphPropertyCapture, copyParagraphPropertySource, createProseParagraphWithPropertySource, decodeTableCellParagraphSourcePayload, getDocumentParagraphPropertySourceContract, getExplicitParagraphPropertySourceTransfers, getParagraphPropertySource, getParagraphPropertySourceCandidate, getParagraphPropertySourceToken, getParagraphPropertySourceTransferId, getProseDocumentParagraphPropertySourceContract, getProseParagraphPropertySourceToken, inspectParagraphSourceIdentity, isParagraphPropertySourceToken, joinProseParagraphsWithRightPropertySource, linkParagraphPropertySourceCandidate, linkProseParagraphPropertySource, markParagraphPropertySourceTransfers, paragraphFormattingWithAuthoredIndentation, paragraphPropertySourceBelongsToDocument, paragraphPropertySourceMatchesAuthoredIndentation, paragraphPropertySourceMatchesEmission, paragraphPropertySourceTokenMatchesContract, proseParagraphAttrsWithoutPropertySource, recreateProseNodeWithDetachedParagraphPropertySource, recreateProseNodeWithParagraphPropertySource, restoreParagraphIndentationProjection, restoreTableCellsWithParagraphPropertySources, sameParagraphIndentationProjection, setProseParagraphMarkupWithPropertySource, transferProseParagraphPropertySource, transportTableCellsWithParagraphPropertySources, visitDocumentStoryParagraphs, visitTableCellParagraphPropertySourceBindings };
Binary file
@@ -6,7 +6,7 @@ import { TRANSITIONAL_NAME_BY_STRICT_NAME } from "../docx/strictNames.gen.js";
6
6
  import { createCapturedXmlSanitizer } from "../docx/verbatimCapture.js";
7
7
  import { NAMESPACES, OOXML_NAMESPACE_SCOPE } from "../docx/xmlParser.js";
8
8
  import { escapeXmlAttribute, serializeOnOffElement } from "@stll/docx-core";
9
- import { outlineLevelStatedValue, paragraphNumberingSlots, sameEffectiveParagraphNumbering } from "@stll/docx-core/model";
9
+ import { mergeParagraphNumbering, outlineLevelStatedValue, paragraphNumberingLevel, paragraphNumberingSlots, sameEffectiveParagraphNumbering } from "@stll/docx-core/model";
10
10
  import { serializeSequenceChildren } from "@stll/docx-core/schema";
11
11
  //#region src/internal/paragraphFormattingSerialization.ts
12
12
  /**
@@ -167,7 +167,10 @@ const modelParagraphFormattingEmission = (input) => {
167
167
  numberingChangeXml,
168
168
  numberingInsertionXml
169
169
  }) : serializeNumbering({
170
- numPr,
170
+ numPr: numPr?.kind === "reference" && numPrFromStyle?.kind === "reference" && numPr.numId === numPrFromStyle.numId ? {
171
+ kind: "levelOnly",
172
+ ilvl: paragraphNumberingLevel(mergeParagraphNumbering(numPrFromStyle, numPr)) ?? 0
173
+ } : numPr,
171
174
  numberingChangeXml,
172
175
  numberingInsertionXml
173
176
  })],
@@ -19,56 +19,60 @@ function renderBlocks(ctx, pkg, blocks) {
19
19
  const projection = ctx.opts.trackedChanges === "clean" ? projectAcceptedBlocks(blocks, pkg) : null;
20
20
  if (projection?.completeness === "partial") pushWarning(ctx, "Some tracked structural changes could not be resolved.");
21
21
  const ordered = projection?.blocks ?? blocks;
22
- for (const block of ordered) switch (block.type) {
23
- case "paragraph": {
24
- const { markdown: md, isListItem } = renderParagraphBlock(ctx, pkg, block);
25
- if (!md) {
26
- prevWasListItem = false;
27
- continue;
22
+ const renderBlock = (block) => {
23
+ switch (block.type) {
24
+ case "paragraph": return renderParagraphBlock(ctx, pkg, block);
25
+ case "table": {
26
+ ctx.listIndentWidths = [];
27
+ const md = renderTable(ctx, pkg, block);
28
+ ctx.listIndentWidths = [];
29
+ return {
30
+ type: "paragraph",
31
+ markdown: md
32
+ };
28
33
  }
29
- if (isListItem && prevWasListItem) out.push(md);
30
- else if (out.length) out.push("", md);
31
- else out.push(md);
32
- prevWasListItem = isListItem;
33
- break;
34
- }
35
- case "table": {
36
- ctx.listIndentWidths = [];
37
- const md = renderTable(ctx, pkg, block);
38
- ctx.listIndentWidths = [];
39
- if (md) {
40
- if (out.length) out.push("");
41
- out.push(md);
34
+ case "blockSdt":
35
+ case "blockCustomXml": {
36
+ ctx.listIndentWidths = [];
37
+ const nested = renderBlocks(ctx, pkg, block.content);
38
+ ctx.listIndentWidths = [];
39
+ return {
40
+ type: "paragraph",
41
+ markdown: nested
42
+ };
42
43
  }
43
- prevWasListItem = false;
44
- break;
45
- }
46
- case "blockSdt":
47
- case "blockCustomXml": {
48
- ctx.listIndentWidths = [];
49
- const nested = renderBlocks(ctx, pkg, block.content);
50
- if (nested) {
51
- if (out.length) out.push("");
52
- out.push(nested);
44
+ case "preservedBlock": {
45
+ let markdown;
46
+ if (isAltChunkMarkup(block.xml)) markdown = block.readerText === void 0 ? ALT_CHUNK_READER_DIAGNOSTIC : `${ALT_CHUNK_READER_DIAGNOSTIC}\n\n${block.readerText}`;
47
+ else if (opaqueRevisionCarrierName(block.xml) !== void 0) markdown = OPAQUE_REVISION_CARRIER_READER_DIAGNOSTIC;
48
+ else if (isOpaqueNestedRowMarkup(block.xml)) markdown = "[Unsupported nested w:tr content]";
49
+ else return { type: "delimiter" };
50
+ ctx.listIndentWidths = [];
51
+ return {
52
+ type: "paragraph",
53
+ markdown
54
+ };
53
55
  }
54
- ctx.listIndentWidths = [];
55
- prevWasListItem = false;
56
- break;
56
+ case "bookmarkStart":
57
+ case "bookmarkEnd": return { type: "delimiter" };
58
+ default: panic(`Unsupported block content in markdown: ${JSON.stringify(block)}`);
57
59
  }
58
- case "preservedBlock": {
59
- const previousLength = out.length;
60
- if (isAltChunkMarkup(block.xml)) out.push(block.readerText === void 0 ? ALT_CHUNK_READER_DIAGNOSTIC : `${ALT_CHUNK_READER_DIAGNOSTIC}\n\n${block.readerText}`);
61
- else if (opaqueRevisionCarrierName(block.xml) !== void 0) out.push(OPAQUE_REVISION_CARRIER_READER_DIAGNOSTIC);
62
- else if (isOpaqueNestedRowMarkup(block.xml)) out.push("[Unsupported nested w:tr content]");
63
- if (out.length !== previousLength) {
64
- ctx.listIndentWidths = [];
60
+ };
61
+ for (const block of ordered) {
62
+ const rendered = renderBlock(block);
63
+ switch (rendered.type) {
64
+ case "delimiter": continue;
65
+ case "paragraph":
66
+ if (rendered.markdown && out.length) out.push("");
65
67
  prevWasListItem = false;
66
- }
67
- break;
68
+ break;
69
+ case "listItem":
70
+ if (out.length && (!prevWasListItem || rendered.separation === "loose")) out.push("");
71
+ prevWasListItem = true;
72
+ break;
73
+ default: panic(`Unsupported rendered block: ${JSON.stringify(rendered)}`);
68
74
  }
69
- case "bookmarkStart":
70
- case "bookmarkEnd": break;
71
- default: panic(`Unsupported block content in markdown: ${JSON.stringify(block)}`);
75
+ if (rendered.markdown) out.push(rendered.markdown);
72
76
  }
73
77
  ctx.listIndentWidths = outerListIndentWidths;
74
78
  return out.join("\n");
@@ -6,10 +6,14 @@ import { RenderContext } from "./types.js";
6
6
  * caller joins blocks.
7
7
  */
8
8
  declare function renderParagraph(ctx: RenderContext, pkg: document_d_exports.DocxPackage | undefined, para: document_d_exports.Paragraph): string;
9
- /** A rendered paragraph, and whether it is a Markdown list item. */
9
+ /** List items declare the boundary needed to preserve their Markdown structure. */
10
10
  type RenderedParagraph = {
11
+ type: "paragraph";
11
12
  markdown: string;
12
- isListItem: boolean;
13
+ } | {
14
+ type: "listItem";
15
+ markdown: string;
16
+ separation: "tight" | "loose";
13
17
  };
14
18
  /**
15
19
  * Render a paragraph. A list item is a paragraph that shows a marker, which is
@@ -47,28 +47,25 @@ function renderParagraphBlock(ctx, pkg, para) {
47
47
  if (headingLevel !== void 0) {
48
48
  ctx.listIndentWidths = [];
49
49
  if (!inline || !inline.trim() && label === void 0) return {
50
- markdown: "",
51
- isListItem: false
50
+ type: "paragraph",
51
+ markdown: ""
52
52
  };
53
53
  const hashes = "#".repeat(Math.min(MAX_MARKDOWN_HEADING_LEVEL, headingLevel + 1));
54
54
  const headingInline = inlineBreaksToHtml(inline);
55
55
  return {
56
56
  markdown: label ? `${hashes} ${label} ${headingInline}` : `${hashes} ${headingInline}`,
57
- isListItem: false
57
+ type: "paragraph"
58
58
  };
59
59
  }
60
- if (list && label !== void 0) return {
61
- markdown: renderListItem(ctx, list, label, inline),
62
- isListItem: true
63
- };
60
+ if (list && label !== void 0) return renderListItem(ctx, list, label, inline);
64
61
  ctx.listIndentWidths = [];
65
62
  if (isQuoteStyle(styleId, ctx.builtInStyles)) return {
66
63
  markdown: inline.split("\n").map((line) => `> ${line}`).join("\n"),
67
- isListItem: false
64
+ type: "paragraph"
68
65
  };
69
66
  return {
70
- markdown: escapeLeadingBlockMarker(inline),
71
- isListItem: false
67
+ type: "paragraph",
68
+ markdown: escapeLeadingBlockMarker(inline)
72
69
  };
73
70
  }
74
71
  /** The paragraph's 0-based heading level, as `builtInStyles` classifies it. */
@@ -86,7 +83,7 @@ const UNNUMBERED = Object.freeze({});
86
83
  * style/list metadata. Escape the leading marker so literal text round-trips.
87
84
  */
88
85
  function escapeLeadingBlockMarker(text) {
89
- return text.replace(/^(?<ws>[ \t]*)(?<marker>[#>])/gmu, "$<ws>\\$<marker>").replace(/^(?<ws>[ \t]*)(?<marker>[-+*])(?<after>[ \t])/gmu, "$<ws>\\$<marker>$<after>").replace(/^(?<ws>[ \t]*)(?<num>\d{1,9})(?<marker>[.)])(?<after>[ \t])/gmu, "$<ws>$<num>\\$<marker>$<after>");
86
+ return text.replace(/^(?<ws>[ \t]*)(?<marker>[#>=])/gmu, "$<ws>\\$<marker>").replace(/^(?<ws>[ \t]*)(?<marker>[-+*])(?=[ \t]|$)/gmu, "$<ws>\\$<marker>").replace(/^(?<ws>[ \t]*)-(?=[- \t]*$)/gmu, "$<ws>\\-").replace(/^(?<ws>[ \t]*)(?<num>\d{1,9})(?<marker>[.)])(?<after>[ \t])/gmu, "$<ws>$<num>\\$<marker>$<after>");
90
87
  }
91
88
  /**
92
89
  * Only emitted Markdown ancestors contribute columns. OOXML levels may skip
@@ -110,13 +107,22 @@ function listIndentWidth(ctx, level) {
110
107
  * a following, deeper item can indent correctly under it.
111
108
  */
112
109
  function renderListItem(ctx, list, label, inline) {
113
- const nativeMarker = list.isBullet || /^\d{1,9}[.)]$/u.test(label);
114
- if (!nativeMarker) ctx.listIndentWidths = [];
110
+ if (!(list.isBullet || /^\d{1,9}[.)]$/u.test(label))) {
111
+ ctx.listIndentWidths = [];
112
+ return {
113
+ type: "paragraph",
114
+ markdown: escapeLeadingBlockMarker(`${label} ${inline}`).trimEnd()
115
+ };
116
+ }
115
117
  ctx.listIndentWidths.length = list.level;
116
118
  const indent = " ".repeat(listIndentWidth(ctx, list.level));
117
119
  const marker = list.isBullet ? "- " : `${label} `;
118
- if (nativeMarker) ctx.listIndentWidths[list.level] = marker.length;
119
- return `${indent}${marker}${inline}`.trimEnd();
120
+ ctx.listIndentWidths[list.level] = marker.length;
121
+ return {
122
+ type: "listItem",
123
+ markdown: `${indent}${marker}${escapeLeadingBlockMarker(inline)}`.trimEnd(),
124
+ separation: inline.trim().length === 0 ? "loose" : "tight"
125
+ };
120
126
  }
121
127
  //#endregion
122
128
  export { renderParagraph, renderParagraphBlock };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.58.3",
3
+ "version": "0.58.5",
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",
@@ -146,7 +146,7 @@
146
146
  "perf": "bun scripts/profile-editor.ts"
147
147
  },
148
148
  "dependencies": {
149
- "@stll/docx-core": "^0.36.0",
149
+ "@stll/docx-core": "^0.36.2",
150
150
  "@stll/docx-utils": "^0.1.0",
151
151
  "@stll/template-conditions": "^0.6.2",
152
152
  "better-result": "3.0.1",