endfield-mcp
Version:
MCP Server for Arknights: Endfield (TypeScript / stdio + Streamable HTTP)
133 lines • 5.07 kB
JavaScript
/**
* Parsetree XML parser — extracts structured template data from MediaWiki
* `action=parse&prop=parsetree` output.
*
* Split out of `endfieldWiki.ts` so the wiki client stays under the file-size
* guard (see `docs/dev/STYLE.md`). This module is a self-contained,
* network-free, side-effect-free pure function over a string — exactly the
* kind of unit that earns its own file: deep nesting in the original,
* independently testable, and consumed by exactly one caller
* (`getTemplateData`) via a single narrow entry point.
*
* Ported verbatim from PRTS-MCP's `ts/src/api/prtsWiki.ts` parsetree block;
* the MediaWiki parsetree XML shape is game-agnostic.
*/
// ---------------------------------------------------------------------------
// Top-level <template> tag splitter
// ---------------------------------------------------------------------------
/**
* Yield each top-level `<tag>...</tag>` substring from `xml`, respecting
* nesting. Self-nesting tags (templates inside templates) are returned as a
* single outermost span so callers see one entry per top-level template.
*/
function* splitTopLevelTags(xml, tag) {
const open = `<${tag}`;
const close = `</${tag}>`;
let depth = 0;
let start = 0;
for (let i = 0; i < xml.length;) {
if (xml.startsWith(open, i)) {
if (depth === 0)
start = i;
depth++;
i += open.length;
}
else if (xml.startsWith(close, i)) {
depth--;
if (depth === 0) {
yield xml.substring(start, i + close.length);
}
i += close.length;
}
else {
i++;
}
}
}
function stripComments(xml) {
return xml.replace(/<comment>[\s\S]*?<\/comment>/g, "");
}
// ---------------------------------------------------------------------------
// <part> parser — one template parameter per <part>
// ---------------------------------------------------------------------------
const PART_RE = /<part>([\s\S]*?)<\/part>/g;
const NAME_RE = /<name[^>]*>([\s\S]*?)<\/name>/;
const VALUE_RE = /<value>([\s\S]*?)<\/value>/;
const INDEX_RE = /\bindex\s*=/;
function parsePart(partXml) {
const nameMatch = partXml.match(NAME_RE);
const valueMatch = partXml.match(VALUE_RE);
if (!valueMatch?.[1])
return null;
// Strip nested template tags from the value
const raw = valueMatch[1].replace(/<template[\s\S]*?<\/template>/g, "");
const value = raw.trim();
if (!value)
return null;
if (nameMatch) {
const nameText = nameMatch[1].replace(/<[^>]+>/g, "").trim();
if (!nameText && INDEX_RE.test(partXml))
return { value }; // positional (index=N)
if (nameText)
return { key: nameText, value };
}
return { value };
}
// ---------------------------------------------------------------------------
// Public entry point
// ---------------------------------------------------------------------------
/**
* Parse a MediaWiki parsetree XML document into a map of template name →
* fields. Each template becomes:
*
* - Named parameters (`{{T|key=value}}`) → top-level keys.
* - Positional parameters (`{{T|value1|value2}}`) → `_positional` array.
* - The template's own `<!-- comment -->` → `_comment` string.
*
* Templates with no extractable fields are omitted. When multiple templates
* share the same name, the last one wins (matches MediaWiki semantics where
* later invocations override earlier display output).
*
* @param xml The raw `parsetree["*"]` string from `action=parse&prop=parsetree`.
* @returns Map keyed by template title (whitespace-trimmed, newlines removed).
*/
export function parseParsetreeXml(xml) {
const templates = {};
for (const tXml of splitTopLevelTags(xml, "template")) {
const titleMatch = tXml.match(/<title>([\s\S]*?)<\/title>/);
if (!titleMatch)
continue;
const title = stripComments(titleMatch[1]).replace(/\n/g, "").trim();
if (!title)
continue;
const commentMatch = tXml.match(/<comment>([\s\S]*?)<\/comment>/);
const comment = commentMatch?.[1]?.trim() ?? "";
const kv = {};
const positional = [];
let pMatch;
PART_RE.lastIndex = 0;
while ((pMatch = PART_RE.exec(tXml)) !== null) {
const parsed = parsePart(pMatch[1]);
if (!parsed)
continue;
if (parsed.key) {
kv[parsed.key] = parsed.value;
}
else {
positional.push(parsed.value);
}
}
const entry = {};
if (Object.keys(kv).length > 0)
Object.assign(entry, kv);
if (positional.length > 0)
entry._positional = positional;
if (comment)
entry._comment = comment;
if (Object.keys(entry).length > 0) {
templates[title] = entry;
}
}
return templates;
}
//# sourceMappingURL=parsetreeParser.js.map