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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { ChangeMode, EditableFormat, EditReceipt, HistoryOptions, JsonSchema, OperationIssue, ReadItem, ReadOptions, ReadResult, TextRange } from "../types.js";
|
|
2
|
+
export interface OutlineOptions extends ReadOptions {
|
|
3
|
+
/** Inclusive 0-based page range; a node outside it is left out unless a descendant is inside. */
|
|
4
|
+
readonly pageRange?: readonly [number, number];
|
|
5
|
+
/** Element kinds to keep; a container of a kept node stays as its parent. */
|
|
6
|
+
readonly kinds?: readonly string[];
|
|
7
|
+
/** Characters of text kept per node; the rest is cut and `truncated` says so. Default 160. */
|
|
8
|
+
readonly maxTextChars?: number;
|
|
9
|
+
}
|
|
10
|
+
/** One element as a prompt sees it: no geometry, no styles, nested where the format nests. */
|
|
11
|
+
export interface OutlineNode {
|
|
12
|
+
/** The element id, as every operation takes it. */
|
|
13
|
+
readonly id: string;
|
|
14
|
+
/** The reading-order path: "3", "3.2" (a cell paragraph under its table). */
|
|
15
|
+
readonly ordinal: string;
|
|
16
|
+
/** The format's element kind. */
|
|
17
|
+
readonly kind: string;
|
|
18
|
+
/** −1 while the page is not laid out (a DOCX page the viewer has not cached). */
|
|
19
|
+
readonly pageIndex: number;
|
|
20
|
+
/** The first `maxTextChars` characters of the element's text. */
|
|
21
|
+
readonly text?: string;
|
|
22
|
+
/** Length of the whole text, in UTF-16 code units. */
|
|
23
|
+
readonly textLength: number;
|
|
24
|
+
/** True when `text` is shorter than the element's text. */
|
|
25
|
+
readonly truncated: boolean;
|
|
26
|
+
/** What a person calls it: "Title 1", "Heading 2", "Table (3×4)". */
|
|
27
|
+
readonly label?: string;
|
|
28
|
+
/** Why the element only accepts insertions next to it, when it does. */
|
|
29
|
+
readonly readOnlyReason?: string;
|
|
30
|
+
/** Listed and editable, but not drawn (a hidden PowerPoint shape). */
|
|
31
|
+
readonly hidden?: boolean;
|
|
32
|
+
/** Names of the operations that accept the element as their target. */
|
|
33
|
+
readonly operations: readonly string[];
|
|
34
|
+
/** A table's cell paragraphs, a group's members, a paragraph's inline pictures. */
|
|
35
|
+
readonly children?: readonly OutlineNode[];
|
|
36
|
+
}
|
|
37
|
+
/** `getOutline()`'s read: the nodes, how many there are and whether the limit cut them. */
|
|
38
|
+
export interface OutlineResult extends ReadResult<OutlineNode> {
|
|
39
|
+
/** Nodes in the result, nested ones included. */
|
|
40
|
+
readonly nodeCount: number;
|
|
41
|
+
/** True when `maxOutlineNodes` cut the outline. */
|
|
42
|
+
readonly truncated: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface DescribeOptions extends OutlineOptions {
|
|
45
|
+
/** Characters the description may take; the tail is cut and `truncated` says so. Default 50 000. */
|
|
46
|
+
readonly maxChars?: number;
|
|
47
|
+
}
|
|
48
|
+
/** The outline rendered as plain text for a prompt. */
|
|
49
|
+
export interface DocumentDescription {
|
|
50
|
+
readonly format: EditableFormat;
|
|
51
|
+
readonly pageCount: number;
|
|
52
|
+
/** Elements the description covers, nested ones included. */
|
|
53
|
+
readonly elementCount: number;
|
|
54
|
+
/**
|
|
55
|
+
* A header line, then one line per node in the documented grammar:
|
|
56
|
+
* `[sld2:7] slide 2 shape "Title 1": Quarterly review`.
|
|
57
|
+
*/
|
|
58
|
+
readonly text: string;
|
|
59
|
+
/** True when `maxChars` or `maxOutlineNodes` cut the description. */
|
|
60
|
+
readonly truncated: boolean;
|
|
61
|
+
}
|
|
62
|
+
/** What a model names: a quoted text, a citation with its page, a kind, a place. */
|
|
63
|
+
export interface TargetQuery {
|
|
64
|
+
/** Text to find; whitespace-insensitive, case-insensitive, tolerant to small differences. */
|
|
65
|
+
readonly text?: string;
|
|
66
|
+
/** A citation as the viewer's `search()` gets them: the passage and the 1-based page it is expected on. */
|
|
67
|
+
readonly citation?: {
|
|
68
|
+
readonly text: string;
|
|
69
|
+
readonly pageNumber?: number;
|
|
70
|
+
};
|
|
71
|
+
readonly pageIndex?: number;
|
|
72
|
+
readonly kinds?: readonly string[];
|
|
73
|
+
/** Restrict to an element and its descendants (a table, a group). */
|
|
74
|
+
readonly within?: string;
|
|
75
|
+
/** Default 5. */
|
|
76
|
+
readonly maxResults?: number;
|
|
77
|
+
}
|
|
78
|
+
/** One way to read a query, with how it was found and how far to trust it. */
|
|
79
|
+
export interface TargetCandidate {
|
|
80
|
+
readonly elementId: string;
|
|
81
|
+
/** The matched part, for ranged operations; absent when the match is the element's whole text. */
|
|
82
|
+
readonly range?: TextRange;
|
|
83
|
+
readonly pageIndex: number;
|
|
84
|
+
/** 1 for an exact match, down to 0.5 for the loosest accepted one. */
|
|
85
|
+
readonly score: number;
|
|
86
|
+
/** The pass that matched: the engine's exact search, folded text, fuzzy alignment, or a kind lookup. */
|
|
87
|
+
readonly reason: "exact" | "normalized" | "fuzzy" | "kind-only";
|
|
88
|
+
/** The matched text with a little context, on one line. */
|
|
89
|
+
readonly snippet: string;
|
|
90
|
+
}
|
|
91
|
+
/** A state the host named, to come back to; session state, gone with the session. */
|
|
92
|
+
export interface EditCheckpoint {
|
|
93
|
+
/** 22 URL-safe characters, unique in the session. */
|
|
94
|
+
readonly id: string;
|
|
95
|
+
readonly label?: string;
|
|
96
|
+
/** The revision it names. */
|
|
97
|
+
readonly revision: number;
|
|
98
|
+
/** ISO 8601. */
|
|
99
|
+
readonly createdAt: string;
|
|
100
|
+
}
|
|
101
|
+
/** One tool as a model provider takes it: a name, a description and a JSON Schema (draft 2020-12). */
|
|
102
|
+
export interface ToolDefinition {
|
|
103
|
+
readonly name: string;
|
|
104
|
+
readonly description: string;
|
|
105
|
+
readonly inputSchema: JsonSchema;
|
|
106
|
+
}
|
|
107
|
+
/** The tools of a session; the same names on every format, the operations of its own. */
|
|
108
|
+
export interface ToolSet {
|
|
109
|
+
/** Raised when a tool's shape changes incompatibly. */
|
|
110
|
+
readonly version: number;
|
|
111
|
+
readonly format: EditableFormat;
|
|
112
|
+
readonly definitions: readonly ToolDefinition[];
|
|
113
|
+
}
|
|
114
|
+
/** A model's call: the tool's name and its JSON arguments, validated against the tool's schema. */
|
|
115
|
+
export interface ToolCall {
|
|
116
|
+
readonly name: string;
|
|
117
|
+
readonly arguments: unknown;
|
|
118
|
+
}
|
|
119
|
+
/** What a tool call produced, for the model and the chat. */
|
|
120
|
+
export interface ToolResult {
|
|
121
|
+
readonly ok: boolean;
|
|
122
|
+
/** JSON for the model: a description, an outline, candidates, a receipt, or the issues of a refused call. */
|
|
123
|
+
readonly content: unknown;
|
|
124
|
+
/** One or two sentences for the model and the chat: "Replaced the title of slide 2." */
|
|
125
|
+
readonly text: string;
|
|
126
|
+
/** Why a call was refused, in the shape `apply()` reports. */
|
|
127
|
+
readonly issues?: readonly OperationIssue[];
|
|
128
|
+
}
|
|
129
|
+
export interface ToolCallOptions {
|
|
130
|
+
/** Checked before any tool that changes the document, like `ApplyOptions.expectedRevision`. */
|
|
131
|
+
readonly expectedRevision?: number;
|
|
132
|
+
/** How `document_apply` writes: in place, or as tracked changes where the format has them. */
|
|
133
|
+
readonly changeMode?: ChangeMode;
|
|
134
|
+
/** The author of tracked changes; required with `changeMode: "tracked"`. */
|
|
135
|
+
readonly author?: string;
|
|
136
|
+
readonly signal?: AbortSignal;
|
|
137
|
+
}
|
|
138
|
+
/** The AI-facing reads, the checkpoints and the tools every edit session has. */
|
|
139
|
+
export interface EditSessionReads {
|
|
140
|
+
/** The body elements in reading order, shaped for a prompt. */
|
|
141
|
+
getOutline(options?: OutlineOptions): Promise<OutlineResult>;
|
|
142
|
+
/** The outline as plain text, one line per element, within a character budget. */
|
|
143
|
+
describe(options?: DescribeOptions): Promise<ReadItem<DocumentDescription>>;
|
|
144
|
+
/**
|
|
145
|
+
* Elements and ranges a query names, best first: an exact match, else a
|
|
146
|
+
* match with whitespace, case, quotes and compatibility forms folded,
|
|
147
|
+
* else the viewer's fuzzy citation match, else (without text) the
|
|
148
|
+
* elements of the asked kinds in reading order.
|
|
149
|
+
*/
|
|
150
|
+
resolveTargets(query: TargetQuery, options?: ReadOptions): Promise<ReadResult<TargetCandidate>>;
|
|
151
|
+
/**
|
|
152
|
+
* Pins the current state under a new id. Rejects with `resource-limit`
|
|
153
|
+
* past `maxEditCheckpoints`.
|
|
154
|
+
*/
|
|
155
|
+
createCheckpoint(label?: string): Promise<EditCheckpoint>;
|
|
156
|
+
/** Every checkpoint alive, in creation order. */
|
|
157
|
+
listCheckpoints(): readonly EditCheckpoint[];
|
|
158
|
+
/**
|
|
159
|
+
* Back to the checkpoint's content as one history entry: undoable, a new
|
|
160
|
+
* revision, `documentchange` with reason `restore`. Rejects with
|
|
161
|
+
* `invalid-operation` for an unknown id.
|
|
162
|
+
*/
|
|
163
|
+
restoreCheckpoint(id: string, options?: HistoryOptions): Promise<EditReceipt>;
|
|
164
|
+
/** Forgets a checkpoint; unknown ids are ignored. */
|
|
165
|
+
dropCheckpoint(id: string): void;
|
|
166
|
+
/** The tool definitions of this session, for a model provider's tool list. */
|
|
167
|
+
readonly tools: ToolSet;
|
|
168
|
+
/**
|
|
169
|
+
* Runs one tool call. A model's mistake (an unknown tool, bad arguments, a
|
|
170
|
+
* refused batch, a stale revision) comes back as `ok: false` with issues
|
|
171
|
+
* and a text to act on; only the session's own errors (`lifecycle-error`,
|
|
172
|
+
* `aborted`) throw.
|
|
173
|
+
*/
|
|
174
|
+
callTool(call: ToolCall, options?: ToolCallOptions): Promise<ToolResult>;
|
|
175
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -81,10 +81,28 @@ function paragraphElement(model, record, base) {
|
|
|
81
81
|
id: record.elementId,
|
|
82
82
|
kind: "paragraph",
|
|
83
83
|
text: record.text.text,
|
|
84
|
-
...(record.table
|
|
84
|
+
...(record.table
|
|
85
|
+
? { parentId: record.table.elementId, ...cellOf(record.table, record) }
|
|
86
|
+
: {}),
|
|
85
87
|
textStyle: resolveTextStyle(model.styles, record.pPr, first?.rPr),
|
|
86
88
|
paragraphStyle: resolveParagraphStyle(model.styles, record.pPr),
|
|
87
89
|
...(record.readOnlyReason ? { readOnlyReason: record.readOnlyReason } : {}),
|
|
88
90
|
operations: operationsOf("paragraph", record.readOnlyReason !== undefined),
|
|
89
91
|
};
|
|
90
92
|
}
|
|
93
|
+
/** Where each paragraph of a table sits, read once per table. */
|
|
94
|
+
const CELLS = new WeakMap();
|
|
95
|
+
/** The cell of a table that holds a paragraph, when the table lists it. */
|
|
96
|
+
function cellOf(table, record) {
|
|
97
|
+
let cells = CELLS.get(table);
|
|
98
|
+
if (!cells) {
|
|
99
|
+
cells = new Map();
|
|
100
|
+
for (const [row, cellsOfRow] of table.rows.entries())
|
|
101
|
+
for (const [column, cell] of cellsOfRow.entries())
|
|
102
|
+
for (const paragraph of cell.paragraphs)
|
|
103
|
+
cells.set(paragraph, { row, column });
|
|
104
|
+
CELLS.set(table, cells);
|
|
105
|
+
}
|
|
106
|
+
const cell = cells.get(record);
|
|
107
|
+
return cell ? { cell } : {};
|
|
108
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { ResourceLimits } from "../../contracts.js";
|
|
2
|
-
import type { EditEngine, EngineBatch, EngineChange, MaterializedDocument, MaterializeOptions, RestoreTarget } from "../engine.js";
|
|
2
|
+
import type { BatchMode, EditEngine, EngineBatch, EngineChange, MaterializedDocument, MaterializeOptions, RestoreTarget } from "../engine.js";
|
|
3
3
|
import { OoxmlPackage } from "../ooxml/package.js";
|
|
4
4
|
import type { EditFindOptions, EditOperation, ElementQuery, OperationIssue, PagePoint, TextTarget } from "../types.js";
|
|
5
5
|
import { DocxModel, type AnyRecord } from "./model.js";
|
|
6
|
-
import type { DocxElement } from "./types.js";
|
|
7
|
-
|
|
6
|
+
import type { DocxElement, DocxRevision } from "./types.js";
|
|
7
|
+
/** Reads the DOCX session adds on top of the core, served by the engine and the worker client alike. */
|
|
8
|
+
export interface DocxEngineReads {
|
|
9
|
+
revisions(id: string, signal: AbortSignal): Promise<readonly DocxRevision[]>;
|
|
10
|
+
}
|
|
11
|
+
export declare class DocxEditEngine implements EditEngine, DocxEngineReads {
|
|
8
12
|
#private;
|
|
9
13
|
readonly schemas: import("../types.js").OperationSchemaSet;
|
|
10
14
|
private constructor();
|
|
@@ -16,9 +20,11 @@ export declare class DocxEditEngine implements EditEngine {
|
|
|
16
20
|
get pageCount(): number;
|
|
17
21
|
/** The block index at the current revision. */
|
|
18
22
|
model(signal?: AbortSignal): Promise<DocxModel>;
|
|
19
|
-
validate(operations: readonly EditOperation[], signal: AbortSignal): Promise<readonly OperationIssue[]>;
|
|
23
|
+
validate(operations: readonly EditOperation[], signal: AbortSignal, mode?: BatchMode): Promise<readonly OperationIssue[]>;
|
|
20
24
|
/** Plain operation arrays, as the unit tests pass them, become the next batch. */
|
|
21
25
|
apply(input: EngineBatch | readonly EditOperation[], signal: AbortSignal): Promise<EngineChange>;
|
|
26
|
+
/** The revisions of a paragraph; none for another element or an unknown id. */
|
|
27
|
+
revisions(id: string, signal: AbortSignal): Promise<readonly DocxRevision[]>;
|
|
22
28
|
materialize(purposeOrSignal?: "show" | "save" | AbortSignal, options?: MaterializeOptions, signal?: AbortSignal): Promise<Uint8Array>;
|
|
23
29
|
/**
|
|
24
30
|
* `save` is the package as the session changed it: ids written only on
|
package/dist/edit/docx/engine.js
CHANGED
|
@@ -7,9 +7,10 @@ import { NO_PAGE, toElement } from "./elements.js";
|
|
|
7
7
|
import { docxHandlers } from "./handlers.js";
|
|
8
8
|
import { freshParagraphId, paragraphsOf } from "./ids.js";
|
|
9
9
|
import { DocxModel } from "./model.js";
|
|
10
|
-
import { issueCollector } from "./operations.js";
|
|
10
|
+
import { issueCollector, } from "./operations.js";
|
|
11
11
|
import { docxOperationSchemas } from "./schemas.js";
|
|
12
|
-
import {
|
|
12
|
+
import { revisionsOf } from "./tracked.js";
|
|
13
|
+
import { attributeProblem, namespacePatches } from "./write.js";
|
|
13
14
|
/*
|
|
14
15
|
* The DOCX edit engine: the package layer under a block index of the body
|
|
15
16
|
* story. It runs inside the OOXML edit worker in the browser and directly
|
|
@@ -39,6 +40,57 @@ function unauthoredIdsOf(bytes) {
|
|
|
39
40
|
const text = new TextDecoder().decode(bytes);
|
|
40
41
|
return [...text.matchAll(/<p id="([0-9A-F]{8})"\/>/g)].map((m) => m[1]);
|
|
41
42
|
}
|
|
43
|
+
/** The tracked-change record of a batch, when it writes revisions. */
|
|
44
|
+
function trackedOf(mode) {
|
|
45
|
+
if (mode.changeMode !== "tracked")
|
|
46
|
+
return undefined;
|
|
47
|
+
return {
|
|
48
|
+
author: mode.author ?? "",
|
|
49
|
+
...(mode.timestamp === undefined ? {} : { date: mode.timestamp }),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** ISO 8601 as `xsd:dateTime` takes it; what `w:date` carries. */
|
|
53
|
+
const DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/;
|
|
54
|
+
/**
|
|
55
|
+
* What a tracked batch must carry before any revision is written: an author
|
|
56
|
+
* the file can hold (the core requires one too; the engine used on its own
|
|
57
|
+
* does the same) and a date the file can hold.
|
|
58
|
+
*/
|
|
59
|
+
function trackedModeIssues(tracked) {
|
|
60
|
+
if (!tracked)
|
|
61
|
+
return [];
|
|
62
|
+
const issues = [];
|
|
63
|
+
const author = attributeProblem(tracked.author);
|
|
64
|
+
if (tracked.author.trim().length === 0)
|
|
65
|
+
issues.push({
|
|
66
|
+
operationIndex: -1,
|
|
67
|
+
path: "/author",
|
|
68
|
+
code: "required",
|
|
69
|
+
message: "Tracked changes name their author; pass ApplyOptions.author",
|
|
70
|
+
});
|
|
71
|
+
else if (author)
|
|
72
|
+
issues.push({
|
|
73
|
+
operationIndex: -1,
|
|
74
|
+
path: "/author",
|
|
75
|
+
code: "invalid-value",
|
|
76
|
+
message: `The author holds ${author}`,
|
|
77
|
+
});
|
|
78
|
+
if (tracked.date !== undefined) {
|
|
79
|
+
const date = attributeProblem(tracked.date);
|
|
80
|
+
if (date ||
|
|
81
|
+
!DATE_TIME.test(tracked.date) ||
|
|
82
|
+
Number.isNaN(Date.parse(tracked.date)))
|
|
83
|
+
issues.push({
|
|
84
|
+
operationIndex: -1,
|
|
85
|
+
path: "/timestamp",
|
|
86
|
+
code: "invalid-value",
|
|
87
|
+
message: date
|
|
88
|
+
? `The timestamp holds ${date}`
|
|
89
|
+
: "The timestamp must be an ISO 8601 date-time",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return issues;
|
|
93
|
+
}
|
|
42
94
|
export class DocxEditEngine {
|
|
43
95
|
schemas = docxOperationSchemas;
|
|
44
96
|
#original;
|
|
@@ -96,9 +148,13 @@ export class DocxEditEngine {
|
|
|
96
148
|
});
|
|
97
149
|
return pending;
|
|
98
150
|
}
|
|
99
|
-
async validate(operations, signal) {
|
|
151
|
+
async validate(operations, signal, mode = {}) {
|
|
100
152
|
const issues = [];
|
|
101
|
-
const
|
|
153
|
+
const tracked = trackedOf(mode);
|
|
154
|
+
const modeIssues = trackedModeIssues(tracked);
|
|
155
|
+
if (modeIssues.length > 0)
|
|
156
|
+
return modeIssues;
|
|
157
|
+
const base = await this.#context(0, signal, 0, new Set(), tracked);
|
|
102
158
|
for (const [index, operation] of operations.entries()) {
|
|
103
159
|
const context = { ...base, operationIndex: index };
|
|
104
160
|
const handler = docxHandlers.get(operation.op);
|
|
@@ -142,6 +198,10 @@ export class DocxEditEngine {
|
|
|
142
198
|
? { stateId: this.#nextStateId(), operations: input }
|
|
143
199
|
: input;
|
|
144
200
|
this.#stateId = Math.max(this.#stateId, batch.stateId);
|
|
201
|
+
const tracked = trackedOf(batch);
|
|
202
|
+
const modeIssues = trackedModeIssues(tracked);
|
|
203
|
+
if (modeIssues.length > 0)
|
|
204
|
+
throw invalidOperationError(modeIssues);
|
|
145
205
|
const snapshot = this.#pkg.snapshot();
|
|
146
206
|
const unauthored = this.#unauthored ? [...this.#unauthored] : undefined;
|
|
147
207
|
const createdIds = [];
|
|
@@ -158,7 +218,7 @@ export class DocxEditEngine {
|
|
|
158
218
|
const handler = docxHandlers.get(operation.op);
|
|
159
219
|
if (!handler)
|
|
160
220
|
throw new ViewerError("invalid-operation", `Unknown operation ${operation.op}`);
|
|
161
|
-
const context = await this.#context(batch.stateId, signal, index, issued);
|
|
221
|
+
const context = await this.#context(batch.stateId, signal, index, issued, tracked);
|
|
162
222
|
const issues = [];
|
|
163
223
|
await handler.validate(operation, context, issueCollector(index, issues));
|
|
164
224
|
if (issues.length > 0)
|
|
@@ -209,10 +269,11 @@ export class DocxEditEngine {
|
|
|
209
269
|
this.#stateId += 1;
|
|
210
270
|
return this.#stateId;
|
|
211
271
|
}
|
|
212
|
-
async #context(stateId, signal, operationIndex = 0, issued = new Set()) {
|
|
272
|
+
async #context(stateId, signal, operationIndex = 0, issued = new Set(), tracked) {
|
|
213
273
|
const model = await this.model(signal);
|
|
214
274
|
const taken = new Set([...model.takenIds, ...issued]);
|
|
215
275
|
let count = 0;
|
|
276
|
+
let revisions = 0;
|
|
216
277
|
return {
|
|
217
278
|
pkg: this.#pkg,
|
|
218
279
|
model,
|
|
@@ -227,8 +288,21 @@ export class DocxEditEngine {
|
|
|
227
288
|
taken.add(id);
|
|
228
289
|
return id;
|
|
229
290
|
},
|
|
291
|
+
...(tracked ? { tracked } : {}),
|
|
292
|
+
nextRevisionId: () => {
|
|
293
|
+
revisions += 1;
|
|
294
|
+
return model.maxRevisionId + revisions;
|
|
295
|
+
},
|
|
230
296
|
};
|
|
231
297
|
}
|
|
298
|
+
/** The revisions of a paragraph; none for another element or an unknown id. */
|
|
299
|
+
async revisions(id, signal) {
|
|
300
|
+
const model = await this.model(signal);
|
|
301
|
+
const record = model.byId.get(id);
|
|
302
|
+
if (!record || record.kind !== "paragraph")
|
|
303
|
+
return [];
|
|
304
|
+
return revisionsOf(model.document, record.node);
|
|
305
|
+
}
|
|
232
306
|
async materialize(purposeOrSignal = "show", options = {}, signal = new AbortController().signal) {
|
|
233
307
|
return (await this.materializeDocument(purposeOrSignal, options, signal))
|
|
234
308
|
.bytes;
|
|
@@ -73,6 +73,8 @@ export declare class DocxModel {
|
|
|
73
73
|
/** Every paragraph id the document uses, in any story part. */
|
|
74
74
|
readonly takenIds: ReadonlySet<string>;
|
|
75
75
|
readonly styles: DocxStyles;
|
|
76
|
+
/** The largest `w:id` of the main part, over revisions and range markup alike; 0 without any. */
|
|
77
|
+
get maxRevisionId(): number;
|
|
76
78
|
private constructor();
|
|
77
79
|
/**
|
|
78
80
|
* Builds the index. `unauthored` carries the ids of the paragraphs that
|
package/dist/edit/docx/model.js
CHANGED
|
@@ -15,6 +15,17 @@ export class DocxModel {
|
|
|
15
15
|
unauthoredIds;
|
|
16
16
|
takenIds;
|
|
17
17
|
styles;
|
|
18
|
+
#maxRevisionId;
|
|
19
|
+
/** The largest `w:id` of the main part, over revisions and range markup alike; 0 without any. */
|
|
20
|
+
get maxRevisionId() {
|
|
21
|
+
if (this.#maxRevisionId === undefined) {
|
|
22
|
+
let max = 0;
|
|
23
|
+
for (const match of this.document.text.matchAll(/ w:id="(\d{1,9})"/g))
|
|
24
|
+
max = Math.max(max, Number(match[1]));
|
|
25
|
+
this.#maxRevisionId = max;
|
|
26
|
+
}
|
|
27
|
+
return this.#maxRevisionId;
|
|
28
|
+
}
|
|
18
29
|
constructor(revision, mainPart, document, body,
|
|
19
30
|
/** The body's own `w:sectPr`, when present. */
|
|
20
31
|
bodySectPr,
|
|
@@ -15,6 +15,16 @@ export interface DocxOperationContext {
|
|
|
15
15
|
readonly operationIndex: number;
|
|
16
16
|
/** A paragraph id no paragraph of the document or of this batch uses. */
|
|
17
17
|
freshParagraphId(): string;
|
|
18
|
+
/** Set when the batch writes tracked changes: who, and when. */
|
|
19
|
+
readonly tracked?: TrackedChange;
|
|
20
|
+
/** A `w:id` no revision of the document or of this batch uses. */
|
|
21
|
+
nextRevisionId(): number;
|
|
22
|
+
}
|
|
23
|
+
/** What every revision of a tracked batch records. */
|
|
24
|
+
export interface TrackedChange {
|
|
25
|
+
readonly author: string;
|
|
26
|
+
/** ISO 8601; left out of the file when the batch has no timestamp. */
|
|
27
|
+
readonly date?: string;
|
|
18
28
|
}
|
|
19
29
|
export interface DocxOperationResult {
|
|
20
30
|
readonly createdIds: readonly string[];
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { EditEngineContext } from "../engine.js";
|
|
2
2
|
import { type OoxmlEditProviderOptions } from "../ooxml/worker.js";
|
|
3
3
|
import { WorkerEngineClient } from "../worker-engine.js";
|
|
4
|
+
import type { DocxEngineReads } from "./engine.js";
|
|
5
|
+
import type { DocxRevision } from "./types.js";
|
|
4
6
|
export type DocxEditProviderOptions = OoxmlEditProviderOptions;
|
|
5
7
|
/**
|
|
6
8
|
* Starts the OOXML edit worker for `original` as a DOCX engine. Loaded
|
|
7
9
|
* lazily by the Office adapter's provider on the first `edit()`.
|
|
8
10
|
*/
|
|
9
11
|
export declare function loadDocxEditEngine(original: Uint8Array, context: EditEngineContext, options?: DocxEditProviderOptions): Promise<DocxEditEngineClient>;
|
|
10
|
-
export declare class DocxEditEngineClient extends WorkerEngineClient {
|
|
12
|
+
export declare class DocxEditEngineClient extends WorkerEngineClient implements DocxEngineReads {
|
|
11
13
|
readonly schemas: import("../types.js").OperationSchemaSet;
|
|
14
|
+
revisions(id: string, signal: AbortSignal): Promise<readonly DocxRevision[]>;
|
|
12
15
|
start(original: Uint8Array): Promise<void>;
|
|
13
16
|
}
|
|
@@ -21,6 +21,9 @@ export async function loadDocxEditEngine(original, context, options = {}) {
|
|
|
21
21
|
}
|
|
22
22
|
export class DocxEditEngineClient extends WorkerEngineClient {
|
|
23
23
|
schemas = docxOperationSchemas;
|
|
24
|
+
revisions(id, signal) {
|
|
25
|
+
return this.request("edit-docx-revisions", { id }, signal);
|
|
26
|
+
}
|
|
24
27
|
async start(original) {
|
|
25
28
|
const data = original.slice().buffer;
|
|
26
29
|
const open = {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { DescribeOptions, DocumentDescription, EditCheckpoint, OutlineOptions, OutlineResult, TargetCandidate, TargetQuery, ToolCall, ToolCallOptions, ToolResult, ToolSet } from "../ai/types.js";
|
|
1
2
|
import type { EditSessionAccess, EditSessionCore } from "../engine.js";
|
|
2
3
|
import type { ApplyOptions, AssetOptions, EditFindOptions, EditOperation, EditReceipt, EditState, ElementQuery, HistoryOptions, OperationSchemaSet, PagePoint, PageRect, ReadItem, ReadOptions, ReadResult, SavedDocument, TextTarget } from "../types.js";
|
|
3
|
-
import type { DocxDeleteElementOperation, DocxEditSession, DocxElement, DocxFields, DocxInsertImageOperation, DocxInsertParagraphOperation, DocxInsertTableOperation, DocxMoveElementOperation, DocxOperation, DocxReplaceTextOperation, DocxSaveOptions, DocxSetParagraphStyleOperation, DocxSetTableCellOperation, DocxSetTextStyleOperation } from "./types.js";
|
|
4
|
+
import type { DocxDeleteElementOperation, DocxEditSession, DocxElement, DocxFields, DocxInsertImageOperation, DocxInsertParagraphOperation, DocxInsertTableOperation, DocxMoveElementOperation, DocxOperation, DocxReplaceTextOperation, DocxRevision, DocxSaveOptions, DocxSetParagraphStyleOperation, DocxSetTableCellOperation, DocxSetTextStyleOperation } from "./types.js";
|
|
4
5
|
/** Per paragraph id (eight hex digits), the union of its runs per page. */
|
|
5
6
|
type Placement = ReadonlyMap<string, ReadonlyMap<number, PageRect>>;
|
|
6
7
|
export declare class DocxSession implements DocxEditSession {
|
|
@@ -22,6 +23,16 @@ export declare class DocxSession implements DocxEditSession {
|
|
|
22
23
|
getElement(id: string, options?: ReadOptions): Promise<ReadItem<DocxElement>>;
|
|
23
24
|
elementsAt(pageIndex: number, point: PagePoint, options?: ReadOptions): Promise<ReadResult<DocxElement>>;
|
|
24
25
|
findText(query: string, options?: EditFindOptions): Promise<ReadResult<TextTarget>>;
|
|
26
|
+
getOutline(options?: OutlineOptions): Promise<OutlineResult>;
|
|
27
|
+
describe(options?: DescribeOptions): Promise<ReadItem<DocumentDescription>>;
|
|
28
|
+
resolveTargets(query: TargetQuery, options?: ReadOptions): Promise<ReadResult<TargetCandidate>>;
|
|
29
|
+
createCheckpoint(label?: string): Promise<EditCheckpoint>;
|
|
30
|
+
listCheckpoints(): readonly EditCheckpoint[];
|
|
31
|
+
restoreCheckpoint(id: string, options?: HistoryOptions): Promise<EditReceipt>;
|
|
32
|
+
dropCheckpoint(id: string): void;
|
|
33
|
+
get tools(): ToolSet;
|
|
34
|
+
callTool(call: ToolCall, options?: ToolCallOptions): Promise<ToolResult>;
|
|
35
|
+
getRevisions(elementId: string, options?: ReadOptions): Promise<ReadResult<DocxRevision>>;
|
|
25
36
|
replaceText(fields: DocxFields<DocxReplaceTextOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|
|
26
37
|
setTextStyle(fields: DocxFields<DocxSetTextStyleOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|
|
27
38
|
setParagraphStyle(fields: DocxFields<DocxSetParagraphStyleOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { readDescription, readOutline } from "../ai/outline.js";
|
|
2
|
+
import { resolveTargets } from "../ai/targets.js";
|
|
3
|
+
import { buildToolSet, callTool as runTool } from "../ai/tools.js";
|
|
4
|
+
import { ViewerError } from "../../errors.js";
|
|
1
5
|
export class DocxSession {
|
|
2
6
|
format = "docx";
|
|
3
7
|
#core;
|
|
8
|
+
#tools;
|
|
4
9
|
#access;
|
|
5
10
|
constructor(core, access) {
|
|
6
11
|
this.#core = core;
|
|
@@ -99,6 +104,36 @@ export class DocxSession {
|
|
|
99
104
|
: placed;
|
|
100
105
|
return Object.freeze({ ...found, items });
|
|
101
106
|
}
|
|
107
|
+
getOutline(options) {
|
|
108
|
+
return readOutline(this, this.#core.limits, options);
|
|
109
|
+
}
|
|
110
|
+
describe(options) {
|
|
111
|
+
return readDescription(this, this.#core.limits, options);
|
|
112
|
+
}
|
|
113
|
+
resolveTargets(query, options) {
|
|
114
|
+
return resolveTargets(this, query, options);
|
|
115
|
+
}
|
|
116
|
+
createCheckpoint(label) {
|
|
117
|
+
return this.#core.createCheckpoint(label);
|
|
118
|
+
}
|
|
119
|
+
listCheckpoints() {
|
|
120
|
+
return this.#core.listCheckpoints();
|
|
121
|
+
}
|
|
122
|
+
restoreCheckpoint(id, options) {
|
|
123
|
+
return this.#core.restoreCheckpoint(id, options);
|
|
124
|
+
}
|
|
125
|
+
dropCheckpoint(id) {
|
|
126
|
+
this.#core.dropCheckpoint(id);
|
|
127
|
+
}
|
|
128
|
+
get tools() {
|
|
129
|
+
return (this.#tools ??= buildToolSet(this.format, this.schemas));
|
|
130
|
+
}
|
|
131
|
+
callTool(call, options) {
|
|
132
|
+
return runTool(this, this.tools, call, options);
|
|
133
|
+
}
|
|
134
|
+
getRevisions(elementId, options) {
|
|
135
|
+
return this.#core.readItems(options, (engine, signal) => docxReads(engine).revisions(elementId, signal));
|
|
136
|
+
}
|
|
102
137
|
replaceText(fields, options) {
|
|
103
138
|
return this.apply([{ op: "replaceText", ...fields }], options);
|
|
104
139
|
}
|
|
@@ -424,3 +459,9 @@ function matchesQuery(element, query) {
|
|
|
424
459
|
return false;
|
|
425
460
|
return !query.intersects || rectsIntersect(fragment.bounds, query.intersects);
|
|
426
461
|
}
|
|
462
|
+
function docxReads(engine) {
|
|
463
|
+
const candidate = engine;
|
|
464
|
+
if (typeof candidate.revisions !== "function")
|
|
465
|
+
throw new ViewerError("edit-unsupported", "The engine does not provide the DOCX reads", { details: { format: "docx", reason: "no-reads" } });
|
|
466
|
+
return candidate;
|
|
467
|
+
}
|
|
@@ -4,6 +4,7 @@ import { patches } from "../ooxml/patch.js";
|
|
|
4
4
|
import { relativeTarget } from "../ooxml/transaction.js";
|
|
5
5
|
import { OFFICE_RELATIONSHIPS, W_NS } from "./ids.js";
|
|
6
6
|
import { firstTextItem } from "./text.js";
|
|
7
|
+
import { deletedParagraphXml, insertedRunXml, markedParagraphProperties, trackedRangeProblem, unsupportedTracked, } from "./tracked.js";
|
|
7
8
|
import { attributeProblem, changedRunProperties, colorProblem, namespacePatches, normalizeText, paragraphPropertiesWithoutSection, paragraphXml, runContentXml, runXml, sliceOf, textProblem, } from "./write.js";
|
|
8
9
|
/*
|
|
9
10
|
* Structure: paragraphs inserted next to other elements, paragraphs,
|
|
@@ -168,15 +169,17 @@ export const insertParagraphHandler = {
|
|
|
168
169
|
const rPr = operation.style
|
|
169
170
|
? changedRunProperties(part, sourceRPr, operation.style, context.model.styles)
|
|
170
171
|
: sliceOf(part, sourceRPr);
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
: "";
|
|
172
|
+
const sourcePPr = record.kind === "paragraph" ? record.pPr : undefined;
|
|
173
|
+
const pPr = paragraphPropertiesWithoutSection(part, sourcePPr);
|
|
174
174
|
const segments = normalizeText(operation.text).split("\n");
|
|
175
175
|
const createdIds = [];
|
|
176
176
|
const xml = segments.map((segment) => {
|
|
177
177
|
const id = context.freshParagraphId();
|
|
178
178
|
createdIds.push(`p:${id}`);
|
|
179
|
-
|
|
179
|
+
// Tracked: the paragraph mark and the run are both insertions.
|
|
180
|
+
return context.tracked
|
|
181
|
+
? paragraphXml(undefined, id, markedParagraphProperties(context, sourcePPr, "ins"), insertedRunXml(context, rPr, segment))
|
|
182
|
+
: paragraphXml(undefined, id, pPr, runXml(rPr, runContentXml(segment)));
|
|
180
183
|
});
|
|
181
184
|
// Inserts at one position land in call order, so the paragraphs
|
|
182
185
|
// keep their order on either side of the reference.
|
|
@@ -236,7 +239,15 @@ export const deleteElementHandler = {
|
|
|
236
239
|
: `No element ${operation.target}`);
|
|
237
240
|
return;
|
|
238
241
|
}
|
|
242
|
+
if (context.tracked && record.kind !== "paragraph") {
|
|
243
|
+
unsupportedTracked(issue, "/target", "Deleting a table or a picture");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
239
246
|
if (record.kind === "paragraph") {
|
|
247
|
+
if (context.tracked &&
|
|
248
|
+
!record.readOnlyReason &&
|
|
249
|
+
trackedRangeProblem(record, 0, record.text.text.length, issue, "/target"))
|
|
250
|
+
return;
|
|
240
251
|
if (record.sectPr)
|
|
241
252
|
issue("/target", "section-break", "A paragraph that ends a section cannot be deleted");
|
|
242
253
|
else if (record.readOnlyReason)
|
|
@@ -253,6 +264,22 @@ export const deleteElementHandler = {
|
|
|
253
264
|
return deleteInline(context, found);
|
|
254
265
|
const record = found;
|
|
255
266
|
const node = blockNode(record);
|
|
267
|
+
if (context.tracked && record.kind === "paragraph") {
|
|
268
|
+
// The paragraph stays, marked deleted; its inline objects leave
|
|
269
|
+
// the model with their runs, Word drops the rest on accept.
|
|
270
|
+
const nested = paragraphIdsUnder(model, node).filter((id) => id !== record.id);
|
|
271
|
+
return commit(context, [
|
|
272
|
+
patches.replaceElement(part, node, deletedParagraphXml(context, record)),
|
|
273
|
+
], {
|
|
274
|
+
createdIds: [],
|
|
275
|
+
removedIds: record.inlines.map((inline) => inline.elementId),
|
|
276
|
+
...(nested.length > 0 ? { removedParagraphIds: nested } : {}),
|
|
277
|
+
...(model.unauthoredSet.has(record.id)
|
|
278
|
+
? { stamped: [record.id] }
|
|
279
|
+
: {}),
|
|
280
|
+
reflowFrom: record.id,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
256
283
|
const items = [patches.removeElement(part, node)];
|
|
257
284
|
const createdIds = [];
|
|
258
285
|
if (model.blocks.includes(record)) {
|
|
@@ -310,6 +337,10 @@ function stripNode(part, node) {
|
|
|
310
337
|
}
|
|
311
338
|
export const moveElementHandler = {
|
|
312
339
|
async validate(operation, context, issue) {
|
|
340
|
+
if (context.tracked) {
|
|
341
|
+
unsupportedTracked(issue, "", "Moving an element");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
313
344
|
const record = context.model.byId.get(operation.target);
|
|
314
345
|
if (!record || (record.kind !== "paragraph" && record.kind !== "table")) {
|
|
315
346
|
issue("/target", record ? "invalid-target" : "unknown-target", record
|
|
@@ -449,6 +480,10 @@ function drawingNamespacePatches(part) {
|
|
|
449
480
|
}
|
|
450
481
|
export const insertImageHandler = {
|
|
451
482
|
async validate(operation, context, issue) {
|
|
483
|
+
if (context.tracked) {
|
|
484
|
+
unsupportedTracked(issue, "", "Inserting a picture");
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
452
487
|
placement(operation, context, issue);
|
|
453
488
|
try {
|
|
454
489
|
const bytes = resolveBinary(operation.data, context.assets);
|