33 */
44package com .codename1 .ui .css ;
55
6+ import com .codename1 .ui .Component ;
67import com .codename1 .ui .EncodedImage ;
78import com .codename1 .ui .Image ;
89import com .codename1 .ui .plaf .CSSBorder ;
4041/// - `var(--name)` dereferencing in declaration values.
4142public class CSSThemeCompiler {
4243
44+ public static class CSSSyntaxException extends IllegalArgumentException {
45+ public CSSSyntaxException (String message ) {
46+ super (message );
47+ }
48+
49+ public CSSSyntaxException (String message , Throwable cause ) {
50+ super (message , cause );
51+ }
52+ }
53+
4354 public void compile (String css , MutableResource resources , String themeName ) {
4455 Hashtable theme = resources .getTheme (themeName );
4556 if (theme == null ) {
@@ -81,7 +92,7 @@ private void compileConstants(String css, Hashtable theme) {
8192 }
8293 int close = stripped .indexOf ('}' , open + 1 );
8394 if (close <= open ) {
84- return ;
95+ throw new CSSSyntaxException ( "Unterminated @constants block" ) ;
8596 }
8697 Declaration [] declarations = parseDeclarations (stripped .substring (open + 1 , close ));
8798 for (Declaration declaration : declarations ) {
@@ -166,6 +177,11 @@ private boolean applySimpleThemeProperty(Hashtable theme, String uiid, String st
166177 theme .put (uiid + "." + statePrefix + "font" , value );
167178 return true ;
168179 }
180+ if ("text-align" .equals (property )) {
181+ Integer align = normalizeAlignment (value );
182+ theme .put (uiid + "." + statePrefix + "align" , align );
183+ return true ;
184+ }
169185 return false ;
170186 }
171187
@@ -196,13 +212,71 @@ private boolean appendBorderProperty(StringBuilder borderCss, String property, S
196212 if (!isBorderProperty (property )) {
197213 return false ;
198214 }
215+ if ("border" .equals (property )) {
216+ String expanded = expandBorderShorthand (value );
217+ if (expanded .length () == 0 ) {
218+ return true ;
219+ }
220+ if (borderCss .length () > 0 ) {
221+ borderCss .append (';' );
222+ }
223+ borderCss .append (expanded );
224+ return true ;
225+ }
199226 if (borderCss .length () > 0 ) {
200227 borderCss .append (';' );
201228 }
202229 borderCss .append (property ).append (':' ).append (value );
203230 return true ;
204231 }
205232
233+ private String expandBorderShorthand (String value ) {
234+ String [] parts = splitOnWhitespace (value );
235+ if (parts .length == 0 ) {
236+ throw new CSSSyntaxException ("border shorthand is missing value" );
237+ }
238+ String width = null ;
239+ String style = null ;
240+ String color = null ;
241+ for (String part : parts ) {
242+ String token = part .trim ().toLowerCase ();
243+ if (token .length () == 0 ) {
244+ continue ;
245+ }
246+ if (width == null && (token .endsWith ("px" ) || token .endsWith ("mm" ) || token .endsWith ("pt" ) || token .endsWith ("%" ) || "0" .equals (token ))) {
247+ width = part ;
248+ continue ;
249+ }
250+ if (style == null && ("none" .equals (token ) || "solid" .equals (token ) || "dashed" .equals (token ) || "dotted" .equals (token ))) {
251+ style = token ;
252+ continue ;
253+ }
254+ if (color == null ) {
255+ normalizeHexColor (part );
256+ color = part ;
257+ continue ;
258+ }
259+ throw new CSSSyntaxException ("Unsupported border shorthand token: " + part );
260+ }
261+ StringBuilder out = new StringBuilder ();
262+ if (width != null ) {
263+ out .append ("border-width:" ).append (width );
264+ }
265+ if (style != null ) {
266+ if (out .length () > 0 ) {
267+ out .append (';' );
268+ }
269+ out .append ("border-style:" ).append (style );
270+ }
271+ if (color != null ) {
272+ if (out .length () > 0 ) {
273+ out .append (';' );
274+ }
275+ out .append ("border-color:" ).append (color );
276+ }
277+ return out .toString ();
278+ }
279+
206280 private String resolveVars (Hashtable theme , String value ) {
207281 String out = value ;
208282 int varPos = out .indexOf ("var(--" );
@@ -223,10 +297,21 @@ private String resolveVars(Hashtable theme, String value) {
223297 private String [] selector (String selector ) {
224298 String statePrefix = "" ;
225299 String uiid = selector .trim ();
300+
226301 int pseudoPos = uiid .indexOf (':' );
227- if (pseudoPos > -1 ) {
228- String pseudo = uiid .substring (pseudoPos + 1 ).trim ();
229- uiid = uiid .substring (0 , pseudoPos ).trim ();
302+ int classStatePos = uiid .indexOf ('.' );
303+ int statePos = -1 ;
304+ if (pseudoPos > -1 && classStatePos > -1 ) {
305+ statePos = Math .min (pseudoPos , classStatePos );
306+ } else if (pseudoPos > -1 ) {
307+ statePos = pseudoPos ;
308+ } else if (classStatePos > -1 ) {
309+ statePos = classStatePos ;
310+ }
311+
312+ if (statePos > -1 ) {
313+ String pseudo = uiid .substring (statePos + 1 ).trim ();
314+ uiid = uiid .substring (0 , statePos ).trim ();
230315 statePrefix = statePrefix (pseudo );
231316 }
232317 if ("*" .equals (uiid ) || uiid .length () == 0 ) {
@@ -245,7 +330,7 @@ private String statePrefix(String pseudo) {
245330 if ("disabled" .equals (pseudo )) {
246331 return "dis#" ;
247332 }
248- return "" ;
333+ throw new CSSSyntaxException ( "Unsupported pseudo state: " + pseudo ) ;
249334 }
250335
251336 private Image createSolidImage (String color ) {
@@ -267,10 +352,33 @@ private boolean isBorderProperty(String property) {
267352 }
268353
269354 private String normalizeHexColor (String cssColor ) {
270- String value = cssColor .trim ();
271- if ("transparent" .equalsIgnoreCase (value )) {
355+ String value = cssColor == null ? "" : cssColor .trim ().toLowerCase ();
356+ if (value .length () == 0 ) {
357+ throw new CSSSyntaxException ("Color value cannot be empty" );
358+ }
359+ if ("transparent" .equals (value )) {
272360 return "000000" ;
273361 }
362+
363+ if (value .startsWith ("rgb(" )) {
364+ if (!value .endsWith (")" )) {
365+ throw new CSSSyntaxException ("Malformed rgb() color: " + cssColor );
366+ }
367+ String [] parts = splitOnComma (value .substring (4 , value .length () - 1 ));
368+ if (parts .length != 3 ) {
369+ throw new CSSSyntaxException ("rgb() must have exactly 3 components: " + cssColor );
370+ }
371+ int r = parseRgbChannel (parts [0 ], cssColor );
372+ int g = parseRgbChannel (parts [1 ], cssColor );
373+ int b = parseRgbChannel (parts [2 ], cssColor );
374+ return toHexColor ((r << 16 ) | (g << 8 ) | b );
375+ }
376+
377+ String keyword = cssColorKeyword (value );
378+ if (keyword != null ) {
379+ return keyword ;
380+ }
381+
274382 if (value .startsWith ("#" )) {
275383 value = value .substring (1 );
276384 }
@@ -279,7 +387,92 @@ private String normalizeHexColor(String cssColor) {
279387 + value .charAt (1 ) + value .charAt (1 )
280388 + value .charAt (2 ) + value .charAt (2 );
281389 }
282- return value .toLowerCase ();
390+ if (value .length () != 6 || !isHexColor (value )) {
391+ throw new CSSSyntaxException ("Unsupported color value: " + cssColor );
392+ }
393+ return value ;
394+ }
395+
396+ private Integer normalizeAlignment (String value ) {
397+ String v = value == null ? "" : value .trim ().toLowerCase ();
398+ if ("left" .equals (v ) || "start" .equals (v )) {
399+ return Integer .valueOf (Component .LEFT );
400+ }
401+ if ("center" .equals (v )) {
402+ return Integer .valueOf (Component .CENTER );
403+ }
404+ if ("right" .equals (v ) || "end" .equals (v )) {
405+ return Integer .valueOf (Component .RIGHT );
406+ }
407+ throw new CSSSyntaxException ("Unsupported text-align value: " + value );
408+ }
409+
410+ private String cssColorKeyword (String value ) {
411+ if ("black" .equals (value )) {
412+ return "000000" ;
413+ }
414+ if ("white" .equals (value )) {
415+ return "ffffff" ;
416+ }
417+ if ("red" .equals (value )) {
418+ return "ff0000" ;
419+ }
420+ if ("green" .equals (value )) {
421+ return "008000" ;
422+ }
423+ if ("blue" .equals (value )) {
424+ return "0000ff" ;
425+ }
426+ if ("pink" .equals (value )) {
427+ return "ffc0cb" ;
428+ }
429+ if ("orange" .equals (value )) {
430+ return "ffa500" ;
431+ }
432+ if ("yellow" .equals (value )) {
433+ return "ffff00" ;
434+ }
435+ if ("purple" .equals (value )) {
436+ return "800080" ;
437+ }
438+ if ("gray" .equals (value ) || "grey" .equals (value )) {
439+ return "808080" ;
440+ }
441+ return null ;
442+ }
443+
444+ private int parseRgbChannel (String value , String originalColor ) {
445+ int out ;
446+ try {
447+ out = Integer .parseInt (value .trim ());
448+ } catch (RuntimeException err ) {
449+ throw new CSSSyntaxException ("Invalid rgb() channel value in " + originalColor + ": " + value , err );
450+ }
451+ if (out < 0 || out > 255 ) {
452+ throw new CSSSyntaxException ("rgb() channel out of range in " + originalColor + ": " + value );
453+ }
454+ return out ;
455+ }
456+
457+ private boolean isHexColor (String value ) {
458+ for (int i = 0 ; i < value .length (); i ++) {
459+ char c = value .charAt (i );
460+ boolean hex = (c >= '0' && c <= '9' )
461+ || (c >= 'a' && c <= 'f' )
462+ || (c >= 'A' && c <= 'F' );
463+ if (!hex ) {
464+ return false ;
465+ }
466+ }
467+ return true ;
468+ }
469+
470+ private String toHexColor (int color ) {
471+ String hex = Integer .toHexString (color & 0xffffff );
472+ while (hex .length () < 6 ) {
473+ hex = "0" + hex ;
474+ }
475+ return hex ;
283476 }
284477
285478 private String normalizeBox (String cssValue ) {
@@ -312,28 +505,40 @@ private Rule[] parseRules(String css) {
312505 ArrayList <Rule > out = new ArrayList <Rule >();
313506 int pos = 0 ;
314507 while (pos < stripped .length ()) {
508+ while (pos < stripped .length () && Character .isWhitespace (stripped .charAt (pos ))) {
509+ pos ++;
510+ }
511+ if (pos >= stripped .length ()) {
512+ break ;
513+ }
315514 int open = stripped .indexOf ('{' , pos );
316515 if (open < 0 ) {
317- break ;
516+ throw new CSSSyntaxException ( "Missing '{' in CSS rule near: " + stripped . substring ( pos )) ;
318517 }
319518 int close = stripped .indexOf ('}' , open + 1 );
320519 if (close < 0 ) {
321- break ;
520+ throw new CSSSyntaxException ("Missing '}' for CSS rule: " + stripped .substring (pos , open ).trim ());
521+ }
522+ if (stripped .indexOf ('{' , open + 1 ) > -1 && stripped .indexOf ('{' , open + 1 ) < close ) {
523+ throw new CSSSyntaxException ("Nested '{' is not supported in CSS block: " + stripped .substring (pos , open ).trim ());
322524 }
323525
324526 String selectors = stripped .substring (pos , open ).trim ();
325527 if (selectors .startsWith ("@constants" )) {
326528 pos = close + 1 ;
327529 continue ;
328530 }
531+ if (selectors .length () == 0 ) {
532+ throw new CSSSyntaxException ("Missing selector before '{'" );
533+ }
329534
330535 String body = stripped .substring (open + 1 , close ).trim ();
331536 Declaration [] declarations = parseDeclarations (body );
332537 String [] selectorsList = splitOnChar (selectors , ',' );
333538 for (String selectorEntry : selectorsList ) {
334539 String selector = selectorEntry .trim ();
335540 if (selector .length () == 0 ) {
336- continue ;
541+ throw new CSSSyntaxException ( "Empty selector in selector list: " + selectors ) ;
337542 }
338543 Rule rule = new Rule ();
339544 rule .selector = selector ;
@@ -353,13 +558,18 @@ private String stripComments(String css) {
353558 char c = css .charAt (i );
354559 if (c == '/' && i + 1 < css .length () && css .charAt (i + 1 ) == '*' ) {
355560 i += 2 ;
561+ boolean closed = false ;
356562 while (i + 1 < css .length ()) {
357563 if (css .charAt (i ) == '*' && css .charAt (i + 1 ) == '/' ) {
358564 i += 2 ;
565+ closed = true ;
359566 break ;
360567 }
361568 i ++;
362569 }
570+ if (!closed ) {
571+ throw new CSSSyntaxException ("Unterminated CSS comment" );
572+ }
363573 continue ;
364574 }
365575 out .append (c );
@@ -372,13 +582,20 @@ private Declaration[] parseDeclarations(String body) {
372582 ArrayList <Declaration > out = new ArrayList <Declaration >();
373583 String [] segments = splitOnChar (body , ';' );
374584 for (String line : segments ) {
375- int colon = line .indexOf ( ':' );
376- if (colon < = 0 ) {
585+ String trimmed = line .trim ( );
586+ if (trimmed . length () = = 0 ) {
377587 continue ;
378588 }
589+ int colon = trimmed .indexOf (':' );
590+ if (colon <= 0 || colon == trimmed .length () - 1 ) {
591+ throw new CSSSyntaxException ("Malformed declaration: " + trimmed );
592+ }
379593 Declaration dec = new Declaration ();
380- dec .property = line .substring (0 , colon ).trim ().toLowerCase ();
381- dec .value = line .substring (colon + 1 ).trim ();
594+ dec .property = trimmed .substring (0 , colon ).trim ().toLowerCase ();
595+ dec .value = trimmed .substring (colon + 1 ).trim ();
596+ if (dec .property .length () == 0 || dec .value .length () == 0 ) {
597+ throw new CSSSyntaxException ("Malformed declaration: " + trimmed );
598+ }
382599 out .add (dec );
383600 }
384601 return out .toArray (new Declaration [out .size ()]);
@@ -398,6 +615,25 @@ private String[] splitOnChar(String input, char delimiter) {
398615 return out .toArray (new String [out .size ()]);
399616 }
400617
618+ private String [] splitOnComma (String input ) {
619+ ArrayList <String > parts = new ArrayList <String >();
620+ int start = 0 ;
621+ for (int i = 0 ; i < input .length (); i ++) {
622+ if (input .charAt (i ) == ',' ) {
623+ String token = input .substring (start , i ).trim ();
624+ if (token .length () > 0 ) {
625+ parts .add (token );
626+ }
627+ start = i + 1 ;
628+ }
629+ }
630+ String tail = input .substring (start ).trim ();
631+ if (tail .length () > 0 ) {
632+ parts .add (tail );
633+ }
634+ return parts .toArray (new String [parts .size ()]);
635+ }
636+
401637 private String [] splitOnWhitespace (String input ) {
402638 ArrayList <String > out = new ArrayList <String >();
403639 StringBuilder token = new StringBuilder ();
0 commit comments