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("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
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}">↩</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();