import * as colors from './00colors.js'; import { DeesInputBase } from './dees-input-base.js'; import { demoFunc } from './dees-input-wysiwyg.demo.js'; import { customElement, type TemplateResult, property, html, cssManager, css, state, } from '@design.estate/dees-element'; import * as domtools from '@design.estate/dees-domtools'; declare global { interface HTMLElementTagNameMap { 'dees-input-wysiwyg': DeesInputWysiwyg; } } interface IBlock { id: string; type: 'paragraph' | 'heading-1' | 'heading-2' | 'heading-3' | 'image' | 'code' | 'quote' | 'list' | 'divider'; content: string; metadata?: any; } @customElement('dees-input-wysiwyg') export class DeesInputWysiwyg extends DeesInputBase { public static demo = demoFunc; @property({ type: String }) public value: string = ''; @property({ type: String }) public outputFormat: 'html' | 'markdown' = 'html'; @state() private blocks: IBlock[] = [ { id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type: 'paragraph', content: '', } ]; @state() private selectedBlockId: string | null = null; @state() private showSlashMenu: boolean = false; @state() private slashMenuPosition: { x: number; y: number } = { x: 0, y: 0 }; @state() private slashMenuFilter: string = ''; @state() private slashMenuSelectedIndex: number = 0; private editorContentRef: HTMLDivElement; private isComposing: boolean = false; public static styles = [ ...DeesInputBase.baseStyles, cssManager.defaultStyles, css` :host { display: block; position: relative; } .wysiwyg-container { background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')}; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border-radius: 8px; min-height: 200px; padding: 20px; position: relative; transition: all 0.2s ease; color: ${cssManager.bdTheme('#000000', '#ffffff')}; } .wysiwyg-container:hover { border-color: ${cssManager.bdTheme('#d0d0d0', '#444')}; } .wysiwyg-container:focus-within { border-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.1)', 'rgba(77, 148, 255, 0.1)')}; } .editor-content { outline: none; min-height: 160px; } .block { margin-bottom: 12px; position: relative; transition: all 0.15s ease; min-height: 1.6em; color: ${cssManager.bdTheme('#000000', '#e0e0e0')}; } .block:last-child { margin-bottom: 0; } .block.selected { background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.08)', 'rgba(77, 148, 255, 0.12)')}; margin-left: -12px; margin-right: -12px; padding: 8px 12px; border-radius: 6px; } .block[contenteditable] { outline: none; } .block.paragraph { font-size: 16px; line-height: 1.6; font-weight: 400; } .block.paragraph:empty::before { content: "Type '/' for commands..."; color: ${cssManager.bdTheme('#999', '#666')}; pointer-events: none; font-size: 16px; line-height: 1.6; font-weight: 400; } .block.heading-1 { font-size: 32px; font-weight: 700; line-height: 1.2; margin-bottom: 16px; color: ${cssManager.bdTheme('#000000', '#ffffff')}; } .block.heading-1:empty::before { content: "Heading 1"; color: ${cssManager.bdTheme('#999', '#666')}; pointer-events: none; font-size: 32px; line-height: 1.2; font-weight: 700; } .block.heading-2 { font-size: 24px; font-weight: 600; line-height: 1.3; margin-bottom: 14px; color: ${cssManager.bdTheme('#000000', '#ffffff')}; } .block.heading-2:empty::before { content: "Heading 2"; color: ${cssManager.bdTheme('#999', '#666')}; pointer-events: none; font-size: 24px; line-height: 1.3; font-weight: 600; } .block.heading-3 { font-size: 20px; font-weight: 600; line-height: 1.4; margin-bottom: 12px; color: ${cssManager.bdTheme('#000000', '#ffffff')}; } .block.heading-3:empty::before { content: "Heading 3"; color: ${cssManager.bdTheme('#999', '#666')}; pointer-events: none; font-size: 20px; line-height: 1.4; font-weight: 600; } .block.quote { border-left: 3px solid ${cssManager.bdTheme('#0066cc', '#4d94ff')}; padding-left: 16px; font-style: italic; color: ${cssManager.bdTheme('#555', '#b0b0b0')}; margin-left: 0; margin-right: 0; } .block.quote:empty::before { content: "Quote"; color: ${cssManager.bdTheme('#999', '#666')}; pointer-events: none; font-size: 16px; line-height: 1.6; font-weight: 400; font-style: italic; } .block.code { background: ${cssManager.bdTheme('#f8f8f8', '#0d0d0d')}; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')}; border-radius: 6px; padding: 12px 16px; font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace; font-size: 14px; white-space: pre-wrap; color: ${cssManager.bdTheme('#d14', '#ff6b6b')}; overflow-x: auto; } .block.code:empty::before { content: "// Code block"; color: ${cssManager.bdTheme('#999', '#666')}; pointer-events: none; font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace; font-size: 14px; line-height: 1.6; font-weight: 400; } .block.list { padding-left: 24px; } .block.list ul, .block.list ol { margin: 0; padding: 0; list-style-position: inside; } .block.list ul { list-style: disc; } .block.list ol { list-style: decimal; } .block.list li { margin-bottom: 4px; line-height: 1.6; } .block.divider { text-align: center; padding: 20px 0; cursor: default; pointer-events: none; } .block.divider hr { border: none; border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; margin: 0; } .slash-menu { position: absolute; background: ${cssManager.bdTheme('#ffffff', '#262626')}; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')}; border-radius: 8px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); padding: 4px; z-index: 1000; min-width: 220px; max-height: 300px; overflow-y: auto; } .slash-menu-item { padding: 10px 12px; cursor: pointer; transition: all 0.15s ease; display: flex; align-items: center; gap: 12px; border-radius: 4px; color: ${cssManager.bdTheme('#000000', '#e0e0e0')}; font-size: 14px; } .slash-menu-item:hover, .slash-menu-item.selected { background: ${cssManager.bdTheme('#f0f0f0', '#333333')}; color: ${cssManager.bdTheme('#000000', '#ffffff')}; } .slash-menu-item .icon { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; font-size: 16px; color: ${cssManager.bdTheme('#666', '#999')}; font-weight: 600; } .slash-menu-item:hover .icon, .slash-menu-item.selected .icon { color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; } .toolbar { position: absolute; top: -40px; left: 0; background: ${cssManager.bdTheme('#ffffff', '#262626')}; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')}; border-radius: 6px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); padding: 4px; display: none; gap: 4px; z-index: 1000; } .toolbar.visible { display: flex; } .toolbar-button { width: 32px; height: 32px; border: none; background: transparent; cursor: pointer; border-radius: 4px; transition: all 0.15s ease; display: flex; align-items: center; justify-content: center; color: ${cssManager.bdTheme('#000000', '#e0e0e0')}; } .toolbar-button:hover { background: ${cssManager.bdTheme('#f0f0f0', '#333333')}; color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; } `, ]; async firstUpdated() { this.updateValue(); this.editorContentRef = this.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; } render(): TemplateResult { return html`
${this.blocks.map(block => this.renderBlock(block))}
${this.showSlashMenu ? this.renderSlashMenu() : ''}
`; } private renderBlock(block: IBlock): TemplateResult { const isSelected = this.selectedBlockId === block.id; if (block.type === 'divider') { return html`

`; } if (block.type === 'list') { return html`
`; } return html`
`; } private renderListContent(content: string, metadata?: any): string { const items = content.split('\n').filter(item => item.trim()); if (items.length === 0) return ''; const listTag = metadata?.listType === 'ordered' ? 'ol' : 'ul'; return `<${listTag}>${items.map(item => `
  • ${this.escapeHtml(item)}
  • `).join('')}`; } private getFilteredMenuItems() { const allItems = [ { type: 'paragraph', label: 'Paragraph', icon: '¶' }, { type: 'heading-1', label: 'Heading 1', icon: 'H₁' }, { type: 'heading-2', label: 'Heading 2', icon: 'H₂' }, { type: 'heading-3', label: 'Heading 3', icon: 'H₃' }, { type: 'quote', label: 'Quote', icon: '"' }, { type: 'code', label: 'Code', icon: '<>' }, { type: 'list', label: 'List', icon: '•' }, { type: 'divider', label: 'Divider', icon: '—' }, ]; return allItems.filter(item => this.slashMenuFilter === '' || item.label.toLowerCase().includes(this.slashMenuFilter.toLowerCase()) ); } private renderSlashMenu(): TemplateResult { const menuItems = this.getFilteredMenuItems(); return html`
    ${menuItems.map((item, index) => html`
    ${item.icon} ${item.label}
    `)}
    `; } private handleBlockInput(e: InputEvent, block: IBlock) { if (this.isComposing) return; const target = e.target as HTMLDivElement; if (block.type === 'list') { // Extract text from list items const listItems = target.querySelectorAll('li'); block.content = Array.from(listItems).map(li => li.textContent || '').join('\n'); // Preserve list type const listElement = target.querySelector('ol, ul'); if (listElement) { block.metadata = { listType: listElement.tagName.toLowerCase() === 'ol' ? 'ordered' : 'bullet' }; } } else { block.content = target.textContent || ''; } // Check for shortcuts at the beginning of a paragraph if (block.type === 'paragraph') { // Check for heading shortcuts const headingPatterns = [ { pattern: /^# $/, type: 'heading-1' }, { pattern: /^## $/, type: 'heading-2' }, { pattern: /^### $/, type: 'heading-3' } ]; for (const { pattern, type } of headingPatterns) { if (pattern.test(block.content)) { e.preventDefault(); block.type = type as IBlock['type']; block.content = ''; // Force update the block setTimeout(() => { const blockElement = this.shadowRoot!.querySelector(`[data-block-id="${block.id}"]`) as HTMLDivElement; if (blockElement) { blockElement.textContent = ''; blockElement.focus(); } }); this.updateValue(); this.requestUpdate(); return; } } // Check for list shortcuts const listPatterns = [ { pattern: /^(\*|-) $/, type: 'bullet' }, { pattern: /^(\d+)\. $/, type: 'ordered' }, { pattern: /^(\d+)\) $/, type: 'ordered' } ]; for (const { pattern, type } of listPatterns) { if (pattern.test(block.content)) { e.preventDefault(); block.type = 'list'; block.content = ''; block.metadata = { listType: type }; // Force update the block to be a list setTimeout(() => { const blockElement = this.shadowRoot!.querySelector(`[data-block-id="${block.id}"]`) as HTMLDivElement; if (blockElement) { const listTag = type === 'ordered' ? 'ol' : 'ul'; blockElement.innerHTML = `<${listTag}>
  • `; // Focus the first list item const firstLi = blockElement.querySelector('li'); if (firstLi) { const range = document.createRange(); const sel = window.getSelection(); range.selectNodeContents(firstLi); range.collapse(true); sel!.removeAllRanges(); sel!.addRange(range); } } }); this.updateValue(); this.requestUpdate(); return; } } // Check for quote shortcut if (block.content === '> ') { e.preventDefault(); block.type = 'quote'; block.content = ''; setTimeout(() => { const blockElement = this.shadowRoot!.querySelector(`[data-block-id="${block.id}"]`) as HTMLDivElement; if (blockElement) { blockElement.textContent = ''; blockElement.focus(); } }); this.updateValue(); this.requestUpdate(); return; } // Check for code block shortcut if (block.content === '```') { e.preventDefault(); block.type = 'code'; block.content = ''; setTimeout(() => { const blockElement = this.shadowRoot!.querySelector(`[data-block-id="${block.id}"]`) as HTMLDivElement; if (blockElement) { blockElement.textContent = ''; blockElement.focus(); } }); this.updateValue(); this.requestUpdate(); return; } // Check for divider shortcut if (block.content === '---' || block.content === '***' || block.content === '___') { e.preventDefault(); block.type = 'divider'; block.content = ' '; // Create a new paragraph block after the divider const blockIndex = this.blocks.findIndex(b => b.id === block.id); const newBlock: IBlock = { id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type: 'paragraph', content: '', }; this.blocks = [...this.blocks.slice(0, blockIndex + 1), newBlock, ...this.blocks.slice(blockIndex + 1)]; setTimeout(() => { const newBlockElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`) as HTMLDivElement; if (newBlockElement) { newBlockElement.focus(); } }); this.updateValue(); this.requestUpdate(); return; } } // Check for slash commands (should be last to allow other shortcuts to take precedence) if (block.content.startsWith('/') && block.type === 'paragraph') { this.slashMenuFilter = block.content.slice(1); this.showSlashMenu = true; this.slashMenuSelectedIndex = 0; const rect = target.getBoundingClientRect(); const containerRect = this.shadowRoot!.querySelector('.wysiwyg-container')!.getBoundingClientRect(); this.slashMenuPosition = { x: rect.left - containerRect.left, y: rect.bottom - containerRect.top + 4 }; } else { this.closeSlashMenu(); } this.updateValue(); } private handleBlockKeyDown(e: KeyboardEvent, block: IBlock) { if (this.showSlashMenu && ['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(e.key)) { this.handleSlashMenuKeyboard(e); return; } // Handle Tab key for indentation if (e.key === 'Tab') { if (block.type === 'code') { // Allow tab in code blocks e.preventDefault(); document.execCommand('insertText', false, ' '); return; } else if (block.type === 'list') { // Future: implement list indentation e.preventDefault(); return; } } if (e.key === 'Enter' && !e.shiftKey) { if (block.type === 'list') { // Handle Enter in lists differently const target = e.target as HTMLDivElement; const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const currentLi = range.startContainer.parentElement?.closest('li'); if (currentLi && currentLi.textContent === '') { // Empty list item - exit list mode e.preventDefault(); const blockIndex = this.blocks.findIndex(b => b.id === block.id); const newBlock: IBlock = { id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type: 'paragraph', content: '', }; this.blocks = [...this.blocks.slice(0, blockIndex + 1), newBlock, ...this.blocks.slice(blockIndex + 1)]; setTimeout(() => { const newBlockElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`) as HTMLDivElement; if (newBlockElement) { newBlockElement.focus(); } }); } // Otherwise, let the browser handle creating new list items } return; } e.preventDefault(); const blockIndex = this.blocks.findIndex(b => b.id === block.id); const newBlock: IBlock = { id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type: 'paragraph', content: '', }; this.blocks = [...this.blocks.slice(0, blockIndex + 1), newBlock, ...this.blocks.slice(blockIndex + 1)]; setTimeout(() => { const newBlockElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`) as HTMLDivElement; if (newBlockElement) { newBlockElement.focus(); } }); } else if (e.key === 'Backspace' && block.content === '' && this.blocks.length > 1) { e.preventDefault(); const blockIndex = this.blocks.findIndex(b => b.id === block.id); if (blockIndex > 0) { const prevBlock = this.blocks[blockIndex - 1]; this.blocks = this.blocks.filter(b => b.id !== block.id); setTimeout(() => { const prevBlockElement = this.shadowRoot!.querySelector(`[data-block-id="${prevBlock.id}"]`) as HTMLDivElement; if (prevBlockElement && prevBlock.type !== 'divider') { prevBlockElement.focus(); this.setCursorToEnd(prevBlockElement); } }); } } } private handleSlashMenuKeyboard(e: KeyboardEvent) { const menuItems = this.getFilteredMenuItems(); switch(e.key) { case 'ArrowDown': e.preventDefault(); this.slashMenuSelectedIndex = (this.slashMenuSelectedIndex + 1) % menuItems.length; break; case 'ArrowUp': e.preventDefault(); this.slashMenuSelectedIndex = this.slashMenuSelectedIndex === 0 ? menuItems.length - 1 : this.slashMenuSelectedIndex - 1; break; case 'Enter': e.preventDefault(); if (menuItems[this.slashMenuSelectedIndex]) { this.insertBlock(menuItems[this.slashMenuSelectedIndex].type as IBlock['type']); } break; case 'Escape': e.preventDefault(); this.closeSlashMenu(); break; } } private closeSlashMenu() { this.showSlashMenu = false; this.slashMenuFilter = ''; this.slashMenuSelectedIndex = 0; } private setCursorToEnd(element: HTMLElement) { const range = document.createRange(); const sel = window.getSelection(); range.selectNodeContents(element); range.collapse(false); sel!.removeAllRanges(); sel!.addRange(range); } private handleBlockFocus(block: IBlock) { if (block.type !== 'divider') { this.selectedBlockId = block.id; } } private handleBlockBlur(block: IBlock) { setTimeout(() => { if (this.selectedBlockId === block.id) { this.selectedBlockId = null; } this.closeSlashMenu(); }, 200); } private handleEditorClick(e: MouseEvent) { const target = e.target as HTMLElement; if (target.classList.contains('editor-content')) { const lastBlock = this.blocks[this.blocks.length - 1]; const lastBlockElement = this.shadowRoot!.querySelector(`[data-block-id="${lastBlock.id}"]`) as HTMLDivElement; if (lastBlockElement && lastBlock.type !== 'divider') { lastBlockElement.focus(); this.setCursorToEnd(lastBlockElement); } } } private insertBlock(type: IBlock['type']) { const currentBlockIndex = this.blocks.findIndex(b => b.id === this.selectedBlockId); const currentBlock = this.blocks[currentBlockIndex]; if (currentBlock && currentBlock.content.startsWith('/')) { currentBlock.type = type; currentBlock.content = ''; if (type === 'divider') { currentBlock.content = ' '; const newBlock: IBlock = { id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type: 'paragraph', content: '', }; this.blocks = [...this.blocks.slice(0, currentBlockIndex + 1), newBlock, ...this.blocks.slice(currentBlockIndex + 1)]; setTimeout(() => { const newBlockElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`) as HTMLDivElement; if (newBlockElement) { newBlockElement.focus(); } }); } else { // Force update the contenteditable element setTimeout(() => { const blockElement = this.shadowRoot!.querySelector(`[data-block-id="${currentBlock.id}"]`) as HTMLDivElement; if (blockElement) { blockElement.textContent = ''; blockElement.focus(); } }); } } this.closeSlashMenu(); this.updateValue(); } private updateValue() { if (this.outputFormat === 'html') { this.value = this.getHtmlOutput(); } else { this.value = this.getMarkdownOutput(); } this.changeSubject.next(this.value); } private getHtmlOutput(): string { return this.blocks.map(block => { switch (block.type) { case 'paragraph': return block.content ? `

    ${this.escapeHtml(block.content)}

    ` : ''; case 'heading-1': return `

    ${this.escapeHtml(block.content)}

    `; case 'heading-2': return `

    ${this.escapeHtml(block.content)}

    `; case 'heading-3': return `

    ${this.escapeHtml(block.content)}

    `; case 'quote': return `
    ${this.escapeHtml(block.content)}
    `; case 'code': return `
    ${this.escapeHtml(block.content)}
    `; 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 => `
  • ${this.escapeHtml(item)}
  • `).join('')}`; } return ''; case 'divider': return '
    '; default: return `

    ${this.escapeHtml(block.content)}

    `; } }).filter(html => html !== '').join('\n'); } private getMarkdownOutput(): string { return this.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'); } private escapeHtml(text: string): string { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } public getValue(): string { return this.value; } public setValue(value: string): void { this.value = value; if (this.outputFormat === 'html') { this.blocks = this.parseHtmlToBlocks(value); } else { this.blocks = this.parseMarkdownToBlocks(value); } if (this.blocks.length === 0) { this.blocks = [{ id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type: 'paragraph', content: '', }]; } this.changeSubject.next(this.value); this.requestUpdate(); } private 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).substr(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).substr(2, 9)}`, type: 'paragraph', content: element.textContent || '', }); break; case 'h1': blocks.push({ id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type: 'heading-1', content: element.textContent || '', }); break; case 'h2': blocks.push({ id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type: 'heading-2', content: element.textContent || '', }); break; case 'h3': blocks.push({ id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type: 'heading-3', content: element.textContent || '', }); break; case 'blockquote': blocks.push({ id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type: 'quote', content: element.textContent || '', }); break; case 'pre': case 'code': blocks.push({ id: `block-${Date.now()}-${Math.random().toString(36).substr(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).substr(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).substr(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; } private 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).substr(2, 9)}`, type: 'heading-1', content: line.substring(2), }); } else if (line.startsWith('## ')) { blocks.push({ id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type: 'heading-2', content: line.substring(3), }); } else if (line.startsWith('### ')) { blocks.push({ id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type: 'heading-3', content: line.substring(4), }); } else if (line.startsWith('> ')) { blocks.push({ id: `block-${Date.now()}-${Math.random().toString(36).substr(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).substr(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).substr(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).substr(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).substr(2, 9)}`, type: 'divider', content: ' ', }); } else if (line.trim()) { blocks.push({ id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type: 'paragraph', content: line, }); } } return blocks; } }