// md-mini.js — a tiny, dependency-free Markdown -> HTML renderer.
//
// Version: b4.1F6r3C  (2026-06-06 17:03 America/New_York). Bump on every change.
//
// Scope is deliberately narrow: a common GitHub-flavored Markdown subset —
// ATX headings, pipe tables, fenced code, ordered/unordered lists,
// blockquotes, horizontal rules, and inline code/bold/italic/links. It is NOT
// a general CommonMark parser; it covers the constructs typical project
// documentation uses, and a test harness verifies it.
//
// Pure string -> string (no DOM), so it runs the same in the browser and Node.
// Output is escaped before any markup is emitted, so rendering untrusted-ish
// markdown can't inject script. Keep it that way.

const ESC = { '&': '&amp;', '<': '&lt;', '>': '&gt;' };
const esc = (s) => s.replace(/[&<>]/g, (c) => ESC[c]);
// URLs reach escAttr already HTML-escaped by esc() (so & is already &amp;); here
// we only neutralize the attribute delimiter ". Re-escaping & would double it to
// &amp;amp; and break query-string links — do not add a & replacement back.
const escAttr = (s) => s.replace(/"/g, '&quot;');

function inline(text) {
  let s = esc(text);

  // Code spans first, swapped for placeholders so their contents are immune
  // to the bold/italic/link passes below.
  const codes = [];
  s = s.replace(/`([^`]+)`/g, (_, c) => {
    codes.push(c);
    return `\u0000C${codes.length - 1}\u0000`;
  });

  // Emphasis runs BEFORE links so it never sees the HTML the link pass emits —
  // notably target="_blank" and the underscores inside an href, which would
  // otherwise be devoured as emphasis. ** or __ (strong) before * or _ (em) so
  // double marks aren't eaten by the single-mark rule. A delimiter must hug its
  // content (no whitespace just inside), so spaced literals like "2 * 3" or
  // "a _ b" are left alone. Underscores must also sit at a word boundary, so
  // snake_case and foo_bar identifiers are never treated as emphasis.
  s = s.replace(/\*\*([^\s*](?:[^*]*[^\s*])?)\*\*/g, '<strong>$1</strong>');
  s = s.replace(/(?<![A-Za-z0-9])__([^\s_](?:[^_]*[^\s_])?)__(?![A-Za-z0-9])/g, '<strong>$1</strong>');
  s = s.replace(/\*([^\s*](?:[^*]*[^\s*])?)\*/g, '<em>$1</em>');
  s = s.replace(/(?<![A-Za-z0-9])_([^\s_](?:[^_]*[^\s_])?)_(?![A-Za-z0-9])/g, '<em>$1</em>');

  // Strikethrough: ~~text~~ -> <del>. Content escaped already; code is protected.
  s = s.replace(/~~([^~]+)~~/g, '<del>$1</del>');

  // Links: [text](url). The URL may contain one level of balanced parens, so
  // targets like .../Foo_(bar) survive. External links open in a new tab. Link
  // text may already hold emphasis tags from the pass above.
  s = s.replace(/\[([^\]]+)\]\(((?:[^()]|\([^()]*\))*)\)/g, (_, t, url) => {
    const u = url.trim();
    const ext = /^https?:\/\//i.test(u);
    const attrs = ext ? ' target="_blank" rel="noopener"' : '';
    return `<a href="${escAttr(u)}"${attrs}>${t}</a>`;
  });

  // Restore code spans (content escaped on the way back in). The sentinel is
  // written as an escape sequence, not a raw NUL byte, so the source stays plain
  // text for git/grep/editors; ?? '' guards a malformed/crafted placeholder.
  s = s.replace(/\u0000C(\d+)\u0000/g, (_, i) => `<code>${esc(codes[+i] ?? '')}</code>`);
  return s;
}

const isTableSep = (line) =>
  /^\s*\|?[\s:|-]*-[\s:|-]*\|?\s*$/.test(line) && line.includes('-');

function splitRow(line) {
  let l = line.trim();
  if (l.startsWith('|')) l = l.slice(1);
  if (l.endsWith('|')) l = l.slice(0, -1);
  return l.split('|').map((c) => c.trim());
}

function alignOf(cell) {
  const l = cell.startsWith(':');
  const r = cell.endsWith(':');
  if (l && r) return 'center';
  if (r) return 'right';
  return '';
}

// Build (possibly nested) list HTML from items carrying their indent width. A
// deeper indent opens a nested list inside the current <li>; a shallower one
// closes back out. Each level keeps the ordered-ness of its first item. A task
// item ([ ]/[x] prefix) renders a GFM-style disabled checkbox.
function renderList(items) {
  const inner = (it) => {
    const tm = it.text.match(/^\[([ xX])\]\s+(.*)$/);
    return tm
      ? `<input type="checkbox" disabled${tm[1] === ' ' ? '' : ' checked'}> ${inline(tm[2])}`
      : inline(it.text);
  };
  let html = '';
  const stack = []; // open levels, outermost first: { indent, ordered }
  for (const it of items) {
    if (!stack.length || it.indent > stack[stack.length - 1].indent) {
      stack.push({ indent: it.indent, ordered: it.ordered });
      html += it.ordered ? '<ol>' : '<ul>';
    } else {
      html += '</li>';
      while (stack.length > 1 && it.indent < stack[stack.length - 1].indent) {
        html += (stack.pop().ordered ? '</ol>' : '</ul>') + '</li>';
      }
    }
    html += '<li>' + inner(it);
  }
  html += '</li>';
  while (stack.length) {
    html += stack.pop().ordered ? '</ol>' : '</ul>';
    if (stack.length) html += '</li>';
  }
  return html;
}

export function renderMarkdown(md) {
  if (typeof md !== 'string') return '';
  const lines = md.replace(/\r\n?/g, '\n').split('\n');
  const out = [];
  let i = 0;

  const tableAhead = (idx) =>
    lines[idx].includes('|') && idx + 1 < lines.length && isTableSep(lines[idx + 1]);

  while (i < lines.length) {
    const line = lines[i];

    if (/^\s*$/.test(line)) { i++; continue; }

    // Fenced code block
    let m = line.match(/^```\s*([\w+-]*)\s*$/);
    if (m) {
      const lang = m[1];
      const buf = [];
      i++;
      while (i < lines.length && !/^```\s*$/.test(lines[i])) { buf.push(lines[i]); i++; }
      i++; // consume closing fence
      const cls = lang ? ` class="lang-${lang}"` : '';
      out.push(`<pre><code${cls}>${esc(buf.join('\n'))}</code></pre>`);
      continue;
    }

    // ATX heading
    m = line.match(/^(#{1,6})\s+(.*?)\s*#*\s*$/);
    if (m) {
      const lvl = m[1].length;
      out.push(`<h${lvl}>${inline(m[2])}</h${lvl}>`);
      i++;
      continue;
    }

    // Horizontal rule
    if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) { out.push('<hr>'); i++; continue; }

    // Pipe table
    if (tableAhead(i)) {
      const header = splitRow(line);
      const aligns = splitRow(lines[i + 1]).map(alignOf);
      i += 2;
      const rows = [];
      while (i < lines.length && lines[i].includes('|') && !/^\s*$/.test(lines[i])) {
        rows.push(splitRow(lines[i])); i++;
      }
      const cell = (tag, c, k) => {
        const a = aligns[k] ? ` style="text-align:${aligns[k]}"` : '';
        return `<${tag}${a}>${inline(c)}</${tag}>`;
      };
      let t = '<table><thead><tr>';
      header.forEach((h, k) => { t += cell('th', h, k); });
      t += '</tr></thead><tbody>';
      rows.forEach((r) => {
        t += '<tr>';
        r.forEach((c, k) => { t += cell('td', c, k); });
        t += '</tr>';
      });
      t += '</tbody></table>';
      out.push(t);
      continue;
    }

    // Blockquote
    if (/^\s*>/.test(line)) {
      const buf = [];
      while (i < lines.length && /^\s*>/.test(lines[i])) {
        buf.push(lines[i].replace(/^\s*>\s?/, '')); i++;
      }
      out.push(`<blockquote>${renderMarkdown(buf.join('\n'))}</blockquote>`);
      continue;
    }

    // List (ordered/unordered, nestable, multi-line items). Collect contiguous
    // list lines with their indent width; a non-marker indented line folds into
    // the prior item. renderList turns the indents into nested <ol>/<ul>.
    if (/^\s*([-*+]|\d+\.)\s+/.test(line)) {
      const items = [];
      while (i < lines.length) {
        const lm = lines[i].match(/^(\s*)([-*+]|\d+\.)\s+(.*)$/);
        if (lm) { items.push({ indent: lm[1].length, ordered: /\d/.test(lm[2]), text: lm[3] }); i++; }
        else if (/^\s+\S/.test(lines[i]) && items.length) { items[items.length - 1].text += ' ' + lines[i].trim(); i++; }
        else break;
      }
      out.push(renderList(items));
      continue;
    }

    // Paragraph
    const buf = [];
    while (
      i < lines.length &&
      !/^\s*$/.test(lines[i]) &&
      !/^```/.test(lines[i]) &&
      !/^#{1,6}\s/.test(lines[i]) &&
      !/^\s*([-*+]|\d+\.)\s+/.test(lines[i]) &&
      !/^\s*>/.test(lines[i]) &&
      !/^\s*([-*_])(\s*\1){2,}\s*$/.test(lines[i]) &&
      !tableAhead(i)
    ) {
      buf.push(lines[i]); i++;
    }
    // Forward-progress guard. A line can reach the paragraph branch yet still
    // match a paragraph terminator: a fence line whose info string fails the
    // stricter opener regex above (four+ backticks, or an info string with
    // spaces or punctuation). Such a line matches the /^```/ terminator on the
    // first iteration, so buf stays empty and i never advances -> infinite loop.
    // Consume it as literal text so the loop always makes progress.
    if (!buf.length) { buf.push(lines[i]); i++; }
    out.push(`<p>${inline(buf.join(' '))}</p>`);
  }

  return out.join('\n');
}

export default { renderMarkdown };
