{"record":{"id":"5d9764f3b2b12d84","repo":"can1357/oh-my-pi","slug":"invalid-xml-mismatched-closing-tag-closingname","errorCode":null,"errorMessage":"Invalid XML: mismatched closing tag ${closingName}","messagePattern":"Invalid XML: mismatched closing tag (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/utils/src/docx/xml.ts","lineNumber":102,"sourceCode":"\t\t\tif (end === -1) throw new Error(\"Invalid XML: unterminated processing instruction\");\n\t\t\toffset = end + 2;\n\t\t\tcontinue;\n\t\t}\n\t\tif (source.startsWith(\"<!\", lessThan)) {\n\t\t\tconst end = source.indexOf(\">\", lessThan + 2);\n\t\t\tif (end === -1) throw new Error(\"Invalid XML: unterminated declaration\");\n\t\t\toffset = end + 1;\n\t\t\tcontinue;\n\t\t}\n\t\tconst end = source.indexOf(\">\", lessThan + 1);\n\t\tif (end === -1) throw new Error(\"Invalid XML: unterminated tag\");\n\t\tconst raw = source.slice(lessThan + 1, end).trim();\n\t\tif (raw.startsWith(\"/\")) {\n\t\t\tif (stack.length === 1) throw new Error(\"Invalid XML: unexpected closing tag\");\n\t\t\tconst closingName = raw.slice(1).trim();\n\t\t\tconst completed = stack.pop();\n\t\t\tif (!completed || completed.name !== closingName)\n\t\t\t\tthrow new Error(`Invalid XML: mismatched closing tag ${closingName}`);\n\t\t\tstack[stack.length - 1].children.push({\n\t\t\t\tkind: \"element\",\n\t\t\t\tname: completed.name,\n\t\t\t\tattributes: completed.attributes,\n\t\t\t\tchildren: completed.children,\n\t\t\t});\n\t\t} else {\n\t\t\tconst selfClosing = raw.endsWith(\"/\");\n\t\t\tconst tag = selfClosing ? raw.slice(0, -1).trim() : raw;\n\t\t\tconst whitespace = tag.search(/\\s/);\n\t\t\tconst name = whitespace === -1 ? tag : tag.slice(0, whitespace);\n\t\t\tconst attributes = new Map<string, string>();\n\t\t\tATTRIBUTE_PATTERN.lastIndex = whitespace === -1 ? tag.length : whitespace;\n\t\t\tfor (let match = ATTRIBUTE_PATTERN.exec(tag); match; match = ATTRIBUTE_PATTERN.exec(tag)) {\n\t\t\t\tattributes.set(match[1], decodeEntities(match[2] ?? match[3] ?? \"\"));\n\t\t\t}\n\t\t\tconst pending = { name, attributes, children: [] as XmlNode[] };\n\t\t\tif (selfClosing) {","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/docx/xml.ts#L84-L120","documentation":"parseXml pops the innermost open element when it sees a closing tag and compares the popped element's name with the closing tag's name. If they differ (e.g. `<a><b></a></b>`) or the stack was empty (`completed` is undefined, meaning the closing tag has no open element), it throws this mismatched-closing-tag error. It enforces proper XML nesting: tags must close in exact reverse order of opening.","triggerScenarios":"Calling parseXml/root/document/parseFootnotes on XML where a closing tag's name differs from the currently innermost open element, e.g. `<w:p><w:r></w:p></w:r>`, or a closing tag appears with no matching opening tag at all (the template-literal message then interpolates the closer's name).","commonSituations":"Incorrectly nested markup generated by template concatenation (`<b>...</i>`); missing an opening tag while keeping its closer; DOCX XML edited by hand or by a script that reorders elements; copy-paste of partial fragments into a document body.","solutions":["Locate the tag named in the message and check that its opening tag exists and properly encloses the content between the pair.","Reorder or rename the closing tag so nesting follows strict LIFO order (last opened, first closed).","Run the input through xmllint --noout to get a line/column report of the mismatch before retrying.","If building XML programmatically, switch to a serializer/builder that tracks nesting instead of concatenating strings."],"exampleFix":"// before: wrong nesting order\nconst xml = `<w:p><w:r></w:p></w:r>`;\nparseXml(xml); // throws: mismatched closing tag w:p\n\n// after: close in reverse order of opening\nconst xml = `<w:p><w:r></w:r></w:p>`;\nparseXml(xml); // ok","handlingStrategy":"validation","validationCode":"function nestingIsLifo(xml: string): boolean {\n  const stack: string[] = [];\n  for (const m of xml.matchAll(/<\\s*(\\/?)\\s*([\\w:.-]+)(?:\\s[^>]*)?(\\/?)\\s*>/g)) {\n    if (m[3] === \"/\") continue;\n    if (m[1]) {\n      if (stack.pop() !== m[2]) return false;\n    } else {\n      stack.push(m[2]);\n    }\n  }\n  return true;\n}\n// call before: if (!nestingIsLifo(xml)) throw new Error(\"bad nesting\"); else parseXml(xml);","typeGuard":null,"tryCatchPattern":"try {\n  return parseXml(xml);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"mismatched closing tag\")) {\n    throw new Error(`Malformed XML nesting: ${err.message}`);\n  }\n  throw err;\n}","preventionTips":["Close tags in strict reverse order of opening; lint templates that emit XML.","Use a builder API (element/append helpers) rather than hand-written tag strings.","Run xmllint --noout on generated documents in CI to catch mismatches early."],"tags":["xml","parsing","docx","malformed-input"],"backgroundTag":"xml-tag-mismatch","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}