@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.
- package/dist/config/config.js +0 -12
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +6 -3
- package/dist/editor/ContentTree.d.ts +2 -1
- package/dist/editor/ContentTree.js +2 -2
- package/dist/editor/ContentTree.js.map +1 -1
- package/dist/editor/ItemInfo.js +1 -1
- package/dist/editor/ItemInfo.js.map +1 -1
- package/dist/editor/field-types/InternalLinkFieldEditor.js +20 -25
- package/dist/editor/field-types/InternalLinkFieldEditor.js.map +1 -1
- package/dist/editor/field-types/RichTextEditor.d.ts +2 -2
- package/dist/editor/field-types/RichTextEditor.js +15 -2
- package/dist/editor/field-types/RichTextEditor.js.map +1 -1
- package/dist/editor/field-types/RichTextEditorComponent.d.ts +5 -5
- package/dist/editor/field-types/RichTextEditorComponent.js +40 -51
- package/dist/editor/field-types/RichTextEditorComponent.js.map +1 -1
- package/dist/editor/field-types/TreeListEditor.js +7 -5
- package/dist/editor/field-types/TreeListEditor.js.map +1 -1
- package/dist/editor/field-types/richtext/components/EditorDropdown.d.ts +11 -0
- package/dist/editor/field-types/richtext/components/EditorDropdown.js +96 -0
- package/dist/editor/field-types/richtext/components/EditorDropdown.js.map +1 -0
- package/dist/editor/field-types/richtext/components/ReactSlate.d.ts +5 -0
- package/dist/editor/field-types/richtext/components/ReactSlate.js +558 -0
- package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -0
- package/dist/editor/field-types/richtext/components/ToolbarButton.d.ts +3 -0
- package/dist/editor/field-types/richtext/components/ToolbarButton.js +13 -0
- package/dist/editor/field-types/richtext/components/ToolbarButton.js.map +1 -0
- package/dist/editor/field-types/richtext/config/pluginFactory.d.ts +17 -0
- package/dist/editor/field-types/richtext/config/pluginFactory.js +14 -0
- package/dist/editor/field-types/richtext/config/pluginFactory.js.map +1 -0
- package/dist/editor/field-types/richtext/hooks/useProfileCache.d.ts +68 -0
- package/dist/editor/field-types/richtext/hooks/useProfileCache.js +208 -0
- package/dist/editor/field-types/richtext/hooks/useProfileCache.js.map +1 -0
- package/dist/editor/field-types/richtext/hooks/useRichTextProfile.d.ts +25 -0
- package/dist/editor/field-types/richtext/hooks/useRichTextProfile.js +64 -0
- package/dist/editor/field-types/richtext/hooks/useRichTextProfile.js.map +1 -0
- package/dist/editor/field-types/richtext/index.d.ts +5 -0
- package/dist/editor/field-types/richtext/index.js +6 -0
- package/dist/editor/field-types/richtext/index.js.map +1 -0
- package/dist/editor/field-types/richtext/types.d.ts +139 -0
- package/dist/editor/field-types/richtext/types.js +107 -0
- package/dist/editor/field-types/richtext/types.js.map +1 -0
- package/dist/editor/field-types/richtext/utils/conversion.d.ts +5 -0
- package/dist/editor/field-types/richtext/utils/conversion.js +539 -0
- package/dist/editor/field-types/richtext/utils/conversion.js.map +1 -0
- package/dist/editor/field-types/richtext/utils/plugins.d.ts +97 -0
- package/dist/editor/field-types/richtext/utils/plugins.js +272 -0
- package/dist/editor/field-types/richtext/utils/plugins.js.map +1 -0
- package/dist/editor/field-types/richtext/utils/profileMapper.d.ts +38 -0
- package/dist/editor/field-types/richtext/utils/profileMapper.js +366 -0
- package/dist/editor/field-types/richtext/utils/profileMapper.js.map +1 -0
- package/dist/editor/field-types/richtext/utils/profileServiceCache.d.ts +37 -0
- package/dist/editor/field-types/richtext/utils/profileServiceCache.js +117 -0
- package/dist/editor/field-types/richtext/utils/profileServiceCache.js.map +1 -0
- package/dist/editor/services/contentService.d.ts +8 -0
- package/dist/editor/services/contentService.js +3 -0
- package/dist/editor/services/contentService.js.map +1 -1
- package/dist/editor/sidebar/ComponentTree.js +7 -1
- package/dist/editor/sidebar/ComponentTree.js.map +1 -1
- package/dist/editor/ui/PerfectTree.d.ts +4 -2
- package/dist/editor/ui/PerfectTree.js +16 -7
- package/dist/editor/ui/PerfectTree.js.map +1 -1
- package/dist/editor/utils/itemutils.js +3 -1
- package/dist/editor/utils/itemutils.js.map +1 -1
- package/dist/editor/views/ItemEditor.js +10 -3
- package/dist/editor/views/ItemEditor.js.map +1 -1
- package/dist/page-wizard/steps/ContentStep.js +1 -3
- package/dist/page-wizard/steps/ContentStep.js.map +1 -1
- package/dist/page-wizard/steps/usePageCreator.js +1 -1
- package/dist/page-wizard/steps/usePageCreator.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/styles.css +9 -0
- package/package.json +4 -1
- package/src/config/config.tsx +0 -12
- package/src/config/types.ts +6 -3
- package/src/editor/ContentTree.tsx +3 -0
- package/src/editor/ItemInfo.tsx +2 -2
- package/src/editor/field-types/InternalLinkFieldEditor.tsx +73 -69
- package/src/editor/field-types/RichTextEditor.tsx +31 -3
- package/src/editor/field-types/RichTextEditorComponent.tsx +52 -69
- package/src/editor/field-types/TreeListEditor.tsx +7 -7
- package/src/editor/field-types/richtext/components/EditorDropdown.tsx +180 -0
- package/src/editor/field-types/richtext/components/ReactSlate.css +163 -0
- package/src/editor/field-types/richtext/components/ReactSlate.tsx +792 -0
- package/src/editor/field-types/richtext/components/ToolbarButton.tsx +23 -0
- package/src/editor/field-types/richtext/config/pluginFactory.tsx +22 -0
- package/src/editor/field-types/richtext/hooks/useProfileCache.ts +270 -0
- package/src/editor/field-types/richtext/hooks/useRichTextProfile.ts +94 -0
- package/src/editor/field-types/richtext/index.ts +5 -0
- package/src/editor/field-types/richtext/types.ts +269 -0
- package/src/editor/field-types/richtext/utils/conversion.ts +589 -0
- package/src/editor/field-types/richtext/utils/plugins.ts +346 -0
- package/src/editor/field-types/richtext/utils/profileMapper.ts +424 -0
- package/src/editor/field-types/richtext/utils/profileServiceCache.ts +154 -0
- package/src/editor/services/contentService.ts +12 -0
- package/src/editor/sidebar/ComponentTree.tsx +12 -1
- package/src/editor/ui/PerfectTree.tsx +19 -6
- package/src/editor/utils/{itemutils.ts → itemutils.tsx} +12 -12
- package/src/editor/views/ItemEditor.tsx +22 -1
- package/src/page-wizard/steps/ContentStep.tsx +1 -3
- package/src/page-wizard/steps/usePageCreator.ts +1 -0
- 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(/</g, '<')
|
|
22
|
+
.replace(/>/g, '>')
|
|
23
|
+
.replace(/"/g, '"')
|
|
24
|
+
.replace(/'/g, "'")
|
|
25
|
+
.replace(/&/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
|
+
};
|