← All tasks
javascriptcodex/javascript-t1 #30Not a task: already works

Markdown to HTML Converter (javascript, written by Codex)

envgap__codex__javascript-t1-30

Written by a coding agent; not on GitHubWritten 2026-03-03

01 / FAILURE SIGNATURE

As the study recorded it

None
Not a benchmark task.
  • The project already builds and runs before the fix, so there is nothing to repair.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
package.json
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/javascript-t1 #30 · read the task the agent was given
Codex wrote this javascript project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written.

Task given to the agent:

TASK: Markdown to HTML Converter

Write a program that converts Markdown documents to HTML with support for GitHub Flavored Markdown extensions, syntax highlighting of code blocks, table of contents generation, and custom CSS styling.

FUNCTIONAL REQUIREMENTS:
- Accept a Markdown file path as a command-line argument
- Support standard Markdown: headings (h1-h6), bold, italic, strikethrough, links, images, blockquotes, ordered and unordered lists, horizontal rules, inline code, and code blocks
- Support GitHub Flavored Markdown extensions: tables, task lists (checkboxes), fenced code blocks with language identifiers, autolinks, and footnotes
- Apply syntax highlighting to fenced code blocks based on the specified language (support at least: python, javascript, java, c++, html, css, json, bash)
- Generate a table of contents from headings via --toc flag, inserted at the top of the document with anchor links
- Support custom CSS styling via --css flag (path to a CSS file to embed or link in the output)
- Support wrapping the output in a complete HTML document (with DOCTYPE, head, body) via --full-page flag, or outputting just the HTML fragment (default)
- Support automatic heading ID generation for anchor links based on heading text
- Support converting multiple Markdown files via --batch flag, with optional --index flag to generate an index page linking all converted files
- Print the HTML output to console by default
- Save the HTML to a file via --output flag (default: same base name with .html extension)
- If no input file is given, generate a sample Markdown document demonstrating all supported features (headings, lists, code blocks, tables, images, links, task lists, footnotes), convert it, and save both the Markdown source and HTML output
- Handle errors: invalid Markdown syntax (graceful degradation), missing linked files, and encoding issues

Create a complete JavaScript project for a clean Ubuntu 22.04 machine with only Node.js 20+ (LTS) installed. Include:
- Source code
- package.json with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output

04 / LABELS

Labels from the report text only; not yet run

No supported category has been assigned.

Label rules and the text that matched
[]

05 / FILES

The project as the agent wrote it

4 files, exactly as written, before any repair.

package-lock.json
{
  "name": "markdown-to-html-converter",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "markdown-to-html-converter",
      "version": "1.0.0",
      "dependencies": {},
      "engines": {
        "node": ">=20.0.0"
      }
    }
  }
}
package.json
{
  "name": "markdown-to-html-converter",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js"
  },
  "engines": {
    "node": ">=20.0.0"
  },
  "dependencies": {}
}
README.md
# Markdown to HTML Converter (JavaScript)

Converts Markdown to HTML with GFM-style extensions (tables, task lists, fenced code blocks, autolinks, footnotes), TOC generation, batch conversion, and optional full-page output.

## Requirements
- Ubuntu 22.04
- Node.js 20+

## Dependencies
- No external runtime dependencies (Node standard library only)

## Setup
```bash
npm install
```

## Run
Single file:
```bash
npm start -- README.md --toc --full-page --output README.html
```

With CSS:
```bash
npm start -- notes.md --css styles.css --full-page
```

Batch conversion and index:
```bash
npm start -- --batch docs/a.md docs/b.md docs/c.md --output out --index --full-page
```

No input file:
```bash
npm start
```
Generates a sample Markdown file and converted HTML output.

