@sveltia/ui 0.41.1 → 0.41.3

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,30 @@ 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
+ /**
56
+ * List of Markdown tags that should be disabled in the editor when converting Lexical nodes to
57
+ * Markdown (but not when converting Markdown to Lexical nodes because we don’t want to lose any
58
+ * formatting). Use underscore for italic text in Markdown instead of asterisks, and use double
59
+ * asterisks for bold text in Markdown instead of underscores. Also, disable triple asterisks and
60
+ * triple underscores for bold+italic text in Markdown, which can be confusing and is not commonly
61
+ * used. This is to ensure that the Markdown output is more readable and consistent.
62
+ */
63
+ export const DISABLED_MARKDOWN_TAGS: string[];
64
+ import type { EditorThemeClasses } from 'lexical';
31
65
  import type { TextEditorFormatType } from '../../typedefs';
32
66
  import type { TextEditorInlineType } from '../../typedefs';
33
67
  import type { TextEditorBlockType } from '../../typedefs';
68
+ import type { TextEditorNodeType } from '../../typedefs';
69
+ import type { Transformer } from '@lexical/markdown';
@@ -1,6 +1,89 @@
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
+ BOLD_UNDERSCORE,
9
+ CODE,
10
+ HEADING,
11
+ INLINE_CODE,
12
+ ITALIC_STAR,
13
+ ITALIC_UNDERSCORE,
14
+ LINK,
15
+ ORDERED_LIST,
16
+ QUOTE,
17
+ STRIKETHROUGH,
18
+ UNORDERED_LIST,
19
+ } from '@lexical/markdown';
20
+ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
21
+
22
+ /**
23
+ * @import { EditorThemeClasses } from 'lexical';
24
+ * @import { Transformer } from '@lexical/markdown';
25
+ * @import {
26
+ * TextEditorBlockType,
27
+ * TextEditorFormatType,
28
+ * TextEditorInlineType,
29
+ * TextEditorNodeType,
30
+ * } from '../../typedefs';
31
+ */
32
+
33
+ export const PRISM_BASE_URL = `https://unpkg.com/prismjs@1.30.0`;
34
+
1
35
  /**
2
- * @import { TextEditorBlockType, TextEditorFormatType, TextEditorInlineType } from '../../typedefs';
36
+ * @type {EditorThemeClasses}
3
37
  */
38
+ export const EDITOR_THEME = {
39
+ text: {
40
+ /**
41
+ * Enable bold+italic and strikethrough styling.
42
+ * @see https://github.com/facebook/lexical/discussions/4381
43
+ */
44
+ italic: 'italic',
45
+ strikethrough: 'strikethrough',
46
+ },
47
+ list: {
48
+ nested: {
49
+ listitem: 'nested',
50
+ },
51
+ },
52
+ code: 'code-block',
53
+ // https://github.com/facebook/lexical/blob/main/packages/lexical-website/docs/getting-started/theming.md
54
+ codeHighlight: {
55
+ atrule: 'token atrule',
56
+ attr: 'token attr',
57
+ boolean: 'token boolean',
58
+ builtin: 'token builtin',
59
+ cdata: 'token cdata',
60
+ char: 'token char',
61
+ class: 'token class',
62
+ 'class-name': 'token class-name',
63
+ comment: 'token comment',
64
+ constant: 'token constant',
65
+ deleted: 'token deleted',
66
+ doctype: 'token doctype',
67
+ entity: 'token entity',
68
+ function: 'token function',
69
+ important: 'token important',
70
+ inserted: 'token inserted',
71
+ keyword: 'token keyword',
72
+ namespace: 'token namespace',
73
+ number: 'token number',
74
+ operator: 'token operator',
75
+ prolog: 'token prolog',
76
+ property: 'token property',
77
+ punctuation: 'token punctuation',
78
+ regex: 'token regex',
79
+ selector: 'token selector',
80
+ string: 'token string',
81
+ symbol: 'token symbol',
82
+ tag: 'token tag',
83
+ url: 'token url',
84
+ variable: 'token variable',
85
+ },
86
+ };
4
87
 
5
88
  /**
6
89
  * List of available buttons.
@@ -120,3 +203,71 @@ export const BLOCK_BUTTON_TYPES = [
120
203
  * Image related components IDs. `linked-image` is used in Sveltia CMS.
121
204
  */
122
205
  export const IMAGE_COMPONENT_IDS = ['image', 'linked-image'];
