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