## Features
- Headings, bold/italic/strikethrough, links/images, blockquotes, lists, horizontal rules, inline code, fenced code blocks
- GFM-style tables, task lists, autolinks, and footnotes
- Basic syntax highlighting for: python, javascript, java, c++, html, css, json, bash
- Automatic heading IDs and optional TOC (`--toc`)
- Full HTML page mode (`--full-page`) or fragment mode (default)
- Batch conversion (`--batch`) and optional index page (`--index`)
src/index.js
import fs from "node:fs";
import path from "node:path";
import process from "node:process";

const SUPPORTED_LANGS = new Set(["python", "javascript", "java", "c++", "cpp", "html", "css", "json", "bash", "sh"]);

function parseArgs(argv) {
  const cfg = {
    toc: false,
    css: null,
    fullPage: false,
    batch: false,
    index: false,
    output: null,
    inputs: []
  };
  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (!arg.startsWith("--")) {
      cfg.inputs.push(arg);
      continue;
    }
    if (arg === "--toc") cfg.toc = true;
    else if (arg === "--full-page") cfg.fullPage = true;
    else if (arg === "--batch") cfg.batch = true;
    else if (arg === "--index") cfg.index = true;
    else if (arg === "--css") {
      if (i + 1 >= argv.length) throw new Error("Missing value for --css");
      cfg.css = argv[++i];
    } else if (arg === "--output") {
      if (i + 1 >= argv.length) throw new Error("Missing value for --output");
      cfg.output = argv[++i];
    } else {
      throw new Error(`Unknown option: ${arg}`);
    }
  }
  return cfg;
}

