@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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Bidirectional sync between ProseMirror and CodeMirror for split-editor UIs.
4
4
 
5
+ [Demo](https://munenick.github.io/prosemirror-codemirror-sync/)
6
+
5
7
  Keeps a WYSIWYG pane (ProseMirror) and a text pane (CodeMirror) in sync. The serialization format is pluggable — you provide `serialize` and `parse` functions (e.g. Markdown, AsciiDoc, plain text).
6
8
 
7
9
  ## Install
package/dist/index.cjs CHANGED
@@ -22,45 +22,161 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  buildCursorMap: () => buildCursorMap,
24
24
  createBoundViewBridge: () => createBoundViewBridge,
25
+ createCursorMapWriter: () => createCursorMapWriter,
25
26
  createViewBridge: () => createViewBridge,
26
27
  cursorMapLookup: () => cursorMapLookup,
27
- reverseCursorMapLookup: () => reverseCursorMapLookup
28
+ diffText: () => diffText,
29
+ reverseCursorMapLookup: () => reverseCursorMapLookup,
30
+ wrapSerialize: () => wrapSerialize
28
31
  });
29
32
  module.exports = __toCommonJS(index_exports);
30
33
 
31
34
  // src/bridge.ts
32
35
  var BRIDGE_META = "pm-cm-bridge";
33
- var defaultNormalize = (s) => s.replace(/\r\n?/g, "\n");
36
+ var DEFAULT_PARSE_CACHE_SIZE = 8;
37
+ var defaultNormalize = (s) => s.indexOf("\r") === -1 ? s : s.replace(/\r\n?/g, "\n");
34
38
  var defaultOnError = (event) => console.error(`[bridge] ${event.code}: ${event.message}`, event.cause);
