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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ApplyOptions, BinaryData, EditColor, EditElement, EditReceipt, EditSessionBase, SavedDocument, SaveOptions, TextRange } from "../types.js";
|
|
1
|
+
import type { ApplyOptions, BinaryData, EditColor, EditElement, EditReceipt, EditSessionBase, ReadOptions, ReadResult, SavedDocument, SaveOptions, TextRange } from "../types.js";
|
|
2
2
|
export type DocxElementKind = "paragraph" | "table" | "image" | "other";
|
|
3
3
|
export type DocxTextAlign = "left" | "center" | "right" | "justify";
|
|
4
4
|
/** Word's sixteen highlight colours (`w:highlight/@w:val`). */
|
|
@@ -54,6 +54,14 @@ export interface DocxElement extends EditElement {
|
|
|
54
54
|
readonly textStyle?: DocxTextStyle;
|
|
55
55
|
/** Present for a paragraph. */
|
|
56
56
|
readonly paragraphStyle?: DocxParagraphStyle;
|
|
57
|
+
/**
|
|
58
|
+
* Present for a paragraph in a table cell: the cell's row and its place in
|
|
59
|
+
* the row, both from 0. Paragraphs move only among those of the same cell.
|
|
60
|
+
*/
|
|
61
|
+
readonly cell?: {
|
|
62
|
+
readonly row: number;
|
|
63
|
+
readonly column: number;
|
|
64
|
+
};
|
|
57
65
|
/** Present for a table: cell text by row. */
|
|
58
66
|
readonly table?: {
|
|
59
67
|
readonly rows: readonly (readonly string[])[];
|
|
@@ -165,11 +173,26 @@ export interface DocxInsertImageOperation {
|
|
|
165
173
|
export type DocxOperation = DocxReplaceTextOperation | DocxSetTextStyleOperation | DocxSetParagraphStyleOperation | DocxInsertParagraphOperation | DocxDeleteElementOperation | DocxMoveElementOperation | DocxInsertTableOperation | DocxSetTableCellOperation | DocxInsertImageOperation;
|
|
166
174
|
/** DOCX saves have no fields of their own. */
|
|
167
175
|
export type DocxSaveOptions = SaveOptions;
|
|
176
|
+
/** One revision of a paragraph, as `getRevisions()` lists them in document order. */
|
|
177
|
+
export interface DocxRevision {
|
|
178
|
+
readonly kind: "ins" | "del" | "moveFrom" | "moveTo" | "rPrChange" | "pPrChange";
|
|
179
|
+
/** The `w:id`; -1 when the file leaves it out. */
|
|
180
|
+
readonly id: number;
|
|
181
|
+
readonly author?: string;
|
|
182
|
+
/** ISO 8601, as the file writes it. */
|
|
183
|
+
readonly date?: string;
|
|
184
|
+
/** Runs of the paragraph, its paragraph mark, or the paragraph's properties. */
|
|
185
|
+
readonly scope: "runs" | "mark" | "paragraph";
|
|
186
|
+
/** The inserted, deleted or restyled text; "\n" for a paragraph mark; absent for a property change. */
|
|
187
|
+
readonly text?: string;
|
|
188
|
+
}
|
|
168
189
|
/** An operation's fields without its `op`, as the typed methods take them. */
|
|
169
190
|
export type DocxFields<T extends DocxOperation> = T extends unknown ? Omit<T, "op"> : never;
|
|
170
191
|
export interface DocxEditSession extends EditSessionBase<DocxOperation, DocxElement> {
|
|
171
192
|
readonly format: "docx";
|
|
172
193
|
save(options?: DocxSaveOptions): Promise<SavedDocument>;
|
|
194
|
+
/** The tracked changes a paragraph holds, in document order; empty for other elements. */
|
|
195
|
+
getRevisions(elementId: string, options?: ReadOptions): Promise<ReadResult<DocxRevision>>;
|
|
173
196
|
/** Replaces the whole text of a paragraph, or the part a range covers. */
|
|
174
197
|
replaceText(fields: DocxFields<DocxReplaceTextOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|
|
175
198
|
/** Changes run properties; unspecified ones keep their bytes. */
|
|
@@ -3,6 +3,10 @@ import { type XmlElement, type XmlPart } from "../ooxml/xml.js";
|
|
|
3
3
|
import type { EditColor } from "../types.js";
|
|
4
4
|
import { type DocxStyles } from "./style.js";
|
|
5
5
|
import type { DocxParagraphStyleChange, DocxTextStyleChange } from "./types.js";
|
|
6
|
+
/** Children of `w:rPr` in schema order (CT_RPr). */
|
|
7
|
+
export declare const RPR_ORDER: string[];
|
|
8
|
+
/** Children of `w:pPr` in schema order (CT_PPr). */
|
|
9
|
+
export declare const PPR_ORDER: string[];
|
|
6
10
|
/** Why a text cannot be written, or undefined when it can. */
|
|
7
11
|
export declare function textProblem(text: string): string | undefined;
|
|
8
12
|
/** Why a value cannot be written as an attribute: text problems plus the breaks text may hold. */
|
|
@@ -23,15 +27,25 @@ export declare function runXml(rPr: string, content: string): string;
|
|
|
23
27
|
* where the schema puts them. Returns "" for no properties at all.
|
|
24
28
|
*/
|
|
25
29
|
export declare function mergedProperties(part: XmlPart, source: XmlElement | undefined, tag: "w:rPr" | "w:pPr", order: readonly string[], set: ReadonlyMap<string, string | null>): string;
|
|
26
|
-
/**
|
|
27
|
-
|
|
30
|
+
/**
|
|
31
|
+
* `w:rPr` bytes with a text style change applied. With `revision` (the
|
|
32
|
+
* attributes of a tracked change) the previous properties are kept in a
|
|
33
|
+
* `w:rPrChange`, so Word shows the change as a suggestion.
|
|
34
|
+
*/
|
|
35
|
+
export declare function changedRunProperties(part: XmlPart, rPr: XmlElement | undefined, change: DocxTextStyleChange, styles: DocxStyles, revision?: string): string;
|
|
28
36
|
/** `w:color` for a colour: RGB, automatic, or a theme name with the theme's value. */
|
|
29
37
|
export declare function colorXml(color: EditColor, styles: DocxStyles): string;
|
|
30
|
-
/**
|
|
31
|
-
export declare function
|
|
38
|
+
/** The children of a properties element as bytes, the named ones left out. */
|
|
39
|
+
export declare function innerPropertiesXml(part: XmlPart, properties: XmlElement | undefined, without: readonly string[]): string;
|
|
40
|
+
/**
|
|
41
|
+
* `w:pPr` bytes with alignment and spacing changed, other children kept.
|
|
42
|
+
* With `revision` the previous paragraph properties are kept in a
|
|
43
|
+
* `w:pPrChange`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function changedParagraphProperties(part: XmlPart, pPr: XmlElement | undefined, change: DocxParagraphStyleChange, revision?: string): string;
|
|
32
46
|
export declare function alignValue(align: NonNullable<DocxParagraphStyleChange["align"]>): string;
|
|
33
47
|
/** `w:pPr` bytes with the paragraph mark's `w:rPr` changed (created when absent). */
|
|
34
|
-
export declare function paragraphMarkProperties(part: XmlPart, pPr: XmlElement | undefined, change: DocxTextStyleChange, styles: DocxStyles): string;
|
|
48
|
+
export declare function paragraphMarkProperties(part: XmlPart, pPr: XmlElement | undefined, change: DocxTextStyleChange, styles: DocxStyles, revision?: string): string;
|
|
35
49
|
/** `w:pPr` bytes without the section properties, for a paragraph copied next to one that ends a section. */
|
|
36
50
|
export declare function paragraphPropertiesWithoutSection(part: XmlPart, pPr: XmlElement | undefined): string;
|
|
37
51
|
/**
|
package/dist/edit/docx/write.js
CHANGED
|
@@ -11,7 +11,7 @@ import { LINE_BREAK, PAGE_BREAK, TAB } from "./text.js";
|
|
|
11
11
|
*/
|
|
12
12
|
const MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006";
|
|
13
13
|
/** Children of `w:rPr` in schema order (CT_RPr). */
|
|
14
|
-
const RPR_ORDER = [
|
|
14
|
+
export const RPR_ORDER = [
|
|
15
15
|
"rStyle",
|
|
16
16
|
"rFonts",
|
|
17
17
|
"b",
|
|
@@ -54,7 +54,7 @@ const RPR_ORDER = [
|
|
|
54
54
|
"rPrChange",
|
|
55
55
|
];
|
|
56
56
|
/** Children of `w:pPr` in schema order (CT_PPr). */
|
|
57
|
-
const PPR_ORDER = [
|
|
57
|
+
export const PPR_ORDER = [
|
|
58
58
|
"pStyle",
|
|
59
59
|
"keepNext",
|
|
60
60
|
"keepLines",
|
|
@@ -215,9 +215,15 @@ export function mergedProperties(part, source, tag, order, set) {
|
|
|
215
215
|
function toggleXml(local, on) {
|
|
216
216
|
return on ? `<w:${local}/>` : `<w:${local} w:val="0"/>`;
|
|
217
217
|
}
|
|
218
|
-
/**
|
|
219
|
-
|
|
218
|
+
/**
|
|
219
|
+
* `w:rPr` bytes with a text style change applied. With `revision` (the
|
|
220
|
+
* attributes of a tracked change) the previous properties are kept in a
|
|
221
|
+
* `w:rPrChange`, so Word shows the change as a suggestion.
|
|
222
|
+
*/
|
|
223
|
+
export function changedRunProperties(part, rPr, change, styles, revision) {
|
|
220
224
|
const set = new Map();
|
|
225
|
+
if (revision !== undefined)
|
|
226
|
+
set.set("rPrChange", `<w:rPrChange${revision}>${wrapped("w:rPr", innerPropertiesXml(part, rPr, ["rPrChange"]))}</w:rPrChange>`);
|
|
221
227
|
if (change.bold !== undefined) {
|
|
222
228
|
set.set("b", toggleXml("b", change.bold));
|
|
223
229
|
set.set("bCs", toggleXml("bCs", change.bold));
|
|
@@ -261,9 +267,26 @@ function fontsXml(part, rPr, family) {
|
|
|
261
267
|
const rest = kept.map((a) => ` ${a.name}="${a.rawValue}"`).join("");
|
|
262
268
|
return `<w:rFonts w:ascii="${face}" w:hAnsi="${face}"${rest}/>`;
|
|
263
269
|
}
|
|
264
|
-
/** `
|
|
265
|
-
|
|
270
|
+
/** `inner` inside `tag`, self-closing when empty. */
|
|
271
|
+
function wrapped(tag, inner) {
|
|
272
|
+
return inner.length === 0 ? `<${tag}/>` : `<${tag}>${inner}</${tag}>`;
|
|
273
|
+
}
|
|
274
|
+
/** The children of a properties element as bytes, the named ones left out. */
|
|
275
|
+
export function innerPropertiesXml(part, properties, without) {
|
|
276
|
+
return (properties?.children ?? [])
|
|
277
|
+
.filter((child) => !without.includes(child.local))
|
|
278
|
+
.map((child) => sliceOf(part, child))
|
|
279
|
+
.join("");
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* `w:pPr` bytes with alignment and spacing changed, other children kept.
|
|
283
|
+
* With `revision` the previous paragraph properties are kept in a
|
|
284
|
+
* `w:pPrChange`.
|
|
285
|
+
*/
|
|
286
|
+
export function changedParagraphProperties(part, pPr, change, revision) {
|
|
266
287
|
const set = new Map();
|
|
288
|
+
if (revision !== undefined)
|
|
289
|
+
set.set("pPrChange", `<w:pPrChange${revision}>${wrapped("w:pPr", innerPropertiesXml(part, pPr, ["rPr", "sectPr", "pPrChange"]))}</w:pPrChange>`);
|
|
267
290
|
if (change.align !== undefined)
|
|
268
291
|
set.set("jc", `<w:jc w:val="${alignValue(change.align)}"/>`);
|
|
269
292
|
if (change.spacing !== undefined) {
|
|
@@ -304,9 +327,9 @@ export function alignValue(align) {
|
|
|
304
327
|
}
|
|
305
328
|
}
|
|
306
329
|
/** `w:pPr` bytes with the paragraph mark's `w:rPr` changed (created when absent). */
|
|
307
|
-
export function paragraphMarkProperties(part, pPr, change, styles) {
|
|
330
|
+
export function paragraphMarkProperties(part, pPr, change, styles, revision) {
|
|
308
331
|
const rPr = pPr?.children.find((child) => child.local === "rPr" && child.namespace === W_NS);
|
|
309
|
-
const changed = changedRunProperties(part, rPr, change, styles);
|
|
332
|
+
const changed = changedRunProperties(part, rPr, change, styles, revision);
|
|
310
333
|
return mergedProperties(part, pPr, "w:pPr", PPR_ORDER, new Map([["rPr", changed || null]]));
|
|
311
334
|
}
|
|
312
335
|
/** `w:pPr` bytes without the section properties, for a paragraph copied next to one that ends a section. */
|
package/dist/edit/engine.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DocumentFormat, RegisteredFont, ResourceLimits, TextRun, ViewerWarning } from "../contracts.js";
|
|
2
2
|
import type { EditSession } from "./sessions.js";
|
|
3
|
-
import type { EditableFormat, EditElement, EditFindOptions, EditOperation, EditSessionBase, ElementQuery, OperationIssue, OperationSchemaSet, PagePoint, ReadItem, ReadOptions, ReadResult, TextTarget } from "./types.js";
|
|
3
|
+
import type { ChangeMode, EditableFormat, EditElement, EditFindOptions, EditOperation, EditSessionBase, ElementQuery, OperationIssue, OperationSchemaSet, PagePoint, ReadItem, ReadOptions, ReadResult, TextTarget } from "./types.js";
|
|
4
4
|
export interface EditEngineContext {
|
|
5
5
|
readonly format: EditableFormat;
|
|
6
6
|
readonly fileName?: string;
|
|
@@ -16,6 +16,8 @@ export interface EditEngineContext {
|
|
|
16
16
|
* earlier calls and carry the same envelope as the core's own reads.
|
|
17
17
|
*/
|
|
18
18
|
export interface EditSessionCore extends EditSessionBase<EditOperation, EditElement> {
|
|
19
|
+
/** The host's limits, for reads a typed session builds over the core. */
|
|
20
|
+
readonly limits: ResourceLimits;
|
|
19
21
|
readItem<T>(options: ReadOptions | undefined, task: (engine: EditEngine, signal: AbortSignal) => Promise<T | undefined>): Promise<ReadItem<T>>;
|
|
20
22
|
readItems<T>(options: ReadOptions | undefined, task: (engine: EditEngine, signal: AbortSignal) => Promise<readonly T[]>): Promise<ReadResult<T>>;
|
|
21
23
|
}
|
|
@@ -44,8 +46,17 @@ export interface MaterializedDocument {
|
|
|
44
46
|
/** For example `privacy-not-guaranteed` when a PDF full save could not be compacted. */
|
|
45
47
|
readonly warnings: readonly ViewerWarning[];
|
|
46
48
|
}
|
|
49
|
+
/** How a batch is written; travels with the batch to the engine and into the history, so a replay writes it the same way. */
|
|
50
|
+
export interface BatchMode {
|
|
51
|
+
/** Default `direct`; `tracked` writes revisions where the format has them. */
|
|
52
|
+
readonly changeMode?: ChangeMode;
|
|
53
|
+
/** The author of tracked changes. */
|
|
54
|
+
readonly author?: string;
|
|
55
|
+
/** ISO 8601, the date of tracked changes. */
|
|
56
|
+
readonly timestamp?: string;
|
|
57
|
+
}
|
|
47
58
|
/** A batch with the identity the core assigned to the state after it. */
|
|
48
|
-
export interface EngineBatch {
|
|
59
|
+
export interface EngineBatch extends BatchMode {
|
|
49
60
|
/** Unique within the session and never reused; engines derive created ids from it. */
|
|
50
61
|
readonly stateId: number;
|
|
51
62
|
readonly operations: readonly EditOperation[];
|
|
@@ -82,7 +93,7 @@ export interface EngineChange {
|
|
|
82
93
|
export interface EditEngine {
|
|
83
94
|
readonly schemas: OperationSchemaSet;
|
|
84
95
|
/** Validates against the current state; an empty result means the batch can be applied. */
|
|
85
|
-
validate(operations: readonly EditOperation[], signal: AbortSignal): Promise<readonly OperationIssue[]>;
|
|
96
|
+
validate(operations: readonly EditOperation[], signal: AbortSignal, mode?: BatchMode): Promise<readonly OperationIssue[]>;
|
|
86
97
|
/**
|
|
87
98
|
* Applies an already validated batch to the working copy. Same-batch
|
|
88
99
|
* references (`"$<n>"` targets) are resolved here; one that resolves to an
|
package/dist/edit/history.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { EngineBatch } from "./engine.js";
|
|
1
|
+
import type { BatchMode, EngineBatch } from "./engine.js";
|
|
2
2
|
import type { EditOperation } from "./types.js";
|
|
3
|
-
export interface HistoryEntry {
|
|
3
|
+
export interface HistoryEntry extends BatchMode {
|
|
4
4
|
readonly operations: readonly EditOperation[];
|
|
5
5
|
readonly label?: string;
|
|
6
6
|
/** Ids the batch created; an undo removes them again. */
|
|
@@ -15,6 +15,19 @@ export interface HistoryEntry {
|
|
|
15
15
|
readonly pageCountAfter: number;
|
|
16
16
|
/** Identifies the content after this batch; equal ids mean equal content. */
|
|
17
17
|
readonly stateId: number;
|
|
18
|
+
/**
|
|
19
|
+
* A restore to a named checkpoint: the content after this entry is the
|
|
20
|
+
* checkpoint's, so a replay starts from its retained bytes or from the
|
|
21
|
+
* original plus `batches`, never from the entries before.
|
|
22
|
+
*/
|
|
23
|
+
readonly base?: HistoryBase;
|
|
24
|
+
}
|
|
25
|
+
/** Where a restore entry's content comes from. */
|
|
26
|
+
export interface HistoryBase {
|
|
27
|
+
/** The checkpoint's state id; its bytes may be retained under it. */
|
|
28
|
+
readonly stateId: number;
|
|
29
|
+
/** The batches that build the checkpoint's state from the original. */
|
|
30
|
+
readonly batches: readonly EngineBatch[];
|
|
18
31
|
}
|
|
19
32
|
/**
|
|
20
33
|
* Linear undo history over an immutable original. Entries beyond the limit are
|
|
@@ -35,6 +48,8 @@ export declare class EditHistory {
|
|
|
35
48
|
get pageCount(): number;
|
|
36
49
|
/** State ids of every entry still in the history, folded and redo tail included. */
|
|
37
50
|
get stateIds(): readonly number[];
|
|
51
|
+
/** Every entry still in the history: folded ones, then the undoable ones and the redo tail. */
|
|
52
|
+
get allEntries(): readonly HistoryEntry[];
|
|
38
53
|
/** The entry `undo()` would revert, if any. */
|
|
39
54
|
get undoEntry(): HistoryEntry | undefined;
|
|
40
55
|
/** The entry `redo()` would re-apply, if any. */
|
|
@@ -46,8 +61,17 @@ export declare class EditHistory {
|
|
|
46
61
|
/** Entries applied when `position` undoable entries are applied, folded ones first. */
|
|
47
62
|
entriesAt(position: number): readonly HistoryEntry[];
|
|
48
63
|
get position(): number;
|
|
49
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Appends an entry with a fresh state id, or with `stateId` when the entry
|
|
66
|
+
* reproduces a known state (a restore to a checkpoint): the same id means
|
|
67
|
+
* the same content, so `dirty` stays exact.
|
|
68
|
+
*/
|
|
69
|
+
push(entry: Omit<HistoryEntry, "stateId">, stateId?: number): HistoryEntry;
|
|
50
70
|
undo(): void;
|
|
51
71
|
redo(): void;
|
|
52
72
|
clear(): void;
|
|
53
73
|
}
|
|
74
|
+
/** The engine batch an entry replays as: its operations and how they were written. */
|
|
75
|
+
export declare function batchOf(entry: HistoryEntry): EngineBatch;
|
|
76
|
+
/** The write mode fields of a batch or an entry, only those set. */
|
|
77
|
+
export declare function modeOf(mode: BatchMode): BatchMode;
|
package/dist/edit/history.js
CHANGED
|
@@ -42,7 +42,11 @@ export class EditHistory {
|
|
|
42
42
|
}
|
|
43
43
|
/** State ids of every entry still in the history, folded and redo tail included. */
|
|
44
44
|
get stateIds() {
|
|
45
|
-
return
|
|
45
|
+
return this.allEntries.map((entry) => entry.stateId);
|
|
46
|
+
}
|
|
47
|
+
/** Every entry still in the history: folded ones, then the undoable ones and the redo tail. */
|
|
48
|
+
get allEntries() {
|
|
49
|
+
return [...this.#folded, ...this.#entries];
|
|
46
50
|
}
|
|
47
51
|
/** The entry `undo()` would revert, if any. */
|
|
48
52
|
get undoEntry() {
|
|
@@ -58,10 +62,7 @@ export class EditHistory {
|
|
|
58
62
|
}
|
|
59
63
|
/** Batches applied to the original when `position` undoable entries are applied. */
|
|
60
64
|
batchesAt(position) {
|
|
61
|
-
return this.entriesAt(position).map(
|
|
62
|
-
stateId: entry.stateId,
|
|
63
|
-
operations: entry.operations,
|
|
64
|
-
}));
|
|
65
|
+
return this.entriesAt(position).map(batchOf);
|
|
65
66
|
}
|
|
66
67
|
/** Entries applied when `position` undoable entries are applied, folded ones first. */
|
|
67
68
|
entriesAt(position) {
|
|
@@ -70,10 +71,15 @@ export class EditHistory {
|
|
|
70
71
|
get position() {
|
|
71
72
|
return this.#position;
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Appends an entry with a fresh state id, or with `stateId` when the entry
|
|
76
|
+
* reproduces a known state (a restore to a checkpoint): the same id means
|
|
77
|
+
* the same content, so `dirty` stays exact.
|
|
78
|
+
*/
|
|
79
|
+
push(entry, stateId) {
|
|
74
80
|
const stored = Object.freeze({
|
|
75
81
|
...entry,
|
|
76
|
-
stateId: this.#nextStateId++,
|
|
82
|
+
stateId: stateId ?? this.#nextStateId++,
|
|
77
83
|
});
|
|
78
84
|
// A new change after an undo drops the redo tail.
|
|
79
85
|
this.#entries.length = this.#position;
|
|
@@ -99,3 +105,19 @@ export class EditHistory {
|
|
|
99
105
|
this.#position = 0;
|
|
100
106
|
}
|
|
101
107
|
}
|
|
108
|
+
/** The engine batch an entry replays as: its operations and how they were written. */
|
|
109
|
+
export function batchOf(entry) {
|
|
110
|
+
return {
|
|
111
|
+
stateId: entry.stateId,
|
|
112
|
+
operations: entry.operations,
|
|
113
|
+
...modeOf(entry),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** The write mode fields of a batch or an entry, only those set. */
|
|
117
|
+
export function modeOf(mode) {
|
|
118
|
+
return {
|
|
119
|
+
...(mode.changeMode === undefined ? {} : { changeMode: mode.changeMode }),
|
|
120
|
+
...(mode.author === undefined ? {} : { author: mode.author }),
|
|
121
|
+
...(mode.timestamp === undefined ? {} : { timestamp: mode.timestamp }),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -535,8 +535,11 @@ export class PdfEditDocument {
|
|
|
535
535
|
},
|
|
536
536
|
// Ids name the state the batch leads to, the operation and the item,
|
|
537
537
|
// so replaying the history reproduces them and an undone id is never
|
|
538
|
-
// handed out again.
|
|
539
|
-
|
|
538
|
+
// handed out again. A file saved by an earlier session carries the ids
|
|
539
|
+
// that session numbered from 1 as well, so one already on the page
|
|
540
|
+
// gets the first free `~n` instead (ACTION-886); the saved objects are
|
|
541
|
+
// part of the base, so a replay meets them and picks the same id.
|
|
542
|
+
newId: (pageIndex, suffix = "") => `${this.#unusedId(pageIndex, `${this.#pages[pageIndex].key}:n${stateId}.${operationIndex}.${created++}`)}${suffix}`,
|
|
540
543
|
withPage: (pageIndex, use) => this.#writePage(pageIndex, use),
|
|
541
544
|
appendObjects: (pageIndex, records) => {
|
|
542
545
|
const page = this.#pages[pageIndex];
|
|
@@ -686,10 +689,52 @@ export class PdfEditDocument {
|
|
|
686
689
|
}
|
|
687
690
|
}
|
|
688
691
|
record.objects = objects.map((object, index) => object.mark && stale.has(object.id)
|
|
689
|
-
? {
|
|
692
|
+
? {
|
|
693
|
+
id: `${record.key}:o${index}`,
|
|
694
|
+
type: object.type,
|
|
695
|
+
staleMarkId: object.id,
|
|
696
|
+
}
|
|
690
697
|
: object);
|
|
691
698
|
return record.objects;
|
|
692
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* `id`, or its first free `~n` when an object saved by an earlier session
|
|
702
|
+
* has it already. Only ids a session numbered (`<key>:n…`) can collide, so
|
|
703
|
+
* only those are gathered; the objects are read without the page's text.
|
|
704
|
+
*/
|
|
705
|
+
#unusedId(pageIndex, id) {
|
|
706
|
+
const record = this.#pages[pageIndex];
|
|
707
|
+
const objects = record.objects ?? this.#loadObjects(pageIndex);
|
|
708
|
+
const numbered = `${record.key}:n`;
|
|
709
|
+
const taken = new Set();
|
|
710
|
+
for (const object of objects) {
|
|
711
|
+
if (object.id.startsWith(numbered))
|
|
712
|
+
taken.add(object.id);
|
|
713
|
+
if (object.staleMarkId?.startsWith(numbered))
|
|
714
|
+
taken.add(object.staleMarkId);
|
|
715
|
+
}
|
|
716
|
+
if (!taken.has(id))
|
|
717
|
+
return id;
|
|
718
|
+
let suffix = 1;
|
|
719
|
+
while (taken.has(`${id}~${suffix}`))
|
|
720
|
+
suffix += 1;
|
|
721
|
+
return `${id}~${suffix}`;
|
|
722
|
+
}
|
|
723
|
+
/** A page's objects, loading the page alone when they are not known yet. */
|
|
724
|
+
#loadObjects(pageIndex) {
|
|
725
|
+
const { lib } = this.#pdfium;
|
|
726
|
+
const page = lib.FPDF_LoadPage(this.#document.handle, pageIndex);
|
|
727
|
+
if (!page)
|
|
728
|
+
throw new ViewerError("render-failed", "PDFium could not load the page", {
|
|
729
|
+
details: { pageIndex },
|
|
730
|
+
});
|
|
731
|
+
try {
|
|
732
|
+
return this.#objectsOf(pageIndex, page);
|
|
733
|
+
}
|
|
734
|
+
finally {
|
|
735
|
+
lib.FPDF_ClosePage(page);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
693
738
|
/** Marks from other sessions keep their id only if it cannot collide with ours. */
|
|
694
739
|
#ownMark(mark, pageKey) {
|
|
695
740
|
return mark.id.startsWith(`${pageKey}:`)
|
|
@@ -21,6 +21,12 @@ export interface ObjectRecord {
|
|
|
21
21
|
readonly id: string;
|
|
22
22
|
readonly type: number;
|
|
23
23
|
readonly mark?: MarkParams;
|
|
24
|
+
/**
|
|
25
|
+
* The id a mark the file still carries names, on an object listed as plain
|
|
26
|
+
* because the mark failed its check: a new id must not take it, or saving
|
|
27
|
+
* would join the new object to this one.
|
|
28
|
+
*/
|
|
29
|
+
readonly staleMarkId?: string;
|
|
24
30
|
}
|
|
25
31
|
/**
|
|
26
32
|
* Reads the objects of a loaded page in drawing order and turns them into
|
|
@@ -110,7 +110,13 @@ export class FontLibrary {
|
|
|
110
110
|
return undefined;
|
|
111
111
|
if (this.#fallback && this.#covers(this.#fallback, request.text))
|
|
112
112
|
return undefined;
|
|
113
|
-
|
|
113
|
+
// Name the character that actually stops the text: the first one the
|
|
114
|
+
// fallback lacks when it was tried, else the first the standard fonts
|
|
115
|
+
// cannot encode. Naming the first Cyrillic letter for a dash the
|
|
116
|
+
// fallback lacked sent people after the wrong character (ACTION-879).
|
|
117
|
+
const bad = this.#fallback
|
|
118
|
+
? this.#firstMissing(this.#fallback, request.text)
|
|
119
|
+
: firstNonWinAnsi(request.text);
|
|
114
120
|
return {
|
|
115
121
|
path: "/text",
|
|
116
122
|
code: "font-unavailable",
|
|
@@ -175,6 +181,10 @@ export class FontLibrary {
|
|
|
175
181
|
return candidates.sort((a, b) => score(b.font) - score(a.font))[0];
|
|
176
182
|
}
|
|
177
183
|
#covers(bytes, text) {
|
|
184
|
+
return this.#firstMissing(bytes, text) === undefined;
|
|
185
|
+
}
|
|
186
|
+
/** The first character of `text` the font has no glyph for; layout handles line breaks and spaces. */
|
|
187
|
+
#firstMissing(bytes, text) {
|
|
178
188
|
let coverage = this.#coverage.get(bytes);
|
|
179
189
|
if (!coverage) {
|
|
180
190
|
coverage = parseCmap(bytes);
|
|
@@ -185,9 +195,9 @@ export class FontLibrary {
|
|
|
185
195
|
if (code === 0x0a || code === 0x20)
|
|
186
196
|
continue;
|
|
187
197
|
if (!coverage.has(code))
|
|
188
|
-
return
|
|
198
|
+
return character;
|
|
189
199
|
}
|
|
190
|
-
return
|
|
200
|
+
return undefined;
|
|
191
201
|
}
|
|
192
202
|
#load(pdfium, document, bytes) {
|
|
193
203
|
let handles = this.#handles.get(document);
|
|
@@ -242,13 +252,6 @@ function isBold(font) {
|
|
|
242
252
|
function isItalic(font) {
|
|
243
253
|
return font.style !== "normal";
|
|
244
254
|
}
|
|
245
|
-
function firstUncovered(text, library) {
|
|
246
|
-
void library;
|
|
247
|
-
for (const character of text)
|
|
248
|
-
if (character !== "\n" && character !== " ")
|
|
249
|
-
return character;
|
|
250
|
-
return undefined;
|
|
251
|
-
}
|
|
252
255
|
/** The code points a TrueType font maps to glyphs, from its cmap table. */
|
|
253
256
|
/**
|
|
254
257
|
* Whether glyphs of a TrueType font have outline data: a subset font can keep
|
|
@@ -74,11 +74,19 @@ export const rotatePage = {
|
|
|
74
74
|
validate(operation, context, issue) {
|
|
75
75
|
if (operation.pageIndex >= context.pageCount)
|
|
76
76
|
issue("/pageIndex", "unknown-target", `No page ${operation.pageIndex}`);
|
|
77
|
+
// A field given as undefined counts as absent, as it does everywhere else.
|
|
78
|
+
if ((operation.rotation === undefined) === (operation.by === undefined))
|
|
79
|
+
issue("", "one-of", "Give exactly one of `rotation` and `by`");
|
|
77
80
|
},
|
|
78
81
|
apply(operation, context) {
|
|
79
82
|
const { lib } = context.pdfium;
|
|
80
83
|
context.withPage(operation.pageIndex, (page) => {
|
|
81
|
-
|
|
84
|
+
// A turn reads the angle the page has now, so it composes with what
|
|
85
|
+
// the file was saved with and with turns earlier in the history.
|
|
86
|
+
const quarters = operation.by !== undefined
|
|
87
|
+
? (lib.FPDFPage_GetRotation(page) + operation.by / 90) % 4
|
|
88
|
+
: operation.rotation / 90;
|
|
89
|
+
lib.FPDFPage_SetRotation(page, quarters);
|
|
82
90
|
});
|
|
83
91
|
context.invalidatePage(operation.pageIndex);
|
|
84
92
|
return {
|
|
@@ -2,10 +2,12 @@ import type { EditOperation, EditReceipt, TextRange } from "../types.js";
|
|
|
2
2
|
export interface MutationRecord {
|
|
3
3
|
/** The revision the call produced. */
|
|
4
4
|
readonly revision: number;
|
|
5
|
-
readonly kind: "apply" | "undo" | "redo" | "reset";
|
|
5
|
+
readonly kind: "apply" | "undo" | "redo" | "reset" | "restore";
|
|
6
6
|
/**
|
|
7
7
|
* The operations the call applied (apply, redo) or took back (undo, reset);
|
|
8
|
-
* for a reset every batch of the session, in order.
|
|
8
|
+
* for a reset every batch of the session, in order. A checkpoint restore
|
|
9
|
+
* carries none: its receipt's ids say what went and what came back, and
|
|
10
|
+
* offsets inside surviving elements are left as they were.
|
|
9
11
|
*/
|
|
10
12
|
readonly operations: readonly EditOperation[];
|
|
11
13
|
readonly receipt: EditReceipt;
|
package/dist/edit/pdf/schemas.js
CHANGED
|
@@ -170,10 +170,13 @@ export const pdfOperationSchemas = Object.freeze({
|
|
|
170
170
|
from: { type: "integer", minimum: 0 },
|
|
171
171
|
to: { type: "integer", minimum: 0 },
|
|
172
172
|
}, ["from", "to"]),
|
|
173
|
+
// Exactly one of `rotation` (absolute) and `by` (a turn from the current
|
|
174
|
+
// angle); the handler reports a batch that gives both or neither.
|
|
173
175
|
rotatePage: operation("rotatePage", {
|
|
174
176
|
pageIndex: { type: "integer", minimum: 0 },
|
|
175
177
|
rotation: { enum: [0, 90, 180, 270] },
|
|
176
|
-
|
|
178
|
+
by: { enum: [90, 180, 270] },
|
|
179
|
+
}, ["pageIndex"]),
|
|
177
180
|
insertShape: operation("insertShape", {
|
|
178
181
|
pageIndex: { type: "integer", minimum: 0 },
|
|
179
182
|
shape: { enum: ["rectangle", "ellipse", "line"] },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TextSelection } from "../../contracts.js";
|
|
2
|
+
import type { DescribeOptions, DocumentDescription, EditCheckpoint, OutlineOptions, OutlineResult, TargetCandidate, TargetQuery, ToolCall, ToolCallOptions, ToolResult, ToolSet } from "../ai/types.js";
|
|
2
3
|
import type { EditSessionCore } from "../engine.js";
|
|
3
4
|
import type { ApplyOptions, AssetOptions, EditFindOptions, EditOperation, EditReceipt, EditState, ElementQuery, HistoryOptions, OperationSchemaSet, PagePoint, PageRect, ReadItem, ReadOptions, ReadResult, SavedDocument, TextPosition, TextRange, TextTarget } from "../types.js";
|
|
4
5
|
import type { DeleteElementOperation, DeletePageOperation, Fields, InsertImageOperation, InsertPageOperation, InsertShapeOperation, InsertTableOperation, InsertTextBoxOperation, MoveElementOperation, MovePageOperation, PdfEditSession, PdfElement, PdfOperation, PdfSaveOptions, ReplaceTextOperation, ResizeElementOperation, RotatePageOperation, SetShapeStyleOperation, SetTableCellOperation, PageBitmap, PageLayout, RenderOptions, SetTextStyleOperation, TextLayout } from "./types.js";
|
|
@@ -28,6 +29,15 @@ export declare class PdfSession implements PdfEditSession {
|
|
|
28
29
|
getElement(id: string, options?: ReadOptions): Promise<ReadItem<PdfElement>>;
|
|
29
30
|
elementsAt(pageIndex: number, point: PagePoint, options?: ReadOptions): Promise<ReadResult<PdfElement>>;
|
|
30
31
|
findText(query: string, options?: EditFindOptions): Promise<ReadResult<TextTarget>>;
|
|
32
|
+
getOutline(options?: OutlineOptions): Promise<OutlineResult>;
|
|
33
|
+
describe(options?: DescribeOptions): Promise<ReadItem<DocumentDescription>>;
|
|
34
|
+
resolveTargets(query: TargetQuery, options?: ReadOptions): Promise<ReadResult<TargetCandidate>>;
|
|
35
|
+
createCheckpoint(label?: string): Promise<EditCheckpoint>;
|
|
36
|
+
listCheckpoints(): readonly EditCheckpoint[];
|
|
37
|
+
restoreCheckpoint(id: string, options?: HistoryOptions): Promise<EditReceipt>;
|
|
38
|
+
dropCheckpoint(id: string): void;
|
|
39
|
+
get tools(): ToolSet;
|
|
40
|
+
callTool(call: ToolCall, options?: ToolCallOptions): Promise<ToolResult>;
|
|
31
41
|
getTextLayout(elementId: string, options?: ReadOptions): Promise<ReadItem<TextLayout>>;
|
|
32
42
|
positionAt(pageIndex: number, point: PagePoint, options?: ReadOptions): Promise<ReadItem<TextPosition>>;
|
|
33
43
|
rangeRects(range: TextRange, options?: ReadOptions): Promise<ReadResult<PageRect>>;
|
package/dist/edit/pdf/session.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { ViewerError } from "../../errors.js";
|
|
2
|
+
import { readDescription, readOutline } from "../ai/outline.js";
|
|
3
|
+
import { resolveTargets } from "../ai/targets.js";
|
|
4
|
+
import { buildToolSet, callTool as runTool } from "../ai/tools.js";
|
|
2
5
|
import { reportError } from "../session.js";
|
|
3
6
|
import { rectContains } from "./engine/geometry.js";
|
|
4
7
|
import { mapRangeThrough } from "./range-map.js";
|
|
@@ -11,6 +14,7 @@ import { resolveSelection } from "./selection.js";
|
|
|
11
14
|
export class PdfSession {
|
|
12
15
|
format = "pdf";
|
|
13
16
|
#core;
|
|
17
|
+
#tools;
|
|
14
18
|
/** Committed calls, oldest first, for `mapRange`; bounded by `LOG_LIMIT`. */
|
|
15
19
|
#log = [];
|
|
16
20
|
/** Batches applied and not undone, and those undone and not redone. */
|
|
@@ -81,6 +85,12 @@ export class PdfSession {
|
|
|
81
85
|
this.#applied = [];
|
|
82
86
|
this.#undone = [];
|
|
83
87
|
break;
|
|
88
|
+
case "restore":
|
|
89
|
+
// The checkpoint's content replaces whatever the stacks describe;
|
|
90
|
+
// the undo of a restore is the core's to replay, not the log's.
|
|
91
|
+
this.#applied = [];
|
|
92
|
+
this.#undone = [];
|
|
93
|
+
break;
|
|
84
94
|
}
|
|
85
95
|
this.#log.push({
|
|
86
96
|
revision: receipt.revision,
|
|
@@ -156,6 +166,35 @@ export class PdfSession {
|
|
|
156
166
|
findText(query, options) {
|
|
157
167
|
return this.#core.findText(query, options);
|
|
158
168
|
}
|
|
169
|
+
getOutline(options) {
|
|
170
|
+
return readOutline(this, this.#core.limits, options);
|
|
171
|
+
}
|
|
172
|
+
describe(options) {
|
|
173
|
+
return readDescription(this, this.#core.limits, options);
|
|
174
|
+
}
|
|
175
|
+
resolveTargets(query, options) {
|
|
176
|
+
return resolveTargets(this, query, options);
|
|
177
|
+
}
|
|
178
|
+
createCheckpoint(label) {
|
|
179
|
+
return this.#core.createCheckpoint(label);
|
|
180
|
+
}
|
|
181
|
+
listCheckpoints() {
|
|
182
|
+
return this.#core.listCheckpoints();
|
|
183
|
+
}
|
|
184
|
+
async restoreCheckpoint(id, options) {
|
|
185
|
+
const receipt = await this.#core.restoreCheckpoint(id, options);
|
|
186
|
+
this.#record("restore", [], receipt);
|
|
187
|
+
return receipt;
|
|
188
|
+
}
|
|
189
|
+
dropCheckpoint(id) {
|
|
190
|
+
this.#core.dropCheckpoint(id);
|
|
191
|
+
}
|
|
192
|
+
get tools() {
|
|
193
|
+
return (this.#tools ??= buildToolSet(this.format, this.schemas));
|
|
194
|
+
}
|
|
195
|
+
callTool(call, options) {
|
|
196
|
+
return runTool(this, this.tools, call, options);
|
|
197
|
+
}
|
|
159
198
|
getTextLayout(elementId, options) {
|
|
160
199
|
return this.#core.readItem(options, (engine, signal) => pdfReads(engine).textLayout(elementId, signal));
|
|
161
200
|
}
|
package/dist/edit/pdf/types.d.ts
CHANGED
|
@@ -192,12 +192,24 @@ export interface SetShapeStyleOperation {
|
|
|
192
192
|
/** A new fill, `null` to remove it, or absent to keep it. */
|
|
193
193
|
readonly fill?: PdfFill | null;
|
|
194
194
|
}
|
|
195
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Sets a page's rotation, or turns it from where it stands: a host offering
|
|
197
|
+
* "rotate" does not need to know the angle a file was saved with, and a
|
|
198
|
+
* replay of the history turns from the same angle again.
|
|
199
|
+
*/
|
|
200
|
+
export type RotatePageOperation = {
|
|
196
201
|
readonly op: "rotatePage";
|
|
197
202
|
readonly pageIndex: number;
|
|
198
203
|
/** Absolute clockwise rotation in degrees. */
|
|
199
204
|
readonly rotation: 0 | 90 | 180 | 270;
|
|
200
|
-
|
|
205
|
+
readonly by?: never;
|
|
206
|
+
} | {
|
|
207
|
+
readonly op: "rotatePage";
|
|
208
|
+
readonly pageIndex: number;
|
|
209
|
+
/** Clockwise turn in degrees from the page's current rotation. */
|
|
210
|
+
readonly by: 90 | 180 | 270;
|
|
211
|
+
readonly rotation?: never;
|
|
212
|
+
};
|
|
201
213
|
/** The PDF operation union; operations are added as they ship. */
|
|
202
214
|
export type PdfOperation = InsertTextBoxOperation | ReplaceTextOperation | SetTextStyleOperation | ResizeElementOperation | MoveElementOperation | DeleteElementOperation | InsertPageOperation | DeletePageOperation | MovePageOperation | RotatePageOperation | InsertShapeOperation | SetShapeStyleOperation | InsertImageOperation | InsertTableOperation | SetTableCellOperation;
|
|
203
215
|
export interface PdfSaveOptions extends SaveOptions {
|
|
@@ -321,7 +333,7 @@ export interface PdfEditSession extends EditSessionBase<PdfOperation, PdfElement
|
|
|
321
333
|
deletePage(fields: Fields<DeletePageOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|
|
322
334
|
/** Reorders pages. */
|
|
323
335
|
movePage(fields: Fields<MovePageOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|
|
324
|
-
/** Sets a page's rotation. */
|
|
336
|
+
/** Sets a page's rotation, or turns it from the one it has. */
|
|
325
337
|
rotatePage(fields: Fields<RotatePageOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|
|
326
338
|
/** Draws a rectangle, ellipse or line as a path object. */
|
|
327
339
|
insertShape(fields: Fields<InsertShapeOperation>, options?: ApplyOptions): Promise<EditReceipt>;
|