web-doc 0.7.0 → 0.9.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 +10 -3
- package/dist/contracts.d.ts +6 -0
- 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 +607 -0
- package/dist/edit/ai/types.d.ts +175 -0
- package/dist/edit/ai/types.js +1 -0
- package/dist/edit/docx/elements.js +19 -1
- package/dist/edit/docx/engine.d.ts +10 -4
- package/dist/edit/docx/engine.js +80 -6
- package/dist/edit/docx/model.d.ts +2 -0
- package/dist/edit/docx/model.js +11 -0
- package/dist/edit/docx/operations.d.ts +10 -0
- package/dist/edit/docx/provider.d.ts +4 -1
- package/dist/edit/docx/provider.js +3 -0
- package/dist/edit/docx/session.d.ts +12 -1
- package/dist/edit/docx/session.js +41 -0
- package/dist/edit/docx/structure-ops.js +39 -4
- package/dist/edit/docx/table-ops.js +26 -4
- package/dist/edit/docx/text-ops.d.ts +22 -0
- package/dist/edit/docx/text-ops.js +49 -16
- package/dist/edit/docx/text.js +21 -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 +24 -1
- package/dist/edit/docx/write.d.ts +19 -5
- package/dist/edit/docx/write.js +31 -8
- package/dist/edit/engine.d.ts +14 -3
- package/dist/edit/history.d.ts +27 -3
- package/dist/edit/history.js +29 -7
- package/dist/edit/pdf/engine/document.js +48 -3
- package/dist/edit/pdf/engine/elements.d.ts +6 -0
- package/dist/edit/pdf/engine/fonts.js +13 -10
- package/dist/edit/pdf/engine/pages.js +9 -1
- package/dist/edit/pdf/range-map.d.ts +4 -2
- package/dist/edit/pdf/schemas.js +4 -1
- package/dist/edit/pdf/session.d.ts +10 -0
- package/dist/edit/pdf/session.js +39 -0
- package/dist/edit/pdf/types.d.ts +15 -3
- package/dist/edit/pptx/handler.js +10 -2
- package/dist/edit/pptx/session.d.ts +10 -0
- package/dist/edit/pptx/session.js +31 -0
- package/dist/edit/session.d.ts +11 -0
- package/dist/edit/session.js +269 -27
- package/dist/edit/types.d.ts +17 -3
- package/dist/edit/worker-engine.d.ts +2 -2
- package/dist/edit/worker-engine.js +2 -2
- package/dist/fonts/THIRD_PARTY_NOTICES.md +6 -3
- package/dist/fonts/manifest.json +4 -4
- package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
- package/dist/fuzzy-alignment.d.ts +11 -4
- package/dist/fuzzy-alignment.js +3 -9
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +4 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +11 -2
- package/dist/limits.js +3 -0
- package/dist/worker-protocol.d.ts +1 -1
- package/dist/workers/fuzzy-search-worker.js +1 -1
- package/dist/workers/ooxml-edit-worker.js +1415 -860
- package/dist/workers/pdf-edit-worker.js +64 -16
- package/package.json +1 -1
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -38,9 +38,16 @@ The release artifact contains or depends on the following principal components.
|
|
|
38
38
|
a flat block index over the original bytes with the raw paragraph and run
|
|
39
39
|
properties kept, property merges that keep the original bytes where a
|
|
40
40
|
value does not change and place new children in schema order, and
|
|
41
|
-
section properties that an edited paragraph never duplicates. The
|
|
41
|
+
section properties that an edited paragraph never duplicates. The AI
|
|
42
|
+
tooling of `packages/viewer/src/edit/ai/` follows its published ideas of
|
|
43
|
+
a numbered document skeleton the model reads first with full content
|
|
44
|
+
pulled on demand within a character budget, writes made only through a
|
|
45
|
+
small validated tool set with a dry run, index addressing guarded by an
|
|
46
|
+
optimistic "document seen" check, a per-turn snapshot to roll back, and
|
|
47
|
+
Word tracked changes authored by the AI as the review channel. The
|
|
42
48
|
implementations in `packages/viewer/src/edit/pdf/selection.ts`,
|
|
43
|
-
`packages/viewer/src/edit/pptx
|
|
44
|
-
web-doc's own; no GenOffice code is
|
|
49
|
+
`packages/viewer/src/edit/pptx/`, `packages/viewer/src/edit/docx/` and
|
|
50
|
+
`packages/viewer/src/edit/ai/` are web-doc's own; no GenOffice code is
|
|
51
|
+
included.
|
|
45
52
|
- [Noto Sans](https://github.com/notofonts/noto-fonts) and [Noto Sans CJK](https://github.com/notofonts/noto-cjk) subset fonts — SIL Open Font License 1.1. The font manifest, complete OFL text, pinned source commits and SHA-256 hashes are included in `dist/fonts/`.
|
|
46
53
|
No Microsoft proprietary font or copyleft runtime component is bundled. Transitive notices and license expressions are verified by `npm run licenses`.
|
package/dist/contracts.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export interface ResourceLimits {
|
|
|
51
51
|
readonly maxEditHistory: number;
|
|
52
52
|
/** Memory for retained edit checkpoints; fewer are kept when a file is big. */
|
|
53
53
|
readonly maxEditCheckpointBytes: number;
|
|
54
|
+
/** Nodes one `EditSession.getOutline()` returns; more is cut and reported. */
|
|
55
|
+
readonly maxOutlineNodes: number;
|
|
56
|
+
/** Upper bound of `describe()`'s character budget. */
|
|
57
|
+
readonly maxDescribeChars: number;
|
|
58
|
+
/** Named edit checkpoints alive at once in a session. */
|
|
59
|
+
readonly maxEditCheckpoints: number;
|
|
54
60
|
}
|
|
55
61
|
export type BinaryDocumentSource = ArrayBuffer | Uint8Array | Blob;
|
|
56
62
|
export type DocumentSource = BinaryDocumentSource | URL | string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ResourceLimits } from "../../contracts.js";
|
|
2
|
+
import type { EditableFormat, EditElement, EditState, ElementQuery, ReadItem, ReadOptions, ReadResult } from "../types.js";
|
|
3
|
+
import type { DescribeOptions, DocumentDescription, OutlineNode, OutlineOptions, OutlineResult } from "./types.js";
|
|
4
|
+
/** What the outline needs of a session: its format, its page count and its elements. */
|
|
5
|
+
export interface OutlineSource {
|
|
6
|
+
readonly format: EditableFormat;
|
|
7
|
+
readonly state: EditState;
|
|
8
|
+
getElements(query?: ElementQuery, options?: ReadOptions): Promise<ReadResult<EditElement>>;
|
|
9
|
+
}
|
|
10
|
+
/** The outline of a session's document; one `getElements()` read, reshaped. */
|
|
11
|
+
export declare function readOutline(session: OutlineSource, limits: ResourceLimits, options?: OutlineOptions): Promise<OutlineResult>;
|
|
12
|
+
/** The description of a session's document: the outline rendered within a character budget. */
|
|
13
|
+
export declare function readDescription(session: OutlineSource, limits: ResourceLimits, options?: DescribeOptions): Promise<ReadItem<DocumentDescription>>;
|
|
14
|
+
export interface BuiltOutline {
|
|
15
|
+
readonly nodes: readonly OutlineNode[];
|
|
16
|
+
readonly nodeCount: number;
|
|
17
|
+
readonly truncated: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Nests elements by `parentId`, numbers them in reading order, keeps the
|
|
21
|
+
* nodes the options ask for (a container stays when a descendant is kept)
|
|
22
|
+
* and cuts the result at `maxNodes`. Ordinals are assigned before any
|
|
23
|
+
* filtering, so "3.2" names the same paragraph whatever the query.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildOutline(format: EditableFormat, elements: readonly EditElement[], options: Pick<OutlineOptions, "pageRange" | "kinds" | "maxTextChars">, maxNodes: number): BuiltOutline;
|
|
26
|
+
/**
|
|
27
|
+
* What a person calls the element, when the format knows: a shape's name
|
|
28
|
+
* or placeholder in a deck, a paragraph's style in a Word document, a
|
|
29
|
+
* table's size everywhere.
|
|
30
|
+
*/
|
|
31
|
+
export declare function labelOf(format: EditableFormat, element: EditElement): string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* The description's grammar, one line per node:
|
|
34
|
+
*
|
|
35
|
+
* <format>: <n> pages|slides, <m> elements
|
|
36
|
+
* [<id>] page <p> <kind> "<label>" (<flags>): <text>…
|
|
37
|
+
* [<child id>] page <p> <kind>: <text>
|
|
38
|
+
*
|
|
39
|
+
* The page is left out while the node has none, the label and the flags
|
|
40
|
+
* while the node has none, the text while it is empty. Tabs in the text
|
|
41
|
+
* become ` | `, line breaks ` ⏎ `; a cut text ends in `…`. Children are
|
|
42
|
+
* indented two spaces per level. The budget cuts whole lines and the last
|
|
43
|
+
* line then says how many were left out.
|
|
44
|
+
*/
|
|
45
|
+
export declare function renderDescription(format: EditableFormat, pageCount: number, outline: BuiltOutline | OutlineResult, maxChars: number, maxNodes: number): DocumentDescription;
|
|
46
|
+
/** One line of text: tabs as cell separators, breaks marked, other controls blanked. */
|
|
47
|
+
export declare function foldText(text: string): string;
|
|
@@ -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;
|