@sveltia/ui 0.41.1 → 0.41.2

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.
@@ -1,6 +1,18 @@
1
1
  /**
2
- * @import { TextEditorBlockType, TextEditorFormatType, TextEditorInlineType } from '../../typedefs';
2
+ * @import { EditorThemeClasses } from 'lexical';
3
+ * @import { Transformer } from '@lexical/markdown';
4
+ * @import {
5
+ * TextEditorBlockType,
6
+ * TextEditorFormatType,
7
+ * TextEditorInlineType,
8
+ * TextEditorNodeType,
9
+ * } from '../../typedefs';
3
10
  */
11
+ export const PRISM_BASE_URL: "https://unpkg.com/prismjs@1.30.0";
12
+ /**
13
+ * @type {EditorThemeClasses}
14
+ */
15
+ export const EDITOR_THEME: EditorThemeClasses;
4
16
  /**
5
17
  * List of available buttons.
6
18
  * @type {{ [key: string]: { labelKey: string, icon: string, inline: boolean } }}
@@ -28,6 +40,21 @@ export const BLOCK_BUTTON_TYPES: TextEditorBlockType[];
28
40
  * Image related components IDs. `linked-image` is used in Sveltia CMS.
29
41
  */
30
42
  export const IMAGE_COMPONENT_IDS: string[];
43
+ /**
44
+ * Map of Lexical nodes for each block type. The `paragraph` block type is excluded because it is
45
+ * the default block type.
46
+ * @type {Record<Exclude<TextEditorBlockType | 'link', 'paragraph'>, any[]>}
47
+ */
48
+ export const NODE_MAP: Record<Exclude<TextEditorBlockType | "link", "paragraph">, any[]>;
49
+ /**
50
+ * Map of Lexical transformers for each block type. The `paragraph` block type is excluded because
51
+ * it is the default block type.
52
+ * @type {Record<Exclude<TextEditorNodeType, 'paragraph'>, Transformer[]>}
53
+ */
54
+ export const TRANSFORMER_MAP: Record<Exclude<TextEditorNodeType, "paragraph">, Transformer[]>;
55
+ import type { EditorThemeClasses } from 'lexical';
31
56
  import type { TextEditorFormatType } from '../../typedefs';
32
57
  import type { TextEditorInlineType } from '../../typedefs';
33
58
  import type { TextEditorBlockType } from '../../typedefs';
59
+ import type { TextEditorNodeType } from '../../typedefs';
60
+ import type { Transformer } from '@lexical/markdown';
@@ -1,6 +1,87 @@
1
+ import { CodeHighlightNode, CodeNode } from '@lexical/code';
2
+ import { LinkNode } from '@lexical/link';
3
+ import { ListItemNode, ListNode } from '@lexical/list';
4
+ import {
5
+ BOLD_ITALIC_STAR,
6
+ BOLD_ITALIC_UNDERSCORE,
7
+ BOLD_STAR,
8
+ CODE,
9
+ HEADING,
10
+ INLINE_CODE,
11
+ ITALIC_UNDERSCORE,
12
+ LINK,
13
+ ORDERED_LIST,
14
+ QUOTE,
15
+ STRIKETHROUGH,
16
+ UNORDERED_LIST,
17
+ } from '@lexical/markdown';
18
+ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
19
+
20
+ /**
21
+ * @import { EditorThemeClasses } from 'lexical';
22
+ * @import { Transformer } from '@lexical/markdown';
23
+ * @import {
24
+ * TextEditorBlockType,
25
+ * TextEditorFormatType,
26
+ * TextEditorInlineType,
27
+ * TextEditorNodeType,
28
+ * } from '../../typedefs';
29
+ */
30
+
31
+ export const PRISM_BASE_URL = `https://unpkg.com/prismjs@1.30.0`;
32
+
1
33
  /**
2
- * @import { TextEditorBlockType, TextEditorFormatType, TextEditorInlineType } from '../../typedefs';
34
+ * @type {EditorThemeClasses}
3
35
  */
