@@ -28,10 +28,16 @@ pub fn coerce_value(raw: &str) -> Value {
2828 Value :: from ( t. to_string ( ) )
2929}
3030
31- /// Parse an inline `style="a:b; c:d"` declaration list into a JSON style object.
32- /// `grid-template-columns`/`-rows` are split into string arrays; all other
33- /// properties pass through their kebab-case name with a coerced value.
34- pub fn parse_inline_style ( decls : & str ) -> Map < String , Value > {
31+ /// Parse an inline `style="a:b; c:d"` declaration list into a JSON style
32+ /// object. `padding`/`margin`/`border-radius` accept the CSS 1/2/3/4-value
33+ /// box shorthand, expanded into the `{top,right,bottom,left}` /
34+ /// `{top-left,top-right,bottom-right,bottom-left}` object the core CSS
35+ /// engine's `Edges`/`BorderRadius` types deserialize. `grid-template-columns`/
36+ /// `-rows` accept a track list, `repeat()`/`minmax()` included. Any other
37+ /// property whose value is more than one top-level (paren-aware) token is
38+ /// refused rather than passed through as an opaque string the core length
39+ /// parser cannot read (see [`HtmlError::UnsupportedStyleShorthand`]).
40+ pub fn parse_inline_style ( decls : & str ) -> Result < Map < String , Value > , HtmlError > {
3541 let mut map = Map :: new ( ) ;
3642 for decl in decls. split ( ';' ) {
3743 let decl = decl. trim ( ) ;
@@ -43,17 +49,198 @@ pub fn parse_inline_style(decls: &str) -> Map<String, Value> {
4349 } ;
4450 let prop = prop. trim ( ) . to_string ( ) ;
4551 let value = value. trim ( ) ;
46- if prop == "grid-template-columns" || prop == "grid-template-rows" {
47- let arr: Vec < Value > = value
48- . split_whitespace ( )
49- . map ( |t| Value :: from ( t. to_string ( ) ) )
50- . collect ( ) ;
51- map. insert ( prop, Value :: Array ( arr) ) ;
52- } else {
53- map. insert ( prop, coerce_value ( value) ) ;
52+ match prop. as_str ( ) {
53+ "grid-template-columns" | "grid-template-rows" => {
54+ map. insert (
55+ prop. clone ( ) ,
56+ Value :: Array ( parse_grid_template ( & prop, value) ?) ,
57+ ) ;
58+ }
59+ "padding" | "margin" => {
60+ let tokens = split_top_level_tokens ( value) ;
61+ match tokens. len ( ) {
62+ 1 => {
63+ map. insert ( prop, coerce_value ( value) ) ;
64+ }
65+ 2 ..=4 => {
66+ map. insert ( prop, expand_box_edges ( & tokens) ) ;
67+ }
68+ _ => {
69+ return Err ( HtmlError :: UnsupportedStyleShorthand {
70+ prop,
71+ value : value. to_string ( ) ,
72+ } )
73+ }
74+ }
75+ }
76+ "border-radius" => {
77+ let tokens = split_top_level_tokens ( value) ;
78+ match tokens. len ( ) {
79+ 1 => {
80+ map. insert ( prop, coerce_value ( value) ) ;
81+ }
82+ 2 ..=4 => {
83+ map. insert ( prop, expand_border_radius_corners ( & tokens) ) ;
84+ }
85+ _ => {
86+ return Err ( HtmlError :: UnsupportedStyleShorthand {
87+ prop,
88+ value : value. to_string ( ) ,
89+ } )
90+ }
91+ }
92+ }
93+ _ => {
94+ if split_top_level_tokens ( value) . len ( ) > 1 {
95+ return Err ( HtmlError :: UnsupportedStyleShorthand {
96+ prop,
97+ value : value. to_string ( ) ,
98+ } ) ;
99+ }
100+ map. insert ( prop, coerce_value ( value) ) ;
101+ }
102+ }
103+ }
104+ Ok ( map)
105+ }
106+
107+ /// Split a CSS value on top-level whitespace: whitespace inside a `(...)`
108+ /// span (e.g. the argument list of `rgba(0, 0, 0, 0.5)` or `repeat(3, 1fr)`)
109+ /// does not count as a separator, so a single functional-notation value
110+ /// stays one token while a genuine multi-value shorthand (`24px 48px`)
111+ /// splits into its parts.
112+ fn split_top_level_tokens ( s : & str ) -> Vec < & str > {
113+ let mut tokens = Vec :: new ( ) ;
114+ let mut depth = 0i32 ;
115+ let mut token_start: Option < usize > = None ;
116+ for ( i, c) in s. char_indices ( ) {
117+ match c {
118+ '(' => depth += 1 ,
119+ ')' => depth = depth. saturating_sub ( 1 ) ,
120+ _ => { }
121+ }
122+ if c. is_whitespace ( ) && depth == 0 {
123+ if let Some ( start) = token_start. take ( ) {
124+ tokens. push ( & s[ start..i] ) ;
125+ }
126+ } else if token_start. is_none ( ) {
127+ token_start = Some ( i) ;
128+ }
129+ }
130+ if let Some ( start) = token_start {
131+ tokens. push ( & s[ start..] ) ;
132+ }
133+ tokens
134+ }
135+
136+ /// Expand a 2/3/4-value `padding`/`margin` shorthand into the
137+ /// `{top,right,bottom,left}` object `Edges::Sides` deserializes, following
138+ /// the standard CSS clockwise-from-top expansion rule.
139+ fn expand_box_edges ( tokens : & [ & str ] ) -> Value {
140+ let ( top, right, bottom, left) = match tokens {
141+ [ a, b] => ( * a, * b, * a, * b) ,
142+ [ a, b, c] => ( * a, * b, * c, * b) ,
143+ [ a, b, c, d] => ( * a, * b, * c, * d) ,
144+ _ => unreachable ! ( "caller only passes 2..=4 tokens" ) ,
145+ } ;
146+ let mut edges = Map :: new ( ) ;
147+ edges. insert ( "top" . into ( ) , coerce_value ( top) ) ;
148+ edges. insert ( "right" . into ( ) , coerce_value ( right) ) ;
149+ edges. insert ( "bottom" . into ( ) , coerce_value ( bottom) ) ;
150+ edges. insert ( "left" . into ( ) , coerce_value ( left) ) ;
151+ Value :: Object ( edges)
152+ }
153+
154+ /// Expand a 2/3/4-value `border-radius` shorthand into the
155+ /// `{top-left,top-right,bottom-right,bottom-left}` object
156+ /// `BorderRadius::Corners` deserializes, following the standard CSS
157+ /// clockwise-from-top-left expansion rule (a different starting corner than
158+ /// [`expand_box_edges`], per the CSS box-shorthand spec).
159+ fn expand_border_radius_corners ( tokens : & [ & str ] ) -> Value {
160+ let ( top_left, top_right, bottom_right, bottom_left) = match tokens {
161+ [ a, b] => ( * a, * b, * a, * b) ,
162+ [ a, b, c] => ( * a, * b, * c, * b) ,
163+ [ a, b, c, d] => ( * a, * b, * c, * d) ,
164+ _ => unreachable ! ( "caller only passes 2..=4 tokens" ) ,
165+ } ;
166+ let mut corners = Map :: new ( ) ;
167+ corners. insert ( "top-left" . into ( ) , coerce_value ( top_left) ) ;
168+ corners. insert ( "top-right" . into ( ) , coerce_value ( top_right) ) ;
169+ corners. insert ( "bottom-right" . into ( ) , coerce_value ( bottom_right) ) ;
170+ corners. insert ( "bottom-left" . into ( ) , coerce_value ( bottom_left) ) ;
171+ Value :: Object ( corners)
172+ }
173+
174+ /// Parse a `grid-template-columns`/`-rows` track list into the flat
175+ /// `Vec<GridTrack>` JSON the core CSS engine expects: `repeat(n, track)`
176+ /// expands into `n` copies of `track`, `minmax(min, max)` becomes
177+ /// `{"min":..,"max":..}`, and every other token passes through
178+ /// [`coerce_value`] unchanged (a bare number for `fr`, a keyword string, or
179+ /// an explicit length).
180+ fn parse_grid_template ( prop : & str , value : & str ) -> Result < Vec < Value > , HtmlError > {
181+ let mut out = Vec :: new ( ) ;
182+ for token in split_top_level_tokens ( value) {
183+ push_grid_track ( prop, token, & mut out) ?;
184+ }
185+ Ok ( out)
186+ }
187+
188+ fn push_grid_track ( prop : & str , token : & str , out : & mut Vec < Value > ) -> Result < ( ) , HtmlError > {
189+ if let Some ( inner) = token
190+ . strip_prefix ( "repeat(" )
191+ . and_then ( |s| s. strip_suffix ( ')' ) )
192+ {
193+ let ( count_str, pattern) =
194+ inner
195+ . split_once ( ',' )
196+ . ok_or_else ( || HtmlError :: UnsupportedStyleShorthand {
197+ prop : prop. to_string ( ) ,
198+ value : token. to_string ( ) ,
199+ } ) ?;
200+ let count: usize =
201+ count_str
202+ . trim ( )
203+ . parse ( )
204+ . map_err ( |_| HtmlError :: UnsupportedStyleShorthand {
205+ prop : prop. to_string ( ) ,
206+ value : token. to_string ( ) ,
207+ } ) ?;
208+ let pattern_tokens = split_top_level_tokens ( pattern. trim ( ) ) ;
209+ if pattern_tokens. is_empty ( ) {
210+ return Err ( HtmlError :: UnsupportedStyleShorthand {
211+ prop : prop. to_string ( ) ,
212+ value : token. to_string ( ) ,
213+ } ) ;
54214 }
215+ for _ in 0 ..count {
216+ for t in & pattern_tokens {
217+ out. push ( parse_single_grid_track ( prop, t) ?) ;
218+ }
219+ }
220+ return Ok ( ( ) ) ;
221+ }
222+ out. push ( parse_single_grid_track ( prop, token) ?) ;
223+ Ok ( ( ) )
224+ }
225+
226+ fn parse_single_grid_track ( prop : & str , token : & str ) -> Result < Value , HtmlError > {
227+ if let Some ( inner) = token
228+ . strip_prefix ( "minmax(" )
229+ . and_then ( |s| s. strip_suffix ( ')' ) )
230+ {
231+ let ( min_s, max_s) =
232+ inner
233+ . split_once ( ',' )
234+ . ok_or_else ( || HtmlError :: UnsupportedStyleShorthand {
235+ prop : prop. to_string ( ) ,
236+ value : token. to_string ( ) ,
237+ } ) ?;
238+ let mut minmax = Map :: new ( ) ;
239+ minmax. insert ( "min" . into ( ) , coerce_value ( min_s. trim ( ) ) ) ;
240+ minmax. insert ( "max" . into ( ) , coerce_value ( max_s. trim ( ) ) ) ;
241+ return Ok ( Value :: Object ( minmax) ) ;
55242 }
56- map
243+ Ok ( coerce_value ( token ) )
57244}
58245
59246/// Parse an `anim` attribute into the `style.animation` JSON array.
@@ -187,15 +374,15 @@ mod tests {
187374
188375 #[ test]
189376 fn parses_declarations_into_style_object ( ) {
190- let m = parse_inline_style ( "font-size:96px; color:#fff; text-align:center" ) ;
377+ let m = parse_inline_style ( "font-size:96px; color:#fff; text-align:center" ) . unwrap ( ) ;
191378 assert_eq ! ( m. get( "font-size" ) , Some ( & json!( 96 ) ) ) ;
192379 assert_eq ! ( m. get( "color" ) , Some ( & json!( "#fff" ) ) ) ;
193380 assert_eq ! ( m. get( "text-align" ) , Some ( & json!( "center" ) ) ) ;
194381 }
195382
196383 #[ test]
197384 fn grid_template_becomes_string_array ( ) {
198- let m = parse_inline_style ( "grid-template-columns: 1fr 1fr" ) ;
385+ let m = parse_inline_style ( "grid-template-columns: 1fr 1fr" ) . unwrap ( ) ;
199386 assert_eq ! ( m. get( "grid-template-columns" ) , Some ( & json!( [ "1fr" , "1fr" ] ) ) ) ;
200387 }
201388
0 commit comments