feat(editor): Add wysiwyg editor
This commit is contained in:
244
ts_web/elements/wysiwyg/wysiwyg.converters.ts
Normal file
244
ts_web/elements/wysiwyg/wysiwyg.converters.ts
Normal file
@ -0,0 +1,244 @@
|
||||
import { type IBlock } from './wysiwyg.types.js';
|
||||
|
||||
export class WysiwygConverters {
|
||||
static escapeHtml(text: string): string {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
static getHtmlOutput(blocks: IBlock[]): string {
|
||||
return blocks.map(block => {
|
||||
switch (block.type) {
|
||||
case 'paragraph':
|
||||
return block.content ? `<p>${this.escapeHtml(block.content)}</p>` : '';
|
||||
case 'heading-1':
|
||||
return `<h1>${this.escapeHtml(block.content)}</h1>`;
|
||||
case 'heading-2':
|
||||
return `<h2>${this.escapeHtml(block.content)}</h2>`;
|
||||
case 'heading-3':
|
||||
return `<h3>${this.escapeHtml(block.content)}</h3>`;
|
||||
case 'quote':
|
||||
return `<blockquote>${this.escapeHtml(block.content)}</blockquote>`;
|
||||
case 'code':
|
||||
return `<pre><code>${this.escapeHtml(block.content)}</code></pre>`;
|
||||
case 'list':
|
||||
const items = block.content.split('\n').filter(item => item.trim());
|
||||
if (items.length > 0) {
|
||||
const listTag = block.metadata?.listType === 'ordered' ? 'ol' : 'ul';
|
||||
return `<${listTag}>${items.map(item => `<li>${this.escapeHtml(item)}</li>`).join('')}</${listTag}>`;
|
||||
}
|
||||
return '';
|
||||
case 'divider':
|
||||
return '<hr>';
|
||||
default:
|
||||
return `<p>${this.escapeHtml(block.content)}</p>`;
|
||||
}
|
||||
}).filter(html => html !== '').join('\n');
|
||||
}
|
||||
|
||||
static getMarkdownOutput(blocks: IBlock[]): string {
|
||||
return blocks.map(block => {
|
||||
switch (block.type) {
|
||||
case 'paragraph':
|
||||
return block.content;
|
||||
case 'heading-1':
|
||||
return `# ${block.content}`;
|
||||
case 'heading-2':
|
||||
return `## ${block.content}`;
|
||||
case 'heading-3':
|
||||
return `### ${block.content}`;
|
||||
case 'quote':
|
||||
return `> ${block.content}`;
|
||||
case 'code':
|
||||
return `\`\`\`\n${block.content}\n\`\`\``;
|
||||
case 'list':
|
||||
const items = block.content.split('\n').filter(item => item.trim());
|
||||
if (block.metadata?.listType === 'ordered') {
|
||||
return items.map((item, index) => `${index + 1}. ${item}`).join('\n');
|
||||
} else {
|
||||
return items.map(item => `- ${item}`).join('\n');
|
||||
}
|
||||
case 'divider':
|
||||
return '---';
|
||||
default:
|
||||
return block.content;
|
||||
}
|
||||
}).filter(md => md !== '').join('\n\n');
|
||||
}
|
||||
|
||||
static parseHtmlToBlocks(html: string): IBlock[] {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const blocks: IBlock[] = [];
|
||||
|
||||
const processNode = (node: Node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) {
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'paragraph',
|
||||
content: node.textContent.trim(),
|
||||
});
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element;
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
|
||||
switch (tagName) {
|
||||
case 'p':
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'paragraph',
|
||||
content: element.textContent || '',
|
||||
});
|
||||
break;
|
||||
case 'h1':
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'heading-1',
|
||||
content: element.textContent || '',
|
||||
});
|
||||
break;
|
||||
case 'h2':
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'heading-2',
|
||||
content: element.textContent || '',
|
||||
});
|
||||
break;
|
||||
case 'h3':
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'heading-3',
|
||||
content: element.textContent || '',
|
||||
});
|
||||
break;
|
||||
case 'blockquote':
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'quote',
|
||||
content: element.textContent || '',
|
||||
});
|
||||
break;
|
||||
case 'pre':
|
||||
case 'code':
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'code',
|
||||
content: element.textContent || '',
|
||||
});
|
||||
break;
|
||||
case 'ul':
|
||||
case 'ol':
|
||||
const listItems = Array.from(element.querySelectorAll('li'));
|
||||
const content = listItems.map(li => li.textContent || '').join('\n');
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'list',
|
||||
content: content,
|
||||
metadata: { listType: tagName === 'ol' ? 'ordered' : 'bullet' }
|
||||
});
|
||||
break;
|
||||
case 'hr':
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'divider',
|
||||
content: ' ',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
// Process children for other elements
|
||||
element.childNodes.forEach(child => processNode(child));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
doc.body.childNodes.forEach(node => processNode(node));
|
||||
return blocks;
|
||||
}
|
||||
|
||||
static parseMarkdownToBlocks(markdown: string): IBlock[] {
|
||||
const lines = markdown.split('\n');
|
||||
const blocks: IBlock[] = [];
|
||||
let currentListItems: string[] = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
if (line.startsWith('# ')) {
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'heading-1',
|
||||
content: line.substring(2),
|
||||
});
|
||||
} else if (line.startsWith('## ')) {
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'heading-2',
|
||||
content: line.substring(3),
|
||||
});
|
||||
} else if (line.startsWith('### ')) {
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'heading-3',
|
||||
content: line.substring(4),
|
||||
});
|
||||
} else if (line.startsWith('> ')) {
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'quote',
|
||||
content: line.substring(2),
|
||||
});
|
||||
} else if (line.startsWith('```')) {
|
||||
const codeLines: string[] = [];
|
||||
i++;
|
||||
while (i < lines.length && !lines[i].startsWith('```')) {
|
||||
codeLines.push(lines[i]);
|
||||
i++;
|
||||
}
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'code',
|
||||
content: codeLines.join('\n'),
|
||||
});
|
||||
} else if (line.match(/^(\*|-) /)) {
|
||||
currentListItems.push(line.substring(2));
|
||||
// Check if next line is not a list item
|
||||
if (i === lines.length - 1 || (!lines[i + 1].match(/^(\*|-) /))) {
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'list',
|
||||
content: currentListItems.join('\n'),
|
||||
metadata: { listType: 'bullet' }
|
||||
});
|
||||
currentListItems = [];
|
||||
}
|
||||
} else if (line.match(/^\d+\. /)) {
|
||||
currentListItems.push(line.replace(/^\d+\. /, ''));
|
||||
// Check if next line is not a numbered list item
|
||||
if (i === lines.length - 1 || (!lines[i + 1].match(/^\d+\. /))) {
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'list',
|
||||
content: currentListItems.join('\n'),
|
||||
metadata: { listType: 'ordered' }
|
||||
});
|
||||
currentListItems = [];
|
||||
}
|
||||
} else if (line === '---' || line === '***' || line === '___') {
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'divider',
|
||||
content: ' ',
|
||||
});
|
||||
} else if (line.trim()) {
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'paragraph',
|
||||
content: line,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user