36
+ export const EDITOR_THEME = {
37
+ text: {
38
+ /**
39
+ * Enable bold+italic and strikethrough styling.
40
+ * @see https://github.com/facebook/lexical/discussions/4381
41
+ */
42
+ italic: 'italic',
43
+ strikethrough: 'strikethrough',
44
+ },
45
+ list: {
46
+ nested: {
47
+ listitem: 'nested',
48
+ },
49
+ },
50
+ code: 'code-block',
51
+ // https://github.com/facebook/lexical/blob/main/packages/lexical-website/docs/getting-started/theming.md
52
+ codeHighlight: {
53
+ atrule: 'token atrule',
54
+ attr: 'token attr',
55
+ boolean: 'token boolean',
56
+ builtin: 'token builtin',
57
+ cdata: 'token cdata',
58
+ char: 'token char',
59
+ class: 'token class',
60
+ 'class-name': 'token class-name',
61
+ comment: 'token comment',
62
+ constant: 'token constant',
63
+ deleted: 'token deleted',
64
+ doctype: 'token doctype',
65
+ entity: 'token entity',
66
+ function: 'token function',
67
+ important: 'token important',
68
+ inserted: 'token inserted',
69
+ keyword: 'token keyword',
70
+ namespace: 'token namespace',
71
+ number: 'token number',
72
+ operator: 'token operator',
73
+ prolog: 'token prolog',
74
+ property: 'token property',
75
+ punctuation: 'token punctuation',
76
+ regex: 'token regex',
77
+ selector: 'token selector',
78
+ string: 'token string',
79
+ symbol: 'token symbol',
80
+ tag: 'token tag',
81
+ url: 'token url',
82
+ variable: 'token variable',
83
+ },
84
+ };
4
85
 
5
86
  /**
6
87
  * List of available buttons.
@@ -120,3 +201,49 @@ export const BLOCK_BUTTON_TYPES = [
120
201
  * Image related components IDs. `linked-image` is used in Sveltia CMS.
121
202
  */
122
203
  export const IMAGE_COMPONENT_IDS = ['image', 'linked-image'];
204
+
205
+ /**
206
+ * Map of Lexical nodes for each block type. The `paragraph` block type is excluded because it is
207
+ * the default block type.
208
+ * @type {Record<Exclude<TextEditorBlockType | 'link', 'paragraph'>, any[]>}
209
+ */
210
+ export const NODE_MAP = {
211
+ 'heading-1': [HeadingNode],
212
+ 'heading-2': [HeadingNode],
213
+ 'heading-3': [HeadingNode],
214
+ 'heading-4': [HeadingNode],
215
+ 'heading-5': [HeadingNode],
216
+ 'heading-6': [HeadingNode],
217
+ 'bulleted-list': [ListNode, ListItemNode],
218
+ 'numbered-list': [ListNode, ListItemNode],
219
+ blockquote: [QuoteNode],
220
+ 'code-block': [CodeNode, CodeHighlightNode],
221
+ link: [LinkNode],
222
+ };
223
+
224
+ /**
225
+ * Map of Lexical transformers for each block type. The `paragraph` block type is excluded because
226
+ * it is the default block type.
227
+ * @type {Record<Exclude<TextEditorNodeType, 'paragraph'>, Transformer[]>}
228
+ */
229
+ export const TRANSFORMER_MAP = {
230
+ // ELEMENT_TRANSFORMERS
231
+ 'heading-1': [HEADING],
232
+ 'heading-2': [HEADING],
233
+ 'heading-3': [HEADING],
234
+ 'heading-4': [HEADING],
235
+ 'heading-5': [HEADING],
236
+ 'heading-6': [HEADING],
237
+ 'bulleted-list': [UNORDERED_LIST],
238
+ 'numbered-list': [ORDERED_LIST],
239
+ blockquote: [QUOTE],
240
+ // MULTILINE_ELEMENT_TRANSFORMERS
241
+ 'code-block': [CODE],
242
+ // TEXT_FORMAT_TRANSFORMERS
243
+ code: [INLINE_CODE],
244
+ bold: [BOLD_ITALIC_STAR, BOLD_ITALIC_UNDERSCORE, BOLD_STAR], // Exclude BOLD_UNDERSCORE
245
+ italic: [BOLD_ITALIC_STAR, BOLD_ITALIC_UNDERSCORE, ITALIC_UNDERSCORE], // Exclude ITALIC_STAR
246
+ strikethrough: [STRIKETHROUGH],
247
+ // TEXT_MATCH_TRANSFORMERS
248
+ link: [LINK],
249
+ };
@@ -1,12 +1,24 @@
1
1
  export function getSelectionTypes(): TextEditorSelectionState;
