Skip to main content

ftml/parsing/
element_condition.rs

1/*
2 * parsing/element_condition.rs
3 *
4 * ftml - Library to parse Wikidot text
5 * Copyright (C) 2019-2026 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}