Files
WoWee/include/ui/xml_parser.hpp
Kelsi 64de52e577 feat(ui): read FrameXML
The XML half of the interface. 144 files in FrameXML define frames that Lua
only fills in, and most real addons put their layout there too — which is why
loading one could report success and draw nothing.

The loader emits Lua rather than building widgets from C++. Doing it the other
way would have meant a second implementation of everything CreateFrame already
does — parenting, naming, templates, script binding — kept in step with the
first by hand. This way XML frames and hand-written frames travel one path, so
anything fixed for one is fixed for both, a template declared in XML is usable
from a script without translation, and the emitter's output is a string a test
can read without a Lua state.

Covered: frames and their types, Size and Anchors in both the AbsDimension and
attribute spellings, Layers with Textures and FontStrings, nested Frames,
Scripts as inline CDATA or a named function, virtual frames as templates with
inherits applying several in order, and $parent expansion — which nearly every
region in the original interface depends on for its name.

The parser handles what FrameXML actually contains rather than what XML permits:
CDATA taken verbatim, because it holds Lua and decoding entities inside it would
corrupt every comparison; entities decoded everywhere else; comments and the
declaration skipped; both quote styles. Malformed input is reported rather than
thrown, so one bad file cannot take the rest of the interface down.

Run over the real data, all 228 XML files under Interface parse — the whole of
FrameXML and every Blizzard addon — emitting 2.7 MB of Lua with two warnings.
Thirteen tests cover the parser and the emitter directly.

This is the reader, not yet the bootstrap: nothing calls it during addon load
yet, and the API surface FrameXML expects is a separate and much longer job.
2026-08-01 11:54:43 -07:00

62 lines
2.3 KiB
C++

#pragma once
// A small XML reader, enough for FrameXML.
//
// FrameXML is not general XML and does not need a general parser: no DTDs, no
// processing instructions beyond the declaration, no namespaces in use beyond a
// single xmlns attribute nobody reads. What it does need is the awkward parts —
// CDATA around inline Lua, comments anywhere, self-closing elements, and both
// quote styles on attributes — because those appear on nearly every page of it.
//
// Kept free of Lua and of the widget tree so it can be tested on its own.
#include <map>
#include <string>
#include <vector>
namespace wowee {
namespace ui {
struct XmlNode {
std::string name;
std::map<std::string, std::string> attrs;
/// Text content, with CDATA sections concatenated in. Inline scripts arrive
/// here.
std::string text;
std::vector<XmlNode> children;
/// Attribute lookup, case-sensitive as XML requires. Returns nullptr when
/// absent, which callers distinguish from present-but-empty.
const std::string* attr(const std::string& key) const {
auto it = attrs.find(key);
return it == attrs.end() ? nullptr : &it->second;
}
std::string attrOr(const std::string& key, const std::string& fallback) const {
const std::string* v = attr(key);
return v ? *v : fallback;
}
/// FrameXML writes booleans as "true"/"false".
bool attrBool(const std::string& key, bool fallback = false) const {
const std::string* v = attr(key);
if (!v) return fallback;
return *v == "true" || *v == "1";
}
float attrFloat(const std::string& key, float fallback = 0.0f) const {
const std::string* v = attr(key);
if (!v || v->empty()) return fallback;
try { return std::stof(*v); } catch (...) { return fallback; }
}
const XmlNode* child(const std::string& name) const {
for (const auto& c : children) if (c.name == name) return &c;
return nullptr;
}
};
/// Parse a document. Returns false and fills `error` on malformed input rather
/// than throwing, because one bad file among a hundred should not take the rest
/// of the interface down with it.
bool parseXml(const std::string& source, XmlNode& outRoot, std::string& error);
} // namespace ui
} // namespace wowee