@latentic/live-markdown 0.2.0 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.1] - 2026-08-29
4
+
5
+ ### Fixed
6
+
7
+ - **The task tick is centred in its box, and its stroke is lighter.** The tick
8
+ is an `L` rotated 45°, and the L's ink sits below and right of its own rect
9
+ centre — the rotation turns that diagonal offset into a purely downward one of
10
+ 0.065em. The rect therefore has to ride that much above the box centre to look
11
+ centred: `top: 0.125em`, not the geometric `0.1875em`, which renders visibly
12
+ low. Stroke 0.125em → 0.1em, from 14.3% of the box to 11.4%.
13
+
14
+ ## [0.3.0] - 2026-08-29
15
+
16
+ ### Added
17
+
18
+ - **A host can contribute extension modules.** `extensions?: readonly
19
+ MarkdownExtension[]` on the editor, merged in one pass with the built-ins and
20
+ host-last, so a module can deliberately override a construct and the merger
21
+ still warns when a node rule is redefined. Applies in source mode as well as
22
+ wysiwyg — a keymap is not a rendering concern. Read when an editor state is
23
+ built, so building the array inline cannot remount the editor.
24
+
25
+ Toolbar contributions now reach the host through the `toolbar` slot's
26
+ context; the merger always collected them and the component dropped them.
27
+
28
+ - `composeExtensions` is renamed **`mergeExtensions`**, with the old name kept
29
+ as a deprecated alias. "compose" read as the name of an app rather than as the
30
+ verb, which is the wrong signal for a package meant for many hosts.
31
+
32
+ ### Fixed
33
+
34
+ - **A checked task box reads as checked in dark.** The tick used
35
+ `--cds-icon-on-color` — white in every theme — against a fill of
36
+ `--cds-icon-primary`, which inverts. In dark that was #ffffff on #f4f4f4, a
37
+ contrast ratio of 1.10. It now uses `--cds-background`, the inverse of the
38
+ fill by construction.
39
+
40
+ - **The task box is sized to the text.** 1rem beside 1rem text is as tall as the
41
+ whole em box; it now derives from its own `font-size: 0.875em`, with box, tick
42
+ and baseline offset all in `em` of that, and sits on the x-height instead of
43
+ hanging off the baseline.
44
+
3
45
  ## [0.2.0] - 2026-08-28
4
46
 
5
47
  ### Added
package/README.md CHANGED
@@ -186,7 +186,8 @@ The `toolbar` render prop receives the live `EditorView`, so host buttons can dr
186
186
  | `value` | `string` | — | The markdown content (controlled) |
187
187
  | `onChange` | `(value: string, changes: DocumentTextChange[]) => void` | — | Called after edits, debounced |
188
188
  | `mode` | `"wysiwyg" \| "source"` | `"wysiwyg"` | Rich rendering or raw markdown |
189
- | `toolbar` | `(ctx: { view: EditorView }) => ReactNode` | — | Host-rendered toolbar, given the live editor view |
189
+ | `extensions` | `readonly MarkdownExtension[]` | — | Host-contributed extension modules, merged after the built-ins |
190
+ | `toolbar` | `(ctx: { view, contributions }) => ReactNode` | — | Host-rendered toolbar, given the live editor view and any toolbar items the extensions contributed |
190
191
  | `selectionActions` | `(ctx: { selection, dismiss }) => ReactNode` | — | Host-rendered actions for the current selection (e.g. a comment bubble) |
191
192
  | `linkTargets` | `ReadonlySet<string>` | — | Known file paths for wikilink resolution |
192
193
  | `onNavigateToLink` | `(path: string) => void` | — | Called on Cmd/Ctrl-click of an internal link |
@@ -209,13 +210,32 @@ Built-in extensions:
209
210
  - `tableExtension` — GFM tables with cell navigation
210
211
  - `wikilinkExtension` — `[[wikilink]]` rendering and navigation
211
212
 
