`;
});
// Images (base64 or URL)
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '');
// Links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
// Bold **text** or __text__
html = html.replace(/\*\*(.+?)\*\*/g, '$1');
html = html.replace(/__(.+?)__/g, '$1');
// Italic *text* or _text_ (not inside words)
html = html.replace(/(?$1');
html = html.replace(/(?$1');
// Inline code `code`
html = html.replace(/`([^`]+)`/g, '$1');
// Headings
html = html.replace(/^### (.+)$/gm, '
$1
');
html = html.replace(/^## (.+)$/gm, '
$1
');
html = html.replace(/^# (.+)$/gm, '
$1
');
// Lists - unordered
html = html.replace(/^[-*] (.+)$/gm, '
$1
');
// Lists - ordered
html = html.replace(/^\d+\. (.+)$/gm, '
$1
');
// Wrap consecutive list items
html = html.replace(/((?:
.+<\/li>\s*)+)/g, '
$1
');
html = html.replace(/((?:
.+<\/li>\s*)+)/g, '$1');
html = html.replace(/ data-list="[uo]l"/g, '');
// Paragraphs - wrap remaining text blocks
html = html.split('\n\n').map(block => {
block = block.trim();
if (!block) return '';
if (/^<(h[1-3]|ul|ol|pre|table|p)/.test(block)) return block;
if (block.includes('<')) return block; // Already has HTML
return `
${block.replace(/\n/g, ' ')}
`;
}).join('\n');
return html;
},
// Convert HTML from editor back to markdown
toMarkdown(html) {
if (!html) return '';
const doc = new DOMParser().parseFromString(html, 'text/html');
return this._nodeToMd(doc.body).trim();
},
_nodeToMd(node) {
let md = '';
for (const child of node.childNodes) {
if (child.nodeType === Node.TEXT_NODE) {
md += child.textContent;
} else if (child.nodeType === Node.ELEMENT_NODE) {
const tag = child.tagName.toLowerCase();
const inner = this._nodeToMd(child);
switch (tag) {
case 'strong': case 'b': md += `**${inner}**`; break;
case 'em': case 'i': md += `*${inner}*`; break;
case 'code':
if (child.parentElement?.tagName === 'PRE') md += inner;
else md += `\`${inner}\``;
break;
case 'pre': md += `\`\`\`\n${inner}\n\`\`\`\n\n`; break;
case 'a': md += `[${inner}](${child.getAttribute('href') || ''})`; break;
case 'img': md += ` || ''})`; break;
case 'h1': md += `# ${inner}\n\n`; break;
case 'h2': md += `## ${inner}\n\n`; break;
case 'h3': md += `### ${inner}\n\n`; break;
case 'p': md += `${inner}\n\n`; break;
case 'br': md += '\n'; break;
case 'li': md += `- ${inner}\n`; break;
case 'ul': case 'ol': md += inner + '\n'; break;
case 'table': md += this._tableToMd(child) + '\n\n'; break;
case 'thead': case 'tbody': case 'tr': case 'th': case 'td': md += inner; break;
case 'div': md += inner + '\n'; break;
default: md += inner;
}
}
}
return md;
},
_tableToMd(table) {
const rows = Array.from(table.querySelectorAll('tr'));
if (!rows.length) return '';
const lines = [];
rows.forEach((row, i) => {
const cells = Array.from(row.querySelectorAll('th, td')).map(c => c.textContent.trim());
lines.push('| ' + cells.join(' | ') + ' |');
if (i === 0) lines.push('|' + cells.map(() => '---').join('|') + '|');
});
return lines.join('\n');
},
// Convert HTML to plain text
toPlainText(html) {
if (!html) return '';
const doc = new DOMParser().parseFromString(html, 'text/html');
return doc.body.textContent || '';
}
};
// ---- Clipboard utilities for multiple formats ----
const ClipboardUtils = {
// Copy to clipboard with multiple formats (HTML primary, plain text also)
async copyHtml(html, markdown) {
const plain = MarkdownUtils.toPlainText(html);
try {
// Modern API: write multiple formats
await navigator.clipboard.write([
new ClipboardItem({
'text/html': new Blob([html], { type: 'text/html' }),
'text/plain': new Blob([plain], { type: 'text/plain' })
})
]);
return true;
} catch {
// Fallback: plain text only
await navigator.clipboard.writeText(plain);
return true;
}
},
async copyMarkdown(markdown) {
await navigator.clipboard.writeText(markdown);
return true;
},
async copyPlainText(html) {
const plain = MarkdownUtils.toPlainText(html);
await navigator.clipboard.writeText(plain);
return true;
}
};
Object.assign(window, {
STATUSES, STATUS_BY_KEY, STATUS_ORDER, PRIORITIES,
parseDate, daysBetween, isOverdue, dueRelative, fmtTimestamp, fmtBytes, fmtRelative, fmtShortDate,
walkTree, findItem, allTags, countAll, countByStatus, filterTree,
Icon, StatusIcon, ColorStatusIcon, STATUS_COLORS, STATUS_EMOJI, StatusStyleContext,
PROGRESS_STEPS, snapProgress, progressFor, shouldShowProgress, cycleProgress, ProgressGauge,
MarkdownUtils, ClipboardUtils
});