5htp-core 0.4.9-5 → 0.4.9-6

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "5htp-core",
3
3
  "description": "Convenient TypeScript framework designed for Performance and Productivity.",
4
- "version": "0.4.9-5",
4
+ "version": "0.4.9-6",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/5htp-core.git",
7
7
  "license": "MIT",
@@ -16,7 +16,6 @@ import ElementFormatDropdown from './ElementFormat';
16
16
  import BlockFormatDropDown, { blockTypeNames, rootTypeToRootName } from './BlockFormat';
17
17
 
18
18
  import {
19
- $createCodeNode,
20
19
  $isCodeNode,
21
20
  CODE_LANGUAGE_FRIENDLY_NAME_MAP,
22
21
  CODE_LANGUAGE_MAP,
@@ -25,9 +24,6 @@ import {
25
24
  import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
26
25
  import {
27
26
  $isListNode,
28
- INSERT_CHECK_LIST_COMMAND,
29
- INSERT_ORDERED_LIST_COMMAND,
30
- INSERT_UNORDERED_LIST_COMMAND,
31
27
  ListNode,
32
28
  } from '@lexical/list';
33
29
  import { INSERT_EMBED_COMMAND } from '@lexical/react/LexicalAutoEmbedPlugin';
@@ -35,17 +31,13 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
35
31
  import { $isDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode';
36
32
  import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode';
37
33
  import {
38
- $createHeadingNode,
39
- $createQuoteNode,
40
34
  $isHeadingNode,
41
35
  $isQuoteNode,
42
- HeadingTagType,
43
36
  } from '@lexical/rich-text';
44
37
  import {
45
38
  $getSelectionStyleValueForProperty,
46
39
  $isParentElementRTL,
47
40
  $patchStyleText,
48
- $setBlocksType,
49
41
  } from '@lexical/selection';
50
42
  import { $isTableNode, $isTableSelection } from '@lexical/table';
51
43
  import {
@@ -126,7 +118,7 @@ function dropDownActiveClass(active: boolean) {
126
118
 
127
119
 
128
120
 
129
- function Divider(): JSX.Element {
121
+ function Divider(): React.JSX.Element {
130
122
  return <div className="divider" />;
131
123
  }
132
124
 
@@ -134,7 +126,7 @@ export default function ToolbarPlugin({
134
126
  setIsLinkEditMode,
135
127
  }: {
136
128
  setIsLinkEditMode: Dispatch<boolean>;
137
- }): JSX.Element {
129
+ }): React.JSX.Element {
138
130
 
139
131
  const { modal } = useContext();
140
132
 
@@ -603,7 +595,6 @@ export default function ToolbarPlugin({
603
595
  <DropDown popover={{ tag: 'li' }} icon="font" size="s"
604
596
  disabled={!isEditable}
605
597
  title="Formatting options for additional text styles"
606
- buttonIconClassName="icon dropdown-more"
607
598
  >
608
599
 
609
600
  <Button icon="strikethrough" size="s"
@@ -0,0 +1,55 @@
1
+
2
+ import { HeadingNode, HeadingTagType } from '@lexical/rich-text';
3
+
4
+ export default class HeadingWithAnchorNode extends HeadingNode {
5
+
6
+ public anchor?: string;
7
+
8
+ constructor( tag: HeadingTagType, key?: string ) {
9
+ super(tag, key);
10
+ }
11
+
12
+ // Adding a static method to register the custom node
13
+ static getType() {
14
+ return 'anchored-heading';
15
+ }
16
+
17
+ static clone(node) {
18
+ return new HeadingWithAnchorNode(node.getTag(), node.__key);
19
+ }
20
+
21
+ // Add a `anchor` attribute to the serialized JSON
22
+ static importJSON(serializedNode) {
23
+ const node = new HeadingWithAnchorNode(serializedNode.tag);
24
+ node.anchor = serializedNode.anchor;
25
+ return node;
26
+ }
27
+
28
+ // Ensure the `anchor` attribute is serialized in JSON
29
+ exportJSON() {
30
+ return {
31
+ ...super.exportJSON(),
32
+ type: HeadingWithAnchorNode.getType(),
33
+ anchor: this.anchor,
34
+ };
35
+ }
36
+
37
+ // Override createDOM to set the `anchor` attribute
38
+ createDOM(config) {
39
+ const dom = super.createDOM(config);
40
+ if (this.anchor) {
41
+ dom.setAttribute('id', this.anchor);
42
+ }
43
+ return dom;
44
+ }
45
+
46
+ // Update the DOM to reflect changes in the `anchor` attribute
47
+ updateDOM(prevNode, dom) {
48
+ const updated = super.updateDOM(prevNode, dom);
49
+ if (this.anchor !== prevNode.anchor) {
50
+ dom.setAttribute('id', this.anchor);
51
+ return true;
52
+ }
53
+ return updated;
54
+ }
55
+ }
@@ -36,7 +36,10 @@ import * as React from 'react';
36
36
  import * as ReactDOM from 'react-dom';
37
37
 
38
38
  // Core
39
- import useContext from '@/client/context';
39
+ import useContext, { ClientContext } from '@/client/context';
40
+
41
+ // Custom Nodes
42
+ import AnchoredHeadingNode from '@client/components/inputv3/Rte/nodes/HeadingNode';
40
43
 
41
44
  import { EmbedConfigs } from '../AutoEmbedPlugin';
42
45
  import { INSERT_COLLAPSIBLE_COMMAND } from '../CollapsiblePlugin';
@@ -50,7 +53,7 @@ class ComponentPickerOption extends MenuOption {
50
53
  // What shows up in the editor
51
54
  title: string;
52
55
  // Icon for display
53
- icon?: JSX.Element;
56
+ icon?: React.JSX.Element;
54
57
  // For extra searching.
55
58
  keywords: Array<string>;
56
59
  // TBD
@@ -61,7 +64,7 @@ class ComponentPickerOption extends MenuOption {
61
64
  constructor(
62
65
  title: string,
63
66
  options: {
64
- icon?: JSX.Element;
67
+ icon?: React.JSX.Element;
65
68
  keywords?: Array<string>;
66
69
  keyboardShortcut?: string;
67
70
  onSelect: (queryString: string) => void;
@@ -141,7 +144,7 @@ function getDynamicOptions(editor: LexicalEditor, queryString: string) {
141
144
  return options;
142
145
  }
143
146
 
144
- function getBaseOptions(editor: LexicalEditor, { modal }: TContext) {
147
+ function getBaseOptions(editor: LexicalEditor, { modal }: ClientContext) {
145
148
  return [
146
149
  new ComponentPickerOption('Paragraph', {
147
150
  icon: <i className="icon paragraph" />,
@@ -163,7 +166,7 @@ function getBaseOptions(editor: LexicalEditor, { modal }: TContext) {
163
166
  editor.update(() => {
164
167
  const selection = $getSelection();
165
168
  if ($isRangeSelection(selection)) {
166
- $setBlocksType(selection, () => $createHeadingNode(`h${n}`));
169
+ $setBlocksType(selection, () => new AnchoredHeadingNode(`h${n}`));
167
170
  }
168
171
  }),
169
172
  }),
@@ -281,7 +284,7 @@ function getBaseOptions(editor: LexicalEditor, { modal }: TContext) {
281
284
  ];
282
285
  }
283
286
 
284
- export default function ComponentPickerMenuPlugin(): JSX.Element {
287
+ export default function ComponentPickerMenuPlugin(): React.JSX.Element {
285
288
 
286
289
  const [editor] = useLexicalComposerContext();
287
290
  const { modal, context } = useContext();
@@ -16,17 +16,6 @@
16
16
  margin: 0;
17
17
  position: relative;
18
18
  }
19
- .PlaygroundEditorTheme__quote {
20
- margin: 0;
21
- margin-left: 20px;
22
- margin-bottom: 10px;
23
- font-size: 15px;
24
- color: rgb(101, 103, 107);
25
- border-left-color: rgb(206, 208, 212);
26
- border-left-width: 4px;
27
- border-left-style: solid;
28
- padding-left: 16px;
29
- }
30
19
  .PlaygroundEditorTheme__h1 {
31
20
  font-size: 24px;
32
21
  color: rgb(5, 5, 5);
@@ -88,7 +88,7 @@ const theme: EditorThemeClasses = {
88
88
  mark: 'PlaygroundEditorTheme__mark',
89
89
  markOverlap: 'PlaygroundEditorTheme__markOverlap',
90
90
  paragraph: 'PlaygroundEditorTheme__paragraph',
91
- quote: 'PlaygroundEditorTheme__quote',
91
+ quote: '',
92
92
  rtl: 'PlaygroundEditorTheme__rtl',
93
93
  table: 'PlaygroundEditorTheme__table',
94
94
  tableCell: 'PlaygroundEditorTheme__tableCell',
@@ -35,8 +35,16 @@ import { StickyNode } from '@client/components/inputv3/Rte/nodes/StickyNode';
35
35
  import { TweetNode } from '@client/components/inputv3/Rte/nodes/TweetNode';
36
36
  import { YouTubeNode } from '@client/components/inputv3/Rte/nodes/YouTubeNode';
37
37
 
38
+ import HeadingWithAnchorNode from '@client/components/inputv3/Rte/nodes/HeadingNode';
39
+
38
40
  const PlaygroundNodes: Array<Klass<LexicalNode>> = [
39
- HeadingNode,
41
+ /*HeadingNode, */HeadingWithAnchorNode,
42
+ {
43
+ replace: HeadingNode,
44
+ with: (node) => {
45
+ return new HeadingWithAnchorNode( node.getTag(), node.__key );
46
+ }
47
+ },
40
48
  ListNode,
41
49
  ListItemNode,
42
50
  QuoteNode,
@@ -10,7 +10,7 @@ import md5 from 'md5';
10
10
  import { fromBuffer } from 'file-type';
11
11
  import { JSDOM } from 'jsdom';
12
12
  // Lexical
13
- import { $getRoot, SerializedEditorState, SerializedLexicalNode } from 'lexical';
13
+ import { $getRoot } from 'lexical';
14
14
  import { createHeadlessEditor } from '@lexical/headless';
15
15
  import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
16
16
 
@@ -19,17 +19,46 @@ import { Anomaly } from '@common/errors';
19
19
  import editorNodes from '@common/data/rte/nodes';
20
20
  import ExampleTheme from '@client/components/inputv3/Rte/themes/PlaygroundEditorTheme';
21
21
  import type Driver from '@server/services/disks/driver';
22
+ import Slug from '@server/utils/slug';
22
23
 
23
24
  /*----------------------------------
24
25
  - TYPES
25
26
  ----------------------------------*/
26
27
 
27
- type SerializedLexicalNodeWithSrc = SerializedLexicalNode & { src: string };
28
+ type LexicalState = {
29
+ root: LexicalNode
30
+ }
28
31
 
29
- type TAttachmentsOptions = {
30
- disk: Driver,
31
- directory: string,
32
- prevVersion?: string | SerializedEditorState | null,
32
+ type LexicalNode = {
33
+ version: number,
34
+ type: string,
35
+ children?: LexicalNode[],
36
+ // Attachement
37
+ src?: string;
38
+ // Headhing
39
+ text?: string;
40
+ anchor?: string;
41
+ tag?: string;
42
+ }
43
+
44
+ type TRenderOptions = {
45
+ attachements?: {
46
+ disk: Driver,
47
+ directory: string,
48
+ prevVersion?: string | LexicalState | null,
49
+ }
50
+ }
51
+
52
+ type TSkeleton = {
53
+ id: string,
54
+ title: string,
55
+ level: number,
56
+ childrens: TSkeleton
57
+ }[];
58
+
59
+ type TContentAssets = {
60
+ attachements: string[],
61
+ skeleton: TSkeleton
33
62
  }
34
63
 
35
64
  /*----------------------------------
@@ -39,70 +68,198 @@ type TAttachmentsOptions = {
39
68
  export class RteUtils {
40
69
 
41
70
  public async render(
42
- content: string | SerializedEditorState,
43
- attachementsOpts?: TAttachmentsOptions
44
- ) {
71
+ content: string | LexicalState,
72
+ options: TRenderOptions = {}
73
+ ): Promise<TContentAssets & {
74
+ html: string,
75
+ json: string | LexicalState,
76
+ }> {
45
77
 
46
78
  // Parse content if string
79
+ let json: LexicalState;
47
80
  if (typeof content === 'string') {
48
81
  try {
49
- content = JSON.parse(content) as SerializedEditorState;
82
+ json = JSON.parse(content) as LexicalState;
50
83
  } catch (error) {
51
84
  throw new Anomaly("Invalid JSON format for the given JSON RTE content.");
52
85
  }
86
+ } else
87
+ json = content;
88
+
89
+ // Parse prev version if string
90
+ if (typeof options?.attachements?.prevVersion === 'string') {
91
+ try {
92
+ options.attachements.prevVersion = JSON.parse(options.attachements.prevVersion) as LexicalState;
93
+ } catch (error) {
94
+ throw new Anomaly("Invalid JSON format for the given JSON RTE prev version.");
95
+ }
96
+ }
97
+
98
+ // Transform content
99
+ const assets: TContentAssets = {
100
+ attachements: [],
101
+ skeleton: []
53
102
  }
54
103
 
55
- // Attachments
56
- let attachements: string[] = [];
57
- if (attachementsOpts) {
104
+ const root = await this.processContent(json.root, async (node) => {
105
+ return await this.transformNode(node, assets, options);
106
+ });
107
+
108
+ json = { ...json, root };
109
+
110
+ // Delete unused attachements
111
+ const attachementOptions = options?.attachements;
112
+ if (attachementOptions && attachementOptions.prevVersion !== undefined) {
113
+
114
+ await this.processContent(root, async (node) => {
115
+ return await this.deleteUnusedFile(node, assets, attachementOptions);
116
+ });
117
+ }
118
+
119
+ // Convert json to HTML
120
+ const html = await this.jsonToHtml( json );
121
+
122
+ return { html, json: content, ...assets };
123
+ }
124
+
125
+ private async processContent(
126
+ node: LexicalNode,
127
+ callback: (node: LexicalNode) => Promise<LexicalNode>
128
+ ) {
129
+
130
+ node = await callback(node);
131
+
132
+ // Recursion
133
+ if (node.children) {
134
+ for (let i = 0; i < node.children.length; i++) {
135
+
136
+ node.children[ i ] = await this.processContent( node.children[ i ], callback );
58
137
 
59
- // Parse prev version if string
60
- if (typeof attachementsOpts.prevVersion === 'string') {
61
- try {
62
- attachementsOpts.prevVersion = JSON.parse(attachementsOpts.prevVersion) as SerializedEditorState;
63
- } catch (error) {
64
- throw new Anomaly("Invalid JSON format for the given JSON RTE prev version.");
65
- }
66
138
  }
139
+ }
140
+
141
+ return node;
142
+ }
143
+
144
+ private async transformNode(node: LexicalNode, assets: TContentAssets, options: TRenderOptions) {
67
145
 
68
- // Upload attachments and replace blobs by URLs
69
- attachements = await this.uploadAttachments(
70
- content,
71
- attachementsOpts.disk, attachementsOpts.directory,
72
- attachementsOpts.prevVersion || undefined
146
+ // Attachment: Upload attachments and replace blobs by URLs
147
+ if (node.type === 'image' || node.type === 'file') {
148
+
149
+ await this.processAttachement(
150
+ node as With<LexicalNode, 'src'>,
151
+ assets,
152
+ options,
73
153
  );
154
+
155
+ } else if (node.type === 'anchored-heading') {
156
+
157
+ await this.processHeading(node, assets);
158
+
74
159
  }
75
160
 
76
- // Convert content to HTML
77
- const html = await this.jsonToHtml( content );
78
-
79
- return { html, json: content, attachements };
161
+ return node;
80
162
  }
81
163
 
82
- public async htmlToJson(htmlString: string): Promise<SerializedEditorState> {
164
+ public async processAttachement(
165
+ node: LexicalNode,
166
+ assets: TContentAssets,
167
+ options: TRenderOptions
168
+ ) {
83
169
 
84
- const editor = createHeadlessEditor({
85
- nodes: editorNodes,
86
- theme: ExampleTheme,
170
+ const attachementOptions = options?.attachements;
171
+ if (typeof node.src !== 'string' || !attachementOptions)
172
+ return;
173
+
174
+ // Already uploaded
175
+ if (!node.src.startsWith('data:')) {
176
+ assets.attachements.push(node.src);
177
+ return node.src;
178
+ }
179
+
180
+ // Transform into buffer
181
+ node.src = node.src.replace(/^data:\w+\/\w+;base64,/, '');
182
+ const fileData = Buffer.from(node.src, 'base64');
183
+
184
+ // Parse file type from buffer
185
+ const fileType = await fromBuffer(fileData);
186
+ if (!fileType)
187
+ throw new Error('Failed to detect file type');
188
+
189
+ // Upload file to disk
190
+ const fileName = md5(node.src) + '.' + fileType.ext;
191
+ const filePath = path.join(attachementOptions.directory, fileName);
192
+ const upoadedFile = await attachementOptions.disk.outputFile('data', filePath, fileData, {
193
+ encoding: 'binary'
87
194
  });
88
195
 
89
- await editor.update(() => {
196
+ // Replace node.src with url
197
+ node.src = upoadedFile.path;
198
+ assets.attachements.push(node.src);
199
+ }
90
200
 
91
- const root = $getRoot();
201
+ private async processHeading( node: LexicalNode, assets: TContentAssets ) {
92
202
 
93
- const dom = new JSDOM(htmlString);
203
+ const title = this.jsonToText(node);
204
+ const titleLevel = parseInt(node.tag?.substring(1) || '1');
205
+ const titleSlug = await Slug.generate(title);
206
+ node.anchor = titleSlug;
94
207
 
95
- // Once you have the DOM instance it's easy to generate LexicalNodes.
96
- const lexicalNodes = $generateNodesFromDOM(editor, dom ? dom.window.document : window.document);
208
+ const findParentContainer = (skeleton: TSkeleton): TSkeleton => {
209
+ for (let i = skeleton.length - 1; i >= 0; i--) {
210
+ const child = skeleton[i];
97
211
 
98
- lexicalNodes.forEach((node) => root.append(node));
212
+ if (child.level === titleLevel - 1) {
213
+ return child.childrens;
214
+ } else if (child.level < titleLevel) {
215
+ return findParentContainer(child.childrens);
216
+ }
217
+ }
218
+
219
+ return skeleton;
220
+ }
221
+
222
+ const parentContainer = (assets.skeleton.length === 0 || titleLevel === 1)
223
+ ? assets.skeleton
224
+ : findParentContainer(assets.skeleton);
225
+
226
+ parentContainer.push({
227
+ title,
228
+ id: titleSlug,
229
+ level: titleLevel,
230
+ childrens: []
99
231
  });
232
+ }
100
233
 
101
- const state = editor.getEditorState();
102
- return state.toJSON();
103
- };
234
+ private async deleteUnusedFile(
235
+ node: LexicalNode,
236
+ assets: TContentAssets,
237
+ options: NonNullable<TRenderOptions["attachements"]>
238
+ ) {
239
+
240
+ if (!node.src)
241
+ return node;
104
242
 
105
- public async jsonToHtml(jsonString: string) {
243
+ // Should be a URL
244
+ if (node.src.startsWith('data:') || !node.src.startsWith('http'))
245
+ return node;
246
+
247
+ // This file is used
248
+ if (assets.attachements.includes(node.src))
249
+ return node;
250
+
251
+ // Extract file name
252
+ const fileName = path.basename(node.src);
253
+ const filePath = path.join(options.directory, fileName);
254
+
255
+ // Delete file from disk
256
+ await options.disk.delete('data', filePath);
257
+ console.log('Deleted file:', filePath);
258
+
259
+ return node;
260
+ }
261
+
262
+ public async jsonToHtml( json: LexicalState ) {
106
263
 
107
264
  // Server side: simulate DOM environment
108
265
  const dom = new JSDOM(`<!DOCTYPE html><body></body>`);
@@ -120,9 +277,9 @@ export class RteUtils {
120
277
  });
121
278
 
122
279
  // Set the editor state from JSON
123
- const state = editor.parseEditorState(jsonString);
280
+ const state = editor.parseEditorState(json);
124
281
  if (state.isEmpty())
125
- return null;
282
+ return '';
126
283
 
127
284
  editor.setEditorState(state);
128
285
 
@@ -131,105 +288,60 @@ export class RteUtils {
131
288
  return $generateHtmlFromNodes(editor);
132
289
  });
133
290
 
134
- // Clean up global variables set for JSDOM to avoid memory leaks
291
+ // Clean up global variables set for JSDOM to avoid memory leaks
292
+ // @ts-ignore
135
293
  delete global.window;
294
+ // @ts-ignore
136
295
  delete global.document;
296
+ // @ts-ignore
137
297
  delete global.DOMParser;
298
+ // @ts-ignore
138
299
  delete global.MutationObserver;
139
300
 
140
301
  return html;
141
302
  }
142
303
 
143
- public async uploadAttachments(
144
- value: SerializedEditorState,
145
- disk: Driver,
146
- destination: string,
147
- oldState?: SerializedEditorState
148
- ) {
149
-
150
- const usedFilesUrl: string[] = [];
304
+ private jsonToText( node: LexicalNode ) {
151
305
 
152
- // Deep check for images / files
153
- const findFiles = async (
154
- node: SerializedLexicalNode,
155
- callback: (node: SerializedLexicalNodeWithSrc) => Promise<void>
156
- ) => {
306
+ let text = '';
157
307
 
158
- // Attachment
159
- if (node.type === 'image' || node.type === 'file') {
160
- if (typeof node.src === 'string') {
161
- await callback(node as SerializedLexicalNodeWithSrc);
162
- }
163
- // Recursion
164
- } else if (node.children) {
165
- for (const child of node.children) {
166
- await findFiles(child, callback);
167
- }
168
- }
308
+ // Check if the node has text content
309
+ if (node.type === 'text' && node.text) {
310
+ text += node.text;
169
311
  }
170
312
 
171
- const uploadFile = async (node: SerializedLexicalNodeWithSrc) => {
172
-
173
- // Already uploaded
174
- if (!node.src.startsWith('data:')) {
175
- usedFilesUrl.push(node.src);
176
- return node.src;
177
- }
178
-
179
- // Transform into buffer
180
- node.src = node.src.replace(/^data:\w+\/\w+;base64,/, '');
181
- const fileData = Buffer.from(node.src, 'base64');
182
-
183
- // Parse file type from buffer
184
- const fileType = await fromBuffer(fileData);
185
- if (!fileType)
186
- throw new Error('Failed to detect file type');
187
-
188
- // Upload file to disk
189
- const fileName = md5(node.src) + '.' + fileType.ext;
190
- const filePath = path.join(destination, fileName);
191
- const upoadedFile = await disk.outputFile('data', filePath, fileData, {
192
- encoding: 'binary'
313
+ // Recursively process children nodes
314
+ if (node.children && Array.isArray(node.children)) {
315
+ node.children.forEach(childNode => {
316
+ text += this.jsonToText(childNode);
193
317
  });
194
-
195
- // Replace node.src with url
196
- node.src = upoadedFile.path;
197
- usedFilesUrl.push(node.src);
198
318
  }
199
319
 
200
- const deleteUnusedFile = async (node: SerializedLexicalNodeWithSrc) => {
320
+ return text;
321
+ }
201
322
 
202
- // Should be a URL
203
- if (node.src.startsWith('data:') || !node.src.startsWith('http'))
204
- return;
323
+ public async htmlToJson(htmlString: string): Promise<LexicalState> {
205
324
 
206
- // This file is used
207
- if (usedFilesUrl.includes(node.src))
208
- return;
325
+ const editor = createHeadlessEditor({
326
+ nodes: editorNodes,
327
+ theme: ExampleTheme,
328
+ });
209
329
 
210
- // Extract file name
211
- const fileName = path.basename(node.src);
212
- const filePath = path.join(destination, fileName);
330
+ await editor.update(() => {
213
331
 
214
- // Delete file from disk
215
- await disk.delete('data', filePath);
216
- console.log('Deleted file:', filePath);
217
- }
332
+ const root = $getRoot();
218
333
 
219
- // Find files in the editor state
220
- for (const child of value.root.children) {
221
- await findFiles(child, uploadFile);
222
- }
334
+ const dom = new JSDOM(htmlString);
223
335
 
224
- // Old state given: remove unused attachments
225
- if (oldState !== undefined) {
226
- for (const child of oldState.root.children) {
227
- await findFiles(child, deleteUnusedFile);
228
- }
229
- }
336
+ // Once you have the DOM instance it's easy to generate LexicalNodes.
337
+ const lexicalNodes = $generateNodesFromDOM(editor, dom ? dom.window.document : window.document);
230
338
 
231
- return usedFilesUrl;
232
- }
339
+ lexicalNodes.forEach((node) => root.append(node));
340
+ });
341
+
342
+ const state = editor.getEditorState();
343
+ return state.toJSON();
344
+ };
233
345
  }
234
346
 
235
347
  export default new RteUtils;
@@ -26,9 +26,9 @@ export class Slug {
26
26
  // Generate slug
27
27
  let slug = slugify(label, {
28
28
  replacement: '-', // replace spaces with replacement character, defaults to `-`
29
- remove: undefined, // remove characters that match regex, defaults to `undefined`
29
+ remove: /[^a-z\s]/ig, // remove characters that match regex, defaults to `undefined`
30
30
  lower: true, // convert to lower case, defaults to `false`
31
- strict: false, // strip special characters except replacement, defaults to `false`
31
+ strict: true, // strip special characters except replacement, defaults to `false`
32
32
  locale: 'vi', // language code of the locale to use
33
33
  trim: true // trim leading and trailing replacement chars, defaults to `true`
34
34
  });
@@ -1,76 +0,0 @@
1
- /**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- *
7
- */
8
-
9
- import type {Klass, LexicalNode} from 'lexical';
10
-
11
- import {CodeHighlightNode, CodeNode} from '@lexical/code';
12
- import {HashtagNode} from '@lexical/hashtag';
13
- import {AutoLinkNode, LinkNode} from '@lexical/link';
14
- import {ListItemNode, ListNode} from '@lexical/list';
15
- import {MarkNode} from '@lexical/mark';
16
- import {OverflowNode} from '@lexical/overflow';
17
- import {HorizontalRuleNode} from '@lexical/react/LexicalHorizontalRuleNode';
18
- import {HeadingNode, QuoteNode} from '@lexical/rich-text';
19
- import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
20
-
21
- import {CollapsibleContainerNode} from '../plugins/CollapsiblePlugin/CollapsibleContainerNode';
22
- import {CollapsibleContentNode} from '../plugins/CollapsiblePlugin/CollapsibleContentNode';
23
- import {CollapsibleTitleNode} from '../plugins/CollapsiblePlugin/CollapsibleTitleNode';
24
- import {AutocompleteNode} from './AutocompleteNode';
25
- import {EmojiNode} from './EmojiNode';
26
- import {EquationNode} from './EquationNode';
27
- import {FigmaNode} from './FigmaNode';
28
- import {ImageNode} from './ImageNode';
29
- import {InlineImageNode} from './InlineImageNode/InlineImageNode';
30
- import {KeywordNode} from './KeywordNode';
31
- import {LayoutContainerNode} from './LayoutContainerNode';
32
- import {LayoutItemNode} from './LayoutItemNode';
33
- import {MentionNode} from './MentionNode';
34
- import {PageBreakNode} from './PageBreakNode';
35
- import {PollNode} from './PollNode';
36
- import {StickyNode} from './StickyNode';
37
- import {TweetNode} from './TweetNode';
38
- import {YouTubeNode} from './YouTubeNode';
39
-
40
- const PlaygroundNodes: Array<Klass<LexicalNode>> = [
41
- HeadingNode,
42
- ListNode,
43
- ListItemNode,
44
- QuoteNode,
45
- CodeNode,
46
- TableNode,
47
- TableCellNode,
48
- TableRowNode,
49
- HashtagNode,
50
- CodeHighlightNode,
51
- AutoLinkNode,
52
- LinkNode,
53
- OverflowNode,
54
- PollNode,
55
- StickyNode,
56
- ImageNode,
57
- InlineImageNode,
58
- MentionNode,
59
- EmojiNode,
60
- EquationNode,
61
- AutocompleteNode,
62
- KeywordNode,
63
- HorizontalRuleNode,
64
- TweetNode,
65
- YouTubeNode,
66
- FigmaNode,
67
- MarkNode,
68
- CollapsibleContainerNode,
69
- CollapsibleContentNode,
70
- CollapsibleTitleNode,
71
- PageBreakNode,
72
- LayoutContainerNode,
73
- LayoutItemNode,
74
- ];
75
-
76
- export default PlaygroundNodes;