213
+ The built-ins load themselves. To add your own, hand the editor your modules:
214
+
215
+ ```tsx
216
+ <CodeMirrorMarkdownEditor value={md} onChange={setMd} extensions={[myExtension]} />
217
+ ```
218
+
219
+ They are merged **after** the built-ins, so a module can deliberately override a
220
+ construct — the merger warns when a node rule is redefined, so an accidental
221
+ shadow is not silent. Modules apply in source mode as well as wysiwyg: a keymap
222
+ or a plain CM6 extension is not a markdown-rendering concern, and node rules
223
+ simply go unread while the painter is off.
224
+
225
+ The prop is read when an editor state is built, not on every render, so building
226
+ the array inline costs nothing and cannot remount the editor. Changing it
227
+ mid-session takes effect at the next rebuild.
228
+
229
+ To merge modules yourself — composing a preset, or feeding a CodeMirror view you
230
+ own — `mergeExtensions` is the same function the editor uses:
231
+
212
232
  ```tsx
213
- import { composeExtensions, mathExtension, tableExtension } from "@latentic/live-markdown";
233
+ import { mergeExtensions, mathExtension, tableExtension } from "@latentic/live-markdown";
214
234
 
215
- const composed = composeExtensions([mathExtension, tableExtension]);
216
- // composed.extensions — CM6 Extension[] (each extension's node rules ride
217
- // along via a facet, so this is all the editor needs)
218
- // composed.toolbar — merged ToolbarContribution[]
235
+ const merged = mergeExtensions([mathExtension, tableExtension]);
236
+ // merged.extensions — CM6 Extension[] (each module's node rules ride along via
237
+ // a facet, so this is all a view needs)
238
+ // merged.toolbar — merged ToolbarContribution[]
219
239
  ```
220
240
 
221
241
  ### A custom extension
@@ -223,7 +243,7 @@ const composed = composeExtensions([mathExtension, tableExtension]);
223
243
  A rule is one function per Lezer node name, returning how that node paints. The `mark` combinator covers the common "style this span" case. Rules merge last-wins, so an extension can introduce a construct from its own grammar or deliberately restyle a built-in one:
224
244
 
225
245
  ```tsx
226
- import { composeExtensions, mark, type MarkdownExtension } from "@latentic/live-markdown";
246
+ import { mark, type MarkdownExtension } from "@latentic/live-markdown";
227
247
 
228
248
  const fancyEmphasis: MarkdownExtension = {
229
249
  name: "fancy-emphasis",
@@ -234,7 +254,7 @@ const fancyEmphasis: MarkdownExtension = {
234
254
  },
235
255
  };
236
256
 
237
- const composed = composeExtensions([fancyEmphasis]);
257
+ <CodeMirrorMarkdownEditor value={md} onChange={setMd} extensions={[fancyEmphasis]} />
238
258
  ```
239
259
 
240
260
  `Paint` is a closed set — line class, span mark, hide, widget, or nothing — while node names grow, so styling a construct is always one rule in one place.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import * as _codemirror_view from '@codemirror/view';
4
- import { Decoration, ViewPlugin, DecorationSet, ViewUpdate, EditorView, Command, KeyBinding } from '@codemirror/view';
4
+ import { Decoration, ViewPlugin, DecorationSet, ViewUpdate, KeyBinding, EditorView, Command } from '@codemirror/view';
5
5
  import * as _codemirror_state from '@codemirror/state';
6
6
  import { EditorState, Facet, Extension } from '@codemirror/state';
7
7
  import { syntaxTree } from '@codemirror/language';
@@ -384,6 +384,71 @@ type CommentOnExcerpt = (excerpt: {
384
384
  range: SourceRange;
385
385
  }, anchor: CommentAnchor) => void;
386
386
 
