Skip to content

Commit f80c42a

Browse files
authored
Add advanced theme CSS editing with live preview in Initializr (#4601)
Fixed CSS parsing and writing logic
1 parent 730a285 commit f80c42a

9 files changed

Lines changed: 806 additions & 50 deletions

File tree

CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java

Lines changed: 251 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44
package com.codename1.ui.css;
55

6+
import com.codename1.ui.Component;
67
import com.codename1.ui.EncodedImage;
78
import com.codename1.ui.Image;
89
import com.codename1.ui.plaf.CSSBorder;
@@ -40,6 +41,16 @@
4041
/// - `var(--name)` dereferencing in declaration values.
4142
public 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

Comments
 (0)