@llui/markdown-editor 0.2.12 → 0.2.14

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.
Files changed (89) hide show
  1. package/README.md +44 -0
  2. package/dist/editor.d.ts.map +1 -1
  3. package/dist/editor.js +34 -8
  4. package/dist/editor.js.map +1 -1
  5. package/dist/effects.d.ts +5 -3
  6. package/dist/effects.d.ts.map +1 -1
  7. package/dist/effects.js +7 -5
  8. package/dist/effects.js.map +1 -1
  9. package/dist/format.js.map +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/paste.d.ts.map +1 -1
  12. package/dist/paste.js +5 -0
  13. package/dist/paste.js.map +1 -1
  14. package/dist/plugins/_preview.js.map +1 -1
  15. package/dist/plugins/callout.d.ts.map +1 -1
  16. package/dist/plugins/callout.js +37 -45
  17. package/dist/plugins/callout.js.map +1 -1
  18. package/dist/plugins/context-menu.js.map +1 -1
  19. package/dist/plugins/core.js.map +1 -1
  20. package/dist/plugins/emoji.js.map +1 -1
  21. package/dist/plugins/floating-toolbar.js.map +1 -1
  22. package/dist/plugins/hr.d.ts.map +1 -1
  23. package/dist/plugins/hr.js +4 -7
  24. package/dist/plugins/hr.js.map +1 -1
  25. package/dist/plugins/image.d.ts.map +1 -1
  26. package/dist/plugins/image.js +28 -20
  27. package/dist/plugins/image.js.map +1 -1
  28. package/dist/plugins/inline.js.map +1 -1
  29. package/dist/plugins/link.d.ts.map +1 -1
  30. package/dist/plugins/link.js +16 -9
  31. package/dist/plugins/link.js.map +1 -1
  32. package/dist/plugins/math.d.ts.map +1 -1
  33. package/dist/plugins/math.js +25 -34
  34. package/dist/plugins/math.js.map +1 -1
  35. package/dist/plugins/mention.js.map +1 -1
  36. package/dist/plugins/mermaid.d.ts.map +1 -1
  37. package/dist/plugins/mermaid.js +25 -34
  38. package/dist/plugins/mermaid.js.map +1 -1
  39. package/dist/plugins/overlay.js.map +1 -1
  40. package/dist/plugins/single-block.js.map +1 -1
  41. package/dist/plugins/slash.js.map +1 -1
  42. package/dist/plugins/table.d.ts.map +1 -1
  43. package/dist/plugins/table.js +7 -4
  44. package/dist/plugins/table.js.map +1 -1
  45. package/dist/plugins/types.js.map +1 -1
  46. package/dist/plugins/ui.js.map +1 -1
  47. package/dist/security.d.ts +26 -0
  48. package/dist/security.d.ts.map +1 -0
  49. package/dist/security.js +87 -0
  50. package/dist/security.js.map +1 -0
  51. package/dist/state.js.map +1 -1
  52. package/dist/surfaces/link-dialog.js.map +1 -1
  53. package/dist/surfaces/toolbar.js.map +1 -1
  54. package/dist/theme.js.map +1 -1
  55. package/dist/transformers/gfm.js.map +1 -1
  56. package/dist/transformers/registry.js.map +1 -1
  57. package/package.json +23 -17
  58. package/src/editor.ts +339 -0
  59. package/src/effects.ts +53 -0
  60. package/src/format.ts +57 -0
  61. package/src/index.ts +93 -0
  62. package/src/paste.ts +96 -0
  63. package/src/plugins/_preview.ts +51 -0
  64. package/src/plugins/callout.ts +180 -0
  65. package/src/plugins/context-menu.ts +118 -0
  66. package/src/plugins/core.ts +179 -0
  67. package/src/plugins/emoji.ts +59 -0
  68. package/src/plugins/floating-toolbar.ts +180 -0
  69. package/src/plugins/hr.ts +56 -0
  70. package/src/plugins/image.ts +240 -0
  71. package/src/plugins/inline.ts +76 -0
  72. package/src/plugins/link.ts +143 -0
  73. package/src/plugins/math.ts +102 -0
  74. package/src/plugins/mention.ts +224 -0
  75. package/src/plugins/mermaid.ts +115 -0
  76. package/src/plugins/overlay.ts +138 -0
  77. package/src/plugins/single-block.ts +193 -0
  78. package/src/plugins/slash.ts +224 -0
  79. package/src/plugins/table.ts +284 -0
  80. package/src/plugins/types.ts +50 -0
  81. package/src/plugins/ui.ts +79 -0
  82. package/src/security.ts +89 -0
  83. package/src/state.ts +220 -0
  84. package/src/styles/editor.css +554 -0
  85. package/src/surfaces/link-dialog.ts +67 -0
  86. package/src/surfaces/toolbar.ts +246 -0
  87. package/src/theme.ts +42 -0
  88. package/src/transformers/gfm.ts +69 -0
  89. package/src/transformers/registry.ts +47 -0
