@stll/folio-core 0.47.2 → 0.47.4

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.
@@ -164,16 +164,49 @@ const FOLIO_RESOLVED_STORY_SERIALIZATION_MISMATCHES = Object.freeze({
164
164
  blockProjection: "block-projection"
165
165
  });
166
166
  var FolioResolvedStorySerializationError = class extends TaggedError("FolioResolvedStorySerializationError") {};
167
+ /** Blocks whose id folio minted, because the package names no id for them. */
168
+ const mintedBlockOrdinals = (snapshot) => {
169
+ const ordinals = /* @__PURE__ */ new Set();
170
+ for (const [ordinal, block] of snapshot.blocks.entries()) if (block.idStability === "positional") ordinals.add(ordinal);
171
+ return ordinals;
172
+ };
167
173
  /**
168
- * `idStability` records how an id entered the current snapshot. A synthesized
169
- * paraId is positional until save writes it into the package, then becomes an
170
- * authored stable id when reopened. Compare the persisted block projection
171
- * without changing that live snapshot identity contract.
174
+ * What a block looks like once the story has been through the package.
175
+ *
176
+ * `idStability` is the snapshot's own fact rather than the package's, so it
177
+ * goes. `minted` names the blocks whose id folio invented for a paragraph the
178
+ * package gives no `w14:paraId`, and each of those stands down to its position.
179
+ * A minted id is derived from the paragraph's text and ordinal, so resolving
180
+ * its text can make an id-less paragraph derive a different id when reopened.
181
+ * The two save paths also disagree about whether it reaches the file at all:
182
+ * the selective patch keeps the author's id-less convention (see
183
+ * `withoutMintedIds`) while a full repack writes the model's ids. Which path
184
+ * ran is not what this check is about, so neither the id nor the choice between
185
+ * them may decide it. An authored id is compared exactly as it is, and a block
186
+ * that moved still fails.
172
187
  */
