@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.
- package/dist/ai-edits/apply.js +67 -2
- package/dist/ai-edits/headless.js +45 -33
- package/dist/docx/server/extractDocxText.d.ts +24 -1
- package/dist/docx/server/extractDocxText.js +192 -9
- package/dist/layout-bridge/convert/toFlowBlocks.js +5 -5
- package/dist/layout-bridge/dom/clickToPositionDom.d.ts +12 -2
- package/dist/layout-bridge/dom/clickToPositionDom.js +39 -14
- package/dist/layout-bridge/headerFooterLayout.js +1 -1
- package/dist/layout-painter/renderParagraph.js +4 -1
- package/dist/render-dom/RenderedDomContext.js +9 -8
- package/dist/server.d.ts +2 -2
- package/package.json +2 -2
package/dist/ai-edits/apply.js
CHANGED
|
@@ -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 = [
|
|
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 = [
|
|
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 {
|
|
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.
|
|
84
|
-
*
|
|
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
|
|
95
|
+
const ensureDeterministicParaIdsInDoc = (doc) => {
|
|
87
96
|
const seen = /* @__PURE__ */ new Set();
|
|
88
|
-
const updates = [];
|
|
89
97
|
let ordinal = 0;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
211
|
+
let tableCount = 0;
|
|
212
|
+
const pushProse = (paragraph) => {
|
|
66
213
|
const text = collectText(paragraph);
|
|
67
214
|
const entry = {
|
|
68
|
-
index: startIndex +
|
|
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.
|
|
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 &&
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|