Text

MARKDOWN PREVIEW

Live Markdown editor with instant rendered preview. Supports all common syntax including tables, code blocks, and links.

Markdown
Words: 0 Chars: 0 Lines: 0
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  }

  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();