206
+
207
+ /**
208
+ * Map of Lexical nodes for each block type. The `paragraph` block type is excluded because it is
209
+ * the default block type.
210
+ * @type {Record<Exclude<TextEditorBlockType | 'link', 'paragraph'>, any[]>}
211
+ */
212
+ export const NODE_MAP = {
213
+ 'heading-1': [HeadingNode],
214
+ 'heading-2': [HeadingNode],
215
+ 'heading-3': [HeadingNode],
216
+ 'heading-4': [HeadingNode],
217
+ 'heading-5': [HeadingNode],
218
+ 'heading-6': [HeadingNode],
219
+ 'bulleted-list': [ListNode, ListItemNode],
220
+ 'numbered-list': [ListNode, ListItemNode],
221
+ blockquote: [QuoteNode],
222
+ 'code-block': [CodeNode, CodeHighlightNode],
223
+ link: [LinkNode],
224
+ };
225
+
226
+ /**
227
+ * Map of Lexical transformers for each block type. The `paragraph` block type is excluded because
228
+ * it is the default block type.
229
+ * @type {Record<Exclude<TextEditorNodeType, 'paragraph'>, Transformer[]>}
230
+ */
231
+ export const TRANSFORMER_MAP = {
232
+ // ELEMENT_TRANSFORMERS
233
+ 'heading-1': [HEADING],
234
+ 'heading-2': [HEADING],
235
+ 'heading-3': [HEADING],
236
+ 'heading-4': [HEADING],
237
+ 'heading-5': [HEADING],
238
+ 'heading-6': [HEADING],
239
+ 'bulleted-list': [UNORDERED_LIST],
240
+ 'numbered-list': [ORDERED_LIST],
241
+ blockquote: [QUOTE],
242
+ // MULTILINE_ELEMENT_TRANSFORMERS
243
+ 'code-block': [CODE],
244
+ // TEXT_FORMAT_TRANSFORMERS
245
+ code: [INLINE_CODE],
246
+ bold: [
247
+ BOLD_STAR,
248
+ // Disabled for Markdown output in `DISABLED_MARKDOWN_TAGS` below
249
+ BOLD_UNDERSCORE,
250
+ BOLD_ITALIC_STAR,
251
+ BOLD_ITALIC_UNDERSCORE,
252
+ ],
253
+ italic: [
254
+ ITALIC_UNDERSCORE,
255
+ // Disabled for Markdown output in `DISABLED_MARKDOWN_TAGS` below
256
+ ITALIC_STAR,
257
+ BOLD_ITALIC_STAR,
258
+ BOLD_ITALIC_UNDERSCORE,
259
+ ],
260
+ strikethrough: [STRIKETHROUGH],
261
+ // TEXT_MATCH_TRANSFORMERS
262
+ link: [LINK],
263
+ };
264
+
265
+ /**
266
+ * List of Markdown tags that should be disabled in the editor when converting Lexical nodes to
267
+ * Markdown (but not when converting Markdown to Lexical nodes because we don’t want to lose any
268
+ * formatting). Use underscore for italic text in Markdown instead of asterisks, and use double
269
+ * asterisks for bold text in Markdown instead of underscores. Also, disable triple asterisks and
270
+ * triple underscores for bold+italic text in Markdown, which can be confusing and is not commonly
271
+ * used. This is to ensure that the Markdown output is more readable and consistent.
272
+ */
273
+ export const DISABLED_MARKDOWN_TAGS = ['*', '__', '***', '___'];
@@ -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,37 @@ 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
+ DISABLED_MARKDOWN_TAGS,
56
+ EDITOR_THEME,
57
+ NODE_MAP,
58
+ PRISM_BASE_URL,
59
+ TEXT_FORMAT_BUTTON_TYPES,
60
+ TRANSFORMER_MAP,
61
+ } from './constants.js';
61
62
  import { increaseListIndentation, splitMultilineFormatting } from './markdown.js';
62
63
  import { HR } from './transformers/hr.js';
63
64
  import { TABLE } from './transformers/table.js';
64
65
 
65
66
  /**
66
67
  * @import { CreateEditorArgs, LexicalEditor } from 'lexical';
68
+ * @import { Transformer } from '@lexical/markdown';
67
69
  * @import {
68
70
  * TextEditorBlockType,
69
71
  * TextEditorConfig,
70
72
  * TextEditorInlineType,
73
+ * TextEditorNodeType,
71
74
  * TextEditorSelectionState,
72
75
  * } from '../../typedefs';
73
76
  */
74
77
 
75
- const allTransformers = [...TRANSFORMERS, HR, TABLE];
76
- const prismBaseURL = `https://unpkg.com/prismjs@1.30.0`;
77
-
78
78
  /**
79
- * Lexical editor configuration.
80
- * @type {CreateEditorArgs}
79
+ * @typedef {object} InitEditorResult
80
+ * @property {LexicalEditor} editor Editor instance.
81
+ * @property {Transformer[]} enabledTransformers List of enabled Markdown transformers.
82
+ * @property {() => void} dispose Remove all registered Lexical listeners.
81
83
  */
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
84
 
