/* =====================================================================
   markdown.jsx — 极简 Markdown 渲染器
   只覆盖这批双语源实际用到的语法：ATX 标题、围栏代码、行内与引用式图片、
   表格、列表、引用块、段落。每个 block 保留 1-based 源行号区间，
   beat 才能锚回到它。
   依赖：React（仅 Block 组件用到 JSX）
   导出：bandu.markdown
   ===================================================================== */

window.bandu = window.bandu || {};

(function (bandu) {

const CJK_RE = /[一-鿿　-〿]/;
const isZh = (s) => {
  const cjk = (s.match(/[一-鿿]/g) || []).length;
  const latin = (s.match(/[A-Za-z]/g) || []).length;
  return cjk > 0 && cjk * 2 >= latin;
};

function escapeHtml(s) {
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

// Inline markdown -> HTML string. Code spans are pulled out first so their
// contents never get re-processed as emphasis or links. The placeholder is
// NUL-delimited because a bare " 1 " would collide with ordinary prose.
function inlineHtml(raw, imageDefs) {
  const spans = [];
  let s = raw.replace(/`([^`]+)`/g, (_, code) => {
    spans.push(`<code>${escapeHtml(code)}</code>`);
    return `\u0000${spans.length - 1}\u0000`;
  });

  s = escapeHtml(s);

  // images: ![alt](src) and reference style ![alt][ref]
  const baseDir = imageDefs["::base"];
  s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)[^)]*\)/g, (_, alt, src) =>
    `<img src="${resolvePath(src, baseDir)}" alt="${alt}" />`);
  s = s.replace(/!\[([^\]]*)\]\[([^\]]+)\]/g, (_, alt, ref) => {
    const src = imageDefs[ref.toLowerCase()];
    return src ? `<img src="${src}" alt="${alt}" />` : `<em>[${alt}]</em>`;
  });

  s = s.replace(/\[([^\]]+)\]\(([^)\s]+)[^)]*\)/g, (_, t, href) =>
    `<a href="${href}" target="_blank" rel="noreferrer">${t}</a>`);
  s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
  s = s.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
  // Markdown backslash escapes ("1\." in numbered inline headings) render as
  // the bare character, not as a literal backslash.
  s = s.replace(/\\([\\`*_{}\[\]()#+\-.!>|])/g, "$1");

  return s.replace(/\u0000(\d+)\u0000/g, (_, i) => spans[Number(i)]);
}

// Collect reference-style image definitions: [image1]: ../images/x.png
// Paths are relative to the source .md, not to /web/, so they are rebased
// against the document's own source_path before being used as <img src>.
function collectImageDefs(lines, baseDir) {
  const defs = { "::base": baseDir };
  for (const line of lines) {
    const m = /^\s*\[([^\]]+)\]:\s*(\S+)/.exec(line);
    if (m) defs[m[1].toLowerCase()] = resolvePath(m[2], baseDir);
  }
  return defs;
}

// Resolve a source-relative asset path against the .md's directory, then
// against the page. Absolute URLs and data: URIs pass through untouched.
function resolvePath(src, baseDir) {
  if (!src || !baseDir) return src;
  if (/^([a-z]+:)?\/\//i.test(src) || /^data:/i.test(src) || src.startsWith("/")) return src;
  const parts = (baseDir + "/" + src).split("/");
  const stack = [];
  for (const part of parts) {
    if (!part || part === ".") continue;
    if (part === "..") stack.pop();
    else stack.push(part);
  }
  // page lives in /web/, artifacts and content live one level up
  return "../" + stack.join("/");
}

// lines: array where index 0 is source line 1.
function parseBlocks(lines, imageDefs) {
  const blocks = [];
  let i = 0;
  const push = (b) => blocks.push(b);

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

    if (!line.trim()) { i++; continue; }

    // reference-style image definition: consumed by collectImageDefs
    if (/^\s*\[[^\]]+\]:\s*\S+/.test(line) && !/^\s*\[[^\]]+\]:\s*http/.test(line)) { i++; continue; }

    // fenced code
    const fence = /^(\s*)(`{3,}|~{3,})(.*)$/.exec(line);
    if (fence) {
      const marker = fence[2][0];
      const lang = fence[3].trim();
      const body = [];
      let j = i + 1;
      while (j < lines.length && !new RegExp(`^\\s*${marker}{3,}\\s*$`).test(lines[j])) {
        body.push(lines[j]); j++;
      }
      push({ type: "code", lang, text: body.join("\n"), start: lineNo, end: Math.min(j + 1, lines.length) });
      i = j + 1;
      continue;
    }

    // heading
    const h = /^(#{1,6})\s+(.*)$/.exec(line);
    if (h) {
      push({ type: "heading", level: h[1].length, text: h[2].trim(), start: lineNo, end: lineNo });
      i++;
      continue;
    }

    if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
      push({ type: "hr", start: lineNo, end: lineNo });
      i++;
      continue;
    }

    // table
    if (line.includes("|") && i + 1 < lines.length && /^\s*\|?[\s:|-]+\|[\s:|-]*$/.test(lines[i + 1])) {
      const rows = [];
      let j = i;
      while (j < lines.length && lines[j].includes("|") && lines[j].trim()) { rows.push(lines[j]); j++; }
      const cells = rows
        .filter((r, idx) => idx !== 1)
        .map((r) => r.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim()));
      push({ type: "table", head: cells[0] || [], body: cells.slice(1), start: lineNo, end: j });
      i = j;
      continue;
    }

    // list
    if (/^\s*([-*+]|\d+[.)])\s+/.test(line)) {
      const ordered = /^\s*\d+[.)]\s+/.test(line);
      const items = [];
      let j = i;
      while (j < lines.length && /^\s*([-*+]|\d+[.)])\s+/.test(lines[j])) {
        items.push(lines[j].replace(/^\s*([-*+]|\d+[.)])\s+/, ""));
        j++;
        // allow a single wrapped continuation line
        while (j < lines.length && lines[j].trim() && !/^\s*([-*+]|\d+[.)])\s+/.test(lines[j]) && /^\s{2,}/.test(lines[j])) {
          items[items.length - 1] += " " + lines[j].trim();
          j++;
        }
      }
      push({ type: "list", ordered, items, start: lineNo, end: j });
      i = j;
      continue;
    }

    // blockquote
    if (/^\s*>\s?/.test(line)) {
      const body = [];
      let j = i;
      while (j < lines.length && /^\s*>\s?/.test(lines[j])) { body.push(lines[j].replace(/^\s*>\s?/, "")); j++; }
      push({ type: "quote", text: body.join(" "), start: lineNo, end: j });
      i = j;
      continue;
    }

    // paragraph: consecutive non-blank lines. In these bilingual sources one
    // paragraph is one line, so this rarely merges — which is what we want,
    // because merging would blur beat boundaries.
    const body = [];
    let j = i;
    while (
      j < lines.length && lines[j].trim() &&
      !/^(#{1,6})\s/.test(lines[j]) &&
      !/^(\s*)(`{3,}|~{3,})/.test(lines[j]) &&
      !/^\s*([-*+]|\d+[.)])\s+/.test(lines[j]) &&
      !/^\s*>\s?/.test(lines[j])
    ) { body.push(lines[j]); j++; }
    push({ type: "para", text: body.join(" ").trim(), start: lineNo, end: j });
    i = j;
  }

  return blocks;
}

