From fbf1e15689ac925e5304359ae54d8fba0e6cb477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=80=E4=B8=9D?= Date: Wed, 17 Dec 2025 23:12:38 +0800 Subject: [PATCH 1/2] Supports parsing `@font-face` Part of https://github.com/linebender/resvg/issues/541 --- README.md | 5 +-- src/lib.rs | 63 ++++++++++++++++++++++++++++++++------ tests/stylesheet.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ea601e7..e64191b 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ Since it's very simple we will start with limitations: ## Limitations -- [At-rules](https://www.w3.org/TR/CSS21/syndata.html#at-rules) are not supported. - They will be skipped during parsing. +- [Most at-rules](https://www.w3.org/TR/CSS21/syndata.html#at-rules) are not supported. + They will be skipped during parsing. The only supported at-rule is `@font-face`. - Property values are not parsed. In CSS like `* { width: 5px }` you will get a `width` property with a `5px` value as a string. - CDO/CDC comments are not supported. @@ -34,6 +34,7 @@ Since it's very simple we will start with limitations: - Selector matching support. - The rules are sorted by specificity. +- `@font-face` parsing support. - `!important` parsing support. - Has a high-level parsers and low-level, zero-allocation tokenizers. - No unsafe. diff --git a/src/lib.rs b/src/lib.rs index a06474e..9d26b80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,8 +12,8 @@ Since it's very simple we will start with limitations: ## Limitations -- [At-rules](https://www.w3.org/TR/CSS21/syndata.html#at-rules) are not supported. - They will be skipped during parsing. +- [Most at-rules](https://www.w3.org/TR/CSS21/syndata.html#at-rules) are not supported. + They will be skipped during parsing. The only supported at-rule is `@font-face`. - Property values are not parsed. In CSS like `* { width: 5px }` you will get a `width` property with a `5px` value as a string. - CDO/CDC comments are not supported. @@ -24,6 +24,7 @@ Since it's very simple we will start with limitations: - Selector matching support. - The rules are sorted by specificity. +- `@font-face` parsing support. - `!important` parsing support. - Has a high-level parsers and low-level, zero-allocation tokenizers. - No unsafe. @@ -192,6 +193,13 @@ pub struct Declaration<'a> { pub important: bool, } +/// A `@font-face` rule. +#[derive(Clone, Debug)] +pub struct FontFaceRule<'a> { + /// A list of declarations inside this `@font-face` rule. + pub declarations: Vec>, +} + /// A rule. #[derive(Clone, Debug)] pub struct Rule<'a> { @@ -206,17 +214,23 @@ pub struct Rule<'a> { pub struct StyleSheet<'a> { /// A list of rules. pub rules: Vec>, + /// A list of `@font-face` rules. + pub font_faces: Vec>, } impl<'a> StyleSheet<'a> { /// Creates an empty style sheet. pub fn new() -> Self { - StyleSheet { rules: Vec::new() } + StyleSheet { + rules: Vec::new(), + font_faces: Vec::new(), + } } /// Parses a style sheet from text. /// - /// At-rules are not supported and will be skipped. + /// Most at-rules are not supported and will be skipped, except `@font-face` + /// rules which are parsed into [`FontFaceRule`]s. /// /// # Errors /// @@ -242,7 +256,7 @@ impl<'a> StyleSheet<'a> { break; } - let _ = consume_statement(&mut s, &mut self.rules); + let _ = consume_statement(&mut s, &mut self.rules, &mut self.font_faces); } if !s.at_end() { @@ -286,19 +300,50 @@ impl Default for StyleSheet<'_> { } } -fn consume_statement<'a>(s: &mut Stream<'a>, rules: &mut Vec>) -> Result<(), Error> { +fn consume_statement<'a>( + s: &mut Stream<'a>, + rules: &mut Vec>, + font_faces: &mut Vec>, +) -> Result<(), Error> { if s.curr_byte() == Ok(b'@') { s.advance(1); - consume_at_rule(s) + consume_at_rule(s, font_faces) } else { consume_rule_set(s, rules) } } -fn consume_at_rule(s: &mut Stream<'_>) -> Result<(), Error> { +fn consume_at_rule<'a>( + s: &mut Stream<'a>, + font_faces: &mut Vec>, +) -> Result<(), Error> { let ident = s.consume_ident()?; - warn!("The @{} rule is not supported. Skipped.", ident); + if ident == "font-face" { + s.skip_spaces_and_comments()?; + + if s.curr_byte() == Ok(b'{') { + s.advance(1); + + let declarations = consume_declarations(s)?; + s.try_consume_byte(b'}'); + + if !declarations.is_empty() { + font_faces.push(FontFaceRule { declarations }); + } + } else { + // Malformed `@font-face`; fall back to skipping it as an unknown at-rule. + skip_at_rule_body(s)?; + } + } else { + warn!("The @{} rule is not supported. Skipped.", ident); + skip_at_rule_body(s)?; + } + + Ok(()) +} + +fn skip_at_rule_body(s: &mut Stream<'_>) -> Result<(), Error> { s.skip_bytes(|c| c != b';' && c != b'{'); match s.curr_byte()? { diff --git a/tests/stylesheet.rs b/tests/stylesheet.rs index 846d65d..37be83e 100644 --- a/tests/stylesheet.rs +++ b/tests/stylesheet.rs @@ -145,3 +145,77 @@ fn style_21() { let style = StyleSheet::parse(":le>*"); assert_eq!(style.to_string(), ""); } + +#[test] +fn font_face_01() { + let style = StyleSheet::parse( + "@font-face { font-family: 'Noto Serif'; src: url(NotoSerif.woff2) format('woff2'); }", + ); + + assert_eq!(style.rules.len(), 0); + assert_eq!(style.font_faces.len(), 1); + + let ff = &style.font_faces[0]; + assert_eq!( + ff.declarations, + vec![ + Declaration { + name: "font-family", + value: "'Noto Serif'", + important: false, + }, + Declaration { + name: "src", + value: "url(NotoSerif.woff2) format('woff2')", + important: false, + }, + ] + ); +} + +#[test] +fn font_face_02_mixed_with_rules() { + let style = StyleSheet::parse( + "@font-face { font-family: 'MyFont'; src: url(https://foo.com/my.woff2); font-weight: normal; } div { color: red; }", + ); + + assert_eq!(style.rules.len(), 1); + assert_eq!(style.font_faces.len(), 1); + + assert_eq!(style.rules[0].selector.to_string(), "div"); + assert_eq!(style.rules[0].declarations.len(), 1); + assert_eq!(style.rules[0].declarations[0].name, "color"); + assert_eq!(style.rules[0].declarations[0].value, "red"); + + assert_eq!(style.font_faces[0].declarations[0].name, "font-family"); + assert_eq!(style.font_faces[0].declarations[0].value, "'MyFont'"); + assert_eq!(style.font_faces[0].declarations[1].name, "src"); + assert_eq!( + style.font_faces[0].declarations[1].value, + "url(https://foo.com/my.woff2)" + ); + assert_eq!(style.font_faces[0].declarations[2].name, "font-weight"); + assert_eq!(style.font_faces[0].declarations[2].value, "normal"); +} + +#[test] +fn font_face_03_mixed_with_rules() { + let style = StyleSheet::parse( + "@font-palette-values --identifier { font-family: Bixa; override-colors: 0 green, 1 #999; } div { color: red; } @font-face { font-family: 'MyFont'; src: local('Airal'); font-weight: 200 800; }", + ); + + assert_eq!(style.rules.len(), 1); + assert_eq!(style.font_faces.len(), 1); + + assert_eq!(style.rules[0].selector.to_string(), "div"); + assert_eq!(style.rules[0].declarations.len(), 1); + assert_eq!(style.rules[0].declarations[0].name, "color"); + assert_eq!(style.rules[0].declarations[0].value, "red"); + + assert_eq!(style.font_faces[0].declarations[0].name, "font-family"); + assert_eq!(style.font_faces[0].declarations[0].value, "'MyFont'"); + assert_eq!(style.font_faces[0].declarations[1].name, "src"); + assert_eq!(style.font_faces[0].declarations[1].value, "local('Airal')"); + assert_eq!(style.font_faces[0].declarations[2].name, "font-weight"); + assert_eq!(style.font_faces[0].declarations[2].value, "200 800"); +} From 4ba165df13130b8ea7b9b068aa935458ec73b379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=80=E4=B8=9D?= Date: Sun, 21 Dec 2025 21:16:55 +0800 Subject: [PATCH 2/2] fix: doc_auto_cfg error error[E0557]: feature has been removed --> src/lib.rs:42:29 | 42 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] | ^^^^^^^^^^^^ feature has been removed | = note: removed in 1.92.0; see for more information = note: merged into `doc_cfg` error: Compilation failed, aborting rustdoc --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 9d26b80..e0da21c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,7 +39,7 @@ Since it's very simple we will start with limitations: // Targeting e.g. 32-bit means structs containing usize can give false positives for 64-bit. #![cfg_attr(target_pointer_width = "64", warn(clippy::trivially_copy_pass_by_ref))] // END LINEBENDER LINT SET -#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #![no_std] // The following lints are part of the Linebender standard set, // but resolving them has been deferred for now.