@alpaca-editor/core 1.0.3992 → 1.0.3993

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.
Files changed (103) hide show
  1. package/dist/config/config.js +0 -12
  2. package/dist/config/config.js.map +1 -1
  3. package/dist/config/types.d.ts +6 -3
  4. package/dist/editor/ContentTree.d.ts +2 -1
  5. package/dist/editor/ContentTree.js +2 -2
  6. package/dist/editor/ContentTree.js.map +1 -1
  7. package/dist/editor/ItemInfo.js +1 -1
  8. package/dist/editor/ItemInfo.js.map +1 -1
  9. package/dist/editor/field-types/InternalLinkFieldEditor.js +20 -25
  10. package/dist/editor/field-types/InternalLinkFieldEditor.js.map +1 -1
  11. package/dist/editor/field-types/RichTextEditor.d.ts +2 -2
  12. package/dist/editor/field-types/RichTextEditor.js +15 -2
  13. package/dist/editor/field-types/RichTextEditor.js.map +1 -1
  14. package/dist/editor/field-types/RichTextEditorComponent.d.ts +5 -5
  15. package/dist/editor/field-types/RichTextEditorComponent.js +40 -51
  16. package/dist/editor/field-types/RichTextEditorComponent.js.map +1 -1
  17. package/dist/editor/field-types/TreeListEditor.js +7 -5
  18. package/dist/editor/field-types/TreeListEditor.js.map +1 -1
  19. package/dist/editor/field-types/richtext/components/EditorDropdown.d.ts +11 -0
  20. package/dist/editor/field-types/richtext/components/EditorDropdown.js +96 -0
  21. package/dist/editor/field-types/richtext/components/EditorDropdown.js.map +1 -0
  22. package/dist/editor/field-types/richtext/components/ReactSlate.d.ts +5 -0
  23. package/dist/editor/field-types/richtext/components/ReactSlate.js +558 -0
  24. package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -0
  25. package/dist/editor/field-types/richtext/components/ToolbarButton.d.ts +3 -0
  26. package/dist/editor/field-types/richtext/components/ToolbarButton.js +13 -0
  27. package/dist/editor/field-types/richtext/components/ToolbarButton.js.map +1 -0
  28. package/dist/editor/field-types/richtext/config/pluginFactory.d.ts +17 -0
  29. package/dist/editor/field-types/richtext/config/pluginFactory.js +14 -0
  30. package/dist/editor/field-types/richtext/config/pluginFactory.js.map +1 -0
  31. package/dist/editor/field-types/richtext/hooks/useProfileCache.d.ts +68 -0
  32. package/dist/editor/field-types/richtext/hooks/useProfileCache.js +208 -0
  33. package/dist/editor/field-types/richtext/hooks/useProfileCache.js.map +1 -0
  34. package/dist/editor/field-types/richtext/hooks/useRichTextProfile.d.ts +25 -0
  35. package/dist/editor/field-types/richtext/hooks/useRichTextProfile.js +64 -0
  36. package/dist/editor/field-types/richtext/hooks/useRichTextProfile.js.map +1 -0
  37. package/dist/editor/field-types/richtext/index.d.ts +5 -0
  38. package/dist/editor/field-types/richtext/index.js +6 -0
  39. package/dist/editor/field-types/richtext/index.js.map +1 -0
  40. package/dist/editor/field-types/richtext/types.d.ts +139 -0
  41. package/dist/editor/field-types/richtext/types.js +107 -0
  42. package/dist/editor/field-types/richtext/types.js.map +1 -0
  43. package/dist/editor/field-types/richtext/utils/conversion.d.ts +5 -0
  44. package/dist/editor/field-types/richtext/utils/conversion.js +539 -0
  45. package/dist/editor/field-types/richtext/utils/conversion.js.map +1 -0
  46. package/dist/editor/field-types/richtext/utils/plugins.d.ts +97 -0
  47. package/dist/editor/field-types/richtext/utils/plugins.js +272 -0
  48. package/dist/editor/field-types/richtext/utils/plugins.js.map +1 -0
  49. package/dist/editor/field-types/richtext/utils/profileMapper.d.ts +38 -0
  50. package/dist/editor/field-types/richtext/utils/profileMapper.js +366 -0
  51. package/dist/editor/field-types/richtext/utils/profileMapper.js.map +1 -0
  52. package/dist/editor/field-types/richtext/utils/profileServiceCache.d.ts +37 -0
  53. package/dist/editor/field-types/richtext/utils/profileServiceCache.js +117 -0
  54. package/dist/editor/field-types/richtext/utils/profileServiceCache.js.map +1 -0
  55. package/dist/editor/services/contentService.d.ts +8 -0
  56. package/dist/editor/services/contentService.js +3 -0
  57. package/dist/editor/services/contentService.js.map +1 -1
  58. package/dist/editor/sidebar/ComponentTree.js +7 -1
  59. package/dist/editor/sidebar/ComponentTree.js.map +1 -1
  60. package/dist/editor/ui/PerfectTree.d.ts +4 -2
  61. package/dist/editor/ui/PerfectTree.js +16 -7
  62. package/dist/editor/ui/PerfectTree.js.map +1 -1
  63. package/dist/editor/utils/itemutils.js +3 -1
  64. package/dist/editor/utils/itemutils.js.map +1 -1
  65. package/dist/editor/views/ItemEditor.js +10 -3
  66. package/dist/editor/views/ItemEditor.js.map +1 -1
  67. package/dist/page-wizard/steps/ContentStep.js +1 -3
  68. package/dist/page-wizard/steps/ContentStep.js.map +1 -1
  69. package/dist/page-wizard/steps/usePageCreator.js +1 -1
  70. package/dist/page-wizard/steps/usePageCreator.js.map +1 -1
  71. package/dist/revision.d.ts +2 -2
  72. package/dist/revision.js +2 -2
  73. package/dist/styles.css +9 -0
  74. package/package.json +4 -1
  75. package/src/config/config.tsx +0 -12
  76. package/src/config/types.ts +6 -3
  77. package/src/editor/ContentTree.tsx +3 -0
  78. package/src/editor/ItemInfo.tsx +2 -2
  79. package/src/editor/field-types/InternalLinkFieldEditor.tsx +73 -69
  80. package/src/editor/field-types/RichTextEditor.tsx +31 -3
  81. package/src/editor/field-types/RichTextEditorComponent.tsx +52 -69
  82. package/src/editor/field-types/TreeListEditor.tsx +7 -7
  83. package/src/editor/field-types/richtext/components/EditorDropdown.tsx +180 -0
  84. package/src/editor/field-types/richtext/components/ReactSlate.css +163 -0
  85. package/src/editor/field-types/richtext/components/ReactSlate.tsx +792 -0
  86. package/src/editor/field-types/richtext/components/ToolbarButton.tsx +23 -0
  87. package/src/editor/field-types/richtext/config/pluginFactory.tsx +22 -0
  88. package/src/editor/field-types/richtext/hooks/useProfileCache.ts +270 -0
  89. package/src/editor/field-types/richtext/hooks/useRichTextProfile.ts +94 -0
  90. package/src/editor/field-types/richtext/index.ts +5 -0
  91. package/src/editor/field-types/richtext/types.ts +269 -0
  92. package/src/editor/field-types/richtext/utils/conversion.ts +589 -0
  93. package/src/editor/field-types/richtext/utils/plugins.ts +346 -0
  94. package/src/editor/field-types/richtext/utils/profileMapper.ts +424 -0
  95. package/src/editor/field-types/richtext/utils/profileServiceCache.ts +154 -0
  96. package/src/editor/services/contentService.ts +12 -0
  97. package/src/editor/sidebar/ComponentTree.tsx +12 -1
  98. package/src/editor/ui/PerfectTree.tsx +19 -6
  99. package/src/editor/utils/{itemutils.ts → itemutils.tsx} +12 -12
  100. package/src/editor/views/ItemEditor.tsx +22 -1
  101. package/src/page-wizard/steps/ContentStep.tsx +1 -3
  102. package/src/page-wizard/steps/usePageCreator.ts +1 -0
  103. package/src/revision.ts +2 -2
