@aseity/idocs-preview 0.2.0 → 0.4.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.
@@ -1,15 +1,12 @@
1
- // The Worker side: owns the wasm instance and answers the protocol in
2
- // protocol.ts. `serve` takes any message endpoint, so Node tests run the same
3
- // code behind a MessageChannel. Layout is synchronous inside one wasm call;
4
- // resources the host has not answered yet come back as `needs`, and the open
5
- // is retried once they arrive.
6
- import init, { openDocument } from '../../wasm/idocs_wasm.js';
1
+ import init, { exportDocx, openDocument, WasmEditorSession } from '../../wasm/idocs_wasm.js';
7
2
  import { canvasMeasure } from './fonts.js';
3
+ const textDecoder = new TextDecoder();
8
4
  export function serve(endpoint, options = {}) {
9
5
  let ready;
10
6
  const start = (wasm = options.wasm) => (ready ??= init(wasm === undefined ? undefined : { module_or_path: wasm }));
11
7
  const measure = options.measure === undefined ? canvasMeasure() : (options.measure ?? undefined);
12
8
  const pending = new Map();
9
+ const editors = new Map();
13
10
  const post = (message, transfer = []) => endpoint.postMessage(message, transfer);
14
11
  const attempt = (id) => {
15
12
  const job = pending.get(id);
@@ -19,12 +16,7 @@ export function serve(endpoint, options = {}) {
19
16
  try {
20
17
  if (!measure)
21
18
  throw new Error('Browser font measurement requires OffscreenCanvas 2D');
22
- if (job.progressive)
23
- post({ type: 'pageReset', id });
24
- const onPage = job.progressive ? (packet) => {
25
- post({ type: 'page', id, ...packet }, [packet.data.buffer, ...packet.images.map(bytes => bytes.buffer)]);
26
- } : undefined;
27
- outcome = openDocument(job.kind, job.bytes, job.name, job.assets, measure, onPage);
19
+ outcome = job.run(measure);
28
20
  }
29
21
  catch (error) {
30
22
  pending.delete(id);
@@ -40,15 +32,83 @@ export function serve(endpoint, options = {}) {
40
32
  post({ type: 'failed', id, error: JSON.parse(outcome.json) });
41
33
  return;
42
34
  }
43
- post({ type: 'opened', id, json: outcome.json, images: outcome.images, data: outcome.data }, [outcome.data.buffer, ...outcome.images.map((bytes) => bytes.buffer)]);
35
+ if (outcome.status === 'exported') {
36
+ post({ type: 'exported', id, bytes: outcome.bytes }, [outcome.bytes.buffer]);
37
+ return;
38
+ }
39
+ if (job.kind === 'editorJson') {
40
+ post({ type: 'editorJson', id, json: outcome.json });
41
+ return;
42
+ }
43
+ const transfers = [
44
+ outcome.data.buffer,
45
+ ...outcome.images.map((bytes) => bytes.buffer),
46
+ ];
47
+ if (job.kind === 'editorRendered') {
48
+ post({
49
+ type: 'editorRendered',
50
+ id,
51
+ json: outcome.json,
52
+ images: outcome.images,
53
+ data: outcome.data,
54
+ metaJson: outcome.metaJson ?? '{}',
55
+ }, transfers);
56
+ return;
57
+ }
58
+ post({ type: 'opened', id, json: outcome.json, images: outcome.images, data: outcome.data }, transfers);
59
+ };
60
+ const enqueueOpen = (message) => {
61
+ const assets = new Map();
62
+ const progressive = message.progressive ?? false;
63
+ pending.set(message.id, {
64
+ kind: 'open',
65
+ assets,
66
+ run: (activeMeasure) => {
67
+ if (progressive)
68
+ post({ type: 'pageReset', id: message.id });
69
+ const onPage = progressive
70
+ ? (packet) => {
71
+ post({ type: 'page', id: message.id, ...packet }, [packet.data.buffer, ...packet.images.map((b) => b.buffer)]);
72
+ }
73
+ : undefined;
74
+ return openDocument(message.kind, message.bytes, message.name, assets, activeMeasure, onPage);
75
+ },
76
+ });
77
+ };
78
+ const enqueueEditorCall = (id, editorId, kind, exec) => {
79
+ const existing = editors.get(editorId);
80
+ if (!existing) {
81
+ pending.set(id, {
82
+ kind,
83
+ assets: new Map(),
84
+ run: () => ({
85
+ status: 'failed',
86
+ json: JSON.stringify({ code: 'session-closed', message: 'The editor session is closed', diagnostics: [] }),
87
+ }),
88
+ });
89
+ return;
90
+ }
91
+ pending.set(id, {
92
+ kind,
93
+ assets: existing.assets,
94
+ run: (activeMeasure) => exec(existing, activeMeasure),
95
+ });
44
96
  };
45
97
  endpoint.addEventListener('message', async (event) => {
46
98
  const message = event.data;
47
99
  if (message.type === 'init') {
48
- // Start fetching early; a failure is reported to the first open.
49
100
  start(message.wasmUrl).catch(() => { });
50
101
  return;
51
102
  }
103
+ if (message.type === 'editorDispose') {
104
+ const existing = editors.get(message.editorId);
105
+ if (existing) {
106
+ existing.session.dispose();
107
+ existing.session.free?.();
108
+ editors.delete(message.editorId);
109
+ }
110
+ return;
111
+ }
52
112
  try {
53
113
  await start();
54
114
  }
@@ -58,22 +118,69 @@ export function serve(endpoint, options = {}) {
58
118
  post({ type: 'failed', id, error: { code: 'wasm-unavailable', message: String(error), diagnostics: [] } });
59
119
  return;
60
120
  }
61
- if (message.type === 'open') {
62
- pending.set(message.id, {
63
- kind: message.kind,
64
- progressive: message.progressive ?? false,
65
- name: message.name,
66
- bytes: message.bytes,
67
- assets: new Map(),
68
- });
69
- }
70
- else if (message.type === 'resources') {
71
- const job = pending.get(message.id);
72
- if (!job)
73
- return;
74
- for (const { need, bytes } of message.answers) {
75
- job.assets.set(need.name, bytes);
121
+ switch (message.type) {
122
+ case 'exportDocx': {
123
+ const assets = new Map();
124
+ pending.set(message.id, {
125
+ kind: 'exportDocx',
126
+ assets,
127
+ run: (activeMeasure) => exportDocx(message.bytes, message.name, assets, activeMeasure),
128
+ });
129
+ break;
130
+ }
131
+ case 'open':
132
+ enqueueOpen(message);
133
+ break;
134
+ case 'resources': {
135
+ const job = pending.get(message.id);
136
+ if (!job)
137
+ return;
138
+ for (const { need, bytes } of message.answers) {
139
+ job.assets.set(need.name, bytes);
140
+ }
141
+ break;
142
+ }
143
+ case 'editorOpen': {
144
+ const session = new WasmEditorSession();
145
+ const assets = new Map();
146
+ editors.set(message.editorId, { session, assets });
147
+ const source = textDecoder.decode(message.bytes);
148
+ pending.set(message.id, {
149
+ kind: 'editorJson',
150
+ assets,
151
+ run: (activeMeasure) => session.open(source, message.name, assets, activeMeasure),
152
+ });
153
+ break;
154
+ }
155
+ case 'editorRange':
156
+ enqueueEditorCall(message.id, message.editorId, 'editorJson', ({ session }) => session.range(message.revision, message.startJson, message.endJson));
157
+ break;
158
+ case 'editorApply':
159
+ enqueueEditorCall(message.id, message.editorId, 'editorJson', ({ session, assets }, m) => session.apply(message.base, message.editJson, assets, m));
160
+ break;
161
+ case 'editorUndo':
162
+ enqueueEditorCall(message.id, message.editorId, 'editorJson', ({ session, assets }, m) => session.undo(message.base, assets, m));
163
+ break;
164
+ case 'editorRedo':
165
+ enqueueEditorCall(message.id, message.editorId, 'editorJson', ({ session, assets }, m) => session.redo(message.base, assets, m));
166
+ break;
167
+ case 'editorRefresh': {
168
+ const source = textDecoder.decode(message.bytes);
169
+ enqueueEditorCall(message.id, message.editorId, 'editorJson', ({ session, assets }, m) => session.refresh(message.base, source, assets, m));
170
+ break;
76
171
  }
172
+ case 'editorMarkSaved':
173
+ enqueueEditorCall(message.id, message.editorId, 'editorJson', ({ session }) => session.markSaved(message.revision));
174
+ break;
175
+ case 'editorSnapshot':
176
+ enqueueEditorCall(message.id, message.editorId, 'editorJson', ({ session }) => session.snapshot());
177
+ break;
178
+ case 'editorRender':
179
+ enqueueEditorCall(message.id, message.editorId, 'editorRendered', ({ session, assets }, m) => session.render(assets, m));
180
+ break;
181
+ case 'editorExportDocx':
182
+ enqueueEditorCall(message.id, message.editorId, 'editorExport', ({ session, assets }, m) => session.exportDocx(assets, m));
183
+ break;
77
184
  }
78
185
  attempt(message.id);
79
186
  });
@@ -1,16 +1,11 @@
1
- import type { DocumentKind, Endpoint, PreviewDocument, PreviewPage } from './protocol.js';
1
+ import { type OpenEditorOptions, type EditorSession, lineCaretX, locateLineSource } from './editor.js';
2
+ import { type DocumentKind, type PreviewDocument, type PreviewPage, type Read, type SessionOptions } from './protocol.js';
2
3
  export type * from './display.js';
3
4
  export type * from './protocol.js';
4
- export interface SessionOptions {
5
- /** Worker script address; defaults to the bundled worker beside this module. */
6
- workerUrl?: string | URL;
7
- /** `.wasm` address; defaults to the file next to the wasm glue. */
8
- wasmUrl?: string | URL;
9
- /** An already created worker (a MessagePort works too); `workerUrl` is then ignored. */
10
- worker?: Endpoint;
11
- }
5
+ export { PreviewError, type Read, type SessionOptions, } from './protocol.js';
6
+ export type { Commit, Edit, EditRange, EditorFrame, EditorSession, EditorState, ImageAlign, InsertBlock, LayoutState, LineHit, Mark, OpenEditorOptions, RenderStats, Selection, SourceSnapshot, TableAction, } from './editor.js';
7
+ export { lineCaretX, locateLineSource };
12
8
  type Bytes = Uint8Array | ArrayBuffer;
13
- export type Read = (name: string) => Bytes | null | undefined | Promise<Bytes | null | undefined>;
14
9
  export interface OpenOptions {
15
10
  /** Receive provisional pages while the worker builds the remaining display list. */
16
11
  onPage?: (page: PreviewPage) => void;
@@ -25,16 +20,9 @@ export interface OpenOptions {
25
20
  /** Reads a path relative to the source document (images). Throw or return null when absent. */
26
21
  readResource?: Read;
27
22
  }
28
- export declare class PreviewError extends Error {
29
- code: string;
30
- diagnostics: PreviewDocument['diagnostics'];
31
- constructor(error: {
32
- code: string;
33
- message: string;
34
- diagnostics: PreviewDocument['diagnostics'];
35
- });
36
- }
23
+ export type ExportOptions = Pick<OpenOptions, 'name' | 'readResource'>;
37
24
  export interface Session {
25
+ exportDocx(bytes: Bytes, options?: ExportOptions): Promise<Uint8Array>;
38
26
  open(bytes: Bytes, options?: OpenOptions): Promise<PreviewDocument>;
39
27
  /**
40
28
  * Terminates the worker; pending opens reject. A worker that fails on its
@@ -43,5 +31,6 @@ export interface Session {
43
31
  */
44
32
  dispose(): void;
45
33
  }
34
+ export declare function openEditor(options: OpenEditorOptions): Promise<EditorSession>;
46
35
  export declare function createSession(options?: SessionOptions): Session;
47
36
  export type { Measure } from './fonts.js';
@@ -1,14 +1,8 @@
1
+ import { openEditorWith, lineCaretX, locateLineSource, } from './editor.js';
2
+ import { copyBytes, PreviewError, } from './protocol.js';
1
3
  import { decodeCompact } from './wire.js';
2
- export class PreviewError extends Error {
3
- code;
4
- diagnostics;
5
- constructor(error) {
6
- super(error.message);
7
- this.name = 'PreviewError';
8
- this.code = error.code;
9
- this.diagnostics = error.diagnostics;
10
- }
11
- }
4
+ export { PreviewError, } from './protocol.js';
5
+ export { lineCaretX, locateLineSource };
12
6
  function isZip(bytes) {
13
7
  return bytes[0] === 0x50 && bytes[1] === 0x4b;
14
8
  }
@@ -18,18 +12,20 @@ function isWordFile(bytes, name) {
18
12
  OLE_MAGIC.every((byte, i) => bytes[i] === byte) ||
19
13
  /\.(docx|docm|dotx|dotm|doc)$/i.test(name ?? ''));
20
14
  }
21
- /** A private copy whose buffer can be transferred (a Node Buffer may share its pool). */
22
- function copyBytes(bytes) {
23
- return new Uint8Array(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
24
- }
25
- export function createSession(options = {}) {
26
- const worker = options.worker ??
15
+ function createWorker(options = {}) {
16
+ return (options.worker ??
27
17
  (options.workerUrl
28
18
  ? new Worker(options.workerUrl, { type: 'module' })
29
19
  : // Literally `new Worker(new URL(...), ...)`: the form bundlers
30
20
  // (Turbopack, webpack, Vite) recognize to bundle the worker with its
31
21
  // imports. Behind `??` they copy the file as a bare asset instead.
32
- new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }));
22
+ new Worker(new URL('./worker.js', import.meta.url), { type: 'module' })));
23
+ }
24
+ export function openEditor(options) {
25
+ return openEditorWith(createWorker(options.runtime), options);
26
+ }
27
+ export function createSession(options = {}) {
28
+ const worker = createWorker(options);
33
29
  const post = (message, transfer = []) => worker.postMessage(message, transfer);
34
30
  const jobs = new Map();
35
31
  let next = 1;
@@ -52,14 +48,14 @@ export function createSession(options = {}) {
52
48
  const job = jobs.get(message.id);
53
49
  if (!job)
54
50
  return;
55
- if (message.type === 'pageReset') {
51
+ if (message.type === 'pageReset' && job.kind === 'open') {
56
52
  job.pages = [];
57
53
  job.images = [];
58
54
  job.previewImages = [];
59
55
  job.outlines = [];
60
56
  return;
61
57
  }
62
- if (message.type === 'page') {
58
+ if (message.type === 'page' && job.kind === 'open') {
63
59
  try {
64
60
  job.images.push(...message.images);
65
61
  const partial = decodeCompact(message.json, message.data, message.images);
@@ -86,7 +82,10 @@ export function createSession(options = {}) {
86
82
  return;
87
83
  }
88
84
  jobs.delete(message.id);
89
- if (message.type === 'opened') {
85
+ if (message.type === 'exported' && job.kind === 'export') {
86
+ job.resolve(message.bytes);
87
+ }
88
+ else if (message.type === 'opened' && job.kind === 'open') {
90
89
  try {
91
90
  const doc = decodeCompact(message.json, message.data, job.options.onPage ? job.images : message.images);
92
91
  if (job.options.onPage) {
@@ -100,8 +99,10 @@ export function createSession(options = {}) {
100
99
  job.reject(new PreviewError({ code: 'internal-error', message: String(error), diagnostics: [] }));
101
100
  }
102
101
  }
103
- else
102
+ else if (message.type === 'failed')
104
103
  job.reject(new PreviewError(message.error));
104
+ else
105
+ job.reject(new PreviewError({ code: 'internal-error', message: 'Unexpected worker response', diagnostics: [] }));
105
106
  });
106
107
  // A worker script that fails to load or run, or a message that cannot be
107
108
  // deserialized, leaves no reply to wait for: give up on the worker.
@@ -124,6 +125,18 @@ export function createSession(options = {}) {
124
125
  worker.start?.();
125
126
  post({ type: 'init', wasmUrl: options.wasmUrl?.toString() });
126
127
  return {
128
+ exportDocx(input, exportOptions = {}) {
129
+ if (disposed)
130
+ return Promise.reject(new Error('The preview session is disposed'));
131
+ if (broken)
132
+ return Promise.reject(broken);
133
+ const bytes = copyBytes(input);
134
+ const id = next++;
135
+ return new Promise((resolve, reject) => {
136
+ jobs.set(id, { kind: 'export', options: exportOptions, resolve, reject });
137
+ post({ type: 'exportDocx', id, name: exportOptions.name ?? 'document.idoc', bytes }, [bytes.buffer]);
138
+ });
139
+ },
127
140
  open(input, openOptions = {}) {
128
141
  if (disposed)
129
142
  return Promise.reject(new Error('The preview session is disposed'));
@@ -132,7 +145,7 @@ export function createSession(options = {}) {
132
145
  const bytes = copyBytes(input);
133
146
  const id = next++;
134
147
  return new Promise((resolve, reject) => {
135
- jobs.set(id, { options: openOptions, resolve, reject, pages: [], images: [], previewImages: [], outlines: [] });
148
+ jobs.set(id, { kind: 'open', options: openOptions, resolve, reject, pages: [], images: [], previewImages: [], outlines: [] });
136
149
  post({
137
150
  type: 'open',
138
151
  progressive: openOptions.onPage !== undefined,
@@ -76,6 +76,27 @@ export interface PreviewDocument {
76
76
  /** For `.idoc`: what export to DOCX would lose. For DOCX: what DOCX conversion lost. */
77
77
  conversionLosses: ConversionLoss[];
78
78
  }
79
+ export type PreviewDiagnostic = Diagnostic;
80
+ export interface SessionOptions {
81
+ /** Worker script address; defaults to the bundled worker beside this module. */
82
+ workerUrl?: string | URL;
83
+ /** `.wasm` address; defaults to the file next to the wasm glue. */
84
+ wasmUrl?: string | URL;
85
+ /** An already created worker (a MessagePort works too); `workerUrl` is then ignored. */
86
+ worker?: Endpoint;
87
+ }
88
+ export type Bytes = Uint8Array | ArrayBuffer;
89
+ export type Read = (name: string) => Bytes | null | undefined | Promise<Bytes | null | undefined>;
90
+ export declare class PreviewError extends Error {
91
+ code: string;
92
+ diagnostics: Diagnostic[];
93
+ constructor(error: {
94
+ code: string;
95
+ message: string;
96
+ diagnostics: Diagnostic[];
97
+ });
98
+ }
99
+ export declare function copyBytes(bytes: Bytes): Uint8Array;
79
100
  /** A lookup core made that the host has not answered yet. */
80
101
  export type ResourceNeed = {
81
102
  kind: 'asset';
@@ -84,6 +105,11 @@ export type ResourceNeed = {
84
105
  export type ToWorker = {
85
106
  type: 'init';
86
107
  wasmUrl?: string;
108
+ } | {
109
+ type: 'exportDocx';
110
+ id: number;
111
+ name: string;
112
+ bytes: Uint8Array;
87
113
  } | {
88
114
  type: 'open';
89
115
  id: number;
@@ -100,6 +126,61 @@ export type ToWorker = {
100
126
  need: ResourceNeed;
101
127
  bytes: Uint8Array | null;
102
128
  }[];
129
+ } | {
130
+ type: 'editorOpen';
131
+ id: number;
132
+ editorId: number;
133
+ name: string;
134
+ bytes: Uint8Array;
135
+ } | {
136
+ type: 'editorRange';
137
+ id: number;
138
+ editorId: number;
139
+ revision: string;
140
+ startJson: string;
141
+ endJson: string;
142
+ } | {
143
+ type: 'editorApply';
144
+ id: number;
145
+ editorId: number;
146
+ base: string;
147
+ editJson: string;
148
+ } | {
149
+ type: 'editorUndo';
150
+ id: number;
151
+ editorId: number;
152
+ base: string;
153
+ } | {
154
+ type: 'editorRedo';
155
+ id: number;
156
+ editorId: number;
157
+ base: string;
158
+ } | {
159
+ type: 'editorRefresh';
160
+ id: number;
161
+ editorId: number;
162
+ base: string;
163
+ bytes: Uint8Array;
164
+ } | {
165
+ type: 'editorMarkSaved';
166
+ id: number;
167
+ editorId: number;
168
+ revision: string;
169
+ } | {
170
+ type: 'editorSnapshot';
171
+ id: number;
172
+ editorId: number;
173
+ } | {
174
+ type: 'editorRender';
175
+ id: number;
176
+ editorId: number;
177
+ } | {
178
+ type: 'editorExportDocx';
179
+ id: number;
180
+ editorId: number;
181
+ } | {
182
+ type: 'editorDispose';
183
+ editorId: number;
103
184
  };
104
185
  export interface PreviewPage {
105
186
  page: DisplayPage;
@@ -120,12 +201,27 @@ export type FromWorker = {
120
201
  type: 'needs';
121
202
  id: number;
122
203
  needs: ResourceNeed[];
204
+ } | {
205
+ type: 'exported';
206
+ id: number;
207
+ bytes: Uint8Array;
123
208
  } | {
124
209
  type: 'opened';
125
210
  id: number;
126
211
  json: string;
127
212
  images: Uint8Array[];
128
213
  data: Uint8Array;
214
+ } | {
215
+ type: 'editorJson';
216
+ id: number;
217
+ json: string;
218
+ } | {
219
+ type: 'editorRendered';
220
+ id: number;
221
+ json: string;
222
+ images: Uint8Array[];
223
+ data: Uint8Array;
224
+ metaJson: string;
129
225
  } | {
130
226
  type: 'failed';
131
227
  id: number;
@@ -1 +1,13 @@
1
- export {};
1
+ export class PreviewError extends Error {
2
+ code;
3
+ diagnostics;
4
+ constructor(error) {
5
+ super(error.message);
6
+ this.name = 'PreviewError';
7
+ this.code = error.code;
8
+ this.diagnostics = error.diagnostics;
9
+ }
10
+ }
11
+ export function copyBytes(bytes) {
12
+ return new Uint8Array(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
13
+ }
@@ -0,0 +1,80 @@
1
+ import { type Anchor, type DisplayPage, type EditRange, type FlowId, type GeneratedRole, type ReadOnlyReason } from '../core/index.js';
2
+ import { type HighlightBox, type PagePoint } from './selection.js';
3
+ export interface CaretBox {
4
+ readonly page: number;
5
+ readonly x: number;
6
+ readonly top: number;
7
+ readonly bottom: number;
8
+ readonly baseline: number;
9
+ readonly height: number;
10
+ readonly turn?: [number, number];
11
+ }
12
+ export interface ReadOnlyTarget {
13
+ readonly page: number;
14
+ readonly path: string;
15
+ readonly reason: ReadOnlyReason;
16
+ readonly role?: GeneratedRole;
17
+ readonly box: HighlightBox;
18
+ readonly before?: Anchor;
19
+ readonly after?: Anchor;
20
+ }
21
+ export type EditPointHit = {
22
+ kind: 'editable';
23
+ anchor: Anchor;
24
+ caret: CaretBox;
25
+ page: number;
26
+ path: string;
27
+ } | {
28
+ kind: 'readOnly';
29
+ target: ReadOnlyTarget;
30
+ } | {
31
+ kind: 'empty';
32
+ };
33
+ export declare class EditDocumentIndex {
34
+ #private;
35
+ constructor(pages: readonly DisplayPage[]);
36
+ flowPath(flow: FlowId): string | undefined;
37
+ firstFlow(): FlowId | undefined;
38
+ flowBounds(flow: FlowId): {
39
+ start: Anchor;
40
+ end: Anchor;
41
+ } | undefined;
42
+ locatePoint(point: PagePoint): EditPointHit;
43
+ locatePointInFlow(flow: FlowId, point: PagePoint): Anchor | undefined;
44
+ wordRangeInFlow(flow: FlowId, point: PagePoint): {
45
+ start: Anchor;
46
+ end: Anchor;
47
+ } | undefined;
48
+ selectionPageBounds(range: EditRange): {
49
+ page: number;
50
+ bounds: {
51
+ left: number;
52
+ right: number;
53
+ top: number;
54
+ bottom: number;
55
+ };
56
+ } | undefined;
57
+ caretForAnchor(anchor: Anchor): CaretBox | undefined;
58
+ highlightsForRange(range: EditRange, page: number): HighlightBox[];
59
+ selectedText(range: EditRange): string;
60
+ neighborStop(anchor: Anchor, direction: 'backward' | 'forward'): Anchor | undefined;
61
+ moveHorizontal(anchor: Anchor, direction: 'backward' | 'forward', granularity?: 'character' | 'word'): Anchor;
62
+ moveVertical(anchor: Anchor, direction: 'up' | 'down', preferredX?: number): {
63
+ anchor: Anchor;
64
+ preferredX: number;
65
+ };
66
+ moveLineBoundary(anchor: Anchor, edge: 'start' | 'end'): Anchor;
67
+ wordRangeAt(point: PagePoint): {
68
+ start: Anchor;
69
+ end: Anchor;
70
+ } | undefined;
71
+ flowRangeAt(point: PagePoint): {
72
+ start: Anchor;
73
+ end: Anchor;
74
+ } | undefined;
75
+ }
76
+ export declare function orderedAnchors(a: Anchor, b: Anchor): {
77
+ start: Anchor;
78
+ end: Anchor;
79
+ reversed: boolean;
80
+ };