style/servo/shadow_parts.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use crate::values::AtomIdent;
6use crate::Atom;
7
8type Mapping<'a> = (&'a str, &'a str);
9
10#[derive(Clone, Debug, MallocSizeOf)]
11pub struct ShadowParts {
12 // FIXME: Consider a smarter data structure for this.
13 // Gecko uses a hashtable in both directions:
14 // https://searchfox.org/mozilla-central/rev/5d4178378f84c7130ccb8ac1723d33e380d7f7d7/layout/style/ShadowParts.h
15 mappings: Vec<(Atom, Atom)>,
16}
17
18/// <https://drafts.csswg.org/css-shadow-parts/#parsing-mapping>
19///
20/// Returns `None` in the failure case.
21pub fn parse_part_mapping(input: &str) -> Option<Mapping<'_>> {
22 // Step 1. Let input be the string being parsed.
23 // Step 2. Let position be a pointer into input, initially pointing at the start of the string.
24 // NOTE: We don't need an explicit position, we just drop stuff from the input
25
26 // Step 3. Collect a sequence of code points that are space characters
27 let input = input.trim_start_matches(|c| c == ' ');
28
29 // Step 4. Collect a sequence of code points that are not space characters or U+003A COLON characters,
30 // and let first token be the result.
31 let space_or_colon_position = input
32 .char_indices()
33 .find(|(_, c)| matches!(c, ' ' | ':'))
34 .map(|(index, _)| index)
35 .unwrap_or(input.len());
36 let (first_token, input) = input.split_at(space_or_colon_position);
37
38 // Step 5. If first token is empty then return error.
39 if first_token.is_empty() {
40 return None;
41 }
42
43 // Step 6. Collect a sequence of code points that are space characters.
44 let input = input.trim_start_matches(|c| c == ' ');
45
46 // Step 7. If the end of the input has been reached, return the tuple (first token, first token)
47 if input.is_empty() {
48 return Some((first_token, first_token));
49 }
50
51 // Step 8. If character at position is not a U+003A COLON character, return error.
52 // Step 9. Consume the U+003A COLON character.
53 let Some(input) = input.strip_prefix(':') else {
54 return None;
55 };
56
57 // Step 10. Collect a sequence of code points that are space characters.
58 let input = input.trim_start_matches(|c| c == ' ');
59
60 // Step 11. Collect a sequence of code points that are not space characters or U+003A COLON characters.
61 // and let second token be the result.
62 let space_or_colon_position = input
63 .char_indices()
64 .find(|(_, c)| matches!(c, ' ' | ':'))
65 .map(|(index, _)| index)
66 .unwrap_or(input.len());
67 let (second_token, input) = input.split_at(space_or_colon_position);
68
69 // Step 12. If second token is empty then return error.
70 if second_token.is_empty() {
71 return None;
72 }
73
74 // Step 13. Collect a sequence of code points that are space characters.
75 let input = input.trim_start_matches(|c| c == ' ');
76
77 // Step 14. If position is not past the end of input then return error.
78 if !input.is_empty() {
79 return None;
80 }
81
82 // Step 14. Return the tuple (first token, second token).
83 Some((first_token, second_token))
84}
85
86/// <https://drafts.csswg.org/css-shadow-parts/#parsing-mapping-list>
87fn parse_mapping_list(input: &str) -> impl Iterator<Item = Mapping> {
88 // Step 1. Let input be the string being parsed.
89 // Step 2. Split the string input on commas. Let unparsed mappings be the resulting list of strings.
90 let unparsed_mappings = input.split(',');
91
92 // Step 3. Let mappings be an initially empty list of tuples of tokens.
93 // This list will be the result of this algorithm.
94 // NOTE: We return an iterator here - it is up to the caller to turn it into a list
95
96 // Step 4. For each string unparsed mapping in unparsed mappings, run the following substeps:
97 unparsed_mappings.filter_map(|unparsed_mapping| {
98 // Step 4.1 If unparsed mapping is empty or contains only space characters,
99 // continue to the next iteration of the loop.
100 if unparsed_mapping.chars().all(|c| c == ' ') {
101 return None;
102 }
103
104 // Step 4.2 Let mapping be the result of parsing unparsed mapping using the rules for parsing part mappings.
105 // Step 4.3 If mapping is an error then continue to the next iteration of the loop.
106 // This allows clients to skip over new syntax that is not understood.
107 // Step 4.4 Append mapping to mappings.
108 parse_part_mapping(unparsed_mapping)
109 })
110}
111
112impl ShadowParts {
113 pub fn parse(input: &str) -> Self {
114 Self {
115 mappings: parse_mapping_list(input)
116 .map(|(first, second)| (first.into(), second.into()))
117 .collect(),
118 }
119 }
120
121 /// Call the provided callback for each exported part with the given name.
122 pub fn for_each_exported_part<F>(&self, name: &Atom, mut callback: F)
123 where
124 F: FnMut(&AtomIdent),
125 {
126 for (from, to) in &self.mappings {
127 if from == name {
128 callback(AtomIdent::cast(to));
129 }
130 }
131 }
132
133 pub fn imported_part(&self, name: &Atom) -> Option<&Atom> {
134 self.mappings
135 .iter()
136 .find(|(_, to)| to == name)
137 .map(|(from, _)| from)
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn parse_valid_mapping() {
147 assert_eq!(
148 parse_part_mapping("foo"),
149 Some(("foo", "foo")),
150 "Single token"
151 );
152 assert_eq!(
153 parse_part_mapping(" foo"),
154 Some(("foo", "foo")),
155 "Single token with leading whitespace"
156 );
157 assert_eq!(
158 parse_part_mapping("foo "),
159 Some(("foo", "foo")),
160 "Single token with trailing whitespace"
161 );
162 assert_eq!(
163 parse_part_mapping("foo:bar"),
164 Some(("foo", "bar")),
165 "Two tokens"
166 );
167 assert_eq!(
168 parse_part_mapping("foo:bar "),
169 Some(("foo", "bar")),
170 "Two tokens with trailing whitespace"
171 );
172 assert_eq!(
173 parse_part_mapping("🦀:🚀"),
174 Some(("🦀", "🚀")),
175 "Two tokens consisting of non-ascii characters"
176 );
177 }
178
179 #[test]
180 fn reject_invalid_mapping() {
181 assert!(parse_part_mapping("").is_none(), "Empty input");
182 assert!(parse_part_mapping(" ").is_none(), "Only whitespace");
183 assert!(parse_part_mapping("foo bar").is_none(), "Missing colon");
184 assert!(parse_part_mapping(":bar").is_none(), "Empty first token");
185 assert!(parse_part_mapping("foo:").is_none(), "Empty second token");
186 assert!(
187 parse_part_mapping("foo:bar baz").is_none(),
188 "Trailing input"
189 );
190 }
191
192 #[test]
193 fn parse_valid_mapping_list() {
194 let mut mappings = parse_mapping_list("foo: bar, totally-invalid-mapping,,");
195
196 // "foo: bar" is a valid mapping
197 assert_eq!(
198 mappings.next(),
199 Some(("foo", "bar")),
200 "First mapping should be in the list"
201 );
202 // "totally-invalid-mapping" is not a valid mapping and should be ignored
203 // "" is not valid (and consists of nothing but whitespace), so it should be ignored
204 assert!(mappings.next().is_none(), "No more mappings should exist");
205 }
206}