329 lines
12 KiB
TypeScript
329 lines
12 KiB
TypeScript
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 formatFileSize(bytes: number): string {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
static getHtmlOutput(blocks: IBlock[]): string {
|
|
return blocks.map(block => {
|
|
// 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);
|
|
|
|
switch (block.type) {
|
|
case 'paragraph':
|
|
return block.content ? `<p>${content}</p>` : '';
|
|
case 'heading-1':
|
|
return `<h1>${content}</h1>`;
|
|
case 'heading-2':
|
|
return `<h2>${content}</h2>`;
|
|
case 'heading-3':
|
|
return `<h3>${content}</h3>`;
|
|
case 'quote':
|
|
return `<blockquote>${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';
|
|
// Don't escape HTML in list items to preserve formatting
|
|
return `<${listTag}>${items.map(item => `<li>${item}</li>`).join('')}</${listTag}>`;
|
|
}
|
|
return '';
|
|
case 'divider':
|
|
return '<hr>';
|
|
case 'image':
|
|
const imageUrl = block.metadata?.url;
|
|
if (imageUrl) {
|
|
const altText = this.escapeHtml(block.content || 'Image');
|
|
return `<img src="${imageUrl}" alt="${altText}" />`;
|
|
}
|
|
return '';
|
|
case 'youtube':
|
|
const videoId = block.metadata?.videoId;
|
|
if (videoId) {
|
|
return `<iframe width="560" height="315" src="https://www.youtube.com/embed/${videoId}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`;
|
|
}
|
|
return '';
|
|
case 'markdown':
|
|
// Return the raw markdown content wrapped in a div
|
|
return `<div class="markdown-content">${this.escapeHtml(block.content)}</div>`;
|
|
case 'html':
|
|
// Return the raw HTML content (already HTML)
|
|
return block.content;
|
|
case 'attachment':
|
|
const files = block.metadata?.files || [];
|
|
if (files.length > 0) {
|
|
return `<div class="attachments">${files.map((file: any) =>
|
|
`<div class="attachment-item" data-file-id="${file.id}">
|
|
<a href="${file.data}" download="${file.name}">${this.escapeHtml(file.name)}</a>
|
|
<span class="file-size">(${this.formatFileSize(file.size)})</span>
|
|
</div>`
|
|
).join('')}</div>`;
|
|
}
|
|
return '';
|
|
default:
|
|
return `<p>${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 '---';
|
|
case 'image':
|
|
const imageUrl = block.metadata?.url;
|
|
const altText = block.content || 'Image';
|
|
return imageUrl ? `` : '';
|
|
case 'youtube':
|
|
const videoId = block.metadata?.videoId;
|
|
const url = block.metadata?.url || (videoId ? `https://youtube.com/watch?v=${videoId}` : '');
|
|
return url ? `[YouTube Video](${url})` : '';
|
|
case 'markdown':
|
|
// Return the raw markdown content
|
|
return block.content;
|
|
case 'html':
|
|
// Return as HTML comment in markdown
|
|
return `<!-- HTML Block\n${block.content}\n-->`;
|
|
case 'attachment':
|
|
const files = block.metadata?.files || [];
|
|
if (files.length > 0) {
|
|
return files.map((file: any) => `- [${file.name}](${file.data})`).join('\n');
|
|
}
|
|
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.innerHTML || '',
|
|
});
|
|
break;
|
|
case 'h1':
|
|
blocks.push({
|
|
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
type: 'heading-1',
|
|
content: element.innerHTML || '',
|
|
});
|
|
break;
|
|
case 'h2':
|
|
blocks.push({
|
|
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
type: 'heading-2',
|
|
content: element.innerHTML || '',
|
|
});
|
|
break;
|
|
case 'h3':
|
|
blocks.push({
|
|
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
type: 'heading-3',
|
|
content: element.innerHTML || '',
|
|
});
|
|
break;
|
|
case 'blockquote':
|
|
blocks.push({
|
|
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
type: 'quote',
|
|
content: element.innerHTML || '',
|
|
});
|
|
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'));
|
|
// Use innerHTML to preserve formatting
|
|
const content = listItems.map(li => li.innerHTML || '').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;
|
|
case 'img':
|
|
const imgElement = element as HTMLImageElement;
|
|
blocks.push({
|
|
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
type: 'image',
|
|
content: imgElement.alt || '',
|
|
metadata: { url: imgElement.src }
|
|
});
|
|
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.match(/^!\[([^\]]*)\]\(([^\)]+)\)$/)) {
|
|
// Parse markdown image syntax 
|
|
const match = line.match(/^!\[([^\]]*)\]\(([^\)]+)\)$/);
|
|
if (match) {
|
|
blocks.push({
|
|
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
type: 'image',
|
|
content: match[1] || '',
|
|
metadata: { url: match[2] }
|
|
});
|
|
}
|
|
} else if (line.trim()) {
|
|
blocks.push({
|
|
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
type: 'paragraph',
|
|
content: line,
|
|
});
|
|
}
|
|
}
|
|
|
|
return blocks;
|
|
}
|
|
} |