1use serde::{Deserialize, Serialize};
17
18use crate::style::element::ELEMENTS;
19use crate::style::properties::{CounterStyle, MarginBox};
20use crate::style::sheet::{
21 COMBINATORS, COMPOUNDS, CUSTOM_PROPERTY, DECLARATION, FIRST_LINE_PROPERTIES,
22 FONT_FACE_DESCRIPTORS, MARGIN_BOX_PROPERTIES, NAMED, PAGE_PROPERTIES, PAGE_SELECTORS,
23 PAGE_SIZES, PROPERTIES, PSEUDO_CLASSES, PSEUDO_ELEMENTS, SELECTOR_LIST, Spec, UNITS, VAR,
24};
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct Subset {
29 pub version: String,
31 pub selectors: Selectors,
33 pub declaration: String,
35 pub custom_property: String,
38 pub var: String,
41 pub properties: Vec<Property>,
43 pub page: Page,
45 pub font_face: FontFace,
47 pub units: Vec<String>,
49 pub color_names: Vec<String>,
51}
52
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct Selectors {
56 pub elements: Vec<String>,
58 pub compounds: Vec<Selector>,
60 pub combinators: Vec<Selector>,
62 pub list: Selector,
64 pub pseudo_classes: Vec<Selector>,
66 pub pseudo_elements: Vec<Selector>,
68 pub first_line_properties: Vec<String>,
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct Selector {
76 pub name: String,
78 pub example: String,
80}
81
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84pub struct Property {
85 pub name: String,
87 pub inherited: bool,
89 pub syntax: String,
91 pub keywords: Vec<String>,
95 pub examples: Vec<String>,
97}
98
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
101pub struct Descriptor {
102 pub name: String,
104 pub syntax: String,
106 pub keywords: Vec<String>,
108 pub examples: Vec<String>,
110}
111
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
114pub struct Page {
115 pub prelude: String,
118 pub selectors: Vec<String>,
120 pub properties: Vec<Property>,
122 pub margin_boxes: Vec<MarginBoxDescription>,
124 pub margin_box_properties: Vec<Property>,
127 pub sizes: Vec<PageSize>,
129 pub counter_styles: Vec<String>,
131}
132
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
135pub struct MarginBoxDescription {
136 pub name: String,
138 pub paints: bool,
141}
142
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub struct PageSize {
146 pub name: String,
148 pub width: f32,
150 pub height: f32,
152}
153
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156pub struct FontFace {
157 pub descriptors: Vec<Descriptor>,
159}
160
161impl Subset {
162 pub fn describe() -> Subset {
164 Subset {
165 version: env!("CARGO_PKG_VERSION").to_string(),
166 selectors: Selectors {
167 elements: strings(&ELEMENTS),
168 compounds: selectors(COMPOUNDS),
169 combinators: selectors(COMBINATORS),
170 list: Selector {
171 name: SELECTOR_LIST.0.to_string(),
172 example: SELECTOR_LIST.1.to_string(),
173 },
174 pseudo_classes: selectors(PSEUDO_CLASSES),
175 pseudo_elements: selectors(PSEUDO_ELEMENTS),
176 first_line_properties: strings(FIRST_LINE_PROPERTIES),
177 },
178 declaration: DECLARATION.to_string(),
179 custom_property: CUSTOM_PROPERTY.to_string(),
180 var: VAR.to_string(),
181 properties: properties(PROPERTIES),
182 page: Page {
183 prelude: format!(
184 "<name>? [ {} ]*",
185 PAGE_SELECTORS
186 .iter()
187 .map(|(name, _)| format!(":{name}"))
188 .collect::<Vec<_>>()
189 .join(" | ")
190 ),
191 selectors: PAGE_SELECTORS
192 .iter()
193 .map(|(name, _)| name.to_string())
194 .collect(),
195 properties: properties(PAGE_PROPERTIES),
196 margin_boxes: MarginBox::ALL
197 .iter()
198 .map(|margin_box| MarginBoxDescription {
199 name: margin_box.keyword().to_string(),
200 paints: margin_box.band().is_some(),
201 })
202 .collect(),
203 margin_box_properties: properties(MARGIN_BOX_PROPERTIES),
204 sizes: PAGE_SIZES
205 .iter()
206 .map(|(name, (width, height))| PageSize {
207 name: name.to_string(),
208 width: *width,
209 height: *height,
210 })
211 .collect(),
212 counter_styles: CounterStyle::ALL
213 .iter()
214 .map(|style| style.keyword().to_string())
215 .collect(),
216 },
217 font_face: FontFace {
218 descriptors: FONT_FACE_DESCRIPTORS
219 .iter()
220 .map(|spec| Descriptor {
221 name: spec.name.to_string(),
222 syntax: spec.syntax.to_string(),
223 keywords: keywords(spec.syntax),
224 examples: strings(spec.examples),
225 })
226 .collect(),
227 },
228 units: UNITS.iter().map(|(name, _)| name.to_string()).collect(),
229 color_names: NAMED.iter().map(|(name, _)| name.to_string()).collect(),
230 }
231 }
232}
233
234fn strings(values: &[&str]) -> Vec<String> {
235 values.iter().map(|value| value.to_string()).collect()
236}
237
238fn selectors(table: &[(&str, &str)]) -> Vec<Selector> {
239 table
240 .iter()
241 .map(|(name, example)| Selector {
242 name: name.to_string(),
243 example: example.to_string(),
244 })
245 .collect()
246}
247
248fn properties<D>(specs: &[Spec<D>]) -> Vec<Property> {
249 specs
250 .iter()
251 .map(|spec| Property {
252 name: spec.name.to_string(),
253 inherited: spec.inherited,
254 syntax: spec.syntax.to_string(),
255 keywords: keywords(spec.syntax),
256 examples: strings(spec.examples),
257 })
258 .collect()
259}
260
261fn keywords(syntax: &str) -> Vec<String> {
265 let mut found: Vec<String> = Vec::new();
266 let mut word = String::new();
267 let mut depth = 0u32;
268 let mut in_type = false;
269 let mut in_braces = false;
270 for c in syntax.chars() {
271 if c.is_ascii_alphanumeric() || c == '-' {
272 word.push(c);
273 continue;
274 }
275 if !word.is_empty() {
276 let keyword = !in_type
277 && !in_braces
278 && depth == 0
279 && c != '('
280 && !word.starts_with(|c: char| c.is_ascii_digit());
281 if keyword && !found.contains(&word) {
282 found.push(word.clone());
283 }
284 word.clear();
285 }
286 match c {
287 '<' => in_type = true,
288 '>' => in_type = false,
289 '{' => in_braces = true,
290 '}' => in_braces = false,
291 '(' => depth += 1,
292 ')' => depth = depth.saturating_sub(1),
293 _ => {}
294 }
295 }
296 if !word.is_empty()
297 && !in_type
298 && !in_braces
299 && depth == 0
300 && !word.starts_with(|c: char| c.is_ascii_digit())
301 && !found.contains(&word)
302 {
303 found.push(word);
304 }
305 found
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use crate::style::element::ElementTree;
312 use crate::style::properties::{Declaration, Edge};
313 use crate::style::sheet::{FaceDeclaration, MarginDeclaration, PageDeclaration};
314 use cssparser::{CowRcStr, Parser, ParserInput};
315 use std::collections::BTreeSet;
316
317 #[test]
318 fn keywords_skip_types_functions_and_multipliers() {
319 assert_eq!(
320 keywords(
321 "none | <string> | counter(page) | counter(page, <counter-style>) | string(<name>)"
322 ),
323 vec!["none"]
324 );
325 assert_eq!(
326 keywords("<length>{1,2} | <page-size> [ portrait | landscape ]?"),
327 vec!["portrait", "landscape"]
328 );
329 assert_eq!(
330 keywords("normal | bold | <number [1,1000]>"),
331 vec!["normal", "bold"]
332 );
333 assert_eq!(
334 keywords("[ <family-name> | serif | sans-serif | monospace ]#"),
335 vec!["serif", "sans-serif", "monospace"]
336 );
337 assert_eq!(
338 keywords("[ <url> format(<string>)? | local(<string>) ]#"),
339 Vec::<String>::new()
340 );
341 }
342
343 #[test]
344 fn version_is_the_crate_version() {
345 assert_eq!(Subset::describe().version, env!("CARGO_PKG_VERSION"));
346 }
347
348 #[test]
349 fn round_trips_through_json() {
350 let subset = Subset::describe();
351 let json = serde_json::to_string(&subset).unwrap();
352 let read: Subset = serde_json::from_str(&json).unwrap();
353 assert_eq!(read, subset);
354 }
355
356 #[test]
360 fn examples_read_into_their_own_property() {
361 fn check<D>(specs: &[Spec<D>], property_of: fn(&D) -> &str) {
362 for spec in specs {
363 for example in spec.examples {
364 let mut input = ParserInput::new(example);
365 let mut parser = Parser::new(&mut input);
366 let name = CowRcStr::from(spec.name);
367 let declarations = spec
368 .read(&name, &mut parser)
369 .unwrap_or_else(|error| panic!("{}: {example}: {error:?}", spec.name));
370 assert!(
371 parser.expect_exhausted().is_ok(),
372 "{}: {example} left input unread",
373 spec.name
374 );
375 assert!(!declarations.is_empty(), "{}: {example}", spec.name);
376 for declaration in &declarations {
377 let read = property_of(declaration);
378 assert!(
379 longhand_of(read, spec.name),
380 "{}: {example} read as {read}",
381 spec.name
382 );
383 }
384 }
385 }
386 }
387 check(PROPERTIES, Declaration::property);
388 check(PAGE_PROPERTIES, |declaration| match declaration {
389 PageDeclaration::Size(..) => "size",
390 PageDeclaration::Margin(edge, _) => margin_of(*edge),
391 PageDeclaration::BackgroundColor(_) => "background-color",
392 PageDeclaration::BackgroundImage(_) => "background-image",
393 PageDeclaration::BackgroundRepeat(_) => "background-repeat",
394 PageDeclaration::BackgroundSize(_) => "background-size",
395 PageDeclaration::BackgroundPosition(..) => "background-position",
396 PageDeclaration::ColumnCount(_) => "column-count",
397 PageDeclaration::ColumnWidth(_) => "column-width",
398 PageDeclaration::ColumnGap(_) => "column-gap",
399 PageDeclaration::ColumnRuleWidth(_) => "column-rule-width",
400 PageDeclaration::ColumnRuleStyle(_) => "column-rule-style",
401 PageDeclaration::AlignContent(_) => "align-content",
402 PageDeclaration::Pending(pending) => &pending.property,
403 });
404 check(MARGIN_BOX_PROPERTIES, |declaration| match declaration {
405 MarginDeclaration::Content(_) => "content",
406 MarginDeclaration::Style(declaration) => declaration.property(),
407 MarginDeclaration::Pending(pending) => &pending.property,
408 });
409 check(FONT_FACE_DESCRIPTORS, |declaration| match declaration {
410 FaceDeclaration::Family(_) => "font-family",
411 FaceDeclaration::Style(_) => "font-style",
412 FaceDeclaration::Weight(_) => "font-weight",
413 FaceDeclaration::Src(_) => "src",
414 });
415 }
416
417 fn longhand_of(read: &str, shorthand: &str) -> bool {
422 let mut words = read.split('-');
423 shorthand
424 .split('-')
425 .all(|word| words.any(|read| read == word))
426 }
427
428 #[test]
429 fn a_longhand_carries_every_word_of_its_shorthand() {
430 assert!(longhand_of("margin-top", "margin"));
431 assert!(longhand_of("margin-top", "margin-top"));
432 assert!(longhand_of("border-top-width", "border"));
433 assert!(longhand_of("border-top-width", "border-top"));
434 assert!(longhand_of("border-top-width", "border-width"));
435 assert!(!longhand_of("border-top-width", "border-color"));
436 assert!(!longhand_of("margin-top", "padding"));
437 }
438
439 fn margin_of(edge: Edge) -> &'static str {
440 match edge {
441 Edge::Top => "margin-top",
442 Edge::Right => "margin-right",
443 Edge::Bottom => "margin-bottom",
444 Edge::Left => "margin-left",
445 }
446 }
447
448 #[test]
451 fn elements_are_the_ones_the_tree_builds() {
452 let inlines = r#"[
453 {"type": "text", "value": "a "},
454 {"type": "emphasis", "children": [{"type": "text", "value": "b"}]},
455 {"type": "strong", "children": [{"type": "text", "value": "c"}]},
456 {"type": "code", "value": "d"},
457 {"type": "link", "url": "x", "children": [{"type": "text", "value": "e"}]},
458 {"type": "strikethrough", "children": [{"type": "text", "value": "f"}]},
459 {"type": "span", "children": [{"type": "text", "value": "g"}]},
460 {"type": "note", "blocks": [{"type": "paragraph", "inlines": [
461 {"type": "text", "value": "h"}
462 ]}]}
463 ]"#;
464 let headings: Vec<String> = (1..=6)
465 .map(|level| {
466 format!(r#"{{"type": "heading", "level": {level}, "inlines": {inlines}}}"#)
467 })
468 .collect();
469 let json = format!(
470 r#"{{"metadata": {{}}, "sections": [{{"blocks": [
471 {},
472 {{"type": "paragraph", "inlines": {inlines}}},
473 {{"type": "blockquote", "blocks": [{{"type": "paragraph", "inlines": {inlines}}}]}},
474 {{"type": "code_block", "text": "fn main() {{}}"}},
475 {{"type": "thematic_break"}},
476 {{"type": "image", "url": "image.png", "alt": "alt"}},
477 {{"type": "list", "items": [{{"blocks": []}}]}},
478 {{"type": "list", "ordered": true, "items": [{{"blocks": []}}]}},
479 {{"type": "table",
480 "head": [{{"cells": [{{"blocks": []}}]}}],
481 "body": [{{"cells": [{{"blocks": []}}]}}]}}
482 ]}}]}}"#,
483 headings.join(",\n")
484 );
485 let mut book: crate::content::Book = serde_json::from_str(&json).unwrap();
486 book.assign_node_ids();
487 let built: BTreeSet<&str> = ElementTree::build(&book)
488 .nodes()
489 .iter()
490 .map(|node| node.name)
491 .collect();
492 let listed: BTreeSet<&str> = ELEMENTS.iter().copied().collect();
493 assert_eq!(built, listed);
494 }
495}