ftml/parsing/element_condition.rs
1/*
2 * parsing/element_condition.rs
3 *
4 * ftml - Library to parse Wikidot text
5 * Copyright (C) 2019-2025 Wikijump Team
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21use std::borrow::Cow;
22use strum_macros::IntoStaticStr;
23
24/// Representation of a single condition to determine element presence.
25///
26/// A list of these constitutes a full condition specification, and is
27/// used in blocks like `[[iftags]]` and `[[ifcategory]]`.
28#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
29pub struct ElementCondition<'t> {
30 #[serde(rename = "condition")]
31 pub ctype: ElementConditionType,
32 pub value: Cow<'t, str>,
33}
34
35impl<'t> ElementCondition<'t> {
36 /// Parse out a specification.
37 ///
38 /// The specification is a space separated list of strings, prefixed with
39 /// either `+` or `-` or nothing.
40 pub fn parse(raw_spec: &'t str) -> Vec<ElementCondition<'t>> {
41 // Helper to get the value and its condition type
42 fn get_spec(value: &str) -> (ElementConditionType, &str) {
43 if let Some(value) = value.strip_prefix('+') {
44 return (ElementConditionType::Required, value);
45 }
46
47 if let Some(value) = value.strip_prefix('-') {
48 return (ElementConditionType::Prohibited, value);
49 }
50
51 (ElementConditionType::Present, value)
52 }
53
54 raw_spec
55 .split(' ')
56 .filter(|s| !s.is_empty())
57 .map(|s| {
58 let (ctype, value) = get_spec(s);
59
60 ElementCondition {
61 ctype,
62 value: cow!(value),
63 }
64 })
65 .collect()
66 }
67
68 /// Determines if this condition is satisfied.
69 ///
70 /// * `ElementConditionType::Required` -- All values of this kind must be present.
71 /// * `ElementConditionType::Prohibited` -- All values of this kind must be absent.
72 /// * `ElementConditionType::Present` -- Some values of this kind must be present.
73 ///
74 /// The full logic is essentially `all(required) && any(present) && all(prohibited)`.
75 pub fn check(conditions: &[ElementCondition], values: &[Cow<str>]) -> bool {
76 let mut required = true;
77 let mut prohibited = true;
78 let mut present = false;
79 let mut had_present = false; // whether there were any present conditions
80
81 for condition in conditions {
82 let has_value = values.contains(&condition.value);
83
84 match condition.ctype {
85 ElementConditionType::Required => required &= has_value,
86 ElementConditionType::Prohibited => prohibited &= !has_value,
87 ElementConditionType::Present => {
88 present |= has_value;
89 had_present = true;
90 }
91 }
92 }
93
94 // Since this is false by default, if there are no present conditions,
95 // it's effectively true.
96 //
97 // Otherwise you have to include a present condition for any iftags to pass!
98 //
99 // We could do "required && prohibited && (present || !had_present)" instead,
100 // but this if block is more readable.
101 if !had_present {
102 present = true;
103 }
104
105 required && prohibited && present
106 }
107}
108
109#[derive(
110 Serialize, Deserialize, IntoStaticStr, Debug, Copy, Clone, Hash, PartialEq, Eq,
111)]
112#[serde(rename_all = "kebab-case")]
113pub enum ElementConditionType {
114 Required,
115 Prohibited,
116 Present,
117}
118
119impl ElementConditionType {
120 #[inline]
121 pub fn name(self) -> &'static str {
122 self.into()
123 }
124}