Text
MARKDOWN PREVIEW
Live Markdown editor with instant rendered preview. Supports all common syntax including tables, code blocks, and links.
Markdown
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
function escapeHtml(s) {
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
}
function parseMarkdown(md) {
const lines = md.split('\n');
let html = '', i = 0;
while (i < lines.length) {
const line = lines[i];
// Fenced code block
if (line.match(/^```/)) {
const lang = line.slice(3).trim();
let code = '';
i++;
while (i < lines.length && !lines[i].match(/^```/)) { code += escapeHtml(lines[i]) + '\n'; i++; }
html += `<pre><code>${code}</code></pre>`;
i++; continue;
}
// Headings
const hm = line.match(/^(#{1,6})\s+(.*)/);
if (hm) { const lvl = hm[1].length; html += `<h${lvl}>${inline(hm[2])}</h${lvl}>`; i++; continue; }
// HR
if (line.match(/^---+$/) || line.match(/^\*\*\*+$/) || line.match(/^___+$/)) { html += '<hr>'; i++; continue; }
// Blockquote
if (line.startsWith('> ')) {
let bq = '';
while (i < lines.length && lines[i].startsWith('> ')) { bq += lines[i].slice(2) + '\n'; i++; }
html += `<blockquote>${parseMarkdown(bq.trim())}</blockquote>`; continue;
}
// Unordered list
if (line.match(/^[-*+]\s/)) {
html += '<ul>';
while (i < lines.length && lines[i].match(/^[-*+]\s/)) { html += `<li>${inline(lines[i].slice(2))}</li>`; i++; }
html += '</ul>'; continue;
}
// Ordered list
if (line.match(/^\d+\.\s/)) {
html += '<ol>';
while (i < lines.length && lines[i].match(/^\d+\.\s/)) { html += `<li>${inline(lines[i].replace(/^\d+\.\s/, ''))}</li>`; i++; }
html += '</ol>'; continue;
}
// Table
if (line.includes('|') && lines[i+1] && lines[i+1].match(/^[|\s:-]+$/)) {
const headers = line.split('|').filter((_, idx, arr) => idx > 0 && idx < arr.length - 1).map(h => `<th>${inline(h.trim())}</th>`).join('');
i += 2;
let rows = '';
while (i < lines.length && lines[i].includes('|')) {
const cells = lines[i].split('|').filter((_, idx, arr) => idx > 0 && idx < arr.length - 1).map(c => `<td>${inline(c.trim())}</td>`).join('');
rows += `<tr>${cells}</tr>`;
i++;
}
html += `<table><thead><tr>${headers}</tr></thead><tbody>${rows}</tbody></table>`; continue;
}
// Empty line
if (!line.trim()) { html += ''; i++; continue; }
// Paragraph
let para = '';
while (i < lines.length && lines[i].trim() && !lines[i].match(/^[#>\-*+`|]/) && !lines[i].match(/^\d+\./)) {
para += (para ? ' ' : '') + lines[i]; i++;
}
if (para) html += `<p>${inline(para)}</p>`;
else i++;
}
return html;
}
function inline(s) {
return s
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/~~(.+?)~~/g, '<del>$1</del>')
.replace(/__(.+?)__/g, '<strong>$1</strong>')
.replace(/_(.+?)_/g, '<em>$1</em>');
}
function render() {
const md = document.getElementById('mdInput').value;
document.getElementById('previewContent').innerHTML = parseMarkdown(md);
const words = md.trim() ? md.trim().split(/\s+/).length : 0;
document.getElementById('statWords').textContent = words;
document.getElementById('statChars').textContent = md.length;
document.getElementById('statLines').textContent = md.split('\n').length;
}
function wrap(before, after) {
const ta = document.getElementById('mdInput');
const s = ta.selectionStart, e = ta.selectionEnd;
const sel = ta.value.slice(s, e);
ta.setRangeText(before + sel + after, s, e, 'end');
render();
}
function insertLine(prefix) {
const ta = document.getElementById('mdInput');
const s = ta.selectionStart;
const lineStart = ta.value.lastIndexOf('\n', s - 1) + 1;
ta.setRangeText('\n' + prefix, s, s, 'end');
render();
}
function insertLink() {
const ta = document.getElementById('mdInput');
const s = ta.selectionStart, e = ta.selectionEnd;
const sel = ta.value.slice(s, e) || 'link text';
ta.setRangeText(`[${sel}](url)`, s, e, 'end');
render();
}
function insertCode() {
const ta = document.getElementById('mdInput');
const s = ta.selectionStart;
ta.setRangeText('\n```\n\n```\n', s, s, 'end');
render();
}
function copyMd() {
const text = document.getElementById('mdInput').value;
navigator.clipboard.writeText(text).catch(() => {});
const btn = document.getElementById('copyMdBtn');
btn.textContent = 'copied!'; btn.classList.add('copied');
setTimeout(() => { btn.textContent = 'copy md'; btn.classList.remove('copied'); }, 2000);
}
function copyHtml() {
const html = document.getElementById('previewContent').innerHTML;
navigator.clipboard.writeText(html).catch(() => {});
const btn = document.getElementById('copyHtmlBtn');
btn.textContent = 'copied!'; btn.classList.add('copied');
setTimeout(() => { btn.textContent = 'copy html'; btn.classList.remove('copied'); }, 2000);
}
function showMobile(view) {
const editorPane = document.getElementById('editorPane');
const previewPane = document.getElementById('previewPane');
if (view === 'editor') {
editorPane.classList.remove('mobile-hide');
previewPane.classList.remove('mobile-show');
document.getElementById('btnEditor').classList.add('active');
document.getElementById('btnPreview').classList.remove('active');
} else {
editorPane.classList.add('mobile-hide');
previewPane.classList.add('mobile-show');
document.getElementById('btnEditor').classList.remove('active');
document.getElementById('btnPreview').classList.add('active');
}
}
render();