148
85
  /**
149
86
  * Get the current selection’s block node key as well as block and inline level types.
@@ -223,15 +160,18 @@ export const getSelectionTypes = () => {
223
160
  * Listen to changes made on the editor and trigger the Update event.
224
161
  * @internal
225
162
  * @param {LexicalEditor} editor Editor instance.
163
+ * @param {Transformer[]} enabledTransformers Enabled Markdown transformers.
226
164
  */
227
- export const onEditorUpdate = (editor) => {
165
+ export const onEditorUpdate = (editor, enabledTransformers) => {
166
+ const transformers = enabledTransformers.filter(
167
+ (/** @type {any} */ { tag }) => !DISABLED_MARKDOWN_TAGS.includes(tag),
168
+ );
169
+
228
170
  editor.getRootElement()?.dispatchEvent(
229
171
  new CustomEvent('Update', {
230
172
  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
173
+ value: convertToMarkdownString(transformers)
174
+ // Remove unnecessary backslash for underscore and backslash characters
235
175
  // @see https://github.com/sveltia/sveltia-cms/issues/430
236
176
  // @see https://github.com/sveltia/sveltia-cms/issues/512
237
177
  .replace(/\\([_\\])/g, '$1')
@@ -249,18 +189,44 @@ export const onEditorUpdate = (editor) => {
249
189
  /**
250
190
  * Initialize the Lexical editor.
251
191
  * @param {TextEditorConfig} config Editor configuration.
252
- * @returns {{ editor: LexicalEditor, dispose: () => void }} Editor instance and cleanup.
192
+ * @returns {InitEditorResult} Editor instance and cleanup.
253
193
  */
254
194
  export const initEditor = ({
195
+ enabledButtons = [],
255
196
  components = [],
256
197
  useMarkdownShortcuts,
257
198
  isCodeEditor = false,
258
199
  defaultLanguage = 'plain',
259
200
  }) => {
260
- components.forEach(({ node, transformer }) => {
261
- /** @type {any[]} */ (editorConfig.nodes).unshift(node);
262
- allTransformers.unshift(transformer);
263
- });
201
+ /** @type {CreateEditorArgs} */
202
+ const editorConfig = {
203
+ namespace: 'editor',
204
+ nodes: [
205
+ ...components.map(({ node }) => node),
206
+ ...new Set(
207
+ Object.entries(NODE_MAP)
208
+ .filter(([button]) => enabledButtons.includes(/** @type {TextEditorNodeType} */ (button)))
209
+ .flatMap(([, nodes]) => nodes),
210
+ ),
211
+ HorizontalRuleNode,
212
+ TableNode,
213
+ TableCellNode,
214
+ TableRowNode,
215
+ ],
216
+ theme: EDITOR_THEME,
217
+ };
218
+
219
+ /** @type {Transformer[]} */
220
+ const enabledTransformers = [
221
+ ...components.map(({ transformer }) => transformer),
222
+ ...new Set(
223
+ Object.entries(TRANSFORMER_MAP)
224
+ .filter(([button]) => enabledButtons.includes(/** @type {TextEditorNodeType} */ (button)))
225
+ .flatMap(([, transformers]) => transformers),
226
+ ),
227
+ HR,
228
+ TABLE,
229
+ ];
264
230
 
265
231
  const editor = createEditor(editorConfig);
266
232
  /** @type {Array<() => void>} */
@@ -282,63 +248,73 @@ export const initEditor = ({
282
248
  addUnregister(registerHistory(editor, createEmptyHistoryState(), 1000));
283
249
 
284
250
  if (useMarkdownShortcuts) {
285
- addUnregister(registerMarkdownShortcuts(editor, allTransformers));
251
+ addUnregister(registerMarkdownShortcuts(editor, enabledTransformers));
286
252
  }
287
253
 
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');
254
+ if (enabledButtons.includes('code-block') || isCodeEditor) {
255
+ addUnregister(
256
+ registerCodeHighlighting(editor, {
257
+ defaultLanguage,
258
+ // eslint-disable-next-line jsdoc/require-jsdoc
259
+ tokenize: (code, lang = 'plain') =>
260
+ window.Prism.tokenize(code, window.Prism.languages[lang] ?? window.Prism.languages.plain),
261
+ $tokenize: PrismTokenizer.$tokenize,
262
+ }),
263
+ );
264
+ }
315
265
 
316
- return true;
317
- },
318
- COMMAND_PRIORITY_NORMAL,
319
- ),
320
- );
266
+ if (enabledButtons.includes('link')) {
267
+ addUnregister(
268
+ editor.registerCommand(
269
+ TOGGLE_LINK_COMMAND,
270
+ (payload) => {
271
+ toggleLink(typeof payload === 'string' ? payload : null);
272
+
273
+ return true;
274
+ },
275
+ COMMAND_PRIORITY_NORMAL,
276
+ ),
277
+ );
278
+ }
321
279
 
322
- addUnregister(
323
- editor.registerCommand(
324
- INSERT_ORDERED_LIST_COMMAND,
325
- () => {
326
- insertList('number');
280
+ if (enabledButtons.includes('bulleted-list')) {
281
+ addUnregister(
282
+ editor.registerCommand(
283
+ INSERT_UNORDERED_LIST_COMMAND,
284
+ () => {
285
+ insertList('bullet');
286
+
287
+ return true;
288
+ },
289
+ COMMAND_PRIORITY_NORMAL,
290
+ ),
291
+ );
292
+ }
327
293
 
328
- return true;
329
- },
330
- COMMAND_PRIORITY_NORMAL,
331
- ),
332
- );
294
+ if (enabledButtons.includes('numbered-list')) {
295
+ addUnregister(
296
+ editor.registerCommand(
297
+ INSERT_ORDERED_LIST_COMMAND,
298
+ () => {
299
+ insertList('number');
300
+
301
+ return true;
302
+ },
303
+ COMMAND_PRIORITY_NORMAL,
304
+ ),
305
+ );
306
+ }
333
307
 
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
- );
308
+ if (enabledButtons.includes('bulleted-list') || enabledButtons.includes('numbered-list')) {
309
+ // https://github.com/facebook/lexical/blob/main/packages/lexical-react/src/shared/useList.ts
310
+ addUnregister(
311
+ editor.registerCommand(
312
+ INSERT_PARAGRAPH_COMMAND,
313
+ () => handleListInsertParagraph(),
314
+ COMMAND_PRIORITY_NORMAL,
315
+ ),
316
+ );
317
+ }
342
318
 
343
319
  addUnregister(
344
320
  editor.registerUpdateListener(() => {
@@ -369,7 +345,7 @@ export const initEditor = ({
369
345
  }
370
346
  }
371
347
 
372
- onEditorUpdate(editor);
348
+ onEditorUpdate(editor, enabledTransformers);
373
349
  });
374
350
  })();
375
351
  }),
@@ -424,6 +400,7 @@ export const initEditor = ({
424
400
 
425
401
  return {
426
402
  editor,
403
+ enabledTransformers,
427
404
  /**
428
405
  * Remove all registered Lexical listeners.
429
406
  */
@@ -455,7 +432,7 @@ export const loadCodeHighlighter = async (lang) => {
455
432
 
456
433
  try {
457
434
  // eslint-disable-next-line jsdoc/no-bad-blocks
458
- await import(/* @vite-ignore */ `${prismBaseURL}/components/prism-${canonicalLang}.min.js`);
435
+ await import(/* @vite-ignore */ `${PRISM_BASE_URL}/components/prism-${canonicalLang}.min.js`);
459
436
  } catch {
460
437
  //
461
438
  }
@@ -465,10 +442,11 @@ export const loadCodeHighlighter = async (lang) => {
465
442
  * Convert Markdown to Lexical nodes.
466
443
  * @param {LexicalEditor} editor Editor instance.
467
444
  * @param {string} value Current Markdown value.
445
+ * @param {Transformer[]} enabledTransformers List of enabled Markdown transformers.
468
446
  * @returns {Promise<void>} Nothing.
469
447
  * @throws {Error} Failed to convert the value to Lexical nodes.
470
448
  */
471
- export const convertMarkdownToLexical = async (editor, value) => {
449
+ export const convertMarkdownToLexical = async (editor, value, enabledTransformers) => {
472
450
  // Load Prism language support on demand; the `loadLanguages` Prism utility method cannot be used
473
451
  await Promise.all(
474
452
  [...value.matchAll(/^```(?<lang>.+?)\n/gm)].map(async ({ groups: { lang = 'plain' } = {} }) =>
@@ -485,7 +463,7 @@ export const convertMarkdownToLexical = async (editor, value) => {
485
463
  return new Promise((resolve, reject) => {
486
464
  editor.update(() => {
487
465
  try {
488
- convertFromMarkdownString(value, allTransformers);
466
+ convertFromMarkdownString(value, enabledTransformers);
489
467
  resolve(undefined);
490
468
  } catch (ex) {
491
469
  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.3",
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"