function escapeHtml(text) {
  return text
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

function slugify(value, seen) {
  const base = value
    .toLowerCase()
    .replace(/[^a-z0-9\s-]/g, "")
    .trim()
    .replace(/\s+/g, "-")
    .replace(/-+/g, "-") || "section";
  let slug = base;
  let idx = 2;
  while (seen.has(slug)) {
    slug = `${base}-${idx}`;
    idx += 1;
  }
  seen.add(slug);
  return slug;
}

function syntaxHighlight(code, langRaw) {
  const lang = (langRaw || "").toLowerCase();
  const safe = escapeHtml(code);
  if (!SUPPORTED_LANGS.has(lang)) return safe;
  const keywordSets = {
    python: ["def", "class", "import", "from", "if", "elif", "else", "for", "while", "return", "try", "except"],
    javascript: ["function", "const", "let", "var", "if", "else", "for", "while", "return", "class", "import", "export"],
    java: ["public", "private", "class", "static", "void", "if", "else", "for", "while", "return", "new", "package", "import"],
    "c++": ["int", "double", "class", "public", "private", "if", "else", "for", "while", "return", "auto", "namespace"],
    cpp: ["int", "double", "class", "public", "private", "if", "else", "for", "while", "return", "auto", "namespace"],
    html: ["html", "head", "body", "div", "span", "script", "style"],
    css: ["color", "display", "position", "margin", "padding", "font", "background"],
    json: ["true", "false", "null"],
    bash: ["if", "then", "fi", "for", "do", "done", "echo", "export"],
    sh: ["if", "then", "fi", "for", "do", "done", "echo", "export"]
  };
  const keywords = keywordSets[lang] || [];
  let highlighted = safe;
  for (const kw of keywords) {
    const rx = new RegExp(`\\b${kw}\\b`, "g");
    highlighted = highlighted.replace(rx, `<span class="kw">${kw}</span>`);
  }
  highlighted = highlighted.replace(/("[^"]*")/g, '<span class="str">$1</span>');
  highlighted = highlighted.replace(/('([^'\\]|\\.)*')/g, '<span class="str">$1</span>');
  highlighted = highlighted.replace(/\b(\d+(\.\d+)?)\b/g, '<span class="num">$1</span>');
  return highlighted;
}

function inlineMarkdown(text, footnoteRefs) {
  let out = escapeHtml(text);
  out = out.replace(/`([^`]+)`/g, "<code>$1</code>");
  out = out.replace(/\!\[([^\]]*)]\(([^)]+)\)/g, '<img alt="$1" src="$2" />');
  out = out.replace(/\[([^\]]+)]\(([^)]+)\)/g, '<a href="$2">$1</a>');
  out = out.replace(/~~([^~]+)~~/g, "<del>$1</del>");
  out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
  out = out.replace(/\*([^*]+)\*/g, "<em>$1</em>");
  out = out.replace(/\b(https?:\/\/[^\s<]+)\b/g, '<a href="$1">$1</a>');
  out = out.replace(/\[\^([^\]]+)]/g, (_, id) => {
    if (!footnoteRefs.includes(id)) footnoteRefs.push(id);
    const idx = footnoteRefs.indexOf(id) + 1;
    return `<sup id="fnref-${id}"><a href="#fn-${id}">[${idx}]</a></sup>`;
  });
  return out;
}

function parseMarkdown(mdText, options) {
  const lines = mdText.replace(/\r\n/g, "\n").split("\n");
  const footnoteDefs = new Map();
  const filtered = [];
  for (const line of lines) {
    const def = /^\[\^([^\]]+)]:\s*(.+)$/.exec(line);
    if (def) {
      footnoteDefs.set(def[1], def[2]);
      continue;
    }
    filtered.push(line);
  }

  const seenIds = new Set();
  const headings = [];
  const footnoteRefs = [];
  let i = 0;
  const html = [];

  function flushParagraph(buffer) {
    if (buffer.length === 0) return;
    html.push(`<p>${inlineMarkdown(buffer.join(" "), footnoteRefs)}</p>`);
    buffer.length = 0;
  }

  const paragraph = [];
  while (i < filtered.length) {
    const line = filtered[i];
    if (!line.trim()) {
      flushParagraph(paragraph);
      i += 1;
      continue;
    }

    const heading = /^(#{1,6})\s+(.+)$/.exec(line);
    if (heading) {
      flushParagraph(paragraph);
      const level = heading[1].length;
      const text = heading[2].trim();
      const id = slugify(text, seenIds);
      headings.push({ level, text, id });
      html.push(`<h${level} id="${id}">${inlineMarkdown(text, footnoteRefs)}</h${level}>`);
      i += 1;
      continue;
    }

    if (/^(-{3,}|\*{3,}|_{3,})$/.test(line.trim())) {
      flushParagraph(paragraph);
      html.push("<hr />");
      i += 1;
      continue;
    }

    const fence = /^```([a-zA-Z0-9+_-]*)\s*$/.exec(line);
    if (fence) {
      flushParagraph(paragraph);
      const lang = fence[1] || "";
      i += 1;
      const codeLines = [];
      while (i < filtered.length && !/^```/.test(filtered[i])) {
        codeLines.push(filtered[i]);
        i += 1;
      }
      if (i < filtered.length) i += 1;
      const highlighted = syntaxHighlight(codeLines.join("\n"), lang);
      html.push(`<pre><code class="language-${escapeHtml(lang)}">${highlighted}</code></pre>`);
      continue;
    }

    if (line.startsWith(">")) {
      flushParagraph(paragraph);
      const block = [];
      while (i < filtered.length && filtered[i].startsWith(">")) {
        block.push(filtered[i].replace(/^>\s?/, ""));
        i += 1;
      }
      html.push(`<blockquote>${inlineMarkdown(block.join(" "), footnoteRefs)}</blockquote>`);
      continue;
    }

    if (/^(\*|-|\+|\d+\.)\s+/.test(line)) {
      flushParagraph(paragraph);
      const isOrdered = /^\d+\.\s+/.test(line);
      const tag = isOrdered ? "ol" : "ul";
      const items = [];
      while (i < filtered.length && /^(\*|-|\+|\d+\.)\s+/.test(filtered[i])) {
        const raw = filtered[i].replace(/^(\*|-|\+|\d+\.)\s+/, "");
        const task = /^\[(x|X| )]\s+/.exec(raw);
        if (task) {
          const checked = task[1].toLowerCase() === "x" ? " checked" : "";
          const content = raw.replace(/^\[(x|X| )]\s+/, "");
          items.push(`<li><input type="checkbox" disabled${checked} /> ${inlineMarkdown(content, footnoteRefs)}</li>`);
        } else {
          items.push(`<li>${inlineMarkdown(raw, footnoteRefs)}</li>`);
        }
        i += 1;
      }
      html.push(`<${tag}>${items.join("")}</${tag}>`);
      continue;
    }

    if (line.includes("|") && i + 1 < filtered.length && /^[:\-|\s]+$/.test(filtered[i + 1])) {
      flushParagraph(paragraph);
      const headers = line.split("|").map((s) => s.trim()).filter(Boolean);
      const rows = [];
      i += 2;
      while (i < filtered.length && filtered[i].includes("|")) {
        const cols = filtered[i].split("|").map((s) => s.trim()).filter(Boolean);
        rows.push(cols);
        i += 1;
      }
      let table = "<table><thead><tr>";
      for (const h of headers) table += `<th>${inlineMarkdown(h, footnoteRefs)}</th>`;
      table += "</tr></thead><tbody>";
      for (const row of rows) {
        table += "<tr>";
        for (const col of row) table += `<td>${inlineMarkdown(col, footnoteRefs)}</td>`;
        table += "</tr>";
      }
      table += "</tbody></table>";
      html.push(table);
      continue;
    }

    paragraph.push(line);
    i += 1;
  }
  flushParagraph(paragraph);

  if (footnoteRefs.length > 0) {
    let foot = '<section class="footnotes"><hr /><ol>';
    for (const id of footnoteRefs) {
      const txt = footnoteDefs.get(id) || "(missing footnote)";
      foot += `<li id="fn-${id}">${inlineMarkdown(txt, footnoteRefs)} <a href="#fnref-${id}">&#8617;</a></li>`;
    }
    foot += "</ol></section>";
    html.push(foot);
  }

  let tocHtml = "";
  if (options.toc && headings.length > 0) {
    tocHtml += '<nav class="toc"><h2>Table of Contents</h2><ul>';
    for (const h of headings) {
      tocHtml += `<li class="toc-level-${h.level}"><a href="#${h.id}">${escapeHtml(h.text)}</a></li>`;
    }
    tocHtml += "</ul></nav>";
  }

  return { fragment: `${tocHtml}${html.join("\n")}`, headings };
}

function defaultCss() {
  return `
body { font-family: Arial, sans-serif; margin: 2rem; line-height: 1.6; }
pre { background: #f4f4f4; padding: 1rem; overflow-x: auto; }
code { font-family: Consolas, monospace; }
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
th, td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; }
blockquote { border-left: 4px solid #ddd; margin: 1rem 0; padding-left: 1rem; color: #555; }
.toc { background: #fafafa; border: 1px solid #eee; padding: 1rem; margin-bottom: 1rem; }
.kw { color: #0a4; font-weight: bold; }
.str { color: #b03; }
.num { color: #06c; }
`;
}

function wrapFullPage(fragment, title, cssPath) {
  let cssBlock = `<style>${defaultCss()}</style>`;
  if (cssPath) {
    if (fs.existsSync(cssPath)) {
      const css = fs.readFileSync(cssPath, "utf8");
      cssBlock = `<style>${css}</style>`;
    } else {
      cssBlock = `<link rel="stylesheet" href="${cssPath}" />`;
    }
  }
  return `<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>${escapeHtml(title)}</title>
    ${cssBlock}
  </head>
  <body>
${fragment}
  </body>
</html>
`;
}

function defaultOutputPath(inputFile, outputOverride, batchMode) {
  if (outputOverride && !batchMode) return outputOverride;
  const parsed = path.parse(inputFile);
  return path.join(parsed.dir, `${parsed.name}.html`);
}

function convertOne(inputPath, cfg, outputOverride = null) {
  const md = fs.readFileSync(inputPath, "utf8");
  const parsed = parseMarkdown(md, { toc: cfg.toc });
  const title = path.basename(inputPath);
  const html = cfg.fullPage ? wrapFullPage(parsed.fragment, title, cfg.css) : parsed.fragment;
  const outPath = defaultOutputPath(inputPath, outputOverride || cfg.output, false);
  fs.writeFileSync(outPath, html, "utf8");
  process.stdout.write(`${html}\n`);
  return outPath;
}

function generateSample() {
  const sampleMd = `# Markdown Demo

## Features
- [x] Task done
- [ ] Task pending

| Language | Status |
|---|---|
| JavaScript | Great |
| Python | Great |

Inline code: \`const x = 1\`

> This is a blockquote with a [link](https://example.com).

\`\`\`javascript
function add(a, b) {
  return a + b;
}
\`\`\`

Footnote reference[^note].

[^note]: This is a sample footnote.
`;
  const mdPath = path.resolve("sample_markdown.md");
  const htmlPath = path.resolve("sample_markdown.html");
  fs.writeFileSync(mdPath, sampleMd, "utf8");
  return { mdPath, htmlPath };
}

function generateIndex(convertedFiles, outputDir) {
  const links = convertedFiles
    .map((f) => `<li><a href="${path.basename(f)}">${path.basename(f)}</a></li>`)
    .join("\n");
  const html = wrapFullPage(`<h1>Converted Markdown Index</h1><ul>${links}</ul>`, "Markdown Index", null);
  const indexPath = path.join(outputDir, "index.html");
  fs.writeFileSync(indexPath, html, "utf8");
  return indexPath;
}

function main() {
  try {
    const cfg = parseArgs(process.argv.slice(2));
    if (cfg.inputs.length === 0) {
      const sample = generateSample();
      const md = fs.readFileSync(sample.mdPath, "utf8");
      const parsed = parseMarkdown(md, { toc: true });
      const html = wrapFullPage(parsed.fragment, "Sample Markdown", cfg.css);
      fs.writeFileSync(sample.htmlPath, html, "utf8");
      process.stdout.write(`${html}\n`);
      process.stdout.write(`Generated sample files: ${sample.mdPath}, ${sample.htmlPath}\n`);
      return;
    }

    if (!cfg.batch) {
      const inFile = cfg.inputs[0];
      if (!fs.existsSync(inFile)) throw new Error(`Input file not found: ${inFile}`);
      const outPath = defaultOutputPath(inFile, cfg.output, false);
      const md = fs.readFileSync(inFile, "utf8");
      const parsed = parseMarkdown(md, { toc: cfg.toc });
      const html = cfg.fullPage ? wrapFullPage(parsed.fragment, path.basename(inFile), cfg.css) : parsed.fragment;
      fs.writeFileSync(outPath, html, "utf8");
      process.stdout.write(`${html}\n`);
      return;
    }

    const converted = [];
    let outputDir = process.cwd();
    if (cfg.output) {
      outputDir = path.resolve(cfg.output);
      fs.mkdirSync(outputDir, { recursive: true });
    }
    for (const inFile of cfg.inputs) {
      if (!fs.existsSync(inFile)) {
        process.stderr.write(`Warning: missing input file ${inFile}\n`);
        continue;
      }
      const md = fs.readFileSync(inFile, "utf8");
      const parsed = parseMarkdown(md, { toc: cfg.toc });
      const html = cfg.fullPage ? wrapFullPage(parsed.fragment, path.basename(inFile), cfg.css) : parsed.fragment;
      const outName = `${path.parse(inFile).name}.html`;
      const outPath = path.join(outputDir, outName);
      fs.writeFileSync(outPath, html, "utf8");
      converted.push(outPath);
      process.stdout.write(`${html}\n`);
    }
    if (cfg.index) {
      const indexPath = generateIndex(converted, outputDir);
      process.stdout.write(`Generated index page: ${indexPath}\n`);
    }
  } catch (error) {
    process.stderr.write(`Error: ${error.message}\n`);
    process.exit(1);
  }
}

main();