All files / src markdown-to-html.ts

88.02% Statements 125/142
72.58% Branches 45/62
87.5% Functions 14/16
88.02% Lines 125/142

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143716x 716x 716x 716x 716x 716x 716x 1x 570x 570x 570x 570x 570x 570x 570x 570x 1x 422x 422x 422x 1x                       1x 157x 157x 157x 157x 157x 157x 157x 157x 1x 25x 25x 25x 1x 25x 25x 25x     25x 25x 25x 24x 24x 25x 25x 25x 25x 25x 25x 1x 1x 1x 3x 3x 3x 3x 3x 243x 243x 243x 3x 3x 3x 33x 33x 33x 3x 3x 3x 3x 3x 3x 243x 243x 25x 25x 157x 157x 157x 25x 25x 25x 243x 243x 243x 32x 32x 32x 32x 32x 243x 243x 20x 20x 20x 20x 243x 243x 1x 1x 1x 1x 1x 1x 1x 1x 243x 243x 122x 122x 122x 243x 243x 40x 40x 40x 40x   243x       234x 40x 3x 3x 3x 1x 1x  
function esc(text: string): string {
  return text
    .replace(/&/g, "&")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
}
 
function formatInline(text: string): string {
  let out = esc(text)
  out = out.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
  out = out.replace(/`([^`]+)`/g, "<code>$1</code>")
  out = out.replace(/\*(.+?)\*/g, "<em>$1</em>")
  out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<span class="md-link">$1</span>')
  return out
}
 
function isTableLine(line: string): boolean {
  return line.trimStart().startsWith("|")
}
 
function isBlockStart(line: string): boolean {
  const t = line.trim()
  return (
    t.startsWith("#") ||
    t.startsWith("```") ||
    isTableLine(line) ||
    /^---+$/.test(t) ||
    /^[-*]\s+/.test(t) ||
    /^\d+\.\s+/.test(t)
  )
}
 
function parseTableRow(line: string): string[] {
  return line
    .trim()
    .replace(/^\|/, "")
    .replace(/\|$/, "")
    .split("|")
    .map((c) => c.trim())
}
 
function isSeparatorRow(cells: string[]): boolean {
  return cells.every((c) => /^:?-{3,}:?$/.test(c))
}
 
function renderTable(lines: string[]): string {
  const rows = lines.map(parseTableRow)
  if (rows.length === 0) {
    return ""
  }
  const header = rows[0] ?? []
  let body = rows.slice(1)
  if (body.length > 0 && isSeparatorRow(body[0] ?? [])) {
    body = body.slice(1)
  }
  const thead = `<tr>${header.map((c) => `<th>${formatInline(c)}</th>`).join("")}</tr>`
  const tbody = body
    .map((row) => `<tr>${row.map((c) => `<td>${formatInline(c)}</td>`).join("")}</tr>`)
    .join("")
  return `<div class="table-wrap"><table>${thead}${tbody}</table></div>`
}
 
/** Minimal markdown → HTML for kit reference pages (agents-help.md). */
export function markdownToHtml(markdown: string): string {
  const lines = markdown.replace(/\r\n/g, "\n").split("\n")
  const out: string[] = []
  let i = 0
 
  while (i < lines.length) {
    const line = lines[i] ?? ""
 
    if (line.startsWith("```")) {
      i++
      const code: string[] = []
      while (i < lines.length && !(lines[i] ?? "").startsWith("```")) {
        code.push(lines[i] ?? "")
        i++
      }
      out.push(`<pre><code>${esc(code.join("\n"))}</code></pre>`)
      if (i < lines.length) {
        i++
      }
      continue
    }
 
    if (isTableLine(line)) {
      const tableLines: string[] = []
      while (i < lines.length && isTableLine(lines[i] ?? "")) {
        tableLines.push(lines[i] ?? "")
        i++
      }
      out.push(renderTable(tableLines))
      continue
    }
 
    const heading = line.match(/^(#{1,6})\s+(.+)$/)
    if (heading) {
      const level = heading[1]?.length ?? 1
      out.push(`<h${level}>${formatInline(heading[2] ?? "")}</h${level}>`)
      i++
      continue
    }
 
    if (/^---+$/.test(line.trim())) {
      out.push("<hr />")
      i++
      continue
    }
 
    if (/^[-*]\s+/.test(line)) {
      const items: string[] = []
      while (i < lines.length && /^[-*]\s+/.test(lines[i] ?? "")) {
        items.push((lines[i] ?? "").replace(/^[-*]\s+/, ""))
        i++
      }
      out.push(`<ul>${items.map((item) => `<li>${formatInline(item)}</li>`).join("")}</ul>`)
      continue
    }
 
    if (line.trim() === "") {
      i++
      continue
    }
 
    const para: string[] = [line]
    i++
    while (
      i < lines.length &&
      (lines[i] ?? "").trim() !== "" &&
      !isBlockStart(lines[i] ?? "")
    ) {
      para.push(lines[i] ?? "")
      i++
    }
    out.push(`<p>${formatInline(para.join(" "))}</p>`)
  }
 
  return out.join("\n")
}
 
export { esc, formatInline }