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