web-doc 0.6.2 → 0.8.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/THIRD_PARTY_NOTICES.md +33 -1
- package/dist/adapters/docx-images.d.ts +13 -4
- package/dist/adapters/docx-images.js +3 -0
- package/dist/adapters/docx-paragraphs.d.ts +65 -0
- package/dist/adapters/docx-paragraphs.js +165 -0
- package/dist/adapters/docx-prepass.d.ts +17 -0
- package/dist/adapters/docx-prepass.js +230 -0
- package/dist/adapters/office.d.ts +31 -5
- package/dist/adapters/office.js +60 -29
- package/dist/adapters/pdf.d.ts +9 -0
- package/dist/adapters/pdf.js +10 -0
- package/dist/assets/pdfium/pdfium.wasm +0 -0
- package/dist/contracts.d.ts +53 -2
- package/dist/edit/ai/outline.d.ts +47 -0
- package/dist/edit/ai/outline.js +338 -0
- package/dist/edit/ai/targets.d.ts +16 -0
- package/dist/edit/ai/targets.js +309 -0
- package/dist/edit/ai/tools.d.ts +28 -0
- package/dist/edit/ai/tools.js +605 -0
- package/dist/edit/ai/types.d.ts +175 -0
- package/dist/edit/ai/types.js +1 -0
- package/dist/edit/assets.d.ts +23 -0
- package/dist/edit/assets.js +75 -0
- package/dist/edit/docx/elements.d.ts +6 -0
- package/dist/edit/docx/elements.js +90 -0
- package/dist/edit/docx/engine.d.ts +57 -0
- package/dist/edit/docx/engine.js +547 -0
- package/dist/edit/docx/handlers.d.ts +3 -0
- package/dist/edit/docx/handlers.js +15 -0
- package/dist/edit/docx/ids.d.ts +45 -0
- package/dist/edit/docx/ids.js +101 -0
- package/dist/edit/docx/model.d.ts +94 -0
- package/dist/edit/docx/model.js +350 -0
- package/dist/edit/docx/operations.d.ts +47 -0
- package/dist/edit/docx/operations.js +3 -0
- package/dist/edit/docx/provider.d.ts +16 -0
- package/dist/edit/docx/provider.js +37 -0
- package/dist/edit/docx/schemas.d.ts +6 -0
- package/dist/edit/docx/schemas.js +192 -0
- package/dist/edit/docx/session.d.ts +48 -0
- package/dist/edit/docx/session.js +467 -0
- package/dist/edit/docx/structure-ops.d.ts +6 -0
- package/dist/edit/docx/structure-ops.js +529 -0
- package/dist/edit/docx/style.d.ts +45 -0
- package/dist/edit/docx/style.js +375 -0
- package/dist/edit/docx/table-ops.d.ts +4 -0
- package/dist/edit/docx/table-ops.js +241 -0
- package/dist/edit/docx/text-ops.d.ts +40 -0
- package/dist/edit/docx/text-ops.js +491 -0
- package/dist/edit/docx/text.d.ts +34 -0
- package/dist/edit/docx/text.js +284 -0
- package/dist/edit/docx/tracked.d.ts +52 -0
- package/dist/edit/docx/tracked.js +347 -0
- package/dist/edit/docx/types.d.ts +206 -0
- package/dist/edit/docx/types.js +1 -0
- package/dist/edit/docx/write.d.ts +64 -0
- package/dist/edit/docx/write.js +375 -0
- package/dist/edit/engine.d.ts +128 -0
- package/dist/edit/engine.js +1 -0
- package/dist/edit/history.d.ts +77 -0
- package/dist/edit/history.js +123 -0
- package/dist/edit/ooxml/names.d.ts +20 -0
- package/dist/edit/ooxml/names.js +61 -0
- package/dist/edit/ooxml/opc.d.ts +50 -0
- package/dist/edit/ooxml/opc.js +150 -0
- package/dist/edit/ooxml/package.d.ts +82 -0
- package/dist/edit/ooxml/package.js +233 -0
- package/dist/edit/ooxml/patch.d.ts +51 -0
- package/dist/edit/ooxml/patch.js +250 -0
- package/dist/edit/ooxml/transaction.d.ts +39 -0
- package/dist/edit/ooxml/transaction.js +316 -0
- package/dist/edit/ooxml/worker.d.ts +8 -0
- package/dist/edit/ooxml/worker.js +12 -0
- package/dist/edit/ooxml/writer.d.ts +21 -0
- package/dist/edit/ooxml/writer.js +187 -0
- package/dist/edit/ooxml/xml.d.ts +74 -0
- package/dist/edit/ooxml/xml.js +451 -0
- package/dist/edit/ooxml/zip.d.ts +54 -0
- package/dist/edit/ooxml/zip.js +280 -0
- package/dist/edit/operations.d.ts +19 -0
- package/dist/edit/operations.js +137 -0
- package/dist/edit/pdf/engine/compact.d.ts +6 -0
- package/dist/edit/pdf/engine/compact.js +442 -0
- package/dist/edit/pdf/engine/document.d.ts +95 -0
- package/dist/edit/pdf/engine/document.js +868 -0
- package/dist/edit/pdf/engine/elements.d.ts +45 -0
- package/dist/edit/pdf/engine/elements.js +313 -0
- package/dist/edit/pdf/engine/existing-text.d.ts +4 -0
- package/dist/edit/pdf/engine/existing-text.js +424 -0
- package/dist/edit/pdf/engine/fonts.d.ts +78 -0
- package/dist/edit/pdf/engine/fonts.js +466 -0
- package/dist/edit/pdf/engine/geometry.d.ts +36 -0
- package/dist/edit/pdf/engine/geometry.js +97 -0
- package/dist/edit/pdf/engine/handler.d.ts +21 -0
- package/dist/edit/pdf/engine/handler.js +108 -0
- package/dist/edit/pdf/engine/images.d.ts +33 -0
- package/dist/edit/pdf/engine/images.js +188 -0
- package/dist/edit/pdf/engine/layout.d.ts +23 -0
- package/dist/edit/pdf/engine/layout.js +177 -0
- package/dist/edit/pdf/engine/operations.d.ts +67 -0
- package/dist/edit/pdf/engine/operations.js +3 -0
- package/dist/edit/pdf/engine/pages.d.ts +6 -0
- package/dist/edit/pdf/engine/pages.js +98 -0
- package/dist/edit/pdf/engine/pdfium.d.ts +195 -0
- package/dist/edit/pdf/engine/pdfium.js +249 -0
- package/dist/edit/pdf/engine/shapes.d.ts +4 -0
- package/dist/edit/pdf/engine/shapes.js +183 -0
- package/dist/edit/pdf/engine/tables.d.ts +44 -0
- package/dist/edit/pdf/engine/tables.js +315 -0
- package/dist/edit/pdf/engine/text-box.d.ts +71 -0
- package/dist/edit/pdf/engine/text-box.js +317 -0
- package/dist/edit/pdf/engine/text-layout.d.ts +28 -0
- package/dist/edit/pdf/engine/text-layout.js +67 -0
- package/dist/edit/pdf/engine/transform.d.ts +5 -0
- package/dist/edit/pdf/engine/transform.js +137 -0
- package/dist/edit/pdf/provider.d.ts +35 -0
- package/dist/edit/pdf/provider.js +92 -0
- package/dist/edit/pdf/range-map.d.ts +16 -0
- package/dist/edit/pdf/range-map.js +107 -0
- package/dist/edit/pdf/schemas.d.ts +11 -0
- package/dist/edit/pdf/schemas.js +291 -0
- package/dist/edit/pdf/selection.d.ts +5 -0
- package/dist/edit/pdf/selection.js +152 -0
- package/dist/edit/pdf/session.d.ts +63 -0
- package/dist/edit/pdf/session.js +312 -0
- package/dist/edit/pdf/types.d.ts +336 -0
- package/dist/edit/pdf/types.js +1 -0
- package/dist/edit/pptx/elements.d.ts +74 -0
- package/dist/edit/pptx/elements.js +301 -0
- package/dist/edit/pptx/engine.d.ts +37 -0
- package/dist/edit/pptx/engine.js +466 -0
- package/dist/edit/pptx/geometry.d.ts +72 -0
- package/dist/edit/pptx/geometry.js +193 -0
- package/dist/edit/pptx/handler.d.ts +7 -0
- package/dist/edit/pptx/handler.js +99 -0
- package/dist/edit/pptx/handlers.d.ts +3 -0
- package/dist/edit/pptx/handlers.js +21 -0
- package/dist/edit/pptx/image-table-ops.d.ts +5 -0
- package/dist/edit/pptx/image-table-ops.js +233 -0
- package/dist/edit/pptx/model.d.ts +69 -0
- package/dist/edit/pptx/model.js +170 -0
- package/dist/edit/pptx/operations.d.ts +50 -0
- package/dist/edit/pptx/operations.js +11 -0
- package/dist/edit/pptx/provider.d.ts +17 -0
- package/dist/edit/pptx/provider.js +40 -0
- package/dist/edit/pptx/schemas.d.ts +6 -0
- package/dist/edit/pptx/schemas.js +220 -0
- package/dist/edit/pptx/session.d.ts +54 -0
- package/dist/edit/pptx/session.js +145 -0
- package/dist/edit/pptx/shape-ops.d.ts +10 -0
- package/dist/edit/pptx/shape-ops.js +486 -0
- package/dist/edit/pptx/slide-ops.d.ts +6 -0
- package/dist/edit/pptx/slide-ops.js +353 -0
- package/dist/edit/pptx/style.d.ts +31 -0
- package/dist/edit/pptx/style.js +181 -0
- package/dist/edit/pptx/text-ops.d.ts +13 -0
- package/dist/edit/pptx/text-ops.js +267 -0
- package/dist/edit/pptx/text-write.d.ts +67 -0
- package/dist/edit/pptx/text-write.js +293 -0
- package/dist/edit/pptx/text.d.ts +41 -0
- package/dist/edit/pptx/text.js +99 -0
- package/dist/edit/pptx/types.d.ts +232 -0
- package/dist/edit/pptx/types.js +1 -0
- package/dist/edit/schema.d.ts +11 -0
- package/dist/edit/schema.js +275 -0
- package/dist/edit/session.d.ts +77 -0
- package/dist/edit/session.js +943 -0
- package/dist/edit/sessions.d.ts +8 -0
- package/dist/edit/sessions.js +1 -0
- package/dist/edit/types.d.ts +290 -0
- package/dist/edit/types.js +1 -0
- package/dist/edit/worker-engine.d.ts +31 -0
- package/dist/edit/worker-engine.js +91 -0
- package/dist/fonts/THIRD_PARTY_NOTICES.md +3 -0
- package/dist/fonts/manifest.json +6 -1
- package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
- package/dist/fonts.d.ts +2 -0
- package/dist/fonts.js +4 -0
- package/dist/fuzzy-alignment.d.ts +11 -4
- package/dist/fuzzy-alignment.js +3 -9
- package/dist/headless.d.ts +5 -1
- package/dist/headless.js +8 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +16 -2
- package/dist/limits.js +6 -0
- package/dist/ooxml-edit-worker.d.ts +1 -0
- package/dist/ooxml-edit-worker.js +8 -0
- package/dist/pdf-edit-worker.d.ts +1 -0
- package/dist/pdf-edit-worker.js +35 -0
- package/dist/spreadsheet-viewport.d.ts +5 -0
- package/dist/spreadsheet-viewport.js +11 -0
- package/dist/ui.js +7 -0
- package/dist/viewer.d.ts +6 -0
- package/dist/viewer.js +293 -19
- package/dist/viewport.d.ts +13 -0
- package/dist/viewport.js +126 -1
- package/dist/worker-protocol.d.ts +39 -2
- package/dist/workers/fuzzy-search-worker.js +1 -1
- package/dist/workers/ooxml-edit-worker.js +9212 -0
- package/dist/workers/pdf-edit-worker.js +10847 -0
- package/package.json +3 -3
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The outline is `getElements()` reshaped for a prompt: nested where the
|
|
3
|
+
* format nests (a table's cell paragraphs, a group's members, a paragraph's
|
|
4
|
+
* inline pictures), numbered in reading order, with a short text and a
|
|
5
|
+
* human label per node and nothing a model cannot use (geometry, styles).
|
|
6
|
+
* The description is the outline as text in a fixed line grammar, so a host
|
|
7
|
+
* pastes it into a prompt without a formatter of its own.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_TEXT_CHARS = 160;
|
|
10
|
+
const DEFAULT_DESCRIBE_CHARS = 50_000;
|
|
11
|
+
/** The outline of a session's document; one `getElements()` read, reshaped. */
|
|
12
|
+
export async function readOutline(session, limits, options = {}) {
|
|
13
|
+
const { pageRange, signal } = options;
|
|
14
|
+
// A single page is read as such: cheaper for formats that load pages on demand.
|
|
15
|
+
const query = pageRange && pageRange[0] === pageRange[1]
|
|
16
|
+
? { pageIndex: pageRange[0] }
|
|
17
|
+
: {};
|
|
18
|
+
const read = await session.getElements(query, signal ? { signal } : {});
|
|
19
|
+
const built = buildOutline(session.format, read.items, options, limits.maxOutlineNodes);
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
sessionId: read.sessionId,
|
|
22
|
+
revision: read.revision,
|
|
23
|
+
items: built.nodes,
|
|
24
|
+
nodeCount: built.nodeCount,
|
|
25
|
+
truncated: built.truncated,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** The description of a session's document: the outline rendered within a character budget. */
|
|
29
|
+
export async function readDescription(session, limits, options = {}) {
|
|
30
|
+
const { maxChars, ...outlineOptions } = options;
|
|
31
|
+
const outline = await readOutline(session, limits, outlineOptions);
|
|
32
|
+
const budget = Math.max(1, Math.min(maxChars ?? DEFAULT_DESCRIBE_CHARS, limits.maxDescribeChars));
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
sessionId: outline.sessionId,
|
|
35
|
+
revision: outline.revision,
|
|
36
|
+
item: renderDescription(session.format, session.state.pageCount, outline, budget, limits.maxOutlineNodes),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Nests elements by `parentId`, numbers them in reading order, keeps the
|
|
41
|
+
* nodes the options ask for (a container stays when a descendant is kept)
|
|
42
|
+
* and cuts the result at `maxNodes`. Ordinals are assigned before any
|
|
43
|
+
* filtering, so "3.2" names the same paragraph whatever the query.
|
|
44
|
+
*/
|
|
45
|
+
export function buildOutline(format, elements, options, maxNodes) {
|
|
46
|
+
const maxText = Math.max(0, options.maxTextChars ?? DEFAULT_TEXT_CHARS);
|
|
47
|
+
const ids = new Set(elements.map((element) => element.id));
|
|
48
|
+
const childrenOf = new Map();
|
|
49
|
+
const roots = [];
|
|
50
|
+
for (const element of elements) {
|
|
51
|
+
const parent = element.parentId;
|
|
52
|
+
if (parent === undefined || parent === element.id || !ids.has(parent)) {
|
|
53
|
+
roots.push(element);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const siblings = childrenOf.get(parent);
|
|
57
|
+
if (siblings)
|
|
58
|
+
siblings.push(element);
|
|
59
|
+
else
|
|
60
|
+
childrenOf.set(parent, [element]);
|
|
61
|
+
}
|
|
62
|
+
const visited = new Set();
|
|
63
|
+
const toNode = (element, ordinal) => {
|
|
64
|
+
visited.add(element.id);
|
|
65
|
+
const children = (childrenOf.get(element.id) ?? [])
|
|
66
|
+
.filter((child) => !visited.has(child.id))
|
|
67
|
+
.map((child, index) => toNode(child, `${ordinal}.${index + 1}`));
|
|
68
|
+
return nodeOf(format, element, ordinal, maxText, children);
|
|
69
|
+
};
|
|
70
|
+
const tree = roots.map((element, index) => toNode(element, `${index + 1}`));
|
|
71
|
+
// Elements whose parents form a cycle are never reached from a root;
|
|
72
|
+
// they are listed after the roots rather than dropped.
|
|
73
|
+
let extra = tree.length;
|
|
74
|
+
for (const element of elements)
|
|
75
|
+
if (!visited.has(element.id)) {
|
|
76
|
+
extra += 1;
|
|
77
|
+
tree.push(toNode(element, `${extra}`));
|
|
78
|
+
}
|
|
79
|
+
const kept = prune(tree, keepPredicate(options));
|
|
80
|
+
const cut = cutNodes(kept, maxNodes);
|
|
81
|
+
return {
|
|
82
|
+
nodes: cut.nodes,
|
|
83
|
+
nodeCount: cut.count,
|
|
84
|
+
truncated: cut.truncated,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function nodeOf(format, element, ordinal, maxText, children) {
|
|
88
|
+
const text = element.text;
|
|
89
|
+
const label = labelOf(format, element);
|
|
90
|
+
const extra = element;
|
|
91
|
+
return Object.freeze({
|
|
92
|
+
id: element.id,
|
|
93
|
+
ordinal,
|
|
94
|
+
kind: element.kind,
|
|
95
|
+
pageIndex: element.pageIndex,
|
|
96
|
+
...(text === undefined ? {} : { text: cutText(text, maxText) }),
|
|
97
|
+
textLength: text?.length ?? 0,
|
|
98
|
+
truncated: text !== undefined && text.length > maxText,
|
|
99
|
+
...(label === undefined ? {} : { label }),
|
|
100
|
+
...(extra.readOnlyReason === undefined
|
|
101
|
+
? {}
|
|
102
|
+
: { readOnlyReason: extra.readOnlyReason }),
|
|
103
|
+
...(extra.hidden ? { hidden: true } : {}),
|
|
104
|
+
operations: Object.freeze([...element.operations]),
|
|
105
|
+
...(children.length > 0 ? { children: Object.freeze(children) } : {}),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/** The first `max` code units of `text`, never ending inside a surrogate pair. */
|
|
109
|
+
function cutText(text, max) {
|
|
110
|
+
if (text.length <= max)
|
|
111
|
+
return text;
|
|
112
|
+
let end = max;
|
|
113
|
+
const last = text.charCodeAt(end - 1);
|
|
114
|
+
if (end > 0 && last >= 0xd800 && last <= 0xdbff)
|
|
115
|
+
end -= 1;
|
|
116
|
+
return text.slice(0, end);
|
|
117
|
+
}
|
|
118
|
+
function keepPredicate(options) {
|
|
119
|
+
const { pageRange, kinds } = options;
|
|
120
|
+
if (!pageRange && !kinds)
|
|
121
|
+
return undefined;
|
|
122
|
+
const wanted = kinds ? new Set(kinds) : undefined;
|
|
123
|
+
return (node) => (!wanted || wanted.has(node.kind)) &&
|
|
124
|
+
(!pageRange ||
|
|
125
|
+
(node.pageIndex >= pageRange[0] && node.pageIndex <= pageRange[1]));
|
|
126
|
+
}
|
|
127
|
+
/** Keeps the nodes `keep` accepts and every ancestor of one, in order. */
|
|
128
|
+
function prune(nodes, keep) {
|
|
129
|
+
if (!keep)
|
|
130
|
+
return nodes;
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const node of nodes) {
|
|
133
|
+
const children = node.children ? prune(node.children, keep) : [];
|
|
134
|
+
if (!keep(node) && children.length === 0)
|
|
135
|
+
continue;
|
|
136
|
+
out.push(children.length === node.children?.length
|
|
137
|
+
? node
|
|
138
|
+
: withChildren(node, children));
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
/** The first `max` nodes in pre-order; the rest are cut, subtrees included. */
|
|
143
|
+
function cutNodes(nodes, max) {
|
|
144
|
+
let count = 0;
|
|
145
|
+
let truncated = false;
|
|
146
|
+
const take = (list) => {
|
|
147
|
+
const out = [];
|
|
148
|
+
for (const node of list) {
|
|
149
|
+
if (count >= max) {
|
|
150
|
+
truncated = true;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
count += 1;
|
|
154
|
+
if (!node.children) {
|
|
155
|
+
out.push(node);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const children = take(node.children);
|
|
159
|
+
out.push(children.length === node.children.length
|
|
160
|
+
? node
|
|
161
|
+
: withChildren(node, children));
|
|
162
|
+
// A cut inside the children leaves nothing for the siblings.
|
|
163
|
+
if (truncated)
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
return Object.freeze(out);
|
|
167
|
+
};
|
|
168
|
+
const kept = take(nodes);
|
|
169
|
+
return { nodes: kept, count, truncated };
|
|
170
|
+
}
|
|
171
|
+
/** `node` with `children`, the key left out when there are none. */
|
|
172
|
+
function withChildren(node, children) {
|
|
173
|
+
const { children: _dropped, ...rest } = node;
|
|
174
|
+
return Object.freeze(children.length > 0 ? { ...rest, children: Object.freeze(children) } : rest);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* What a person calls the element, when the format knows: a shape's name
|
|
178
|
+
* or placeholder in a deck, a paragraph's style in a Word document, a
|
|
179
|
+
* table's size everywhere.
|
|
180
|
+
*/
|
|
181
|
+
export function labelOf(format, element) {
|
|
182
|
+
const known = element;
|
|
183
|
+
const size = known.table ? tableSize(known.table.rows) : undefined;
|
|
184
|
+
if (format === "pptx") {
|
|
185
|
+
const name = known.name?.trim();
|
|
186
|
+
if (name)
|
|
187
|
+
return size ? `${name} (${size})` : name;
|
|
188
|
+
if (known.placeholder)
|
|
189
|
+
return placeholderLabel(known.placeholder.type);
|
|
190
|
+
}
|
|
191
|
+
if (format === "docx" && element.kind === "paragraph") {
|
|
192
|
+
const style = known.paragraphStyle;
|
|
193
|
+
const styleId = style?.styleId;
|
|
194
|
+
const name = styleId && styleId !== "Normal" ? styleName(styleId) : undefined;
|
|
195
|
+
if (name)
|
|
196
|
+
return name;
|
|
197
|
+
if (style?.numbering)
|
|
198
|
+
return `List item (level ${style.numbering.level + 1})`;
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
if (size)
|
|
202
|
+
return `Table (${size})`;
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
function tableSize(rows) {
|
|
206
|
+
const columns = rows.reduce((max, row) => Math.max(max, row.length), 0);
|
|
207
|
+
return `${rows.length}×${columns}`;
|
|
208
|
+
}
|
|
209
|
+
const PLACEHOLDER_LABELS = {
|
|
210
|
+
title: "Title",
|
|
211
|
+
ctrTitle: "Title",
|
|
212
|
+
subTitle: "Subtitle",
|
|
213
|
+
body: "Body",
|
|
214
|
+
obj: "Content",
|
|
215
|
+
pic: "Picture",
|
|
216
|
+
tbl: "Table",
|
|
217
|
+
chart: "Chart",
|
|
218
|
+
dgm: "Diagram",
|
|
219
|
+
media: "Media",
|
|
220
|
+
clipArt: "Clip art",
|
|
221
|
+
dt: "Date",
|
|
222
|
+
ftr: "Footer",
|
|
223
|
+
sldNum: "Slide number",
|
|
224
|
+
hdr: "Header",
|
|
225
|
+
sldImg: "Slide image",
|
|
226
|
+
};
|
|
227
|
+
function placeholderLabel(type) {
|
|
228
|
+
return PLACEHOLDER_LABELS[type] ?? `${type} placeholder`;
|
|
229
|
+
}
|
|
230
|
+
/** "Heading1" → "Heading 1", "ListParagraph" → "List Paragraph". */
|
|
231
|
+
function styleName(styleId) {
|
|
232
|
+
return styleId
|
|
233
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
234
|
+
.replace(/([A-Za-z])(\d)/g, "$1 $2");
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The description's grammar, one line per node:
|
|
238
|
+
*
|
|
239
|
+
* <format>: <n> pages|slides, <m> elements
|
|
240
|
+
* [<id>] page <p> <kind> "<label>" (<flags>): <text>…
|
|
241
|
+
* [<child id>] page <p> <kind>: <text>
|
|
242
|
+
*
|
|
243
|
+
* The page is left out while the node has none, the label and the flags
|
|
244
|
+
* while the node has none, the text while it is empty. Tabs in the text
|
|
245
|
+
* become ` | `, line breaks ` ⏎ `; a cut text ends in `…`. Children are
|
|
246
|
+
* indented two spaces per level. The budget cuts whole lines and the last
|
|
247
|
+
* line then says how many were left out.
|
|
248
|
+
*/
|
|
249
|
+
export function renderDescription(format, pageCount, outline, maxChars, maxNodes) {
|
|
250
|
+
const nodes = "items" in outline ? outline.items : outline.nodes;
|
|
251
|
+
const noun = format === "pptx" ? "slide" : "page";
|
|
252
|
+
const lines = [
|
|
253
|
+
`${format}: ${count(pageCount, noun)}, ${count(outline.nodeCount, "element")}`,
|
|
254
|
+
];
|
|
255
|
+
const push = (node, depth) => {
|
|
256
|
+
lines.push(`${" ".repeat(depth)}${describeNode(node, noun)}`);
|
|
257
|
+
for (const child of node.children ?? [])
|
|
258
|
+
push(child, depth + 1);
|
|
259
|
+
};
|
|
260
|
+
for (const node of nodes)
|
|
261
|
+
push(node, 0);
|
|
262
|
+
// Whole lines within the budget; the last line then says what was left
|
|
263
|
+
// out and makes room for itself. Prefix sums keep this linear in the
|
|
264
|
+
// number of lines: `joined[k]` is the length of the first `k` lines joined.
|
|
265
|
+
const joined = new Array(lines.length + 1);
|
|
266
|
+
joined[0] = 0;
|
|
267
|
+
for (let index = 0; index < lines.length; index += 1)
|
|
268
|
+
joined[index + 1] =
|
|
269
|
+
joined[index] + lines[index].length + (index > 0 ? 1 : 0);
|
|
270
|
+
const tailOf = (keep) => {
|
|
271
|
+
const left = lines.length - keep;
|
|
272
|
+
if (left > 0)
|
|
273
|
+
return `… (${count(left, "more line")})`;
|
|
274
|
+
if (outline.truncated)
|
|
275
|
+
return `… (outline cut at ${count(maxNodes, "node")})`;
|
|
276
|
+
return undefined;
|
|
277
|
+
};
|
|
278
|
+
const lengthOf = (keep) => {
|
|
279
|
+
const tail = tailOf(keep);
|
|
280
|
+
const body = joined[keep];
|
|
281
|
+
if (tail === undefined)
|
|
282
|
+
return body;
|
|
283
|
+
return body === 0 ? tail.length : body + 1 + tail.length;
|
|
284
|
+
};
|
|
285
|
+
let keep = lines.length;
|
|
286
|
+
while (keep > 0 && lengthOf(keep) > maxChars)
|
|
287
|
+
keep -= 1;
|
|
288
|
+
let text;
|
|
289
|
+
let truncated = outline.truncated || keep < lines.length;
|
|
290
|
+
if (keep === 0 && lengthOf(0) > maxChars) {
|
|
291
|
+
// Not even the header fits: hand back what does.
|
|
292
|
+
text = lines[0].slice(0, maxChars);
|
|
293
|
+
truncated = true;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const tail = tailOf(keep);
|
|
297
|
+
const body = lines.slice(0, keep).join("\n");
|
|
298
|
+
text = tail === undefined ? body : body ? `${body}\n${tail}` : tail;
|
|
299
|
+
}
|
|
300
|
+
return Object.freeze({
|
|
301
|
+
format,
|
|
302
|
+
pageCount,
|
|
303
|
+
elementCount: outline.nodeCount,
|
|
304
|
+
text,
|
|
305
|
+
truncated,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function describeNode(node, noun) {
|
|
309
|
+
const parts = [`[${node.id}]`];
|
|
310
|
+
if (node.pageIndex >= 0)
|
|
311
|
+
parts.push(`${noun} ${node.pageIndex + 1}`);
|
|
312
|
+
parts.push(node.kind);
|
|
313
|
+
if (node.label !== undefined)
|
|
314
|
+
parts.push(`"${node.label.replaceAll('"', "'")}"`);
|
|
315
|
+
const flags = [
|
|
316
|
+
...(node.readOnlyReason ? [`read-only: ${node.readOnlyReason}`] : []),
|
|
317
|
+
...(node.hidden ? ["hidden"] : []),
|
|
318
|
+
];
|
|
319
|
+
if (flags.length > 0)
|
|
320
|
+
parts.push(`(${flags.join(", ")})`);
|
|
321
|
+
let line = parts.join(" ");
|
|
322
|
+
if (node.text)
|
|
323
|
+
line += `: ${foldText(node.text)}${node.truncated ? "…" : ""}`;
|
|
324
|
+
else if (node.truncated)
|
|
325
|
+
line += ": …";
|
|
326
|
+
return line;
|
|
327
|
+
}
|
|
328
|
+
/** One line of text: tabs as cell separators, breaks marked, other controls blanked. */
|
|
329
|
+
export function foldText(text) {
|
|
330
|
+
return text
|
|
331
|
+
.replaceAll("\r\n", "\n")
|
|
332
|
+
.replaceAll("\t", " | ")
|
|
333
|
+
.replaceAll(/[\n\r\u2028\u2029\v\f]/g, " ⏎ ")
|
|
334
|
+
.replaceAll(/[\u0000-\u0008\u000e-\u001f\u007f]/g, " ");
|
|
335
|
+
}
|
|
336
|
+
function count(value, noun) {
|
|
337
|
+
return `${value} ${noun}${value === 1 ? "" : "s"}`;
|
|
338
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EditElement, EditFindOptions, ElementQuery, ReadOptions, ReadResult, TextTarget } from "../types.js";
|
|
2
|
+
import type { TargetCandidate, TargetQuery } from "./types.js";
|
|
3
|
+
/** What resolution needs of a session: its elements and its exact text search. */
|
|
4
|
+
export interface TargetSource {
|
|
5
|
+
getElements(query?: ElementQuery, options?: ReadOptions): Promise<ReadResult<EditElement>>;
|
|
6
|
+
findText(query: string, options?: EditFindOptions): Promise<ReadResult<TextTarget>>;
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveTargets(session: TargetSource, query: TargetQuery, options?: ReadOptions): Promise<ReadResult<TargetCandidate>>;
|
|
9
|
+
/** Whitespace runs to one space, quotes and dashes to ASCII, NFKC, case folded; offsets kept. */
|
|
10
|
+
export declare function foldWithMap(text: string): {
|
|
11
|
+
readonly text: string;
|
|
12
|
+
readonly starts: readonly number[];
|
|
13
|
+
readonly ends: readonly number[];
|
|
14
|
+
};
|
|
15
|
+
/** The matched text with a little context on each side, on one line. */
|
|
16
|
+
export declare function snippetOf(text: string, start: number, end: number): string;
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { alignFuzzyPassage } from "../../fuzzy-alignment.js";
|
|
2
|
+
import { DEFAULT_FUZZY_SEARCH_OPTIONS, FuzzyPageIndex, } from "../../fuzzy-search.js";
|
|
3
|
+
import { graphemeSegments, normalizeSearchText } from "../../search-text.js";
|
|
4
|
+
import { foldText } from "./outline.js";
|
|
5
|
+
/*
|
|
6
|
+
* A model names what it wants to edit by quoting it ("the sentence that
|
|
7
|
+
* starts with…"), by citing it with a page, or by kind ("the first table on
|
|
8
|
+
* slide 3"). Resolution turns that into element ids and ranges in named
|
|
9
|
+
* steps, so a host knows how much to trust a candidate: the engine's exact
|
|
10
|
+
* search first, then a match with whitespace, case, quotes and compatibility
|
|
11
|
+
* forms folded, then the viewer's fuzzy citation matching over the
|
|
12
|
+
* elements' text, and a kind lookup when there is no text at all.
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_MAX_RESULTS = 5;
|
|
15
|
+
const SNIPPET_CONTEXT = 40;
|
|
16
|
+
/** Fuzzy candidates score from this down to 0.5 at the loosest accepted edit ratio. */
|
|
17
|
+
const FUZZY_TOP = 0.85;
|
|
18
|
+
export async function resolveTargets(session, query, options = {}) {
|
|
19
|
+
const needle = (query.text ?? query.citation?.text ?? "").trim();
|
|
20
|
+
const hint = query.citation?.pageNumber !== undefined &&
|
|
21
|
+
Number.isFinite(query.citation.pageNumber)
|
|
22
|
+
? Math.max(0, Math.trunc(query.citation.pageNumber) - 1)
|
|
23
|
+
: undefined;
|
|
24
|
+
const limit = Math.max(1, Math.trunc(query.maxResults ?? DEFAULT_MAX_RESULTS));
|
|
25
|
+
const readOptions = options.signal ? { signal: options.signal } : {};
|
|
26
|
+
const elementQuery = query.pageIndex === undefined ? {} : { pageIndex: query.pageIndex };
|
|
27
|
+
// The two reads queue one after the other; a change landing between them
|
|
28
|
+
// would make the elements stale, so they are read again until they agree.
|
|
29
|
+
let elements;
|
|
30
|
+
let found;
|
|
31
|
+
for (let attempt = 0;; attempt += 1) {
|
|
32
|
+
elements = await session.getElements(elementQuery, readOptions);
|
|
33
|
+
found = needle
|
|
34
|
+
? await session.findText(needle, {
|
|
35
|
+
...readOptions,
|
|
36
|
+
...(query.pageIndex === undefined
|
|
37
|
+
? {}
|
|
38
|
+
: { pageRange: [query.pageIndex, query.pageIndex] }),
|
|
39
|
+
})
|
|
40
|
+
: undefined;
|
|
41
|
+
if (!found || found.revision === elements.revision || attempt >= 2)
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
const envelope = {
|
|
45
|
+
sessionId: (found ?? elements).sessionId,
|
|
46
|
+
revision: (found ?? elements).revision,
|
|
47
|
+
};
|
|
48
|
+
const allowed = allowedElements(elements.items, query);
|
|
49
|
+
const byId = new Map(allowed.map((element) => [element.id, element]));
|
|
50
|
+
let candidates = [];
|
|
51
|
+
if (needle && found) {
|
|
52
|
+
candidates = exactCandidates(found.items, byId);
|
|
53
|
+
if (candidates.length === 0) {
|
|
54
|
+
const corpus = buildCorpus(allowed);
|
|
55
|
+
candidates = normalizedCandidates(corpus, needle);
|
|
56
|
+
if (candidates.length === 0)
|
|
57
|
+
candidates = fuzzyCandidates(corpus, needle);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (!needle && query.kinds && query.kinds.length > 0) {
|
|
61
|
+
candidates = allowed.map((element) => ({
|
|
62
|
+
elementId: element.id,
|
|
63
|
+
pageIndex: element.pageIndex,
|
|
64
|
+
score: 0.5,
|
|
65
|
+
reason: "kind-only",
|
|
66
|
+
snippet: snippetOf(element.text ?? "", 0, 0),
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
if (hint !== undefined)
|
|
70
|
+
candidates = candidates
|
|
71
|
+
.map((candidate, index) => ({ candidate, index }))
|
|
72
|
+
.sort((a, b) => distance(a.candidate.pageIndex, hint) -
|
|
73
|
+
distance(b.candidate.pageIndex, hint) || a.index - b.index)
|
|
74
|
+
.map((entry) => entry.candidate);
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
...envelope,
|
|
77
|
+
items: Object.freeze(candidates.slice(0, limit).map((candidate) => Object.freeze(candidate))),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function distance(pageIndex, hint) {
|
|
81
|
+
return pageIndex < 0 ? Number.POSITIVE_INFINITY : Math.abs(pageIndex - hint);
|
|
82
|
+
}
|
|
83
|
+
/** The elements a query may name: by kind, inside `within`, on the page. */
|
|
84
|
+
function allowedElements(elements, query) {
|
|
85
|
+
let allowed = elements;
|
|
86
|
+
if (query.within !== undefined) {
|
|
87
|
+
const inside = new Set([query.within]);
|
|
88
|
+
// Children follow their parents in reading order, so one pass closes
|
|
89
|
+
// the set; a second catches a child listed before its parent.
|
|
90
|
+
for (let pass = 0; pass < 2; pass += 1)
|
|
91
|
+
for (const element of elements)
|
|
92
|
+
if (element.parentId !== undefined && inside.has(element.parentId))
|
|
93
|
+
inside.add(element.id);
|
|
94
|
+
allowed = allowed.filter((element) => inside.has(element.id));
|
|
95
|
+
}
|
|
96
|
+
if (query.kinds) {
|
|
97
|
+
const kinds = new Set(query.kinds);
|
|
98
|
+
allowed = allowed.filter((element) => kinds.has(element.kind));
|
|
99
|
+
}
|
|
100
|
+
return allowed;
|
|
101
|
+
}
|
|
102
|
+
function exactCandidates(targets, byId) {
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const target of targets) {
|
|
105
|
+
if (target.elementIds.length === 0)
|
|
106
|
+
continue;
|
|
107
|
+
if (!target.elementIds.every((id) => byId.has(id)))
|
|
108
|
+
continue;
|
|
109
|
+
const first = target.ranges[0];
|
|
110
|
+
const last = target.ranges.at(-1);
|
|
111
|
+
const element = byId.get(target.elementIds[0]);
|
|
112
|
+
const range = first && last ? { start: first.start, end: last.end } : undefined;
|
|
113
|
+
out.push({
|
|
114
|
+
elementId: element.id,
|
|
115
|
+
...(range && !coversWhole(range, element) ? { range } : {}),
|
|
116
|
+
pageIndex: target.pageIndex,
|
|
117
|
+
score: 1,
|
|
118
|
+
reason: "exact",
|
|
119
|
+
snippet: range &&
|
|
120
|
+
element.text !== undefined &&
|
|
121
|
+
range.start.elementId === element.id
|
|
122
|
+
? snippetOf(element.text, range.start.offset, range.end.elementId === element.id
|
|
123
|
+
? range.end.offset
|
|
124
|
+
: element.text.length)
|
|
125
|
+
: foldText(target.text),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
function coversWhole(range, element) {
|
|
131
|
+
return (range.start.elementId === element.id &&
|
|
132
|
+
range.end.elementId === element.id &&
|
|
133
|
+
range.start.offset === 0 &&
|
|
134
|
+
range.end.offset === (element.text?.length ?? 0));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The elements' text in reading order, cut into chunks that never span a
|
|
138
|
+
* page. A container whose children carry text (a Word table, whose cells are
|
|
139
|
+
* paragraphs) contributes nothing itself, so a match names the cell.
|
|
140
|
+
*/
|
|
141
|
+
function buildCorpus(elements) {
|
|
142
|
+
const textChildren = new Set();
|
|
143
|
+
for (const element of elements)
|
|
144
|
+
if (element.parentId !== undefined && element.text)
|
|
145
|
+
textChildren.add(element.parentId);
|
|
146
|
+
const chunks = [];
|
|
147
|
+
let pageIndex;
|
|
148
|
+
let text = "";
|
|
149
|
+
let segments = [];
|
|
150
|
+
const flush = () => {
|
|
151
|
+
if (segments.length > 0 && pageIndex !== undefined)
|
|
152
|
+
chunks.push({ pageIndex, text, segments });
|
|
153
|
+
text = "";
|
|
154
|
+
segments = [];
|
|
155
|
+
};
|
|
156
|
+
const budget = DEFAULT_FUZZY_SEARCH_OPTIONS.maxPageTextLength;
|
|
157
|
+
for (const element of elements) {
|
|
158
|
+
if (!element.text || textChildren.has(element.id))
|
|
159
|
+
continue;
|
|
160
|
+
if (pageIndex !== element.pageIndex ||
|
|
161
|
+
text.length + 1 + element.text.length > budget) {
|
|
162
|
+
flush();
|
|
163
|
+
pageIndex = element.pageIndex;
|
|
164
|
+
}
|
|
165
|
+
const start = text.length === 0 ? 0 : text.length + 1;
|
|
166
|
+
text = text.length === 0 ? element.text : `${text} ${element.text}`;
|
|
167
|
+
segments.push({ element, start, end: start + element.text.length });
|
|
168
|
+
}
|
|
169
|
+
flush();
|
|
170
|
+
return chunks;
|
|
171
|
+
}
|
|
172
|
+
/** Whitespace runs to one space, quotes and dashes to ASCII, NFKC, case folded; offsets kept. */
|
|
173
|
+
export function foldWithMap(text) {
|
|
174
|
+
const out = [];
|
|
175
|
+
const starts = [];
|
|
176
|
+
const ends = [];
|
|
177
|
+
let spaceStart = -1;
|
|
178
|
+
let spaceEnd = -1;
|
|
179
|
+
for (const segment of graphemeSegments(text)) {
|
|
180
|
+
const value = foldPunctuation(normalizeSearchText(segment.value));
|
|
181
|
+
if (value.length === 0)
|
|
182
|
+
continue;
|
|
183
|
+
if (/^\s+$/u.test(value)) {
|
|
184
|
+
if (out.length === 0)
|
|
185
|
+
continue;
|
|
186
|
+
if (spaceStart < 0)
|
|
187
|
+
spaceStart = segment.start;
|
|
188
|
+
spaceEnd = segment.end;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (spaceStart >= 0) {
|
|
192
|
+
out.push(" ");
|
|
193
|
+
starts.push(spaceStart);
|
|
194
|
+
ends.push(spaceEnd);
|
|
195
|
+
spaceStart = -1;
|
|
196
|
+
}
|
|
197
|
+
for (const char of value) {
|
|
198
|
+
out.push(char);
|
|
199
|
+
starts.push(segment.start);
|
|
200
|
+
ends.push(segment.end);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { text: out.join(""), starts, ends };
|
|
204
|
+
}
|
|
205
|
+
const PUNCTUATION = {
|
|
206
|
+
"‘": "'",
|
|
207
|
+
"’": "'",
|
|
208
|
+
"‚": "'",
|
|
209
|
+
"‛": "'",
|
|
210
|
+
"′": "'",
|
|
211
|
+
"“": '"',
|
|
212
|
+
"”": '"',
|
|
213
|
+
"„": '"',
|
|
214
|
+
"‟": '"',
|
|
215
|
+
"″": '"',
|
|
216
|
+
"«": '"',
|
|
217
|
+
"»": '"',
|
|
218
|
+
"‐": "-",
|
|
219
|
+
"‑": "-",
|
|
220
|
+
"‒": "-",
|
|
221
|
+
"–": "-",
|
|
222
|
+
"—": "-",
|
|
223
|
+
"―": "-",
|
|
224
|
+
"−": "-",
|
|
225
|
+
"…": "...",
|
|
226
|
+
" ": " ",
|
|
227
|
+
};
|
|
228
|
+
function foldPunctuation(value) {
|
|
229
|
+
let out = "";
|
|
230
|
+
for (const char of value)
|
|
231
|
+
out += PUNCTUATION[char] ?? char;
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
function normalizedCandidates(corpus, needle) {
|
|
235
|
+
const pattern = foldWithMap(needle).text;
|
|
236
|
+
if (pattern.length === 0)
|
|
237
|
+
return [];
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const chunk of corpus) {
|
|
240
|
+
const folded = foldWithMap(chunk.text);
|
|
241
|
+
let from = 0;
|
|
242
|
+
while (from <= folded.text.length - pattern.length) {
|
|
243
|
+
const at = folded.text.indexOf(pattern, from);
|
|
244
|
+
if (at < 0)
|
|
245
|
+
break;
|
|
246
|
+
const start = folded.starts[at];
|
|
247
|
+
const end = folded.ends[at + pattern.length - 1];
|
|
248
|
+
const candidate = candidateOf(chunk, start, end, 0.9, "normalized");
|
|
249
|
+
if (candidate)
|
|
250
|
+
out.push(candidate);
|
|
251
|
+
from = at + Math.max(1, pattern.length);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
function fuzzyCandidates(corpus, needle) {
|
|
257
|
+
if (corpus.length === 0)
|
|
258
|
+
return [];
|
|
259
|
+
const fuzzy = DEFAULT_FUZZY_SEARCH_OPTIONS;
|
|
260
|
+
const pattern = needle.slice(0, fuzzy.maxQueryLength);
|
|
261
|
+
const pages = corpus.map((chunk, index) => ({
|
|
262
|
+
pageIndex: index,
|
|
263
|
+
text: chunk.text,
|
|
264
|
+
}));
|
|
265
|
+
const index = new FuzzyPageIndex(pages, fuzzy, false);
|
|
266
|
+
const out = [];
|
|
267
|
+
for (const match of index.search(pattern, fuzzy.maxScore)) {
|
|
268
|
+
const chunk = corpus[match.pageIndex];
|
|
269
|
+
const passage = alignFuzzyPassage(chunk.text, pattern, false, fuzzy.maxScore);
|
|
270
|
+
if (!passage)
|
|
271
|
+
continue;
|
|
272
|
+
const ratio = passage.length === 0 ? 0 : passage.cost / passage.length;
|
|
273
|
+
const score = FUZZY_TOP - (FUZZY_TOP - 0.5) * Math.min(1, ratio / fuzzy.maxScore);
|
|
274
|
+
const candidate = candidateOf(chunk, passage.start, passage.end, Math.round(score * 100) / 100, "fuzzy");
|
|
275
|
+
if (candidate)
|
|
276
|
+
out.push(candidate);
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
/** The candidate for a span of a chunk: the first element it touches, with a range unless it covers the whole element. */
|
|
281
|
+
function candidateOf(chunk, start, end, score, reason) {
|
|
282
|
+
const touched = chunk.segments.filter((segment) => segment.end > start && segment.start < end);
|
|
283
|
+
const first = touched[0];
|
|
284
|
+
const last = touched.at(-1);
|
|
285
|
+
if (!first || !last)
|
|
286
|
+
return undefined;
|
|
287
|
+
const startOffset = Math.max(0, start - first.start);
|
|
288
|
+
const endOffset = Math.min(last.end, end) - last.start;
|
|
289
|
+
const range = {
|
|
290
|
+
start: { elementId: first.element.id, offset: startOffset },
|
|
291
|
+
end: { elementId: last.element.id, offset: endOffset },
|
|
292
|
+
};
|
|
293
|
+
return {
|
|
294
|
+
elementId: first.element.id,
|
|
295
|
+
...(coversWhole(range, first.element) ? {} : { range }),
|
|
296
|
+
pageIndex: first.element.pageIndex,
|
|
297
|
+
score,
|
|
298
|
+
reason,
|
|
299
|
+
snippet: snippetOf(chunk.text, start, end),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/** The matched text with a little context on each side, on one line. */
|
|
303
|
+
export function snippetOf(text, start, end) {
|
|
304
|
+
const from = Math.max(0, start - SNIPPET_CONTEXT);
|
|
305
|
+
const to = Math.min(text.length, end + SNIPPET_CONTEXT);
|
|
306
|
+
const head = from > 0 ? "…" : "";
|
|
307
|
+
const tail = to < text.length ? "…" : "";
|
|
308
|
+
return `${head}${foldText(text.slice(from, to))}${tail}`;
|
|
309
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ApplyOptions, EditableFormat, EditElement, EditOperation, EditReceipt, EditState, HistoryOptions, OperationSchemaSet, ReadItem, ReadOptions, ReadResult } from "../types.js";
|
|
2
|
+
import type { DescribeOptions, DocumentDescription, EditCheckpoint, OutlineOptions, OutlineResult, TargetCandidate, TargetQuery, ToolCall, ToolCallOptions, ToolResult, ToolSet } from "./types.js";
|
|
3
|
+
export declare const TOOL_SET_VERSION = 1;
|
|
4
|
+
/** What the tools need of a session. */
|
|
5
|
+
export interface ToolSource {
|
|
6
|
+
readonly format: EditableFormat;
|
|
7
|
+
readonly state: EditState;
|
|
8
|
+
readonly schemas: OperationSchemaSet;
|
|
9
|
+
describe(options?: DescribeOptions): Promise<ReadItem<DocumentDescription>>;
|
|
10
|
+
getOutline(options?: OutlineOptions): Promise<OutlineResult>;
|
|
11
|
+
resolveTargets(query: TargetQuery, options?: ReadOptions): Promise<ReadResult<TargetCandidate>>;
|
|
12
|
+
getElement(id: string, options?: ReadOptions): Promise<ReadItem<EditElement>>;
|
|
13
|
+
applyJson(operations: readonly EditOperation[], options?: ApplyOptions): Promise<EditReceipt>;
|
|
14
|
+
undo(options?: HistoryOptions): Promise<EditReceipt>;
|
|
15
|
+
createCheckpoint(label?: string): Promise<EditCheckpoint>;
|
|
16
|
+
listCheckpoints(): readonly EditCheckpoint[];
|
|
17
|
+
restoreCheckpoint(id: string, options?: HistoryOptions): Promise<EditReceipt>;
|
|
18
|
+
}
|
|
19
|
+
/** The tool definitions of a format, built from its operation schemas. */
|
|
20
|
+
export declare function buildToolSet(format: EditableFormat, schemas: OperationSchemaSet): ToolSet;
|
|
21
|
+
/** Runs one tool call on a session. */
|
|
22
|
+
export declare function callTool(session: ToolSource, tools: ToolSet, call: ToolCall, options?: ToolCallOptions): Promise<ToolResult>;
|
|
23
|
+
/**
|
|
24
|
+
* What a batch did, in the format's words: one sentence per operation with
|
|
25
|
+
* the id it created when the receipt names one per creating operation,
|
|
26
|
+
* then what else the receipt reports.
|
|
27
|
+
*/
|
|
28
|
+
export declare function describeReceipt(format: EditableFormat, operations: readonly EditOperation[], receipt: EditReceipt): string;
|