@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.
- package/README.md +44 -0
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +34 -8
- package/dist/editor.js.map +1 -1
- package/dist/effects.d.ts +5 -3
- package/dist/effects.d.ts.map +1 -1
- package/dist/effects.js +7 -5
- package/dist/effects.js.map +1 -1
- package/dist/format.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/paste.d.ts.map +1 -1
- package/dist/paste.js +5 -0
- package/dist/paste.js.map +1 -1
- package/dist/plugins/_preview.js.map +1 -1
- package/dist/plugins/callout.d.ts.map +1 -1
- package/dist/plugins/callout.js +37 -45
- package/dist/plugins/callout.js.map +1 -1
- package/dist/plugins/context-menu.js.map +1 -1
- package/dist/plugins/core.js.map +1 -1
- package/dist/plugins/emoji.js.map +1 -1
- package/dist/plugins/floating-toolbar.js.map +1 -1
- package/dist/plugins/hr.d.ts.map +1 -1
- package/dist/plugins/hr.js +4 -7
- package/dist/plugins/hr.js.map +1 -1
- package/dist/plugins/image.d.ts.map +1 -1
- package/dist/plugins/image.js +28 -20
- package/dist/plugins/image.js.map +1 -1
- package/dist/plugins/inline.js.map +1 -1
- package/dist/plugins/link.d.ts.map +1 -1
- package/dist/plugins/link.js +16 -9
- package/dist/plugins/link.js.map +1 -1
- package/dist/plugins/math.d.ts.map +1 -1
- package/dist/plugins/math.js +25 -34
- package/dist/plugins/math.js.map +1 -1
- package/dist/plugins/mention.js.map +1 -1
- package/dist/plugins/mermaid.d.ts.map +1 -1
- package/dist/plugins/mermaid.js +25 -34
- package/dist/plugins/mermaid.js.map +1 -1
- package/dist/plugins/overlay.js.map +1 -1
- package/dist/plugins/single-block.js.map +1 -1
- package/dist/plugins/slash.js.map +1 -1
- package/dist/plugins/table.d.ts.map +1 -1
- package/dist/plugins/table.js +7 -4
- package/dist/plugins/table.js.map +1 -1
- package/dist/plugins/types.js.map +1 -1
- package/dist/plugins/ui.js.map +1 -1
- package/dist/security.d.ts +26 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +87 -0
- package/dist/security.js.map +1 -0
- package/dist/state.js.map +1 -1
- package/dist/surfaces/link-dialog.js.map +1 -1
- package/dist/surfaces/toolbar.js.map +1 -1
- package/dist/theme.js.map +1 -1
- package/dist/transformers/gfm.js.map +1 -1
- package/dist/transformers/registry.js.map +1 -1
- package/package.json +23 -17
- package/src/editor.ts +339 -0
- package/src/effects.ts +53 -0
- package/src/format.ts +57 -0
- package/src/index.ts +93 -0
- package/src/paste.ts +96 -0
- package/src/plugins/_preview.ts +51 -0
- package/src/plugins/callout.ts +180 -0
- package/src/plugins/context-menu.ts +118 -0
- package/src/plugins/core.ts +179 -0
- package/src/plugins/emoji.ts +59 -0
- package/src/plugins/floating-toolbar.ts +180 -0
- package/src/plugins/hr.ts +56 -0
- package/src/plugins/image.ts +240 -0
- package/src/plugins/inline.ts +76 -0
- package/src/plugins/link.ts +143 -0
- package/src/plugins/math.ts +102 -0
- package/src/plugins/mention.ts +224 -0
- package/src/plugins/mermaid.ts +115 -0
- package/src/plugins/overlay.ts +138 -0
- package/src/plugins/single-block.ts +193 -0
- package/src/plugins/slash.ts +224 -0
- package/src/plugins/table.ts +284 -0
- package/src/plugins/types.ts +50 -0
- package/src/plugins/ui.ts +79 -0
- package/src/security.ts +89 -0
- package/src/state.ts +220 -0
- package/src/styles/editor.css +554 -0
- package/src/surfaces/link-dialog.ts +67 -0
- package/src/surfaces/toolbar.ts +246 -0
- package/src/theme.ts +42 -0
- package/src/transformers/gfm.ts +69 -0
- 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"
|
|
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"
|
|
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"
|
|
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.
|
|
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
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
11
|
},
|
|
12
12
|
"./plugins/core": {
|
|
13
|
-
"
|
|
14
|
-
"
|
|
13
|
+
"types": "./dist/plugins/core.d.ts",
|
|
14
|
+
"import": "./dist/plugins/core.js"
|
|
15
15
|
},
|
|
16
16
|
"./plugins/callout": {
|
|
17
|
-
"
|
|
18
|
-
"
|
|
17
|
+
"types": "./dist/plugins/callout.d.ts",
|
|
18
|
+
"import": "./dist/plugins/callout.js"
|
|
19
19
|
},
|
|
20
20
|
"./surfaces/toolbar": {
|
|
21
|
-
"
|
|
22
|
-
"
|
|
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.
|
|
54
|
-
"@llui/
|
|
55
|
-
"@llui/components": "0.12.
|
|
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
|
+
}
|