@synclineapi/mdx-editor 1.0.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -120,7 +120,6 @@ const editor = new SynclineMDXEditor({
120
120
  | `mode` | `'split' \| 'editor' \| 'preview'` | `'split'` | Initial layout mode. |
121
121
  | `placeholder` | `string` | — | Placeholder shown in an empty editor. |
122
122
  | `readOnly` | `boolean` | `false` | Prevent all content mutations. |
123
- | `renderers` | `Record<string, RendererFn>` | — | Custom preview renderer overrides. |
124
123
  | `locale` | `Partial<EditorLocale>` | — | i18n label overrides. |
125
124
  ---
126
125
 
@@ -291,65 +290,107 @@ Use `$1`, `$2`, … as cursor tab stops. The cursor lands on `$1` after expansio
291
290
 
292
291
  Plugins are the recommended way to bundle a toolbar button, keyboard shortcut, preview renderer, and autocomplete items together as one reusable unit.
293
292
 
293
+ ### `components` — declarative JSX-style components (recommended)
294
+
295
+ Use `defineComponent()` to create fully typed component renderers. TypeScript infers the exact prop types from your `attrs` declaration — no regex, no string parsing, no manual casts.
296
+
297
+ ```ts
298
+ import { defineComponent } from '@synclineapi/mdx-editor';
299
+ import type { EditorPlugin } from '@synclineapi/mdx-editor';
300
+
301
+ export function badgePlugin(): EditorPlugin {
302
+ return {
303
+ name: 'badge',
304
+
305
+ toolbarItems: [{
306
+ id: 'badge',
307
+ label: 'Badge',
308
+ icon: '<svg>...</svg>',
309
+ tooltip: 'Insert badge',
310
+ action: ({ editor }) => {
311
+ editor.insertBlock('<Badge type="info" label="New" />');
312
+ },
313
+ }],
314
+
315
+ // ✅ Declarative, typed, zero regex
316
+ components: [
317
+ defineComponent({
318
+ tag: 'Badge',
319
+ attrs: {
320
+ type: { type: 'enum', options: ['success', 'warning', 'error', 'info'], default: 'info' },
321
+ label: { type: 'string', default: 'Badge' },
322
+ },
323
+ render: ({ type, label }) =>
324
+ // ^ 'success' | 'warning' | 'error' | 'info' (fully inferred!)
325
+ // ^ string
326
+ `<span class="badge badge-${type}">${label}</span>`,
327
+ }),
328
+ ],
329
+
330
+ styles: `.badge { padding: 2px 8px; border-radius: 4px; font-size: 0.8em; font-weight: 600; }`,
331
+ };
332
+ }
333
+ ```
334
+
335
+ **`attrs` type mapping:**
336
+
337
+ | Declaration | `render` receives |
338
+ |---|---|
339
+ | `{ type: 'enum', options: ['a','b','c'] }` | `'a' \| 'b' \| 'c'` |
340
+ | `{ type: 'number', default: 2 }` | `number` |
341
+ | `{ type: 'boolean', default: false }` | `boolean` |
342
+ | `{ type: 'string' }` | `string` |
343
+ | _(block component)_ | `+ children?: string` |
344
+
345
+ Tokens and autocomplete completions are **registered automatically** for every tag listed in `components` — no `provideTokens` or `completions` arrays needed.
346
+
347
+ ### `patterns` — raw regex for non-JSX fenced syntax
348
+
349
+ Use `patterns` only when you need to match syntax that cannot be expressed as a JSX component — fenced code blocks, `:::type` admonition fences, `$...$` math delimiters, and similar:
350
+
294
351
  ```ts
295
352
  import type { EditorPlugin } from '@synclineapi/mdx-editor';
296
353
 
297
354
  const myPlugin: EditorPlugin = {
298
355
  name: 'my-custom-block',
299
356
 
300
- // Toolbar button
301
357
  toolbarItems: [{
302
358
  id: 'myBlock',
303
359
  label: 'My Block',
304
360
  icon: '<svg>...</svg>',
305
361
  tooltip: 'Insert my block (Ctrl+Shift+M)',
306
362
  action: ({ editor }) => {
307
- editor.insertBlock('<MyBlock>\n Content\n</MyBlock>');
363
+ editor.insertBlock(':::myblock\nContent\n:::');
308
364
  },
309
365
  }],
310
366
 
311
- // Autocomplete same content as the toolbar action, now also reachable
312
- // by typing the trigger word in the editor
313
- completions: [
314
- {
315
- label: 'myblock',
316
- kind: 'snip',
317
- detail: '<MyBlock> component',
318
- body: '<MyBlock>\n $1Content\n</MyBlock>',
319
- },
320
- // Component name entry (shows in autocomplete as a class suggestion)
321
- { label: 'MyBlock', kind: 'cls', detail: 'custom block component' },
322
- ],
323
-
324
- // Preview renderer
325
- renderers: [{
367
+ // ⚠️ Only for non-JSX fenced syntax use `components` for JSX-style tags
368
+ patterns: [{
326
369
  name: 'myBlock',
327
- pattern: /<MyBlock>([\s\S]*?)<\/MyBlock>/g,
370
+ pattern: /:::myblock\n([\s\S]*?):::/g,
328
371
  render: (match) => {
329
- const m = match.match(/<MyBlock>([\s\S]*?)<\/MyBlock>/);
372
+ const m = match.match(/:::myblock\n([\s\S]*?):::/);
330
373
  return `<div class="my-block">${m?.[1] ?? ''}</div>`;
331
374
  },
375
+ priority: 5,
376
+ // Co-locate syntax token colouring with the pattern — fully self-contained
377
+ provideTokens: (line) => {
378
+ const segs = [];
379
+ const m = line.match(/^(:::)(myblock)/);
380
+ if (m) {
381
+ segs.push({ cls: 'kw', start: 0, end: 3 });
382
+ segs.push({ cls: 'cls', start: 3, end: 3 + m[2].length });
383
+ } else if (line === ':::') {
384
+ segs.push({ cls: 'kw', start: 0, end: 3 });
385
+ }
386
+ return segs;
387
+ },
332
388
  }],
333
389
 
334
- // Syntax highlighting — called per-line, highlight component tag
335
- // names, attribute names, and attribute values automatically.
336
- // Use the built-in helper for JSX components:
337
- provideTokens: componentTagTokens(['MyBlock']),
338
- // Or write a custom provider for any syntax pattern:
339
- // provideTokens: (line) => {
340
- // const segs = [];
341
- // const m = line.match(/^(:::)(\w+)/);
342
- // if (m) {
343
- // segs.push({ cls: 'kw', start: 0, end: 3 });
344
- // segs.push({ cls: 'cls', start: 3, end: 3 + m[2].length });
345
- // }
346
- // return segs;
347
- // },
348
-
349
390
  shortcuts: [
350
391
  {
351
392
  key: 'Ctrl+Shift+m',
352
- action: ({ editor }) => editor.insertBlock('<MyBlock>\n Content\n</MyBlock>'),
393
+ action: ({ editor }) => editor.insertBlock(':::myblock\nContent\n:::'),
353
394
  description: 'Insert custom block',
354
395
  },
355
396
  ],
@@ -0,0 +1,124 @@
1
+ import { AttrDef, AnyAttrDef, ComponentDefinition, CompletionItem, InferComponentProps, RendererConfig } from './types';
2
+ type Schema = Record<string, AttrDef>;
3
+ /**
4
+ * Parses a JSX attribute string like ` type="success" cols={2} open`
5
+ * into a plain object, coercing values according to the provided schema.
6
+ *
7
+ * Handles three syntaxes (in this precedence order):
8
+ * 1. `key="value"` and `key='value'` → string (coerced per schema)
9
+ * 2. `key={value}` → number, boolean, or unquoted string
10
+ * 3. `key` → boolean `true` (flag attribute)
11
+ *
12
+ * Schema `default` values fill any attribute not present in the source.
13
+ */
14
+ export declare function parseAttrs(attrStr: string, schema: Schema, tag?: string): Record<string, string | number | boolean>;
15
+ /**
16
+ * Extracts all occurrences of a block component tag from a raw HTML/markdown
17
+ * string (typically the `children` of an outer component).
18
+ *
19
+ * Each returned item has:
20
+ * - `attrs` — the parsed attribute values (typed as a plain object)
21
+ * - `children` — the trimmed inner content of the block
22
+ *
23
+ * This is the key utility for **nested component rendering** (e.g. `<Tabs>`
24
+ * parsing inner `<Tab>` items, `<Steps>` parsing inner `<Step>` items).
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * defineComponent({
29
+ * tag: 'Tabs',
30
+ * selfClosing: false,
31
+ * attrs: {},
32
+ * render: ({ children }) => {
33
+ * const tabs = parseInnerComponents(children ?? '', 'Tab', {
34
+ * title: { type: 'string', default: 'Tab' },
35
+ * });
36
+ * // tabs: Array<{ attrs: { title: string }; children: string }>
37
+ * const buttons = tabs.map((t, i) =>
38
+ * `<button class="${i === 0 ? 'active' : ''}">${t.attrs.title}</button>`
39
+ * ).join('');
40
+ * const panels = tabs.map((t, i) =>
41
+ * `<div style="display:${i === 0 ? 'block' : 'none'}">${t.children}</div>`
42
+ * ).join('');
43
+ * return `<div class="tabs">${buttons}${panels}</div>`;
44
+ * },
45
+ * })
46
+ * ```
47
+ */
48
+ export declare function parseInnerComponents<S extends Schema>(html: string, tag: string, schema?: S): Array<{
49
+ attrs: {
50
+ [K in keyof S]: string | number | boolean;
51
+ };
52
+ children: string;
53
+ }>;
54
+ /**
55
+ * Builds a sensible default insertion template for a component.
56
+ * Used internally to generate toolbar-insert strings and autocomplete snippets.
57
+ */
58
+ export declare function buildInsertTemplate(def: ComponentDefinition): string;
59
+ /**
60
+ * Converts a declarative `ComponentDefinition` into a `RendererConfig`.
61
+ *
62
+ * **Strict typing:** TypeScript infers the exact prop types from `attrs` and
63
+ * enforces them on `render` — no regex, no match groups, no manual casting.
64
+ *
65
+ * | `attrs` declaration | `render` receives |
66
+ * |--------------------------------------------------|-----------------------|
67
+ * | `{ type: 'enum', options: ['a','b','c'] }` | `'a' \| 'b' \| 'c'` |
68
+ * | `{ type: 'number', default: 2 }` | `number` |
69
+ * | `{ type: 'boolean', default: false }` | `boolean` |
70
+ * | `{ type: 'string' }` | `string` |
71
+ *
72
+ * ### Self-closing component
73
+ * ```ts
74
+ * defineComponent({
75
+ * tag: 'Badge',
76
+ * attrs: {
77
+ * type: { type: 'enum', options: ['success', 'warning', 'error', 'info'], default: 'info' },
78
+ * label: { type: 'string', default: 'Badge' },
79
+ * },
80
+ * render: ({ type, label }) =>
81
+ * // ^ 'success' | 'warning' | 'error' | 'info'
82
+ * // ^ string — fully typed, no regex!
83
+ * `<span class="badge badge-${type}">${label}</span>`,
84
+ * })
85
+ * ```
86
+ *
87
+ * ### Block component
88
+ * ```ts
89
+ * defineComponent({
90
+ * tag: 'Card',
91
+ * selfClosing: false,
92
+ * attrs: {
93
+ * title: { type: 'string', required: true, default: 'Card Title' },
94
+ * cols: { type: 'number', default: 1 },
95
+ * },
96
+ * render: ({ title, cols, children }) =>
97
+ * `<div class="card cols-${cols}"><h3>${title}</h3>${children}</div>`,
98
+ * })
99
+ * ```
100
+ *
101
+ * ### Using inside a plugin
102
+ * ```ts
103
+ * // Via the `components` shorthand (recommended):
104
+ * export function myPlugin(): EditorPlugin {
105
+ * return {
106
+ * name: 'my-plugin',
107
+ * components: [
108
+ * defineComponent({ tag: 'Badge', attrs: { … }, render: ({ … }) => `…` }),
109
+ * ],
110
+ * };
111
+ * }
112
+ * ```
113
+ */
114
+ export declare function defineComponent<S extends Record<string, AnyAttrDef>>(def: {
115
+ readonly tag: string;
116
+ readonly label?: string;
117
+ readonly description?: string;
118
+ readonly attrs?: S;
119
+ readonly selfClosing?: boolean;
120
+ readonly render: (props: InferComponentProps<S>) => string;
121
+ readonly priority?: number;
122
+ readonly completions?: CompletionItem[];
123
+ }): RendererConfig;
124
+ export {};
@@ -18,8 +18,6 @@ export interface EditorConfig {
18
18
  placeholder?: string;
19
19
  /** Read-only mode */
20
20
  readOnly?: boolean;
21
- /** Custom renderer overrides */
22
- renderers?: Record<string, RendererFn>;
23
21
  /** Locale / i18n overrides */
24
22
  locale?: Partial<EditorLocale>;
25
23
  /** Callback invoked whenever the markdown content changes */
@@ -41,8 +39,14 @@ export interface EditorPlugin {
41
39
  toolbarItems?: ToolbarItemConfig[];
42
40
  /** Keyboard shortcuts */
43
41
  shortcuts?: ShortcutConfig[];
44
- /** Custom markdown-to-HTML renderers for preview */
45
- renderers?: RendererConfig[];
42
+ /**
43
+ * Raw regex-based pattern renderers — **escape hatch for non-JSX syntax**.
44
+ *
45
+ * Use this only for content that cannot be expressed as a JSX component
46
+ * (e.g. fenced code blocks like ` ```mermaid` , `:::type` admonition fences,
47
+ * or `$...$` math). For all JSX-style components prefer `components`.
48
+ */
49
+ patterns?: RendererConfig[];
46
50
  /** Custom block parsers for extended MDX syntax */
47
51
  parsers?: ParserConfig[];
48
52
  /** CSS to inject */
@@ -102,6 +106,36 @@ export interface EditorPlugin {
102
106
  * ```
103
107
  */
104
108
  provideTokens?: PluginTokenProvider;
109
+ /**
110
+ * Declarative MDX component definitions — **the easiest way to add custom
111
+ * components without writing any regex**.
112
+ *
113
+ * Pass the return value of `defineComponent()` for each component.
114
+ * Syntax-highlight tokens and autocomplete completions are registered
115
+ * automatically for every listed tag.
116
+ *
117
+ * Use `patterns` only when you need to match non-JSX fenced-block syntax
118
+ * (` ```mermaid`, `:::type`, `$...$`) that cannot be expressed as a component.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * import { defineComponent } from '@synclineapi/mdx-editor';
123
+ *
124
+ * components: [
125
+ * defineComponent({
126
+ * tag: 'Badge',
127
+ * attrs: {
128
+ * type: { type: 'enum', options: ['success', 'warning', 'error', 'info'], default: 'info' },
129
+ * label: { type: 'string', default: 'Badge' },
130
+ * },
131
+ * render: ({ type, label }) =>
132
+ * // ^ 'success' | 'warning' | 'error' | 'info' (strictly typed!)
133
+ * `<span class="my-badge my-badge-${type}">${label}</span>`,
134
+ * }),
135
+ * ],
136
+ * ```
137
+ */
138
+ components?: RendererConfig[];
105
139
  }
106
140
  export interface PluginContext {
107
141
  /** The editor instance */
@@ -322,16 +356,263 @@ export interface SelectionState {
322
356
  beforeText: string;
323
357
  afterText: string;
324
358
  }
359
+ /**
360
+ * Attribute schema for a plain string attribute.
361
+ * @example `{ type: 'string', default: 'Card Title' }`
362
+ */
363
+ export interface StringAttrDef {
364
+ readonly type: 'string';
365
+ readonly default?: string;
366
+ readonly required?: boolean;
367
+ }
368
+ /**
369
+ * Attribute schema for a numeric attribute.
370
+ * @example `{ type: 'number', default: 2 }`
371
+ */
372
+ export interface NumberAttrDef {
373
+ readonly type: 'number';
374
+ readonly default?: number;
375
+ readonly required?: boolean;
376
+ }
377
+ /**
378
+ * Attribute schema for a boolean flag attribute.
379
+ * @example `{ type: 'boolean', default: false }`
380
+ */
381
+ export interface BooleanAttrDef {
382
+ readonly type: 'boolean';
383
+ readonly default?: boolean;
384
+ readonly required?: boolean;
385
+ }
386
+ /**
387
+ * Attribute schema for an enum attribute.
388
+ *
389
+ * Use a non-empty `options` array. TypeScript will infer each string literal
390
+ * so the `render` function receives a union of the exact allowed values — no
391
+ * `as const` needed.
392
+ *
393
+ * @example
394
+ * ```ts
395
+ * { type: 'enum', options: ['success', 'warning', 'error', 'info'], default: 'info' }
396
+ * // render receives: { type: 'success' | 'warning' | 'error' | 'info' }
397
+ * ```
398
+ */
399
+ export interface EnumAttrDef<O extends string = string> {
400
+ readonly type: 'enum';
401
+ /**
402
+ * Non-empty tuple of allowed string values.
403
+ * The tuple type enables TypeScript to infer each literal without `as const`.
404
+ */
405
+ readonly options: readonly [O, ...O[]];
406
+ readonly default?: O;
407
+ readonly required?: boolean;
408
+ }
409
+ /**
410
+ * Union of all attribute definition kinds.
411
+ * Use the specific interfaces (`StringAttrDef`, `NumberAttrDef`, etc.) when
412
+ * you need the strict form, or `AttrDef` when you need the loose union.
413
+ */
414
+ export type AttrDef = StringAttrDef | NumberAttrDef | BooleanAttrDef | EnumAttrDef<string>;
415
+ export type AnyAttrDef = StringAttrDef | NumberAttrDef | BooleanAttrDef | EnumAttrDef<string>;
416
+ /**
417
+ * Infers the runtime value type of a single `AttrDef`.
418
+ *
419
+ * | AttrDef kind | Inferred type |
420
+ * |---------------|----------------------------|
421
+ * | `string` | `string` |
422
+ * | `number` | `number` |
423
+ * | `boolean` | `boolean` |
424
+ * | `enum` | union of `options` literals |
425
+ *
426
+ * @example
427
+ * ```ts
428
+ * type T = InferAttrValue<EnumAttrDef<'a' | 'b' | 'c'>>; // 'a' | 'b' | 'c'
429
+ * type N = InferAttrValue<NumberAttrDef>; // number
430
+ * ```
431
+ */
432
+ export type InferAttrValue<D extends AnyAttrDef> = D extends EnumAttrDef<infer O> ? O : D extends NumberAttrDef ? number : D extends BooleanAttrDef ? boolean : string;
433
+ /**
434
+ * Derives the complete **props object type** of a `ComponentDefinition` from
435
+ * its `attrs` schema. This is the exact type that the `render` function
436
+ * receives at the call site.
437
+ *
438
+ * For block components (`selfClosing: false`) the type also includes the
439
+ * optional `children` field containing the inner content.
440
+ *
441
+ * @example
442
+ * ```ts
443
+ * type Props = InferComponentProps<{
444
+ * type: EnumAttrDef<'success' | 'warning' | 'error' | 'info'>;
445
+ * count: NumberAttrDef;
446
+ * open: BooleanAttrDef;
447
+ * label: StringAttrDef;
448
+ * }>;
449
+ * // {
450
+ * // type: 'success' | 'warning' | 'error' | 'info';
451
+ * // count: number;
452
+ * // open: boolean;
453
+ * // label: string;
454
+ * // children?: string;
455
+ * // }
456
+ * ```
457
+ */
458
+ export type InferComponentProps<S extends Record<string, AnyAttrDef>> = {
459
+ readonly [K in keyof S]: InferAttrValue<S[K]>;
460
+ } & {
461
+ readonly children?: string;
462
+ };
463
+ /**
464
+ * Loose props alias — kept for backward compatibility and for cases where
465
+ * you do not need per-attribute literal precision.
466
+ */
467
+ export type ComponentProps = Record<string, string | number | boolean> & {
468
+ children?: string;
469
+ };
470
+ /**
471
+ * Declarative MDX component definition — **no regex required**.
472
+ *
473
+ * When used with `defineComponent()`, TypeScript infers the exact prop types
474
+ * from `attrs` and enforces them on `render`. When listed inline inside
475
+ * `EditorPlugin.components`, props are still type-checked at the base level.
476
+ *
477
+ * The engine automatically:
478
+ * - Builds the match regex from the `tag` name
479
+ * - Parses and type-coerces all attributes
480
+ * - Applies declared defaults for missing attributes
481
+ * - Registers syntax-highlight tokens for the tag name
482
+ * - Creates autocomplete entries
483
+ *
484
+ * @example Self-closing component
485
+ * ```ts
486
+ * defineComponent({
487
+ * tag: 'Badge',
488
+ * attrs: {
489
+ * type: { type: 'enum', options: ['success', 'warning', 'error', 'info'], default: 'info' },
490
+ * label: { type: 'string', default: 'Badge' },
491
+ * count: { type: 'number', default: 0 },
492
+ * },
493
+ * render: ({ type, label, count }) => {
494
+ * // ^ 'success' | 'warning' | 'error' | 'info'
495
+ * // ^ string ^ number — all strictly typed!
496
+ * return `<span class="badge badge-${type}">${label} (${count})</span>`;
497
+ * },
498
+ * })
499
+ * ```
500
+ *
501
+ * @example Block component (`selfClosing: false`)
502
+ * ```ts
503
+ * defineComponent({
504
+ * tag: 'Card',
505
+ * selfClosing: false,
506
+ * attrs: {
507
+ * title: { type: 'string', required: true, default: 'Card Title' },
508
+ * icon: { type: 'string' },
509
+ * href: { type: 'string' },
510
+ * },
511
+ * render: ({ title, icon, href, children }) =>
512
+ * `<div class="card"><h3>${icon ?? ''} ${title}</h3>${children}</div>`,
513
+ * })
514
+ * ```
515
+ */
516
+ export interface ComponentDefinition<S extends Record<string, AnyAttrDef> = Record<string, AttrDef>> {
517
+ /** JSX tag name, e.g. `'Badge'`, `'Card'`, `'CalloutBox'`. */
518
+ readonly tag: string;
519
+ /** Human-readable label for autocomplete. Falls back to `tag` when omitted. */
520
+ readonly label?: string;
521
+ /** Short description shown in the autocomplete popup detail / tooltip. */
522
+ readonly description?: string;
523
+ /**
524
+ * Attribute schema — keys are attribute names, values are their type
525
+ * definitions. Attributes absent from the schema are forwarded as strings.
526
+ */
527
+ readonly attrs?: S;
528
+ /**
529
+ * `true` → self-closing inline component: `<Tag attr="val" />` *(default)*
530
+ * `false` → block component that wraps children: `<Tag>…</Tag>`
531
+ */
532
+ readonly selfClosing?: boolean;
533
+ /**
534
+ * Transforms the parsed, typed props into an HTML string.
535
+ * TypeScript infers the exact type of each prop from `attrs`.
536
+ */
537
+ readonly render: (props: InferComponentProps<S>) => string;
538
+ /** Renderer priority — higher values run before lower ones. Default: `10`. */
539
+ readonly priority?: number;
540
+ /**
541
+ * Autocomplete completions co-located with this component — typically
542
+ * snippet variants for the different attribute combinations.
543
+ * PluginManager merges these automatically; no separate plugin-level
544
+ * `completions` array needed.
545
+ */
546
+ readonly completions?: CompletionItem[];
547
+ }
325
548
  export type RendererFn = (content: string, attrs?: Record<string, string>) => string;
326
549
  export interface RendererConfig {
327
550
  /** Name of the custom block/component */
328
551
  name: string;
552
+ /**
553
+ * JSX tag name — set automatically by `defineComponent()`.
554
+ * Used internally by the PluginManager to auto-register syntax-highlight
555
+ * tokens and autocomplete entries. Leave unset for raw regex renderers.
556
+ */
557
+ tag?: string;
558
+ /**
559
+ * Human-readable label shown in the autocomplete popup.
560
+ * Falls back to `tag` when omitted.
561
+ */
562
+ label?: string;
563
+ /**
564
+ * Short description shown as the autocomplete detail / tooltip.
565
+ * Falls back to `<Tag> component` when omitted.
566
+ */
567
+ description?: string;
568
+ /**
569
+ * Autocomplete completions co-located with this component.
570
+ * Typically snippet entries for different variants of the tag.
571
+ * PluginManager merges these into the plugin-level completion list
572
+ * automatically — no separate top-level `completions` array needed.
573
+ *
574
+ * @example
575
+ * ```ts
576
+ * defineComponent({
577
+ * tag: 'Badge',
578
+ * completions: [
579
+ * { label: 'badge-success', kind: 'snip', body: '<Badge type="success" label="$1" />' },
580
+ * { label: 'badge-error', kind: 'snip', body: '<Badge type="error" label="$1" />' },
581
+ * ],
582
+ * ...
583
+ * })
584
+ * ```
585
+ */
586
+ completions?: CompletionItem[];
329
587
  /** Regex pattern to match blocks in markdown */
330
588
  pattern: RegExp;
331
589
  /** Render function: matched content → HTML */
332
590
  render: RendererFn;
333
591
  /** Priority (higher = processed first) */
334
592
  priority?: number;
593
+ /**
594
+ * Optional per-pattern line-level token provider for syntax highlighting.
595
+ *
596
+ * Co-locate the tokeniser with its pattern so the pattern entry is fully
597
+ * self-contained — no separate top-level `provideTokens` needed.
598
+ * PluginManager automatically composes all per-pattern providers together
599
+ * with the plugin-level `provideTokens` (if any).
600
+ *
601
+ * @example
602
+ * ```ts
603
+ * patterns: [{
604
+ * name: 'myfence',
605
+ * pattern: /```myfence\n([\s\S]*?)```/g,
606
+ * render: (m) => `<div>${m[1]}</div>`,
607
+ * provideTokens: (line) => {
608
+ * if (line.startsWith('```myfence'))
609
+ * return [{ cls: 'kw', start: 0, end: 10 }];
610
+ * return [];
611
+ * },
612
+ * }],
613
+ * ```
614
+ */
615
+ provideTokens?: PluginTokenProvider;
335
616
  }
336
617
  export interface ParserConfig {
337
618
  /** Parser name */
@@ -1,4 +1,5 @@
1
1
  import type { EditorPlugin } from '../../src/core/types';
2
+ import { defineComponent } from '../../src/core/define-component';
2
3
 
3
4
  /**
4
5
  * Example custom plugin: Badge
@@ -67,23 +68,20 @@ export function badgePlugin(): EditorPlugin {
67
68
  },
68
69
  ],
69
70
 
70
- // ── Custom renderer (markdown → HTML in preview) ─────
71
- renderers: [
72
- {
73
- name: 'badge',
74
- pattern: /<Badge\s+type="([^"]*)"(?:\s+label="([^"]*)")?\s*\/>/g,
75
- render: (match) => {
76
- const m = match.match(
77
- /<Badge\s+type="([^"]*)"(?:\s+label="([^"]*)")?\s*\/>/
78
- );
79
- if (!m) return match;
80
-
81
- const type = m[1] || 'info';
82
- const label = m[2] || type;
83
-
84
- return `<span class="smdx-badge smdx-badge-${type}">${label}</span>`;
71
+ // ── Declarative component ─────────────────────────────
72
+ // tokens, pattern matching, and attribute parsing are handled
73
+ // automatically — no regex, no provideTokens needed.
74
+ components: [
75
+ defineComponent({
76
+ tag: 'Badge',
77
+ selfClosing: true,
78
+ attrs: {
79
+ type: { type: 'enum', options: ['success', 'warning', 'error', 'info'] as const, default: 'info' },
80
+ label: { type: 'string', default: 'Badge' },
85
81
  },
86
- },
82
+ render: ({ type, label }) =>
83
+ `<span class="smdx-badge smdx-badge-${type}">${label}</span>`,
84
+ }),
87
85
  ],
88
86
 
89
87
  // ── Scoped CSS ───────────────────────────────────────
package/dist/index.d.ts CHANGED
@@ -7,7 +7,8 @@ export { Toolbar } from './core/toolbar';
7
7
  export { Renderer } from './core/renderer';
8
8
  export { History } from './core/history';
9
9
  export { icons } from './core/icons';
10
- export type { EditorConfig, EditorPlugin, EditorAPI, EditorMode, PluginContext, ToolbarConfig, ToolbarRow, ToolbarGroup, ToolbarDivider, ToolbarItemConfig, ToolbarAction, ToolbarActionContext, SelectionState, RendererFn, RendererConfig, ParserConfig, ShortcutConfig, EventHandler, EditorLocale, CompletionItem, CompletionKind, TokenSegment, PluginTokenProvider, } from './core/types';
10
+ export { defineComponent, parseInnerComponents, buildInsertTemplate } from './core/define-component';
11
+ export type { EditorConfig, EditorPlugin, EditorAPI, EditorMode, PluginContext, ToolbarConfig, ToolbarRow, ToolbarGroup, ToolbarDivider, AttrDef, AnyAttrDef, StringAttrDef, NumberAttrDef, BooleanAttrDef, EnumAttrDef, InferAttrValue, InferComponentProps, ComponentProps, ComponentDefinition, ToolbarItemConfig, ToolbarAction, ToolbarActionContext, SelectionState, RendererFn, RendererConfig, ParserConfig, ShortcutConfig, EventHandler, EditorLocale, CompletionItem, CompletionKind, TokenSegment, PluginTokenProvider, } from './core/types';
11
12
  export { componentTagTokens, mdxBaseTokens, composeTokenProviders } from './plugins/token-utils';
12
13
  export { headingPlugin, boldPlugin, italicPlugin, strikethroughPlugin, quotePlugin, linkPlugin, imagePlugin, codePlugin, unorderedListPlugin, orderedListPlugin, taskListPlugin, tablePlugin, highlightPlugin, admonitionPlugin, tabPlugin, imageBackgroundPlugin, imageFramePlugin, accordionPlugin, accordionGroupPlugin, multiColumnPlugin, cardPlugin, cardGroupPlugin, stepPlugin, tipPlugin, containerPlugin, copyTextPlugin, tooltipPlugin, embedVideoPlugin, embedOthersPlugin, mermaidPlugin, emojiPlugin, formulaPlugin, insertPlugin, tocPlugin, breakLinePlugin, horizontalRulePlugin, allPlugins, defaultToolbar, } from './plugins';
13
14
  export { Renderer as RendererPipeline } from './core/renderer/Renderer';