@latentic/live-markdown 0.0.1

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.
@@ -0,0 +1,805 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+ import * as _codemirror_view from '@codemirror/view';
5
+ import { Decoration, ViewPlugin, DecorationSet, ViewUpdate, EditorView, Command, KeyBinding } from '@codemirror/view';
6
+ import * as _codemirror_state from '@codemirror/state';
7
+ import { EditorState, Facet, Extension } from '@codemirror/state';
8
+ import { syntaxTree } from '@codemirror/language';
9
+
10
+ /**
11
+ * Core value types for the editor's public surface.
12
+ *
13
+ * `SourceRange` is a half-open byte range into the document; `DocumentTextChange`
14
+ * describes one edit. They are structurally identical to the host app's own
15
+ * range/change types, so a host can pass its `onChange` straight through without
16
+ * adapting — structural typing makes them interchangeable.
17
+ */
18
+ /** A half-open range into the document, in UTF-8 byte offsets. */
19
+ interface SourceRange {
20
+ start: number;
21
+ end: number;
22
+ }
23
+ /** One text edit: the replaced byte range and the text inserted in its place. */
24
+ interface DocumentTextChange {
25
+ range: SourceRange;
26
+ text: string;
27
+ }
28
+
29
+ /**
30
+ * Rich copy/cut (#135): every copy writes BOTH clipboard flavors —
31
+ *
32
+ * text/plain — the markdown SOURCE of the selection (lossless: editors,
33
+ * terminals, and Compose→Compose round trips);
34
+ * text/html — the selection rendered by the host (Google Docs, Slack,
35
+ * Word, and Gmail paste it formatted).
36
+ *
37
+ * The HTML carries {@link COMPOSE_CLIPBOARD_ATTR} so our own paste handler
38
+ * prefers the markdown flavor instead of re-converting our rendering.
39
+ *
40
+ * Stays out of the way of: the table surface's TSV cell-selection copy (a
41
+ * capture-phase document listener that preventDefaults first), and CM's
42
+ * native empty-selection behavior. Without a host renderer the handler still
43
+ * runs — plain-only, but through one code path.
44
+ */
45
+
46
+ /** Host-supplied markdown → HTML renderer for clipboard writes. Null (the
47
+ * default) copies plain markdown only. Wired like the other host seams —
48
+ * see hostFacets.ts. */
49
+ type RenderClipboardHtml = (markdown: string) => string | null;
50
+
51
+ /**
52
+ * The rendering contract: one polymorphic shape for "how does a Lezer node
53
+ * render", replacing three parallel dispatch surfaces (a data-entry union, a
54
+ * widget-name union + builders map, and a contextual-override switch).
55
+ *
56
+ * NodeRule = (ctx) => Paint — one function per node NAME
57
+ * Paint = which CM6 mechanism — a CLOSED union
58
+ *
59
+ * The openness is split on the right axis: CONSTRUCTS grow (every new node
60
+ * adds a rule — one entry, one place, usually one line via the combinators
61
+ * below), while the ways to PAINT don't (line class / span mark / hide /
62
+ * widget / nothing — CodeMirror's own vocabulary). The single switch over
63
+ * `Paint` lives in the painter (plugin.ts) and never changes when a
64
+ * construct is added.
65
+ *
66
+ * Context (the bare-URL lesson: a node name can mean different things in
67
+ * different parents) is not a bolt-on — every rule IS a function of context;
68
+ * simple rules just ignore it.
69
+ *
70
+ * Extensions contribute rules through {@link nodeRulesFacet}: a plugin that
71
+ * introduces node names ships its rules alongside its grammar, touching no
72
+ * core file.
73
+ */
74
+
75
+ interface NodeLike {
76
+ readonly name: string;
77
+ readonly from: number;
78
+ readonly to: number;
79
+ readonly parent: NodeLike | null;
80
+ readonly firstChild: NodeLike | null;
81
+ readonly nextSibling: NodeLike | null;
82
+ readonly prevSibling: NodeLike | null;
83
+ getChild(type: string): NodeLike | null;
84
+ }
85
+ /** Everything a rule may consult. */
86
+ interface NodeContext {
87
+ readonly name: string;
88
+ readonly from: number;
89
+ readonly to: number;
90
+ /** The parent node's name — the common contextual discriminator. */
91
+ readonly parentName: string | undefined;
92
+ /** Full structural node, for rules that need siblings/children. */
93
+ readonly node: NodeLike;
94
+ /** Rules are pure over state — no view dependency, so non-editor
95
+ * renderers (table cells) can invoke the same rules. */
96
+ readonly state: EditorState;
97
+ }
98
+ /** One painting instruction, in CodeMirror's own vocabulary. */
99
+ type Paint =
100
+ /** Stamp a line class — on the node's first line, or every spanned line. */
101
+ {
102
+ readonly paint: "lineClass";
103
+ readonly className: string;
104
+ readonly span: "first" | "all";
105
+ }
106
+ /** Style the node's span. */
107
+ | {
108
+ readonly paint: "mark";
109
+ readonly className: string;
110
+ }
111
+ /**
112
+ * Hide a range (default: the node, plus its one separator space when
113
+ * line-leading) and make it atomic to caret motion. `atomicTo` widens the
114
+ * atom past the hidden range (marker + space move as one unit).
115
+ */
116
+ | {
117
+ readonly paint: "hide";
118
+ readonly range?: {
119
+ readonly from: number;
120
+ readonly to: number;
121
+ };
122
+ readonly expandSpace?: boolean;
123
+ readonly atomicTo?: number;
124
+ }
125
+ /** Replace the (hidden) span with a widget decoration. */
126
+ | {
127
+ readonly paint: "widget";
128
+ readonly deco: Decoration;
129
+ readonly atomicTo?: number;
130
+ }
131
+ /** Leave the node alone — visible raw source. */
132
+ | {
133
+ readonly paint: "none";
134
+ };
135
+ interface RuleMeta {
136
+ readonly intent: "render-raw" | "structural";
137
+ readonly why: string;
138
+ }
139
+ /** How one node name renders. `meta` tags the deliberate do-nothing rules so
140
+ * the coverage test can insist their reason is documented. */
141
+ type NodeRule = ((ctx: NodeContext) => Paint) & {
142
+ readonly meta?: RuleMeta;
143
+ };
144
+ type NodeRules = Readonly<Record<string, NodeRule>>;
145
+ /** Style the node's span with a class. */
146
+ declare function mark(className: string): NodeRule;
147
+ /** Stamp a line class on every line the node spans. */
148
+ declare function line(className: string): NodeRule;
149
+ /** Stamp a line class on the node's first line only (headings). */
150
+ declare function headingLine(className: string): NodeRule;
151
+ /** Hide the node (marker chrome), atomically. */
152
+ declare function hideAlways(): NodeRule;
153
+ /** Deliberately unstyled-for-now, visible raw — `why` documents the intent. */
154
+ declare function raw(why: string): NodeRule;
155
+ /** A parser grouping construct, never directly visible — `why` says which. */
156
+ declare function structural(why: string): NodeRule;
157
+ /**
158
+ * Rules contributed by extensions, merged over the base table (an extension
159
+ * may also deliberately override a base rule — last provider wins). The
160
+ * painter reads THIS, never the base table directly.
161
+ */
162
+ declare const nodeRulesFacet: Facet<Readonly<Record<string, NodeRule>>, Readonly<Record<string, NodeRule>>>;
163
+
164
+ /**
165
+ * A syntax tree that reaches `pos`.
166
+ *
167
+ * `syntaxTree` returns only what the **viewport** has driven the parser
168
+ * through, so `resolveInner` past that point reports the position as bare
169
+ * `Document`. Every command that asks "am I inside a list / a fence / a table?"
170
+ * then gets "no" and takes the plain-text path: a wrong answer, not a slow one.
171
+ *
172
+ * Measured in WebKit, the engine we ship on, with real layout — two regimes.
173
+ *
174
+ * On the frame a document opens the parser has covered a few thousand
175
+ * characters: 3,009 of 7,902, viewport 0–331, for an editor 11,404px tall. The
176
+ * idle parse then catches up within about half a second, so near the viewport
177
+ * this is a race — and it heals before a hand-test can see it.
178
+ *
179
+ * Further out it never heals. The idle parse stops 100,000 characters past the
180
+ * viewport: the tree plateaus at 100,739 for a 167k-character document and a
181
+ * 341k one alike, and stays there. `ensureSyntaxTree` resumes from that plateau
182
+ * and finishes the 341k document in 43ms — inside the budget above.
183
+ *
184
+ * Only for questions about a *position*. Decoration plugins iterate
185
+ * `view.visibleRanges` and should keep using `syntaxTree` directly: there,
186
+ * being limited to the viewport is the point.
187
+ */
188
+ type Tree = ReturnType<typeof syntaxTree>;
189
+ declare function treeAt(state: EditorState, pos: number): Tree;
190
+
191
+ /**
192
+ * Markdown decoration plugin — the PAINTER. Walks the Lezer tree against the
193
+ * visible viewport, asks each node's {@link NodeRule} how to render, and
194
+ * applies the returned {@link Paint}.
195
+ *
196
+ * The painter contains *no* construct knowledge: rules live in the base
197
+ * table (`NODE_RULES`) merged with any extension-contributed rules
198
+ * (`nodeRulesFacet`). The one switch here is over `Paint` — CodeMirror's
199
+ * closed set of mechanisms — so adding a construct, widget, contextual
200
+ * override, or whole extension never edits this file.
201
+ *
202
+ * Perf shape (unchanged from the spike):
203
+ * * Walks `view.visibleRanges` against `syntaxTree(view.state)` —
204
+ * the work is proportional to the viewport, not the document.
205
+ * * Rebuilt on doc change or viewport change.
206
+ *
207
+ * Decoration ordering (CM6 requires `from`-then-`startSide` ascending):
208
+ * * Line decorations collected in one bucket, mark / replace in
209
+ * another. The final `Decoration.set` lets CM6 sort defensively,
210
+ * costing ~µs on the typical viewport — cheaper than risking the
211
+ * "decorations out of order" runtime error.
212
+ */
213
+
214
+ declare const markdownDecorationsPlugin: ViewPlugin<{
215
+ decorations: DecorationSet;
216
+ atomic: DecorationSet;
217
+ update(update: ViewUpdate): void;
218
+ }, undefined>;
219
+
220
+ /**
221
+ * Editor theme.
222
+ *
223
+ * Lives in `EditorView.baseTheme` rather than in `global.scss` for
224
+ * three load-bearing reasons:
225
+ *
226
+ * 1. **Line-metric integrity.** CM6 measures line heights from the
227
+ * DOM through a `requestMeasure` cycle to compute click → byte
228
+ * and scroll → viewport mappings. Styles injected via
229
+ * `EditorView.theme` participate in that cycle at the right
230
+ * moment; styles from an external sheet can land *after* the
231
+ * first measurement, so clicks land on the wrong line until the
232
+ * next measure fires. (This was the click-drift the spike
233
+ * shipped — fixed by moving here.)
234
+ * 2. **Specificity that beats the base theme without `!important`.**
235
+ * CM6 ships its monospace-and-purple base theme via the same
236
+ * mechanism; layering `EditorView.theme` on top is the only
237
+ * collision-free pattern.
238
+ * 3. **No margin/padding on heading lines.** Margins between
239
+ * `.cm-line` siblings shift visual position without changing
240
+ * `offsetTop`, so the metric cache and the eye disagree —
241
+ * that's the canonical click-on-wrong-line bug. We change
242
+ * `font:` (which doesn't touch line-height), nothing else.
243
+ *
244
+ * Zettlr's `markdown-editor/theme/editor.ts` is the canonical
245
+ * reference for this pattern in the open-source ecosystem.
246
+ */
247
+ /**
248
+ * `EditorView.theme` (not `baseTheme`) — `baseTheme` is the
249
+ * low-priority slot meant for theme packages and gets out-specifity'd
250
+ * by any app-level CSS (Carbon's globals in our case). `theme` is the
251
+ * app-priority slot; our intent is to override CM6's defaults, so
252
+ * this is the right tier.
253
+ */
254
+ declare const editorBaseTheme: _codemirror_state.Extension;
255
+
256
+ /**
257
+ * Display-time resolution of image `src` values for the editor — the
258
+ * environment-agnostic pieces.
259
+ *
260
+ * Markdown stores image references **workspace-relative and portable** (e.g.
261
+ * `images/pasted-….png`), which is what lands on disk and survives moving the
262
+ * folder between machines. A WebView, though, can't load a bare relative path
263
+ * against its app origin, so a host that streams local files needs to map the
264
+ * reference onto its own asset protocol.
265
+ *
266
+ * This module provides the host-independent parts: the POSIX path helpers
267
+ * (`computeFileDir` / `joinPath` / …), the `hasUriScheme` guard, and
268
+ * `defaultResolveImageSrc` (render as-is — the browser/SSR default). A desktop
269
+ * host composes these with its file API (e.g. Tauri `convertFileSrc`) to build
270
+ * its own resolver and injects it via the editor's `resolveImageSrc` prop. The
271
+ * stored markdown reference is never rewritten — only the rendered `<img src>`.
272
+ *
273
+ * Paths are treated as POSIX (`/`), matching the macOS/Linux workspace folders
274
+ * this targets. Windows-style drive paths are passed through unresolved.
275
+ */
276
+ interface ImageResolveContext {
277
+ /** Absolute OS directory of the markdown file being edited, or null. */
278
+ fileDir: string | null;
279
+ }
280
+ /**
281
+ * True when `src` already carries a URI scheme, a protocol-relative `//`, or a
282
+ * bare fragment — i.e. it's directly loadable and needs no path resolution. A
283
+ * host's display-src resolver uses this to decide whether to map a relative
284
+ * workspace path onto its own asset protocol.
285
+ */
286
+ declare function hasUriScheme(src: string): boolean;
287
+ /**
288
+ * Environment-agnostic default for the editor's `resolveImageSrcFacet`: render
289
+ * the reference as-is. Data URLs and absolute/schemed URLs load directly; a
290
+ * relative ref resolves against the page origin (or shows broken if no backing
291
+ * file). A desktop host overrides the facet with its own resolver, which maps
292
+ * relative paths onto a local asset protocol.
293
+ */
294
+ declare function defaultResolveImageSrc(rawSrc: string, _ctx: ImageResolveContext): string;
295
+ /**
296
+ * The directory a relative image reference resolves against: the folder
297
+ * containing the active markdown file. Falls back to the workspace root when
298
+ * the file path is unknown, and to null when there's no workspace.
299
+ */
300
+ declare function computeFileDir(workspaceRoot: string | null | undefined, filePath: string | null | undefined): string | null;
301
+ declare function isAbsolutePath(p: string): boolean;
302
+ /** POSIX `dirname`: the parent of a path, with trailing slashes ignored. */
303
+ declare function dirnamePath(p: string): string;
304
+ /**
305
+ * Join `rel` onto `dir`, normalizing `.` and `..` segments. Absolute-ness is
306
+ * inherited from `dir`; `..` never escapes above an absolute root.
307
+ */
308
+ declare function joinPath(dir: string, rel: string): string;
309
+
310
+ /**
311
+ * Host-environment injection seams.
312
+ *
313
+ * The editor surface is environment-agnostic: it knows how to render and edit
314
+ * markdown, but NOT how to read/write files, resolve asset URLs, or open links —
315
+ * those depend on where it's embedded (a Tauri desktop shell, a plain browser, a
316
+ * server-rendered preview). Each capability is a CM6 facet with a sensible
317
+ * browser default; the React host overrides it by setting the facet from a prop.
318
+ *
319
+ * Keeping these as facets (not React context) lets the non-React CM6 plugins and
320
+ * widgets — image paste handlers, the inline `<img>` widget, the click model —
321
+ * read them straight off `view.state`.
322
+ */
323
+
324
+ type ResolveImageSrc = (rawSrc: string, ctx: ImageResolveContext) => string;
325
+ type SaveImageBytes = (relPath: string, bytes: Uint8Array) => Promise<void>;
326
+ type OpenExternalUrl = (url: string) => void;
327
+ /** Viewport point a comment composer should anchor to (the right-click point). */
328
+ type CommentAnchor = {
329
+ x: number;
330
+ y: number;
331
+ };
332
+ type CommentOnExcerpt = (excerpt: {
333
+ text: string;
334
+ range: SourceRange;
335
+ }, anchor: CommentAnchor) => void;
336
+
337
+ type CodeMirrorEditorMode = "wysiwyg" | "source";
338
+ /** A non-empty editor selection, in document byte offsets. */
339
+ interface EditorSelectionSnapshot {
340
+ range: SourceRange;
341
+ text: string;
342
+ }
343
+ interface CodeMirrorMarkdownEditorProps {
344
+ mode?: CodeMirrorEditorMode;
345
+ onChange: (value: string, changes: DocumentTextChange[]) => void;
346
+ value: string;
347
+ workspaceRoot?: string;
348
+ filePath?: string;
349
+ linkTargets?: ReadonlySet<string>;
350
+ onNavigateToLink?: (path: string) => void;
351
+ /**
352
+ * Host-rendered toolbar. The editor owns the live `EditorView` and hands it to
353
+ * the slot; the host builds whatever toolbar UI it wants (formatting buttons,
354
+ * file actions, …) around it. Omit for a chromeless editor. Return a STABLE
355
+ * element shape so the host's own memoisation can hold across keystrokes.
356
+ */
357
+ toolbar?: (ctx: {
358
+ view: EditorView;
359
+ }) => ReactNode;
360
+ /**
361
+ * Host-rendered actions for the current text selection (e.g. a comment / ask
362
+ * bubble). Called with the live selection (or `null` when collapsed) and a
363
+ * `dismiss` that collapses the selection back to a caret. Omit for none.
364
+ */
365
+ selectionActions?: (ctx: {
366
+ selection: EditorSelectionSnapshot | null;
367
+ dismiss: () => void;
368
+ }) => ReactNode;
369
+ /**
370
+ * Map a markdown image `src` to a loadable URL. Default: render the reference
371
+ * as-is. A desktop host maps workspace-relative paths onto its asset protocol.
372
+ */
373
+ resolveImageSrc?: ResolveImageSrc;
374
+ /**
375
+ * Persist a pasted/dropped image's bytes at a workspace-relative path.
376
+ * Default: omitted ⇒ the image is inlined as a `data:` URL.
377
+ */
378
+ saveImageBytes?: SaveImageBytes;
379
+ /**
380
+ * Open a clicked external link. Default: a new browser tab. A desktop host
381
+ * overrides this to leave the app's webview.
382
+ */
383
+ onOpenExternalUrl?: OpenExternalUrl;
384
+ /**
385
+ * Comment on a selected table row/column — wired to the table context menu's
386
+ * "Comment on this row / column". The host opens its comment composer at the
387
+ * given anchor, seeded with the excerpt. Default: omitted ⇒ no such menu items.
388
+ */
389
+ onCommentOnExcerpt?: CommentOnExcerpt;
390
+ /**
391
+ * Render a markdown selection to HTML for the clipboard, so a copy pastes
392
+ * formatted into Google Docs / Slack / Word (the markdown source always
393
+ * rides along as text/plain). Default: omitted ⇒ plain-only copies.
394
+ */
395
+ renderClipboardHtml?: RenderClipboardHtml;
396
+ /**
397
+ * Called once, right after a tab-switch content swap commits and paints.
398
+ * Used by the host for latency instrumentation; no-op by default.
399
+ */
400
+ onAfterContentSwap?: () => void;
401
+ /**
402
+ * Receives a synchronous `flush()` that pulls the editor's live (debounce-
403
+ * lagged) content into the last `onChange` immediately — the host calls it
404
+ * before persisting. `null` is passed on unmount. Lets a host avoid writing
405
+ * stale buffers on Cmd+S / tab close.
406
+ */
407
+ onFlushReady?: (flush: (() => void) | null) => void;
408
+ }
409
+ declare function CodeMirrorMarkdownEditorInner({ mode, onChange, value, workspaceRoot, filePath, linkTargets, onNavigateToLink, toolbar, selectionActions, resolveImageSrc, saveImageBytes, onOpenExternalUrl, onCommentOnExcerpt, renderClipboardHtml, onAfterContentSwap, onFlushReady, }: CodeMirrorMarkdownEditorProps): react_jsx_runtime.JSX.Element;
410
+ /**
411
+ * Memoised export — same reason as the Tiptap editor. AppShell
412
+ * re-renders on every chat-thread token; without memoisation each
413
+ * token would blow through the editor's render path.
414
+ */
415
+ declare const CodeMirrorMarkdownEditor: react.MemoExoticComponent<typeof CodeMirrorMarkdownEditorInner>;
416
+
417
+ declare const formatCommands: {
418
+ toggleBold: Command;
419
+ toggleItalic: Command;
420
+ toggleInlineCode: Command;
421
+ };
422
+
423
+ declare const blockCommands: {
424
+ toggleHeading1: Command;
425
+ toggleHeading2: Command;
426
+ toggleHeading3: Command;
427
+ toggleBulletList: Command;
428
+ toggleOrderedList: Command;
429
+ toggleTaskList: Command;
430
+ toggleBlockquote: Command;
431
+ toggleCodeBlock: Command;
432
+ insertTable: Command;
433
+ };
434
+
435
+ /**
436
+ * Editor-update fan-out that survives `view.setState`.
437
+ *
438
+ * Reactive chrome (toolbar pressed-states, the selection-actions bubble)
439
+ * needs a callback on every selection/doc change. Injecting an
440
+ * `EditorView.updateListener` with `StateEffect.appendConfig` only patches
441
+ * the CURRENT state's configuration — the next `view.setState` (tab switch,
442
+ * file reload) builds a fresh state from the base extensions and the
443
+ * listener silently dies, freezing the chrome at the previous document's
444
+ * context.
445
+ *
446
+ * The bus lives IN the base extensions, so every created state carries it,
447
+ * while subscriptions key on the `EditorView` — the object that is stable
448
+ * across state swaps.
449
+ */
450
+
451
+ /**
452
+ * Subscribe to every update of `view`, across all its future states.
453
+ * Returns the unsubscribe function — call it in the effect cleanup.
454
+ */
455
+ declare function onEditorUpdate(view: EditorView, fn: (update: ViewUpdate) => void): () => void;
456
+
457
+ interface ToolbarContribution {
458
+ readonly id: string;
459
+ readonly group: "heading" | "format" | "block" | "insert" | string;
460
+ readonly label: string;
461
+ readonly icon: ReactNode;
462
+ readonly shortcut?: string;
463
+ readonly isActive?: (caretContext: CaretContextSnapshot) => boolean;
464
+ readonly run: (view: _codemirror_view.EditorView) => void;
465
+ }
466
+ interface CaretContextSnapshot {
467
+ readonly bold: boolean;
468
+ readonly italic: boolean;
469
+ readonly code: boolean;
470
+ readonly link: boolean;
471
+ readonly heading: 1 | 2 | 3 | 4 | 5 | 6 | 0;
472
+ readonly bulletList: boolean;
473
+ readonly orderedList: boolean;
474
+ readonly blockquote: boolean;
475
+ }
476
+ interface MarkdownExtension {
477
+ readonly name: string;
478
+ readonly version: string;
479
+ readonly description?: string;
480
+ /** Node rules for constructs this extension's grammar introduces (or
481
+ * deliberately overrides) — merged into the decoration painter via
482
+ * `nodeRulesFacet`, so an extension never edits the base table. */
483
+ readonly rules?: NodeRules;
484
+ readonly extensions?: Extension[];
485
+ readonly keymap?: KeyBinding[];
486
+ readonly toolbar?: ToolbarContribution[];
487
+ }
488
+
489
+ interface ComposedExtension {
490
+ extensions: Extension[];
491
+ toolbar: ToolbarContribution[];
492
+ }
493
+ declare function composeExtensions(modules: readonly MarkdownExtension[]): ComposedExtension;
494
+
495
+ declare const highlightExtension: MarkdownExtension;
496
+
497
+ declare const footnoteExtension: MarkdownExtension;
498
+
499
+ declare const mathExtension: MarkdownExtension;
500
+
501
+ declare const mermaidExtension: MarkdownExtension;
502
+
503
+ /**
504
+ * A FACTORY, not a const: each composition gets its own editing surface (the
505
+ * one-active-edit state), so two mounted editors can never share a cell edit.
506
+ */
507
+ declare function tableExtension(): MarkdownExtension;
508
+
509
+ declare const wikilinkExtension: MarkdownExtension;
510
+
511
+ /**
512
+ * Mermaid rendering, decoupled from CodeMirror — shared by the editor widget,
513
+ * the document export (which ships the SVG to the backend), and the clipboard
514
+ * (which needs a PNG, since Google Docs/Word drop SVG on paste).
515
+ *
516
+ * The mermaid library is heavy (~2 MB minified), so it loads lazily on the
517
+ * first diagram — bundled with the app, never fetched at runtime, so diagrams
518
+ * work fully offline. Render results are cached by source text; PNGs are
519
+ * rasterised once per diagram in the background (see {@link warmMermaidPng})
520
+ * so the SYNCHRONOUS copy event can embed them from cache.
521
+ */
522
+ type MermaidRenderResult = {
523
+ ok: true;
524
+ svg: string;
525
+ } | {
526
+ ok: false;
527
+ message: string;
528
+ };
529
+ /** Whether a fence info string denotes a rendered mermaid diagram — the tag
530
+ * alone, any casing, no trailing meta ("mermaid title=x" is source, not a
531
+ * diagram). The one definition shared by the editor plugin, the export's SVG
532
+ * collector, and the clipboard, so every surface classifies identically. */
533
+ declare function isMermaidFenceInfo(info: string): boolean;
534
+ /** Render a mermaid diagram source to SVG, memoised by source. Never rejects —
535
+ * a parse error resolves to `{ ok: false, message }`. */
536
+ declare function renderMermaidToSvg(source: string): Promise<MermaidRenderResult>;
537
+ /** The cached PNG data URI for a diagram source, if it has been rasterised. */
538
+ declare function getCachedMermaidPng(source: string): string | null;
539
+ /** Render + rasterise a diagram in the background so a LATER (synchronous)
540
+ * clipboard copy can embed it from cache. Fire-and-forget: failures leave the
541
+ * cache empty and the copy degrades to the source block. */
542
+ declare function warmMermaidPng(source: string): Promise<void>;
543
+
544
+ /**
545
+ * Synchronous fence highlighting for the clipboard (#149).
546
+ *
547
+ * A copy event writes `text/html` synchronously, so highlighting must not
548
+ * await anything. The trick: the grammars are the SAME `@codemirror/language-data`
549
+ * singletons the editor's nested fence parse loads — any fence visible in the
550
+ * editor has its grammar warm, so highlighting the copied code is a sync parse.
551
+ * A cold grammar (copy from a doc whose fence never rendered) kicks the load
552
+ * and returns null: THIS copy ships plain, the next one is highlighted.
553
+ *
554
+ * Output is inline-styled spans (the palette from
555
+ * [codePalette](./codePalette.ts), same colors as the editor) — pasted HTML
556
+ * carries no stylesheet, so classes would be dead weight in Docs/Word.
557
+ */
558
+ interface HighlightedSpan {
559
+ text: string;
560
+ /** Inline CSS for the span; absent for unstyled text (incl. line breaks). */
561
+ style?: string;
562
+ }
563
+ /** Highlight `code` as `lang` into inline-styled spans, or null when no
564
+ * grammar matches or the grammar isn't loaded yet (the load is kicked so a
565
+ * later copy succeeds). Never throws — a parser hiccup falls back to null. */
566
+ declare function highlightFenceSpans(lang: string, code: string): HighlightedSpan[] | null;
567
+
568
+ /**
569
+ * Image-insertion pipeline. Used by both paste-from-clipboard and
570
+ * drag-and-drop. Both produce a `Blob`; this module turns the blob
571
+ * into a markdown-reference-able path:
572
+ *
573
+ * * When a `saveBytes` writer is injected (a desktop host): writes
574
+ * the bytes to `images/<timestamp>-<hash>.<ext>` and returns the
575
+ * relative path. The markdown reference is portable across
576
+ * machines because it's a real on-disk file.
577
+ * * Otherwise (plain browser, or the write fails): falls back to a
578
+ * data URL embedded directly in the markdown. The markdown file
579
+ * bloats but the image stays visible through page reload without
580
+ * needing a binary file API.
581
+ *
582
+ * The writer is injected rather than imported so this module stays
583
+ * environment-agnostic — the editor reads it off a CM6 facet
584
+ * ({@link saveImageBytesFacet}) the host wires up.
585
+ *
586
+ * Returned `markdownReference` is what the caller drops into
587
+ * `![alt](…)`. `warning` is set in the data-URL fallback case so
588
+ * the caller can surface a note to the user.
589
+ */
590
+ interface ImageInsertOptions {
591
+ blob: Blob;
592
+ /**
593
+ * Persist the bytes at a workspace-relative path. `null`/omitted ⇒
594
+ * inline as a data URL (browser fallback).
595
+ */
596
+ saveBytes?: ((relPath: string, bytes: Uint8Array) => Promise<void>) | null;
597
+ /**
598
+ * Alt text the user typed (currently always synthesized as
599
+ * "pasted-image" or "dropped-image"; in phase 3d the inline
600
+ * rename UI lets the user edit it post-insert).
601
+ */
602
+ alt?: string;
603
+ }
604
+ interface ImageInsertResult {
605
+ markdownReference: string;
606
+ alt: string;
607
+ warning?: string;
608
+ }
609
+ /**
610
+ * Save the blob and return the markdown reference. Errors are
611
+ * structured (the caller decides whether to retry with a fallback
612
+ * or surface a notification).
613
+ */
614
+ declare function insertImageBlob(opts: ImageInsertOptions): Promise<ImageInsertResult>;
615
+ /**
616
+ * Build a markdown image insertion string from the result. Keeps
617
+ * the call site (handlePaste / handleDrop) tidy.
618
+ */
619
+ declare function buildImageMarkdown(result: ImageInsertResult): string;
620
+ /**
621
+ * Scan a DataTransferItemList for image blobs. Returns every
622
+ * image found, in order. Empty array when nothing matches —
623
+ * caller treats that as "fall through to text paste".
624
+ */
625
+ declare function extractImageBlobs(items: DataTransferItemList | null | undefined): Blob[];
626
+ /**
627
+ * Scan a FileList (drag-and-drop's `dataTransfer.files`) for
628
+ * image files. Drag-drop and paste use different shapes; this
629
+ * keeps the two paths symmetrical at the call site.
630
+ */
631
+ declare function extractImageFiles(files: FileList | null | undefined): File[];
632
+
633
+ declare const imageInsertHandlers: _codemirror_state.Extension;
634
+ /**
635
+ * Open the browser's native file picker, then run the picked files
636
+ * through the same insert pipeline. Called by the toolbar's Image
637
+ * button.
638
+ */
639
+ declare function pickImageFileForCaret(view: EditorView): void;
640
+
641
+ interface ShowMenuArgs {
642
+ x: number;
643
+ y: number;
644
+ view: EditorView;
645
+ alt: string;
646
+ rawSrc: string;
647
+ sourceFrom: number;
648
+ sourceTo: number;
649
+ }
650
+ declare function showImageActionMenu(args: ShowMenuArgs): void;
651
+
652
+ /**
653
+ * The "edit this image's alt text" custom-event contract — a plain constant and
654
+ * type with NO runtime CodeMirror dependency (the `EditorView` reference is
655
+ * `import type`, erased at build). The lazy-loaded editor dispatches this event;
656
+ * the host shell listens for it. Keeping the contract here means a host can
657
+ * import the event name without pulling the whole editor into its initial
658
+ * bundle (it previously came from `imageActionMenu`, which imports CodeMirror).
659
+ */
660
+ declare const IMAGE_EDIT_ALT_EVENT = "ai-editor:edit-image-alt";
661
+ interface ImageEditAltEventDetail {
662
+ view: EditorView;
663
+ sourceFrom: number;
664
+ sourceTo: number;
665
+ currentAlt: string;
666
+ rawSrc: string;
667
+ }
668
+
669
+ /**
670
+ * YAML frontmatter parsing + serialization.
671
+ *
672
+ * Convention: `--- yaml ---` block at the very top of a markdown
673
+ * file (before any other content). Format:
674
+ *
675
+ * ---
676
+ * status: draft
677
+ * tags:
678
+ * - application
679
+ * - fellowship
680
+ * ---
681
+ *
682
+ * # Real content starts here
683
+ *
684
+ * This module separates the YAML chunk from the prose chunk so
685
+ * the WYSIWYG editor can render only the prose (no raw `key:
686
+ * value` lines bleeding into the user's writing surface) while a
687
+ * separate properties UI shows / edits the frontmatter.
688
+ *
689
+ * Round-trip rules:
690
+ * - If a file has no frontmatter on disk, we MUST NOT add one
691
+ * on save just because the properties panel exists.
692
+ * - If a file has frontmatter, we serialize it using the same
693
+ * keys/order the user wrote (best effort — yaml lib preserves
694
+ * scalar formatting but not always blank lines / comments).
695
+ * - The fence marker is always exactly `---` on its own line.
696
+ * Some Obsidian users prefer `+++` (TOML) — out of scope for
697
+ * v1; we round-trip those as body content unchanged.
698
+ */
699
+ /**
700
+ * The frontmatter value type. Each entry is whatever YAML can
701
+ * decode to — strings, numbers, booleans, arrays, nested objects.
702
+ * The properties UI handles primitives + flat arrays cleanly;
703
+ * nested objects fall back to a raw YAML editor.
704
+ */
705
+ type FrontmatterValue = string | number | boolean | null | FrontmatterValue[] | {
706
+ [key: string]: FrontmatterValue;
707
+ };
708
+ type Frontmatter = Record<string, FrontmatterValue>;
709
+ interface MarkdownDocument {
710
+ /** Parsed YAML key/values. `null` means "no frontmatter block present". */
711
+ frontmatter: Frontmatter | null;
712
+ /** The prose portion — everything after the closing `---`. */
713
+ body: string;
714
+ }
715
+ /**
716
+ * Split a markdown string into its frontmatter + body. Tolerant
717
+ * of:
718
+ * - No frontmatter at all → `frontmatter: null, body: input`
719
+ * - Malformed YAML → `frontmatter: null, body: input` (we treat
720
+ * the whole thing as prose rather than show parse errors)
721
+ * - Empty frontmatter block (`---\n---`) → `frontmatter: {}`
722
+ */
723
+ declare function parseFrontmatter(markdown: string): MarkdownDocument;
724
+ /**
725
+ * Recombine frontmatter + body into a markdown string. Inverse
726
+ * of `parseFrontmatter` for legal inputs.
727
+ *
728
+ * - `frontmatter: null` → just the body, no `---` fences added
729
+ * - `frontmatter: {}` → no fences either (an empty frontmatter
730
+ * block is semantically the same as "no frontmatter" and we
731
+ * prefer the cleaner shape on save)
732
+ * - Otherwise → `---\n<yaml>\n---\n<body>`
733
+ */
734
+ declare function serializeMarkdown(doc: MarkdownDocument): string;
735
+ /**
736
+ * Helper: update a single frontmatter field in a markdown string
737
+ * without touching the body. Used by the Properties UI when the
738
+ * user edits one value at a time.
739
+ *
740
+ * If the file had no frontmatter, this adds one with just the
741
+ * one field. If the value is `null` and the key exists, removes
742
+ * the key (and removes the whole frontmatter block if it becomes
743
+ * empty).
744
+ */
745
+ declare function setFrontmatterField(markdown: string, key: string, value: FrontmatterValue | null): string;
746
+
747
+ /**
748
+ * Wikilink (`[[target]]` / `[[target|label]]`) parsing + target resolution.
749
+ *
750
+ * The canonical rule lives in the `workspace-index` crate
751
+ * (`wikilink_target_and_label` / `resolve_document_target` / `path_stem_matches`
752
+ * / `slug_key`) — that's what builds the backlink graph. This is the
753
+ * **client-side mirror** used to navigate a *clicked* wikilink, so the editor
754
+ * and chat agree with the sidebar's backlinks. Keep the two in sync; if the
755
+ * crate's rule changes, change it here too.
756
+ *
757
+ * Difference from the crate: an unresolved target returns `null` (we only
758
+ * navigate to files that exist), whereas the crate keeps a would-be path for
759
+ * the graph edge.
760
+ */
761
+ /** Split a wikilink body into its target and display label. */
762
+ declare function parseWikilinkBody(body: string): {
763
+ target: string;
764
+ label: string;
765
+ };
766
+ /** Resolve a wikilink target to an existing workspace file path, or `null`. */
767
+ declare function resolveWikilinkTarget(rawTarget: string, options: {
768
+ fromPath?: string;
769
+ knownPaths: ReadonlySet<string>;
770
+ }): string | null;
771
+
772
+ /**
773
+ * Resolve a markdown link href to a navigation target.
774
+ *
775
+ * A link in a document (or an agent's chat reply) is either:
776
+ * - **external** — anything with a URI scheme (`https:`, `mailto:`, …) or a
777
+ * protocol-relative `//host`; opened in the browser, and
778
+ * - **internal** — a workspace-relative path that resolves to a file the
779
+ * workspace actually contains; opened in a tab.
780
+ *
781
+ * This is pure path resolution (normalize `.`/`..`, reject escaping the vault
782
+ * root, membership-check against the known files) — it is **not** the index's
783
+ * link parser. The index already resolves links into `graphEdges`; this just
784
+ * decides where a *clicked* href points so the UI can navigate. An href that
785
+ * looks internal but matches no known file returns `null` (a broken link, not
786
+ * something to navigate to).
787
+ */
788
+ type ResolvedWorkspaceLink = {
789
+ kind: "internal";
790
+ path: string;
791
+ } | {
792
+ kind: "external";
793
+ href: string;
794
+ };
795
+ interface ResolveWorkspaceLinkOptions {
796
+ /** Workspace-relative path of the document the link lives in. Relative hrefs
797
+ * resolve against this file's directory. Omitted ⇒ resolve from the vault
798
+ * root (used for agent chat replies, which emit root-relative paths). */
799
+ fromPath?: string;
800
+ /** Every workspace-relative file path, for membership checks. */
801
+ knownPaths: ReadonlySet<string>;
802
+ }
803
+ declare function resolveWorkspaceLink(href: string, options: ResolveWorkspaceLinkOptions): ResolvedWorkspaceLink | null;
804
+
805
+ export { type CaretContextSnapshot, type CodeMirrorEditorMode, CodeMirrorMarkdownEditor, type CodeMirrorMarkdownEditorProps, type ComposedExtension, type DocumentTextChange, type EditorSelectionSnapshot, type Frontmatter, type FrontmatterValue, type HighlightedSpan, IMAGE_EDIT_ALT_EVENT, type ImageEditAltEventDetail, type ImageInsertOptions, type ImageInsertResult, type ImageResolveContext, type MarkdownDocument, type MarkdownExtension, type MermaidRenderResult, type NodeContext, type NodeRule, type NodeRules, type OpenExternalUrl, type Paint, type ResolveImageSrc, type ResolveWorkspaceLinkOptions, type ResolvedWorkspaceLink, type SaveImageBytes, type SourceRange, type ToolbarContribution, blockCommands, buildImageMarkdown, composeExtensions, computeFileDir, defaultResolveImageSrc, dirnamePath, editorBaseTheme, extractImageBlobs, extractImageFiles, footnoteExtension, formatCommands, getCachedMermaidPng, hasUriScheme, headingLine, hideAlways, highlightExtension, highlightFenceSpans, imageInsertHandlers, insertImageBlob, isAbsolutePath, isMermaidFenceInfo, joinPath, line, mark, markdownDecorationsPlugin, mathExtension, mermaidExtension, nodeRulesFacet, onEditorUpdate, parseFrontmatter, parseWikilinkBody, pickImageFileForCaret, raw, renderMermaidToSvg, resolveWikilinkTarget, resolveWorkspaceLink, serializeMarkdown, setFrontmatterField, showImageActionMenu, structural, tableExtension, treeAt, warmMermaidPng, wikilinkExtension };