387
+ interface ToolbarContribution {
388
+ readonly id: string;
389
+ readonly group: "heading" | "format" | "block" | "insert" | string;
390
+ readonly label: string;
391
+ readonly icon: ReactNode;
392
+ readonly shortcut?: string;
393
+ readonly isActive?: (caretContext: CaretContextSnapshot) => boolean;
394
+ readonly run: (view: _codemirror_view.EditorView) => void;
395
+ }
396
+ interface CaretContextSnapshot {
397
+ readonly bold: boolean;
398
+ readonly italic: boolean;
399
+ readonly code: boolean;
400
+ readonly link: boolean;
401
+ readonly heading: 1 | 2 | 3 | 4 | 5 | 6 | 0;
402
+ readonly bulletList: boolean;
403
+ readonly orderedList: boolean;
404
+ readonly blockquote: boolean;
405
+ }
406
+ interface MarkdownExtension {
407
+ readonly name: string;
408
+ readonly version: string;
409
+ readonly description?: string;
410
+ /** Node rules for constructs this extension's grammar introduces (or
411
+ * deliberately overrides) — merged into the decoration painter via
412
+ * `nodeRulesFacet`, so an extension never edits the base table. */
413
+ readonly rules?: NodeRules;
414
+ readonly extensions?: Extension[];
415
+ readonly keymap?: KeyBinding[];
416
+ readonly toolbar?: ToolbarContribution[];
417
+ }
418
+
419
+ interface ComposedExtension {
420
+ extensions: Extension[];
421
+ toolbar: ToolbarContribution[];
422
+ }
423
+ /**
424
+ * Merge extension modules into one set of CodeMirror extensions plus the
425
+ * toolbar items they contribute. Later modules win: a host's rules are applied
426
+ * after the built-ins, so it can deliberately override a construct.
427
+ */
428
+ declare function mergeExtensions(modules: readonly MarkdownExtension[]): ComposedExtension;
429
+ /**
430
+ * @deprecated Renamed to {@link mergeExtensions}. "compose" reads as the app
431
+ * this package was extracted from rather than as the verb; the editor is meant
432
+ * for more hosts than that one. Kept as an alias so the rename is not breaking.
433
+ */
434
+ declare const composeExtensions: typeof mergeExtensions;
435
+
436
+ declare const highlightExtension: MarkdownExtension;
437
+
438
+ declare const footnoteExtension: MarkdownExtension;
439
+
440
+ declare const mathExtension: MarkdownExtension;
441
+
442
+ declare const mermaidExtension: MarkdownExtension;
443
+
444
+ /**
445
+ * A FACTORY, not a const: each composition gets its own editing surface (the
446
+ * one-active-edit state), so two mounted editors can never share a cell edit.
447
+ */
448
+ declare function tableExtension(): MarkdownExtension;
449
+
450
+ declare const wikilinkExtension: MarkdownExtension;
451
+
387
452
  type CodeMirrorEditorMode = "wysiwyg" | "source";
388
453
  /** A non-empty editor selection, in document byte offsets. */