2
- export function onEditorUpdate(editor: LexicalEditor): void;
3
- export function initEditor({ components, useMarkdownShortcuts, isCodeEditor, defaultLanguage, }: TextEditorConfig): {
2
+ export function onEditorUpdate(editor: LexicalEditor, enabledTransformers: Transformer[]): void;
3
+ export function initEditor({ enabledButtons, components, useMarkdownShortcuts, isCodeEditor, defaultLanguage, }: TextEditorConfig): InitEditorResult;
4
+ export function loadCodeHighlighter(lang: string): Promise<void>;
5
+ export function convertMarkdownToLexical(editor: LexicalEditor, value: string, enabledTransformers: Transformer[]): Promise<void>;
6
+ export function focusEditor(editor: LexicalEditor): Promise<void>;
7
+ export type InitEditorResult = {
8
+ /**
9
+ * Editor instance.
10
+ */
4
11
  editor: LexicalEditor;
12
+ /**
13
+ * List of enabled Markdown transformers.
14
+ */
15
+ enabledTransformers: Transformer[];
16
+ /**
17
+ * Remove all registered Lexical listeners.
18
+ */
5
19
  dispose: () => void;
6
20
  };
7
- export function loadCodeHighlighter(lang: string): Promise<void>;
8
- export function convertMarkdownToLexical(editor: LexicalEditor, value: string): Promise<void>;
9
- export function focusEditor(editor: LexicalEditor): Promise<void>;
10
21
  import type { TextEditorSelectionState } from '../../typedefs';
11
22
  import type { LexicalEditor } from 'lexical';
23
+ import type { Transformer } from '@lexical/markdown';
12
24
  import type { TextEditorConfig } from '../../typedefs';
@@ -3,8 +3,6 @@
3
3
  import 'prismjs';
4
4
 
5
5
  import {
6
- CodeHighlightNode,
7
- CodeNode,
8
6
  $createCodeNode as createCodeNode,
9
7
  $isCodeHighlightNode as isCodeHighlightNode,
10
8
  $isCodeNode as isCodeNode,
@@ -15,7 +13,6 @@ import { HorizontalRuleNode } from '@lexical/extension';
15
13
  import { createEmptyHistoryState, registerHistory } from '@lexical/history';
16
14
  import {
17
15
  $isLinkNode as isLinkNode,
18
- LinkNode,
19
16
  TOGGLE_LINK_COMMAND,
20
17
  $toggleLink as toggleLink,
21
18
  } from '@lexical/link';
@@ -26,20 +23,16 @@ import {
26
23
  $insertList as insertList,
27
24
  $isListItemNode as isListItemNode,
28
25
  $isListNode as isListNode,
29
- ListItemNode,
30
26
  ListNode,
31
27
  } from '@lexical/list';
32
28
  import {
33
29
  $convertFromMarkdownString as convertFromMarkdownString,
34
30
  $convertToMarkdownString as convertToMarkdownString,
35
31
  registerMarkdownShortcuts,
36
- TRANSFORMERS,
37
32
  } from '@lexical/markdown';
38
33
  import {
39
- HeadingNode,
40
34
  $isHeadingNode as isHeadingNode,
41
35
  $isQuoteNode as isQuoteNode,
42
- QuoteNode,
43
36
  registerRichText,
44
37
  } from '@lexical/rich-text';
45
38
  import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
@@ -57,93 +50,36 @@ import {
57
50
  OUTDENT_CONTENT_COMMAND,
58
51
  } from 'lexical';
59
52
  import prismComponents from 'prismjs/components';
60
- import { BLOCK_BUTTON_TYPES, TEXT_FORMAT_BUTTON_TYPES } from './constants.js';
53
+ import {
54
+ BLOCK_BUTTON_TYPES,
55
+ EDITOR_THEME,
56
+ NODE_MAP,
57
+ PRISM_BASE_URL,
58
+ TEXT_FORMAT_BUTTON_TYPES,
59
+ TRANSFORMER_MAP,
60
+ } from './constants.js';
61
61
  import { increaseListIndentation, splitMultilineFormatting } from './markdown.js';
62
62
  import { HR } from './transformers/hr.js';
63
63
  import { TABLE } from './transformers/table.js';
64
64
 
65
65
  /**
66
66
  * @import { CreateEditorArgs, LexicalEditor } from 'lexical';
67
+ * @import { Transformer } from '@lexical/markdown';
67
68
  * @import {
68
69
  * TextEditorBlockType,
69
70
  * TextEditorConfig,
70
71
  * TextEditorInlineType,
72
+ * TextEditorNodeType,
71
73
  * TextEditorSelectionState,
72
74
  * } from '../../typedefs';
73
75
  */
74
76
 
75
- const allTransformers = [...TRANSFORMERS, HR, TABLE];
76
- const prismBaseURL = `https://unpkg.com/prismjs@1.30.0`;
77
-
78
77
  /**
79
- * Lexical editor configuration.
80
- * @type {CreateEditorArgs}
78
+ * @typedef {object} InitEditorResult
79
+ * @property {LexicalEditor} editor Editor instance.
80
+ * @property {Transformer[]} enabledTransformers List of enabled Markdown transformers.
81
+ * @property {() => void} dispose Remove all registered Lexical listeners.
81
82
  */
82
- const editorConfig = {
83
- namespace: 'editor',
84
- nodes: [
85
- HeadingNode,
86
- QuoteNode,
87
- LinkNode,
88
- ListNode,
89
- ListItemNode,
90
- CodeNode,
91
- CodeHighlightNode,
92
- HorizontalRuleNode,
93
- TableNode,
94
- TableCellNode,
95
- TableRowNode,
96
- ],
97
- theme: {
98
- text: {
99
- /**
100
- * Enable bold+italic and strikethrough styling.
101
- * @see https://github.com/facebook/lexical/discussions/4381
102
- */
103
- italic: 'italic',
104
- strikethrough: 'strikethrough',
105
- },
106
- list: {
107
- nested: {
108
- listitem: 'nested',
109
- },
110
- },
111
- code: 'code-block',
112
- // https://github.com/facebook/lexical/blob/main/packages/lexical-website/docs/getting-started/theming.md
113
- codeHighlight: {
114
- atrule: 'token atrule',
115
- attr: 'token attr',
116
- boolean: 'token boolean',
117
- builtin: 'token builtin',
118
- cdata: 'token cdata',
119
- char: 'token char',
120
- class: 'token class',
121
- 'class-name': 'token class-name',
122
- comment: 'token comment',
123
- constant: 'token constant',
124
- deleted: 'token deleted',
125
- doctype: 'token doctype',
126
- entity: 'token entity',
127
- function: 'token function',
128
- important: 'token important',
129
- inserted: 'token inserted',
130
- keyword: 'token keyword',
131
- namespace: 'token namespace',
132
- number: 'token number',
133
- operator: 'token operator',
134
- prolog: 'token prolog',
135
- property: 'token property',
136
- punctuation: 'token punctuation',
137
- regex: 'token regex',
138
- selector: 'token selector',
139
- string: 'token string',
140
- symbol: 'token symbol',
141
- tag: 'token tag',
142
- url: 'token url',
143
- variable: 'token variable',
144
- },
145
- },
146
- };
147
83
 
148
84
  /**
149
85
  * Get the current selection’s block node key as well as block and inline level types.
@@ -223,15 +159,14 @@ export const getSelectionTypes = () => {
223
159
  * Listen to changes made on the editor and trigger the Update event.
224
160
  * @internal
225
161
  * @param {LexicalEditor} editor Editor instance.
162
+ * @param {Transformer[]} enabledTransformers Enabled Markdown transformers.
226
163
  */
227
- export const onEditorUpdate = (editor) => {
164
+ export const onEditorUpdate = (editor, enabledTransformers) => {
228
165
  editor.getRootElement()?.dispatchEvent(
229
166
  new CustomEvent('Update', {
230
167
  detail: {
231
- value: convertToMarkdownString(
232
- // Use underscores for italic text in Markdown instead of asterisks
233
- allTransformers.filter((/** @type {any} */ { tag }) => tag !== '*'),
234
- ) // Remove unnecessary backslash for underscore and backslash characters
168
+ value: convertToMarkdownString(enabledTransformers)
169
+ // Remove unnecessary backslash for underscore and backslash characters
235
170
  // @see https://github.com/sveltia/sveltia-cms/issues/430
236
171
  // @see https://github.com/sveltia/sveltia-cms/issues/512
237
172
  .replace(/\\([_\\])/g, '$1')
@@ -249,18 +184,44 @@ export const onEditorUpdate = (editor) => {
249
184
  /**
250
185
  * Initialize the Lexical editor.
251
186
  * @param {TextEditorConfig} config Editor configuration.
252
- * @returns {{ editor: LexicalEditor, dispose: () => void }} Editor instance and cleanup.
187
+ * @returns {InitEditorResult} Editor instance and cleanup.
253
188
  */
254
189
  export const initEditor = ({
190
+ enabledButtons = [],
255
191
  components = [],
256
192
  useMarkdownShortcuts,
257
193
  isCodeEditor = false,
258
194
  defaultLanguage = 'plain',
259
195
  }) => {
260
- components.forEach(({ node, transformer }) => {
261
- /** @type {any[]} */ (editorConfig.nodes).unshift(node);
262
- allTransformers.unshift(transformer);
263
- });
196
+ /** @type {CreateEditorArgs} */
197
+ const editorConfig = {
198
+ namespace: 'editor',
199
+ nodes: [
200
+ ...components.map(({ node }) => node),
201
+ ...new Set(
202
+ Object.entries(NODE_MAP)
203
+ .filter(([button]) => enabledButtons.includes(/** @type {TextEditorNodeType} */ (button)))
204
+ .flatMap(([, nodes]) => nodes),
205
+ ),
206
+ HorizontalRuleNode,
207
+ TableNode,
208
+ TableCellNode,
209
+ TableRowNode,
210
+ ],
211
+ theme: EDITOR_THEME,
212
+ };
213
+
214
+ /** @type {Transformer[]} */
215
+ const enabledTransformers = [
216
+ ...components.map(({ transformer }) => transformer),
217
+ ...new Set(
218
+ Object.entries(TRANSFORMER_MAP)
219
+ .filter(([button]) => enabledButtons.includes(/** @type {TextEditorNodeType} */ (button)))
220
+ .flatMap(([, transformers]) => transformers),
221
+ ),
222
+ HR,
223
+ TABLE,
224
+ ];
264
225
 
265
226
  const editor = createEditor(editorConfig);
266
227
  /** @type {Array<() => void>} */
@@ -282,63 +243,73 @@ export const initEditor = ({
282
243
  addUnregister(registerHistory(editor, createEmptyHistoryState(), 1000));
283
244
 
284
245
  if (useMarkdownShortcuts) {
285
- addUnregister(registerMarkdownShortcuts(editor, allTransformers));
246
+ addUnregister(registerMarkdownShortcuts(editor, enabledTransformers));
286
247
  }
287
248
 
288
- addUnregister(
289
- registerCodeHighlighting(editor, {
290
- defaultLanguage,
291
- // eslint-disable-next-line jsdoc/require-jsdoc
292
- tokenize: (code, lang = 'plain') =>
293
- window.Prism.tokenize(code, window.Prism.languages[lang] ?? window.Prism.languages.plain),
294
- $tokenize: PrismTokenizer.$tokenize,
295
- }),
296
- );
297
-
298
- addUnregister(
299
- editor.registerCommand(
300
- TOGGLE_LINK_COMMAND,
301
- (payload) => {
302
- toggleLink(typeof payload === 'string' ? payload : null);
303
-
304
- return true;
305
- },
306
- COMMAND_PRIORITY_NORMAL,
307
- ),
308
- );
309
-
310
- addUnregister(
311
- editor.registerCommand(
312
- INSERT_UNORDERED_LIST_COMMAND,
313
- () => {
314
- insertList('bullet');
249
+ if (enabledButtons.includes('code-block') || isCodeEditor) {
250
+ addUnregister(
251
+ registerCodeHighlighting(editor, {
252
+ defaultLanguage,
253
+ // eslint-disable-next-line jsdoc/require-jsdoc
254
+ tokenize: (code, lang = 'plain') =>
255
+ window.Prism.tokenize(code, window.Prism.languages[lang] ?? window.Prism.languages.plain),
256
+ $tokenize: PrismTokenizer.$tokenize,
257
+ }),
258
+ );
259
+ }
315
260
 
316
- return true;
317
- },
318
- COMMAND_PRIORITY_NORMAL,
319
- ),
320
- );
261
+ if (enabledButtons.includes('link')) {
262
+ addUnregister(
263
+ editor.registerCommand(
264
+ TOGGLE_LINK_COMMAND,
265
+ (payload) => {
266
+ toggleLink(typeof payload === 'string' ? payload : null);
267
+
268
+ return true;
269
+ },
270
+ COMMAND_PRIORITY_NORMAL,
271
+ ),
272
+ );
273
+ }
321
274
 
322
- addUnregister(
323
- editor.registerCommand(
324
- INSERT_ORDERED_LIST_COMMAND,
325
- () => {
326
- insertList('number');
275
+ if (enabledButtons.includes('bulleted-list')) {
276
+ addUnregister(
277
+ editor.registerCommand(
278
+ INSERT_UNORDERED_LIST_COMMAND,
279
+ () => {
280
+ insertList('bullet');
281
+
282
+ return true;
283
+ },
284
+ COMMAND_PRIORITY_NORMAL,
285
+ ),
286
+ );
287
+ }
327
288
 
328
- return true;
329
- },
330
- COMMAND_PRIORITY_NORMAL,
331
- ),
332
- );
289
+ if (enabledButtons.includes('numbered-list')) {
290
+ addUnregister(
291
+ editor.registerCommand(
292
+ INSERT_ORDERED_LIST_COMMAND,
293
+ () => {
294
+ insertList('number');
295
+
296
+ return true;
297
+ },
298
+ COMMAND_PRIORITY_NORMAL,
299
+ ),
300
+ );
301
+ }
333
302
 
334
- // https://github.com/facebook/lexical/blob/main/packages/lexical-react/src/shared/useList.ts
335
- addUnregister(
336
- editor.registerCommand(
337
- INSERT_PARAGRAPH_COMMAND,
338
- () => handleListInsertParagraph(),
339
- COMMAND_PRIORITY_NORMAL,
340
- ),
341
- );
303
+ if (enabledButtons.includes('bulleted-list') || enabledButtons.includes('numbered-list')) {
304
+ // https://github.com/facebook/lexical/blob/main/packages/lexical-react/src/shared/useList.ts
305
+ addUnregister(
306
+ editor.registerCommand(
307
+ INSERT_PARAGRAPH_COMMAND,
308
+ () => handleListInsertParagraph(),
309
+ COMMAND_PRIORITY_NORMAL,
310
+ ),
311
+ );
312
+ }
342
313
 
343
314
  addUnregister(
344
315
  editor.registerUpdateListener(() => {
@@ -369,7 +340,7 @@ export const initEditor = ({
369
340
  }
370
341
  }
371
342
 
372
- onEditorUpdate(editor);
343
+ onEditorUpdate(editor, enabledTransformers);
373
344
  });
374
345
  })();
375
346
  }),
@@ -424,6 +395,7 @@ export const initEditor = ({
424
395
 
425
396
  return {
426
397
  editor,
398
+ enabledTransformers,
427
399
  /**
428
400
  * Remove all registered Lexical listeners.
429
401
  */
@@ -455,7 +427,7 @@ export const loadCodeHighlighter = async (lang) => {
455
427
 
456
428
  try {
457
429
  // eslint-disable-next-line jsdoc/no-bad-blocks
458
- await import(/* @vite-ignore */ `${prismBaseURL}/components/prism-${canonicalLang}.min.js`);
430
+ await import(/* @vite-ignore */ `${PRISM_BASE_URL}/components/prism-${canonicalLang}.min.js`);
459
431
  } catch {
460
432
  //
461
433
  }
@@ -465,10 +437,11 @@ export const loadCodeHighlighter = async (lang) => {
465
437
  * Convert Markdown to Lexical nodes.
466
438
  * @param {LexicalEditor} editor Editor instance.
467
439
  * @param {string} value Current Markdown value.
440
+ * @param {Transformer[]} enabledTransformers List of enabled Markdown transformers.
468
441
  * @returns {Promise<void>} Nothing.
469
442
  * @throws {Error} Failed to convert the value to Lexical nodes.
470
443
  */
471
- export const convertMarkdownToLexical = async (editor, value) => {
444
+ export const convertMarkdownToLexical = async (editor, value, enabledTransformers) => {
472
445
  // Load Prism language support on demand; the `loadLanguages` Prism utility method cannot be used
473
446
  await Promise.all(
474
447
  [...value.matchAll(/^```(?<lang>.+?)\n/gm)].map(async ({ groups: { lang = 'plain' } = {} }) =>
@@ -485,7 +458,7 @@ export const convertMarkdownToLexical = async (editor, value) => {
485
458
  return new Promise((resolve, reject) => {
486
459
  editor.update(() => {
487
460
  try {
488
- convertFromMarkdownString(value, allTransformers);
461
+ convertFromMarkdownString(value, enabledTransformers);
489
462
  resolve(undefined);
490
463
  } catch (ex) {
491
464
  reject(new Error('Failed to convert Markdown', { cause: ex }));
@@ -88,9 +88,10 @@
88
88
  };
89
89
 
90
90
  onMount(() => {
91
- const { editor, dispose } = initEditor(editorStore.config);
91
+ const { editor, enabledTransformers, dispose } = initEditor(editorStore.config);
92
92
 
93
93
  editorStore.editor = editor;
94
+ editorStore.enabledTransformers = enabledTransformers;
94
95
 
95
96
  lexicalRoot?.addEventListener('Update', onUpdate);
96
97
  lexicalRoot?.addEventListener('click', onClick);
@@ -2,8 +2,9 @@ import { generateElementId } from '@sveltia/utils/element';
2
2
  import { convertMarkdownToLexical } from './core.js';
3
3
 
4
4
  /**
5
- * @import { TextEditorConfig, TextEditorSelectionState, TextEditorStore } from '../../typedefs';
6
5
  * @import { LexicalEditor } from 'lexical';
6
+ * @import { Transformer } from '@lexical/markdown';
7
+ * @import { TextEditorConfig, TextEditorSelectionState, TextEditorStore } from '../../typedefs';
7
8
  */
8
9
 
9
10
  /**
@@ -17,6 +18,8 @@ export const createEditorStore = () => {
17
18
  let initialized = $state(false);
18
19
  /** @type {LexicalEditor | undefined} */
19
20
  let editor = $state();
21
+ /** @type {Transformer[]} */
22
+ let enabledTransformers = $state([]);
20
23
 
21
24
  /** @type {TextEditorConfig} */
22
25
  let config = $state({
@@ -53,7 +56,7 @@ export const createEditorStore = () => {
53
56
  // We should avoid an empty editor; there should be at least one `<p>`, so give it an empty
54
57
  // string if the `value` is `undefined`
55
58
  // @see https://github.com/facebook/lexical/issues/2308
56
- await convertMarkdownToLexical(editor, inputValue || '');
59
+ await convertMarkdownToLexical(editor, inputValue || '', enabledTransformers);
57
60
  } catch (ex) {
58
61
  hasConverterError = true;
59
62
  inputValue = originalValue;
@@ -70,6 +73,12 @@ export const createEditorStore = () => {
70
73
  set editor(newValue) {
71
74
  editor = newValue;
72
75
  },
76
+ set enabledTransformers(newValue) {
77
+ enabledTransformers = newValue;
78
+ },
79
+ get enabledTransformers() {
80
+ return enabledTransformers;
81
+ },
73
82
  get initialized() {
74
83
  return initialized;
75
84
  },
@@ -15,12 +15,7 @@
15
15
 
16
16
  /**
17
17
  * @import { Snippet } from 'svelte';
18
- * @import {
19
- * TextEditorBlockType,
20
- * TextEditorComponent,
21
- * TextEditorInlineType,
22
- * TextEditorMode,
23
- * } from '../../typedefs';
18
+ * @import { TextEditorComponent, TextEditorMode, TextEditorNodeType } from '../../typedefs';
24
19
  */
25
20
 
26
21
  /**
@@ -29,7 +24,7 @@
29
24
  * @property {boolean} [flex] Make the text input container flexible.
30
25
  * @property {'ltr' | 'rtl' | 'auto'} [dir] The `dir` attribute on the `<textarea>` element.
31
26
  * @property {TextEditorMode[]} [modes] Enabled modes.
32
- * @property {(TextEditorBlockType | TextEditorInlineType)[]} [buttons] Enabled buttons.
27
+ * @property {TextEditorNodeType[]} [buttons] Enabled buttons.
33
28
  * @property {TextEditorComponent[]} [components] Editor components.
34
29
  * @property {boolean} [useMarkdownShortcuts] Whether to enable Markdown keyboard shortcuts in the
35
30
  * rich text editor.
@@ -24,7 +24,7 @@ declare const TextEditor: import("svelte").Component<{
24
24
  /**
25
25
  * Enabled buttons.
26
26
  */
27
- buttons?: (TextEditorBlockType | TextEditorInlineType)[] | undefined;
27
+ buttons?: TextEditorNodeType[] | undefined;
28
28
  /**
29
29
  * Editor components.
30
30
  */
@@ -87,7 +87,7 @@ type Props = {
87
87
  /**
88
88
  * Enabled buttons.
89
89
  */
90
- buttons?: (TextEditorBlockType | TextEditorInlineType)[] | undefined;
90
+ buttons?: TextEditorNodeType[] | undefined;
91
91
  /**
92
92
  * Editor components.
93
93
  */
@@ -131,7 +131,6 @@ type Props = {
131
131
  children?: Snippet<[]> | undefined;
132
132
  };
133
133
  import type { TextEditorMode } from '../../typedefs';
134
- import type { TextEditorBlockType } from '../../typedefs';
135
- import type { TextEditorInlineType } from '../../typedefs';
134
+ import type { TextEditorNodeType } from '../../typedefs';
136
135
  import type { TextEditorComponent } from '../../typedefs';
137
136
  import type { Snippet } from 'svelte';
@@ -82,24 +82,27 @@
82
82
  </script>
83
83
 
84
84
  <ToolbarWrapper disabled={disabled || readonly} aria-label={_('_sui.text_editor.text_editor')}>
85
- <MenuButton
86
- disabled={!editorStore.useRichText}
87
- aria-label={_('_sui.text_editor.show_text_style_options')}
88
- aria-controls="{editorStore.editorId}-lexical-root"
89
- >
90
- {#snippet startIcon()}
91
- <Icon
92
- name={AVAILABLE_BUTTONS[editorStore.selection.blockType ?? '']?.icon ?? 'format_paragraph'}
93
- />
94
- {/snippet}
95
- {#snippet popup()}
96
- <Menu aria-label={_('_sui.text_editor.text_style_options')}>
97
- {#each blockLevelButtons as type (type)}
98
- <ToggleBlockMenuItem {type} />
99
- {/each}
100
- </Menu>
101
- {/snippet}
102
- </MenuButton>
85
+ {#if blockLevelButtons.length > 1}
86
+ <MenuButton
87
+ disabled={!editorStore.useRichText}
88
+ aria-label={_('_sui.text_editor.show_text_style_options')}
89
+ aria-controls="{editorStore.editorId}-lexical-root"
90
+ >
91
+ {#snippet startIcon()}
92
+ <Icon
93
+ name={AVAILABLE_BUTTONS[editorStore.selection.blockType ?? '']?.icon ??
94
+ 'format_paragraph'}
95
+ />
96
+ {/snippet}
97
+ {#snippet popup()}
98
+ <Menu aria-label={_('_sui.text_editor.text_style_options')}>
99
+ {#each blockLevelButtons as type (type)}
100
+ <ToggleBlockMenuItem {type} />
101
+ {/each}
102
+ </Menu>
103
+ {/snippet}
104
+ </MenuButton>
105
+ {/if}
103
106
  {#if editorStore.selection.blockType === 'code-block'}
104
107
  <Divider orientation="vertical" />
105
108
  <CodeLanguageSwitcher disabled={!editorStore.useRichText} />
@@ -664,6 +664,7 @@ export type ToastPosition = "auto" | "top-left" | "top-center" | "top-right" | "
664
664
  export type TextEditorBlockType = "paragraph" | "heading-1" | "heading-2" | "heading-3" | "heading-4" | "heading-5" | "heading-6" | "bulleted-list" | "numbered-list" | "blockquote" | "code-block";
665
665
  export type TextEditorFormatType = "bold" | "italic" | "strikethrough" | "code";
666
666
  export type TextEditorInlineType = TextEditorFormatType | "link";
667
+ export type TextEditorNodeType = TextEditorBlockType | TextEditorInlineType;
667
668
  export type TextEditorMode = "rich-text" | "plain-text";
668
669
  export type TextEditorComponent = {
669
670
  /**
@@ -701,7 +702,7 @@ export type TextEditorConfig = {
701
702
  * Enabled buttons for the
702
703
  * editor.
703
704
  */
704
- enabledButtons: (TextEditorBlockType | TextEditorInlineType)[];
705
+ enabledButtons: TextEditorNodeType[];
705
706
  /**
706
707
  * Editor components.
707
708
  */
@@ -739,6 +740,10 @@ export type TextEditorStore = {
739
740
  * Lexical editor instance.
740
741
  */
741
742
  editor: LexicalEditor | undefined;
743
+ /**
744
+ * Enabled Lexical transformers.
745
+ */
746
+ enabledTransformers: Transformer[];
742
747
  /**
743
748
  * Whether the Lexical editor is initialized.
744
749
  */
package/dist/typedefs.js CHANGED
@@ -270,6 +270,10 @@
270
270
  * @typedef {TextEditorFormatType | 'link'} TextEditorInlineType
271
271
  */
272
272
 
273
+ /**
274
+ * @typedef {TextEditorBlockType | TextEditorInlineType} TextEditorNodeType
275
+ */
276
+
273
277
  /**
274
278
  * @typedef {'rich-text' | 'plain-text'} TextEditorMode
275
279
  */
@@ -288,7 +292,7 @@
288
292
  /**
289
293
  * @typedef {object} TextEditorConfig
290
294
  * @property {TextEditorMode[]} modes Enabled modes.
291
- * @property {(TextEditorBlockType | TextEditorInlineType)[]} enabledButtons Enabled buttons for the
295
+ * @property {TextEditorNodeType[]} enabledButtons Enabled buttons for the
292
296
  * editor.
293
297
  * @property {TextEditorComponent[]} components Editor components.
294
298
  * @property {boolean} useMarkdownShortcuts Whether to enable Markdown keyboard shortcuts in the
@@ -307,6 +311,7 @@
307
311
  /**
308
312
  * @typedef {object} TextEditorStore
309
313
  * @property {LexicalEditor | undefined} editor Lexical editor instance.
314
+ * @property {Transformer[]} enabledTransformers Enabled Lexical transformers.
310
315
  * @property {boolean} initialized Whether the Lexical editor is initialized.
311
316
  * @property {string} editorId Random ID assigned to the editor.
312
317
  * @property {TextEditorConfig} config Editor configuration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltia/ui",
3
- "version": "0.41.1",
3
+ "version": "0.41.2",
4
4
  "description": "A collection of Svelte components and utilities for building user interfaces.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -55,19 +55,19 @@
55
55
  "@sveltejs/adapter-auto": "^7.0.1",
56
56
  "@sveltejs/kit": "^2.69.1",
57
57
  "@sveltejs/package": "^2.5.8",
58
- "@sveltejs/vite-plugin-svelte": "^7.1.2",
59
- "@vitest/coverage-v8": "^4.1.9",
58
+ "@sveltejs/vite-plugin-svelte": "^7.1.4",
59
+ "@vitest/coverage-v8": "^4.1.10",
60
60
  "cspell": "^10.0.1",
61
61
  "eslint": "^9.39.4",
62
62
  "eslint-config-airbnb-extended": "^3.1.0",
63
63
  "eslint-config-prettier": "^10.1.8",
64
64
  "eslint-plugin-import": "^2.32.0",
65
- "eslint-plugin-jsdoc": "^63.0.11",
65
+ "eslint-plugin-jsdoc": "^63.0.12",
66
66
  "eslint-plugin-package-json": "^1.5.0",
67
67
  "eslint-plugin-svelte": "^3.20.0",
68
68
  "globals": "^17.7.0",
69
69
  "happy-dom": "^20.10.6",
70
- "oxlint": "^1.72.0",
70
+ "oxlint": "^1.73.0",
71
71
  "postcss": "^8.5.16",
72
72
  "postcss-html": "^1.8.1",
73
73
  "prettier": "^3.9.4",
@@ -81,7 +81,7 @@
81
81
  "svelte-preprocess": "^6.0.5",
82
82
  "tslib": "^2.8.1",
83
83
  "vite": "^8.1.3",
84
- "vitest": "^4.1.9"
84
+ "vitest": "^4.1.10"
85
85
  },
86
86
  "peerDependencies": {
87
87
  "svelte": "^5.0.0"