The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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
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
52static 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
57constexpr std::size_t max_recursion_levels = 1000;
58
59namespace io
60{
61namespace
62{
63// ==================================================================================
64// PARSER
65// ==================================================================================
66
67class parser
68{
69public:
70 parser(std::istream& in, abstract_validator* validator = nullptr)
71 : tok_(in)
72 , validator_(validator)
73 , elements()
74 {
75 }
76
77 config operator()();
78
79private:
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
116config 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
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
186void 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
287void 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
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
418finish:
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 */
438std::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
462void 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
493class write_key_val_visitor
494#ifdef USING_BOOST_VARIANT
495 : public boost::static_visitor<void>
496#endif
497{
498public:
499 write_key_val_visitor(std::ostream& out, unsigned level, std::string& textdomain, const std::string& key, bool strong_quotes)
500 : out_(out)
501 , level_(level)
502 , textdomain_(textdomain)
503 , key_(key)
504 , strong_quotes_(strong_quotes)
505 {
506 }
507
508 // Generic visitor just streams "key=value".
509 template<typename T>
510 void operator()(const T& v) const
511 {
512 indent();
513 if constexpr(std::is_arithmetic_v<T>) {
514 // for number values, this has to use the same method as in from_string_verify
515 auto buf = utils::charconv_buffer(v);
516 out_ << key_ << '=' << buf.get_view() << '\n';
517 } else {
518 out_ << key_ << '=' << v << '\n';
519 }
520 }
521
522 //
523 // Specialized visitors for things that go in quotes:
524 //
525
526 void operator()(const utils::monostate&) const
527 {
528 // Treat blank values as nonexistent which fits better than treating them as empty strings.
529 }
530
531 void operator()(const std::string& s) const
532 {
533 indent();
534 if(strong_quotes_) {
535 out_ << key_ << '=' << "<<" << utils::wml_escape_strong(s) << ">>" << '\n';
536 } else {
537 out_ << key_ << '=' << '"' << utils::wml_escape_string(s) << '"' << '\n';
538 }
539 }
540
541 void operator()(const t_string& s) const;
542
543private:
544 void indent() const
545 {
546 for(unsigned i = 0; i < level_; ++i) {
547 out_ << '\t';
548 }
549 }
550
551 std::ostream& out_;
552 const unsigned level_;
553 std::string& textdomain_;
554 const std::string& key_;
555 bool strong_quotes_;
556};
557
558/**
559 * Writes all the parts of a translatable string.
560 *
561 * @note If the first part is translatable and in the wrong textdomain,
562 * the textdomain change has to happen before the attribute name.
563 * That is the reason for not outputting the key beforehand and
564 * letting this function do it.
565 */
566void write_key_val_visitor::operator()(const t_string& value) const
567{
568 bool first = true;
569
570 for(t_string::walker w(value); !w.eos(); w.next()) {
571 if(!first) {
572 out_ << " +\n";
573 }
574
575 if(w.translatable() && w.textdomain() != textdomain_) {
576 textdomain_ = w.textdomain();
577 out_ << "#textdomain " << textdomain_ << '\n';
578 }
579
580 indent();
581
582 if(first) {
583 out_ << key_ << '=';
584 } else {
585 out_ << '\t';
586 }
587
588 if(w.translatable()) {
589 out_ << '_';
590 }
591
592 if(strong_quotes_) {
593 out_ << "<<" << utils::wml_escape_strong(w) << ">>";
594 } else {
595 out_ << '"' << utils::wml_escape_string(w) << '"';
596 }
597 first = false;
598 }
599
600 out_ << '\n';
601}
602
603} // end anon namespace
604
605
606// ==================================================================================
607// PUBLIC FUNCTION IMPLEMENTATIONS
608// ==================================================================================
609
611{
612 return parser(in, validator)();
613}
614
616{
617 std::istringstream ss(in);
618 return parser(ss, validator)();
619}
620
621template<typename Decompressor>
623{
624 // An empty gzip file seems to confuse boost on MSVC, so return early if this is the case.
625 if(file.peek() == EOF) {
626 return {};
627 }
628
629 boost::iostreams::filtering_stream<boost::iostreams::input> filter;
630 filter.push(Decompressor());
631 filter.push(file);
632
633 /* This causes gzip_error (and the corresponding bz2 error, std::ios_base::failure) to be
634 * thrown here. save_index_class::data expects that and config_cache::read_cache and other
635 * functions are also capable of catching.
636 *
637 * Note that parser(cuff, filter,validator)(); -> tokenizer::tokenizer can throw exceptions
638 * too (meaning this function already threw these exceptions before this patch).
639 *
640 * We try to fix https://svn.boost.org/trac/boost/ticket/5237 by not creating empty gz files.
641 */
642 filter.exceptions(filter.exceptions() | std::ios_base::badbit);
643
644 /*
645 * It sometimes seems the file is not empty but still has no real data.
646 * Filter that case here. The previous test might be no longer required but keep it for now.
647 *
648 * On msvc filter.peek() != EOF does not imply filter.good().
649 * We never create empty compressed gzip files because boosts gzip fails at doing that, but
650 * empty compressed bz2 files are possible.
651 */
652 if(filter.peek() == EOF) {
653 LOG_CF << "Empty compressed file or error at reading a compressed file.";
654 return {};
655 }
656
657 if(!filter.good()) {
658 LOG_CF << " filter.peek() != EOF but !filter.good()."
659 << "This indicates a malformed gz stream and can make Wesnoth crash.";
660 }
661
662 return parser(filter, validator)();
663}
664
665/** Might throw a std::ios_base::failure especially a gzip_error. */
667{
668 return read_compressed<boost::iostreams::gzip_decompressor>(file, validator);
669}
670
671/** Might throw a std::ios_base::failure especially bzip2_error. */
673{
674 return read_compressed<boost::iostreams::bzip2_decompressor>(file, validator);
675}
676
677void write_key_val(std::ostream& out,
678 const std::string& key,
680 unsigned level,
681 std::string& textdomain,
682 bool strong_quotes)
683{
684 value.apply_visitor(write_key_val_visitor(out, level, textdomain, key, strong_quotes));
685}
686
687void write_open_child(std::ostream& out, const std::string& child, unsigned int level)
688{
689 out << std::string(level, '\t') << '[' << child << "]\n";
690}
691
692void write_close_child(std::ostream& out, const std::string& child, unsigned int level)
693{
694 out << std::string(level, '\t') << "[/" << child << "]\n";
695}
696
697static void write_internal(const config& cfg, std::ostream& out, std::string& textdomain, std::size_t tab = 0, bool strong_quotes = false)
698{
699 if(tab > max_recursion_levels) {
700 throw config::error("Too many recursion levels in config write");
701 }
702
703 for(const auto& [key, value] : cfg.attribute_range()) {
704 if(!config::valid_attribute(key)) {
705 ERR_CF << "Config contains invalid attribute name '" << key << "', skipping...";
706 continue;
707 }
708
709 write_key_val(out, key, value, tab, textdomain, strong_quotes);
710 }
711
712 for(const auto [key, item_cfg] : cfg.all_children_view()) {
713 if(!config::valid_tag(key)) {
714 ERR_CF << "Config contains invalid tag name '" << key << "', skipping...";
715 continue;
716 }
717
718 write_open_child(out, key, tab);
719 write_internal(item_cfg, out, textdomain, tab + 1, strong_quotes);
720 write_close_child(out, key, tab);
721 }
722}
723
724static void write_internal(const configr_of& cfg, std::ostream& out, std::string& textdomain, std::size_t tab = 0, bool strong_quotes = false)
725{
726 if(tab > max_recursion_levels) {
727 throw config::error("Too many recursion levels in config write");
728 }
729
730 if(cfg.data_) {
731 write_internal(*cfg.data_, out, textdomain, tab, strong_quotes);
732 }
733
734 for(const auto& pair : cfg.subtags_) {
735 assert(pair.first && pair.second);
736
737 if(!config::valid_tag(*pair.first)) {
738 ERR_CF << "Config contains invalid tag name '" << *pair.first << "', skipping...";
739 continue;
740 }
741
742 write_open_child(out, *pair.first, tab);
743 write_internal(*pair.second, out, textdomain, tab + 1, strong_quotes);
744 write_close_child(out, *pair.first, tab);
745 }
746}
747
748void write(std::ostream& out, const configr_of& cfg, unsigned int level, bool strong_quotes)
749{
750 std::string textdomain = PACKAGE;
751 write_internal(cfg, out, textdomain, level, strong_quotes);
752}
753
754template<typename Compressor>
755void write_compressed(std::ostream& out, const configr_of& cfg)
756{
757 boost::iostreams::filtering_stream<boost::iostreams::output> filter;
758 filter.push(Compressor());
759 filter.push(out);
760
761 write(filter, cfg);
762
763 // prevent empty gz files because of https://svn.boost.org/trac/boost/ticket/5237
764 filter << "\n";
765}
766
767void write_gz(std::ostream& out, const configr_of& cfg)
768{
769 write_compressed<boost::iostreams::gzip_compressor>(out, cfg);
770}
771
772void write_bz2(std::ostream& out, const configr_of& cfg)
773{
774 write_compressed<boost::iostreams::bzip2_compressor>(out, cfg);
775}
776
777} // namespace io
#define LOG_CF
unsigned in
If equal to search_counter, the node is off the list.
Used in parsing config file.
Definition validator.hpp:38
Variant for storing WML attributes.
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
static bool valid_tag(std::string_view name)
Definition config.cpp:129
config & add_child(std::string_view key)
Definition config.cpp:436
optional_config_impl< config > optional_child(std::string_view key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition config.cpp:380
const_attr_itors attribute_range() const
Definition config.cpp:740
auto all_children_view() const
In-order iteration over all children.
Definition config.hpp:795
static bool valid_attribute(std::string_view name)
Definition config.cpp:152
bool eos() const
Definition tstring.hpp:38
Helper class for translatable strings.
Definition tstring.hpp:27
bool translatable() const
Definition tstring.hpp:113
const std::string & value() const
Definition tstring.hpp:117
class responsible for parsing the provided text into tokens and tracking information about the curren...
Definition tokenizer.hpp:94
#define ERR_CF
Definition config.cpp:37
Definitions for the interface to Wesnoth Markup Language (WML).
const config * cfg
static std::string _(const char *str)
Definition gettext.hpp:100
static int indent
Definition log.cpp:88
Standard logging facilities (interface).
Definition parser.cpp:60
config read(std::istream &in, abstract_validator *validator)
Definition parser.cpp:610
void write_open_child(std::ostream &out, const std::string &child, unsigned int level)
Definition parser.cpp:687
void write(std::ostream &out, const configr_of &cfg, unsigned int level, bool strong_quotes)
Definition parser.cpp:748
config read_compressed(std::istream &file, abstract_validator *validator)
Definition parser.cpp:622
void write_close_child(std::ostream &out, const std::string &child, unsigned int level)
Definition parser.cpp:692
config read_bz2(std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially bzip2_error.
Definition parser.cpp:672
void write_bz2(std::ostream &out, const configr_of &cfg)
Definition parser.cpp:772
config read_gz(std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially a gzip_error.
Definition parser.cpp:666
void write_compressed(std::ostream &out, const configr_of &cfg)
Definition parser.cpp:755
void write_gz(std::ostream &out, const configr_of &cfg)
Definition parser.cpp:767
void write_key_val(std::ostream &out, const std::string &key, const config::attribute_value &value, unsigned level, std::string &textdomain, bool strong_quotes)
Definition parser.cpp:677
static void write_internal(const config &cfg, std::ostream &out, std::string &textdomain, std::size_t tab=0, bool strong_quotes=false)
Definition parser.cpp:697
std::string wml_escape_strong(const std::string &str)
Format str as a strongly quoted WML value.
std::map< std::string, t_string > string_map
std::string wml_escape_string(std::string_view str)
Format str as a WML value
int w
Definition pathfind.cpp:188
std::string lineno_string(const std::string &lineno)
constexpr std::size_t max_recursion_levels
Definition parser.cpp:57
static lg::log_domain log_config("config")
std::unique_ptr< MIX_Audio, decltype(&MIX_DestroyAudio)> value
Definition sound.cpp:139
@ QSTRING
quoted string, contained within double quotes or by less than/greater than symbols
Definition tokenizer.hpp:51
@ CLOSE_BRACKET
Definition tokenizer.hpp:64
@ UNTERMINATED_QSTRING
reached end of file without finding the closing character for a QSTRING
Definition tokenizer.hpp:53
@ NEWLINE
Definition tokenizer.hpp:58
@ UNDERSCORE
Definition tokenizer.hpp:65
@ OPEN_BRACKET
Definition tokenizer.hpp:63
@ END
set when EOF is returned by the input stream
Definition tokenizer.hpp:68
@ STRING
unquoted text
Definition tokenizer.hpp:49
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
#define i
#define c