173
- const resolvedStoryBlockProjection = (block) => {
188
+ const resolvedStoryBlockProjection = (block, ordinal, minted) => {
174
189
  const persisted = { ...block };
175
190
  delete persisted.idStability;
176
- return persisted;
191
+ return minted.has(ordinal) ? {
192
+ ...persisted,
193
+ id: `minted-${String(ordinal)}`
194
+ } : persisted;
195
+ };
196
+ /**
197
+ * The two halves of the expectation, from one projection.
198
+ *
199
+ * The reading text carries each block's id, so it has to be built from the
200
+ * same projected blocks the block comparison uses; formatting the raw snapshot
201
+ * instead would put a minted id back into the text and fail the check the
202
+ * block projection just passed.
203
+ */
204
+ const resolvedStoryProjection = (snapshot, minted) => {
205
+ const blocks = snapshot.blocks.map((block, ordinal) => resolvedStoryBlockProjection(block, ordinal, minted));
206
+ return {
207
+ text: blocks.filter(isFolioAIContentBlock).map(formatBlockForLLM).join("\n"),
208
+ blocks
209
+ };
177
210
  };
178
211
  const numberingLevelsOf = (numbering) => {
179
212
  if (!numbering) return [];
@@ -735,10 +768,11 @@ var FolioDocxReviewer = class FolioDocxReviewer {
735
768
  const resolvedState = resolveReviewedState(sourceState, view);
736
769
  this.setEditableStoryState(story, resolvedState);
737
770
  const snapshot = createStateSnapshot(resolvedState);
771
+ const minted = mintedBlockOrdinals(snapshot);
738
772
  this.resolvedStoryExpectations.set(editableStoryKey(story), {
739
773
  story,
740
- text: formatStorySnapshotForLLM(snapshot, false),
741
- blocks: snapshot.blocks.map(resolvedStoryBlockProjection)
774
+ minted,
775
+ ...resolvedStoryProjection(snapshot, minted)
742
776
  });
743
777
  return snapshot;
744
778
  }
@@ -1356,13 +1390,14 @@ var FolioDocxReviewer = class FolioDocxReviewer {
1356
1390
  async assertResolvedStoriesSerialized(buffer, expectations) {
1357
1391
  if (expectations.length === 0) return;
1358
1392
  const reopened = await FolioDocxReviewer.fromBuffer(buffer);
1359
- for (const { story, text, blocks } of expectations) {
1393
+ for (const { story, text, blocks, minted } of expectations) {
1360
1394
  const serialized = reopened.readReviewedStory({
1361
1395
  story,
1362
1396
  view: "current-markup"
1363
1397
  });
1364
- const serializedText = serialized ? formatStorySnapshotForLLM(serialized.snapshot, false) : null;
1365
- const serializedBlocks = serialized ? serialized.snapshot.blocks.map(resolvedStoryBlockProjection) : null;
1398
+ const persisted = serialized ? resolvedStoryProjection(serialized.snapshot, minted) : null;
1399
+ const serializedText = persisted?.text ?? null;
1400
+ const serializedBlocks = persisted?.blocks ?? null;
1366
1401
  const mismatches = [];
1367
1402
  if (!serialized) mismatches.push(FOLIO_RESOLVED_STORY_SERIALIZATION_MISMATCHES.storyMissing);
1368
1403
  else {
@@ -128,8 +128,10 @@ function parseParagraphProperties(pPr, theme) {
128
128
  if (stated !== void 0) formatting.numPr = stated;
129
129
  const numberingChange = findChild(child, "w", "numberingChange");
130
130
  if (numberingChange) formatting.numberingChangeXml = captureVerbatimXml(numberingChange);
131
+ const numberingInsertion = findChild(child, "w", "ins");
132
+ if (numberingInsertion) formatting.numberingInsertionXml = captureVerbatimXml(numberingInsertion);
131
133
  const statedLevel = parseNumericAttribute(findChild(child, "w", "ilvl"), "w", "val");
132
- return stated !== void 0 || numberingChange !== null || statedLevel === -1;
134
+ return stated !== void 0 || numberingChange !== null || numberingInsertion !== null || statedLevel === -1;
133
135
  }),
134
136
  suppressLineNumbers: toggle("suppressLineNumbers", "suppressLineNumbers"),
135
137
  pBdr: once("pBdr", (child) => {
@@ -29,10 +29,10 @@ const RESERVED_PARAGRAPH_PROPERTY_CHILDREN = /* @__PURE__ */ new Set(["pPrChange
29
29
  /**
30
30
  * Revision records a captured `w:pPr` may not carry into a replay.
31
31
  *
32
- * `w:numberingChange` is not among them: the model holds it
33
- * (`ParagraphFormatting.numberingChangeXml`) and the serializer writes it
34
- * back, so both paths keep it. Refusing the capture used to force a rebuild
35
- * that could not write it, which turned a replay gate into a lost revision.
32
+ * The revision records inside `w:numPr` are not among them: the model holds
33
+ * `w:numberingChange` and `w:ins`, and the serializer writes both back, so both
34
+ * paths keep them. Refusing the capture used to force a rebuild that could not
35
+ * write the records, which turned a replay gate into a lost revision.
36
36
  */
37
37
  const RESERVED_PARAGRAPH_CAPTURE_CHILDREN = /* @__PURE__ */ new Set([
38
38
  ...RESERVED_PARAGRAPH_PROPERTY_CHILDREN,
@@ -1,7 +1,7 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
2
  import { ParagraphNumberingOverride } from "@stll/docx-core/model";
3
3
  //#region src/internal/paragraphFormattingSerialization.d.ts
4
- type ClassifiedParagraphFormattingField = "alignment" | "bidi" | "kinsoku" | "overflowPunctuation" | "spaceBefore" | "spaceAfter" | "lineSpacing" | "lineSpacingRule" | "snapToGrid" | "beforeAutospacing" | "afterAutospacing" | "spacingExplicit" | "spacingPreservedAttributes" | "indentLeft" | "indentRight" | "indentFirstLine" | "hangingIndent" | "indentPreservedAttributes" | "borders" | "shading" | "tabs" | "keepNext" | "keepLines" | "widowControl" | "pageBreakBefore" | "contextualSpacing" | "numPr" | "numPrFromStyle" | "numberingChangeXml" | "outlineLevel" | "styleId" | "frame" | "suppressLineNumbers" | "suppressAutoHyphens" | "runProperties" | "runInWithNext" | "preserved";
4
+ type ClassifiedParagraphFormattingField = "alignment" | "bidi" | "kinsoku" | "overflowPunctuation" | "spaceBefore" | "spaceAfter" | "lineSpacing" | "lineSpacingRule" | "snapToGrid" | "beforeAutospacing" | "afterAutospacing" | "spacingExplicit" | "spacingPreservedAttributes" | "indentLeft" | "indentRight" | "indentFirstLine" | "hangingIndent" | "indentPreservedAttributes" | "borders" | "shading" | "tabs" | "keepNext" | "keepLines" | "widowControl" | "pageBreakBefore" | "contextualSpacing" | "numPr" | "numPrFromStyle" | "numberingChangeXml" | "numberingInsertionXml" | "outlineLevel" | "styleId" | "frame" | "suppressLineNumbers" | "suppressAutoHyphens" | "runProperties" | "runInWithNext" | "preserved";
5
5
  type ExhaustiveParagraphFormatting = document_d_exports.ExhaustiveFields<document_d_exports.ParagraphFormatting, ClassifiedParagraphFormattingField>;
6
6
  /**
7
7
  * Whether resolved numbering still belongs to the paragraph's style tier.
@@ -72,32 +72,30 @@ const serializeIndentation = (formatting) => {
72
72
  const written = serializePreservedAttributes(attrs, formatting.indentPreservedAttributes);
73
73
  return written.length === 0 ? "" : `<w:ind ${written.join(" ")}/>`;
74
74
  };
75
- /**
76
- * `w:numPr`, with the tracked record of the numbering it replaced.
77
- *
78
- * `w:numberingChange` is the revision a reviewer's numbering change left
79
- * behind. It is history: nothing in the model derives it, and a rebuilt
80
- * `w:numPr` that drops it discards the revision silently. It is written even
81
- * when the paragraph's own numbering reference is gone, because a change
82
- * record with nothing left to describe is still a record — the schema declares
83
- * it last in `CT_NumPr`, after `w:ilvl` and `w:numId`.
84
- */
85
- const serializeNumbering = (numPr, numberingChangeXml) => {
75
+ const serializeNumbering = ({ numPr, numberingChangeXml, numberingInsertionXml }) => {
86
76
  const slots = numPr === void 0 ? {} : paragraphNumberingSlots(numPr);
87
77
  const parts = [];
88
78
  if (slots.ilvl !== void 0) parts.push(`<w:ilvl w:val="${intAttr(slots.ilvl)}"/>`);
89
79
  if (slots.numId !== void 0) parts.push(`<w:numId w:val="${intAttr(slots.numId)}"/>`);
90
80
  const change = replayableNumberingChangeXml(numberingChangeXml);
91
81
  if (change !== null) parts.push(change);
82
+ const insertion = replayableNumberingInsertionXml(numberingInsertionXml);
83
+ if (insertion !== null) parts.push(insertion);
92
84
  return parts.length === 0 ? "" : `<w:numPr>${parts.join("")}</w:numPr>`;
93
85
  };
94
86
  const NUMBERING_CHANGE_ROOT_NAME = /* @__PURE__ */ new Set(["numberingChange"]);
87
+ const NUMBERING_INSERTION_ROOT_NAME = /* @__PURE__ */ new Set(["ins"]);
95
88
  const WORDPROCESSINGML_NAMESPACE = /* @__PURE__ */ new Set([NAMESPACES.w]);
96
89
  const replayableNumberingChangeXml = (numberingChangeXml) => sanitizeCapturedXmlElement(numberingChangeXml, {
97
90
  allowedLocalNames: NUMBERING_CHANGE_ROOT_NAME,
98
91
  allowedNamespaceUris: WORDPROCESSINGML_NAMESPACE,
99
92
  inheritedNamespaceScope: OOXML_NAMESPACE_SCOPE
100
93
  });
94
+ const replayableNumberingInsertionXml = (numberingInsertionXml) => sanitizeCapturedXmlElement(numberingInsertionXml, {
95
+ allowedLocalNames: NUMBERING_INSERTION_ROOT_NAME,
96
+ allowedNamespaceUris: WORDPROCESSINGML_NAMESPACE,
97
+ inheritedNamespaceScope: OOXML_NAMESPACE_SCOPE
98
+ });
101
99
  const serializeFrameProperties = (frame) => {
102
100
  if (!frame) return "";
103
101
  const { dropCap, lines, width, height, hSpace, vSpace, hAnchor, vAnchor, x, y, xAlign, yAlign, wrap, preservedAttributes } = frame;
@@ -152,7 +150,7 @@ const paragraphMarkPropertiesInner = (runProperties, runInWithNext) => {
152
150
  */
153
151
  const modelParagraphFormattingEmission = (input) => {
154
152
  if (!input) return {};
155
- const { alignment, bidi, kinsoku, overflowPunctuation, spaceBefore, spaceAfter, lineSpacing, lineSpacingRule, snapToGrid, beforeAutospacing, afterAutospacing, spacingExplicit, spacingPreservedAttributes, indentLeft, indentRight, indentFirstLine, hangingIndent, indentPreservedAttributes, borders, shading, tabs, keepNext, keepLines, widowControl, pageBreakBefore, contextualSpacing, numPr, numPrFromStyle, numberingChangeXml, outlineLevel, styleId, frame, suppressLineNumbers, suppressAutoHyphens, runProperties, runInWithNext, preserved } = input;
153
+ const { alignment, bidi, kinsoku, overflowPunctuation, spaceBefore, spaceAfter, lineSpacing, lineSpacingRule, snapToGrid, beforeAutospacing, afterAutospacing, spacingExplicit, spacingPreservedAttributes, indentLeft, indentRight, indentFirstLine, hangingIndent, indentPreservedAttributes, borders, shading, tabs, keepNext, keepLines, widowControl, pageBreakBefore, contextualSpacing, numPr, numPrFromStyle, numberingChangeXml, numberingInsertionXml, outlineLevel, styleId, frame, suppressLineNumbers, suppressAutoHyphens, runProperties, runInWithNext, preserved } = input;
156
154
  modelSpacingProvenance(spacingExplicit);
157
155
  const propertiesXml = serializeSequenceChildren({
158
156
  container: "paragraph-properties",
@@ -164,7 +162,15 @@ const modelParagraphFormattingEmission = (input) => {
164
162
  ["pageBreakBefore", serializeOnOffElement(pageBreakBefore, "pageBreakBefore")],
165
163
  ["framePr", serializeFrameProperties(frame)],
166
164
  ["widowControl", serializeOnOffElement(widowControl, "widowControl")],
167
- ["numPr", isStyleSourcedParagraphNumbering(numPr, numPrFromStyle) ? serializeNumbering(void 0, numberingChangeXml) : serializeNumbering(numPr, numberingChangeXml)],
165
+ ["numPr", isStyleSourcedParagraphNumbering(numPr, numPrFromStyle) ? serializeNumbering({
166
+ numPr: void 0,
167
+ numberingChangeXml,
168
+ numberingInsertionXml
169
+ }) : serializeNumbering({
170
+ numPr,
171
+ numberingChangeXml,
172
+ numberingInsertionXml
173
+ })],
168
174
  ["suppressLineNumbers", serializeOnOffElement(suppressLineNumbers, "suppressLineNumbers")],
169
175
  ["pBdr", serializeParagraphBorders(borders)],
170
176
  ["shd", serializeShading(shading)],
@@ -2124,6 +2124,7 @@ const validateParagraphFormatting = (value, path, issues) => {
2124
2124
  optionalOneOf(value, "lineSpacingRule", `${path}.lineSpacingRule`, issues, LINE_SPACING_RULE_VALUES);
2125
2125
  optionalString(value, "styleId", `${path}.styleId`, issues);
2126
2126
  optionalString(value, "numberingChangeXml", `${path}.numberingChangeXml`, issues);
2127
+ optionalString(value, "numberingInsertionXml", `${path}.numberingInsertionXml`, issues);
2127
2128
  optionalOneOf(value, "listNumFmt", `${path}.listNumFmt`, issues, COUNTER_FORMAT_VALUES);
2128
2129
  optionalString(value, "listMarker", `${path}.listMarker`, issues);
2129
2130
  optionalString(value, "listMarkerTemplate", `${path}.listMarkerTemplate`, issues);
@@ -129,6 +129,9 @@ declare const PARAGRAPH_FORMATTING_WRITE_BACK: {
129
129
  readonly numberingChangeXml: {
130
130
  readonly kind: "original-only";
131
131
  };
132
+ readonly numberingInsertionXml: {
133
+ readonly kind: "original-only";
134
+ };
132
135
  readonly outlineLevel: {
133
136
  readonly kind: "style-resolved-attr";
134
137
  readonly attr: "outlineLevel";
@@ -78,6 +78,7 @@ const PARAGRAPH_FORMATTING_WRITE_BACK = {
78
78
  numPr: { kind: "direct-provenance" },
79
79
  numPrFromStyle: { kind: "derived" },
80
80
  numberingChangeXml: { kind: "original-only" },
81
+ numberingInsertionXml: { kind: "original-only" },
81
82
  outlineLevel: {
82
83
  kind: "style-resolved-attr",
83
84
  attr: "outlineLevel"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.47.2",
3
+ "version": "0.47.4",
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.1",
131
+ "@stll/docx-core": "^0.25.2",
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",