39
+ function diffText(a, b) {
40
+ let start = 0;
41
+ const minLen = Math.min(a.length, b.length);
42
+ while (start < minLen && a.charCodeAt(start) === b.charCodeAt(start)) start++;
43
+ let endA = a.length;
44
+ let endB = b.length;
45
+ while (endA > start && endB > start && a.charCodeAt(endA - 1) === b.charCodeAt(endB - 1)) {
46
+ endA--;
47
+ endB--;
48
+ }
49
+ return { start, endA, endB };
50
+ }
51
+ var ParseLru = class {
52
+ map = /* @__PURE__ */ new Map();
53
+ limit;
54
+ constructor(limit) {
55
+ this.limit = limit;
56
+ }
57
+ get(key) {
58
+ const v = this.map.get(key);
59
+ if (v !== void 0) {
60
+ this.map.delete(key);
61
+ this.map.set(key, v);
62
+ }
63
+ return v;
64
+ }
65
+ set(key, value) {
66
+ if (this.map.has(key)) this.map.delete(key);
67
+ this.map.set(key, value);
68
+ if (this.map.size > this.limit) {
69
+ const first = this.map.keys().next().value;
70
+ this.map.delete(first);
71
+ }
72
+ }
73
+ };
35
74
  function createViewBridge(config) {
36
75
  const { schema, serialize, parse } = config;
37
76
  const normalize = config.normalize ?? defaultNormalize;
38
77
  const onError = config.onError ?? defaultOnError;
78
+ const incrementalParse = config.incrementalParse ?? null;
79
+ const cacheSize = config.parseCacheSize ?? DEFAULT_PARSE_CACHE_SIZE;
80
+ const serializeCache = /* @__PURE__ */ new WeakMap();
81
+ function cachedSerialize(doc) {
82
+ let text = serializeCache.get(doc);
83
+ if (text === void 0) {
84
+ text = normalize(serialize(doc));
85
+ serializeCache.set(doc, text);
86
+ }
87
+ return text;
88
+ }
89
+ const parseLru = cacheSize > 0 ? new ParseLru(cacheSize) : null;
90
+ function cachedParse(text) {
91
+ if (parseLru) {
92
+ const cached = parseLru.get(text);
93
+ if (cached) return cached;
94
+ }
95
+ const doc = parse(text, schema);
96
+ if (parseLru) parseLru.set(text, doc);
97
+ return doc;
98
+ }
99
+ let lastDoc = null;
100
+ let lastRaw = null;
101
+ let lastIncoming = null;
102
+ function markUnchanged(doc, raw, incoming) {
103
+ lastDoc = doc;
104
+ lastRaw = raw;
105
+ lastIncoming = incoming;
106
+ return { ok: false, reason: "unchanged" };
107
+ }
39
108
  return {
40
109
  applyText(view, text, options) {
41
- const incoming = normalize(text);
42
- const current = normalize(serialize(view.state.doc));
43
- if (incoming === current) {
110
+ const prevDoc = view.state.doc;
111
+ if (prevDoc === lastDoc && text === lastRaw) {
44
112
  return { ok: false, reason: "unchanged" };
45
113
  }
114
+ const incoming = options?.normalized ? text : normalize(text);
115
+ if (prevDoc === lastDoc && incoming === lastIncoming) {
116
+ lastRaw = text;
117
+ return { ok: false, reason: "unchanged" };
118
+ }
119
+ const current = cachedSerialize(prevDoc);
120
+ if (incoming === current) {
121
+ return markUnchanged(prevDoc, text, incoming);
122
+ }
46
123
  let nextDoc;
124
+ let rangeHint = null;
125
+ const diff = options?.diff ?? diffText(current, incoming);
47
126
  try {
48
- nextDoc = parse(incoming, schema);
127
+ if (incrementalParse) {
128
+ const result = incrementalParse({ prevDoc, prevText: current, text: incoming, diff, schema });
129
+ if (result == null) {
130
+ nextDoc = cachedParse(incoming);
131
+ } else if ("doc" in result && "from" in result) {
132
+ nextDoc = result.doc;
133
+ rangeHint = { from: result.from, to: result.to, toB: result.toB };
134
+ } else {
135
+ nextDoc = result;
136
+ }
137
+ } else {
138
+ nextDoc = cachedParse(incoming);
139
+ }
49
140
  } catch (error) {
50
141
  onError({ code: "parse-error", message: "failed to parse text into ProseMirror document", cause: error });
51
142
  return { ok: false, reason: "parse-error" };
52
143
  }
144
+ let from, to, toB;
145
+ if (rangeHint) {
146
+ from = rangeHint.from;
147
+ to = rangeHint.to;
148
+ toB = rangeHint.toB;
149
+ } else {
150
+ const start = prevDoc.content.findDiffStart(nextDoc.content);
151
+ if (start == null) {
152
+ return markUnchanged(prevDoc, text, incoming);
153
+ }
154
+ const end = prevDoc.content.findDiffEnd(nextDoc.content);
155
+ if (!end) {
156
+ return markUnchanged(prevDoc, text, incoming);
157
+ }
158
+ from = Math.min(start, end.a);
159
+ to = Math.max(start, end.a);
160
+ toB = Math.max(start, end.b);
161
+ }
53
162
  const tr = view.state.tr;
54
- tr.replaceWith(0, tr.doc.content.size, nextDoc.content);
163
+ tr.replace(from, to, nextDoc.slice(from, toB));
55
164
  tr.setMeta(BRIDGE_META, true);
56
165
  if (options?.addToHistory === false) {
57
166
  tr.setMeta("addToHistory", false);
58
167
  }
59
168
  view.dispatch(tr);
169
+ const newDoc = view.state.doc;
170
+ serializeCache.set(newDoc, incoming);
171
+ lastDoc = newDoc;
172
+ lastRaw = text;
173
+ lastIncoming = incoming;
60
174
  return { ok: true };
61
175
  },
62
176
  extractText(view) {
63
- return serialize(view.state.doc);
177
+ const text = serialize(view.state.doc);
178
+ serializeCache.set(view.state.doc, normalize(text));
179
+ return text;
64
180
  },
65
181
  isBridgeChange(tr) {
66
182
  return tr.getMeta(BRIDGE_META) === true;
@@ -87,39 +203,116 @@ function createBoundViewBridge(view, config) {
87
203
  }
88
204
 
89
205
  // src/cursor-map.ts
90
- var defaultLocate = (serialized, nodeText, from) => serialized.indexOf(nodeText, from);
91
- function buildCursorMap(doc, serialize, locate = defaultLocate) {
92
- const fullText = serialize(doc);
206
+ function createCursorMapWriter() {
207
+ let offset = 0;
208
+ const parts = [];
209
+ const segments = [];
210
+ let mappedCount = 0;
211
+ const writer = {
212
+ write(text) {
213
+ parts.push(text);
214
+ offset += text.length;
215
+ },
216
+ writeMapped(pmStart, pmEnd, text) {
217
+ segments.push({
218
+ pmStart,
219
+ pmEnd,
220
+ textStart: offset,
221
+ textEnd: offset + text.length
222
+ });
223
+ parts.push(text);
224
+ offset += text.length;
225
+ mappedCount++;
226
+ },
227
+ getText() {
228
+ return parts.join("");
229
+ },
230
+ getMappedCount() {
231
+ return mappedCount;
232
+ },
233
+ finish(doc) {
234
+ const textNodes = [];
235
+ function collectTextNodes(node, contentStart) {
236
+ node.forEach((child, childOffset) => {
237
+ const childPos = contentStart + childOffset;
238
+ if (child.isText && child.text) {
239
+ textNodes.push({ start: childPos, end: childPos + child.text.length });
240
+ } else if (!child.isLeaf) {
241
+ collectTextNodes(child, childPos + 1);
242
+ }
243
+ });
244
+ }
245
+ collectTextNodes(doc, 0);
246
+ let mappedNodes = 0;
247
+ let segIdx = 0;
248
+ for (const n of textNodes) {
249
+ while (segIdx < segments.length && segments[segIdx].pmEnd <= n.start) segIdx++;
250
+ let k = segIdx;
251
+ while (k < segments.length && segments[k].pmStart < n.end) {
252
+ const s = segments[k];
253
+ if (s.pmEnd > n.start && s.pmStart < n.end) {
254
+ mappedNodes++;
255
+ break;
256
+ }
257
+ k++;
258
+ }
259
+ }
260
+ return {
261
+ segments,
262
+ textLength: offset,
263
+ skippedNodes: Math.max(0, textNodes.length - mappedNodes)
264
+ };
265
+ }
266
+ };
267
+ return writer;
268
+ }
269
+ function buildCursorMap(doc, serialize) {
270
+ const writer = createCursorMapWriter();
271
+ const result = serialize(doc, writer);
272
+ if (typeof result === "string" && writer.getMappedCount() === 0) {
273
+ return forwardScanBuildMap(doc, result);
274
+ }
275
+ const map = writer.finish(doc);
276
+ for (let i = 1; i < map.segments.length; i++) {
277
+ const prev = map.segments[i - 1];
278
+ const curr = map.segments[i];
279
+ if (curr.pmStart < prev.pmEnd || curr.textStart < prev.textEnd) {
280
+ console.warn(
281
+ `[pm-cm] buildCursorMap: non-monotonic segment at index ${i} (pmStart ${curr.pmStart} < prev pmEnd ${prev.pmEnd} or textStart ${curr.textStart} < prev textEnd ${prev.textEnd}). Ensure writeMapped calls are in ascending PM document order.`
282
+ );
283
+ }
284
+ }
285
+ return map;
286
+ }
287
+ function forwardScanBuildMap(doc, text) {
93
288
  const segments = [];
94
289
  let searchFrom = 0;
290
+ let totalTextNodes = 0;
95
291
  let skippedNodes = 0;
96
- function walkChildren(node, contentStart) {
97
- node.forEach((child, offset) => {
98
- const childPos = contentStart + offset;
292
+ function visit(node, contentStart) {
293
+ node.forEach((child, childOffset) => {
294
+ const childPos = contentStart + childOffset;
99
295
  if (child.isText && child.text) {
100
- const text = child.text;
101
- const idx = locate(fullText, text, searchFrom);
296
+ totalTextNodes++;
297
+ const idx = text.indexOf(child.text, searchFrom);
102
298
  if (idx >= 0) {
103
299
  segments.push({
104
300
  pmStart: childPos,
105
- pmEnd: childPos + text.length,
301
+ pmEnd: childPos + child.text.length,
106
302
  textStart: idx,
107
- textEnd: idx + text.length
303
+ textEnd: idx + child.text.length
108
304
  });
109
- searchFrom = idx + text.length;
305
+ searchFrom = idx + child.text.length;
110
306
  } else {
111
307
  skippedNodes++;
112
308
  }
113
- return;
309
+ } else if (!child.isLeaf) {
310
+ visit(child, childPos + 1);
114
311
  }
115
- if (child.isLeaf) {
116
- return;
117
- }
118
- walkChildren(child, childPos + 1);
119
312
  });
120
313
  }
121
- walkChildren(doc, 0);
122
- return { segments, textLength: fullText.length, skippedNodes };
314
+ visit(doc, 0);
315
+ return { segments, textLength: text.length, skippedNodes };
123
316
  }
124
317
  function cursorMapLookup(map, pmPos) {
125
318
  const { segments } = map;
@@ -169,12 +362,70 @@ function reverseCursorMapLookup(map, cmOffset) {
169
362
  const distAfter = after.textStart - cmOffset;
170
363
  return distBefore <= distAfter ? before.pmEnd : after.pmStart;
171
364
  }
365
+ function wrapSerialize(serialize, matcher) {
366
+ return (doc, writer) => {
367
+ const text = serialize(doc);
368
+ const segments = collectMatchedSegments(doc, text, matcher);
369
+ let pos = 0;
370
+ for (const seg of segments) {
371
+ if (seg.textStart > pos) writer.write(text.slice(pos, seg.textStart));
372
+ writer.writeMapped(seg.pmStart, seg.pmEnd, text.slice(seg.textStart, seg.textEnd));
373
+ pos = seg.textEnd;
374
+ }
375
+ if (pos < text.length) writer.write(text.slice(pos));
376
+ };
377
+ }
378
+ function collectMatchedSegments(doc, text, matcher) {
379
+ const segments = [];
380
+ let searchFrom = 0;
381
+ function visit(node, contentStart) {
382
+ node.forEach((child, childOffset) => {
383
+ const childPos = contentStart + childOffset;
384
+ if (child.isText && child.text) {
385
+ const content = child.text;
386
+ const exactIdx = text.indexOf(content, searchFrom);
387
+ if (exactIdx >= 0) {
388
+ segments.push({
389
+ pmStart: childPos,
390
+ pmEnd: childPos + content.length,
391
+ textStart: exactIdx,
392
+ textEnd: exactIdx + content.length
393
+ });
394
+ searchFrom = exactIdx + content.length;
395
+ return;
396
+ }
397
+ if (matcher) {
398
+ const result = matcher(text, content, searchFrom);
399
+ if (result) {
400
+ for (const run of result.runs) {
401
+ segments.push({
402
+ pmStart: childPos + run.contentStart,
403
+ pmEnd: childPos + run.contentEnd,
404
+ textStart: run.textStart,
405
+ textEnd: run.textEnd
406
+ });
407
+ }
408
+ searchFrom = result.nextSearchFrom;
409
+ return;
410
+ }
411
+ }
412
+ } else if (!child.isLeaf) {
413
+ visit(child, childPos + 1);
414
+ }
415
+ });
416
+ }
417
+ visit(doc, 0);
418
+ return segments;
419
+ }
172
420
  // Annotate the CommonJS export names for ESM import in node:
173
421
  0 && (module.exports = {
174
422
  buildCursorMap,
175
423
  createBoundViewBridge,
424
+ createCursorMapWriter,
176
425
  createViewBridge,
177
426
  cursorMapLookup,
178
- reverseCursorMapLookup
427
+ diffText,
428
+ reverseCursorMapLookup,
429
+ wrapSerialize
179
430
  });
180
431
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/bridge.ts","../src/cursor-map.ts"],"sourcesContent":["export { createViewBridge, createBoundViewBridge } from './bridge.js'\nexport type { ViewBridgeConfig, ViewBridgeHandle, BoundViewBridgeHandle, ApplyTextOptions, ApplyTextResult } from './bridge.js'\nexport type { Serialize, Parse, Normalize, OnError, ErrorCode, ErrorEvent } from './types.js'\nexport { buildCursorMap, cursorMapLookup, reverseCursorMapLookup } from './cursor-map.js'\nexport type { TextSegment, CursorMap, LocateText } from './cursor-map.js'\n","import type { Node, Schema } from 'prosemirror-model'\nimport type { Transaction } from 'prosemirror-state'\nimport type { EditorView } from 'prosemirror-view'\nimport type { Normalize, Serialize, Parse, OnError } from './types.js'\n\nconst BRIDGE_META = 'pm-cm-bridge'\nconst defaultNormalize: Normalize = (s) => s.replace(/\\r\\n?/g, '\\n')\nconst defaultOnError: OnError = (event) => console.error(`[bridge] ${event.code}: ${event.message}`, event.cause)\n\n/** Configuration for {@link createViewBridge}. */\nexport type ViewBridgeConfig = {\n schema: Schema\n serialize: Serialize\n parse: Parse\n normalize?: Normalize\n /** Called on non-fatal errors (e.g. parse failures). Defaults to `console.error`. */\n onError?: OnError\n}\n\n/** Options for {@link ViewBridgeHandle.applyText}. */\nexport type ApplyTextOptions = {\n /** Set `false` to prevent the change from being added to undo history. Default `true`. */\n addToHistory?: boolean\n}\n\n/**\n * Discriminated-union result of {@link ViewBridgeHandle.applyText}.\n * `ok: true` when the text was applied; `ok: false` with a `reason` otherwise.\n */\nexport type ApplyTextResult =\n | { ok: true }\n | { ok: false; reason: 'unchanged' | 'parse-error' }\n\n/** Handle returned by {@link createViewBridge}. */\nexport type ViewBridgeHandle = {\n /** Parse `text` and replace the ProseMirror document. Returns an {@link ApplyTextResult}. */\n applyText(view: EditorView, text: string, options?: ApplyTextOptions): ApplyTextResult\n /** Serialize the current ProseMirror document to text. */\n extractText(view: EditorView): string\n /** Returns `true` if the transaction was dispatched by {@link applyText}. */\n isBridgeChange(tr: Transaction): boolean\n}\n\n/** Handle returned by {@link createBoundViewBridge}. View is bound; no need to pass it each call. */\nexport type BoundViewBridgeHandle = {\n /** Parse `text` and replace the ProseMirror document. */\n applyText(text: string, options?: ApplyTextOptions): ApplyTextResult\n /** Serialize the current ProseMirror document to text. */\n extractText(): string\n /** Returns `true` if the transaction was dispatched by {@link applyText}. */\n isBridgeChange(tr: Transaction): boolean\n /** Replace the bound EditorView. */\n setView(view: EditorView): void\n}\n\n/**\n * Create a document-sync bridge between ProseMirror and a text editor.\n *\n * Returns a {@link ViewBridgeHandle} with methods to push/pull text and\n * detect bridge-originated transactions.\n */\nexport function createViewBridge(config: ViewBridgeConfig): ViewBridgeHandle {\n const { schema, serialize, parse } = config\n const normalize = config.normalize ?? defaultNormalize\n const onError = config.onError ?? defaultOnError\n\n return {\n applyText(view: EditorView, text: string, options?: ApplyTextOptions): ApplyTextResult {\n const incoming = normalize(text)\n const current = normalize(serialize(view.state.doc))\n\n if (incoming === current) {\n return { ok: false, reason: 'unchanged' }\n }\n\n let nextDoc: Node\n try {\n nextDoc = parse(incoming, schema)\n } catch (error) {\n onError({ code: 'parse-error', message: 'failed to parse text into ProseMirror document', cause: error })\n return { ok: false, reason: 'parse-error' }\n }\n\n const tr = view.state.tr\n tr.replaceWith(0, tr.doc.content.size, nextDoc.content)\n tr.setMeta(BRIDGE_META, true)\n if (options?.addToHistory === false) {\n tr.setMeta('addToHistory', false)\n }\n view.dispatch(tr)\n return { ok: true }\n },\n\n extractText(view: EditorView): string {\n return serialize(view.state.doc)\n },\n\n isBridgeChange(tr: Transaction): boolean {\n return tr.getMeta(BRIDGE_META) === true\n },\n }\n}\n\n/**\n * Create a view-bound document-sync bridge. Wraps {@link createViewBridge}\n * so that the `EditorView` does not need to be passed to each method call.\n *\n * @param view - The initial EditorView to bind.\n * @param config - Configuration for the underlying bridge.\n */\nexport function createBoundViewBridge(view: EditorView, config: ViewBridgeConfig): BoundViewBridgeHandle {\n const inner = createViewBridge(config)\n let currentView = view\n\n return {\n applyText(text: string, options?: ApplyTextOptions): ApplyTextResult {\n return inner.applyText(currentView, text, options)\n },\n extractText(): string {\n return inner.extractText(currentView)\n },\n isBridgeChange(tr: Transaction): boolean {\n return inner.isBridgeChange(tr)\n },\n setView(v: EditorView): void {\n currentView = v\n },\n }\n}\n","import type { Node } from 'prosemirror-model'\nimport type { Serialize } from './types.js'\n\n/** A mapping between a ProseMirror position range and a serialized-text offset range. */\nexport type TextSegment = {\n pmStart: number // PM position (inclusive)\n pmEnd: number // PM position (exclusive)\n textStart: number // serialized text offset (inclusive)\n textEnd: number // serialized text offset (exclusive)\n}\n\n/**\n * Sorted list of {@link TextSegment}s produced by {@link buildCursorMap}.\n * Use {@link cursorMapLookup} and {@link reverseCursorMapLookup} for O(log n) queries.\n */\nexport type CursorMap = {\n segments: TextSegment[]\n textLength: number\n /** Number of text nodes that could not be located in the serialized output. */\n skippedNodes: number\n}\n\n/**\n * Locate a text-node string within the serialized output.\n * Return the starting index, or -1 if not found.\n * Default: `(serialized, nodeText, from) => serialized.indexOf(nodeText, from)`\n */\nexport type LocateText = (serialized: string, nodeText: string, searchFrom: number) => number\n\nconst defaultLocate: LocateText = (serialized, nodeText, from) =>\n serialized.indexOf(nodeText, from)\n\n/**\n * Build a cursor map that aligns ProseMirror positions with serialized-text offsets.\n *\n * Walks the document tree and locates each text node within the serialized output,\n * producing a sorted list of {@link TextSegment}s.\n *\n * @param doc - The ProseMirror document to map.\n * @param serialize - Serializer used to produce the full text.\n * @param locate - Optional custom text-location function. Defaults to `indexOf`.\n */\nexport function buildCursorMap(\n doc: Node,\n serialize: Serialize,\n locate: LocateText = defaultLocate,\n): CursorMap {\n const fullText = serialize(doc)\n const segments: TextSegment[] = []\n let searchFrom = 0\n let skippedNodes = 0\n\n function walkChildren(node: Node, contentStart: number): void {\n node.forEach((child, offset) => {\n const childPos = contentStart + offset\n\n if (child.isText && child.text) {\n const text = child.text\n const idx = locate(fullText, text, searchFrom)\n if (idx >= 0) {\n segments.push({\n pmStart: childPos,\n pmEnd: childPos + text.length,\n textStart: idx,\n textEnd: idx + text.length,\n })\n searchFrom = idx + text.length\n } else {\n skippedNodes++\n }\n return\n }\n\n if (child.isLeaf) {\n return\n }\n\n // Container node: content starts at childPos + 1 (open tag)\n walkChildren(child, childPos + 1)\n })\n }\n\n // doc's content starts at position 0\n walkChildren(doc, 0)\n\n return { segments, textLength: fullText.length, skippedNodes }\n}\n\n/**\n * Look up a ProseMirror position in a cursor map and return the corresponding text offset.\n * Returns `null` when the map has no segments.\n */\nexport function cursorMapLookup(map: CursorMap, pmPos: number): number | null {\n const { segments } = map\n if (segments.length === 0) return null\n\n // Binary search for the segment containing pmPos\n let lo = 0\n let hi = segments.length - 1\n\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1\n const seg = segments[mid]\n\n if (pmPos < seg.pmStart) {\n hi = mid - 1\n } else if (pmPos >= seg.pmEnd) {\n lo = mid + 1\n } else {\n // Inside segment: exact mapping\n return seg.textStart + (pmPos - seg.pmStart)\n }\n }\n\n // pmPos is between segments — snap to nearest boundary\n // After binary search: hi < lo, pmPos falls between segments[hi] and segments[lo]\n const before = hi >= 0 ? segments[hi] : null\n const after = lo < segments.length ? segments[lo] : null\n\n if (!before) return after ? after.textStart : 0\n if (!after) return before.textEnd\n\n const distBefore = pmPos - before.pmEnd\n const distAfter = after.pmStart - pmPos\n return distBefore <= distAfter ? before.textEnd : after.textStart\n}\n\n/**\n * Look up a text offset (e.g. CodeMirror position) in a cursor map and return the corresponding ProseMirror position.\n * Returns `null` when the map has no segments.\n */\nexport function reverseCursorMapLookup(map: CursorMap, cmOffset: number): number | null {\n const { segments } = map\n if (segments.length === 0) return null\n\n // Binary search for the segment containing cmOffset\n let lo = 0\n let hi = segments.length - 1\n\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1\n const seg = segments[mid]\n\n if (cmOffset < seg.textStart) {\n hi = mid - 1\n } else if (cmOffset >= seg.textEnd) {\n lo = mid + 1\n } else {\n // Inside segment: exact mapping\n return seg.pmStart + (cmOffset - seg.textStart)\n }\n }\n\n // cmOffset is between segments — snap to nearest boundary\n const before = hi >= 0 ? segments[hi] : null\n const after = lo < segments.length ? segments[lo] : null\n\n if (!before) return after ? after.pmStart : 0\n if (!after) return before.pmEnd\n\n const distBefore = cmOffset - before.textEnd\n const distAfter = after.textStart - cmOffset\n return distBefore <= distAfter ? before.pmEnd : after.pmStart\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/bridge.ts","../src/cursor-map.ts"],"sourcesContent":["export { createViewBridge, createBoundViewBridge, diffText } from './bridge.js'\nexport type { ViewBridgeConfig, ViewBridgeHandle, BoundViewBridgeHandle, ApplyTextOptions, ApplyTextResult } from './bridge.js'\nexport type { Serialize, Parse, Normalize, OnError, ErrorCode, ErrorEvent, IncrementalParse, IncrementalParseResult, TextDiff, CursorMapWriter, SerializeWithMap, Matcher, MatchResult, MatchRun } from './types.js'\nexport { buildCursorMap, createCursorMapWriter, cursorMapLookup, reverseCursorMapLookup, wrapSerialize } from './cursor-map.js'\nexport type { TextSegment, CursorMap } from './cursor-map.js'\n","import type { Node, Schema } from 'prosemirror-model'\nimport type { Transaction } from 'prosemirror-state'\nimport type { EditorView } from 'prosemirror-view'\nimport type { Normalize, Serialize, Parse, OnError, IncrementalParse, IncrementalParseResult, TextDiff } from './types.js'\n\nconst BRIDGE_META = 'pm-cm-bridge'\nconst DEFAULT_PARSE_CACHE_SIZE = 8\nconst defaultNormalize: Normalize = (s) => (s.indexOf('\\r') === -1 ? s : s.replace(/\\r\\n?/g, '\\n'))\nconst defaultOnError: OnError = (event) => console.error(`[bridge] ${event.code}: ${event.message}`, event.cause)\n\n/** Compute the changed region between two strings. */\nexport function diffText(a: string, b: string): TextDiff {\n let start = 0\n const minLen = Math.min(a.length, b.length)\n while (start < minLen && a.charCodeAt(start) === b.charCodeAt(start)) start++\n let endA = a.length\n let endB = b.length\n while (endA > start && endB > start && a.charCodeAt(endA - 1) === b.charCodeAt(endB - 1)) {\n endA--\n endB--\n }\n return { start, endA, endB }\n}\n\n/** Simple LRU cache for parse results. */\nclass ParseLru {\n private map = new Map<string, Node>()\n private limit: number\n constructor(limit: number) {\n this.limit = limit\n }\n get(key: string): Node | undefined {\n const v = this.map.get(key)\n if (v !== undefined) {\n this.map.delete(key)\n this.map.set(key, v)\n }\n return v\n }\n set(key: string, value: Node): void {\n if (this.map.has(key)) this.map.delete(key)\n this.map.set(key, value)\n if (this.map.size > this.limit) {\n const first = this.map.keys().next().value!\n this.map.delete(first)\n }\n }\n}\n\n/** Configuration for {@link createViewBridge}. */\nexport type ViewBridgeConfig = {\n schema: Schema\n serialize: Serialize\n parse: Parse\n normalize?: Normalize\n /** Called on non-fatal errors (e.g. parse failures). Defaults to `console.error`. */\n onError?: OnError\n /**\n * Optional incremental parser for large documents.\n * When provided, the bridge computes a text-level diff and passes it to this\n * function instead of calling the full {@link Parse}. Return `null` to fall\n * back to full parse.\n */\n incrementalParse?: IncrementalParse\n /** Maximum number of parse results to cache. Defaults to `8`. Set `0` to disable. */\n parseCacheSize?: number\n}\n\n/** Options for {@link ViewBridgeHandle.applyText}. */\nexport type ApplyTextOptions = {\n /** Set `false` to prevent the change from being added to undo history. Default `true`. */\n addToHistory?: boolean\n /**\n * Pre-computed text diff from the editor's change set.\n * When provided, skips the internal `diffText` O(n) scan.\n * The diff describes the changed region between the previous and incoming\n * **normalized** text.\n */\n diff?: TextDiff\n /**\n * Set `true` when the caller guarantees the text is already normalized\n * (no `\\r` characters). Skips the `normalize` pass entirely.\n */\n normalized?: boolean\n}\n\n/**\n * Discriminated-union result of {@link ViewBridgeHandle.applyText}.\n * `ok: true` when the text was applied; `ok: false` with a `reason` otherwise.\n */\nexport type ApplyTextResult =\n | { ok: true }\n | { ok: false; reason: 'unchanged' | 'parse-error' }\n\n/** Handle returned by {@link createViewBridge}. */\nexport type ViewBridgeHandle = {\n /** Parse `text` and replace the ProseMirror document. Returns an {@link ApplyTextResult}. */\n applyText(view: EditorView, text: string, options?: ApplyTextOptions): ApplyTextResult\n /** Serialize the current ProseMirror document to text. */\n extractText(view: EditorView): string\n /** Returns `true` if the transaction was dispatched by {@link applyText}. */\n isBridgeChange(tr: Transaction): boolean\n}\n\n/** Handle returned by {@link createBoundViewBridge}. View is bound; no need to pass it each call. */\nexport type BoundViewBridgeHandle = {\n /** Parse `text` and replace the ProseMirror document. */\n applyText(text: string, options?: ApplyTextOptions): ApplyTextResult\n /** Serialize the current ProseMirror document to text. */\n extractText(): string\n /** Returns `true` if the transaction was dispatched by {@link applyText}. */\n isBridgeChange(tr: Transaction): boolean\n /** Replace the bound EditorView. */\n setView(view: EditorView): void\n}\n\n/**\n * Create a document-sync bridge between ProseMirror and a text editor.\n *\n * Returns a {@link ViewBridgeHandle} with methods to push/pull text and\n * detect bridge-originated transactions.\n */\nexport function createViewBridge(config: ViewBridgeConfig): ViewBridgeHandle {\n const { schema, serialize, parse } = config\n const normalize = config.normalize ?? defaultNormalize\n const onError = config.onError ?? defaultOnError\n const incrementalParse = config.incrementalParse ?? null\n const cacheSize = config.parseCacheSize ?? DEFAULT_PARSE_CACHE_SIZE\n\n // --- Serialize cache: keyed by immutable Node reference ---\n const serializeCache = new WeakMap<Node, string>()\n\n function cachedSerialize(doc: Node): string {\n let text = serializeCache.get(doc)\n if (text === undefined) {\n text = normalize(serialize(doc))\n serializeCache.set(doc, text)\n }\n return text\n }\n\n // --- Parse LRU cache ---\n const parseLru = cacheSize > 0 ? new ParseLru(cacheSize) : null\n\n function cachedParse(text: string): Node {\n if (parseLru) {\n const cached = parseLru.get(text)\n if (cached) return cached\n }\n const doc = parse(text, schema)\n if (parseLru) parseLru.set(text, doc)\n return doc\n }\n\n // --- Last-applied guard ---\n let lastDoc: Node | null = null\n let lastRaw: string | null = null\n let lastIncoming: string | null = null\n\n function markUnchanged(doc: Node, raw: string, incoming: string): ApplyTextResult {\n lastDoc = doc\n lastRaw = raw\n lastIncoming = incoming\n return { ok: false, reason: 'unchanged' }\n }\n\n return {\n applyText(view: EditorView, text: string, options?: ApplyTextOptions): ApplyTextResult {\n const prevDoc = view.state.doc\n\n // Fast path: same doc + same raw text reference as last call → skip normalize\n if (prevDoc === lastDoc && text === lastRaw) {\n return { ok: false, reason: 'unchanged' }\n }\n\n const incoming = options?.normalized ? text : normalize(text)\n\n // Fast path: same doc + same normalized text as last call\n if (prevDoc === lastDoc && incoming === lastIncoming) {\n lastRaw = text\n return { ok: false, reason: 'unchanged' }\n }\n\n // Serialize cache: avoid full tree walk when doc reference is unchanged\n const current = cachedSerialize(prevDoc)\n\n if (incoming === current) {\n return markUnchanged(prevDoc, text, incoming)\n }\n\n // --- Parse (with incremental and LRU cache) ---\n let nextDoc: Node\n let rangeHint: { from: number; to: number; toB: number } | null = null\n const diff = options?.diff ?? diffText(current, incoming)\n try {\n if (incrementalParse) {\n const result: IncrementalParseResult | null =\n incrementalParse({ prevDoc, prevText: current, text: incoming, diff, schema })\n if (result == null) {\n nextDoc = cachedParse(incoming)\n } else if ('doc' in result && 'from' in result) {\n nextDoc = result.doc\n rangeHint = { from: result.from, to: result.to, toB: result.toB }\n } else {\n nextDoc = result as Node\n }\n } else {\n nextDoc = cachedParse(incoming)\n }\n } catch (error) {\n onError({ code: 'parse-error', message: 'failed to parse text into ProseMirror document', cause: error })\n return { ok: false, reason: 'parse-error' }\n }\n\n // Determine the changed document range.\n // If incrementalParse provided positions, skip the O(n) tree diff.\n let from: number, to: number, toB: number\n if (rangeHint) {\n from = rangeHint.from\n to = rangeHint.to\n toB = rangeHint.toB\n } else {\n const start = prevDoc.content.findDiffStart(nextDoc.content)\n if (start == null) {\n return markUnchanged(prevDoc, text, incoming)\n }\n const end = prevDoc.content.findDiffEnd(nextDoc.content)\n if (!end) {\n return markUnchanged(prevDoc, text, incoming)\n }\n from = Math.min(start, end.a)\n to = Math.max(start, end.a)\n toB = Math.max(start, end.b)\n }\n\n const tr = view.state.tr\n tr.replace(from, to, nextDoc.slice(from, toB))\n tr.setMeta(BRIDGE_META, true)\n if (options?.addToHistory === false) {\n tr.setMeta('addToHistory', false)\n }\n view.dispatch(tr)\n\n // Update caches after successful dispatch\n const newDoc = view.state.doc\n serializeCache.set(newDoc, incoming)\n lastDoc = newDoc\n lastRaw = text\n lastIncoming = incoming\n\n return { ok: true }\n },\n\n extractText(view: EditorView): string {\n const text = serialize(view.state.doc)\n serializeCache.set(view.state.doc, normalize(text))\n return text\n },\n\n isBridgeChange(tr: Transaction): boolean {\n return tr.getMeta(BRIDGE_META) === true\n },\n }\n}\n\n/**\n * Create a view-bound document-sync bridge. Wraps {@link createViewBridge}\n * so that the `EditorView` does not need to be passed to each method call.\n *\n * @param view - The initial EditorView to bind.\n * @param config - Configuration for the underlying bridge.\n */\nexport function createBoundViewBridge(view: EditorView, config: ViewBridgeConfig): BoundViewBridgeHandle {\n const inner = createViewBridge(config)\n let currentView = view\n\n return {\n applyText(text: string, options?: ApplyTextOptions): ApplyTextResult {\n return inner.applyText(currentView, text, options)\n },\n extractText(): string {\n return inner.extractText(currentView)\n },\n isBridgeChange(tr: Transaction): boolean {\n return inner.isBridgeChange(tr)\n },\n setView(v: EditorView): void {\n currentView = v\n },\n }\n}\n","import type { Node } from 'prosemirror-model'\nimport type { CursorMapWriter, Matcher, Serialize, SerializeWithMap } from './types.js'\n\n/** A mapping between a ProseMirror position range and a serialized-text offset range. */\nexport type TextSegment = {\n pmStart: number // PM position (inclusive)\n pmEnd: number // PM position (exclusive)\n textStart: number // serialized text offset (inclusive)\n textEnd: number // serialized text offset (exclusive)\n}\n\n/**\n * Sorted list of {@link TextSegment}s produced by {@link buildCursorMap}.\n * Use {@link cursorMapLookup} and {@link reverseCursorMapLookup} for O(log n) queries.\n */\nexport type CursorMap = {\n segments: TextSegment[]\n textLength: number\n /** Number of text nodes that could not be located in the serialized output. */\n skippedNodes: number\n}\n\n/**\n * Create a {@link CursorMapWriter} that tracks offsets and builds segments.\n *\n * Call `getText()` to retrieve the full serialized text.\n * Call `finish(doc)` to produce the final {@link CursorMap}.\n */\nexport function createCursorMapWriter(): CursorMapWriter & {\n getText(): string\n finish(doc: Node): CursorMap\n getMappedCount(): number\n} {\n let offset = 0\n const parts: string[] = []\n const segments: TextSegment[] = []\n let mappedCount = 0\n\n const writer: CursorMapWriter & { getText(): string; finish(doc: Node): CursorMap; getMappedCount(): number } = {\n write(text: string): void {\n parts.push(text)\n offset += text.length\n },\n\n writeMapped(pmStart: number, pmEnd: number, text: string): void {\n segments.push({\n pmStart,\n pmEnd,\n textStart: offset,\n textEnd: offset + text.length,\n })\n parts.push(text)\n offset += text.length\n mappedCount++\n },\n\n getText(): string {\n return parts.join('')\n },\n\n getMappedCount(): number {\n return mappedCount\n },\n\n finish(doc: Node): CursorMap {\n const textNodes: { start: number; end: number }[] = []\n function collectTextNodes(node: Node, contentStart: number): void {\n node.forEach((child, childOffset) => {\n const childPos = contentStart + childOffset\n if (child.isText && child.text) {\n textNodes.push({ start: childPos, end: childPos + child.text.length })\n } else if (!child.isLeaf) {\n collectTextNodes(child, childPos + 1)\n }\n })\n }\n collectTextNodes(doc, 0)\n\n // Count PM text nodes with at least one overlapping mapped segment.\n let mappedNodes = 0\n let segIdx = 0\n for (const n of textNodes) {\n while (segIdx < segments.length && segments[segIdx].pmEnd <= n.start) segIdx++\n let k = segIdx\n while (k < segments.length && segments[k].pmStart < n.end) {\n const s = segments[k]\n if (s.pmEnd > n.start && s.pmStart < n.end) {\n mappedNodes++\n break\n }\n k++\n }\n }\n\n return {\n segments,\n textLength: offset,\n skippedNodes: Math.max(0, textNodes.length - mappedNodes),\n }\n },\n }\n\n return writer\n}\n\n/**\n * Build a cursor map that aligns ProseMirror positions with serialized-text offsets.\n *\n * Accepts either a plain {@link Serialize} `(doc) => string` or a\n * {@link SerializeWithMap} `(doc, writer) => void`. Detection is automatic:\n * if the serializer uses the writer, the exact-by-construction path is used;\n * if it returns a string, an internal `indexOf`-based forward match is applied.\n *\n * The plain `Serialize` path uses exact `indexOf` matching (format-agnostic).\n * For better mapping quality with serializers that transform text (escaping,\n * entity encoding, etc.), use {@link wrapSerialize} with a format-specific\n * {@link Matcher}, or implement {@link SerializeWithMap} directly.\n *\n * @param doc - The ProseMirror document to map.\n * @param serialize - A plain serializer or a writer-based serializer.\n */\nexport function buildCursorMap(\n doc: Node,\n serialize: Serialize | SerializeWithMap,\n): CursorMap {\n const writer = createCursorMapWriter()\n const result = (serialize as (...args: unknown[]) => unknown)(doc, writer)\n\n // Plain Serialize: writer was not used, return value is the serialized string.\n if (typeof result === 'string' && writer.getMappedCount() === 0) {\n return forwardScanBuildMap(doc, result)\n }\n\n // SerializeWithMap: writer was used — exact-by-construction path.\n const map = writer.finish(doc)\n\n // Monotonicity validation for writer-produced segments.\n for (let i = 1; i < map.segments.length; i++) {\n const prev = map.segments[i - 1]\n const curr = map.segments[i]\n if (curr.pmStart < prev.pmEnd || curr.textStart < prev.textEnd) {\n console.warn(\n `[pm-cm] buildCursorMap: non-monotonic segment at index ${i} ` +\n `(pmStart ${curr.pmStart} < prev pmEnd ${prev.pmEnd} or ` +\n `textStart ${curr.textStart} < prev textEnd ${prev.textEnd}). ` +\n 'Ensure writeMapped calls are in ascending PM document order.',\n )\n }\n }\n\n return map\n}\n\n/**\n * Build a cursor map using plain `indexOf` forward matching.\n * Format-agnostic: no character or escape assumptions.\n */\nfunction forwardScanBuildMap(doc: Node, text: string): CursorMap {\n const segments: TextSegment[] = []\n let searchFrom = 0\n let totalTextNodes = 0\n let skippedNodes = 0\n\n function visit(node: Node, contentStart: number): void {\n node.forEach((child, childOffset) => {\n const childPos = contentStart + childOffset\n if (child.isText && child.text) {\n totalTextNodes++\n const idx = text.indexOf(child.text, searchFrom)\n if (idx >= 0) {\n segments.push({\n pmStart: childPos,\n pmEnd: childPos + child.text.length,\n textStart: idx,\n textEnd: idx + child.text.length,\n })\n searchFrom = idx + child.text.length\n } else {\n skippedNodes++\n }\n } else if (!child.isLeaf) {\n visit(child, childPos + 1)\n }\n })\n }\n\n visit(doc, 0)\n return { segments, textLength: text.length, skippedNodes }\n}\n\n/**\n * Look up a ProseMirror position in a cursor map and return the corresponding text offset.\n * Returns `null` when the map has no segments.\n */\nexport function cursorMapLookup(map: CursorMap, pmPos: number): number | null {\n const { segments } = map\n if (segments.length === 0) return null\n\n // Binary search for the segment containing pmPos\n let lo = 0\n let hi = segments.length - 1\n\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1\n const seg = segments[mid]\n\n if (pmPos < seg.pmStart) {\n hi = mid - 1\n } else if (pmPos >= seg.pmEnd) {\n lo = mid + 1\n } else {\n // Inside segment: exact mapping\n return seg.textStart + (pmPos - seg.pmStart)\n }\n }\n\n // pmPos is between segments — snap to nearest boundary\n // After binary search: hi < lo, pmPos falls between segments[hi] and segments[lo]\n const before = hi >= 0 ? segments[hi] : null\n const after = lo < segments.length ? segments[lo] : null\n\n if (!before) return after ? after.textStart : 0\n if (!after) return before.textEnd\n\n const distBefore = pmPos - before.pmEnd\n const distAfter = after.pmStart - pmPos\n return distBefore <= distAfter ? before.textEnd : after.textStart\n}\n\n/**\n * Look up a text offset (e.g. CodeMirror position) in a cursor map and return the corresponding ProseMirror position.\n * Returns `null` when the map has no segments.\n */\nexport function reverseCursorMapLookup(map: CursorMap, cmOffset: number): number | null {\n const { segments } = map\n if (segments.length === 0) return null\n\n // Binary search for the segment containing cmOffset\n let lo = 0\n let hi = segments.length - 1\n\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1\n const seg = segments[mid]\n\n if (cmOffset < seg.textStart) {\n hi = mid - 1\n } else if (cmOffset >= seg.textEnd) {\n lo = mid + 1\n } else {\n // Inside segment: exact mapping\n return seg.pmStart + (cmOffset - seg.textStart)\n }\n }\n\n // cmOffset is between segments — snap to nearest boundary\n const before = hi >= 0 ? segments[hi] : null\n const after = lo < segments.length ? segments[lo] : null\n\n if (!before) return after ? after.pmStart : 0\n if (!after) return before.pmEnd\n\n const distBefore = cmOffset - before.textEnd\n const distAfter = after.textStart - cmOffset\n return distBefore <= distAfter ? before.pmEnd : after.pmStart\n}\n\n/**\n * Wrap a plain {@link Serialize} function as a {@link SerializeWithMap}.\n *\n * When called without a `matcher`, the wrapper uses `indexOf` internally\n * (identical to the default `buildCursorMap` path — useful only for type\n * compatibility).\n *\n * When called with a format-specific {@link Matcher}, the wrapper uses\n * `indexOf` first for each text node, falling back to the matcher when\n * `indexOf` fails. This enables multi-run mapping for serializers that\n * transform text (escaping, entity encoding, etc.).\n *\n * @param serialize - A plain `(doc: Node) => string` serializer.\n * @param matcher - Optional format-specific matcher for improved mapping.\n * @returns A {@link SerializeWithMap} that can be passed to {@link buildCursorMap}.\n */\nexport function wrapSerialize(serialize: Serialize, matcher?: Matcher): SerializeWithMap {\n return (doc: Node, writer: CursorMapWriter): void => {\n const text = serialize(doc)\n const segments = collectMatchedSegments(doc, text, matcher)\n\n // Emit in text order: unmapped gaps then mapped text\n let pos = 0\n for (const seg of segments) {\n if (seg.textStart > pos) writer.write(text.slice(pos, seg.textStart))\n writer.writeMapped(seg.pmStart, seg.pmEnd, text.slice(seg.textStart, seg.textEnd))\n pos = seg.textEnd\n }\n if (pos < text.length) writer.write(text.slice(pos))\n }\n}\n\n/**\n * Collect matched segments for all PM text nodes using indexOf + optional matcher fallback.\n */\nfunction collectMatchedSegments(\n doc: Node,\n text: string,\n matcher: Matcher | undefined,\n): TextSegment[] {\n const segments: TextSegment[] = []\n let searchFrom = 0\n\n function visit(node: Node, contentStart: number): void {\n node.forEach((child, childOffset) => {\n const childPos = contentStart + childOffset\n if (child.isText && child.text) {\n const content = child.text\n\n // 1. Try exact indexOf first (strongest signal, no false positives)\n const exactIdx = text.indexOf(content, searchFrom)\n if (exactIdx >= 0) {\n segments.push({\n pmStart: childPos,\n pmEnd: childPos + content.length,\n textStart: exactIdx,\n textEnd: exactIdx + content.length,\n })\n searchFrom = exactIdx + content.length\n return\n }\n\n // 2. If matcher provided, try format-specific matching\n if (matcher) {\n const result = matcher(text, content, searchFrom)\n if (result) {\n for (const run of result.runs) {\n segments.push({\n pmStart: childPos + run.contentStart,\n pmEnd: childPos + run.contentEnd,\n textStart: run.textStart,\n textEnd: run.textEnd,\n })\n }\n searchFrom = result.nextSearchFrom\n return\n }\n }\n\n // 3. Both failed — node skipped (searchFrom not advanced)\n } else if (!child.isLeaf) {\n visit(child, childPos + 1)\n }\n })\n }\n\n visit(doc, 0)\n return segments\n}\n"],"mappings":"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","names":[]}