389
454
  interface EditorSelectionSnapshot {
@@ -399,13 +464,27 @@ interface CodeMirrorMarkdownEditorProps {
399
464
  linkTargets?: ReadonlySet<string>;
400
465
  onNavigateToLink?: (path: string) => void;
401
466
  /**
402
- * Host-rendered toolbar. The editor owns the live `EditorView` and hands it to
403
- * the slot; the host builds whatever toolbar UI it wants (formatting buttons,
404
- * file actions, ) around it. Omit for a chromeless editor. Return a STABLE
405
- * element shape so the host's own memoisation can hold across keystrokes.
467
+ * Extension modules contributed by the host, merged AFTER the built-ins so a
468
+ * host can deliberately override a construct (the merger warns when a node
469
+ * rule is redefined).
470
+ *
471
+ * Read when an editor state is built — on mount, and on the swaps that
472
+ * rebuild one — not on every render, so passing a fresh array each time costs
473
+ * nothing and cannot remount the editor. Changing it mid-session therefore
474
+ * takes effect at the next rebuild.
475
+ *
476
+ * Applies in BOTH modes: a module's node rules simply go unread while the
477
+ * decoration painter is off, but its keymaps and plain CodeMirror extensions
478
+ * are not markdown-rendering concerns and should not be silently dropped in
479
+ * source mode.
406
480
  */
481
+ extensions?: readonly MarkdownExtension[];
482
+ /** Host-rendered toolbar. `contributions` carries the toolbar items the
483
+ * extension modules asked for; the host decides whether and how to render
484
+ * them alongside its own. */
407
485
  toolbar?: (ctx: {
408
486
  view: EditorView;
487
+ contributions: readonly ToolbarContribution[];
409
488
  }) => ReactNode;
410
489
  /**
411
490
  * Host-rendered actions for the current text selection (e.g. a comment / ask
@@ -456,7 +535,7 @@ interface CodeMirrorMarkdownEditorProps {
456
535
  */
457
536
  onFlushReady?: (flush: (() => void) | null) => void;
458
537
  }
459
- declare function CodeMirrorMarkdownEditorInner({ mode, onChange, value, workspaceRoot, filePath, linkTargets, onNavigateToLink, toolbar, selectionActions, resolveImageSrc, saveImageBytes, onOpenExternalUrl, onCommentOnExcerpt, renderClipboardHtml, onAfterContentSwap, onFlushReady, }: CodeMirrorMarkdownEditorProps): react.JSX.Element;
538
+ declare function CodeMirrorMarkdownEditorInner({ mode, onChange, value, workspaceRoot, filePath, linkTargets, onNavigateToLink, extensions: hostExtensions, toolbar, selectionActions, resolveImageSrc, saveImageBytes, onOpenExternalUrl, onCommentOnExcerpt, renderClipboardHtml, onAfterContentSwap, onFlushReady, }: CodeMirrorMarkdownEditorProps): react.JSX.Element;
460
539
  /**
461
540
  * Memoised export — same reason as the Tiptap editor. AppShell
462
541
  * re-renders on every chat-thread token; without memoisation each
@@ -504,60 +583,6 @@ declare const blockCommands: {
504
583
  */
505
584
  declare function onEditorUpdate(view: EditorView, fn: (update: ViewUpdate) => void): () => void;
506
585
 
507
- interface ToolbarContribution {
508
- readonly id: string;
509
- readonly group: "heading" | "format" | "block" | "insert" | string;
510
- readonly label: string;
511
- readonly icon: ReactNode;
512
- readonly shortcut?: string;
513
- readonly isActive?: (caretContext: CaretContextSnapshot) => boolean;
514
- readonly run: (view: _codemirror_view.EditorView) => void;
515
- }
516
- interface CaretContextSnapshot {
517
- readonly bold: boolean;
518
- readonly italic: boolean;
519
- readonly code: boolean;
520
- readonly link: boolean;
521
- readonly heading: 1 | 2 | 3 | 4 | 5 | 6 | 0;
522
- readonly bulletList: boolean;
523
- readonly orderedList: boolean;
524
- readonly blockquote: boolean;
525
- }
526
- interface MarkdownExtension {
527
- readonly name: string;
528
- readonly version: string;
529
- readonly description?: string;
530
- /** Node rules for constructs this extension's grammar introduces (or
531
- * deliberately overrides) — merged into the decoration painter via
532
- * `nodeRulesFacet`, so an extension never edits the base table. */
533
- readonly rules?: NodeRules;
534
- readonly extensions?: Extension[];
535
- readonly keymap?: KeyBinding[];
536
- readonly toolbar?: ToolbarContribution[];
537
- }
538
-
539
- interface ComposedExtension {
540
- extensions: Extension[];
541
- toolbar: ToolbarContribution[];
542
- }
543
- declare function composeExtensions(modules: readonly MarkdownExtension[]): ComposedExtension;
544
-
545
- declare const highlightExtension: MarkdownExtension;
546
-
547
- declare const footnoteExtension: MarkdownExtension;
548
-
549
- declare const mathExtension: MarkdownExtension;
550
-
551
- declare const mermaidExtension: MarkdownExtension;
552
-
553
- /**
554
- * A FACTORY, not a const: each composition gets its own editing surface (the
555
- * one-active-edit state), so two mounted editors can never share a cell edit.
556
- */
557
- declare function tableExtension(): MarkdownExtension;
558
-
559
- declare const wikilinkExtension: MarkdownExtension;
560
-
561
586
  /**
562
587
  * Mermaid rendering, decoupled from CodeMirror — shared by the editor widget,
563
588
  * the document export (which ships the SVG to the backend), and the clipboard
@@ -852,4 +877,4 @@ interface ResolveWorkspaceLinkOptions {
852
877
  }
853
878
  declare function resolveWorkspaceLink(href: string, options: ResolveWorkspaceLinkOptions): ResolvedWorkspaceLink | null;
854
879
 
855
- 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 InlineScanMatch, type InlineScanRule, 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, escapeAttr, escapeText, extractImageBlobs, extractImageFiles, footnoteExtension, formatCommands, getCachedMermaidPng, hasUriScheme, headingLine, hideAlways, highlightExtension, highlightFenceSpans, imageInsertHandlers, inlineScanRulesFacet, insertImageBlob, isAbsolutePath, isMermaidFenceInfo, joinPath, line, mark, markdownDecorationsPlugin, mathExtension, mermaidExtension, nodeRulesFacet, onEditorUpdate, parseFrontmatter, parseWikilinkBody, pickImageFileForCaret, raw, renderMermaidToSvg, resolveWikilinkTarget, resolveWorkspaceLink, scanInline, serializeMarkdown, setFrontmatterField, showImageActionMenu, structural, tableExtension, treeAt, warmMermaidPng, wikilinkExtension };
880
+ 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 InlineScanMatch, type InlineScanRule, 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, escapeAttr, escapeText, extractImageBlobs, extractImageFiles, footnoteExtension, formatCommands, getCachedMermaidPng, hasUriScheme, headingLine, hideAlways, highlightExtension, highlightFenceSpans, imageInsertHandlers, inlineScanRulesFacet, insertImageBlob, isAbsolutePath, isMermaidFenceInfo, joinPath, line, mark, markdownDecorationsPlugin, mathExtension, mergeExtensions, mermaidExtension, nodeRulesFacet, onEditorUpdate, parseFrontmatter, parseWikilinkBody, pickImageFileForCaret, raw, renderMermaidToSvg, resolveWikilinkTarget, resolveWorkspaceLink, scanInline, serializeMarkdown, setFrontmatterField, showImageActionMenu, structural, tableExtension, treeAt, warmMermaidPng, wikilinkExtension };
package/dist/index.js CHANGED
@@ -2615,23 +2615,30 @@ var editorBaseTheme = EditorView.theme({
2615
2615
  marginRight: "0.3em",
2616
2616
  fontWeight: "normal"
2617
2617
  },
2618
- // Task list checkbox — drawn as a Carbon checkbox, not the native control: a
2619
- // 1rem square that fills with the icon token and shows a white tick when
2620
- // checked. `appearance: none` is what replaces WebKit's small rounded default;
2621
- // the box then matches the design system rather than approximating it with an
2622
- // accent colour over the native shape.
2618
+ // Task list checkbox — drawn as a Carbon checkbox, not the native control.
2619
+ // `appearance: none` is what replaces WebKit's small rounded default; the box
2620
+ // then matches the design system rather than approximating it with an accent
2621
+ // colour over the native shape.
2622
+ //
2623
+ // Sized in `em`, not `rem`: a 1rem box beside 1rem text is as tall as the
2624
+ // whole em box, so it towered over lowercase letters, whose cap height is
2625
+ // nearer 0.7em. One knob — the checkbox's own `font-size` — drives the box,
2626
+ // the tick and the baseline offset, so the parts cannot drift apart, and the
2627
+ // control tracks the text if a list ever renders at another size.
2623
2628
  ".cm-task-checkbox": {
2624
2629
  appearance: "none",
2625
2630
  WebkitAppearance: "none",
2626
2631
  boxSizing: "border-box",
2627
2632
  position: "relative",
2628
- width: "1rem",
2629
- height: "1rem",
2630
- margin: "0 0.4em 0 0",
2633
+ fontSize: "0.875em",
2634
+ width: "1em",
2635
+ height: "1em",
2636
+ margin: "0 0.45em 0 0",
2631
2637
  cursor: "pointer",
2632
- verticalAlign: "-0.15em",
2633
- border: "0.0625rem solid var(--cds-icon-primary, #161616)",
2634
- borderRadius: "0.0625rem",
2638
+ // Centres the box on the x-height rather than hanging it off the baseline.
2639
+ verticalAlign: "-0.2em",
2640
+ border: "0.0625em solid var(--cds-icon-primary, #161616)",
2641
+ borderRadius: "0.0625em",
2635
2642
  background: "transparent"
2636
2643
  },
2637
2644
  ".cm-task-checkbox:checked": {
@@ -2639,15 +2646,27 @@ var editorBaseTheme = EditorView.theme({
2639
2646
  borderColor: "var(--cds-icon-primary, #161616)"
2640
2647
  },
2641
2648
  // The tick: an L (right + bottom border) rotated 45° into a check.
2649
+ //
2650
+ // Drawn in `--cds-background`, NOT `--cds-icon-on-color`. "On color" means an
2651
+ // icon on a branded fill and is white in every theme, while the box here is
2652
+ // filled with `--cds-icon-primary` — which inverts. In dark that was a white
2653
+ // tick on a near-white box: a checked item read as unchecked.
2654
+ //
2655
+ // `top` is the OPTICAL centre, not the geometric one. The L's ink sits below
2656
+ // and right of its own rect centre, and `rotate(45deg)` turns that diagonal
2657
+ // offset into a purely downward one — 0.065em of it — so the rect has to ride
2658
+ // that much higher for the tick to look centred. Geometric centring (0.1875em)
2659
+ // renders visibly low; the old 0.0625em was visibly high.
2642
2660
  ".cm-task-checkbox:checked::after": {
2643
2661
  content: "''",
2644
2662
  position: "absolute",
2645
- left: "0.3125rem",
2646
- top: "0.0625rem",
2647
- width: "0.25rem",
2648
- height: "0.5rem",
2649
- border: "solid var(--cds-icon-on-color, #ffffff)",
2650
- borderWidth: "0 0.125rem 0.125rem 0",
2663
+ left: "0.3125em",
2664
+ top: "0.125em",
2665
+ width: "0.25em",
2666
+ height: "0.5em",
2667
+ border: "solid var(--cds-background, #ffffff)",
2668
+ // 0.125em read as a heavy slab once the box shrank to 0.875em.
2669
+ borderWidth: "0 0.1em 0.1em 0",
2651
2670
  transform: "rotate(45deg)"
2652
2671
  },
2653
2672
  ".cm-task-checkbox:focus-visible": {
@@ -4350,7 +4369,7 @@ function pickImageFileForCaret(view) {
4350
4369
  };
4351
4370
  input.click();
4352
4371
  }
4353
- function composeExtensions(modules) {
4372
+ function mergeExtensions(modules) {
4354
4373
  const extensions = [];
4355
4374
  const toolbar = [];
4356
4375
  const allKeyBindings = [];
@@ -4374,6 +4393,7 @@ function composeExtensions(modules) {
4374
4393
  if (allKeyBindings.length) extensions.push(keymap.of(allKeyBindings));
4375
4394
  return { extensions, toolbar };
4376
4395
  }
4396
+ var composeExtensions = mergeExtensions;
4377
4397
  var HIGHLIGHT_RE = /==([^=\n]+?)==/g;
4378
4398
  var HIDE2 = Decoration.replace({});
4379
4399
  var highlightMark = Decoration.mark({ class: "cm-highlight" });
@@ -5576,6 +5596,7 @@ function CodeMirrorMarkdownEditorInner({
5576
5596
  filePath,
5577
5597
  linkTargets,
5578
5598
  onNavigateToLink,
5599
+ extensions: hostExtensions,
5579
5600
  toolbar,
5580
5601
  selectionActions,
5581
5602
  resolveImageSrc,
@@ -5624,6 +5645,9 @@ function CodeMirrorMarkdownEditorInner({
5624
5645
  const syncedHashRef = useRef(/* @__PURE__ */ new Map());