function Block({ block, imageDefs }) {
  const zh = block.text ? isZh(block.text) : false;
  switch (block.type) {
    case "heading": {
      const Tag = "h" + Math.min(block.level, 3);
      return <Tag className={zh ? "zh" : ""} dangerouslySetInnerHTML={{ __html: inlineHtml(block.text, imageDefs) }} />;
    }
    case "code":
      return <pre><code>{block.text}</code></pre>;
    case "hr":
      return <hr />;
    case "quote":
      return <blockquote dangerouslySetInnerHTML={{ __html: inlineHtml(block.text, imageDefs) }} />;
    case "list": {
      const Tag = block.ordered ? "ol" : "ul";
      return (
        <Tag>
          {block.items.map((it, k) => (
            <li key={k} dangerouslySetInnerHTML={{ __html: inlineHtml(it, imageDefs) }} />
          ))}
        </Tag>
      );
    }
    case "table":
      return (
        <table>
          <thead><tr>{block.head.map((c, k) => <th key={k} dangerouslySetInnerHTML={{ __html: inlineHtml(c, imageDefs) }} />)}</tr></thead>
          <tbody>
            {block.body.map((row, r) => (
              <tr key={r}>{row.map((c, k) => <td key={k} dangerouslySetInnerHTML={{ __html: inlineHtml(c, imageDefs) }} />)}</tr>
            ))}
          </tbody>
        </table>
      );
    default:
      return <p className={zh ? "zh" : ""} dangerouslySetInnerHTML={{ __html: inlineHtml(block.text, imageDefs) }} />;
  }
}

bandu.markdown = {
  CJK_RE, isZh, escapeHtml, inlineHtml,
  collectImageDefs, resolvePath, parseBlocks, Block,
};

})(window.bandu);
