Skip to main content

ferrocat_icu/
utils.rs

1use std::collections::BTreeSet;
2
3use crate::ast::{IcuMessage, IcuNode, IcuOption, IcuPluralKind};
4
5/// Validates ICU `MessageFormat` input without returning the parsed AST.
6///
7/// # Errors
8///
9/// Returns [`crate::IcuParseError`] when the input is malformed.
10pub fn validate_icu(input: &str) -> Result<(), crate::IcuParseError> {
11    crate::parse_icu(input).map(|_| ())
12}
13
14/// Extracts variable names in first-seen order.
15#[must_use]
16pub fn extract_variables(message: &IcuMessage) -> Vec<String> {
17    let mut out = Vec::new();
18    let mut seen = BTreeSet::new();
19    visit_nodes(&message.nodes, &mut |name| {
20        if seen.insert(name.to_owned()) {
21            out.push(name.to_owned());
22        }
23    });
24    out
25}
26
27/// Returns `true` when the message contains a cardinal plural expression.
28#[must_use]
29pub fn has_plural(message: &IcuMessage) -> bool {
30    any_nodes(&message.nodes, &|node| {
31        matches!(
32            node,
33            IcuNode::Plural {
34                kind: IcuPluralKind::Cardinal,
35                ..
36            }
37        )
38    })
39}
40
41/// Returns `true` when the message contains a select expression.
42#[must_use]
43pub fn has_select(message: &IcuMessage) -> bool {
44    any_nodes(&message.nodes, &|node| {
45        matches!(node, IcuNode::Select { .. })
46    })
47}
48
49/// Returns `true` when the message contains an ordinal plural expression.
50#[must_use]
51pub fn has_selectordinal(message: &IcuMessage) -> bool {
52    any_nodes(&message.nodes, &|node| {
53        matches!(
54            node,
55            IcuNode::Plural {
56                kind: IcuPluralKind::Ordinal,
57                ..
58            }
59        )
60    })
61}
62
63/// Returns `true` when the message contains rich-text style tags.
64#[must_use]
65pub fn has_tag(message: &IcuMessage) -> bool {
66    any_nodes(&message.nodes, &|node| matches!(node, IcuNode::Tag { .. }))
67}
68
69fn visit_nodes(nodes: &[IcuNode], visitor: &mut impl FnMut(&str)) {
70    for node in nodes {
71        match node {
72            IcuNode::Literal(_) | IcuNode::Pound => {}
73            IcuNode::Argument { name }
74            | IcuNode::Number { name, .. }
75            | IcuNode::Date { name, .. }
76            | IcuNode::Time { name, .. }
77            | IcuNode::List { name, .. }
78            | IcuNode::Duration { name, .. }
79            | IcuNode::Ago { name, .. }
80            | IcuNode::Name { name, .. } => visitor(name),
81            IcuNode::Select { name, options } | IcuNode::Plural { name, options, .. } => {
82                visitor(name);
83                visit_options(options, visitor);
84            }
85            IcuNode::Tag { name, children } => {
86                visitor(name);
87                visit_nodes(children, visitor);
88            }
89        }
90    }
91}
92
93fn visit_options(options: &[IcuOption], visitor: &mut impl FnMut(&str)) {
94    for option in options {
95        visit_nodes(&option.value, visitor);
96    }
97}
98
99fn any_nodes(nodes: &[IcuNode], predicate: &impl Fn(&IcuNode) -> bool) -> bool {
100    nodes.iter().any(|node| match node {
101        IcuNode::Select { options, .. } | IcuNode::Plural { options, .. } => {
102            predicate(node)
103                || options
104                    .iter()
105                    .any(|option| any_nodes(&option.value, predicate))
106        }
107        IcuNode::Tag { children, .. } => predicate(node) || any_nodes(children, predicate),
108        _ => predicate(node),
109    })
110}
111
112#[cfg(test)]
113mod tests {
114    use crate::{extract_variables, has_plural, has_select, has_selectordinal, has_tag, parse_icu};
115
116    #[test]
117    fn extracts_variables_in_first_seen_order() {
118        let message = parse_icu(
119            "{name} has {count, plural, one {{when, time, short}} other {{when, date, medium} in <link>{name}</link>}}",
120        )
121        .expect("parse");
122
123        assert_eq!(
124            extract_variables(&message),
125            vec!["name", "count", "when", "link"]
126        );
127    }
128
129    #[test]
130    fn reports_structure_helpers() {
131        let message = parse_icu(
132            "{gender, select, male {{count, plural, one {<b>#</b>} other {# items}}} other {{n, selectordinal, one {#st} other {#th}}}}",
133        )
134        .expect("parse");
135
136        assert!(has_select(&message));
137        assert!(has_plural(&message));
138        assert!(has_selectordinal(&message));
139        assert!(has_tag(&message));
140    }
141
142    #[test]
143    fn reports_absence_of_optional_structures() {
144        let message = parse_icu("Hello {name}").expect("parse");
145        assert_eq!(extract_variables(&message), vec!["name"]);
146        assert!(!has_plural(&message));
147        assert!(!has_select(&message));
148        assert!(!has_selectordinal(&message));
149        assert!(!has_tag(&message));
150    }
151}