@@ -0,0 +1,589 @@
1
+ import { Descendant, Element as SlateElement } from 'slate';
2
+ import { CustomText, Alignment, CustomElement, LinkElement, SimplifiedProfile, SLATE_MARKS, SLATE_BLOCKS } from '../types';
3
+
4
+ // Elements that should be completely preserved as raw HTML
5
+ const PRESERVE_AS_RAW_ELEMENTS = new Set([
6
+ 'table', 'tbody', 'thead', 'tfoot', 'tr', 'td', 'th',
7
+ 'svg', 'canvas', 'iframe', 'object', 'embed',
8
+ 'form', 'input', 'textarea', 'select', 'button',
9
+ 'video', 'audio', 'source', 'track',
10
+ 'script', 'style', 'noscript'
11
+ ]);
12
+
13
+ // Shared utility: Get default block type from profile
14
+ const getDefaultBlockType = (blocks: string[]): string => {
15
+ return blocks.length > 0 && blocks[0] ? blocks[0] : 'paragraph';
16
+ };
17
+
18
+ // Shared utility: Unescape HTML entities
19
+ const unescapeHtmlEntities = (text: string): string => {
20
+ return text
21
+ .replace(/&lt;/g, '<')
22
+ .replace(/&gt;/g, '>')
23
+ .replace(/&quot;/g, '"')
24
+ .replace(/&#39;/g, "'")
25
+ .replace(/&amp;/g, '&'); // This should be last to avoid double-unescaping
26
+ };
27
+
28
+ // Simplified preservation check
29
+ const shouldPreserveAsRaw = (element: HTMLElement): boolean => {
30
+ const tagName = element.tagName.toLowerCase();
31
+
32
+ // Check if it's in the preserve list
33
+ if (PRESERVE_AS_RAW_ELEMENTS.has(tagName)) {
34
+ return true;
35
+ }
36
+
37
+ // Check if this element contains any elements that should be preserved
38
+ const containsPreserveElements = element.querySelector(Array.from(PRESERVE_AS_RAW_ELEMENTS).join(','));
39
+ if (containsPreserveElements) {
40
+ return true;
41
+ }
42
+
43
+ // Check for complex styling that would be hard to preserve
44
+ const style = element.getAttribute('style') || '';
45
+ const hasComplexStyling = style.includes('background-color') ||
46
+ style.includes('font-family') ||
47
+ style.includes('font-size') ||
48
+ style.includes('color:');
49
+
50
+ return hasComplexStyling && element.children.length > 0;
51
+ };
52
+
53
+ // Shared utility: Process marks from element
54
+ const getMarksFromElement = (element: HTMLElement, configuredMarks: string[]): Record<string, boolean> => {
55
+ const marks: Record<string, boolean> = {};
56
+
57
+ // Check for HTML tag-based marks
58
+ const tagName = element.tagName.toLowerCase();
59
+ const markConfig = configuredMarks.find(markId => {
60
+ const config = SLATE_MARKS[markId];
61
+ return config && config.htmlTag === tagName;
62
+ });
63
+
64
+ if (markConfig) {
65
+ marks[markConfig] = true;
66
+ }
67
+
68
+ // Check for inline style-based marks
69
+ const style = element.getAttribute('style');
70
+ if (style) {
71
+ if (style.includes('font-weight: bold') || style.includes('font-weight:bold')) {
72
+ marks['bold'] = true;
73
+ }
74
+ if (style.includes('font-style: italic') || style.includes('font-style:italic')) {
75
+ marks['italic'] = true;
76
+ }
77
+ if (style.includes('text-decoration: underline') || style.includes('text-decoration:underline')) {
78
+ marks['underline'] = true;
79
+ }
80
+ if (style.includes('text-decoration: line-through') || style.includes('text-decoration:line-through')) {
81
+ marks['strikethrough'] = true;
82
+ }
83
+ }
84
+
85
+ return marks;
86
+ };
87
+
88
+ export const htmlToSlate = (html: string, profile: SimplifiedProfile): Descendant[] => {
89
+ const defaultBlockType = getDefaultBlockType(profile.blocks);
90
+
91
+ if (!html || html === '<p><br></p>') {
92
+ return [{ type: defaultBlockType, children: [{ text: '' }] }];
93
+ }
94
+
95
+ const div = document.createElement('div');
96
+ div.innerHTML = html;
97
+
98
+ const nodes: Descendant[] = [];
99
+
100
+ // Determine block type for element
101
+ const getBlockTypeForElement = (element: HTMLElement): string => {
102
+ const tagName = element.tagName.toLowerCase();
103
+
104
+ // Check if we have a configured block for this element
105
+ const matchingBlockId = profile.blocks.find(blockId => {
106
+ const blockConfig = SLATE_BLOCKS[blockId];
107
+ return blockConfig && blockConfig.htmlTag === tagName;
108
+ });
109
+
110
+ return matchingBlockId || defaultBlockType;
111
+ };
112
+
113
+ // Process list elements
114
+ const processListElement = (element: HTMLElement, currentIndent: number = 0): void => {
115
+ const tagName = element.tagName.toLowerCase();
116
+ const listType = tagName === 'ul' ? 'unordered' : 'ordered';
117
+
118
+ Array.from(element.children).forEach(child => {
119
+ if (child.tagName.toLowerCase() === 'li') {
120
+ const liElement = child as HTMLElement;
121
+ const style = liElement.getAttribute('style') || '';
122
+ const alignMatch = style.match(/text-align:\s*([^;]+)/);
123
+ const align = alignMatch ? alignMatch[1] as Alignment : undefined;
124
+
125
+ // Process text content and inline elements (excluding nested lists)
126
+ const children = processNodeWithInlinesExcludingNestedLists(liElement, profile.marks);
127
+
128
+ nodes.push({
129
+ type: 'list-item',
130
+ listType: listType,
131
+ indent: currentIndent,
132
+ align,
133
+ children: children.length > 0 ? children : [{ text: '' }]
134
+ });
135
+
136
+ // Process nested lists separately
137
+ Array.from(liElement.children).forEach(childEl => {
138
+ const childTagName = childEl.tagName.toLowerCase();
139
+ if (childTagName === 'ul' || childTagName === 'ol') {
140
+ processListElement(childEl as HTMLElement, currentIndent + 1);
141
+ }
142
+ });
143
+ }
144
+ });
145
+ };
146
+
147
+ // Process inline elements and text, excluding nested lists
148
+ const processNodeWithInlinesExcludingNestedLists = (element: HTMLElement, configuredMarks: string[]): (CustomText | CustomElement)[] => {
149
+ const results: (CustomText | CustomElement)[] = [];
150
+ const currentElementMarks = getMarksFromElement(element, configuredMarks);
151
+
152
+ Array.from(element.childNodes).forEach(child => {
153
+ if (child.nodeType === Node.TEXT_NODE) {
154
+ if (child.textContent) {
155
+ results.push({
156
+ text: child.textContent,
157
+ ...currentElementMarks
158
+ });
159
+ }
160
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
161
+ const childElement = child as HTMLElement;
162
+ const tagName = childElement.tagName.toLowerCase();
163
+
164
+ // Skip nested lists - they're processed separately
165
+ if (tagName === 'ul' || tagName === 'ol') {
166
+ return;
167
+ }
168
+
169
+ if (tagName === 'br') {
170
+ results.push({ text: '<br>' });
171
+ } else if (tagName === 'a') {
172
+ const href = childElement.getAttribute('href') || '';
173
+ const target = childElement.getAttribute('target') || '_self';
174
+ const isInternal = childElement.hasAttribute('data-internal');
175
+ const itemId = childElement.getAttribute('data-item-id') || '';
176
+ const targetItemLongId = childElement.getAttribute('data-item-longid') || '';
177
+ const queryString = childElement.getAttribute('data-querystring') || '';
178
+
179
+ const linkChildren = processNodeWithInlines(childElement, configuredMarks);
180
+
181
+ results.push({
182
+ type: 'link',
183
+ url: href,
184
+ link: {
185
+ type: isInternal ? 'internal' : 'external',
186
+ url: href,
187
+ target,
188
+ itemId,
189
+ targetItemLongId,
190
+ queryString
191
+ },
192
+ children: linkChildren.length ? linkChildren : [{ text: childElement.textContent || 'Link' }]
193
+ });
194
+ } else if (shouldPreserveAsRaw(childElement)) {
195
+ // Preserve complex elements as raw HTML
196
+ results.push({
197
+ text: childElement.outerHTML,
198
+ isRawHtml: true
199
+ });
200
+ } else {
201
+ // Process as regular inline element with marks
202
+ const childMarks = getMarksFromElement(childElement, configuredMarks);
203
+ const combinedMarks = { ...currentElementMarks, ...childMarks };
204
+
205
+ const childContent = processNodeWithInlines(childElement, configuredMarks);
206
+ childContent.forEach(node => {
207
+ if ('text' in node) {
208
+ Object.assign(node, combinedMarks);
209
+ }
210
+ results.push(node);
211
+ });
212
+ }
213
+ }
214
+ });
215
+
216
+ return results;
217
+ };
218
+
219
+ // Process inline elements and text
220
+ const processNodeWithInlines = (element: HTMLElement, configuredMarks: string[]): (CustomText | CustomElement)[] => {
221
+ const results: (CustomText | CustomElement)[] = [];
222
+ const currentElementMarks = getMarksFromElement(element, configuredMarks);
223
+
224
+ Array.from(element.childNodes).forEach(child => {
225
+ if (child.nodeType === Node.TEXT_NODE) {
226
+ if (child.textContent) {
227
+ results.push({
228
+ text: child.textContent,
229
+ ...currentElementMarks
230
+ });
231
+ }
232
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
233
+ const childElement = child as HTMLElement;
234
+ const tagName = childElement.tagName.toLowerCase();
235
+
236
+ if (tagName === 'br') {
237
+ results.push({ text: '<br>' });
238
+ } else if (tagName === 'a') {
239
+ const href = childElement.getAttribute('href') || '';
240
+ const target = childElement.getAttribute('target') || '_self';
241
+ const isInternal = childElement.hasAttribute('data-internal');
242
+ const itemId = childElement.getAttribute('data-item-id') || '';
243
+ const targetItemLongId = childElement.getAttribute('data-item-longid') || '';
244
+ const queryString = childElement.getAttribute('data-querystring') || '';
245
+
246
+ const linkChildren = processNodeWithInlines(childElement, configuredMarks);
247
+
248
+ results.push({
249
+ type: 'link',
250
+ url: href,
251
+ link: {
252
+ type: isInternal ? 'internal' : 'external',
253
+ url: href,
254
+ target,
255
+ itemId,
256
+ targetItemLongId,
257
+ queryString
258
+ },
259
+ children: linkChildren.length ? linkChildren : [{ text: childElement.textContent || 'Link' }]
260
+ });
261
+ } else if (tagName === 'ul' || tagName === 'ol' || shouldPreserveAsRaw(childElement)) {
262
+ // Preserve nested lists and complex elements as raw HTML
263
+ results.push({
264
+ text: childElement.outerHTML,
265
+ isRawHtml: true
266
+ });
267
+ } else {
268
+ // Process as regular inline element with marks
269
+ const childMarks = getMarksFromElement(childElement, configuredMarks);
270
+ const combinedMarks = { ...currentElementMarks, ...childMarks };
271
+
272
+ const childContent = processNodeWithInlines(childElement, configuredMarks);
273
+ childContent.forEach(node => {
274
+ if ('text' in node) {
275
+ Object.assign(node, combinedMarks);
276
+ }
277
+ results.push(node);
278
+ });
279
+ }
280
+ }
281
+ });
282
+
283
+ return results;
284
+ };
285
+
286
+ // Process top-level nodes
287
+ // First, check if we have mixed content (text and br elements) that should be combined
288
+ const topLevelNodes = Array.from(div.childNodes);
289
+ const hasMixedContent = topLevelNodes.some(node =>
290
+ (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) ||
291
+ (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName.toLowerCase() === 'br')
292
+ );
293
+
294
+ if (hasMixedContent && topLevelNodes.length > 1) {
295
+ // Combine mixed text and br elements into a single element
296
+ const combinedChildren: (CustomText | CustomElement)[] = [];
297
+
298
+ topLevelNodes.forEach(node => {
299
+ if (node.nodeType === Node.TEXT_NODE && node.textContent) {
300
+ combinedChildren.push({ text: node.textContent });
301
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
302
+ const element = node as HTMLElement;
303
+ const tagName = element.tagName.toLowerCase();
304
+
305
+ if (tagName === 'br') {
306
+ combinedChildren.push({ text: '<br>' });
307
+ } else {
308
+ // Process other inline elements
309
+ const inlineChildren = processNodeWithInlines(element, profile.marks);
310
+ combinedChildren.push(...inlineChildren);
311
+ }
312
+ }
313
+ });
314
+
315
+ if (combinedChildren.length > 0) {
316
+ nodes.push({
317
+ type: defaultBlockType,
318
+ children: combinedChildren
319
+ });
320
+ }
321
+ } else {
322
+ // Process normally for single elements or non-mixed content
323
+ Array.from(div.childNodes).forEach(node => {
324
+ if (node.nodeType === Node.ELEMENT_NODE) {
325
+ const element = node as HTMLElement;
326
+ const tagName = element.tagName.toLowerCase();
327
+
328
+ // Extract alignment if present
329
+ const style = element.getAttribute('style') || '';
330
+ const alignMatch = style.match(/text-align:\s*([^;]+)/);
331
+ const align = alignMatch ? alignMatch[1] as Alignment : undefined;
332
+
333
+ if (tagName === 'br') {
334
+ // Handle standalone br elements at top level
335
+ nodes.push({
336
+ type: defaultBlockType,
337
+ children: [{ text: '<br>' }]
338
+ });
339
+ } else if (tagName === 'ul' || tagName === 'ol') {
340
+ if (profile.lists) {
341
+ processListElement(element);
342
+ } else {
343
+ // If lists are not enabled, treat as preserved elements
344
+ nodes.push({
345
+ type: defaultBlockType,
346
+ children: [{ text: element.outerHTML, isRawHtml: true }]
347
+ });
348
+ }
349
+ } else if (tagName === 'li') {
350
+ if (profile.lists) {
351
+ // Handle standalone li elements
352
+ const parentTagName = element.parentElement?.tagName.toLowerCase();
353
+ const listType = parentTagName === 'ul' ? 'unordered' : parentTagName === 'ol' ? 'ordered' : 'unordered';
354
+ const children = processNodeWithInlines(element, profile.marks);
355
+
356
+ nodes.push({
357
+ type: 'list-item',
358
+ listType: listType,
359
+ indent: 0,
360
+ align,
361
+ children: children.length > 0 ? children : [{ text: '' }]
362
+ });
363
+ } else {
364
+ nodes.push({
365
+ type: defaultBlockType,
366
+ children: [{ text: element.outerHTML, isRawHtml: true }]
367
+ });
368
+ }
369
+ } else if (shouldPreserveAsRaw(element)) {
370
+ // Preserve complex elements as raw HTML
371
+ nodes.push({
372
+ type: defaultBlockType,
373
+ children: [{ text: element.outerHTML, isRawHtml: true }]
374
+ });
375
+ } else {
376
+ const children = processNodeWithInlines(element, profile.marks);
377
+ const blockType = getBlockTypeForElement(element);
378
+
379
+ nodes.push({
380
+ type: blockType,
381
+ align,
382
+ children: children.length > 0 ? children : [{ text: '' }]
383
+ });
384
+ }
385
+ } else if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) {
386
+ nodes.push({
387
+ type: defaultBlockType,
388
+ children: [{ text: node.textContent }]
389
+ });
390
+ }
391
+ });
392
+ }
393
+
394
+ return nodes.length > 0 ? nodes : [{ type: defaultBlockType, children: [{ text: '' }] }];
395
+ };
396
+
397
+ export const formatTextWithMarks = (child: CustomText, configuredMarks: string[]): string => {
398
+ // If this is raw HTML, return it as-is with unescaping
399
+ if (child.isRawHtml) {
400
+ return unescapeHtmlEntities(child.text);
401
+ }
402
+
403
+ let text = child.text;
404
+
405
+ // Convert literal <br> text back to <br> tags
406
+ text = text.replace(/<br>/g, '<br>');
407
+
408
+ // Apply marks using Slate's built-in mark system
409
+ configuredMarks.forEach(markId => {
410
+ const markConfig = SLATE_MARKS[markId];
411
+ if (markConfig && (child as any)[markId]) {
412
+ text = markConfig.renderHtml(text);
413
+ }
414
+ });
415
+
416
+ return text;
417
+ };
418
+
419
+ export const slateToHtml = (value: Descendant[], profile: SimplifiedProfile): string => {
420
+ let html = '';
421
+ let i = 0;
422
+
423
+ const defaultBlockType = getDefaultBlockType(profile.blocks);
424
+
425
+ // Build nested list HTML
426
+ const buildNestedListHtml = (startIndex: number, targetIndent: number = 0): { html: string; nextIndex: number } => {
427
+ let listHtml = '';
428
+ let currentIndex = startIndex;
429
+
430
+ // Find first element at target level to determine list type
431
+ let firstListType: 'ordered' | 'unordered' = 'unordered';
432
+
433
+ while (currentIndex < value.length) {
434
+ const element = value[currentIndex] as CustomElement;
435
+ if (element.type !== 'list-item') break;
436
+
437
+ const elementIndent = element.indent || 0;
438
+ if (elementIndent === targetIndent) {
439
+ firstListType = element.listType || 'unordered';
440
+ break;
441
+ } else if (elementIndent < targetIndent) {
442
+ break;
443
+ }
444
+ currentIndex++;
445
+ }
446
+
447
+ const listTag = firstListType === 'ordered' ? 'ol' : 'ul';
448
+ listHtml += `<${listTag}>`;
449
+
450
+ currentIndex = startIndex;
451
+ while (currentIndex < value.length) {
452
+ const element = value[currentIndex] as CustomElement;
453
+
454
+ if (element.type !== 'list-item') break;
455
+
456
+ const elementIndent = element.indent || 0;
457
+ const elementListType = element.listType || 'unordered';
458
+
459
+ if (elementIndent < targetIndent) break;
460
+
461
+ if (elementIndent === targetIndent && elementListType === firstListType) {
462
+ const processedChildren = processChildrenWithInlines(element.children as CustomText[], profile.marks);
463
+ listHtml += `<li>${processedChildren}`;
464
+
465
+ // Check for nested lists
466
+ if (currentIndex + 1 < value.length) {
467
+ const nextElement = value[currentIndex + 1] as CustomElement;
468
+ if (nextElement.type === 'list-item' && (nextElement.indent || 0) > elementIndent) {
469
+ const nestedResult = buildNestedListHtml(currentIndex + 1, elementIndent + 1);
470
+ listHtml += nestedResult.html;
471
+ currentIndex = nestedResult.nextIndex - 1;
472
+ }
473
+ }
474
+
475
+ listHtml += `</li>`;
476
+ currentIndex++;
477
+ } else if (elementIndent > targetIndent) {
478
+ const nestedResult = buildNestedListHtml(currentIndex, elementIndent);
479
+ listHtml += nestedResult.html;
480
+ currentIndex = nestedResult.nextIndex;
481
+ } else {
482
+ break;
483
+ }
484
+ }
485
+
486
+ listHtml += `</${listTag}>`;
487
+ return { html: listHtml, nextIndex: currentIndex };
488
+ };
489
+
490
+ // Process children with inline elements
491
+ const processChildrenWithInlines = (children: (CustomText | CustomElement)[], configuredMarks: string[]): string => {
492
+ let result = '';
493
+
494
+ children.forEach(child => {
495
+ if (SlateElement.isElement(child)) {
496
+ if (child.type === 'link') {
497
+ const linkElement = child as LinkElement;
498
+ const isInternal = linkElement.link?.type === 'internal';
499
+
500
+ const linkText = linkElement.children?.map((c: CustomText | CustomElement) => {
501
+ if (SlateElement.isElement(c)) {
502
+ return processChildrenWithInlines(c.children as (CustomText | CustomElement)[], configuredMarks);
503
+ } else {
504
+ const textNode = c as CustomText;
505
+ return textNode.isRawHtml ? unescapeHtmlEntities(textNode.text) : formatTextWithMarks(textNode, configuredMarks);
506
+ }
507
+ }).join('') || '';
508
+
509
+ if (isInternal) {
510
+ const itemId = linkElement.link?.itemId || '';
511
+ const targetItemLongId = linkElement.link?.targetItemLongId || '';
512
+ const target = linkElement.link?.target || '_self';
513
+ const queryString = linkElement.link?.queryString || '';
514
+ result += `<a href="#" data-internal="true" data-item-id="${itemId}" data-item-longid="${targetItemLongId}" target="${target}" data-querystring="${queryString}" class="internal-link">${linkText}</a>`;
515
+ } else {
516
+ const url = linkElement.url || linkElement.link?.url || '#';
517
+ const target = linkElement.link?.target || '_self';
518
+ const queryString = linkElement.link?.queryString || '';
519
+ result += `<a href="${url}" target="${target}" data-querystring="${queryString}">${linkText}</a>`;
520
+ }
521
+ } else {
522
+ // Handle other element types
523
+ result += processChildrenWithInlines(child.children as (CustomText | CustomElement)[], configuredMarks);
524
+ }
525
+ } else {
526
+ // Regular text node
527
+ const textNode = child as CustomText;
528
+ result += textNode.isRawHtml ? unescapeHtmlEntities(textNode.text) : formatTextWithMarks(textNode, configuredMarks);
529
+ }
530
+ });
531
+
532
+ return result;
533
+ };
534
+
535
+ // Process all elements
536
+ while (i < value.length) {
537
+ const element = value[i] as CustomElement;
538
+
539
+ // Handle list items
540
+ if (element.type === 'list-item') {
541
+ const listResult = buildNestedListHtml(i, element.indent || 0);
542
+ html += listResult.html;
543
+ i = listResult.nextIndex;
544
+ continue;
545
+ }
546
+
547
+ const alignStyle = element.align ? ` style="text-align: ${element.align};"` : '';
548
+
549
+ if (element.type === 'preserved-element') {
550
+ // Handle preserved elements by restoring their original HTML
551
+ const preservedElement = element as any;
552
+ html += preservedElement.originalHtml;
553
+ } else {
554
+ const blockConfig = SLATE_BLOCKS[element.type];
555
+
556
+ if (blockConfig && profile.blocks.includes(element.type) && element.children) {
557
+ const processedChildren = processChildrenWithInlines(element.children as CustomText[], profile.marks);
558
+ html += blockConfig.renderHtml(processedChildren, alignStyle);
559
+ } else {
560
+ // Fallback for unknown types
561
+ const processedChildren = processChildrenWithInlines(element.children as CustomText[], profile.marks);
562
+ const defaultBlockConfig = SLATE_BLOCKS[defaultBlockType];
563
+ if (defaultBlockConfig) {
564
+ html += defaultBlockConfig.renderHtml(processedChildren, alignStyle);
565
+ } else if (defaultBlockType === 'no-tag') {
566
+ html += processedChildren;
567
+ } else {
568
+ html += `<p${alignStyle}>${processedChildren}</p>`;
569
+ }
570
+ }
571
+ }
572
+
573
+ i++;
574
+ }
575
+
576
+ // Handle empty content
577
+ if (!html) {
578
+ const defaultBlockConfig = SLATE_BLOCKS[defaultBlockType];
579
+ if (defaultBlockConfig) {
580
+ return defaultBlockConfig.renderHtml('<br>', '');
581
+ } else if (defaultBlockType === 'no-tag') {
582
+ return '<br>';
583
+ } else {
584
+ return '<p><br></p>';
585
+ }
586
+ }
587
+
588
+ return html;
589
+ };