Converting XML documents to Markdown sounds simple until you actually try it. Here's what we learned building a converter for Turkey's UYAP legal document format.
The Core Algorithm
XML DOM → Walk tree → Emit Markdown tokens → Join & clean
Step 1: Parse XML
const parser = new DOMParser();
const doc = parser.parseFromString(xmlText, "text/xml");
// Always check for parse errors!
const err = doc.querySelector("parsererror");
if (err) throw new Error("Invalid XML: " + err.textContent);
Step 2: Walk the Element Tree
function walkElements(container) {
let md = "";
for (const child of container.children) {
switch (child.nodeName.toLowerCase()) {
case "paragraph": md += paragraphToMd(child) + "\n\n"; break;
case "table": md += tableToMd(child) + "\n\n"; break;
case "image": md += imageToMd(child) + "\n\n"; break;
}
}
return md;
}
Step 3: Inline Formatting
The tricky part — XML attributes map to Markdown syntax:
function formatText(text, attrs) {
if (attrs.bold && attrs.italic) return `***${text}***`;
if (attrs.bold) return `**${text}**`;
if (attrs.italic) return `*${text}*`;
return text;
}
Edge case: What if bold and italic spans overlap partially? XML handles this naturally with separate <content> elements, but you need to be careful not to produce broken Markdown like **bold *both** italic*.
Heading Detection Heuristics
UYAP XML doesn't have explicit heading elements. We detect headings via heuristics:
function isHeading(paragraph, firstContent) {
const fontSize = parseInt(firstContent.getAttribute("fontSize") || "12");
const isBold = firstContent.getAttribute("bold") === "true";
const isCenter = paragraph.getAttribute("alignment") === "2";
const text = paragraph.textContent.trim();
// Short, large-font text → H1
if (fontSize >= 18 && text.length < 120) return 1;
// Bold + large or bold + centered → H2
if (fontSize >= 14 && isBold && text.length < 120) return 2;
if (isBold && isCenter && text.length < 120) return 2;
return 0; // Not a heading
}
Table Conversion
XML tables → Markdown tables require knowing the column count upfront:
function tableToMd(tableNode) {
const rows = tableNode.querySelectorAll("row");
let md = "";
let headerDone = false;
for (const row of rows) {
const cells = [...row.querySelectorAll("cell")]
.map(c => c.textContent.trim().replace(/\|/g, "\\|"));
md += "| " + cells.join(" | ") + " |\n";
if (!headerDone) {
md += "| " + cells.map(() => "---").join(" | ") + " |\n";
headerDone = true;
}
}
return md;
}
Edge case: Cells containing pipe characters (|) must be escaped, or the table breaks.
Character Encoding
Turkish legal documents may use Windows-1254 encoding:
// Try UTF-8 first
let text = new TextDecoder("utf-8").decode(buffer);
// If it looks garbled, try Windows-1254
if (!text.includes("<template")) {
text = new TextDecoder("windows-1254").decode(buffer);
}
The Result
Input: Opaque binary .udf file
Output: Clean, structured Markdown that any LLM can understand
Check out the full implementation: github.com/eimza-kep/udf2md
Top comments (0)