@pm-cm/core 0.0.1 → 0.0.3

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/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Schema, Node } from 'prosemirror-model';
1
+ import { Node, Schema } from 'prosemirror-model';
2
2
  import { Transaction } from 'prosemirror-state';
3
3
  import { EditorView } from 'prosemirror-view';
4
4
 
@@ -24,7 +24,84 @@ type ErrorEvent = {
24
24
  * - `'serialize-error'` — failed to serialize a ProseMirror document to text.
25
25
  */
26
26
  type OnError = (event: ErrorEvent) => void;
27
+ /** Describes the changed region between two text strings. */
28
+ type TextDiff = {
29
+ /** Byte offset where the two strings first differ. */
30
+ start: number;
31
+ /** End of the changed region in the old (previous) text. */
32
+ endA: number;
33
+ /** End of the changed region in the new (incoming) text. */
34
+ endB: number;
35
+ };
36
+ /**
37
+ * Result of an incremental parse. Either a plain `Node` (the bridge will
38
+ * diff against `prevDoc` to find changed positions), or an object with
39
+ * pre-computed document positions so the bridge can skip tree diffing entirely.
40
+ */
41
+ type IncrementalParseResult = Node | {
42
+ doc: Node;
43
+ from: number;
44
+ to: number;
45
+ toB: number;
46
+ };
47
+ /**
48
+ * Optional incremental parser that re-parses only the changed region.
49
+ *
50
+ * Receives the previous document, previous/next text, and the text-level diff
51
+ * computed by the library. Returns an {@link IncrementalParseResult}, or `null`
52
+ * to fall back to the full {@link Parse} function.
53
+ *
54
+ * When the result includes document positions (`{ doc, from, to, toB }`),
55
+ * the bridge skips `findDiffStart`/`findDiffEnd` entirely.
56
+ */
57
+ type IncrementalParse = (args: {
58
+ prevDoc: Node;
59
+ prevText: string;
60
+ text: string;
61
+ diff: TextDiff;
62
+ schema: Schema;
63
+ }) => IncrementalParseResult | null;
64
+ /** Writer that tracks text offsets for cursor mapping. */
65
+ type CursorMapWriter = {
66
+ /** Write unmapped text (e.g. syntax like `> `, `**`, `[`, `](url)`). */
67
+ write(text: string): void;
68
+ /**
69
+ * Write mapped text: content from a PM text node at `[pmStart, pmEnd)`.
70
+ * Must be called in PM document order (ascending `pmStart`).
71
+ */
72
+ writeMapped(pmStart: number, pmEnd: number, text: string): void;
73
+ };
74
+ /** Serializer that writes to a {@link CursorMapWriter} for exact cursor mapping. */
75
+ type SerializeWithMap = (doc: Node, writer: CursorMapWriter) => void;
76
+ /** A single contiguous run within a match result. */
77
+ type MatchRun = {
78
+ /** Start offset within the PM text node content (0-based). */
79
+ contentStart: number;
80
+ /** End offset within the PM text node content (exclusive). */
81
+ contentEnd: number;
82
+ /** Start offset in the serialized text. */
83
+ textStart: number;
84
+ /** End offset in the serialized text (exclusive). */
85
+ textEnd: number;
86
+ };
87
+ /** Result of a successful match from a {@link Matcher}. */
88
+ type MatchResult = {
89
+ /** Contiguous runs mapping PM content to serialized text. */
90
+ runs: MatchRun[];
91
+ /** Position from which the next node's search should start. */
92
+ nextSearchFrom: number;
93
+ };
94
+ /**
95
+ * Format-specific matching function for use with {@link wrapSerialize}.
96
+ *
97
+ * Given a PM text node's content, the full serialized text, and a search-from
98
+ * position, return a {@link MatchResult} describing how the content maps to
99
+ * the serialized text, or `null` if no match is found.
100
+ */
101
+ type Matcher = (serialized: string, nodeText: string, searchFrom: number) => MatchResult | null;
27
102
 
103
+ /** Compute the changed region between two strings. */
104
+ declare function diffText(a: string, b: string): TextDiff;
28
105
  /** Configuration for {@link createViewBridge}. */
29
106
  type ViewBridgeConfig = {
30
107
  schema: Schema;
@@ -33,11 +110,32 @@ type ViewBridgeConfig = {
33
110
  normalize?: Normalize;
34
111
  /** Called on non-fatal errors (e.g. parse failures). Defaults to `console.error`. */
35
112
  onError?: OnError;
113
+ /**
114
+ * Optional incremental parser for large documents.
115
+ * When provided, the bridge computes a text-level diff and passes it to this
116
+ * function instead of calling the full {@link Parse}. Return `null` to fall
117
+ * back to full parse.
118
+ */
119
+ incrementalParse?: IncrementalParse;
120
+ /** Maximum number of parse results to cache. Defaults to `8`. Set `0` to disable. */
121
+ parseCacheSize?: number;
36
122
  };
37
123
  /** Options for {@link ViewBridgeHandle.applyText}. */
38
124
  type ApplyTextOptions = {
39
125
  /** Set `false` to prevent the change from being added to undo history. Default `true`. */
40
126
  addToHistory?: boolean;
127
+ /**
128
+ * Pre-computed text diff from the editor's change set.
129
+ * When provided, skips the internal `diffText` O(n) scan.
130
+ * The diff describes the changed region between the previous and incoming
131
+ * **normalized** text.
132
+ */
133
+ diff?: TextDiff;
134
+ /**
135
+ * Set `true` when the caller guarantees the text is already normalized
136
+ * (no `\r` characters). Skips the `normalize` pass entirely.
137
+ */
138
+ normalized?: boolean;
41
139
  };
42
140
  /**
43
141
  * Discriminated-union result of {@link ViewBridgeHandle.applyText}.
@@ -103,22 +201,33 @@ type CursorMap = {
103
201
  skippedNodes: number;
104
202
  };
105
203
  /**
106
- * Locate a text-node string within the serialized output.
107
- * Return the starting index, or -1 if not found.
108
- * Default: `(serialized, nodeText, from) => serialized.indexOf(nodeText, from)`
204
+ * Create a {@link CursorMapWriter} that tracks offsets and builds segments.
205
+ *
206
+ * Call `getText()` to retrieve the full serialized text.
207
+ * Call `finish(doc)` to produce the final {@link CursorMap}.
109
208
  */
110
- type LocateText = (serialized: string, nodeText: string, searchFrom: number) => number;
209
+ declare function createCursorMapWriter(): CursorMapWriter & {
210
+ getText(): string;
211
+ finish(doc: Node): CursorMap;
212
+ getMappedCount(): number;
213
+ };
111
214
  /**
112
215
  * Build a cursor map that aligns ProseMirror positions with serialized-text offsets.
113
216
  *
114
- * Walks the document tree and locates each text node within the serialized output,
115
- * producing a sorted list of {@link TextSegment}s.
217
+ * Accepts either a plain {@link Serialize} `(doc) => string` or a
218
+ * {@link SerializeWithMap} `(doc, writer) => void`. Detection is automatic:
219
+ * if the serializer uses the writer, the exact-by-construction path is used;
220
+ * if it returns a string, an internal `indexOf`-based forward match is applied.
221
+ *
222
+ * The plain `Serialize` path uses exact `indexOf` matching (format-agnostic).
223
+ * For better mapping quality with serializers that transform text (escaping,
224
+ * entity encoding, etc.), use {@link wrapSerialize} with a format-specific
225
+ * {@link Matcher}, or implement {@link SerializeWithMap} directly.
116
226
  *
117
227
  * @param doc - The ProseMirror document to map.
118
- * @param serialize - Serializer used to produce the full text.
119
- * @param locate - Optional custom text-location function. Defaults to `indexOf`.
228
+ * @param serialize - A plain serializer or a writer-based serializer.
120
229
  */
121
- declare function buildCursorMap(doc: Node, serialize: Serialize, locate?: LocateText): CursorMap;
230
+ declare function buildCursorMap(doc: Node, serialize: Serialize | SerializeWithMap): CursorMap;
122
231
  /**
123
232
  * Look up a ProseMirror position in a cursor map and return the corresponding text offset.
124
233
  * Returns `null` when the map has no segments.
@@ -129,5 +238,22 @@ declare function cursorMapLookup(map: CursorMap, pmPos: number): number | null;
129
238
  * Returns `null` when the map has no segments.
130
239
  */
131
240
  declare function reverseCursorMapLookup(map: CursorMap, cmOffset: number): number | null;
241
+ /**
242
+ * Wrap a plain {@link Serialize} function as a {@link SerializeWithMap}.
243
+ *
244
+ * When called without a `matcher`, the wrapper uses `indexOf` internally
245
+ * (identical to the default `buildCursorMap` path — useful only for type
246
+ * compatibility).
247
+ *
248
+ * When called with a format-specific {@link Matcher}, the wrapper uses
249
+ * `indexOf` first for each text node, falling back to the matcher when
250
+ * `indexOf` fails. This enables multi-run mapping for serializers that
251
+ * transform text (escaping, entity encoding, etc.).
252
+ *
253
+ * @param serialize - A plain `(doc: Node) => string` serializer.
254
+ * @param matcher - Optional format-specific matcher for improved mapping.
255
+ * @returns A {@link SerializeWithMap} that can be passed to {@link buildCursorMap}.
256
+ */
257
+ declare function wrapSerialize(serialize: Serialize, matcher?: Matcher): SerializeWithMap;
132
258
 
133
- export { type ApplyTextOptions, type ApplyTextResult, type BoundViewBridgeHandle, type CursorMap, type ErrorCode, type ErrorEvent, type LocateText, type Normalize, type OnError, type Parse, type Serialize, type TextSegment, type ViewBridgeConfig, type ViewBridgeHandle, buildCursorMap, createBoundViewBridge, createViewBridge, cursorMapLookup, reverseCursorMapLookup };
259
+ export { type ApplyTextOptions, type ApplyTextResult, type BoundViewBridgeHandle, type CursorMap, type CursorMapWriter, type ErrorCode, type ErrorEvent, type IncrementalParse, type IncrementalParseResult, type MatchResult, type MatchRun, type Matcher, type Normalize, type OnError, type Parse, type Serialize, type SerializeWithMap, type TextDiff, type TextSegment, type ViewBridgeConfig, type ViewBridgeHandle, buildCursorMap, createBoundViewBridge, createCursorMapWriter, createViewBridge, cursorMapLookup, diffText, reverseCursorMapLookup, wrapSerialize };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Schema, Node } from 'prosemirror-model';
1
+ import { Node, Schema } from 'prosemirror-model';
2
2
  import { Transaction } from 'prosemirror-state';
3
3
  import { EditorView } from 'prosemirror-view';
4
4
 
@@ -24,7 +24,84 @@ type ErrorEvent = {
24
24
  * - `'serialize-error'` — failed to serialize a ProseMirror document to text.
25
25
  */
26
26
  type OnError = (event: ErrorEvent) => void;
27
+ /** Describes the changed region between two text strings. */
28
+ type TextDiff = {
29
+ /** Byte offset where the two strings first differ. */
30
+ start: number;
31
+ /** End of the changed region in the old (previous) text. */
32
+ endA: number;
33
+ /** End of the changed region in the new (incoming) text. */
34
+ endB: number;
35
+ };
36
+ /**
37
+ * Result of an incremental parse. Either a plain `Node` (the bridge will
38
+ * diff against `prevDoc` to find changed positions), or an object with
39
+ * pre-computed document positions so the bridge can skip tree diffing entirely.
40
+ */
41
+ type IncrementalParseResult = Node | {
42
+ doc: Node;
43
+ from: number;
44
+ to: number;
45
+ toB: number;
46
+ };
47
+ /**
48
+ * Optional incremental parser that re-parses only the changed region.
49
+ *
50
+ * Receives the previous document, previous/next text, and the text-level diff
51
+ * computed by the library. Returns an {@link IncrementalParseResult}, or `null`
52
+ * to fall back to the full {@link Parse} function.
53
+ *
54
+ * When the result includes document positions (`{ doc, from, to, toB }`),
55
+ * the bridge skips `findDiffStart`/`findDiffEnd` entirely.
56
+ */
57
+ type IncrementalParse = (args: {
58
+ prevDoc: Node;
59
+ prevText: string;
60
+ text: string;
61
+ diff: TextDiff;
62
+ schema: Schema;
63
+ }) => IncrementalParseResult | null;
64
+ /** Writer that tracks text offsets for cursor mapping. */
65
+ type CursorMapWriter = {
66
+ /** Write unmapped text (e.g. syntax like `> `, `**`, `[`, `](url)`). */
67
+ write(text: string): void;
68
+ /**
69
+ * Write mapped text: content from a PM text node at `[pmStart, pmEnd)`.
70
+ * Must be called in PM document order (ascending `pmStart`).
71
+ */
72
+ writeMapped(pmStart: number, pmEnd: number, text: string): void;
73
+ };
74
+ /** Serializer that writes to a {@link CursorMapWriter} for exact cursor mapping. */
75
+ type SerializeWithMap = (doc: Node, writer: CursorMapWriter) => void;
76
+ /** A single contiguous run within a match result. */
77
+ type MatchRun = {
78
+ /** Start offset within the PM text node content (0-based). */
79
+ contentStart: number;
80
+ /** End offset within the PM text node content (exclusive). */
81
+ contentEnd: number;
82
+ /** Start offset in the serialized text. */
83
+ textStart: number;
84
+ /** End offset in the serialized text (exclusive). */
85
+ textEnd: number;
86
+ };
87
+ /** Result of a successful match from a {@link Matcher}. */
88
+ type MatchResult = {
89
+ /** Contiguous runs mapping PM content to serialized text. */
90
+ runs: MatchRun[];
91
+ /** Position from which the next node's search should start. */
92
+ nextSearchFrom: number;
93
+ };
94
+ /**
95
+ * Format-specific matching function for use with {@link wrapSerialize}.
96
+ *
97
+ * Given a PM text node's content, the full serialized text, and a search-from
98
+ * position, return a {@link MatchResult} describing how the content maps to
99
+ * the serialized text, or `null` if no match is found.
100
+ */
101
+ type Matcher = (serialized: string, nodeText: string, searchFrom: number) => MatchResult | null;
27
102
 
103
+ /** Compute the changed region between two strings. */
104
+ declare function diffText(a: string, b: string): TextDiff;
28
105
  /** Configuration for {@link createViewBridge}. */
29
106
  type ViewBridgeConfig = {
30
107
  schema: Schema;
@@ -33,11 +110,32 @@ type ViewBridgeConfig = {
33
110
  normalize?: Normalize;
34
111
  /** Called on non-fatal errors (e.g. parse failures). Defaults to `console.error`. */
35
112
  onError?: OnError;
113
+ /**
114
+ * Optional incremental parser for large documents.
115
+ * When provided, the bridge computes a text-level diff and passes it to this
116
+ * function instead of calling the full {@link Parse}. Return `null` to fall
117
+ * back to full parse.
118
+ */
119
+ incrementalParse?: IncrementalParse;
120
+ /** Maximum number of parse results to cache. Defaults to `8`. Set `0` to disable. */
121
+ parseCacheSize?: number;
36
122
  };
37
123
  /** Options for {@link ViewBridgeHandle.applyText}. */
38
124
  type ApplyTextOptions = {
39
125
  /** Set `false` to prevent the change from being added to undo history. Default `true`. */
40
126
  addToHistory?: boolean;
127
+ /**
128
+ * Pre-computed text diff from the editor's change set.
129
+ * When provided, skips the internal `diffText` O(n) scan.
130
+ * The diff describes the changed region between the previous and incoming
131
+ * **normalized** text.
132
+ */
133
+ diff?: TextDiff;
134
+ /**
135
+ * Set `true` when the caller guarantees the text is already normalized
136
+ * (no `\r` characters). Skips the `normalize` pass entirely.
137
+ */
138
+ normalized?: boolean;
41
139
  };
42
140
  /**
43
141
  * Discriminated-union result of {@link ViewBridgeHandle.applyText}.
@@ -103,22 +201,33 @@ type CursorMap = {
103
201
  skippedNodes: number;
104
202
  };
105
203
  /**
106
- * Locate a text-node string within the serialized output.
107
- * Return the starting index, or -1 if not found.
108
- * Default: `(serialized, nodeText, from) => serialized.indexOf(nodeText, from)`
204
+ * Create a {@link CursorMapWriter} that tracks offsets and builds segments.
205
+ *
206
+ * Call `getText()` to retrieve the full serialized text.
207
+ * Call `finish(doc)` to produce the final {@link CursorMap}.
109
208
  */
110
- type LocateText = (serialized: string, nodeText: string, searchFrom: number) => number;
209
+ declare function createCursorMapWriter(): CursorMapWriter & {
210
+ getText(): string;
211
+ finish(doc: Node): CursorMap;
212
+ getMappedCount(): number;
213
+ };
111
214
  /**
112
215
  * Build a cursor map that aligns ProseMirror positions with serialized-text offsets.
113
216
  *
114
- * Walks the document tree and locates each text node within the serialized output,
115
- * producing a sorted list of {@link TextSegment}s.
217
+ * Accepts either a plain {@link Serialize} `(doc) => string` or a
218
+ * {@link SerializeWithMap} `(doc, writer) => void`. Detection is automatic:
219
+ * if the serializer uses the writer, the exact-by-construction path is used;
220
+ * if it returns a string, an internal `indexOf`-based forward match is applied.
221
+ *
222
+ * The plain `Serialize` path uses exact `indexOf` matching (format-agnostic).
223
+ * For better mapping quality with serializers that transform text (escaping,
224
+ * entity encoding, etc.), use {@link wrapSerialize} with a format-specific
225
+ * {@link Matcher}, or implement {@link SerializeWithMap} directly.
116
226
  *
117
227
  * @param doc - The ProseMirror document to map.
118
- * @param serialize - Serializer used to produce the full text.
119
- * @param locate - Optional custom text-location function. Defaults to `indexOf`.
228
+ * @param serialize - A plain serializer or a writer-based serializer.
120
229
  */
121
- declare function buildCursorMap(doc: Node, serialize: Serialize, locate?: LocateText): CursorMap;
230
+ declare function buildCursorMap(doc: Node, serialize: Serialize | SerializeWithMap): CursorMap;
122
231
  /**
123
232
  * Look up a ProseMirror position in a cursor map and return the corresponding text offset.
124
233
  * Returns `null` when the map has no segments.
@@ -129,5 +238,22 @@ declare function cursorMapLookup(map: CursorMap, pmPos: number): number | null;
129
238
  * Returns `null` when the map has no segments.
130
239
  */
131
240
  declare function reverseCursorMapLookup(map: CursorMap, cmOffset: number): number | null;
241
+ /**
242
+ * Wrap a plain {@link Serialize} function as a {@link SerializeWithMap}.
243
+ *
244
+ * When called without a `matcher`, the wrapper uses `indexOf` internally
245
+ * (identical to the default `buildCursorMap` path — useful only for type
246
+ * compatibility).
247
+ *
248
+ * When called with a format-specific {@link Matcher}, the wrapper uses
249
+ * `indexOf` first for each text node, falling back to the matcher when
250
+ * `indexOf` fails. This enables multi-run mapping for serializers that
251
+ * transform text (escaping, entity encoding, etc.).
252
+ *
253
+ * @param serialize - A plain `(doc: Node) => string` serializer.
254
+ * @param matcher - Optional format-specific matcher for improved mapping.
255
+ * @returns A {@link SerializeWithMap} that can be passed to {@link buildCursorMap}.
256
+ */
257
+ declare function wrapSerialize(serialize: Serialize, matcher?: Matcher): SerializeWithMap;
132
258
 
133
- export { type ApplyTextOptions, type ApplyTextResult, type BoundViewBridgeHandle, type CursorMap, type ErrorCode, type ErrorEvent, type LocateText, type Normalize, type OnError, type Parse, type Serialize, type TextSegment, type ViewBridgeConfig, type ViewBridgeHandle, buildCursorMap, createBoundViewBridge, createViewBridge, cursorMapLookup, reverseCursorMapLookup };
259
+ export { type ApplyTextOptions, type ApplyTextResult, type BoundViewBridgeHandle, type CursorMap, type CursorMapWriter, type ErrorCode, type ErrorEvent, type IncrementalParse, type IncrementalParseResult, type MatchResult, type MatchRun, type Matcher, type Normalize, type OnError, type Parse, type Serialize, type SerializeWithMap, type TextDiff, type TextSegment, type ViewBridgeConfig, type ViewBridgeHandle, buildCursorMap, createBoundViewBridge, createCursorMapWriter, createViewBridge, cursorMapLookup, diffText, reverseCursorMapLookup, wrapSerialize };