import * as Document from "./Document.js"; // Map of original reference names to their slugified anchors let customRefMap: Map = new Map(); export function fromMarkdown(markdown: string): string { return fromDocument(Document.fromMarkdown(markdown)); } export function fromDocument(doc: Document.Document): string { // Build the custom reference map from both normative and informative refs // Maps original name (e.g., "Tempo Payment Method") to anchor (e.g., "tempo-payment-method") customRefMap = new Map(); for (const ref of [...doc.references.customNormative, ...doc.references.customInformative]) { customRefMap.set(ref.originalName, ref.anchor); } const parts: string[] = []; parts.push(''); parts.push("'); parts.push(' '); parts.push("]>"); parts.push(''); parts.push(""); parts.push(buildFront(doc)); parts.push(""); parts.push(buildMiddle(doc)); parts.push(""); parts.push(buildBack(doc)); parts.push(""); parts.push(""); return parts.join("\n"); } function buildFront(doc: Document.Document): string { const lines: string[] = []; lines.push(" "); lines.push(` ${escapeXml(doc.meta.title)}`); lines.push(` `); for (const author of doc.meta.authors) { lines.push(` `); if (author.organization || author.email) { lines.push("
"); if (author.email) { lines.push(` ${escapeXml(author.email)}`); } lines.push("
"); } lines.push("
"); } lines.push(" "); if (doc.meta.abstract) { lines.push(" "); const paragraphs = doc.meta.abstract.split(/\n\n+/); for (const para of paragraphs) { if (para.trim()) { lines.push(` ${processInlineElements(para.trim())}`); } } lines.push(" "); } lines.push("
"); return lines.join("\n"); } function buildMiddle(doc: Document.Document): string { const lines: string[] = []; lines.push(" "); for (const section of doc.sections) { lines.push(buildSection(section, 2)); } lines.push(" "); return lines.join("\n"); } function buildSection(section: Document.Section, indent: number): string { const pad = " ".repeat(indent); const lines: string[] = []; lines.push(`${pad}
`); lines.push(`${pad} ${escapeXml(section.name)}`); const content = section.content.join("\n"); lines.push(processContent(content, indent + 1)); for (const child of section.children) { lines.push(buildSection(child, indent + 1)); } lines.push(`${pad}
`); return lines.join("\n"); } function buildCustomReference(ref: Document.CustomReference, indent: number): string { const pad = " ".repeat(indent); const lines: string[] = []; if (ref.target) { lines.push(`${pad}`); } else { lines.push(`${pad}`); } lines.push(`${pad} `); lines.push(`${pad} ${escapeXml(ref.title)}`); if (ref.author) { // Try to parse author - could be "Org" or "LastName, FirstName" or just a name const orgMatch = ref.author.match(/^([A-Z0-9]+)$/); if (orgMatch) { lines.push(`${pad} ${escapeXml(ref.author)}`); } else { lines.push(`${pad} `); } } else { lines.push(`${pad} `); } if (ref.date) { lines.push(`${pad} `); } lines.push(`${pad} `); lines.push(`${pad}`); return lines.join("\n"); } function buildBack(doc: Document.Document): string { const lines: string[] = []; lines.push(" "); const hasNormative = doc.references.normative.length > 0 || doc.references.customNormative.length > 0; const hasInformative = doc.references.informative.length > 0 || doc.references.customInformative.length > 0; if (hasNormative || hasInformative) { lines.push(" "); lines.push(" References"); if (hasNormative) { lines.push(" "); lines.push(" Normative References"); for (const rfc of doc.references.normative) { lines.push( ` `, ); } for (const ref of doc.references.customNormative) { lines.push(buildCustomReference(ref, 4)); } lines.push(" "); } if (hasInformative) { lines.push(" "); lines.push(" Informative References"); for (const rfc of doc.references.informative) { lines.push( ` `, ); } for (const ref of doc.references.customInformative) { lines.push(buildCustomReference(ref, 4)); } lines.push(" "); } lines.push(" "); } for (const appendix of doc.appendices) { lines.push(buildSection(appendix, 2)); } if (doc.acknowledgements) { lines.push('
'); lines.push(" Acknowledgements"); lines.push(processContent(doc.acknowledgements, 3)); lines.push("
"); } lines.push("
"); return lines.join("\n"); } function processContent(content: string, indent: number): string { const pad = " ".repeat(indent); const lines: string[] = []; const rawLines = content.split("\n"); let i = 0; while (i < rawLines.length) { const line = rawLines[i]; if (line.startsWith("```")) { const langMatch = line.match(/^```(\w*)/); const lang = langMatch?.[1] || ""; const codeLines: string[] = []; i++; while (i < rawLines.length && !rawLines[i].startsWith("```")) { codeLines.push(rawLines[i]); i++; } i++; const isAsciiArt = lang === "" && codeLines.some((l) => l.includes("+--") || l.includes("│") || l.includes("├")); const tag = isAsciiArt ? "artwork" : "sourcecode"; const typeAttr = lang && !isAsciiArt ? ` type="${lang}"` : isAsciiArt ? ' type="ascii-art"' : ""; lines.push(`${pad}<${tag}${typeAttr}>`); continue; } if (line.startsWith("| ")) { const tableLines: string[] = []; while (i < rawLines.length && rawLines[i].startsWith("|")) { tableLines.push(rawLines[i]); i++; } lines.push(processTable(tableLines, indent)); continue; } if (line.match(/^[-*]\s+/)) { const listItems: string[] = []; while (i < rawLines.length && rawLines[i].match(/^[-*]\s+/)) { listItems.push(rawLines[i].replace(/^[-*]\s+/, "")); i++; } lines.push(`${pad}`); continue; } if (line.match(/^\d+\.\s+/)) { const listItems: string[] = []; while (i < rawLines.length && rawLines[i].match(/^\d+\.\s+/)) { listItems.push(rawLines[i].replace(/^\d+\.\s+/, "")); i++; } lines.push(`${pad}
    `); for (const item of listItems) { lines.push(`${pad}
  1. ${processInlineElements(item)}
  2. `); } lines.push(`${pad}
