@synclineapi/mdx-editor 1.0.2 → 2.0.0
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 +77 -36
- package/dist/core/define-component.d.ts +124 -0
- package/dist/core/types.d.ts +285 -4
- package/dist/examples/badge-plugin.ts +14 -16
- package/dist/index.d.ts +2 -1
- package/dist/style.css +1 -1
- package/dist/syncline-mdx-editor.js +1365 -1255
- package/dist/syncline-mdx-editor.umd.cjs +98 -111
- package/package.json +1 -1
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('
|
|
363
|
+
editor.insertBlock(':::myblock\nContent\n:::');
|
|
308
364
|
},
|
|
309
365
|
}],
|
|
310
366
|
|
|
311
|
-
//
|
|
312
|
-
|
|
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:
|
|
370
|
+
pattern: /:::myblock\n([\s\S]*?):::/g,
|
|
328
371
|
render: (match) => {
|
|
329
|
-
const m = match.match(
|
|
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('
|
|
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 {};
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
45
|
-
|
|
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
|
-
// ──
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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';
|