@@ -1 +1 @@
1
- {"version":3,"file":"toolbar.js","sourceRoot":"","sources":["../../src/surfaces/toolbar.ts"],"names":[],"mappings":"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","sourcesContent":["// Toolbar surface. `connectToolbar` returns a parts bag (the @llui/components\n// idiom) driven reactively by the format signal; `toolbar` is a ready-made\n// grouped toolbar Mountable for consumers who don't want to hand-render buttons.\n\nimport {\n button,\n div,\n option,\n select,\n span,\n text,\n tagSend,\n unsafeHtml,\n type Mountable,\n type Send,\n type Signal,\n} from '@llui/dom'\nimport type { CommandItem } from '../plugins/types.js'\nimport type { CollabStatus, EditorMsg, FormatState } from '../state.js'\n\nexport interface ToolbarItemParts {\n type: 'button'\n 'data-scope': 'md-toolbar'\n 'data-part': 'item'\n 'data-id': string\n 'aria-label': string\n title: string\n 'aria-pressed': Signal<'true' | 'false'>\n 'aria-disabled': Signal<'true' | undefined>\n disabled: Signal<boolean>\n 'data-active': Signal<'' | undefined>\n onClick: (e: MouseEvent) => void\n}\n\nexport interface ToolbarParts {\n root: {\n role: 'toolbar'\n 'aria-label': string\n 'data-scope': 'md-toolbar'\n 'data-part': 'root'\n }\n item: (id: string) => ToolbarItemParts\n}\n\n/** Build reactive toolbar parts from the format signal. Spread `item(id)` onto a\n * `<button>`; `aria-pressed` / `data-active` / `disabled` track the format. */\nexport function connectToolbar(\n format: Signal<FormatState>,\n send: Send<EditorMsg>,\n items: readonly CommandItem[],\n): ToolbarParts {\n const byId = new Map(items.map((i) => [i.id, i]))\n return {\n root: {\n role: 'toolbar',\n 'aria-label': 'Formatting',\n 'data-scope': 'md-toolbar',\n 'data-part': 'root',\n },\n item: (id) => {\n const item = byId.get(id)\n const label = item?.label ?? id\n return {\n type: 'button',\n 'data-scope': 'md-toolbar',\n 'data-part': 'item',\n 'data-id': id,\n 'aria-label': label,\n title: label,\n 'aria-pressed': format.map((f) => (item?.isActive?.(f) ? 'true' : 'false')),\n 'aria-disabled': format.map((f) => (item?.isDisabled?.(f) ? 'true' : undefined)),\n disabled: format.map((f) => item?.isDisabled?.(f) ?? false),\n 'data-active': format.map((f) => (item?.isActive?.(f) ? '' : undefined)),\n onClick: tagSend(send, ['runCommand'], () => send({ type: 'runCommand', id })),\n }\n },\n }\n}\n\n/** Inline SVG icon (monochrome, inherits `currentColor`) used where a glyph\n * reads poorly. A glyph string starting with `<svg` is rendered as markup. */\nconst LINK_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"15\" height=\"15\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M9.5 13.5a3.5 3.5 0 0 0 5 0l3-3a3.5 3.5 0 1 0-5-5l-1 1\"/><path d=\"M14.5 10.5a3.5 3.5 0 0 0-5 0l-3 3a3.5 3.5 0 1 0 5 5l1-1\"/></svg>'\n\nconst IMAGE_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"15\" height=\"15\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><circle cx=\"9\" cy=\"9.5\" r=\"1.6\"/><path d=\"m4 18 5-5 4 4 3-3 4 4\"/></svg>'\n\n/** Compact glyphs so the default toolbar reads as a real toolbar without icon\n * assets. SVG strings render as icons; everything else as text. Override via\n * `ToolbarOptions.glyphs`. */\nexport const DEFAULT_GLYPHS: Readonly<Record<string, string>> = {\n bold: 'B',\n italic: 'I',\n strikethrough: 'S',\n code: '</>',\n link: LINK_ICON,\n image: IMAGE_ICON,\n hr: '—',\n table: '▦',\n paragraph: '¶',\n h1: 'H1',\n h2: 'H2',\n h3: 'H3',\n quote: '❝',\n codeBlock: '{ }',\n bulletList: '•',\n numberList: '1.',\n checkList: '☑',\n undo: '↺',\n redo: '↻',\n}\n\nconst GROUP_ORDER = ['inline', 'block', 'list', 'insert', 'history']\n\nfunction groupItems(items: readonly CommandItem[]): string[][] {\n const groups = new Map<string, string[]>()\n for (const item of items) {\n const key = item.group ?? 'other'\n const list = groups.get(key) ?? []\n list.push(item.id)\n groups.set(key, list)\n }\n const ordered: string[][] = []\n for (const key of GROUP_ORDER) {\n const list = groups.get(key)\n if (list) ordered.push(list)\n }\n for (const [key, list] of groups) {\n if (!GROUP_ORDER.includes(key)) ordered.push(list)\n }\n return ordered\n}\n\nexport interface ToolbarOptions {\n format: Signal<FormatState>\n send: Send<EditorMsg>\n items: readonly CommandItem[]\n /** Explicit grouped layout of ids; defaults to grouping by `item.group`. */\n groups?: readonly (readonly string[])[]\n /** Glyph overrides (id → text/emoji). Merged over {@link DEFAULT_GLYPHS}. */\n glyphs?: Readonly<Record<string, string>>\n /** Render the `block` group as a `<select>` dropdown instead of buttons\n * (default true). */\n blockSelect?: boolean\n /** Collaborative-session status. When supplied AND `enabled`, the toolbar\n * appends a presence indicator (connection dot + live peer count). */\n collab?: Signal<CollabStatus>\n 'aria-label'?: string\n}\n\n/** Connection/presence indicator for a collaborative session. */\nfunction presenceIndicator(collab: Signal<CollabStatus>): Mountable {\n const statusOf = (c: CollabStatus): string =>\n c.connected ? (c.synced ? 'synced' : 'connecting') : 'offline'\n return div({ 'data-scope': 'md-toolbar', 'data-part': 'presence' }, [\n span({\n 'data-part': 'presence-status',\n 'data-status': collab.map(statusOf),\n 'aria-hidden': 'true',\n title: collab.map(statusOf),\n }),\n span(\n {\n 'data-part': 'presence-peers',\n role: 'status',\n 'aria-live': 'polite',\n },\n [text(collab.map((c) => (c.peers === 0 ? 'Only you' : `${c.peers + 1} editing`)))],\n ),\n ])\n}\n\n/** Render the `block` group as a native `<select>` of block types. */\nfunction blockTypeSelect(\n format: Signal<FormatState>,\n send: Send<EditorMsg>,\n blockItems: readonly CommandItem[],\n): Mountable {\n return select(\n {\n 'data-scope': 'md-toolbar',\n 'data-part': 'block-select',\n 'aria-label': 'Block type',\n value: format.map((f) => blockItems.find((i) => i.isActive?.(f))?.id ?? ''),\n onChange: (e: Event) => {\n const id = (e.currentTarget as HTMLSelectElement).value\n if (id) send({ type: 'runCommand', id })\n },\n },\n [\n option({ value: '', hidden: true }, [text('—')]),\n ...blockItems.map((i) => option({ value: i.id }, [text(i.label)])),\n ],\n )\n}\n\n/** A ready-made grouped toolbar. Items not surfaced to `'toolbar'` are dropped. */\nexport function toolbar(opts: ToolbarOptions): Mountable {\n const surfaceItems = opts.items.filter((i) => i.surfaces?.includes('toolbar') ?? true)\n const byId = new Map(surfaceItems.map((i) => [i.id, i]))\n const parts = connectToolbar(opts.format, opts.send, surfaceItems)\n const glyphs = { ...DEFAULT_GLYPHS, ...(opts.glyphs ?? {}) }\n\n const useBlockSelect = opts.blockSelect !== false && !opts.groups\n const blockItems = useBlockSelect ? surfaceItems.filter((i) => i.group === 'block') : []\n const buttonItems = useBlockSelect\n ? surfaceItems.filter((i) => i.group !== 'block')\n : surfaceItems\n const groups = opts.groups ?? groupItems(buttonItems)\n\n const children: Mountable[] = []\n if (blockItems.length > 0) {\n children.push(\n div({ 'data-scope': 'md-toolbar', 'data-part': 'group' }, [\n blockTypeSelect(opts.format, opts.send, blockItems),\n ]),\n )\n }\n for (const ids of groups) {\n const present = ids.filter((id) => byId.has(id))\n if (present.length === 0) continue\n children.push(\n div(\n { 'data-scope': 'md-toolbar', 'data-part': 'group' },\n present.map((id) => {\n const item = byId.get(id)!\n const glyph = glyphs[id] ?? item.label\n const glyphNode = glyph.trimStart().startsWith('<svg') ? unsafeHtml(glyph) : text(glyph)\n return button({ ...parts.item(id) }, [\n span({ 'data-part': 'glyph', 'aria-hidden': 'true' }, [glyphNode]),\n ])\n }),\n ),\n )\n }\n\n // Presence indicator (constant `enabled` after init, so a one-shot peek is safe).\n if (opts.collab && opts.collab.peek().enabled) {\n children.push(presenceIndicator(opts.collab))\n }\n\n return div(\n { ...parts.root, ...(opts['aria-label'] ? { 'aria-label': opts['aria-label'] } : {}) },\n children,\n )\n}\n"]}
1
+ {"version":3,"file":"toolbar.js","sourceRoot":"","sources":["../../src/surfaces/toolbar.ts"],"names":[],"mappings":"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"}
package/dist/theme.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"theme.js","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,EAAE;AACF,sEAAsE;AACtE,yEAAyE;AACzE,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,6EAA6E;AAC7E,EAAE;AACF,gFAAgF;AAChF,wDAAwD;AAIxD,MAAM,CAAC,MAAM,mBAAmB,GAAG,kBAAkB,CAAA;AACrD,MAAM,CAAC,MAAM,eAAe,GAAG,cAAc,CAAA;AAC7C,MAAM,CAAC,MAAM,6BAA6B,GAAG,4BAA4B,CAAA;AAEzE;;+DAE+D;AAC/D,MAAM,CAAC,MAAM,YAAY,GAAuB;IAC9C,IAAI,EAAE;QACJ,aAAa,EAAE,mBAAmB;QAClC,SAAS,EAAE,eAAe;QAC1B,sBAAsB,EAAE,6BAA6B;KACtD;CACF,CAAA;AAED;;;;;;;8EAO8E;AAC9E,MAAM,UAAU,UAAU,CAAC,KAA0B;IACnD,MAAM,IAAI,GAAG,EAAE,GAAG,YAAY,CAAC,IAAI,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,CAAA;IACrD,OAAQ,IAA8C,CAAC,uBAAuB,CAAA;IAC9E,OAAO,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,CAAA;AAC3B,CAAC","sourcesContent":["// Default Lexical theme for the markdown editor.\n//\n// Lexical styles most inline formats through semantic tags that carry\n// browser-default styling — bold→<strong>, italic→<em>, code→<code>. But\n// `strikethrough` (and `underline`) render as a bare <span> whose ONLY styling\n// hook is a `theme.text.<format>` class. With no theme the format is applied to\n// the model but is visually invisible. This default theme supplies those class\n// names; the bundled `styles/editor.css` gives them their `text-decoration`.\n//\n// Consumers can override any entry via `markdownEditor({ theme })` — the user's\n// theme is merged over this default (see `mergeTheme`).\n\nimport type { EditorThemeClasses } from 'lexical'\n\nexport const STRIKETHROUGH_CLASS = 'md-strikethrough'\nexport const UNDERLINE_CLASS = 'md-underline'\nexport const UNDERLINE_STRIKETHROUGH_CLASS = 'md-underline-strikethrough'\n\n/** The class hooks Lexical needs for text-decoration formats it renders as a\n * plain <span>. (`underlineStrikethrough` is Lexical's special composite key for\n * the case where both apply — both want `text-decoration`.) */\nexport const defaultTheme: EditorThemeClasses = {\n text: {\n strikethrough: STRIKETHROUGH_CLASS,\n underline: UNDERLINE_CLASS,\n underlineStrikethrough: UNDERLINE_STRIKETHROUGH_CLASS,\n },\n}\n\n/** Merge a consumer theme over the default. `text` is merged per-key so a\n * consumer overriding (say) `strikethrough` keeps the default `underline`.\n *\n * Always returns a FRESH theme (never the shared `defaultTheme` singleton):\n * Lexical caches resolved class arrays by MUTATING the `text` object it is\n * handed (`text.__lexicalClassNameCache`). Handing it a fresh copy keeps the\n * exported singleton clean, and stripping any inherited cache prevents a stale\n * entry from a previously-used theme object shadowing an overridden class. */\nexport function mergeTheme(theme?: EditorThemeClasses): EditorThemeClasses {\n const text = { ...defaultTheme.text, ...theme?.text }\n delete (text as { __lexicalClassNameCache?: unknown }).__lexicalClassNameCache\n return { ...theme, text }\n}\n"]}
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,EAAE;AACF,sEAAsE;AACtE,yEAAyE;AACzE,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,6EAA6E;AAC7E,EAAE;AACF,gFAAgF;AAChF,wDAAwD;AAIxD,MAAM,CAAC,MAAM,mBAAmB,GAAG,kBAAkB,CAAA;AACrD,MAAM,CAAC,MAAM,eAAe,GAAG,cAAc,CAAA;AAC7C,MAAM,CAAC,MAAM,6BAA6B,GAAG,4BAA4B,CAAA;AAEzE;;+DAE+D;AAC/D,MAAM,CAAC,MAAM,YAAY,GAAuB;IAC9C,IAAI,EAAE;QACJ,aAAa,EAAE,mBAAmB;QAClC,SAAS,EAAE,eAAe;QAC1B,sBAAsB,EAAE,6BAA6B;KACtD;CACF,CAAA;AAED;;;;;;;8EAO8E;AAC9E,MAAM,UAAU,UAAU,CAAC,KAA0B;IACnD,MAAM,IAAI,GAAG,EAAE,GAAG,YAAY,CAAC,IAAI,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,CAAA;IACrD,OAAQ,IAA8C,CAAC,uBAAuB,CAAA;IAC9E,OAAO,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,CAAA;AAC3B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"gfm.js","sourceRoot":"","sources":["../../src/transformers/gfm.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,kFAAkF;AAGlF,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,gFAAgF;AAChF,wEAAwE;AACxE,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAChE,OAAO,EAEL,OAAO,EACP,KAAK,EACL,cAAc,EACd,YAAY,EACZ,UAAU,EACV,IAAI,EACJ,gBAAgB,EAChB,sBAAsB,EACtB,SAAS,EACT,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,SAAS,EACT,IAAI,GACL,MAAM,mBAAmB,CAAA;AAE1B,wDAAwD;AACxD,MAAM,CAAC,MAAM,SAAS,GAAsC;IAC1D,WAAW;IACX,SAAS;IACT,QAAQ;IACR,YAAY;IACZ,QAAQ;IACR,iBAAiB;IACjB,QAAQ;CACT,CAAA;AAED;;kEAEkE;AAClE,MAAM,CAAC,MAAM,wBAAwB,GAA2B;IAC9D,gBAAgB;IAChB,sBAAsB;IACtB,SAAS;IACT,eAAe;IACf,WAAW;IACX,iBAAiB;IACjB,aAAa;IACb,WAAW;IACX,SAAS;CACV,CAAA;AAED,yDAAyD;AACzD,MAAM,CAAC,MAAM,gBAAgB,GAA2B;IACtD,OAAO;IACP,KAAK;IACL,4EAA4E;IAC5E,2EAA2E;IAC3E,UAAU;IACV,cAAc;IACd,YAAY;IACZ,IAAI;IACJ,GAAG,wBAAwB;IAC3B,IAAI;CACL,CAAA","sourcesContent":["// The built-in GFM superset: node classes + the explicit transformer set that\n// maps exactly to those nodes (no surprise nodes — HR/tables are opt-in plugins).\n\nimport type { Klass, LexicalNode } from 'lexical'\nimport { HeadingNode, QuoteNode } from '@lexical/rich-text'\nimport { ListNode, ListItemNode } from '@lexical/list'\nimport { LinkNode } from '@lexical/link'\n// `@lexical/code-core` (not `@lexical/code`) keeps Prism out of the bundle — we\n// never register syntax highlighting, so plain CodeNode is all we need.\nimport { CodeNode, CodeHighlightNode } from '@lexical/code-core'\nimport {\n type Transformer,\n HEADING,\n QUOTE,\n UNORDERED_LIST,\n ORDERED_LIST,\n CHECK_LIST,\n CODE,\n BOLD_ITALIC_STAR,\n BOLD_ITALIC_UNDERSCORE,\n BOLD_STAR,\n BOLD_UNDERSCORE,\n ITALIC_STAR,\n ITALIC_UNDERSCORE,\n STRIKETHROUGH,\n INLINE_CODE,\n HIGHLIGHT,\n LINK,\n} from '@lexical/markdown'\n\n/** Node classes required to render the GFM superset. */\nexport const GFM_NODES: ReadonlyArray<Klass<LexicalNode>> = [\n HeadingNode,\n QuoteNode,\n ListNode,\n ListItemNode,\n CodeNode,\n CodeHighlightNode,\n LinkNode,\n]\n\n/** Inline text-format transformers (no block nodes, no node registration). These\n * are the only transformers a single-block / inline-only editor needs; `LINK` is\n * kept separate since it requires `LinkNode` to be registered. */\nexport const INLINE_TEXT_TRANSFORMERS: readonly Transformer[] = [\n BOLD_ITALIC_STAR,\n BOLD_ITALIC_UNDERSCORE,\n BOLD_STAR,\n BOLD_UNDERSCORE,\n ITALIC_STAR,\n ITALIC_UNDERSCORE,\n STRIKETHROUGH,\n INLINE_CODE,\n HIGHLIGHT,\n]\n\n/** Markdown ↔ node transformers for the GFM superset. */\nexport const GFM_TRANSFORMERS: readonly Transformer[] = [\n HEADING,\n QUOTE,\n // CHECK_LIST must precede the plain list transformers: `- [ ]`/`- [x]` also\n // match `- `, so UNORDERED_LIST would otherwise swallow it as bullet text.\n CHECK_LIST,\n UNORDERED_LIST,\n ORDERED_LIST,\n CODE,\n ...INLINE_TEXT_TRANSFORMERS,\n LINK,\n]\n"]}
1
+ {"version":3,"file":"gfm.js","sourceRoot":"","sources":["../../src/transformers/gfm.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,kFAAkF;AAGlF,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,gFAAgF;AAChF,wEAAwE;AACxE,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAChE,OAAO,EAEL,OAAO,EACP,KAAK,EACL,cAAc,EACd,YAAY,EACZ,UAAU,EACV,IAAI,EACJ,gBAAgB,EAChB,sBAAsB,EACtB,SAAS,EACT,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,SAAS,EACT,IAAI,GACL,MAAM,mBAAmB,CAAA;AAE1B,wDAAwD;AACxD,MAAM,CAAC,MAAM,SAAS,GAAsC;IAC1D,WAAW;IACX,SAAS;IACT,QAAQ;IACR,YAAY;IACZ,QAAQ;IACR,iBAAiB;IACjB,QAAQ;CACT,CAAA;AAED;;kEAEkE;AAClE,MAAM,CAAC,MAAM,wBAAwB,GAA2B;IAC9D,gBAAgB;IAChB,sBAAsB;IACtB,SAAS;IACT,eAAe;IACf,WAAW;IACX,iBAAiB;IACjB,aAAa;IACb,WAAW;IACX,SAAS;CACV,CAAA;AAED,yDAAyD;AACzD,MAAM,CAAC,MAAM,gBAAgB,GAA2B;IACtD,OAAO;IACP,KAAK;IACL,4EAA4E;IAC5E,2EAA2E;IAC3E,UAAU;IACV,cAAc;IACd,YAAY;IACZ,IAAI;IACJ,GAAG,wBAAwB;IAC3B,IAAI;CACL,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/transformers/registry.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,+EAA+E;AAC/E,kDAAkD;AAKlD,6EAA6E;AAC7E,6EAA6E;AAC7E,mEAAmE;AACnE,MAAM,SAAS,GAA2B;IACxC,mBAAmB,EAAE,CAAC;IACtB,OAAO,EAAE,CAAC;IACV,aAAa,EAAE,CAAC;IAChB,YAAY,EAAE,CAAC;CAChB,CAAA;AAED,+DAA+D;AAC/D,MAAM,UAAU,iBAAiB,CAAC,YAAoC;IACpE,OAAO,CAAC,GAAG,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACrC,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACjC,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACjC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,EAAE,GAAG,EAAE,CAAA;QAC7B,IAAI,CAAC,CAAC,IAAI,KAAK,aAAa,IAAI,CAAC,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YACzD,qDAAqD;YACrD,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAA;QACpC,CAAC;QACD,OAAO,CAAC,CAAA;IACV,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;kCAEkC;AAClC,MAAM,UAAU,iBAAiB,CAAC,OAAkC;IAClE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAe,CAAA;IACnC,MAAM,SAAS,GAAkB,EAAE,CAAA;IACnC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,YAAY,IAAI,EAAE,EAAE,CAAC;YACpD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;gBAC3B,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;gBACrB,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YAC7B,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,iBAAiB,CAAC,SAAS,CAAC,CAAA;AACrC,CAAC","sourcesContent":["// The transformer registry: flatten plugin transformer contributions into a\n// single, correctly-ordered Lexical TRANSFORMERS array. Ordering is the single\n// place markdown round-trip fidelity is governed.\n\nimport type { Transformer } from '@lexical/markdown'\nimport type { MarkdownPlugin } from '../plugins/types.js'\n\n// Lexical resolves transformers in array order. Element/multiline constructs\n// must be tried before inline text matching, and longer text-format patterns\n// (e.g. `***`) before shorter (`**`, `*`) so they aren't shadowed.\nconst TYPE_RANK: Record<string, number> = {\n 'multiline-element': 0,\n element: 1,\n 'text-format': 2,\n 'text-match': 3,\n}\n\n/** Stable-sort transformers into the order Lexical expects. */\nexport function orderTransformers(transformers: readonly Transformer[]): Transformer[] {\n return [...transformers].sort((a, b) => {\n const ra = TYPE_RANK[a.type] ?? 9\n const rb = TYPE_RANK[b.type] ?? 9\n if (ra !== rb) return ra - rb\n if (a.type === 'text-format' && b.type === 'text-format') {\n // Longer trigger (`***`) before shorter (`**`, `*`).\n return b.tag.length - a.tag.length\n }\n return 0\n })\n}\n\n/** Collect every plugin's transformers (de-duplicated by reference) and order\n * them. The result is passed to `$convertTo/FromMarkdownString` and\n * `registerMarkdownShortcuts`. */\nexport function buildTransformers(plugins: readonly MarkdownPlugin[]): Transformer[] {\n const seen = new Set<Transformer>()\n const collected: Transformer[] = []\n for (const plugin of plugins) {\n for (const transformer of plugin.transformers ?? []) {\n if (!seen.has(transformer)) {\n seen.add(transformer)\n collected.push(transformer)\n }\n }\n }\n return orderTransformers(collected)\n}\n"]}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/transformers/registry.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,+EAA+E;AAC/E,kDAAkD;AAKlD,6EAA6E;AAC7E,6EAA6E;AAC7E,mEAAmE;AACnE,MAAM,SAAS,GAA2B;IACxC,mBAAmB,EAAE,CAAC;IACtB,OAAO,EAAE,CAAC;IACV,aAAa,EAAE,CAAC;IAChB,YAAY,EAAE,CAAC;CAChB,CAAA;AAED,+DAA+D;AAC/D,MAAM,UAAU,iBAAiB,CAAC,YAAoC;IACpE,OAAO,CAAC,GAAG,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACrC,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACjC,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACjC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,EAAE,GAAG,EAAE,CAAA;QAC7B,IAAI,CAAC,CAAC,IAAI,KAAK,aAAa,IAAI,CAAC,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YACzD,qDAAqD;YACrD,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAA;QACpC,CAAC;QACD,OAAO,CAAC,CAAA;IACV,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;kCAEkC;AAClC,MAAM,UAAU,iBAAiB,CAAC,OAAkC;IAClE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAe,CAAA;IACnC,MAAM,SAAS,GAAkB,EAAE,CAAA;IACnC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,YAAY,IAAI,EAAE,EAAE,CAAC;YACpD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;gBAC3B,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;gBACrB,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YAC7B,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,iBAAiB,CAAC,SAAS,CAAC,CAAA;AACrC,CAAC"}
package/package.json CHANGED
@@ -1,32 +1,29 @@
1
1
  {
2
2
  "name": "@llui/markdown-editor",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
8
8
  ".": {
9
- "import": "./dist/index.js",
10
- "types": "./dist/index.d.ts"
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
11
  },
12
12
  "./plugins/core": {
13
- "import": "./dist/plugins/core.js",
14
- "types": "./dist/plugins/core.d.ts"
13
+ "types": "./dist/plugins/core.d.ts",
14
+ "import": "./dist/plugins/core.js"
15
15
  },
16
16
  "./plugins/callout": {
17
- "import": "./dist/plugins/callout.js",
18
- "types": "./dist/plugins/callout.d.ts"
17
+ "types": "./dist/plugins/callout.d.ts",
18
+ "import": "./dist/plugins/callout.js"
19
19
  },
20
20
  "./surfaces/toolbar": {
21
- "import": "./dist/surfaces/toolbar.js",
22
- "types": "./dist/surfaces/toolbar.d.ts"
21
+ "types": "./dist/surfaces/toolbar.d.ts",
22
+ "import": "./dist/surfaces/toolbar.js"
23
23
  },
24
24
  "./styles/editor.css": "./dist/styles/editor.css"
25
25
  },
26
26
  "peerDependencies": {
27
- "@llui/dom": "^0.11.5",
28
- "@llui/lexical": "^0.2.8",
29
- "@llui/components": "^0.12.2",
30
27
  "lexical": "^0.46.0",
31
28
  "@lexical/markdown": "^0.46.0",
32
29
  "@lexical/rich-text": "^0.46.0",
@@ -35,7 +32,11 @@
35
32
  "@lexical/table": "^0.46.0",
36
33
  "@lexical/selection": "^0.46.0",
37
34
  "@lexical/utils": "^0.46.0",
38
- "@lexical/code-core": "^0.46.0"
35
+ "@lexical/code-core": "^0.46.0",
36
+ "@llui/dom": "^0.11.8",
37
+ "@llui/lexical": "^0.2.10",
38
+ "@llui/markdown": "^0.11.3",
39
+ "@llui/components": "^0.12.4"
39
40
  },
40
41
  "devDependencies": {
41
42
  "lexical": "^0.46.0",
@@ -50,9 +51,10 @@
50
51
  "typescript": "^6.0.0",
51
52
  "vitest": "^4.1.2",
52
53
  "@lexical/code-core": "^0.46.0",
53
- "@llui/dom": "0.11.5",
54
- "@llui/lexical": "0.2.8",
55
- "@llui/components": "0.12.2"
54
+ "@llui/dom": "0.11.8",
55
+ "@llui/markdown": "0.11.3",
56
+ "@llui/components": "0.12.4",
57
+ "@llui/lexical": "0.2.10"
56
58
  },
57
59
  "sideEffects": [
58
60
  "./dist/styles/editor.css"
@@ -78,8 +80,12 @@
78
80
  },
79
81
  "homepage": "https://github.com/fponticelli/llui/tree/main/packages/markdown-editor#readme",
80
82
  "files": [
81
- "dist"
83
+ "dist",
84
+ "src"
82
85
  ],
86
+ "publishConfig": {
87
+ "access": "public"
88
+ },
83
89
  "scripts": {
84
90
  "build": "tsc -p tsconfig.build.json && mkdir -p dist/styles && cp src/styles/editor.css dist/styles/",
85
91
  "check": "tsc --noEmit -p tsconfig.check.json",
package/src/editor.ts ADDED
@@ -0,0 +1,339 @@
1
+ // `markdownEditor(config)` — the high-level component. Lexical owns the live
2
+ // document; this wires the foreign seam to the markdown transformer converters,
3
+ // surfaces the format state for the chrome, routes command intents back to the
4
+ // live editor through effects, and COMPOSES plugin UI extensions (each plugin's
5
+ // state slice + reducer + view + effects) into the single component.
6
+
7
+ import { $getRoot, $setSelection, type EditorThemeClasses, type LexicalEditor } from 'lexical'
8
+ import {
9
+ $convertFromMarkdownString,
10
+ $convertToMarkdownString,
11
+ registerMarkdownShortcuts,
12
+ } from '@lexical/markdown'
13
+ import { component, div, type Renderable, type Signal, type SignalComponentDef } from '@llui/dom'
14
+ import {
15
+ lexicalForeign,
16
+ registerDecoratorBridges,
17
+ PROGRAMMATIC_TAG,
18
+ type DecoratorBridge,
19
+ } from '@llui/lexical'
20
+ import { corePlugin } from './plugins/core.js'
21
+ import { linkPlugin } from './plugins/link.js'
22
+ import { registerMarkdownPaste } from './paste.js'
23
+ import { registerLinkSanitizer } from './security.js'
24
+ import { toolbar as renderToolbar } from './surfaces/toolbar.js'
25
+ import type { CommandItem, MarkdownPlugin } from './plugins/types.js'
26
+ import type { PluginUI } from './plugins/ui.js'
27
+ import { buildTransformers } from './transformers/registry.js'
28
+ import { mergeTheme } from './theme.js'
29
+ import { computeFormatState } from './format.js'
30
+ import { makeOnEffect } from './effects.js'
31
+ import {
32
+ countWords,
33
+ init,
34
+ update,
35
+ type EditorEffect,
36
+ type EditorMsg,
37
+ type EditorOutMsg,
38
+ type EditorState,
39
+ type FormatState,
40
+ } from './state.js'
41
+
42
+ export interface EditorConfig {
43
+ /** Plugins composing the feature set; order defines transformer precedence.
44
+ * Defaults to `[corePlugin(), linkPlugin()]` so the minimal editor has GFM + links. */
45
+ plugins?: readonly MarkdownPlugin[]
46
+ /** Initial markdown (uncontrolled seed). */
47
+ defaultValue?: string
48
+ /** Controlled: the consumer owns this signal; the editor follows it. */
49
+ value?: Signal<string>
50
+ /** Debounced markdown-emission window (ms). Default 300. */
51
+ changeDebounceMs?: number
52
+ placeholder?: string
53
+ readonly?: boolean
54
+ /** Lexical theme class map. */
55
+ theme?: EditorThemeClasses
56
+ /** Editor namespace (instance isolation). */
57
+ namespace?: string
58
+ /** Outbound markdown (after debounce). */
59
+ onChange?: (markdown: string) => void
60
+ /** Outbound format surface (for chrome built outside this package). */
61
+ onFormatChange?: (format: FormatState) => void
62
+ /** Receives the live Lexical editor at mount (imperative access, collab hooks). */
63
+ onReady?: (editor: LexicalEditor) => void
64
+ /** Render the built-in toolbar above the editor. Default false (minimal). */
65
+ toolbar?: boolean
66
+ /** Convert plain-text Markdown to rich content on paste. Default true.
67
+ * Pastes that carry `text/html` are always left to Lexical's HTML import,
68
+ * regardless of this flag. Set false to paste Markdown as literal text. */
69
+ pasteMarkdown?: boolean
70
+ /** Enable collaborative editing. The editor hands you a markdown `seed` and
71
+ * status sinks; return a binding (build it with `yjsCollab` from
72
+ * `@llui/lexical-collab`, wiring your own provider). Mutually exclusive with
73
+ * `value` — the shared CRDT document, not a markdown signal, owns the content.
74
+ * `defaultValue` becomes the seed the bootstrapping peer writes. */
75
+ collab?: CollabFactory
76
+ }
77
+
78
+ /** Disposer-returning binding the collab layer installs on the live editor.
79
+ * `@llui/lexical-collab`'s `YjsCollab` satisfies this structurally, so
80
+ * `@llui/markdown-editor` needs no Yjs dependency of its own. */
81
+ export interface CollabBinding {
82
+ register: (editor: LexicalEditor) => () => void
83
+ }
84
+
85
+ /** Hooks the editor injects into the {@link CollabFactory}: a markdown `seed`
86
+ * (run once by the bootstrapping peer to fill an empty shared doc from
87
+ * `defaultValue`) plus status sinks the editor mirrors into `state.collab`.
88
+ * Spread straight into `yjsCollab({ id, provider, user, ...hooks })`. */
89
+ export interface CollabHooks {
90
+ seed: (editor: LexicalEditor) => void
91
+ onStatus: (connected: boolean) => void
92
+ onSync: (synced: boolean) => void
93
+ onPeers: (count: number) => void
94
+ }
95
+
96
+ /** Builds the collab binding from the editor-supplied hooks. */
97
+ export type CollabFactory = (hooks: CollabHooks) => CollabBinding
98
+
99
+ /** Hooks the chrome layer (toolbar/menus) uses to compose around the editor. */
100
+ export interface EditorParts {
101
+ /** The merged, surface-filtered command items. */
102
+ items: readonly CommandItem[]
103
+ /** Reactive format signal for `connect`-style toolbars. */
104
+ format: Signal<FormatState>
105
+ }
106
+
107
+ /** Default plugin set when the consumer supplies none. */
108
+ function defaultPlugins(): MarkdownPlugin[] {
109
+ return [corePlugin(), linkPlugin()]
110
+ }
111
+
112
+ /** The per-mount `send` closure — the WeakMap key that ties a live editor to the
113
+ * one mount it belongs to. */
114
+ type EditorSend = (msg: EditorMsg) => void
115
+
116
+ /** Per-mount live state: the Lexical editor captured by that mount's `onReady`. */
117
+ interface MountContext {
118
+ editor: LexicalEditor | null
119
+ }
120
+
121
+ /**
122
+ * Build the markdown editor component. Embed it with `mountApp(el, markdownEditor(...))`
123
+ * or compose it inside a larger component.
124
+ */
125
+ export function markdownEditor(
126
+ config: EditorConfig = {},
127
+ ): SignalComponentDef<EditorState, EditorMsg, EditorEffect> {
128
+ const plugins = config.plugins && config.plugins.length > 0 ? config.plugins : defaultPlugins()
129
+ const transformers = buildTransformers(plugins)
130
+
131
+ const items: CommandItem[] = plugins.flatMap((p) => p.items ?? [])
132
+ const itemsById = new Map(items.map((i) => [i.id, i]))
133
+ // Share the merged item list with plugins that want it (e.g. the slash menu).
134
+ for (const plugin of plugins) plugin.onItems?.(items)
135
+ const decorators: DecoratorBridge[] = plugins.flatMap((p) => p.decorators ?? [])
136
+ const pluginUIs: Array<{ name: string; ui: PluginUI }> = plugins
137
+ .filter((p): p is MarkdownPlugin & { ui: PluginUI } => p.ui !== undefined)
138
+ .map((p) => ({ name: p.name, ui: p.ui }))
139
+ const pluginUIByName = new Map(pluginUIs.map((p) => [p.name, p.ui]))
140
+
141
+ // Per-MOUNT live-editor context, keyed by the mount's stable `send` closure —
142
+ // the SAME object the runtime hands to both `view` and `onEffect`. A single
143
+ // definition can be mounted more than once (or re-mounted); each mount gets its
144
+ // own editor reference here, so effects and plugin handlers dispatch to their
145
+ // own editor and unmount clears only that mount's reference (no cross-wiring,
146
+ // no leaked disposed editor). Entries are collected with their `send` key.
147
+ const mountContexts = new WeakMap<EditorSend, MountContext>()
148
+ const contextFor = (send: EditorSend): MountContext => {
149
+ let ctx = mountContexts.get(send)
150
+ if (!ctx) {
151
+ ctx = { editor: null }
152
+ mountContexts.set(send, ctx)
153
+ }
154
+ return ctx
155
+ }
156
+
157
+ const baseOnEffect = makeOnEffect((api) => contextFor(api.send).editor, itemsById, {
158
+ onChange: config.onChange,
159
+ onFormatChange: config.onFormatChange,
160
+ applyValue: (editor, value) =>
161
+ editor.update(
162
+ () => {
163
+ $convertFromMarkdownString(value, transformers)
164
+ // Clear selection so the reconciler doesn't pull DOM focus into the
165
+ // editor on an external push (e.g. typing in a bound source textarea).
166
+ $setSelection(null)
167
+ },
168
+ { tag: PROGRAMMATIC_TAG },
169
+ ),
170
+ })
171
+
172
+ if (config.collab && config.value) {
173
+ throw new Error(
174
+ 'markdownEditor: `collab` and `value` are mutually exclusive — in a collaborative ' +
175
+ 'session the shared CRDT document owns the content, not a markdown signal. ' +
176
+ 'Use `defaultValue` as the bootstrap seed instead.',
177
+ )
178
+ }
179
+
180
+ const collabEnabled = !!config.collab
181
+ const seedValue = config.value ? config.value.peek() : (config.defaultValue ?? '')
182
+
183
+ // ── Composed TEA: core + plugin UI slices ──────────────────────────────────
184
+ const composedInit = (): [EditorState, EditorEffect[]] => {
185
+ const [core, effects] = init({
186
+ value: seedValue,
187
+ readonly: config.readonly ?? false,
188
+ collab: collabEnabled,
189
+ })
190
+ const slices: Record<string, unknown> = {}
191
+ for (const { name, ui } of pluginUIs) slices[name] = ui.init()
192
+ return [{ ...core, plugins: slices }, effects]
193
+ }
194
+
195
+ const composedUpdate = (state: EditorState, msg: EditorMsg): [EditorState, EditorEffect[]] => {
196
+ if (msg.type === 'plugin') {
197
+ const ui = pluginUIByName.get(msg.name)
198
+ if (!ui?.update) return [state, []]
199
+ const result = ui.update(state.plugins[msg.name], msg.msg)
200
+ const [slice, effects] = (Array.isArray(result) ? result : [result, []]) as [
201
+ unknown,
202
+ unknown[],
203
+ ]
204
+ return [
205
+ { ...state, plugins: { ...state.plugins, [msg.name]: slice } },
206
+ effects.map((effect) => ({ type: 'pluginEffect' as const, name: msg.name, effect })),
207
+ ]
208
+ }
209
+ return update(state, msg)
210
+ }
211
+
212
+ const composedOnEffect = (
213
+ effect: EditorEffect,
214
+ api: { send: (msg: EditorMsg) => void; state: Signal<EditorState> },
215
+ ): void => {
216
+ if (effect.type === 'pluginEffect') {
217
+ const mount = contextFor(api.send)
218
+ const ui = pluginUIByName.get(effect.name)
219
+ ui?.onEffect?.(effect.effect, {
220
+ editor: () => mount.editor,
221
+ send: (msg) => api.send({ type: 'plugin', name: effect.name, msg }),
222
+ emit: (msg) => api.send(msg as EditorMsg),
223
+ })
224
+ return
225
+ }
226
+ baseOnEffect(effect, api)
227
+ }
228
+
229
+ const view = ({
230
+ state,
231
+ send,
232
+ }: {
233
+ state: Signal<EditorState>
234
+ send: (msg: EditorMsg) => void
235
+ }): Renderable => {
236
+ // This mount's live-editor slot (keyed by this mount's `send`). `onReady`
237
+ // fills it; effects/plugins read it through `contextFor(api.send)`.
238
+ const mount = contextFor(send)
239
+
240
+ // Build the collab binding (once, at mount) from the consumer's factory,
241
+ // injecting the markdown seed + status sinks that mirror into `state.collab`.
242
+ const collabBinding: CollabBinding | null = config.collab
243
+ ? config.collab({
244
+ seed: () => {
245
+ $convertFromMarkdownString(seedValue, transformers)
246
+ $setSelection(null)
247
+ },
248
+ onStatus: (connected) => send({ type: 'collabStatus', connected }),
249
+ onSync: (synced) => send({ type: 'collabSync', synced }),
250
+ onPeers: (peers) => send({ type: 'collabPeers', peers }),
251
+ })
252
+ : null
253
+
254
+ const host = lexicalForeign<EditorOutMsg>({
255
+ namespace: config.namespace ?? 'llui-markdown',
256
+ theme: mergeTheme(config.theme),
257
+ plugins,
258
+ serialize: (editor) =>
259
+ editor.getEditorState().read(() => $convertToMarkdownString(transformers)),
260
+ deserialize: (_editor, value) => {
261
+ $convertFromMarkdownString(value, transformers)
262
+ $setSelection(null)
263
+ },
264
+ // In collab mode the shared CRDT owns the document: the local undo stack
265
+ // and the boot-time seed are disabled — the binding supplies a scoped undo
266
+ // manager and a sync-gated bootstrap instead.
267
+ ...(collabBinding ? { history: false, seedMode: 'deferred' as const } : {}),
268
+ defaultValue: collabBinding || config.value ? undefined : (config.defaultValue ?? ''),
269
+ ...(config.value && !collabBinding ? { value: config.value } : {}),
270
+ readonly: state.at('readonly'),
271
+ ...(config.changeDebounceMs !== undefined
272
+ ? { changeDebounceMs: config.changeDebounceMs }
273
+ : {}),
274
+ register: (editor) => {
275
+ const disposers = [
276
+ registerMarkdownShortcuts(editor, transformers),
277
+ // Global scheme allowlist for links: the single backstop covering the
278
+ // link dialog, pasted/imported markdown, and the typed `[x](url)`
279
+ // shortcut. Image src is enforced in the image transformer.
280
+ registerLinkSanitizer(editor),
281
+ ]
282
+ if (config.pasteMarkdown !== false)
283
+ disposers.push(registerMarkdownPaste(editor, transformers))
284
+ if (decorators.length > 0) disposers.push(registerDecoratorBridges(editor, decorators))
285
+ if (collabBinding) disposers.push(collabBinding.register(editor))
286
+ return () => {
287
+ for (const dispose of disposers) dispose()
288
+ // Release this mount's editor reference so a disposed editor never
289
+ // leaks and stale effects can't reach a torn-down editor.
290
+ mount.editor = null
291
+ }
292
+ },
293
+ onReady: (editor) => {
294
+ mount.editor = editor
295
+ if (config.placeholder) {
296
+ editor.getRootElement()?.setAttribute('data-placeholder', config.placeholder)
297
+ }
298
+ config.onReady?.(editor)
299
+ },
300
+ onChange: (value) => send({ type: 'markdownChanged', value }),
301
+ onSelectionChange: (ctx) => {
302
+ const format = computeFormatState(ctx.editor, ctx)
303
+ const text = ctx.editor.getEditorState().read(() => $getRoot().getTextContent())
304
+ // Toggle an empty marker so CSS can show the placeholder.
305
+ ctx.editor.getRootElement()?.setAttribute('data-empty', text === '' ? 'true' : 'false')
306
+ send({ type: 'formatChanged', format, wordCount: countWords(text), charCount: text.length })
307
+ },
308
+ emit: (msg) => send(msg),
309
+ })
310
+
311
+ // Plugin view contributions (overlays/panels) — each gets its own slice + send.
312
+ const pluginViews: Renderable = pluginUIs.flatMap(({ name, ui }) => {
313
+ if (!ui.view) return []
314
+ const rendered = ui.view({
315
+ state: state.at(`plugins.${name}`),
316
+ send: (msg) => send({ type: 'plugin', name, msg }),
317
+ editor: () => mount.editor,
318
+ })
319
+ return Array.isArray(rendered) ? rendered : [rendered]
320
+ })
321
+
322
+ if (!config.toolbar) return [host, ...pluginViews]
323
+ return [
324
+ div({ 'data-scope': 'md-editor', 'data-part': 'root' }, [
325
+ renderToolbar({ format: state.at('format'), send, items, collab: state.at('collab') }),
326
+ div({ 'data-scope': 'md-editor', 'data-part': 'surface' }, [host]),
327
+ ]),
328
+ ...pluginViews,
329
+ ]
330
+ }
331
+
332
+ return component<EditorState, EditorMsg, EditorEffect>({
333
+ name: 'MarkdownEditor',
334
+ init: composedInit,
335
+ update: composedUpdate,
336
+ view,
337
+ onEffect: composedOnEffect,
338
+ })
339
+ }
package/src/effects.ts ADDED
@@ -0,0 +1,53 @@
1
+ // Effect handler: the only place TEA reaches back into the live Lexical editor.
2
+ // `execCommand` looks an id up in the merged command-item map and runs it on the
3
+ // editor captured at mount; emit* forward to the consumer's callbacks.
4
+
5
+ import type { LexicalEditor } from 'lexical'
6
+ import type { CommandItem } from './plugins/types.js'
7
+ import type { EditorEffect, EditorMsg, FormatState } from './state.js'
8
+
9
+ /** The api the component passes to `onEffect` (send + state). */
10
+ export interface EffectApi {
11
+ send: (msg: EditorMsg) => void
12
+ }
13
+
14
+ export interface EffectConfig {
15
+ onChange?: (markdown: string) => void
16
+ onFormatChange?: (format: FormatState) => void
17
+ /** Push markdown into the live editor (deserialize), without echoing onChange. */
18
+ applyValue: (editor: LexicalEditor, value: string) => void
19
+ }
20
+
21
+ /** Build the component's `onEffect`. `resolveEditor` maps the per-mount effect
22
+ * `api` (whose `send` identifies the mount) to that mount's live editor — so two
23
+ * mounts of one definition dispatch to their own editors; `items` is the merged
24
+ * id → command map. */
25
+ export function makeOnEffect(
26
+ resolveEditor: (api: EffectApi) => LexicalEditor | null,
27
+ items: ReadonlyMap<string, CommandItem>,
28
+ config: EffectConfig,
29
+ ): (effect: EditorEffect, api: EffectApi) => void {
30
+ return (effect, api) => {
31
+ switch (effect.type) {
32
+ case 'execCommand': {
33
+ const editor = resolveEditor(api)
34
+ const item = items.get(effect.id)
35
+ if (editor && item) item.run(editor, { send: api.send })
36
+ return
37
+ }
38
+ case 'applyValue': {
39
+ const editor = resolveEditor(api)
40
+ if (editor) config.applyValue(editor, effect.value)
41
+ return
42
+ }
43
+ case 'emitChange': {
44
+ config.onChange?.(effect.value)
45
+ return
46
+ }
47
+ case 'emitFormat': {
48
+ config.onFormatChange?.(effect.format)
49
+ return
50
+ }
51
+ }
52
+ }
53
+ }
package/src/format.ts ADDED
@@ -0,0 +1,57 @@
1
+ // Compute the full toolbar FormatState from a Lexical selection — the base
2
+ // rich-text format (from @llui/lexical) refined with list/code/link detection
3
+ // (which needs the list/code/link packages this layer depends on).
4
+
5
+ import { $getSelection, $isRangeSelection, type LexicalEditor } from 'lexical'
6
+ import { $findMatchingParent, $getNearestNodeOfType } from '@lexical/utils'
7
+ import { ListNode, $isListItemNode } from '@lexical/list'
8
+ import { $isCodeNode } from '@lexical/code-core'
9
+ import { $isLinkNode } from '@lexical/link'
10
+ import { $readBaseFormat } from '@llui/lexical'
11
+ import type { SelectionContext } from '@llui/lexical'
12
+ import { EMPTY_FORMAT, type BlockType, type FormatState } from './state.js'
13
+
14
+ /** Read the full format surface at the current selection (opens a read ctx). */
15
+ export function computeFormatState(
16
+ editor: LexicalEditor,
17
+ history: Pick<SelectionContext, 'canUndo' | 'canRedo'>,
18
+ ): FormatState {
19
+ return editor.getEditorState().read(() => {
20
+ const base = $readBaseFormat()
21
+ if (!base.hasSelection) {
22
+ return { ...EMPTY_FORMAT, canUndo: history.canUndo, canRedo: history.canRedo }
23
+ }
24
+
25
+ let blockType: BlockType = base.blockType
26
+ let link = false
27
+
28
+ const selection = $getSelection()
29
+ if ($isRangeSelection(selection)) {
30
+ const anchorNode = selection.anchor.getNode()
31
+ link = $findMatchingParent(anchorNode, (node) => $isLinkNode(node)) !== null
32
+
33
+ if (base.blockType === 'other') {
34
+ const listItem = $findMatchingParent(anchorNode, (node) => $isListItemNode(node))
35
+ if (listItem) {
36
+ const list = $getNearestNodeOfType(anchorNode, ListNode)
37
+ if (list) blockType = list.getListType()
38
+ } else {
39
+ const top = anchorNode.getKey() === 'root' ? null : anchorNode.getTopLevelElement()
40
+ if (top && $isCodeNode(top)) blockType = 'code'
41
+ }
42
+ }
43
+ }
44
+
45
+ return {
46
+ bold: base.bold,
47
+ italic: base.italic,
48
+ strikethrough: base.strikethrough,
49
+ code: base.code,
50
+ link,
51
+ blockType,
52
+ alignment: base.alignment,
53
+ canUndo: history.canUndo,
54
+ canRedo: history.canRedo,
55
+ }
56
+ })
57
+ }