`); continue; } if (line.match(/^\*\*[^*]+\*\*\s*$/) && rawLines[i + 1]?.startsWith(": ")) { const dlItems: { term: string; def: string[] }[] = []; while (i < rawLines.length) { const termMatch = rawLines[i].match(/^\*\*([^*]+)\*\*\s*$/); if (!termMatch) break; const term = termMatch[1]; i++; if (rawLines[i]?.startsWith(": ")) { const defLines: string[] = [rawLines[i].slice(2)]; i++; while ( i < rawLines.length && rawLines[i].trim() !== "" && !rawLines[i].match(/^\*\*[^*]+\*\*\s*$/) && !rawLines[i].startsWith("## ") ) { defLines.push(rawLines[i].startsWith(" ") ? rawLines[i].slice(2) : rawLines[i]); i++; } dlItems.push({ term, def: defLines }); } else { break; } while (i < rawLines.length && rawLines[i].trim() === "") i++; } lines.push(`${pad}
`); for (const { term, def } of dlItems) { lines.push(`${pad}
${escapeXml(term)}
`); lines.push(`${pad}
${processInlineElements(def.join(" "))}
`); } lines.push(`${pad}
`); continue; } if (line.trim() === "" || line === "---") { i++; continue; } if (line.startsWith("#")) { i++; continue; } let paragraph = line; i++; while ( i < rawLines.length && rawLines[i].trim() !== "" && !rawLines[i].startsWith("```") && !rawLines[i].startsWith("| ") && !rawLines[i].match(/^[-*]\s+/) && !rawLines[i].match(/^\d+\.\s+/) && !rawLines[i].startsWith("#") ) { paragraph += ` ${rawLines[i]}`; i++; } if (paragraph.trim()) { const standaloneBoldMatch = paragraph.trim().match(/^\*\*([^*]+)\*\*$/); if (standaloneBoldMatch) { lines.push(`${pad}${escapeXml(standaloneBoldMatch[1])}`); } else { lines.push(`${pad}${processInlineElements(paragraph.trim())}`); } } } return lines.join("\n"); } function processTable(tableLines: string[], indent: number): string { const pad = " ".repeat(indent); const lines: string[] = []; const rows = tableLines .filter((l) => !l.match(/^\|[-:| ]+\|$/)) .map((l) => l .split("|") .slice(1, -1) .map((c) => c.trim()), ); if (rows.length === 0) return ""; lines.push(`${pad}`); const [header, ...body] = rows; lines.push(`${pad} `); lines.push(`${pad} `); for (const cell of header) { lines.push(`${pad} `); } lines.push(`${pad} `); lines.push(`${pad} `); if (body.length > 0) { lines.push(`${pad} `); for (const row of body) { lines.push(`${pad} `); for (const cell of row) { lines.push(`${pad} `); } lines.push(`${pad} `); } lines.push(`${pad} `); } lines.push(`${pad}
${processInlineElements(cell)}
${processInlineElements(cell)}
`); return lines.join("\n"); } function processInlineElements(text: string): string { let result = escapeXml(text); result = result.replace(/\*\*([^*]+)\*\*/g, "$1"); result = result.replace(/\*([^*]+)\*/g, "$1"); result = result.replace(/`([^`]+)`/g, "$1"); result = result.replace(/\[RFC(\d+)\]/g, ''); // Convert custom references like [Tempo Payment Method] to xref result = result.replace(/\[([^\]]+)\]/g, (match, name) => { // Skip if it's a markdown link (will be handled below) if (result.includes(`${match}(`)) { return match; } // Check if this is a known custom reference const anchor = customRefMap.get(name); if (anchor) { return ``; } // Not a known reference, leave as-is return match; }); result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); result = Document.wrapBcp14Keywords(result); return result; } function escapeXml(text: string): string { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }