@stll/folio-core 0.15.12 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,6 +42,10 @@ const applyReplaceBlockStyleId = ({ item, tr }) => {
42
42
  styleId: item.operation.styleId
43
43
  });
44
44
  };
45
+ const REPLACEMENT_BACKGROUND_CLEAR_FORMATTING = {
46
+ highlight: false,
47
+ runShading: false
48
+ };
45
49
  const applyInlineFormatting = ({ tr, schema, from, to, formatting }) => {
46
50
  for (const [name, enabled] of Object.entries(formatting)) {
47
51
  const markType = schema.marks[name];
@@ -60,6 +64,29 @@ const formattingWouldChange = (marks, formatting) => Object.entries(formatting).
60
64
  if (name === "underline") return mark?.attrs["style"] !== "single";
61
65
  return mark === void 0;
62
66
  });
67
+ const clearReplacementBackground = ({ tr, schema, from, to, mode, revisionId, author, date, initials, suggestionId = null }) => {
68
+ if (![schema.marks["highlight"], schema.marks["runShading"]].some((markType) => markType !== void 0 && tr.doc.rangeHasMark(from, to, markType))) return tr;
69
+ if (mode === "direct") return applyInlineFormatting({
70
+ tr,
71
+ schema,
72
+ from,
73
+ to,
74
+ formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING
75
+ });
76
+ return applyTrackedInlineFormatting({
77
+ tr,
78
+ schema,
79
+ doc: tr.doc,
80
+ from,
81
+ to,
82
+ formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING,
83
+ revisionId,
84
+ author,
85
+ date,
86
+ initials,
87
+ suggestionId
88
+ });
89
+ };
63
90
  const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, revisionId, author, date, initials, suggestionId = null }) => {
64
91
  const propertyChangeType = schema.marks["runPropertyChange"];
65
92
  if (!propertyChangeType) return tr;
@@ -418,6 +445,21 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
418
445
  case "replaceRange": {
419
446
  const revisionIdDelete = revisionSeed++;
420
447
  const revisionIdInsert = revisionSeed++;
448
+ const revisionIdBackground = revisionSeed++;
449
+ const stepsBeforeBackgroundClear = tr.steps.length;
450
+ tr = clearReplacementBackground({
451
+ tr,
452
+ schema: view.state.schema,
453
+ from: item.from,
454
+ to: item.to,
455
+ mode,
456
+ revisionId: revisionIdBackground,
457
+ author,
458
+ date,
459
+ initials,
460
+ suggestionId
461
+ });
462
+ const clearedBackground = tr.steps.length > stepsBeforeBackgroundClear;
421
463
  tr = applyTextReplacement({
422
464
  tr,
423
465
  item,
@@ -430,7 +472,11 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
430
472
  suggestionId,
431
473
  initials
432
474
  });
433
- if (producesTrackedChanges) appliedRevisionIds = [revisionIdDelete, revisionIdInsert];
475
+ if (producesTrackedChanges) appliedRevisionIds = [
476
+ revisionIdDelete,
477
+ revisionIdInsert,
478
+ ...clearedBackground ? [revisionIdBackground] : []
479
+ ];
434
480
  break;
435
481
  }
436
482
  case "commentOnRange":
@@ -466,6 +512,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
466
512
  case "replaceBlock": {
467
513
  const revisionIdDelete = revisionSeed++;
468
514
  const revisionIdInsert = revisionSeed++;
515
+ const revisionIdBackground = revisionSeed++;
469
516
  if (item.operation.preserveFormatting === false && mode === "direct") {
470
517
  const replacement = item.operation.text;
471
518
  const paragraphType = view.state.schema.nodes["paragraph"];
@@ -485,6 +532,20 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
485
532
  tr = tr.replaceWith(item.blockFrom, item.blockTo, node);
486
533
  break;
487
534
  }
535
+ const stepsBeforeBackgroundClear = tr.steps.length;
536
+ tr = clearReplacementBackground({
537
+ tr,
538
+ schema: view.state.schema,
539
+ from: item.from,
540
+ to: item.to,
541
+ mode,
542
+ revisionId: revisionIdBackground,
543
+ author,
544
+ date,
545
+ initials,
546
+ suggestionId
547
+ });
548
+ const clearedBackground = tr.steps.length > stepsBeforeBackgroundClear;
488
549
  tr = applyTextReplacement({
489
550
  tr,
490
551
  item,
@@ -501,7 +562,11 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
501
562
  item,
502
563
  tr
503
564
  });
504
- if (producesTrackedChanges) appliedRevisionIds = [revisionIdDelete, revisionIdInsert];
565
+ if (producesTrackedChanges) appliedRevisionIds = [
566
+ revisionIdDelete,
567
+ revisionIdInsert,
568
+ ...clearedBackground ? [revisionIdBackground] : []
569
+ ];
505
570
  break;
506
571
  }
507
572
  case "insertAfterBlock":
@@ -8,13 +8,15 @@ import { attemptSelectiveSave } from "../docx/selectiveSave.js";
8
8
  import { acceptAIEditRevision, acceptAllChanges, rejectAIEditRevision, rejectAllChanges } from "../prosemirror/commands/comments.js";
9
9
  import { proseDocToBlocks, updateDocumentContent } from "../prosemirror/conversion/fromProseDoc.js";
10
10
  import { footnoteToProseDoc, headerFooterToProseDoc, toProseDoc } from "../prosemirror/conversion/toProseDoc.js";
11
- import { getChangedParagraphIds, hasStructuralChanges, hasUntrackedChanges, ignoreTrackedChanges } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
11
+ import { ensureBaseDirectionInState } from "../prosemirror/extensions/features/AutoBidiDetectionExtension.js";
12
+ import { getChangedParagraphIds, hasStructuralChanges, hasUntrackedChanges } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
12
13
  import { schema, singletonManager } from "../prosemirror/schema/index.js";
13
14
  import { deterministicHexId } from "../utils/hexId.js";
14
15
  import { buildAnnotatedBlockText } from "./clean-text.js";
15
16
  import { getCommentAnchorsFromDoc, getTrackedChangesFromDoc } from "./read.js";
16
17
  import { createFolioAIEditSnapshot, normalizeFolioAIBlockText } from "./snapshot.js";
17
18
  import { TaggedError } from "better-result";
19
+ import { Fragment } from "prosemirror-model";
18
20
  import { EditorState } from "prosemirror-state";
19
21
  //#region src/ai-edits/headless.ts
20
22
  /**
@@ -80,39 +82,48 @@ const createReviewerComment = (text, author) => ({
80
82
  *
81
83
  * The shared `ParaIdAllocatorExtension` mints RANDOM ids (correct for freshly
82
84
  * typed paragraphs in the live editor); this load-time pass is deterministic so
83
- * a paraId-less corpus document anchors reproducibly. The transaction is marked
84
- * ignore-tracked so the change baseline stays clean.
85
+ * a paraId-less corpus document anchors reproducibly.
86
+ *
87
+ * Rebuilding only the changed branches, like {@link ensureParaIdsInDoc}, keeps
88
+ * the pass linear in paragraph count. Seeding through a transaction instead
89
+ * costs one `setNodeMarkup` step per paragraph, and both halves of that are
90
+ * quadratic: every step rebuilds the containing fragment, and the plugin
91
+ * `appendTransaction` chain rescans the accumulated step maps. Because the pass
92
+ * runs before the state exists, no history, mapping, or change-tracking
93
+ * semantics depend on it.
85
94
  */
86
- const ensureDeterministicParaIdsInState = (state) => {
95
+ const ensureDeterministicParaIdsInDoc = (doc) => {
87
96
  const seen = /* @__PURE__ */ new Set();
88
- const updates = [];
89
97
  let ordinal = 0;
90
- state.doc.descendants((node, pos) => {
91
- if (node.type.name !== "paragraph") return;
92
- ordinal += 1;
93
- const existing = node.attrs["paraId"];
94
- if (typeof existing === "string" && existing.length > 0 && !seen.has(existing)) {
95
- seen.add(existing);
96
- return false;
97
- }
98
- let paraId = deterministicHexId(`${node.textContent}:${ordinal}`);
99
- for (let salt = 1; seen.has(paraId); salt++) paraId = deterministicHexId(`${node.textContent}:${ordinal}:${salt}`);
100
- seen.add(paraId);
101
- updates.push({
102
- pos,
103
- attrs: {
104
- ...node.attrs,
105
- paraId
98
+ const rewrite = (parent) => {
99
+ let changed = false;
100
+ const children = [];
101
+ parent.forEach((child) => {
102
+ let next = child;
103
+ if (child.type.name === "paragraph") {
104
+ ordinal += 1;
105
+ const existing = child.attrs["paraId"];
106
+ if (typeof existing === "string" && existing.length > 0 && !seen.has(existing)) seen.add(existing);
107
+ else {
108
+ let paraId = deterministicHexId(`${child.textContent}:${ordinal}`);
109
+ for (let salt = 1; seen.has(paraId); salt++) paraId = deterministicHexId(`${child.textContent}:${ordinal}:${salt}`);
110
+ seen.add(paraId);
111
+ next = child.type.create({
112
+ ...child.attrs,
113
+ paraId
114
+ }, child.content, child.marks);
115
+ }
116
+ } else if (child.childCount > 0) {
117
+ const content = rewrite(child);
118
+ if (content !== child.content) next = child.copy(content);
106
119
  }
120
+ if (next !== child) changed = true;
121
+ children.push(next);
107
122
  });
108
- return false;
109
- });
110
- if (updates.length === 0) return state;
111
- const tr = state.tr;
112
- for (const update of updates) tr.setNodeMarkup(update.pos, void 0, update.attrs);
113
- ignoreTrackedChanges(tr);
114
- tr.setMeta("addToHistory", false);
115
- return state.apply(tr);
123
+ return changed ? Fragment.fromArray(children) : parent.content;
124
+ };
125
+ const content = rewrite(doc);
126
+ return content === doc.content ? doc : doc.copy(content);
116
127
  };
117
128
  const FOLIO_REVIEWED_VIEWS = Object.freeze([
118
129
  "original",
@@ -225,9 +236,9 @@ var FolioDocxReviewer = class FolioDocxReviewer {
225
236
  password: options.password
226
237
  });
227
238
  const plugins = singletonManager.getPlugins();
228
- const state = ensureDeterministicParaIdsInState(EditorState.create({
239
+ const state = ensureBaseDirectionInState(EditorState.create({
229
240
  schema,
230
- doc: toProseDoc(baseDocument),
241
+ doc: ensureDeterministicParaIdsInDoc(toProseDoc(baseDocument)),
231
242
  plugins
232
243
  }));
233
244
  return new FolioDocxReviewer({
@@ -711,9 +722,10 @@ var FolioDocxReviewer = class FolioDocxReviewer {
711
722
  ...this.baseDocument.package.styles !== void 0 && { styles: this.baseDocument.package.styles },
712
723
  ...this.baseDocument.package.theme !== void 0 && { theme: this.baseDocument.package.theme }
713
724
  };
714
- const state = ensureDeterministicParaIdsInState(EditorState.create({
725
+ const storyDoc = story.type === "header" || story.type === "footer" ? headerFooterToProseDoc(source.content, conversionOptions) : footnoteToProseDoc(source.content, conversionOptions);
726
+ const state = ensureBaseDirectionInState(EditorState.create({
715
727
  schema,
716
- doc: story.type === "header" || story.type === "footer" ? headerFooterToProseDoc(source.content, conversionOptions) : footnoteToProseDoc(source.content, conversionOptions),
728
+ doc: ensureDeterministicParaIdsInDoc(storyDoc),
717
729
  plugins: singletonManager.getPlugins()
718
730
  }));
719
731
  this.secondaryStoryStates.set(key, {
@@ -1,6 +1,22 @@
1
1
  //#region src/docx/server/extractDocxText.d.ts
2
2
  /** Document part containing an extracted paragraph. */
3
3
  type DocxParagraphSource = "header" | "body" | "footer";
4
+ /**
5
+ * Role of an emitted markdown table row.
6
+ *
7
+ * - `cells` — a `w:tr` rendered as a pipe row, including the first row when the
8
+ * table declares it as its header.
9
+ * - `syntheticHeader` — the empty header row emitted for a table that declares
10
+ * no header row; GFM has no headerless table.
11
+ * - `delimiter` — the `| --- |` line GFM requires under the header.
12
+ */
13
+ type DocxTableRowKind = "cells" | "syntheticHeader" | "delimiter";
14
+ /** Table membership of a paragraph whose `text` is a markdown table row. */
15
+ type DocxTableRowPosition = {
16
+ /** 0-based index of the source `w:tbl`, in extraction order across all parts. */
17
+ table: number;
18
+ kind: DocxTableRowKind;
19
+ };
4
20
  /** Paragraph text and lightweight formatting metadata from a DOCX archive. */
5
21
  type ExtractedDocxParagraph = {
6
22
  index: number;
@@ -10,6 +26,13 @@ type ExtractedDocxParagraph = {
10
26
  bold?: boolean;
11
27
  fontSize?: number;
12
28
  alignment?: "left" | "center" | "right" | "both";
29
+ /**
30
+ * Present only when `text` is a markdown table row rendered from a `w:tbl`,
31
+ * absent for ordinary prose paragraphs. Consumers that join `text` across
32
+ * paragraphs need no change; consumers that want to regroup a table's rows,
33
+ * or drop the rows GFM forced into existence, can key off this.
34
+ */
35
+ tableRow?: DocxTableRowPosition;
13
36
  };
14
37
  /** Accepted-revision paragraph text extracted in deterministic part order. */
15
38
  type ExtractedDocxText = {
@@ -20,4 +43,4 @@ type ExtractedDocxText = {
20
43
  /** Extract paragraph text and formatting metadata from a DOCX archive. */
21
44
  declare const extractDocxText: (bytes: ArrayBuffer | Uint8Array) => Promise<ExtractedDocxText>;
22
45
  //#endregion
23
- export { DocxParagraphSource, ExtractedDocxParagraph, ExtractedDocxText, extractDocxText };
46
+ export { DocxParagraphSource, DocxTableRowKind, DocxTableRowPosition, ExtractedDocxParagraph, ExtractedDocxText, extractDocxText };
@@ -1,3 +1,4 @@
1
+ import { escapeTableCell } from "../../markdown/escape.js";
1
2
  import { RELATIONSHIP_TYPES, parseRelationships } from "../relsParser.js";
2
3
  import { findAllDeep, findChild, findDeep, getAttribute, getAttributeAnyPrefix, getLocalName, getTextContent, parseXml } from "../xmlParser.js";
3
4
  import { loadDocxArchive } from "./boundedArchive.js";
@@ -59,13 +60,159 @@ const readRunMetrics = (paragraph) => {
59
60
  }
60
61
  return metrics;
61
62
  };
62
- const extractContainer = ({ container, source, startIndex }) => {
63
+ const TABLE_DELIMITER_CELL = "---";
64
+ /** GFM cannot nest tables; an inner table joins its cells inside the outer cell. */
65
+ const NESTED_TABLE_CELL_SEPARATOR = " / ";
66
+ /** Word supports 63 table columns; cap well above that so a hostile `w:gridSpan` cannot balloon a row. */
67
+ const MAX_TABLE_COLUMNS = 256;
68
+ /** Bound the mutual recursion between a cell and the tables nested inside it. */
69
+ const MAX_NESTED_TABLE_DEPTH = 8;
70
+ /**
71
+ * Collect a table's `w:tr`, or a row's `w:tc`, seeing through the wrappers Word
72
+ * puts around them (`w:sdt` / `w:sdtContent` content controls, `w:customXml`).
73
+ * The walk stops at `w:tbl` and `w:p` so a nested table's rows and cells never
74
+ * leak into the grid of the table that contains them.
75
+ */
76
+ const collectTableParts = (parent, localName) => {
77
+ const parts = [];
78
+ const walk = (node) => {
79
+ for (const child of childElements(node)) {
80
+ const childName = getLocalName(child.name ?? "");
81
+ if (childName === localName) {
82
+ parts.push(child);
83
+ continue;
84
+ }
85
+ if (childName === "tbl" || childName === "p") continue;
86
+ walk(child);
87
+ }
88
+ };
89
+ walk(parent);
90
+ return parts;
91
+ };
92
+ /**
93
+ * Raw (unescaped) text of one cell: its paragraphs in order, one per line.
94
+ * Blank paragraphs are dropped so a cell padded with empty paragraphs does not
95
+ * render as a run of `<br>`. A nested table contributes one line per inner row.
96
+ */
97
+ const readCellText = (cell, depth) => {
98
+ const lines = [];
99
+ const walk = (node) => {
100
+ for (const child of childElements(node)) {
101
+ const childName = getLocalName(child.name ?? "");
102
+ if (childName === "p") {
103
+ const text = collectText(child);
104
+ if (text.length > 0) lines.push(text);
105
+ continue;
106
+ }
107
+ if (childName === "tbl") {
108
+ if (depth >= MAX_NESTED_TABLE_DEPTH) continue;
109
+ for (const line of flattenNestedTable(child, depth + 1)) lines.push(line);
110
+ continue;
111
+ }
112
+ walk(child);
113
+ }
114
+ };
115
+ walk(cell);
116
+ return lines.join("\n");
117
+ };
118
+ const flattenNestedTable = (table, depth) => {
119
+ const lines = [];
120
+ for (const row of collectTableParts(table, "tr")) {
121
+ const cells = collectTableParts(row, "tc").map((cell) => readCellText(cell, depth));
122
+ if (cells.some((text) => text.length > 0)) lines.push(cells.join(NESTED_TABLE_CELL_SEPARATOR));
123
+ }
124
+ return lines;
125
+ };
126
+ const readTableCell = (cell, depth) => {
127
+ const properties = findChild(cell, "w", "tcPr");
128
+ const gridSpanValue = getAttributeAnyPrefix(findChild(properties, "w", "gridSpan"), "val");
129
+ const parsedGridSpan = gridSpanValue === null ? 1 : Number.parseInt(gridSpanValue, 10);
130
+ const gridSpan = Number.isFinite(parsedGridSpan) && parsedGridSpan > 1 ? parsedGridSpan : 1;
131
+ const vMerge = findChild(properties, "w", "vMerge");
132
+ if (vMerge !== null && getAttributeAnyPrefix(vMerge, "val") !== "restart") return {
133
+ text: "",
134
+ gridSpan
135
+ };
136
+ return {
137
+ text: readCellText(cell, depth),
138
+ gridSpan
139
+ };
140
+ };
141
+ /**
142
+ * Does the row declare itself a header? `w:tblHeader` is the only OOXML signal
143
+ * that says so: it marks the row Word repeats at the top of each page. The
144
+ * neighbouring `w:tblLook/@w:firstRow` is conditional *formatting* that Word
145
+ * writes on essentially every table (its default `w:val="04A0"`), so keying off
146
+ * it would promote the first data row of almost every document.
147
+ *
148
+ * Without the flag the table is headerless and GFM gets a synthetic empty header
149
+ * row: a table's first row is data until the document says otherwise, and column
150
+ * names invented here would be read back as facts about the document.
151
+ */
152
+ const declaresHeaderRow = (row) => {
153
+ const header = findChild(findChild(row, "w", "trPr"), "w", "tblHeader");
154
+ if (header === null) return false;
155
+ const value = getAttributeAnyPrefix(header, "val");
156
+ return value !== "0" && value !== "false";
157
+ };
158
+ const readTableGrid = (table) => {
159
+ const rows = [];
160
+ let columnCount = 0;
161
+ let firstRowIsHeader = false;
162
+ for (const [rowIndex, row] of collectTableParts(table, "tr").entries()) {
163
+ if (rowIndex === 0) firstRowIsHeader = declaresHeaderRow(row);
164
+ const columns = [];
165
+ for (const cell of collectTableParts(row, "tc")) {
166
+ if (columns.length >= MAX_TABLE_COLUMNS) break;
167
+ const { text, gridSpan } = readTableCell(cell, 0);
168
+ columns.push(text);
169
+ const padding = Math.min(gridSpan - 1, MAX_TABLE_COLUMNS - columns.length);
170
+ for (let index = 0; index < padding; index++) columns.push("");
171
+ }
172
+ if (columns.length > columnCount) columnCount = columns.length;
173
+ rows.push(columns);
174
+ }
175
+ return {
176
+ rows,
177
+ columnCount,
178
+ firstRowIsHeader
179
+ };
180
+ };
181
+ /** Pad a row to the table's column count and escape each cell into a pipe row. */
182
+ const toRowLine = (columns, columnCount) => {
183
+ const cells = [];
184
+ for (let column = 0; column < columnCount; column++) cells.push(escapeTableCell(columns[column] ?? ""));
185
+ return `| ${cells.join(" | ")} |`;
186
+ };
187
+ /** Render a `w:tbl` as GFM rows. A table with no cell at all renders nothing. */
188
+ const renderTableRows = (table, tableIndex) => {
189
+ const { rows, columnCount, firstRowIsHeader } = readTableGrid(table);
190
+ const [firstRow, ...remainingRows] = rows;
191
+ if (columnCount === 0 || firstRow === void 0) return [];
192
+ const rendered = [];
193
+ const push = (text, kind) => {
194
+ rendered.push({
195
+ text,
196
+ position: {
197
+ table: tableIndex,
198
+ kind
199
+ }
200
+ });
201
+ };
202
+ if (firstRowIsHeader) push(toRowLine(firstRow, columnCount), "cells");
203
+ else push(toRowLine([], columnCount), "syntheticHeader");
204
+ push(toRowLine(Array.from({ length: columnCount }, () => TABLE_DELIMITER_CELL), columnCount), "delimiter");
205
+ for (const row of firstRowIsHeader ? remainingRows : rows) push(toRowLine(row, columnCount), "cells");
206
+ return rendered;
207
+ };
208
+ const extractContainer = ({ container, source, startIndex, startTableIndex }) => {
63
209
  const paragraphs = [];
64
210
  let charCount = 0;
65
- for (const [offset, paragraph] of findAllDeep(container, "w", "p").entries()) {
211
+ let tableCount = 0;
212
+ const pushProse = (paragraph) => {
66
213
  const text = collectText(paragraph);
67
214
  const entry = {
68
- index: startIndex + offset,
215
+ index: startIndex + paragraphs.length,
69
216
  text,
70
217
  source
71
218
  };
@@ -81,15 +228,45 @@ const extractContainer = ({ container, source, startIndex }) => {
81
228
  }
82
229
  paragraphs.push(entry);
83
230
  charCount += text.length;
84
- }
231
+ };
232
+ const pushTableRow = ({ text, position }) => {
233
+ paragraphs.push({
234
+ index: startIndex + paragraphs.length,
235
+ text,
236
+ source,
237
+ tableRow: position
238
+ });
239
+ charCount += text.length;
240
+ };
241
+ /**
242
+ * Walk block content in document order. Descent mirrors the previous
243
+ * `findAllDeep(container, "w", "p")` — every wrapper (`w:sdt`, textboxes) is
244
+ * still entered — except that a `w:tbl` is consumed as a table instead of
245
+ * having its cell paragraphs emitted individually.
246
+ */
247
+ const walkBlocks = (node) => {
248
+ for (const child of childElements(node)) {
249
+ const childName = getLocalName(child.name ?? "");
250
+ if (childName === "tbl") {
251
+ for (const row of renderTableRows(child, startTableIndex + tableCount)) pushTableRow(row);
252
+ tableCount += 1;
253
+ continue;
254
+ }
255
+ if (childName === "p") pushProse(child);
256
+ walkBlocks(child);
257
+ }
258
+ };
259
+ walkBlocks(container);
85
260
  return {
86
261
  paragraphs,
87
- charCount
262
+ charCount,
263
+ tableCount
88
264
  };
89
265
  };
90
- const extractParts = async ({ archive, source, rootName, startIndex, paths }) => {
266
+ const extractParts = async ({ archive, source, rootName, startIndex, startTableIndex, paths }) => {
91
267
  const paragraphs = [];
92
268
  let charCount = 0;
269
+ let tableCount = 0;
93
270
  let nextIndex = startIndex;
94
271
  for (const path of paths) {
95
272
  const xml = await archive.readEntryString(path);
@@ -99,15 +276,18 @@ const extractParts = async ({ archive, source, rootName, startIndex, paths }) =>
99
276
  const result = extractContainer({
100
277
  container,
101
278
  source,
102
- startIndex: nextIndex
279
+ startIndex: nextIndex,
280
+ startTableIndex: startTableIndex + tableCount
103
281
  });
104
282
  paragraphs.push(...result.paragraphs);
105
283
  charCount += result.charCount;
284
+ tableCount += result.tableCount;
106
285
  nextIndex += result.paragraphs.length;
107
286
  }
108
287
  return {
109
288
  paragraphs,
110
- charCount
289
+ charCount,
290
+ tableCount
111
291
  };
112
292
  };
113
293
  /** A `word/_rels/document.xml.rels` `Target` is relative to `word/`; resolve it to a full archive-entry path. */
@@ -173,18 +353,21 @@ const extractDocxText = async (bytes) => {
173
353
  source: "header",
174
354
  rootName: "hdr",
175
355
  startIndex: 0,
356
+ startTableIndex: 0,
176
357
  paths: referencedParts.headers
177
358
  });
178
359
  const bodyResult = extractContainer({
179
360
  container: body,
180
361
  source: "body",
181
- startIndex: headers.paragraphs.length
362
+ startIndex: headers.paragraphs.length,
363
+ startTableIndex: headers.tableCount
182
364
  });
183
365
  const footers = await extractParts({
184
366
  archive,
185
367
  source: "footer",
186
368
  rootName: "ftr",
187
369
  startIndex: headers.paragraphs.length + bodyResult.paragraphs.length,
370
+ startTableIndex: headers.tableCount + bodyResult.tableCount,
188
371
  paths: referencedParts.footers
189
372
  });
190
373
  return {
@@ -69,7 +69,7 @@ function formatNumberedMarker(counters, level) {
69
69
  const parts = [];
70
70
  for (let i = 0; i <= level; i += 1) {
71
71
  const value = counters[i] ?? 0;
72
- if (value <= 0) break;
72
+ if (!Number.isFinite(value) || value <= 0) break;
73
73
  parts.push(value);
74
74
  }
75
75
  if (parts.length === 0) return "1.";
@@ -156,23 +156,23 @@ function computeListMarker(pmAttrs, listCounters, abstractCounters, seenNumIds)
156
156
  const abstractNumId = pmAttrs.listAbstractNumId;
157
157
  if (level > 0) {
158
158
  const latestAbstractCounters = abstractNumId === void 0 ? void 0 : abstractCounters.get(abstractNumId);
159
- if (counters.slice(0, level).every(Number.isNaN)) for (let i = 0; i < level; i += 1) {
159
+ if (counters.slice(0, level).every((counter) => !Number.isFinite(counter))) for (let i = 0; i < level; i += 1) {
160
160
  const latestCounter = latestAbstractCounters?.[i];
161
- counters[i] = latestCounter !== void 0 && !Number.isNaN(latestCounter) ? latestCounter : pmAttrs.listLevelStarts?.[i] ?? 1;
161
+ counters[i] = latestCounter !== void 0 && Number.isFinite(latestCounter) ? latestCounter : pmAttrs.listLevelStarts?.[i] ?? 1;
162
162
  }
163
163
  }
164
164
  const seenKey = `${numId}:${level}`;
165
165
  if (!seenNumIds.has(seenKey)) {
166
166
  seenNumIds.add(seenKey);
167
167
  if (pmAttrs.listStartOverride != null) counters[level] = pmAttrs.listStartOverride - 1;
168
- else if (Number.isNaN(counters[level])) counters[level] = (pmAttrs.listLevelStarts?.[level] ?? 1) - 1;
169
168
  }
169
+ if (!Number.isFinite(counters[level])) counters[level] = (pmAttrs.listLevelStarts?.[level] ?? 1) - 1;
170
170
  counters[level] = (counters[level] ?? 0) + 1;
171
171
  for (let i = level + 1; i < counters.length; i += 1) counters[i] = NaN;
172
172
  const childAdvances = pmAttrs.listImplicitChildLevelAdvances ?? 0;
173
173
  if (childAdvances > 0 && level + 1 < counters.length) {
174
174
  const childCounter = counters[level + 1];
175
- counters[level + 1] = (childCounter === void 0 || Number.isNaN(childCounter) ? 0 : childCounter) + childAdvances;
175
+ counters[level + 1] = (childCounter === void 0 || !Number.isFinite(childCounter) ? 0 : childCounter) + childAdvances;
176
176
  }
177
177
  listCounters.set(numId, counters);
178
178
  if (abstractNumId !== void 0) abstractCounters.set(abstractNumId, [...counters]);
@@ -75,7 +75,17 @@ type CollapsedLineEdgeCaretGeometry = {
75
75
  * below the line until visible text is typed. Anchor the caret to the nearest
76
76
  * painted sibling instead; an all-whitespace line falls back to its line box.
77
77
  */
78
- declare function getCollapsedLineEdgeCaretGeometry(spanEl: HTMLElement): CollapsedLineEdgeCaretGeometry | null;
78
+ declare function getCollapsedLineEdgeCaretGeometry(spanEl: HTMLElement, pmPos?: number): CollapsedLineEdgeCaretGeometry | null;
79
+ type CollapsedLineEdgeCaretTarget = {
80
+ span: HTMLElement;
81
+ geometry: CollapsedLineEdgeCaretGeometry;
82
+ };
83
+ /**
84
+ * Prefer a collapsed line-edge span when multiple painted spans share the PM
85
+ * position. Generic text runs use inclusive endpoints, so DOM order alone
86
+ * cannot decide which span owns a boundary.
87
+ */
88
+ declare function findCollapsedLineEdgeCaretTarget(spans: readonly HTMLElement[], pmPos: number): CollapsedLineEdgeCaretTarget | null;
79
89
  declare function getCaretPositionFromDom(container: HTMLElement, pmPos: number, overlayRect: DOMRect): DomCaretPosition | null;
80
90
  //#endregion
81
- export { CollapsedLineEdgeCaretGeometry, DomCaretPosition, DomSelectionRect, clickToPositionDom, findPositionInSpan, getCaretPositionFromDom, getCollapsedLineEdgeCaretGeometry, getSelectionRectsFromDom };
91
+ export { CollapsedLineEdgeCaretGeometry, CollapsedLineEdgeCaretTarget, DomCaretPosition, DomSelectionRect, clickToPositionDom, findCollapsedLineEdgeCaretTarget, findPositionInSpan, getCaretPositionFromDom, getCollapsedLineEdgeCaretGeometry, getSelectionRectsFromDom };
@@ -232,7 +232,7 @@ function getSelectionRectsFromDom(container, from, to, overlayRect) {
232
232
  * below the line until visible text is typed. Anchor the caret to the nearest
233
233
  * painted sibling instead; an all-whitespace line falls back to its line box.
234
234
  */
235
- function getCollapsedLineEdgeCaretGeometry(spanEl) {
235
+ function getCollapsedLineEdgeCaretGeometry(spanEl, pmPos) {
236
236
  const isLeading = spanEl.dataset["collapsedLeadingSpaces"] === "true";
237
237
  const isTrailing = spanEl.dataset["collapsedTrailingSpaces"] === "true";
238
238
  if (!isLeading && !isTrailing) return null;
@@ -242,14 +242,50 @@ function getCollapsedLineEdgeCaretGeometry(spanEl) {
242
242
  const line = closestHtmlElement(spanEl, ".layout-line");
243
243
  const anchorRect = sibling?.getBoundingClientRect() ?? line?.getBoundingClientRect() ?? spanRect;
244
244
  const lineRect = line?.getBoundingClientRect();
245
+ const pmStart = Number(spanEl.dataset["pmStart"]);
246
+ const pmEnd = Number(spanEl.dataset["pmEnd"]);
247
+ const storedSpaceAdvance = Number(spanEl.dataset["collapsedSpaceAdvance"]);
248
+ const lineScale = line && line.offsetWidth > 0 && lineRect ? lineRect.width / line.offsetWidth : 1;
249
+ const trailingSpaceCount = isTrailing && pmPos !== void 0 && Number.isFinite(pmStart) && Number.isFinite(pmEnd) ? Math.max(0, Math.min(pmPos, pmEnd) - pmStart) : 0;
250
+ const inlineDirection = spanRect.left <= anchorRect.left ? -1 : 1;
251
+ const trailingAdvance = Number.isFinite(storedSpaceAdvance) && lineScale > 0 ? trailingSpaceCount * storedSpaceAdvance * lineScale * inlineDirection : 0;
245
252
  return {
246
- left: spanRect.left,
253
+ left: spanRect.left + trailingAdvance,
247
254
  top: anchorRect.top,
248
255
  height: anchorRect.height || lineRect?.height || 16
249
256
  };
250
257
  }
258
+ /**
259
+ * Prefer a collapsed line-edge span when multiple painted spans share the PM
260
+ * position. Generic text runs use inclusive endpoints, so DOM order alone
261
+ * cannot decide which span owns a boundary.
262
+ */
263
+ function findCollapsedLineEdgeCaretTarget(spans, pmPos) {
264
+ for (const span of spans) {
265
+ const pmStart = Number(span.dataset["pmStart"]);
266
+ const pmEnd = Number(span.dataset["pmEnd"]);
267
+ if (pmPos < pmStart || pmPos > pmEnd) continue;
268
+ const geometry = getCollapsedLineEdgeCaretGeometry(span, pmPos);
269
+ if (geometry) return {
270
+ span,
271
+ geometry
272
+ };
273
+ }
274
+ return null;
275
+ }
251
276
  function getCaretPositionFromDom(container, pmPos, overlayRect) {
252
277
  const spans = htmlQueryAll(container, ".layout-page-content span[data-pm-start][data-pm-end]");
278
+ const collapsedTarget = findCollapsedLineEdgeCaretTarget(spans, pmPos);
279
+ if (collapsedTarget) {
280
+ const pageEl = closestHtmlElement(collapsedTarget.span, ".layout-page");
281
+ const pageIndex = pageEl ? Number(pageEl.dataset["pageNumber"] || 1) - 1 : 0;
282
+ return {
283
+ x: collapsedTarget.geometry.left - overlayRect.left,
284
+ y: collapsedTarget.geometry.top - overlayRect.top,
285
+ height: collapsedTarget.geometry.height,
286
+ pageIndex
287
+ };
288
+ }
253
289
  for (const spanEl of spans) {
254
290
  const pmStart = Number(spanEl.dataset["pmStart"]);
255
291
  const pmEnd = Number(spanEl.dataset["pmEnd"]);
@@ -270,17 +306,6 @@ function getCaretPositionFromDom(container, pmPos, overlayRect) {
270
306
  continue;
271
307
  }
272
308
  if (pmPos >= pmStart && pmPos <= pmEnd) {
273
- const collapsedGeometry = getCollapsedLineEdgeCaretGeometry(spanEl);
274
- if (collapsedGeometry) {
275
- const pageEl = closestHtmlElement(spanEl, ".layout-page");
276
- const pageIndex = pageEl ? Number(pageEl.dataset["pageNumber"] || 1) - 1 : 0;
277
- return {
278
- x: collapsedGeometry.left - overlayRect.left,
279
- y: collapsedGeometry.top - overlayRect.top,
280
- height: collapsedGeometry.height,
281
- pageIndex
282
- };
283
- }
284
309
  const textNode = spanEl.firstChild;
285
310
  if (!textNode || textNode.nodeType !== Node.TEXT_NODE) {
286
311
  const spanRect = spanEl.getBoundingClientRect();
@@ -335,4 +360,4 @@ function getCaretPositionFromDom(container, pmPos, overlayRect) {
335
360
  return null;
336
361
  }
337
362
  //#endregion
338
- export { clickToPositionDom, findPositionInSpan, getCaretPositionFromDom, getCollapsedLineEdgeCaretGeometry, getSelectionRectsFromDom };
363
+ export { clickToPositionDom, findCollapsedLineEdgeCaretTarget, findPositionInSpan, getCaretPositionFromDom, getCollapsedLineEdgeCaretGeometry, getSelectionRectsFromDom };
@@ -69,7 +69,7 @@ function computeHfCaretRectFromView(view, section, doc = globalThis.document) {
69
69
  const end = Number(span.dataset["pmEnd"]);
70
70
  if (!Number.isFinite(start) || !Number.isFinite(end)) continue;
71
71
  if (pmPos >= start && pmPos <= end) {
72
- const collapsedGeometry = getCollapsedLineEdgeCaretGeometry(span);
72
+ const collapsedGeometry = getCollapsedLineEdgeCaretGeometry(span, pmPos);
73
73
  if (collapsedGeometry) return {
74
74
  top: collapsedGeometry.top,
75
75
  left: collapsedGeometry.left,
@@ -988,11 +988,14 @@ function renderLine(block, line, alignment, doc, options) {
988
988
  const splitRuns = splitTextRunsByEastAsia(sliceRunsForLine(block, line));
989
989
  const { runs: runsForLine, collapsedLeadingRuns: collapsedLeadingSpaceRuns, collapsedTrailingRuns: collapsedTrailingSpaceRuns } = splitCollapsibleLineEdgeSpaces(splitRuns, startsAfterSoftWrap(block, line));
990
990
  const isCollapsedLineEdgeSpaceRun = (run) => collapsedLeadingSpaceRuns.has(run) || collapsedTrailingSpaceRuns.has(run);
991
+ const collapsedSpaceMeasureText = collapsedLeadingSpaceRuns.size > 0 || collapsedTrailingSpaceRuns.size > 0 ? createTextMeasurer(doc) : void 0;
991
992
  const renderLineTextRun = (run) => {
992
993
  const runEl = renderTextRun(run, doc);
993
994
  if (collapsedLeadingSpaceRuns.has(run)) runEl.dataset["collapsedLeadingSpaces"] = "true";
994
995
  if (collapsedTrailingSpaceRuns.has(run)) runEl.dataset["collapsedTrailingSpaces"] = "true";
995
996
  if (isCollapsedLineEdgeSpaceRun(run)) {
997
+ const spaceAdvance = (collapsedSpaceMeasureText?.(" ", run.fontSize || 11, run.fontFamily || "Calibri", runMeasureStyle(run)) ?? 0) * ((run.horizontalScale ?? 100) / 100);
998
+ runEl.dataset["collapsedSpaceAdvance"] = String(spaceAdvance);
996
999
  runEl.style.fontSize = "0";
997
1000
  runEl.style.letterSpacing = "0";
998
1001
  runEl.style.wordSpacing = "0";
@@ -1055,7 +1058,7 @@ function renderLine(block, line, alignment, doc, options) {
1055
1058
  lineEl.style.overflow = "visible";
1056
1059
  let tabContext;
1057
1060
  const hasScaledTextRun = runsForLine.some((run) => (isTextRun(run) || isFieldRun(run) || isMathRun(run)) && run.horizontalScale !== void 0 && run.horizontalScale !== 100);
1058
- const measureText = hasTabRuns || hasScaledTextRun ? createTextMeasurer(doc) : void 0;
1061
+ const measureText = collapsedSpaceMeasureText ?? (hasTabRuns || hasScaledTextRun ? createTextMeasurer(doc) : void 0);
1059
1062
  if (hasTabRuns) {
1060
1063
  const explicitStops = options?.tabStops?.map(convertTabStopToCalc);
1061
1064
  const leftIndentTwips = options?.leftIndentPx ? Math.round(options.leftIndentPx * 15) : 0;
@@ -1,4 +1,4 @@
1
- import { getCollapsedLineEdgeCaretGeometry } from "../layout-bridge/dom/clickToPositionDom.js";
1
+ import { findCollapsedLineEdgeCaretTarget } from "../layout-bridge/dom/clickToPositionDom.js";
2
2
  import { findBodyEmptyRuns, findBodyPmSpans } from "../layout-bridge/dom/findBodyPmSpans.js";
3
3
  import { closestHtmlElement } from "../utils/domGuards.js";
4
4
  //#region src/render-dom/RenderedDomContext.ts
@@ -27,7 +27,14 @@ var RenderedDomContextImpl = class {
27
27
  }
28
28
  getCoordinatesForPosition(pmPos) {
29
29
  const containerRect = this.#pagesContainer.getBoundingClientRect();
30
- for (const span of findBodyPmSpans(this.#pagesContainer)) {
30
+ const spans = findBodyPmSpans(this.#pagesContainer);
31
+ const collapsedTarget = findCollapsedLineEdgeCaretTarget(spans, pmPos);
32
+ if (collapsedTarget) return {
33
+ x: (collapsedTarget.geometry.left - containerRect.left) / this.#zoom,
34
+ y: (collapsedTarget.geometry.top - containerRect.top) / this.#zoom,
35
+ height: lineHeightFor(collapsedTarget.span, this.#zoom)
36
+ };
37
+ for (const span of spans) {
31
38
  const pmStart = Number(span.dataset["pmStart"]);
32
39
  const pmEnd = Number(span.dataset["pmEnd"]);
33
40
  if (!(span.classList.contains("layout-run-tab") ? pmPos >= pmStart && pmPos < pmEnd : pmPos >= pmStart && pmPos <= pmEnd)) continue;
@@ -37,12 +44,6 @@ var RenderedDomContextImpl = class {
37
44
  y: (spanRect.top - containerRect.top) / this.#zoom,
38
45
  height: lineHeightFor(span, this.#zoom)
39
46
  };
40
- const collapsedGeometry = getCollapsedLineEdgeCaretGeometry(span);
41
- if (collapsedGeometry) return {
42
- x: (collapsedGeometry.left - containerRect.left) / this.#zoom,
43
- y: (collapsedGeometry.top - containerRect.top) / this.#zoom,
44
- height: lineHeightFor(span, this.#zoom)
45
- };
46
47
  const textNode = textNodeForSpan(span);
47
48
  if (!textNode) return {
48
49
  x: (spanRect.left - containerRect.left) / this.#zoom,
package/dist/server.d.ts CHANGED
@@ -18,8 +18,8 @@ import { EvaluateDocxXmlPatchProposalArgs, FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULT
18
18
  import { FOLIO_DOCX_CONFORMANCE_CHECKS, FOLIO_DOCX_CONFORMANCE_ISSUE_CODES, FOLIO_DOCX_CONFORMANCE_PROFILE, FOLIO_DOCX_CONFORMANCE_REPORT_VERSION, FolioDocxConformanceCheck, FolioDocxConformanceCheckId, FolioDocxConformanceCheckStatus, FolioDocxConformanceIssue, FolioDocxConformanceIssueCode, FolioDocxConformanceReport, FolioDocxConformanceStatus, ValidateDocxConformanceOptions, validateDocxConformance } from "./docx/server/validateDocxConformance.js";
19
19
  import { ApplyDocxXmlPatchProposalArgs, FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE, FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION, FolioDocxXmlPatchApplication, FolioDocxXmlPatchApplicationError, FolioDocxXmlPatchApplicationReceipt, UnsupportedFolioDocxXmlPatchApplicationProfileError, applyDocxXmlPatchProposal } from "./docx/server/applyDocxXmlPatchProposal.js";
20
20
  import { docxToMarkdown } from "./docx/server/docxToMarkdown.js";
21
- import { DocxParagraphSource, ExtractedDocxParagraph, ExtractedDocxText, extractDocxText } from "./docx/server/extractDocxText.js";
21
+ import { DocxParagraphSource, DocxTableRowKind, DocxTableRowPosition, ExtractedDocxParagraph, ExtractedDocxText, extractDocxText } from "./docx/server/extractDocxText.js";
22
22
  import { FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS, FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES, FOLIO_DOCX_PACKAGE_INSPECTION_VERSION, FolioDocxInspectedXmlPart, FolioDocxPackageInspection, FolioDocxPackageInspectionError, FolioDocxPackageInspectionErrorCode, FolioDocxPackageInspectionLimits, FolioDocxPackagePart, FolioDocxPackagePartKind, InspectDocxPackageOptions, inspectDocxPackage } from "./docx/server/inspectDocxPackage.js";
23
23
  import { GenerateRedlineDocxOptions, GenerateRedlineDocxResult, GenerateRedlineUnprocessedStory, InvalidGenerateRedlineDocxOptionsError, generateRedlineDocx } from "./redline.js";
24
24
  import { FOLIO_VERSION_COMPARISON_PRIVACY_TRANSFORMS, FOLIO_VERSION_COMPARISON_SCOPES, FolioBlockDiff, FolioCompareDocxVersionsOptions, FolioDocumentMetadataValue, FolioFormatProperty, FolioMetadataDiff, FolioStoryDiff, FolioVersionBlockHandle, FolioVersionComparisonPrivacyTransform, FolioVersionComparisonScope, FolioVersionDiff, FolioVersionDiffPrivacyOptions, FolioVersionDiffPrivacyReport, FolioVersionDiffSegment, FolioVersionDiffSummaryCounts, InvalidFolioVersionComparisonOptionsError, applyFolioVersionDiffPrivacy, compareDocxVersions, isFolioVersionComparisonPrivacyTransform, isFolioVersionComparisonScope } from "./version-comparison.js";
25
- export { type ApplyDocxXmlPatchProposalArgs, type ApplyFolioAIEditsToBufferOptions, type ApplyFolioAIEditsToBufferResult, type CreateCommentReplyInput, type CreateEmptyDocumentOptions, DOCUMENT_PRESET_VERSION, DOCUMENT_STYLE_SET_VERSION, type DeriveBlockIdInput, type DocumentPreset, type DocumentStyleCatalog, type DocumentStyleCatalogEntry, type DocumentStyleSet, DocxArchiveError, type DocxArchiveOptions, type DocxParagraphSource, EnsureParaIdsError, type EnsureParaIdsOptions, type EnsureParaIdsResult, type EvaluateDocxXmlPatchProposalArgs, type ExtractDocumentStyleSetOptions, type ExtractedDocxParagraph, type ExtractedDocxText, FOLIO_DOCUMENT_METADATA_PROPERTIES, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FOLIO_DOCUMENT_PRIVACY_TRANSFORMS, FOLIO_DOCX_CONFORMANCE_CHECKS, FOLIO_DOCX_CONFORMANCE_ISSUE_CODES, FOLIO_DOCX_CONFORMANCE_PROFILE, FOLIO_DOCX_CONFORMANCE_REPORT_VERSION, FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS, FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES, FOLIO_DOCX_PACKAGE_INSPECTION_VERSION, FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE, FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION, FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS, FOLIO_DOCX_XML_PATCH_PROPOSAL_ISSUE_CODES, FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE, FOLIO_DOCX_XML_PATCH_PROPOSAL_VERSION, FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FOLIO_VERSION_COMPARISON_PRIVACY_TRANSFORMS, FOLIO_VERSION_COMPARISON_SCOPES, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditSnapshot, type FolioAIInlineFormatting, type FolioAITextRangeHandle, type FolioApplyDocumentOperationsOptions, type FolioApplyDocumentOperationsToStoryOptions, type FolioApplyOperationsOptions, type FolioBlockDiff, type FolioBlockId, type FolioCompareDocxVersionsOptions, type FolioDocumentMetadataProperty, type FolioDocumentMetadataValue, type FolioDocumentNavigationTarget, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationStatus, type FolioDocumentOperationStory, type FolioDocumentOperationType, type FolioDocumentOperationUndoFailureReason, type FolioDocumentOperationUndoHandle, type FolioDocumentOperationUndoResult, type FolioDocumentOutline, type FolioDocumentOutlineEntry, FolioDocumentPrivacyArchiveError, type FolioDocumentPrivacyOptions, type FolioDocumentPrivacyReport, type FolioDocumentPrivacyTransform, type FolioDocumentSection, type FolioDocumentSectionHandle, type FolioDocumentSectionReadResult, type FolioDocumentStory, type FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, type FolioDocxConformanceCheck, type FolioDocxConformanceCheckId, type FolioDocxConformanceCheckStatus, type FolioDocxConformanceIssue, type FolioDocxConformanceIssueCode, type FolioDocxConformanceReport, type FolioDocxConformanceStatus, type FolioDocxInspectedXmlPart, type FolioDocxPackageInspection, FolioDocxPackageInspectionError, type FolioDocxPackageInspectionErrorCode, type FolioDocxPackageInspectionLimits, type FolioDocxPackagePart, type FolioDocxPackagePartKind, type FolioDocxPreparedXmlReplacement, FolioDocxReviewer, type FolioDocxReviewerOptions, type FolioDocxXmlPatchApplication, FolioDocxXmlPatchApplicationError, type FolioDocxXmlPatchApplicationReceipt, type FolioDocxXmlPatchProposal, type FolioDocxXmlPatchProposalEvaluation, type FolioDocxXmlPatchProposalIssue, type FolioDocxXmlPatchProposalIssueCode, type FolioDocxXmlPatchProposalLimits, type FolioDocxXmlReplacement, type FolioEditableDocumentStoryHandle, type FolioFormatProperty, type FolioMetadataDiff, type FolioReadReviewedStoryOptions, type FolioResolveReviewedStoryOptions, type FolioResolvedReviewedView, type FolioReviewChange, type FolioReviewChangeFilter, type FolioReviewChangeKind, type FolioReviewComment, type FolioReviewCommentFilter, type FolioReviewCommentReply, type FolioReviewReplyInput, type FolioReviewedStory, type FolioReviewedView, type FolioStoryDiff, type FolioVersionBlockHandle, type FolioVersionComparisonPrivacyTransform, type FolioVersionComparisonScope, type FolioVersionDiff, type FolioVersionDiffPrivacyOptions, type FolioVersionDiffPrivacyReport, type FolioVersionDiffSegment, type FolioVersionDiffSummaryCounts, type GenerateRedlineDocxOptions, type GenerateRedlineDocxResult, type GenerateRedlineUnprocessedStory, type InspectDocxPackageOptions, InvalidFolioDocumentOperationBatchError, InvalidFolioDocumentPrivacyOptionsError, InvalidFolioDocxXmlPatchProposalError, InvalidFolioDocxXmlPatchProposalOptionsError, InvalidFolioVersionComparisonOptionsError, InvalidGenerateRedlineDocxOptionsError, type RewriteDocxMetadataPrivacyResult, STELLA_STYLE_SET_NAME, UnsupportedFolioDocumentOperationVersionError, UnsupportedFolioDocxXmlPatchApplicationProfileError, UnsupportedFolioReviewedViewError, type ValidateDocxConformanceOptions, applyDocxXmlPatchProposal, applyFolioAIEditsToBuffer, applyFolioVersionDiffPrivacy, assertSupportedFolioDocumentOperationVersion, compareDocxVersions, createDocx, createEmptyDocument, createFolioAITextRangeHandle, createStellaStyleDocumentPreset, createStellaStyleSet, deriveBlockId, docxToMarkdown, ensureParaIds, evaluateDocxXmlPatchProposal, extractDocumentStyleSet, extractDocumentStyleSetFromDocx, extractDocxText, generateRedlineDocx, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioDocumentOutline, getFolioParaIdFromBlockId, hashFolioAIBlockText, inspectDocumentStyles, inspectDocumentStylesFromDocx, inspectDocxPackage, isFolioBlockId, isFolioDocumentOperationModeSupported, isFolioDocumentPrivacyTransform, isFolioResolvedReviewedView, isFolioReviewedView, isFolioVersionComparisonPrivacyTransform, isFolioVersionComparisonScope, isSequentialFolioBlockId, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, parseFolioDocxXmlPatchProposal, readFolioDocumentSection, replyToComment, rewriteDocxMetadataPrivacy, validateDocxConformance };
25
+ export { type ApplyDocxXmlPatchProposalArgs, type ApplyFolioAIEditsToBufferOptions, type ApplyFolioAIEditsToBufferResult, type CreateCommentReplyInput, type CreateEmptyDocumentOptions, DOCUMENT_PRESET_VERSION, DOCUMENT_STYLE_SET_VERSION, type DeriveBlockIdInput, type DocumentPreset, type DocumentStyleCatalog, type DocumentStyleCatalogEntry, type DocumentStyleSet, DocxArchiveError, type DocxArchiveOptions, type DocxParagraphSource, type DocxTableRowKind, type DocxTableRowPosition, EnsureParaIdsError, type EnsureParaIdsOptions, type EnsureParaIdsResult, type EvaluateDocxXmlPatchProposalArgs, type ExtractDocumentStyleSetOptions, type ExtractedDocxParagraph, type ExtractedDocxText, FOLIO_DOCUMENT_METADATA_PROPERTIES, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FOLIO_DOCUMENT_PRIVACY_TRANSFORMS, FOLIO_DOCX_CONFORMANCE_CHECKS, FOLIO_DOCX_CONFORMANCE_ISSUE_CODES, FOLIO_DOCX_CONFORMANCE_PROFILE, FOLIO_DOCX_CONFORMANCE_REPORT_VERSION, FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS, FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES, FOLIO_DOCX_PACKAGE_INSPECTION_VERSION, FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE, FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION, FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS, FOLIO_DOCX_XML_PATCH_PROPOSAL_ISSUE_CODES, FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE, FOLIO_DOCX_XML_PATCH_PROPOSAL_VERSION, FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FOLIO_VERSION_COMPARISON_PRIVACY_TRANSFORMS, FOLIO_VERSION_COMPARISON_SCOPES, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditSnapshot, type FolioAIInlineFormatting, type FolioAITextRangeHandle, type FolioApplyDocumentOperationsOptions, type FolioApplyDocumentOperationsToStoryOptions, type FolioApplyOperationsOptions, type FolioBlockDiff, type FolioBlockId, type FolioCompareDocxVersionsOptions, type FolioDocumentMetadataProperty, type FolioDocumentMetadataValue, type FolioDocumentNavigationTarget, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationStatus, type FolioDocumentOperationStory, type FolioDocumentOperationType, type FolioDocumentOperationUndoFailureReason, type FolioDocumentOperationUndoHandle, type FolioDocumentOperationUndoResult, type FolioDocumentOutline, type FolioDocumentOutlineEntry, FolioDocumentPrivacyArchiveError, type FolioDocumentPrivacyOptions, type FolioDocumentPrivacyReport, type FolioDocumentPrivacyTransform, type FolioDocumentSection, type FolioDocumentSectionHandle, type FolioDocumentSectionReadResult, type FolioDocumentStory, type FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, type FolioDocxConformanceCheck, type FolioDocxConformanceCheckId, type FolioDocxConformanceCheckStatus, type FolioDocxConformanceIssue, type FolioDocxConformanceIssueCode, type FolioDocxConformanceReport, type FolioDocxConformanceStatus, type FolioDocxInspectedXmlPart, type FolioDocxPackageInspection, FolioDocxPackageInspectionError, type FolioDocxPackageInspectionErrorCode, type FolioDocxPackageInspectionLimits, type FolioDocxPackagePart, type FolioDocxPackagePartKind, type FolioDocxPreparedXmlReplacement, FolioDocxReviewer, type FolioDocxReviewerOptions, type FolioDocxXmlPatchApplication, FolioDocxXmlPatchApplicationError, type FolioDocxXmlPatchApplicationReceipt, type FolioDocxXmlPatchProposal, type FolioDocxXmlPatchProposalEvaluation, type FolioDocxXmlPatchProposalIssue, type FolioDocxXmlPatchProposalIssueCode, type FolioDocxXmlPatchProposalLimits, type FolioDocxXmlReplacement, type FolioEditableDocumentStoryHandle, type FolioFormatProperty, type FolioMetadataDiff, type FolioReadReviewedStoryOptions, type FolioResolveReviewedStoryOptions, type FolioResolvedReviewedView, type FolioReviewChange, type FolioReviewChangeFilter, type FolioReviewChangeKind, type FolioReviewComment, type FolioReviewCommentFilter, type FolioReviewCommentReply, type FolioReviewReplyInput, type FolioReviewedStory, type FolioReviewedView, type FolioStoryDiff, type FolioVersionBlockHandle, type FolioVersionComparisonPrivacyTransform, type FolioVersionComparisonScope, type FolioVersionDiff, type FolioVersionDiffPrivacyOptions, type FolioVersionDiffPrivacyReport, type FolioVersionDiffSegment, type FolioVersionDiffSummaryCounts, type GenerateRedlineDocxOptions, type GenerateRedlineDocxResult, type GenerateRedlineUnprocessedStory, type InspectDocxPackageOptions, InvalidFolioDocumentOperationBatchError, InvalidFolioDocumentPrivacyOptionsError, InvalidFolioDocxXmlPatchProposalError, InvalidFolioDocxXmlPatchProposalOptionsError, InvalidFolioVersionComparisonOptionsError, InvalidGenerateRedlineDocxOptionsError, type RewriteDocxMetadataPrivacyResult, STELLA_STYLE_SET_NAME, UnsupportedFolioDocumentOperationVersionError, UnsupportedFolioDocxXmlPatchApplicationProfileError, UnsupportedFolioReviewedViewError, type ValidateDocxConformanceOptions, applyDocxXmlPatchProposal, applyFolioAIEditsToBuffer, applyFolioVersionDiffPrivacy, assertSupportedFolioDocumentOperationVersion, compareDocxVersions, createDocx, createEmptyDocument, createFolioAITextRangeHandle, createStellaStyleDocumentPreset, createStellaStyleSet, deriveBlockId, docxToMarkdown, ensureParaIds, evaluateDocxXmlPatchProposal, extractDocumentStyleSet, extractDocumentStyleSetFromDocx, extractDocxText, generateRedlineDocx, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioDocumentOutline, getFolioParaIdFromBlockId, hashFolioAIBlockText, inspectDocumentStyles, inspectDocumentStylesFromDocx, inspectDocxPackage, isFolioBlockId, isFolioDocumentOperationModeSupported, isFolioDocumentPrivacyTransform, isFolioResolvedReviewedView, isFolioReviewedView, isFolioVersionComparisonPrivacyTransform, isFolioVersionComparisonScope, isSequentialFolioBlockId, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, parseFolioDocxXmlPatchProposal, readFolioDocumentSection, replyToComment, rewriteDocxMetadataPrivacy, validateDocxConformance };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.15.12",
3
+ "version": "0.16.0",
4
4
  "description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
5
5
  "keywords": [
6
6
  "document-model",
@@ -112,7 +112,7 @@
112
112
  "@stll/template-conditions": "^0.1.0",
113
113
  "better-result": "2.10.0",
114
114
  "csstype": "^3.1.3",
115
- "dompurify": "^3.4.12",
115
+ "dompurify": "^3.4.13",
116
116
  "fast-xml-parser": "^5.9.3",
117
117
  "hyphen": "1.14.1",
118
118
  "jszip": "3.10.1",