The Battle for Wesnoth  1.19.11+dev
parser.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2005 - 2025
3  by Philippe Plantier <ayin@anathas.org>
4  Copyright (C) 2005 by Guillaume Melquiond <guillaume.melquiond@gmail.com>
5  Copyright (C) 2003 by David White <dave@whitevine.net>
6  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
7 
8  This program is free software; you can redistribute it and/or modify
9  it under the terms of the GNU General Public License as published by
10  the Free Software Foundation; either version 2 of the License, or
11  (at your option) any later version.
12  This program is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY.
14 
15  See the COPYING file for more details.
16 */
17 
18 /**
19  * @file
20  * Read/Write & analyze WML- and config-files.
21  */
22 
23 #include "serialization/parser.hpp"
24 
25 #include "config.hpp"
26 #include "gettext.hpp"
27 #include "log.hpp"
31 #include "utils/charconv.hpp"
32 #include "wesconfig.h"
33 
34 #include <boost/algorithm/string/replace.hpp>
35 #include <boost/iostreams/filter/bzip2.hpp>
36 #include <boost/iostreams/filtering_stream.hpp>
37 
38 #if defined(_MSC_VER)
39 #pragma warning(push)
40 #pragma warning(disable : 4456)
41 #pragma warning(disable : 4458)
42 #endif
43 
44 #include <boost/iostreams/filter/gzip.hpp>
45 
46 #if defined(_MSC_VER)
47 #pragma warning(pop)
48 #endif
49 
50 #include <stack>
51 
52 static lg::log_domain log_config("config");
53 #define ERR_CF LOG_STREAM(err, log_config)
54 #define WRN_CF LOG_STREAM(warn, log_config)
55 #define LOG_CF LOG_STREAM(info, log_config)
56 
57 constexpr std::size_t max_recursion_levels = 1000;
58 
59 namespace io
60 {
61 namespace
62 {
63 // ==================================================================================
64 // PARSER
65 // ==================================================================================
66 
67 class parser
68 {
69 public:
70  parser(std::istream& in, abstract_validator* validator = nullptr)
71  : tok_(in)
72  , validator_(validator)
73  , elements()
74  {
75  }
76 
77  config operator()();
78 
79 private:
80  void parse_element();
81  void parse_variable();
82 
83  std::string lineno_string(utils::string_map& map,
84  const std::string& lineno,
85  const std::string& error_string,
86  const std::string& hint_string = "",
87  const std::string& debug_string = "");
88 
89  /** @throws config::error */
90  [[noreturn]] void error(const std::string& message, const std::string& pos_format = "");
91 
92  struct element
93  {
94 #ifndef __cpp_aggregate_paren_init
95  element(config* cfg, const std::string& name, int start_line = 0, const std::string& file = "")
96  : cfg(cfg)
97  , name(name)
98  , start_line(start_line)
99  , file(file)
100  {
101  }
102 #endif
103 
104  config* cfg{};
105  std::string name{};
106  int start_line{};
107  std::string file{};
108  };
109 
110  tokenizer tok_;
111  abstract_validator* validator_;
112 
113  std::stack<element> elements;
114 };
115 
116 config parser::operator()()
117 {
118  config res;
119  elements.emplace(&res, "");
120 
121  if(validator_) {
122  validator_->open_tag("", res, tok_.get_start_line(), tok_.get_file());
123  }
124 
125  do {
126  tok_.next_token();
127 
128  switch(tok_.current_token().type) {
129  case token::NEWLINE:
130  continue;
131 
132  case token::OPEN_BRACKET:
133  parse_element();
134  break;
135 
136  case token::STRING:
137  parse_variable();
138  break;
139 
140  default:
141  if(static_cast<unsigned char>(tok_.current_token().value[0]) == 0xEF &&
142  static_cast<unsigned char>(tok_.next_token().value[0]) == 0xBB &&
143  static_cast<unsigned char>(tok_.next_token().value[0]) == 0xBF
144  ) {
145  utils::string_map i18n_symbols;
146  std::stringstream ss;
147  ss << tok_.get_start_line() << " " << tok_.get_file();
148  ERR_CF << lineno_string(i18n_symbols, ss.str(), "Skipping over a utf8 BOM at $pos");
149  } else {
150  error(_("Unexpected characters at line start"));
151  }
152 
153  break;
154 
155  case token::END:
156  break;
157  }
158  } while(tok_.current_token().type != token::END);
159 
160  // The main element should be there. If it is not, this is a parser error.
161  assert(!elements.empty());
162 
163  if(validator_) {
164  element& el = elements.top();
165  validator_->validate(*el.cfg, el.name, el.start_line, el.file);
166  validator_->close_tag();
167  }
168 
169  if(elements.size() != 1) {
170  utils::string_map i18n_symbols;
171  i18n_symbols["tag"] = elements.top().name;
172 
173  std::stringstream ss;
174  ss << elements.top().start_line << " " << elements.top().file;
175 
176  error(lineno_string(i18n_symbols, ss.str(),
177  _("Missing closing tag for tag [$tag]"),
178  _("expected at $pos")),
179  _("opened at $pos")
180  );
181  }
182 
183  return res;
184 }
185 
186 void parser::parse_element()
187 {
188  tok_.next_token();
189 
190  std::string elname;
191  config* current_element = nullptr;
192  config* parent = nullptr;
193 
194  switch(tok_.current_token().type) {
195  case token::STRING: // [element]
196  elname = tok_.current_token().value;
197 
198  if(tok_.next_token().type != token::CLOSE_BRACKET) {
199  error(_("Unterminated [element] tag"));
200  }
201 
202  // Add the element
203  parent = elements.top().cfg;
204  current_element = &(parent->add_child(elname));
205  elements.emplace(current_element, elname, tok_.get_start_line(), tok_.get_file());
206 
207  if(validator_) {
208  validator_->open_tag(elname, *parent, tok_.get_start_line(), tok_.get_file());
209  }
210 
211  break;
212 
213  case token::PLUS: // [+element]
214  if(tok_.next_token().type != token::STRING) {
215  error(_("Invalid tag name"));
216  }
217 
218  elname = tok_.current_token().value;
219 
220  if(tok_.next_token().type != token::CLOSE_BRACKET) {
221  error(_("Unterminated [+element] tag"));
222  }
223 
224  // Find the last child of the current element whose name is element
225  parent = elements.top().cfg;
226  if(auto c = parent->optional_child(elname, -1)) {
227  current_element = c.ptr();
228 
229  if(validator_) {
230  validator_->open_tag(elname, *parent, tok_.get_start_line(), tok_.get_file(), true);
231  }
232  } else {
233  current_element = &parent->add_child(elname);
234 
235  if(validator_) {
236  validator_->open_tag(elname, *parent, tok_.get_start_line(), tok_.get_file());
237  }
238  }
239 
240  elements.emplace(current_element, elname, tok_.get_start_line(), tok_.get_file());
241  break;
242 
243  case token::SLASH: // [/element]
244  if(tok_.next_token().type != token::STRING) {
245  error(_("Invalid closing tag name"));
246  }
247 
248  elname = tok_.current_token().value;
249 
250  if(tok_.next_token().type != token::CLOSE_BRACKET) {
251  error(_("Unterminated closing tag"));
252  }
253 
254  if(elements.size() <= 1) {
255  error(_("Unexpected closing tag"));
256  }
257 
258  if(elname != elements.top().name) {
259  utils::string_map i18n_symbols;
260  i18n_symbols["tag1"] = elements.top().name;
261  i18n_symbols["tag2"] = elname;
262 
263  std::stringstream ss;
264  ss << elements.top().start_line << " " << elements.top().file;
265 
266  error(lineno_string(i18n_symbols, ss.str(),
267  _("Found invalid closing tag [/$tag2] for tag [$tag1]"),
268  _("opened at $pos")),
269  _("closed at $pos")
270  );
271  }
272 
273  if(validator_) {
274  element& el = elements.top();
275  validator_->validate(*el.cfg, el.name, el.start_line, el.file);
276  validator_->close_tag();
277  }
278 
279  elements.pop();
280  break;
281 
282  default:
283  error(_("Invalid tag name"));
284  }
285 }
286 
287 void parser::parse_variable()
288 {
289  config& cfg = *elements.top().cfg;
290  std::vector<std::string> variables;
291  variables.emplace_back();
292 
293  while(tok_.current_token().type != token::EQUALS) {
294  switch(tok_.current_token().type) {
295  case token::STRING:
296  if(!variables.back().empty()) {
297  variables.back() += ' ';
298  }
299 
300  variables.back() += tok_.current_token().value;
301  break;
302 
303  case token::COMMA:
304  if(variables.back().empty()) {
305  error(_("Empty variable name"));
306  } else {
307  variables.emplace_back();
308  }
309 
310  break;
311 
312  default:
313  error(_("Unexpected characters after variable name (expected , or =)"));
314  break;
315  }
316 
317  tok_.next_token();
318  }
319 
320  if(variables.back().empty()) {
321  error(_("Empty variable name"));
322  }
323 
324  t_string_base buffer;
325 
326  std::vector<std::string>::const_iterator curvar = variables.begin();
327 
328  bool ignore_next_newlines = false, previous_string = false;
329 
330  while(true) {
331  tok_.next_token();
332  assert(curvar != variables.end());
333 
334  switch(tok_.current_token().type) {
335  case token::COMMA:
336  if((curvar + 1) != variables.end()) {
337  if(buffer.translatable()) {
338  cfg[*curvar] = t_string(buffer);
339  } else {
340  cfg[*curvar] = buffer.value();
341  }
342 
343  if(validator_) {
344  validator_->validate_key(cfg, *curvar, cfg[*curvar], tok_.get_start_line(), tok_.get_file());
345  }
346 
347  buffer = t_string_base();
348  ++curvar;
349  } else {
350  buffer += ",";
351  }
352 
353  break;
354 
355  case token::UNDERSCORE:
356  tok_.next_token();
357 
358  switch(tok_.current_token().type) {
360  error(_("Unterminated quoted string"));
361  break;
362 
363  case token::QSTRING:
364  buffer += t_string_base(tok_.current_token().value, tok_.textdomain());
365  break;
366 
367  default:
368  buffer += "_";
369  buffer += tok_.current_token().value;
370  break;
371 
372  case token::END:
373  case token::NEWLINE:
374  buffer += "_";
375  goto finish;
376  }
377 
378  break;
379 
380  case token::PLUS:
381  ignore_next_newlines = true;
382  continue;
383 
384  case token::STRING:
385  if(previous_string) {
386  buffer += " ";
387  }
388 
389  [[fallthrough]];
390 
391  default:
392  buffer += tok_.current_token().value;
393  break;
394 
395  case token::QSTRING:
396  buffer += tok_.current_token().value;
397  break;
398 
400  error(_("Unterminated quoted string"));
401  break;
402 
403  case token::NEWLINE:
404  if(ignore_next_newlines) {
405  continue;
406  }
407 
408  [[fallthrough]];
409 
410  case token::END:
411  goto finish;
412  }
413 
414  previous_string = tok_.current_token().type == token::STRING;
415  ignore_next_newlines = false;
416  }
417 
418 finish:
419 
420  if(buffer.translatable()) {
421  cfg[*curvar] = t_string(buffer);
422  } else {
423  cfg[*curvar] = buffer.value();
424  }
425 
426  if(validator_) {
427  validator_->validate_key(cfg, *curvar, cfg[*curvar], tok_.get_start_line(), tok_.get_file());
428  }
429 
430  while(++curvar != variables.end()) {
431  cfg[*curvar] = "";
432  }
433 }
434 
435 /**
436  * This function is crap. Don't use it on a string_map with prefixes.
437  */
438 std::string parser::lineno_string(utils::string_map& i18n_symbols,
439  const std::string& lineno,
440  const std::string& error_string,
441  const std::string& hint_string,
442  const std::string& debug_string)
443 {
444  i18n_symbols["pos"] = ::lineno_string(lineno);
445  std::string result = error_string;
446 
447  if(!hint_string.empty()) {
448  result += '\n' + hint_string;
449  }
450 
451  if(!debug_string.empty()) {
452  result += '\n' + debug_string;
453  }
454 
455  for(utils::string_map::value_type& var : i18n_symbols) {
456  boost::algorithm::replace_all(result, std::string("$") + var.first, std::string(var.second));
457  }
458 
459  return result;
460 }
461 
462 void parser::error(const std::string& error_type, const std::string& pos_format)
463 {
464  std::string hint_string = pos_format;
465 
466  if(hint_string.empty()) {
467  hint_string = _("at $pos");
468  }
469 
470  utils::string_map i18n_symbols;
471  i18n_symbols["error"] = error_type;
472 
473  std::stringstream ss;
474  ss << tok_.get_start_line() << " " << tok_.get_file();
475 
476 #ifdef DEBUG_TOKENIZER
477  i18n_symbols["value"] = tok_.current_token().value;
478  i18n_symbols["previous_value"] = tok_.previous_token().value;
479 
480  const std::string tok_state = _("Value: ‘$value’ Previous: ‘$previous_value’");
481 #else
482  const std::string tok_state = "";
483 #endif
484 
485  throw config::error(lineno_string(i18n_symbols, ss.str(), "$error", hint_string, tok_state));
486 }
487 
488 
489 // ==================================================================================
490 // HELPERS FOR WRITE_KEY_VAL
491 // ==================================================================================
492 
493 class write_key_val_visitor
494 #ifdef USING_BOOST_VARIANT
495  : public boost::static_visitor<void>
496 #endif
497 {
498 public:
499  write_key_val_visitor(std::ostream& out, unsigned level, std::string& textdomain, const std::string& key)
500  : out_(out)
501  , level_(level)
502  , textdomain_(textdomain)
503  , key_(key)
504  {
505  }
506 
507  // Generic visitor just streams "key=value".
508  template<typename T>
509  void operator()(const T& v) const
510  {
511  indent();
512  if constexpr(std::is_arithmetic_v<T>) {
513  // for number values, this has to use the same method as in from_string_verify
514  auto buf = utils::charconv_buffer(v);
515  out_ << key_ << '=' << buf.get_view() << '\n';
516  } else {
517  out_ << key_ << '=' << v << '\n';
518  }
519  }
520 
521  //
522  // Specialized visitors for things that go in quotes:
523  //
524 
525  void operator()(const utils::monostate&) const
526  {
527  // Treat blank values as nonexistent which fits better than treating them as empty strings.
528  }
529 
530  void operator()(const std::string& s) const
531  {
532  indent();
533  out_ << key_ << '=' << '"' << utils::wml_escape_string(s) << '"' << '\n';
534  }
535 
536  void operator()(const t_string& s) const;
537 
538 private:
539  void indent() const
540  {
541  for(unsigned i = 0; i < level_; ++i) {
542  out_ << '\t';
543  }
544  }
545 
546  std::ostream& out_;
547  const unsigned level_;
548  std::string& textdomain_;
549  const std::string& key_;
550 };
551 
552 /**
553  * Writes all the parts of a translatable string.
554  *
555  * @note If the first part is translatable and in the wrong textdomain,
556  * the textdomain change has to happen before the attribute name.
557  * That is the reason for not outputting the key beforehand and
558  * letting this function do it.
559  */
560 void write_key_val_visitor::operator()(const t_string& value) const
561 {
562  bool first = true;
563 
564  for(t_string::walker w(value); !w.eos(); w.next()) {
565  if(!first) {
566  out_ << " +\n";
567  }
568 
569  if(w.translatable() && w.textdomain() != textdomain_) {
570  textdomain_ = w.textdomain();
571  out_ << "#textdomain " << textdomain_ << '\n';
572  }
573 
574  indent();
575 
576  if(first) {
577  out_ << key_ << '=';
578  } else {
579  out_ << '\t';
580  }
581 
582  if(w.translatable()) {
583  out_ << '_';
584  }
585 
586  out_ << '"' << utils::wml_escape_string(w) << '"';
587  first = false;
588  }
589 
590  out_ << '\n';
591 }
592 
593 } // end anon namespace
594 
595 
596 // ==================================================================================
597 // PUBLIC FUNCTION IMPLEMENTATIONS
598 // ==================================================================================
599 
600 config read(std::istream& in, abstract_validator* validator)
601 {
602  return parser(in, validator)();
603 }
604 
605 config read(const std::string& in, abstract_validator* validator)
606 {
607  std::istringstream ss(in);
608  return parser(ss, validator)();
609 }
610 
611 template<typename Decompressor>
612 config read_compressed(std::istream& file, abstract_validator* validator)
613 {
614  // An empty gzip file seems to confuse boost on MSVC, so return early if this is the case.
615  if(file.peek() == EOF) {
616  return {};
617  }
618 
619  boost::iostreams::filtering_stream<boost::iostreams::input> filter;
620  filter.push(Decompressor());
621  filter.push(file);
622 
623  /* This causes gzip_error (and the corresponding bz2 error, std::ios_base::failure) to be
624  * thrown here. save_index_class::data expects that and config_cache::read_cache and other
625  * functions are also capable of catching.
626  *
627  * Note that parser(cuff, filter,validator)(); -> tokenizer::tokenizer can throw exceptions
628  * too (meaning this function already threw these exceptions before this patch).
629  *
630  * We try to fix https://svn.boost.org/trac/boost/ticket/5237 by not creating empty gz files.
631  */
632  filter.exceptions(filter.exceptions() | std::ios_base::badbit);
633 
634  /*
635  * It sometimes seems the file is not empty but still has no real data.
636  * Filter that case here. The previous test might be no longer required but keep it for now.
637  *
638  * On msvc filter.peek() != EOF does not imply filter.good().
639  * We never create empty compressed gzip files because boosts gzip fails at doing that, but
640  * empty compressed bz2 files are possible.
641  */
642  if(filter.peek() == EOF) {
643  LOG_CF << "Empty compressed file or error at reading a compressed file.";
644  return {};
645  }
646 
647  if(!filter.good()) {
648  LOG_CF << " filter.peek() != EOF but !filter.good()."
649  << "This indicates a malformed gz stream and can make Wesnoth crash.";
650  }
651 
652  return parser(filter, validator)();
653 }
654 
655 /** Might throw a std::ios_base::failure especially a gzip_error. */
656 config read_gz(std::istream& file, abstract_validator* validator)
657 {
658  return read_compressed<boost::iostreams::gzip_decompressor>(file, validator);
659 }
660 
661 /** Might throw a std::ios_base::failure especially bzip2_error. */
662 config read_bz2(std::istream& file, abstract_validator* validator)
663 {
664  return read_compressed<boost::iostreams::bzip2_decompressor>(file, validator);
665 }
666 
667 void write_key_val(std::ostream& out,
668  const std::string& key,
669  const config::attribute_value& value,
670  unsigned level,
671  std::string& textdomain)
672 {
673  value.apply_visitor(write_key_val_visitor(out, level, textdomain, key));
674 }
675 
676 void write_open_child(std::ostream& out, const std::string& child, unsigned int level)
677 {
678  out << std::string(level, '\t') << '[' << child << "]\n";
679 }
680 
681 void write_close_child(std::ostream& out, const std::string& child, unsigned int level)
682 {
683  out << std::string(level, '\t') << "[/" << child << "]\n";
684 }
685 
686 static void write_internal(const config& cfg, std::ostream& out, std::string& textdomain, std::size_t tab = 0)
687 {
688  if(tab > max_recursion_levels) {
689  throw config::error("Too many recursion levels in config write");
690  }
691 
692  for(const auto& [key, value] : cfg.attribute_range()) {
693  if(!config::valid_attribute(key)) {
694  ERR_CF << "Config contains invalid attribute name '" << key << "', skipping...";
695  continue;
696  }
697 
698  write_key_val(out, key, value, tab, textdomain);
699  }
700 
701  for(const auto [key, item_cfg] : cfg.all_children_view()) {
702  if(!config::valid_tag(key)) {
703  ERR_CF << "Config contains invalid tag name '" << key << "', skipping...";
704  continue;
705  }
706 
707  write_open_child(out, key, tab);
708  write_internal(item_cfg, out, textdomain, tab + 1);
709  write_close_child(out, key, tab);
710  }
711 }
712 
713 static void write_internal(const configr_of& cfg, std::ostream& out, std::string& textdomain, std::size_t tab = 0)
714 {
715  if(tab > max_recursion_levels) {
716  throw config::error("Too many recursion levels in config write");
717  }
718 
719  if(cfg.data_) {
720  write_internal(*cfg.data_, out, textdomain, tab);
721  }
722 
723  for(const auto& pair : cfg.subtags_) {
724  assert(pair.first && pair.second);
725 
726  if(!config::valid_tag(*pair.first)) {
727  ERR_CF << "Config contains invalid tag name '" << *pair.first << "', skipping...";
728  continue;
729  }
730 
731  write_open_child(out, *pair.first, tab);
732  write_internal(*pair.second, out, textdomain, tab + 1);
733  write_close_child(out, *pair.first, tab);
734  }
735 }
736 
737 void write(std::ostream& out, const configr_of& cfg, unsigned int level)
738 {
739  std::string textdomain = PACKAGE;
740  write_internal(cfg, out, textdomain, level);
741 }
742 
743 template<typename Compressor>
744 void write_compressed(std::ostream& out, const configr_of& cfg)
745 {
746  boost::iostreams::filtering_stream<boost::iostreams::output> filter;
747  filter.push(Compressor());
748  filter.push(out);
749 
750  write(filter, cfg);
751 
752  // prevent empty gz files because of https://svn.boost.org/trac/boost/ticket/5237
753  filter << "\n";
754 }
755 
756 void write_gz(std::ostream& out, const configr_of& cfg)
757 {
758  write_compressed<boost::iostreams::gzip_compressor>(out, cfg);
759 }
760 
761 void write_bz2(std::ostream& out, const configr_of& cfg)
762 {
763  write_compressed<boost::iostreams::bzip2_compressor>(out, cfg);
764 }
765 
766 } // namespace io
Used in parsing config file.
Definition: validator.hpp:38
Variant for storing WML attributes.
auto apply_visitor(const V &visitor) const
Visitor support: Applies a visitor to the underlying variant.
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
const_attr_itors attribute_range() const
Definition: config.cpp:756
auto all_children_view() const
In-order iteration over all children.
Definition: config.hpp:796
static bool valid_tag(config_key_type name)
Definition: config.cpp:129
static bool valid_attribute(config_key_type name)
Definition: config.cpp:152
optional_config_impl< config > optional_child(config_key_type key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:380
config & add_child(config_key_type key)
Definition: config.cpp:436
Helper class for translatable strings.
Definition: tstring.hpp:27
bool translatable() const
Definition: tstring.hpp:112
const std::string & value() const
Definition: tstring.hpp:116
class responsible for parsing the provided text into tokens and tracking information about the curren...
Definition: tokenizer.hpp:99
Definitions for the interface to Wesnoth Markup Language (WML).
std::size_t i
Definition: function.cpp:1030
int w
unsigned in
If equal to search_counter, the node is off the list.
static std::string _(const char *str)
Definition: gettext.hpp:97
static int indent
Definition: log.cpp:59
Standard logging facilities (interface).
Definition: parser.cpp:60
config read(std::istream &in, abstract_validator *validator)
Definition: parser.cpp:600
void write_open_child(std::ostream &out, const std::string &child, unsigned int level)
Definition: parser.cpp:676
config read_compressed(std::istream &file, abstract_validator *validator)
Definition: parser.cpp:612
void write_close_child(std::ostream &out, const std::string &child, unsigned int level)
Definition: parser.cpp:681
config read_bz2(std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially bzip2_error.
Definition: parser.cpp:662
void write_bz2(std::ostream &out, const configr_of &cfg)
Definition: parser.cpp:761
void write(std::ostream &out, const configr_of &cfg, unsigned int level)
Definition: parser.cpp:737
config read_gz(std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially a gzip_error.
Definition: parser.cpp:656
static void write_internal(const config &cfg, std::ostream &out, std::string &textdomain, std::size_t tab=0)
Definition: parser.cpp:686
void write_compressed(std::ostream &out, const configr_of &cfg)
Definition: parser.cpp:744
void write_gz(std::ostream &out, const configr_of &cfg)
Definition: parser.cpp:756
void write_key_val(std::ostream &out, const std::string &key, const config::attribute_value &value, unsigned level, std::string &textdomain)
Definition: parser.cpp:667
constexpr auto filter
Definition: ranges.hpp:38
std::map< std::string, t_string > string_map
std::string wml_escape_string(std::string_view str)
Format str as a WML value
std::string lineno_string(const std::string &lineno)
constexpr std::size_t max_recursion_levels
Definition: parser.cpp:57
#define LOG_CF
Definition: parser.cpp:55
#define ERR_CF
Definition: parser.cpp:53
static lg::log_domain log_config("config")
std::vector< std::pair< const std::string *, const configr_of * > > subtags_
const config * data_
@ SLASH
Definition: tokenizer.hpp:67
@ QSTRING
quoted string, contained within double quotes or by less than/greater than symbols
Definition: tokenizer.hpp:56
@ COMMA
Definition: tokenizer.hpp:65
@ PLUS
Definition: tokenizer.hpp:66
@ CLOSE_BRACKET
Definition: tokenizer.hpp:69
@ UNTERMINATED_QSTRING
reached end of file without finding the closing character for a QSTRING
Definition: tokenizer.hpp:58
@ NEWLINE
Definition: tokenizer.hpp:63
@ EQUALS
Definition: tokenizer.hpp:64
@ UNDERSCORE
Definition: tokenizer.hpp:70
@ OPEN_BRACKET
Definition: tokenizer.hpp:68
@ END
set when EOF is returned by the input stream
Definition: tokenizer.hpp:73
@ STRING
unquoted text
Definition: tokenizer.hpp:54
mock_char c
static map_location::direction s
This file contains information about validation abstract level interface.
Some defines: VERSION, PACKAGE, MIN_SAVEGAME_VERSION.
#define PACKAGE
Definition: wesconfig.h:23