5625
5646
  const currentFileRef = useRef(filePath);
5626
5647
  const modeInitializedRef = useRef(decorationsEnabled);
5648
+ const hostExtensionsRef = useRef(hostExtensions);
5649
+ hostExtensionsRef.current = hostExtensions;
5650
+ const toolbarContributionsRef = useRef([]);
5627
5651
  function buildExtensions() {
5628
5652
  const base = [
5629
5653
  history(),
@@ -5783,16 +5807,20 @@ function CodeMirrorMarkdownEditorInner({
5783
5807
  ];
5784
5808
  if (decorationsEnabled) {
5785
5809
  base.push(markdownDecorationsPlugin);
5786
- const composed = composeExtensions([
5810
+ }
5811
+ const merged = mergeExtensions([
5812
+ ...decorationsEnabled ? [
5787
5813
  wikilinkExtension,
5788
5814
  highlightExtension,
5789
5815
  footnoteExtension,
5790
5816
  mathExtension,
5791
5817
  mermaidExtension,
5792
5818
  tableExtension()
5793
- ]);
5794
- base.push(...composed.extensions);
5795
- }
5819
+ ] : [],
5820
+ ...hostExtensionsRef.current ?? []
5821
+ ]);
5822
+ base.push(...merged.extensions);
5823
+ toolbarContributionsRef.current = merged.toolbar;
5796
5824
  return base;
5797
5825
  }
5798
5826
  const buildExtensionsRef = useRef(buildExtensions);
@@ -5950,7 +5978,7 @@ function CodeMirrorMarkdownEditorInner({
5950
5978
  view.dispatch({ selection: EditorSelection.cursor(head) });
5951
5979
  }, []);
5952
5980
  const toolbarNode = useMemo(
5953
- () => viewForToolbar && toolbar ? toolbar({ view: viewForToolbar }) : null,
5981
+ () => viewForToolbar && toolbar ? toolbar({ view: viewForToolbar, contributions: toolbarContributionsRef.current }) : null,
5954
5982
  [viewForToolbar, toolbar]
5955
5983
  );
5956
5984
  const selectionNode = useMemo(
@@ -5965,6 +5993,6 @@ function CodeMirrorMarkdownEditorInner({
5965
5993
  }
5966
5994
  var CodeMirrorMarkdownEditor = memo(CodeMirrorMarkdownEditorInner);
5967
5995
 
5968
- export { CodeMirrorMarkdownEditor, IMAGE_EDIT_ALT_EVENT, blockCommands, buildImageMarkdown, composeExtensions, computeFileDir, defaultResolveImageSrc, dirnamePath, editorBaseTheme, escapeAttr, escapeText, extractImageBlobs, extractImageFiles, footnoteExtension, formatCommands, getCachedMermaidPng, hasUriScheme, headingLine, hideAlways, highlightExtension, highlightFenceSpans, imageInsertHandlers, inlineScanRulesFacet, insertImageBlob, isAbsolutePath, isMermaidFenceInfo, joinPath, line, mark, markdownDecorationsPlugin, mathExtension, mermaidExtension, nodeRulesFacet, onEditorUpdate, parseFrontmatter, parseWikilinkBody, pickImageFileForCaret, raw, renderMermaidToSvg, resolveWikilinkTarget, resolveWorkspaceLink, scanInline, serializeMarkdown, setFrontmatterField, showImageActionMenu, structural, tableExtension, treeAt, warmMermaidPng, wikilinkExtension };
5996
+ export { CodeMirrorMarkdownEditor, IMAGE_EDIT_ALT_EVENT, blockCommands, buildImageMarkdown, composeExtensions, computeFileDir, defaultResolveImageSrc, dirnamePath, editorBaseTheme, escapeAttr, escapeText, extractImageBlobs, extractImageFiles, footnoteExtension, formatCommands, getCachedMermaidPng, hasUriScheme, headingLine, hideAlways, highlightExtension, highlightFenceSpans, imageInsertHandlers, inlineScanRulesFacet, insertImageBlob, isAbsolutePath, isMermaidFenceInfo, joinPath, line, mark, markdownDecorationsPlugin, mathExtension, mergeExtensions, mermaidExtension, nodeRulesFacet, onEditorUpdate, parseFrontmatter, parseWikilinkBody, pickImageFileForCaret, raw, renderMermaidToSvg, resolveWikilinkTarget, resolveWorkspaceLink, scanInline, serializeMarkdown, setFrontmatterField, showImageActionMenu, structural, tableExtension, treeAt, warmMermaidPng, wikilinkExtension };
5969
5997
  //# sourceMappingURL=index.js.map
5970
5998
  //# sourceMappingURL=index.js.map