Files
dees-catalog/ts_web/elements/wysiwyg/wysiwyg.converters.ts

251 lines
8.7 KiB
TypeScript
Raw Normal View History

2025-06-23 17:36:39 +00:00
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 => {
2025-06-23 21:15:04 +00:00
// Check if content already contains HTML formatting
const content = block.content.includes('<') && block.content.includes('>')
? block.content // Already contains HTML formatting
: this.escapeHtml(block.content);
2025-06-23 17:36:39 +00:00
switch (block.type) {
case 'paragraph':
2025-06-23 21:15:04 +00:00
return block.content ? `<p>${content}</p>` : '';
2025-06-23 17:36:39 +00:00
case 'heading-1':
2025-06-23 21:15:04 +00:00
return `<h1>${content}</h1>`;
2025-06-23 17:36:39 +00:00
case 'heading-2':
2025-06-23 21:15:04 +00:00
return `<h2>${content}</h2>`;
2025-06-23 17:36:39 +00:00
case 'heading-3':
2025-06-23 21:15:04 +00:00
return `<h3>${content}</h3>`;
2025-06-23 17:36:39 +00:00
case 'quote':
2025-06-23 21:15:04 +00:00
return `<blockquote>${content}</blockquote>`;
2025-06-23 17:36:39 +00:00
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';
2025-06-24 10:45:06 +00:00
// Don't escape HTML in list items to preserve formatting
return `<${listTag}>${items.map(item => `<li>${item}</li>`).join('')}</${listTag}>`;
2025-06-23 17:36:39 +00:00
}
return '';
case 'divider':
return '<hr>';
default:
2025-06-23 21:15:04 +00:00
return `<p>${content}</p>`;
2025-06-23 17:36:39 +00:00
}
}).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',
2025-06-23 21:15:04 +00:00
content: element.innerHTML || '',
2025-06-23 17:36:39 +00:00
});
break;
case 'h1':
blocks.push({
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
type: 'heading-1',
2025-06-23 21:15:04 +00:00
content: element.innerHTML || '',
2025-06-23 17:36:39 +00:00
});
break;
case 'h2':
blocks.push({
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
type: 'heading-2',
2025-06-23 21:15:04 +00:00
content: element.innerHTML || '',
2025-06-23 17:36:39 +00:00
});
break;
case 'h3':
blocks.push({
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
type: 'heading-3',
2025-06-23 21:15:04 +00:00
content: element.innerHTML || '',
2025-06-23 17:36:39 +00:00
});
break;
case 'blockquote':
blocks.push({
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
type: 'quote',
2025-06-23 21:15:04 +00:00
content: element.innerHTML || '',
2025-06-23 17:36:39 +00:00
});
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'));
2025-06-24 10:45:06 +00:00
// Use innerHTML to preserve formatting
const content = listItems.map(li => li.innerHTML || '').join('\n');
2025-06-23 17:36:39 +00:00
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;
}
}