@powerduck/md-editor 0.2.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/dist/index.mjs ADDED
@@ -0,0 +1,482 @@
1
+ import { Compartment as w, EditorState as k } from "@codemirror/state";
2
+ import { EditorView as m, keymap as b, lineNumbers as p } from "@codemirror/view";
3
+ import { history as v, defaultKeymap as g, historyKeymap as y } from "@codemirror/commands";
4
+ import { markdown as C } from "@codemirror/lang-markdown";
5
+ import { languages as B } from "@codemirror/language-data";
6
+ import M from "markdown-it";
7
+ import E from "markdown-it-texmath";
8
+ import P from "katex";
9
+ class x {
10
+ constructor(e, t) {
11
+ this.lineNumbersCompartment = new w();
12
+ const r = [
13
+ v(),
14
+ b.of([...g, ...y]),
15
+ C({ codeLanguages: B }),
16
+ m.lineWrapping,
17
+ this.lineNumbersCompartment.of(t.lineNumbers ? p() : []),
18
+ m.updateListener.of((i) => {
19
+ var s;
20
+ i.docChanged && ((s = t.onChange) == null || s.call(t, i.state.doc.toString()));
21
+ })
22
+ ];
23
+ this.view = new m({
24
+ state: k.create({ doc: t.value ?? "", extensions: r }),
25
+ parent: e
26
+ });
27
+ }
28
+ getValue() {
29
+ return this.view.state.doc.toString();
30
+ }
31
+ setValue(e) {
32
+ this.view.dispatch({
33
+ changes: { from: 0, to: this.view.state.doc.length, insert: e }
34
+ });
35
+ }
36
+ setLineNumbers(e) {
37
+ this.view.dispatch({
38
+ effects: this.lineNumbersCompartment.reconfigure(e ? p() : [])
39
+ });
40
+ }
41
+ insertAtCursor(e) {
42
+ const { from: t, to: r } = this.view.state.selection.main;
43
+ this.view.dispatch({
44
+ changes: { from: t, to: r, insert: e },
45
+ selection: { anchor: t + e.length }
46
+ }), this.view.focus();
47
+ }
48
+ focus() {
49
+ this.view.focus();
50
+ }
51
+ destroy() {
52
+ this.view.destroy();
53
+ }
54
+ }
55
+ function S(n) {
56
+ n.use(E, {
57
+ engine: P,
58
+ delimiters: "dollars",
59
+ katexOptions: {
60
+ throwOnError: !1,
61
+ strict: !1
62
+ }
63
+ });
64
+ }
65
+ let T = 0;
66
+ function L(n) {
67
+ const e = n.renderer.rules.fence ? n.renderer.rules.fence.bind(n.renderer.rules) : (t, r, i, s, o) => o.renderToken(t, r, i);
68
+ n.renderer.rules.fence = (t, r, i, s, o) => {
69
+ const a = t[r];
70
+ if (!a) return e(t, r, i, s, o);
71
+ if (a.info.trim().toLowerCase() === "mindmap") {
72
+ const h = `md-editor-mindmap-${++T}`, u = encodeURIComponent(a.content);
73
+ return `<div class="md-editor-mindmap" id="${h}" data-source="${u}"></div>`;
74
+ }
75
+ return e(t, r, i, s, o);
76
+ };
77
+ }
78
+ async function N(n) {
79
+ const e = n.querySelectorAll(
80
+ ".md-editor-mindmap:not([data-hydrated])"
81
+ );
82
+ if (e.length === 0) return;
83
+ const { Transformer: t } = await import("markmap-lib"), r = await import("markmap-view"), i = r.Markmap ?? r.default;
84
+ if (!i) return;
85
+ const s = new t();
86
+ for (const o of Array.from(e)) {
87
+ o.setAttribute("data-hydrated", "1");
88
+ const a = decodeURIComponent(o.dataset.source ?? "") || "- Empty mindmap", { root: d } = s.transform(a);
89
+ o.innerHTML = "";
90
+ const h = document.createElementNS("http://www.w3.org/2000/svg", "svg");
91
+ h.setAttribute("style", "width:100%;height:100%;min-height:280px;"), o.appendChild(h), i.create(h, { autoFit: !0 }, d);
92
+ }
93
+ }
94
+ class R {
95
+ constructor(e = {}) {
96
+ this.md = new M({
97
+ html: e.html ?? !1,
98
+ linkify: !0,
99
+ breaks: e.breaks ?? !1,
100
+ typographer: !0
101
+ }), e.math !== !1 && S(this.md), this.mindmapEnabled = e.mindmap !== !1, this.mindmapEnabled && L(this.md);
102
+ }
103
+ render(e) {
104
+ return this.md.render(e || "");
105
+ }
106
+ /**
107
+ * Call after the rendered HTML is inserted into the DOM to hydrate
108
+ * mindmap placeholders into real SVG diagrams.
109
+ */
110
+ async hydrate(e) {
111
+ this.mindmapEnabled && await N(e);
112
+ }
113
+ }
114
+ class H {
115
+ constructor(e) {
116
+ this.buttons = /* @__PURE__ */ new Map(), this.el = document.createElement("div"), this.el.className = "md-editor-toolbar", this.el.setAttribute("role", "toolbar");
117
+ for (const t of e) {
118
+ const r = document.createElement("button");
119
+ if (r.type = "button", r.className = "md-editor-toolbar-btn", t.toggle && r.classList.add("is-toggle"), t.active && r.classList.add("is-active"), r.title = t.title, r.setAttribute("aria-label", t.title), r.setAttribute("data-action", t.name), r.innerHTML = t.icon, r.addEventListener("click", t.handler), this.buttons.set(t.name, r), this.el.appendChild(r), t.groupEnd) {
120
+ const i = document.createElement("span");
121
+ i.className = "md-editor-toolbar-sep", this.el.appendChild(i);
122
+ }
123
+ }
124
+ }
125
+ setActive(e, t) {
126
+ var r;
127
+ (r = this.buttons.get(e)) == null || r.classList.toggle("is-active", t);
128
+ }
129
+ setIcon(e, t) {
130
+ const r = this.buttons.get(e);
131
+ r && (r.innerHTML = t);
132
+ }
133
+ destroy() {
134
+ for (const e of this.buttons.values())
135
+ e.onclick = null;
136
+ this.buttons.clear(), this.el.remove();
137
+ }
138
+ }
139
+ function f(n) {
140
+ const e = n.trim();
141
+ return {
142
+ charCount: n.length,
143
+ wordCount: e ? e.split(/\s+/).length : 0
144
+ };
145
+ }
146
+ class A {
147
+ constructor(e) {
148
+ this.el = document.createElement("div"), this.el.className = "md-editor-statusbar", this.countEl = document.createElement("span"), this.blockEl = document.createElement("span"), this.blockEl.className = "md-editor-statusbar-muted", this.refreshBtn = document.createElement("button"), this.refreshBtn.type = "button", this.refreshBtn.className = "md-editor-statusbar-refresh", this.refreshBtn.textContent = "Refresh preview", this.refreshBtn.style.display = "none", this.refreshBtn.addEventListener("click", e.onRefreshPreview), this.el.appendChild(this.countEl), this.el.appendChild(this.blockEl), this.el.appendChild(this.refreshBtn);
149
+ }
150
+ update(e) {
151
+ this.countEl.textContent = `${e.charCount} chars · ${e.wordCount} words`, this.blockEl.textContent = `${e.blockCount} render blocks`, this.refreshBtn.style.display = e.autoPreview ? "none" : "";
152
+ }
153
+ destroy() {
154
+ this.refreshBtn.onclick = null, this.el.remove();
155
+ }
156
+ }
157
+ const I = /^ {0,3}#{1,6}\s/, V = /^ {0,3}(`{3,}|~{3,})/;
158
+ function $(n) {
159
+ let e = 5381;
160
+ for (let t = 0; t < n.length; t++)
161
+ e = e * 33 ^ n.charCodeAt(t);
162
+ return (e >>> 0).toString(36) + ":" + n.length;
163
+ }
164
+ function j(n) {
165
+ var a;
166
+ const e = n.split(`
167
+ `), t = [];
168
+ let r = [], i = !1, s = "";
169
+ for (const d of e) {
170
+ const h = d.match(V);
171
+ if (h) {
172
+ const u = ((a = h[1]) == null ? void 0 : a[0]) ?? "";
173
+ i ? u === s && (i = !1) : (i = !0, s = u), r.push(d);
174
+ continue;
175
+ }
176
+ if (!i && I.test(d) && r.length > 0) {
177
+ t.push(r.join(`
178
+ `)), r = [d];
179
+ continue;
180
+ }
181
+ r.push(d);
182
+ }
183
+ r.length > 0 && t.push(r.join(`
184
+ `)), t.length === 0 && t.push("");
185
+ const o = /* @__PURE__ */ new Map();
186
+ return t.map((d) => {
187
+ const h = $(d), u = (o.get(h) ?? 0) + 1;
188
+ return o.set(h, u), { key: `${h}#${u}`, hash: h, source: d };
189
+ });
190
+ }
191
+ class K {
192
+ constructor(e, t, r = 800) {
193
+ this.container = e, this.renderBlock = t, this.maxCacheEntries = r, this.nodeByKey = /* @__PURE__ */ new Map(), this.htmlByHash = /* @__PURE__ */ new Map(), this.prevBlocks = [];
194
+ }
195
+ update(e) {
196
+ const t = j(e), r = document.createDocumentFragment(), i = /* @__PURE__ */ new Map();
197
+ let s = 0;
198
+ for (const o of t) {
199
+ let a = this.nodeByKey.get(o.key);
200
+ if (!a) {
201
+ let d = this.htmlByHash.get(o.hash);
202
+ d === void 0 && (d = this.renderBlock(o.source), this.htmlByHash.set(o.hash, d), s++), a = document.createElement("div"), a.className = "md-editor-block", a.dataset.blockKey = o.key, a.innerHTML = d;
203
+ }
204
+ i.set(o.key, a), r.appendChild(a);
205
+ }
206
+ return this.container.innerHTML = "", this.container.appendChild(r), this.nodeByKey = i, this.prevBlocks = t, this.evictCacheIfNeeded(t), { rerenderedCount: s, totalCount: t.length };
207
+ }
208
+ /**
209
+ * When the cache exceeds the limit, evict hashes that no longer appear in
210
+ * the current document to prevent unbounded memory growth during long
211
+ * editing sessions.
212
+ */
213
+ evictCacheIfNeeded(e) {
214
+ if (this.htmlByHash.size <= this.maxCacheEntries) return;
215
+ const t = new Set(e.map((r) => r.hash));
216
+ for (const r of Array.from(this.htmlByHash.keys()))
217
+ t.has(r) || this.htmlByHash.delete(r);
218
+ }
219
+ getBlockCount() {
220
+ return this.prevBlocks.length;
221
+ }
222
+ destroy() {
223
+ this.container.innerHTML = "", this.nodeByKey.clear(), this.htmlByHash.clear(), this.prevBlocks = [];
224
+ }
225
+ }
226
+ function q(n, e) {
227
+ let t, r;
228
+ const i = (...s) => {
229
+ r = s, t !== void 0 && clearTimeout(t), t = setTimeout(() => {
230
+ t = void 0, r && n(...r);
231
+ }, e);
232
+ };
233
+ return i.cancel = () => {
234
+ t !== void 0 && clearTimeout(t), t = void 0;
235
+ }, i.flush = () => {
236
+ t !== void 0 && (clearTimeout(t), t = void 0, r && n(...r));
237
+ }, i;
238
+ }
239
+ function D(n, e = 120, t = 600) {
240
+ const r = Math.max(0, n), i = Math.floor(r / 2e4) * 60;
241
+ return Math.min(t, e + i);
242
+ }
243
+ function Q(n, e = 300) {
244
+ const t = window;
245
+ return typeof t.requestIdleCallback == "function" ? t.requestIdleCallback(n, { timeout: e }) : window.setTimeout(n, 0);
246
+ }
247
+ function X(n) {
248
+ const e = window;
249
+ typeof e.cancelIdleCallback == "function" ? e.cancelIdleCallback(n) : clearTimeout(n);
250
+ }
251
+ const c = (n) => `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">${n}</svg>`, l = {
252
+ bold: c(
253
+ '<path d="M4 3h4.2a2.4 2.4 0 0 1 0 4.8H4V3Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M4 7.8h4.8a2.4 2.4 0 1 1 0 4.8H4V7.8Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>'
254
+ ),
255
+ italic: c(
256
+ '<path d="M9.5 3h-3M6.5 13h-3M8.2 3 5.8 13" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>'
257
+ ),
258
+ heading: c(
259
+ '<path d="M3.5 3v10M11.5 3v10M3.5 8h8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>'
260
+ ),
261
+ link: c(
262
+ '<path d="M6.8 9.2 9.2 6.8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M7.6 4.4 8.4 3.6a2.6 2.6 0 1 1 3.7 3.7l-.9.9M8.4 11.6l-.8.8a2.6 2.6 0 1 1-3.7-3.7l.9-.9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>'
263
+ ),
264
+ code: c(
265
+ '<path d="M5.5 4.5 2 8l3.5 3.5M10.5 4.5 14 8l-3.5 3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>'
266
+ ),
267
+ table: c(
268
+ '<rect x="2.5" y="3" width="11" height="10" rx="1.2" stroke="currentColor" stroke-width="1.3"/><path d="M2.5 6.6h11M6.5 3v10" stroke="currentColor" stroke-width="1.3"/>'
269
+ ),
270
+ sigma: c(
271
+ '<path d="M11.5 3.5h-7l3.5 4.5-3.5 4.5h7" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" stroke-linecap="round"/>'
272
+ ),
273
+ mindmap: c(
274
+ '<circle cx="3.2" cy="8" r="1.6" stroke="currentColor" stroke-width="1.3"/><circle cx="12.2" cy="3.6" r="1.6" stroke="currentColor" stroke-width="1.3"/><circle cx="12.2" cy="12.4" r="1.6" stroke="currentColor" stroke-width="1.3"/><path d="M4.6 7.3 10.7 4.2M4.6 8.7l6.1 3.1" stroke="currentColor" stroke-width="1.3"/>'
275
+ ),
276
+ eye: c(
277
+ '<path d="M1.5 8S4 3.5 8 3.5 14.5 8 14.5 8 12 12.5 8 12.5 1.5 8 1.5 8Z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><circle cx="8" cy="8" r="1.8" stroke="currentColor" stroke-width="1.3"/>'
278
+ ),
279
+ refresh: c(
280
+ '<path d="M13 8a5 5 0 1 1-1.6-3.7" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M13 3v3.2H9.8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>'
281
+ ),
282
+ moon: c(
283
+ '<path d="M13 9.6A5.4 5.4 0 1 1 6.4 3a4.3 4.3 0 0 0 6.6 6.6Z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/>'
284
+ ),
285
+ sun: c(
286
+ '<circle cx="8" cy="8" r="3" stroke="currentColor" stroke-width="1.3"/><path d="M8 1.8v1.4M8 12.8v1.4M14.2 8h-1.4M3.2 8H1.8M12.4 3.6l-1 1M4.6 11.4l-1 1M12.4 12.4l-1-1M4.6 4.6l-1-1" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>'
287
+ )
288
+ }, F = {
289
+ bold: ["**", "**", "bold text"],
290
+ italic: ["_", "_", "italic text"],
291
+ heading: ["## ", "", "Heading"],
292
+ link: ["[", "](https://)", "link text"],
293
+ code: ["```\n", "\n```", "code"],
294
+ math: ["$", "$", "E=mc^2"],
295
+ mindmapBlock: [
296
+ `\`\`\`mindmap
297
+ # Root topic
298
+ ## Branch one
299
+ - Child A
300
+ - Child B
301
+ ## Branch two
302
+ \`\`\`
303
+ `,
304
+ "",
305
+ ""
306
+ ],
307
+ table: [`| Col 1 | Col 2 |
308
+ | --- | --- |
309
+ | `, ` | content |
310
+ `, "content"]
311
+ };
312
+ class Y {
313
+ constructor(e, t = {}) {
314
+ this.previewPane = null, this.incremental = null, this.toolbar = null, this.statusBar = null, this.scheduledRender = null, this.scheduledWait = 0;
315
+ const r = typeof e == "string" ? document.querySelector(e) : e;
316
+ if (!r) throw new Error("MarkdownEditor: mount container not found");
317
+ this.container = r, this.mode = t.mode ?? "simple", this.theme = t.theme ?? "light", this.showPreview = t.preview ?? !0, this.autoPreview = t.autoPreview ?? !0, this.renderDebounceOpt = t.renderDebounce, this.onChange = t.onChange, this.container.classList.add("md-editor-root", `md-editor-mode-${this.mode}`), this.container.setAttribute("data-theme", this.theme), this.renderer = new R({
318
+ math: t.math ?? !0,
319
+ mindmap: t.mindmap ?? !0
320
+ }), this.buildLayout(), this.codeEditor = new x(this.editorPane, {
321
+ value: t.value ?? "",
322
+ lineNumbers: this.mode === "complex",
323
+ onChange: (i) => {
324
+ var s;
325
+ (s = this.onChange) == null || s.call(this, i), this.autoPreview && this.requestPreviewRender(i), this.updateStatusBar(i);
326
+ }
327
+ }), this.mode === "complex" && (this.buildToolbar(), this.buildStatusBar()), this.previewPane && (this.incremental = new K(
328
+ this.previewPane,
329
+ (i) => this.renderer.render(i)
330
+ )), this.renderPreview(t.value ?? ""), this.updateStatusBar(t.value ?? "");
331
+ }
332
+ // ---------- Layout ----------
333
+ buildLayout() {
334
+ this.container.innerHTML = "", this.body = document.createElement("div"), this.body.className = "md-editor-body", this.editorPane = document.createElement("div"), this.editorPane.className = "md-editor-pane md-editor-source", this.body.appendChild(this.editorPane), this.showPreview && (this.previewPane = document.createElement("div"), this.previewPane.className = "md-editor-pane md-editor-preview", this.body.appendChild(this.previewPane)), this.container.appendChild(this.body);
335
+ }
336
+ buildToolbar() {
337
+ const e = [
338
+ { name: "heading", title: "Heading", icon: l.heading, handler: () => this.insertSnippet("heading") },
339
+ { name: "bold", title: "Bold", icon: l.bold, handler: () => this.insertSnippet("bold") },
340
+ {
341
+ name: "italic",
342
+ title: "Italic",
343
+ icon: l.italic,
344
+ handler: () => this.insertSnippet("italic"),
345
+ groupEnd: !0
346
+ },
347
+ { name: "link", title: "Link", icon: l.link, handler: () => this.insertSnippet("link") },
348
+ { name: "code", title: "Code block", icon: l.code, handler: () => this.insertSnippet("code") },
349
+ {
350
+ name: "table",
351
+ title: "Table",
352
+ icon: l.table,
353
+ handler: () => this.insertSnippet("table"),
354
+ groupEnd: !0
355
+ },
356
+ { name: "math", title: "Math formula", icon: l.sigma, handler: () => this.insertSnippet("math") },
357
+ {
358
+ name: "mindmap",
359
+ title: "Insert mindmap",
360
+ icon: l.mindmap,
361
+ handler: () => this.insertSnippet("mindmapBlock"),
362
+ groupEnd: !0
363
+ },
364
+ {
365
+ name: "preview",
366
+ title: "Toggle preview",
367
+ icon: l.eye,
368
+ toggle: !0,
369
+ active: this.showPreview,
370
+ handler: () => this.togglePreview()
371
+ },
372
+ {
373
+ name: "theme",
374
+ title: "Toggle theme",
375
+ icon: this.theme === "dark" ? l.sun : l.moon,
376
+ handler: () => this.setTheme(this.theme === "dark" ? "light" : "dark")
377
+ }
378
+ ];
379
+ this.toolbar = new H(e), this.container.insertBefore(this.toolbar.el, this.body);
380
+ }
381
+ buildStatusBar() {
382
+ this.statusBar = new A({
383
+ onRefreshPreview: () => this.renderPreview(this.codeEditor.getValue())
384
+ }), this.container.appendChild(this.statusBar.el);
385
+ }
386
+ insertSnippet(e) {
387
+ const [t, r, i] = F[e];
388
+ this.codeEditor.insertAtCursor(`${t}${i}${r}`);
389
+ }
390
+ togglePreview() {
391
+ var t;
392
+ if (!this.previewPane) return;
393
+ const e = this.previewPane.style.display !== "none";
394
+ this.previewPane.style.display = e ? "none" : "", (t = this.toolbar) == null || t.setActive("preview", !e);
395
+ }
396
+ // ---------- Render scheduling (performance core) ----------
397
+ requestPreviewRender(e) {
398
+ var r, i;
399
+ const t = this.renderDebounceOpt === !1 ? 0 : this.renderDebounceOpt ?? D(e.length);
400
+ if (t === 0) {
401
+ (r = this.scheduledRender) == null || r.cancel(), this.scheduledRender = null, this.renderPreview(e);
402
+ return;
403
+ }
404
+ (!this.scheduledRender || this.scheduledWait !== t) && ((i = this.scheduledRender) == null || i.cancel(), this.scheduledRender = q((s) => this.renderPreview(s), t), this.scheduledWait = t), this.scheduledRender(e);
405
+ }
406
+ renderPreview(e) {
407
+ if (!this.previewPane || !this.incremental) return;
408
+ const { totalCount: t } = this.incremental.update(e);
409
+ if (this.renderer.hydrate(this.previewPane), this.statusBar) {
410
+ const { charCount: r, wordCount: i } = f(e);
411
+ this.statusBar.update({
412
+ charCount: r,
413
+ wordCount: i,
414
+ blockCount: t,
415
+ mode: this.mode,
416
+ autoPreview: this.autoPreview
417
+ });
418
+ }
419
+ }
420
+ updateStatusBar(e) {
421
+ var i;
422
+ if (!this.statusBar) return;
423
+ const { charCount: t, wordCount: r } = f(e);
424
+ this.statusBar.update({
425
+ charCount: t,
426
+ wordCount: r,
427
+ blockCount: ((i = this.incremental) == null ? void 0 : i.getBlockCount()) ?? 0,
428
+ mode: this.mode,
429
+ autoPreview: this.autoPreview
430
+ });
431
+ }
432
+ // ---------- Public API ----------
433
+ getValue() {
434
+ return this.codeEditor.getValue();
435
+ }
436
+ setValue(e) {
437
+ var t;
438
+ this.codeEditor.setValue(e), (t = this.scheduledRender) == null || t.cancel(), this.renderPreview(e), this.updateStatusBar(e);
439
+ }
440
+ getHtml() {
441
+ return this.renderer.render(this.getValue());
442
+ }
443
+ /** Manually trigger a preview render (use when autoPreview is false). */
444
+ renderNow() {
445
+ var e;
446
+ (e = this.scheduledRender) == null || e.cancel(), this.renderPreview(this.codeEditor.getValue());
447
+ }
448
+ setAutoPreview(e) {
449
+ this.autoPreview = e, e && this.renderPreview(this.codeEditor.getValue()), this.updateStatusBar(this.codeEditor.getValue());
450
+ }
451
+ /** Switch between simple and complex modes at runtime. */
452
+ setMode(e) {
453
+ var t, r;
454
+ e !== this.mode && (this.mode = e, this.container.classList.remove("md-editor-mode-simple", "md-editor-mode-complex"), this.container.classList.add(`md-editor-mode-${e}`), this.codeEditor.setLineNumbers(e === "complex"), (t = this.toolbar) == null || t.destroy(), this.toolbar = null, (r = this.statusBar) == null || r.destroy(), this.statusBar = null, e === "complex" && (this.buildToolbar(), this.buildStatusBar(), this.updateStatusBar(this.codeEditor.getValue())));
455
+ }
456
+ setTheme(e) {
457
+ var t;
458
+ this.theme = e, this.container.setAttribute("data-theme", e), (t = this.toolbar) == null || t.setIcon("theme", e === "dark" ? l.sun : l.moon);
459
+ }
460
+ focus() {
461
+ this.codeEditor.focus();
462
+ }
463
+ destroy() {
464
+ var e, t, r, i;
465
+ (e = this.scheduledRender) == null || e.cancel(), this.scheduledRender = null, this.codeEditor.destroy(), (t = this.toolbar) == null || t.destroy(), this.toolbar = null, (r = this.statusBar) == null || r.destroy(), this.statusBar = null, (i = this.incremental) == null || i.destroy(), this.incremental = null, this.container.innerHTML = "", this.container.classList.remove("md-editor-root", `md-editor-mode-${this.mode}`), this.container.removeAttribute("data-theme");
466
+ }
467
+ }
468
+ export {
469
+ x as CodeEditor,
470
+ K as IncrementalRenderer,
471
+ Y as MarkdownEditor,
472
+ R as Renderer,
473
+ A as StatusBar,
474
+ H as Toolbar,
475
+ D as adaptiveDebounceMs,
476
+ X as cancelIdle,
477
+ f as countText,
478
+ q as debounce,
479
+ l as icons,
480
+ Q as scheduleIdle,
481
+ j as splitIntoBlocks
482
+ };
@@ -0,0 +1,41 @@
1
+ export interface IncrementalRenderResult {
2
+ /** Number of blocks actually re-rendered (cache miss) this update */
3
+ rerenderedCount: number;
4
+ /** Total number of blocks in this update */
5
+ totalCount: number;
6
+ }
7
+ /**
8
+ * Core performance strategy:
9
+ *
10
+ * 1. Source is split into blocks (see blocks.ts). Only changed blocks are
11
+ * re-run through markdown-it / KaTeX.
12
+ * 2. Unchanged blocks reuse their existing DOM nodes directly (including
13
+ * already-hydrated mindmap SVGs), with no string concatenation, innerHTML
14
+ * assignment, or KaTeX computation.
15
+ * 3. Reordering is handled by appending existing nodes to a DocumentFragment
16
+ * -- the browser moves them natively (auto-detaching from the old parent),
17
+ * so no manual node-shuttling code is needed.
18
+ * 4. The result cache is keyed by content hash (not position), so undo/redo
19
+ * and copy-paste of identical paragraphs also hit the cache.
20
+ * 5. Each block is wrapped in content-visibility: auto (see editor.css), so
21
+ * off-screen blocks skip layout and paint entirely -- a decisive win for
22
+ * multi-thousand-line documents.
23
+ */
24
+ export declare class IncrementalRenderer {
25
+ private readonly container;
26
+ private readonly renderBlock;
27
+ private readonly maxCacheEntries;
28
+ private nodeByKey;
29
+ private htmlByHash;
30
+ private prevBlocks;
31
+ constructor(container: HTMLElement, renderBlock: (source: string) => string, maxCacheEntries?: number);
32
+ update(source: string): IncrementalRenderResult;
33
+ /**
34
+ * When the cache exceeds the limit, evict hashes that no longer appear in
35
+ * the current document to prevent unbounded memory growth during long
36
+ * editing sessions.
37
+ */
38
+ private evictCacheIfNeeded;
39
+ getBlockCount(): number;
40
+ destroy(): void;
41
+ }
@@ -0,0 +1,23 @@
1
+ export interface SourceBlock {
2
+ /** Stable key for DOM node reuse (content hash + occurrence index) */
3
+ key: string;
4
+ /** Content hash used to detect whether this block changed */
5
+ hash: string;
6
+ /** Original markdown source for this block */
7
+ source: string;
8
+ }
9
+ /**
10
+ * Split a markdown document into blocks at ATX heading lines (# through ######).
11
+ *
12
+ * This split is safe under CommonMark semantics: a heading line is always the
13
+ * start of a new block, so unlike splitting on blank lines we never break
14
+ * list or blockquote structures that span blank lines.
15
+ *
16
+ * Content inside code fences (``` or ~~~) is never treated as a heading,
17
+ * preventing "# comment" in example code from being misread as a split point.
18
+ *
19
+ * Documents with no headings degrade to a single block. In that case
20
+ * incremental rendering falls back to full-document rendering, but the
21
+ * cross-edit result cache (htmlByHash) is still effective.
22
+ */
23
+ export declare function splitIntoBlocks(source: string): SourceBlock[];
@@ -0,0 +1,21 @@
1
+ export interface DebouncedFn<A extends unknown[]> {
2
+ (...args: A): void;
3
+ cancel: () => void;
4
+ flush: () => void;
5
+ }
6
+ /**
7
+ * Trailing-edge debounce. Editor input events fire on every keystroke; the
8
+ * preview does not need to re-render on every one. Rendering after a pause
9
+ * is the first gatekeeping mechanism for keeping large documents responsive.
10
+ */
11
+ export declare function debounce<A extends unknown[]>(fn: (...args: A) => void, wait: number): DebouncedFn<A>;
12
+ /**
13
+ * Adaptive debounce interval based on document length. Larger documents cost
14
+ * more to render, so the pause should be longer to avoid redundant renders.
15
+ * Capped at max so small edits in large docs do not wait excessively.
16
+ */
17
+ export declare function adaptiveDebounceMs(docLength: number, base?: number, max?: number): number;
18
+ type IdleHandle = number;
19
+ export declare function scheduleIdle(callback: () => void, timeout?: number): IdleHandle;
20
+ export declare function cancelIdle(handle: IdleHandle): void;
21
+ export {};
@@ -0,0 +1,7 @@
1
+ import type MarkdownIt from 'markdown-it';
2
+ /**
3
+ * Enable math formula support via KaTeX.
4
+ * Inline: $E=mc^2$
5
+ * Block: $$ \\int_0^1 x^2 dx $$
6
+ */
7
+ export declare function useMath(md: MarkdownIt): void;
@@ -0,0 +1,24 @@
1
+ import type MarkdownIt from 'markdown-it';
2
+ /**
3
+ * Intercept ```mindmap code fences and convert their content (a markdown
4
+ * outline) into a placeholder div. The actual SVG rendering happens
5
+ * asynchronously in hydrateMindmaps() because markdown-it renders strings
6
+ * synchronously while markmap requires real DOM nodes.
7
+ *
8
+ * Usage (in the editor markdown source):
9
+ *
10
+ * ```mindmap
11
+ * # Root topic
12
+ * ## Branch one
13
+ * - Child A
14
+ * - Child B
15
+ * ## Branch two
16
+ * ```
17
+ */
18
+ export declare function useMindmap(md: MarkdownIt): void;
19
+ /**
20
+ * After the preview DOM is updated, render all non-hydrated mindmap nodes.
21
+ * markmap-lib / markmap-view are dynamically imported so they do not
22
+ * increase the initial bundle size when mindmaps are absent.
23
+ */
24
+ export declare function hydrateMindmaps(root: HTMLElement): Promise<void>;
@@ -0,0 +1,32 @@
1
+ import { type CSSProperties } from "react";
2
+ import { type EditorMode, type EditorTheme, type MarkdownEditorOptions } from "../index.js";
3
+ export interface MarkdownEditorProps extends Omit<MarkdownEditorOptions, "value" | "onChange"> {
4
+ /** Controlled value. When provided and different from internal state, syncs the editor. */
5
+ value?: string;
6
+ /** Uncontrolled initial value. Only applied on mount. */
7
+ defaultValue?: string;
8
+ onChange?: (value: string) => void;
9
+ className?: string;
10
+ style?: CSSProperties;
11
+ }
12
+ export interface MarkdownEditorHandle {
13
+ getValue: () => string;
14
+ setValue: (value: string) => void;
15
+ getHtml: () => string;
16
+ focus: () => void;
17
+ setMode: (mode: EditorMode) => void;
18
+ setTheme: (theme: EditorTheme) => void;
19
+ /** Manually trigger a preview render when autoPreview is false. */
20
+ renderNow: () => void;
21
+ }
22
+ /**
23
+ * Only "structural" options (mode, math, mindmap, preview, autoPreview,
24
+ * renderDebounce) trigger a destroy-and-recreate of the underlying instance,
25
+ * because they change DOM structure or the rendering pipeline.
26
+ *
27
+ * theme and value use their own lightweight sync paths and never recreate
28
+ * the instance -- critical for large documents where rebuilding CodeMirror
29
+ * and clearing the incremental render cache on every React re-render would
30
+ * be prohibitively expensive.
31
+ */
32
+ export declare const MarkdownEditorReact: import("react").ForwardRefExoticComponent<MarkdownEditorProps & import("react").RefAttributes<MarkdownEditorHandle>>;
@@ -0,0 +1,3 @@
1
+ export { MarkdownEditorReact } from './MarkdownEditor.js';
2
+ export type { MarkdownEditorProps, MarkdownEditorHandle } from './MarkdownEditor.js';
3
+ export type { EditorMode, EditorTheme, MarkdownEditorOptions } from '../index.js';
package/dist/react.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h=require("react/jsx-runtime"),n=require("react"),y=require("./index.cjs"),k=n.forwardRef(function(g,v){const{value:u,defaultValue:E,onChange:a,className:M,style:V,mode:i,theme:c,math:d,mindmap:l,preview:f,autoPreview:m,renderDebounce:w}=g,s=n.useRef(null),t=n.useRef(null),o=n.useRef(a);return o.current=a,n.useLayoutEffect(()=>{if(!s.current)return;const e=new y.MarkdownEditor(s.current,{value:u??E??"",mode:i,theme:c,math:d,mindmap:l,preview:f,autoPreview:m,renderDebounce:w,onChange:r=>{var R;return(R=o.current)==null?void 0:R.call(o,r)}});return t.current=e,()=>{e.destroy(),t.current=null}},[i,d,l,f,m,w]),n.useEffect(()=>{const e=t.current;!e||u===void 0||e.getValue()!==u&&e.setValue(u)},[u]),n.useEffect(()=>{t.current&&c&&t.current.setTheme(c)},[c]),n.useImperativeHandle(v,()=>({getValue:()=>{var e;return((e=t.current)==null?void 0:e.getValue())??""},setValue:e=>{var r;return(r=t.current)==null?void 0:r.setValue(e)},getHtml:()=>{var e;return((e=t.current)==null?void 0:e.getHtml())??""},focus:()=>{var e;return(e=t.current)==null?void 0:e.focus()},setMode:e=>{var r;return(r=t.current)==null?void 0:r.setMode(e)},setTheme:e=>{var r;return(r=t.current)==null?void 0:r.setTheme(e)},renderNow:()=>{var e;return(e=t.current)==null?void 0:e.renderNow()}}),[]),h.jsx("div",{ref:s,className:M,style:V})});exports.MarkdownEditorReact=k;