(function(){
function escapeHtml(value){
return String(value || '').replace(/[&<>"']/g, function(ch){
return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch];
});
}
function safeUrl(url){
url = String(url || '').trim();
if (!url) return '#';
try {
const parsed = new URL(url, window.location.origin);
if (!['http:', 'https:', 'mailto:'].includes(parsed.protocol)) return '#';
return parsed.href;
} catch (_) {
return '#';
}
}
function inlineMarkdown(text){
let out = escapeHtml(text);
out = out.replace(/`([^`]+)`/g, '$1');
out = out.replace(/\*\*([^*]+)\*\*/g, '$1');
out = out.replace(/\*([^*]+)\*/g, '$1');
out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(_, label, url){
return ''+label+'';
});
return out;
}
function renderMarkdown(markdown){
const lines = String(markdown || '').replace(/\r\n?/g, '\n').trim().split('\n');
if (!String(markdown || '').trim()) return '
Nothing to preview yet.
'; let html = '', para = [], list = null, quote = [], code = false, codeLines = []; function softBreaks(value){ return inlineMarkdown(value).replace(/\n/g, ''+softBreaks(para.join('\n'))+'
'; para = []; } } function flushList(){ if (!list) return; html += '<'+list.type+'>'; list.items.forEach(item => html += ''+softBreaks(quote.join('\n'))+''; quote = []; } } lines.forEach(function(line){ if (/^```/.test(line)) { if (code) { html += '
'+escapeHtml(codeLines.join('\n'))+'';
code = false; codeLines = [];
} else {
flushPara(); flushList(); flushQuote(); code = true;
}
return;
}
if (code) { codeLines.push(line); return; }
if (!line.trim()) { flushPara(); flushList(); flushQuote(); return; }
let m;
if ((m = line.match(/^(#{1,3})\s+(.+)$/))) {
flushPara(); flushList(); flushQuote();
const level = m[1].length + 2;
html += ''+escapeHtml(codeLines.join('\n'))+'';
flushPara(); flushList(); flushQuote();
return html;
}
function applyWrap(textarea, before, after, placeholder){
const start = textarea.selectionStart || 0;
const end = textarea.selectionEnd || 0;
const value = textarea.value;
const selected = value.slice(start, end) || placeholder || '';
textarea.value = value.slice(0, start) + before + selected + after + value.slice(end);
textarea.focus();
textarea.selectionStart = start + before.length;
textarea.selectionEnd = start + before.length + selected.length;
textarea.dispatchEvent(new Event('input', {bubbles:true}));
}
function applyLine(textarea, prefix){
const start = textarea.selectionStart || 0;
const value = textarea.value;
const lineStart = value.lastIndexOf('\n', Math.max(0, start - 1)) + 1;
textarea.value = value.slice(0, lineStart) + prefix + value.slice(lineStart);
textarea.focus();
textarea.selectionStart = textarea.selectionEnd = start + prefix.length;
textarea.dispatchEvent(new Event('input', {bubbles:true}));
}
function enhance(textarea){
if (textarea.dataset.markdownEnhanced === '1') return;
textarea.dataset.markdownEnhanced = '1';
const shell = document.createElement('div');
shell.className = 'md-editor';
const toolbar = document.createElement('div');
toolbar.className = 'md-toolbar';
const tools = [
['B', 'Bold', () => applyWrap(textarea, '**', '**', 'bold text')],
['I', 'Italic', () => applyWrap(textarea, '*', '*', 'italic text')],
['#', 'Heading', () => applyLine(textarea, '### ')],
['•', 'Bullet list', () => applyLine(textarea, '- ')],
['1.', 'Numbered list', () => applyLine(textarea, '1. ')],
['“”', 'Quote', () => applyLine(textarea, '> ')],
['{}', 'Code', () => applyWrap(textarea, '`', '`', 'code')],
['↗', 'Link', () => {
const url = window.prompt('Link URL');
if (url) applyWrap(textarea, '[', ']('+url+')', 'link text');
}]
];
tools.forEach(function(tool){
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'md-tool';
btn.textContent = tool[0];
btn.title = tool[1];
btn.setAttribute('aria-label', tool[1]);
btn.addEventListener('click', tool[2]);
toolbar.appendChild(btn);
});
const panes = document.createElement('div');
panes.className = 'md-panes';
const write = document.createElement('div');
write.className = 'md-write';
const preview = document.createElement('div');
preview.className = 'md-preview markdown-body';
preview.setAttribute('aria-label', 'Markdown preview');
textarea.parentNode.insertBefore(shell, textarea);
shell.appendChild(toolbar);
shell.appendChild(panes);
panes.appendChild(write);
panes.appendChild(preview);
write.appendChild(textarea);
textarea.classList.add('md-source');
function update(){ preview.innerHTML = renderMarkdown(textarea.value); }
textarea.addEventListener('input', update);
update();
}
document.addEventListener('DOMContentLoaded', function(){
document.querySelectorAll('textarea.markdown-editor').forEach(enhance);
});
})();