11 if (attr.name ==
name) {
return &attr.value; }
17 const std::string &fallback)
const {
19 return value ? *value : fallback;
23 for (
const auto &element :
children) {
24 if (element.name ==
name) {
return &element; }
29std::vector<const Element *>
31 std::vector<const Element *> found;
32 for (
const auto &element :
children) {
33 if (element.name ==
name) { found.push_back(&element); }
40bool isNameStart(
char c) {
41 return std::isalpha(
static_cast<unsigned char>(c)) != 0 || c ==
'_' || c ==
':';
44bool isNameChar(
char c) {
45 return std::isalnum(
static_cast<unsigned char>(c)) != 0 || c ==
'_' ||
46 c ==
'-' || c ==
'.' || c ==
':';
51 explicit Parser(
const std::string &text) :
text_(text) {}
53 Element parseDocument() {
55 if (!looking_at(
"<")) { fail(
"expected a root element"); }
56 Element root = parseElement();
58 if (
pos_ !=
text_.size()) { fail(
"trailing content after the root element"); }
63 [[noreturn]]
void fail(
const std::string &message)
const {
65 std::size_t column = 1;
66 for (std::size_t i = 0; i <
pos_ && i <
text_.size(); ++i) {
67 if (
text_[i] ==
'\n') {
74 std::ostringstream out;
75 out <<
"XML parse error at line " << line <<
", column " << column <<
": "
77 throw XmlError(out.str());
80 [[nodiscard]]
bool eof()
const {
return pos_ >=
text_.size(); }
82 [[nodiscard]]
bool looking_at(
const char *literal)
const {
83 return text_.compare(
pos_, std::char_traits<char>::length(literal),
87 void expect(
const char *literal,
const std::string &context) {
88 if (!looking_at(literal)) { fail(
"expected '" + std::string(literal) +
"' " + context); }
89 pos_ += std::char_traits<char>::length(literal);
93 while (!eof() && std::isspace(
static_cast<unsigned char>(
text_[
pos_])) != 0) {
102 if (looking_at(
"<!--")) {
103 std::size_t end =
text_.find(
"-->",
pos_ + 4);
104 if (end == std::string::npos) { fail(
"unterminated comment"); }
115 if (looking_at(
"<?xml")) {
116 std::size_t end =
text_.find(
"?>",
pos_);
117 if (end == std::string::npos) { fail(
"unterminated XML declaration"); }
123 std::string parseName(
const std::string &context) {
124 if (eof() || !isNameStart(
text_[
pos_])) { fail(
"expected a name " + context); }
125 std::size_t start =
pos_;
127 return text_.substr(start,
pos_ - start);
130 std::string decodeEntities(
const std::string &raw) {
131 if (raw.find(
'&') == std::string::npos) {
return raw; }
133 out.reserve(raw.size());
134 for (std::size_t i = 0; i < raw.size(); ++i) {
136 out.push_back(raw[i]);
139 std::size_t semi = raw.find(
';', i);
140 if (semi == std::string::npos) { fail(
"unterminated entity reference"); }
141 std::string entity = raw.substr(i + 1, semi - i - 1);
142 if (entity ==
"amp") {
144 }
else if (entity ==
"lt") {
146 }
else if (entity ==
"gt") {
148 }
else if (entity ==
"quot") {
150 }
else if (entity ==
"apos") {
152 }
else if (entity.size() > 1 && entity[0] ==
'#') {
154 std::size_t offset = 1;
155 if (entity[1] ==
'x' || entity[1] ==
'X') {
160 unsigned long code = std::stoul(entity.substr(offset),
nullptr, base);
162 fail(
"only ASCII character references are supported");
164 out.push_back(
static_cast<char>(code));
165 }
catch (
const std::invalid_argument &) {
166 fail(
"malformed character reference '&" + entity +
";'");
167 }
catch (
const std::out_of_range &) {
168 fail(
"character reference out of range '&" + entity +
";'");
171 fail(
"unknown entity '&" + entity +
";'");
178 Attribute parseAttribute() {
180 attr.name = parseName(
"for an attribute");
182 expect(
"=",
"after attribute '" + attr.name +
"'");
185 fail(
"attribute '" + attr.name +
"' must have a quoted value");
188 std::size_t start =
pos_;
190 if (
text_[
pos_] ==
'<') { fail(
"'<' is not allowed inside an attribute value"); }
193 if (eof()) { fail(
"unterminated attribute value for '" + attr.name +
"'"); }
194 attr.value = decodeEntities(
text_.substr(start,
pos_ - start));
199 Element parseElement() {
200 expect(
"<",
"to open an element");
201 if (looking_at(
"!") || looking_at(
"?")) {
202 fail(
"DOCTYPE, CDATA and processing instructions are not supported");
205 element.name = parseName(
"for an element");
209 if (looking_at(
"/>")) {
213 if (looking_at(
">")) {
217 if (eof()) { fail(
"unterminated start tag '" + element.name +
"'"); }
218 element.attributes.push_back(parseAttribute());
224 skipTextAndComments();
225 if (eof()) { fail(
"unterminated element '" + element.name +
"'"); }
226 if (looking_at(
"</")) {
228 std::string closing = parseName(
"for a closing tag");
229 if (closing != element.name) {
230 fail(
"closing tag '" + closing +
"' does not match '" + element.name +
"'");
233 expect(
">",
"to close '" + closing +
"'");
236 element.children.push_back(parseElement());
240 void skipTextAndComments() {
242 if (looking_at(
"<!--")) {
243 std::size_t end =
text_.find(
"-->",
pos_ + 4);
244 if (end == std::string::npos) { fail(
"unterminated comment"); }
248 if (eof() ||
text_[
pos_] ==
'<') {
return; }
259Element parse(
const std::string &text) {
return Parser(text).parseDocument(); }
262 std::ifstream in(path, std::ios::binary);
263 if (!in) {
throw XmlError(
"cannot open '" + path +
"'"); }
264 std::ostringstream buffer;
265 buffer << in.rdbuf();
266 return parse(buffer.str());
Exception thrown by parse() and parseFile().
Element parseFile(const std::string &path)
Element parse(const std::string &text)
One XML element with its attributes and child elements.
std::vector< Attribute > attributes
in document order
std::string name
tag name as written
const Element * child(const std::string &name) const
const std::string * attribute(const std::string &name) const
std::vector< Element > children
child elements in document order
std::string attributeOr(const std::string &name, const std::string &fallback) const
std::vector< const Element * > childrenNamed(const std::string &name) const
const std::string & text_
Minimal XML reader used by the .fta parser: the Element tree, its lookup helpers, and parse() / parse...