The Battle for Wesnoth  1.19.5+dev
tag.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2011 - 2024
3  by Sytyi Nick <nsytyi@gmail.com>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 /**
17  * @file
18  * Implementation of tag.hpp.
19  */
20 
23 #include "formatter.hpp"
24 
25 namespace schema_validation
26 {
27 
28 wml_tag any_tag("", 0, -1, "", true);
29 
30 wml_tag::wml_tag(const std::string& name, int min, int max, const std::string& super, bool any)
31  : name_(name)
32  , min_(min)
33  , max_(max)
34  , super_(super)
35  , fuzzy_(name.find_first_of("*?") != std::string::npos)
36  , any_tag_(any)
37 {
38 }
39 
41  : name_(cfg["name"].str())
42  , min_(cfg["min"].to_int())
43  , max_(cfg["max"].str() == "infinite" ? -1 : cfg["max"].to_int(1))
44  , min_children_(cfg["min_tags"].to_int())
45  , max_children_(cfg["max_tags"].str() == "infinite" ? -1 : cfg["max_tags"].to_int(-1))
46  , super_("")
47  , tags_()
48  , keys_()
49  , links_()
50  , conditions_()
51  , super_refs_()
52  , fuzzy_(name_.find_first_of("*?+") != std::string::npos)
53  , any_tag_(cfg["any_tag"].to_bool())
54 {
55  if(max_ < 0) {
56  max_ = std::numeric_limits<int>::max();
57  }
58  if(max_children_ < 0) {
59  max_children_ = std::numeric_limits<int>::max();
60  }
61 
62  if(cfg.has_attribute("super")) {
63  super_ = cfg["super"].str();
64  }
65 
66  for(const config& child : cfg.child_range("tag")) {
67  wml_tag child_tag(child);
68  add_tag(child_tag);
69  }
70 
71  for(const config& child : cfg.child_range("key")) {
72  wml_key child_key(child);
73  add_key(child_key);
74  }
75 
76  for(const config& link : cfg.child_range("link")) {
77  std::string link_name = link["name"].str();
78  add_link(link_name);
79  }
80 
81  for(const config& sw : cfg.child_range("switch")) {
82  add_switch(sw);
83  }
84 
85  for(const config& filter : cfg.child_range("if")) {
86  add_filter(filter);
87  }
88 }
89 
90 void wml_tag::print(std::ostream& os)
91 {
92  printl(os, 4, 4);
93 }
94 
95 void wml_tag::set_min(const std::string& s)
96 {
97  std::istringstream i(s);
98  if(!(i >> min_)) {
99  min_ = 0;
100  }
101 }
102 
103 void wml_tag::set_max(const std::string& s)
104 {
105  std::istringstream i(s);
106  if(!(i >> max_)) {
107  max_ = 0;
108  }
109 }
110 
111 void wml_tag::add_link(const std::string& link)
112 {
113  std::string::size_type pos_last = link.rfind('/');
114  // if (pos_last == std::string::npos) return;
115  std::string name_link = link.substr(pos_last + 1, link.length());
116  links_.emplace(name_link, link);
117 }
118 
119 const wml_key* wml_tag::find_key(const std::string& name, const config& match, bool ignore_super) const
120 {
121  auto visited = std::vector<const wml_tag*>();
122  return find_key(name, match, ignore_super, visited);
123 }
124 
125 const wml_key* wml_tag::find_key(const std::string& name, const config& match, bool ignore_super, std::vector<const wml_tag*>& visited) const
126 {
127  // Returns nullptr if a super cycle is detected.
128  if(std::find(visited.begin(), visited.end(), this) != visited.end()) {
129  return nullptr;
130  }
131 
132  visited.push_back(this);
133 
134  // Check the conditions first, so that conditional definitions
135  // override base definitions in the event of duplicates.
136  for(auto& cond : conditions_) {
137  if(cond.matches(match)) {
138  // Not considered for super cycle detection as super tags are ignored.
139  if(auto key = cond.find_key(name, match, true)) {
140  return key;
141  }
142  }
143  }
144 
145  const auto it_keys = keys_.find(name);
146  if(it_keys != keys_.end()) {
147  return &(it_keys->second);
148  }
149 
150  key_map::const_iterator it_fuzzy = std::find_if(keys_.begin(), keys_.end(), [&name](const key_map::value_type& key){
151  if(!key.second.is_fuzzy()) {
152  return false;
153  }
154  return utils::wildcard_string_match(name, key.second.get_name());
155  });
156  if(it_fuzzy != keys_.end()) {
157  return &(it_fuzzy->second);
158  }
159 
160  if(!ignore_super) {
161  for(auto& cond : conditions_) {
162  if(cond.matches(match)) {
163  // This results in a little redundancy (checking things twice) but at least it still works.
164  if(auto key = cond.find_key(name, match, false, visited)) {
165  return key;
166  }
167  }
168  }
169  for(auto& [_, super_tag] : super_refs_) {
170  if(const wml_key* found_key = super_tag->find_key(name, match, false, visited)) {
171  return found_key;
172  }
173  }
174  }
175 
176  return nullptr;
177 }
178 
179 const std::string* wml_tag::find_link(const std::string& name) const
180 {
181  const auto it_links = links_.find(name);
182  if(it_links != links_.end()) {
183  return &(it_links->second);
184  }
185 
186  return nullptr;
187 }
188 
189 const wml_tag* wml_tag::find_tag(const std::string& fullpath, const wml_tag& root, const config& match, bool ignore_super) const
190 {
191  auto visited = std::vector<const wml_tag*>();
192  return find_tag(fullpath, root, match, ignore_super, visited);
193 }
194 
195 const wml_tag* wml_tag::find_tag(const std::string& fullpath, const wml_tag& root, const config& match, bool ignore_super, std::vector<const wml_tag*>& visited) const
196 {
197  // Returns nullptr if a super cycle is detected.
198  if(std::find(visited.begin(), visited.end(), this) != visited.end()) {
199  return nullptr;
200  }
201 
202  visited.push_back(this);
203 
204  if(fullpath.empty()) {
205  return nullptr;
206  }
207 
208  std::string::size_type pos = fullpath.find('/');
209  std::string name;
210  std::string next_path;
211 
212  if(pos != std::string::npos) {
213  name = fullpath.substr(0, pos);
214  next_path = fullpath.substr(pos + 1, fullpath.length());
215  } else {
216  name = fullpath;
217  }
218 
219  // Check the conditions first, so that conditional definitions
220  // override base definitions in the event of duplicates.
221  for(auto& cond : conditions_) {
222  if(cond.matches(match)) {
223  // Not considered for super cycle detection as super tags are ignored.
224  if(auto tag = cond.find_tag(fullpath, root, match, true)) {
225  return tag;
226  }
227  }
228  }
229 
230  const auto it_tags = tags_.find(name);
231  if(it_tags != tags_.end()) {
232  if(next_path.empty()) {
233  return &(it_tags->second);
234  } else {
235  return it_tags->second.find_tag(next_path, root, match, false, visited);
236  }
237  }
238 
239  const auto it_links = links_.find(name);
240  if(it_links != links_.end()) {
241  // Reset cycle detection on links as we restart from the root.
242  return root.find_tag(it_links->second + "/" + next_path, root, match, false);
243  }
244 
245  const auto it_fuzzy = std::find_if(tags_.begin(), tags_.end(), [&name](const tag_map::value_type& tag) {
246  if(!tag.second.fuzzy_) {
247  return false;
248  }
249  return utils::wildcard_string_match(name, tag.second.name_);
250  });
251  if(it_fuzzy != tags_.end()) {
252  if(next_path.empty()) {
253  return &(it_fuzzy->second);
254  } else {
255  return it_tags->second.find_tag(next_path, root, match, false, visited);
256  }
257  }
258 
259  if(!ignore_super) {
260  for(auto& cond : conditions_) {
261  if(cond.matches(match)) {
262  // This results in a little redundancy (checking things twice) but at least it still works.
263  if(auto tag = cond.find_tag(fullpath, root, match, false, visited)) {
264  return tag;
265  }
266  }
267  }
268  for(auto& [_, super_tag] : super_refs_) {
269  if(const wml_tag* found_tag = super_tag->find_tag(fullpath, root, match, false, visited)) {
270  return found_tag;
271  }
272  }
273  }
274 
275  if(any_tag_) {
276  return &any_tag;
277  }
278 
279  return nullptr;
280  }
281 
282 void wml_tag::expand_all(wml_tag& root)
283 {
284  for(auto& tag : tags_) {
285  tag.second.expand(root);
286  tag.second.expand_all(root);
287  }
288  for(auto& cond : conditions_) {
289  cond.expand(root);
290  cond.expand_all(root);
291  }
292 }
293 
294 void wml_tag::remove_keys_by_type(const std::string& type)
295 {
296  auto i = keys_.begin();
297  while(i != keys_.end()) {
298  if(i->second.get_type() == type) {
299  keys_.erase(i++);
300  } else {
301  ++i;
302  }
303  }
304 
305  for(auto& tag : tags_) {
306  tag.second.remove_keys_by_type(type);
307  }
308 }
309 
310 void wml_tag::printl(std::ostream& os, int level, int step)
311 {
312  std::string s;
313  for(int j = 0; j < level; j++) {
314  s.append(" ");
315  }
316 
317  os << s << "[tag]\n"
318  << s << " name=\"" << name_ << "\"\n"
319  << s << " min=\"" << min_ << "\"\n"
320  << s << " max=\"" << max_ << "\"\n";
321 
322  if(!super_.empty()) {
323  os << s << " super=\"" << super_ << "\"\n";
324  }
325 
326  for(auto& tag : tags_) {
327  tag.second.printl(os, level + step, step);
328  }
329 
330  for(auto& link : links_) {
331  os << s << ""
332  << "[link]\n"
333  << s << ""
334  << " name=\"" << link.second << "\"\n"
335  << s << ""
336  << "[/link]\n";
337  }
338 
339  for(auto& key : keys_) {
340  key.second.print(os, level + step);
341  }
342 
343  // TODO: Other attributes
344 
345  os << s << "[/tag]\n";
346 }
347 
348 void wml_tag::add_tag(const std::string& path, const wml_tag& tag, wml_tag& root)
349 {
350  if(path.empty() || path == "/") {
351  auto it = tags_.find(tag.name_);
352 
353  if(it == tags_.end()) {
354  tags_.emplace(tag.name_, tag);
355  } else {
356  it->second.set_min(tag.min_);
357  it->second.set_max(tag.max_);
358  it->second.add_tags(tag.tags_);
359  it->second.add_keys(tag.keys_);
360  it->second.add_links(tag.links_);
361  // TODO: Other attributes
362  }
363 
364  links_.erase(tag.get_name());
365  return;
366  }
367 
368  std::string::size_type pos = path.find('/');
369  std::string name = path.substr(0, pos);
370  std::string next_path = path.substr(pos + 1, path.length());
371 
372  auto it_links = links_.find(name);
373  if(it_links != links_.end()) {
374  root.add_tag(it_links->second + "/" + next_path, tag, root);
375  }
376 
377  auto it_tags = tags_.find(name);
378  if(it_tags == tags_.end()) {
379  wml_tag subtag;
380  subtag.set_name(name);
381  subtag.add_tag(next_path, tag, root);
382  tags_.emplace(name, subtag);
383  return;
384  }
385 
386  it_tags->second.add_tag(next_path, tag, root);
387 }
388 
389 void wml_tag::add_conditions(const condition_list& list)
390 {
391  conditions_.insert(conditions_.end(), list.begin(), list.end());
392 }
393 
394 void wml_tag::expand(wml_tag& root)
395 {
396  for(auto& super : utils::split(super_)) {
397  wml_tag* super_tag = root.find_tag(super, root, config());
398  if(super_tag) {
399  if(super_tag != this) {
400  super_refs_.emplace(super, super_tag);
401  }
402  }
403  }
404 }
405 
406 void wml_tag::add_switch(const config& switch_cfg)
407 {
408  config default_cfg;
409  const std::string key = switch_cfg["key"];
410  bool allow_missing = false;
411  for(const auto& case_cfg : switch_cfg.child_range("case")) {
412  if(case_cfg.has_attribute("value")) {
413  const std::vector<std::string> values = utils::split(case_cfg["value"].str(), ',', utils::STRIP_SPACES);
414  config filter;
415  for(const auto& value : values) {
416  // An [or] filter only works if there's something in the main filter.
417  // So, the first case cannot be wrapped in [or].
418  if(filter.empty()) {
419  filter[key] = value;
420  } else {
421  filter.add_child("or")[key] = value;
422  }
423  default_cfg.add_child("not")[key] = value;
424  }
425  if(!allow_missing && case_cfg["trigger_if_missing"].to_bool()) {
426  config& missing_filter = filter.add_child("or").add_child("not");
427  missing_filter["glob_on_" + key] = "*";
428  allow_missing = true;
429  }
430  conditions_.emplace_back(case_cfg, filter);
431  } else {
432  // Match if the attribute is missing
433  conditions_.emplace_back(case_cfg, config());
434  }
435  const std::string name = formatter() << get_name() << '[' << key << '=' << case_cfg["value"] << ']';
436  conditions_.back().set_name(name);
437  }
438  if(switch_cfg.has_child("else")) {
439  if(allow_missing) {
440  // If a [case] matches the absence of the key, then [else] should not
441  // The previous [not] keys already failed if it had a value matched by another [case]
442  // So just add an [and] tag that matches any other value
443  default_cfg.add_child("and")["glob_on_" + key] = "*";
444  }
445  conditions_.emplace_back(switch_cfg.mandatory_child("else"), default_cfg);
446  const std::string name = formatter() << get_name() << "[else]";
447  conditions_.back().set_name(name);
448  }
449 }
450 
451 void wml_tag::add_filter(const config& cond_cfg)
452 {
453  config filter = cond_cfg, else_filter;
454  filter.clear_children("then", "else", "elseif");
455  // Note in case someone gets trigger-happy:
456  // DO NOT MOVE THIS! It needs to be copied!
457  else_filter.add_child("not", filter);
458  if(cond_cfg.has_child("then")) {
459  conditions_.emplace_back(cond_cfg.mandatory_child("then"), filter);
460  const std::string name = formatter() << get_name() << "[then]";
461  conditions_.back().set_name(name);
462  }
463  int i = 1;
464  for(auto elseif_cfg : cond_cfg.child_range("elseif")) {
465  config elseif_filter = elseif_cfg, old_else_filter = else_filter;
466  elseif_filter.clear_children("then");
467  else_filter.add_child("not", elseif_filter);
468  // Ensure it won't match for any of the preceding cases, either
469  elseif_filter.append_children(old_else_filter);
470  conditions_.emplace_back(elseif_cfg.child_or_empty("then"), elseif_filter);
471  const std::string name = formatter() << get_name() << "[elseif " << i++ << "]";
472  conditions_.back().set_name(name);
473  }
474  if(cond_cfg.has_child("else")) {
475  conditions_.emplace_back(cond_cfg.mandatory_child("else"), else_filter);
476  const std::string name = formatter() << get_name() << "[else]";
477  conditions_.back().set_name(name);
478  }
479 }
480 
481 bool wml_condition::matches(const config& cfg) const
482 {
483  if(cfg.empty()) {
484  // Conditions are not allowed to match an empty config.
485  // If they were, the conditions might be considered when expanding super-tags.
486  // That would result in a condition tag being used for the expansion, rather than
487  // the base tag, which would be bad.
488  return false;
489  }
490  return cfg.matches(filter_);
491 }
492 
493 template<>
495 {
496  current = base_tag.tags_.begin();
497  condition_queue.push(&base_tag);
498 }
499 
500 template<>
501 void wml_tag::tag_iterator::ensure_valid_or_end() {
502  while(current == condition_queue.front()->tags_.end()) {
503  condition_queue.pop();
504  if(condition_queue.empty()) {
505  return;
506  }
507  const wml_tag& new_base = *condition_queue.front();
508  current= new_base.tags_.begin();
509  push_new_tag_conditions(new_base);
510  }
511 }
512 
513 template<>
514 void wml_tag::key_iterator::init(const wml_tag& base_tag)
515 {
516  current = base_tag.keys_.begin();
517  condition_queue.push(&base_tag);
518 }
519 
520 template<>
521 void wml_tag::key_iterator::ensure_valid_or_end() {
522  while(current == condition_queue.front()->keys_.end()) {
523  condition_queue.pop();
524  if(condition_queue.empty()) {
525  return;
526  }
527  const wml_tag& new_base = *condition_queue.front();
528  current = new_base.keys_.begin();
529  push_new_tag_conditions(new_base);
530  }
531 }
532 
533 template<>
534 void wml_tag::super_iterator::init(const wml_tag& base_tag)
535 {
536  current = base_tag.super_refs_.begin();
537  condition_queue.push(&base_tag);
538 }
539 
540 template<>
541 void wml_tag::super_iterator::ensure_valid_or_end()
542 {
543  while(current == condition_queue.front()->super_refs_.end()) {
544  condition_queue.pop();
545  if(condition_queue.empty()) {
546  return;
547  }
548  const wml_tag& new_base = *condition_queue.front();
549  current = new_base.super_refs_.begin();
550  push_new_tag_conditions(new_base);
551  }
552 }
553 
554 void wml_tag::push_new_tag_conditions(std::queue<const wml_tag*>& q, const config& match, const wml_tag& tag)
555 {
556  for(const auto& condition : tag.conditions_) {
557  if(condition.matches(match)) {
558  q.push(&condition);
559  }
560  }
561 }
562 
563 } // namespace schema_validation
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:172
config & mandatory_child(config_key_type key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition: config.cpp:366
bool matches(const config &filter) const
Definition: config.cpp:1194
void clear_children(T... keys)
Definition: config.hpp:616
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:316
bool has_attribute(config_key_type key) const
Definition: config.cpp:157
child_itors child_range(config_key_type key)
Definition: config.cpp:272
void append_children(const config &cfg)
Adds children from cfg.
Definition: config.cpp:167
bool empty() const
Definition: config.cpp:849
config & add_child(config_key_type key)
Definition: config.cpp:440
std::ostringstream wrapper.
Definition: formatter.hpp:40
wml_key is used to save the information about one key.
Definition: key.hpp:37
Stores information about tag.
Definition: tag.hpp:48
void add_link(const std::string &link)
Definition: tag.cpp:111
std::string super_
name of tag to extend "super-tag" Extension is smth like inheritance and is used in case when you nee...
Definition: tag.hpp:326
const wml_tag * find_tag(const std::string &fullpath, const wml_tag &root, const config &match, bool ignore_super=false) const
Returns pointer to tag using full path to it.
Definition: tag.cpp:189
void set_max(int o)
Definition: tag.hpp:185
void set_name(const std::string &name)
Definition: tag.hpp:175
void add_filter(const config &cond_cfg)
Definition: tag.cpp:451
void add_tag(const wml_tag &new_tag)
Definition: tag.hpp:224
void set_min(int o)
Definition: tag.hpp:180
void print(std::ostream &os)
Prints information about tag to outputstream, recursively is used to print tag info the format is nex...
Definition: tag.cpp:90
link_map links_
links to possible children.
Definition: tag.hpp:335
int min_
minimum number of occurrences.
Definition: tag.hpp:308
void add_switch(const config &switch_cfg)
Definition: tag.cpp:406
condition_list conditions_
conditional partial matches
Definition: tag.hpp:338
std::vector< wml_condition > condition_list
Definition: tag.hpp:53
const wml_key * find_key(const std::string &name, const config &match, bool ignore_super=false) const
Returns pointer to child key.
Definition: tag.cpp:119
tag_map tags_
children tags
Definition: tag.hpp:329
void add_key(const wml_key &new_key)
Definition: tag.hpp:219
int max_
maximum number of occurrences.
Definition: tag.hpp:311
void printl(std::ostream &os, int level, int step=4)
the same as wml_tag::print(std::ostream&) but indents different levels with step space.
Definition: tag.cpp:310
int max_children_
maximum number of children.
Definition: tag.hpp:317
std::size_t i
Definition: function.cpp:1028
static std::string _(const char *str)
Definition: gettext.hpp:93
std::string path
Definition: filesystem.cpp:90
std::string tag(const std::string &tag_name, Args &&... contents)
Definition: markup.hpp:45
wml_tag any_tag("", 0, -1, "", true)
struct utils::detail::formula_initer init
@ STRIP_SPACES
REMOVE_EMPTY: remove empty elements.
bool wildcard_string_match(const std::string &str, const std::string &match)
Match using '*' as any number of characters (including none), '+' as one or more characters,...
std::vector< std::string > split(const config_attribute_value &val)
This file contains object "tag", which is used to store information about tags while annotation parsi...
static map_location::direction sw
static map_location::direction s