├── .gitignore ├── LICENSE ├── README.md ├── convert.js ├── css-color-parser.js ├── main.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | dist 5 | .cache 6 | *.log 7 | *.swp 8 | .vscode 9 | test.css 10 | test.c 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 embeddedt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lv_css 2 | 3 | This is an experimental compiler that translates a CSS-like file into LVGL style function calls. 4 | 5 | ```css 6 | /* test.css */ 7 | .testClass { 8 | background-color: blue; 9 | border-radius: 4px; 10 | } 11 | .testClass:focused, .testClass2 { 12 | background-color: rgba(255, 0, 0, 0.1); 13 | opacity: 0.2; 14 | background-blend-mode: normal; 15 | border-color: red; 16 | } 17 | ``` 18 | 19 | ``` 20 | ./main.js test.css 21 | ``` 22 | ```c 23 | /* 24 | * Autogenerated file; do not edit. 25 | */ 26 | 27 | lv_style_t lv_style_testClass; 28 | lv_style_t lv_style_testClass2; 29 | 30 | void lv_style_css_init(void) { 31 | lv_style_init(&lv_style_testClass); 32 | lv_style_set_bg_color(&lv_style_testClass, LV_STATE_FOCUSED, LV_COLOR_MAKE(255, 0, 0)); lv_style_set_bg_opa(&lv_style_testClass, LV_STATE_FOCUSED, 26); 33 | lv_style_set_radius(&lv_style_testClass, LV_STATE_NORMAL, 4); 34 | lv_style_set_opa_scale(&lv_style_testClass, LV_STATE_FOCUSED, 51); 35 | lv_style_set_bg_blend_mode(&lv_style_testClass, LV_STATE_FOCUSED, LV_BLEND_MODE_NORMAL); 36 | lv_style_set_border_color(&lv_style_testClass, LV_STATE_FOCUSED, LV_COLOR_MAKE(255, 0, 0)); lv_style_set_border_opa(&lv_style_testClass, LV_STATE_FOCUSED, 255); 37 | lv_style_init(&lv_style_testClass2); 38 | lv_style_set_bg_color(&lv_style_testClass2, LV_STATE_NORMAL, LV_COLOR_MAKE(255, 0, 0)); lv_style_set_bg_opa(&lv_style_testClass2, LV_STATE_NORMAL, 26); 39 | lv_style_set_opa_scale(&lv_style_testClass2, LV_STATE_NORMAL, 51); 40 | lv_style_set_bg_blend_mode(&lv_style_testClass2, LV_STATE_NORMAL, LV_BLEND_MODE_NORMAL); 41 | lv_style_set_border_color(&lv_style_testClass2, LV_STATE_NORMAL, LV_COLOR_MAKE(255, 0, 0)); lv_style_set_border_opa(&lv_style_testClass2, LV_STATE_NORMAL, 255); 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /convert.js: -------------------------------------------------------------------------------- 1 | const cssom = require('cssom'); 2 | const fs = require('fs'); 3 | const parseCSSColor = require('./css-color-parser').parseCSSColor; 4 | var CssSelectorParser = require('css-selector-parser').CssSelectorParser, cssSelectorParser = new CssSelectorParser(); 5 | 6 | let classes = {}; 7 | let objectTypeStyles = {}; 8 | 9 | const blendModes = { 10 | "normal": "LV_BLEND_MODE_NORMAL", 11 | "additive": "LV_BLEND_MODE_ADDITIVE", 12 | "subtractive": "LV_BLEND_MODE_SUBTRACTIVE" 13 | }; 14 | 15 | const alignments = { 16 | "center": "LV_ALIGN_CENTER", 17 | "top-left": "LV_ALIGN_IN_TOP_LEFT", 18 | "top-center": "LV_ALIGN_IN_TOP_MID", 19 | "top-right": "LV_ALIGN_IN_TOP_RIGHT", 20 | "bottom-left": "LV_ALIGN_IN_BOTTOM_LEFT", 21 | "bottom-center": "LV_ALIGN_IN_BOTTOM_MID", 22 | "bottom-right": "LV_ALIGN_IN_BOTTOM_RIGHT", 23 | "left-center": "LV_ALIGN_IN_LEFT_MID", 24 | "right-center": "LV_ALIGN_IN_RIGHT_MID", 25 | "out-top-left": "LV_ALIGN_OUT_TOP_LEFT", 26 | "out-top-center": "LV_ALIGN_OUT_TOP_MID", 27 | "out-top-right": "LV_ALIGN_OUT_TOP_RIGHT", 28 | "out-bottom-left": "LV_ALIGN_OUT_BOTTOM_LEFT", 29 | "out-bottom-center": "LV_ALIGN_OUT_BOTTOM_MID", 30 | "out-bottom-right": "LV_ALIGN_OUT_BOTTOM_RIGHT", 31 | "out-left-top": "LV_ALIGN_OUT_LEFT_TOP", 32 | "out-left-center": "LV_ALIGN_OUT_LEFT_MID", 33 | "out-left-bottom": "LV_ALIGN_OUT_LEFT_BOTTOM", 34 | "out-right-top": "LV_ALIGN_OUT_RIGHT_TOP", 35 | "out-right-center": "LV_ALIGN_OUT_RIGHT_MID", 36 | "out-right-bottom": "LV_ALIGN_OUT_RIGHT_BOTTOM" 37 | }; 38 | const textDecors = { 39 | "none": "LV_TEXT_DECOR_NONE", 40 | "underline": "LV_TEXT_DECOR_UNDERLINE", 41 | "strikethrough": "LV_TEXT_DECOR_STRIKETHROUGH" 42 | }; 43 | 44 | let convertable_properties = { 45 | "background-color": { 46 | lv_name: "BG_COLOR", 47 | type: "color", 48 | alpha: "bg_opa" 49 | }, 50 | "border-radius": { 51 | lv_name: "RADIUS", 52 | type: "int" 53 | }, 54 | "clip-corner": { 55 | lv_name: "CLIP_CORNER", 56 | type: "bool" 57 | }, 58 | "transform-x": { 59 | lv_name: "TRANSFORM_WIDTH", 60 | type: "int" 61 | }, 62 | "transform-y": { 63 | lv_name: "TRANSFORM_HEIGHT", 64 | type: "int", 65 | }, 66 | "transform-angle": { 67 | lv_name: "TRANSFORM_ANGLE", 68 | type: "int" 69 | }, 70 | "opacity": { 71 | lv_name: "OPA_SCALE", 72 | type: "percent" 73 | }, 74 | "padding-left": { 75 | lv_name: "PAD_LEFT", 76 | type: "number" 77 | }, 78 | "padding-top": { 79 | lv_name: "PAD_TOP", 80 | type: "number" 81 | }, 82 | "padding-right": { 83 | lv_name: "PAD_RIGHT", 84 | type: "number" 85 | }, 86 | "padding-bottom": { 87 | lv_name: "PAD_BOTTOM", 88 | type: "number" 89 | }, 90 | "padding-inner": { 91 | lv_name: "PAD_INNER", 92 | type: "number" 93 | }, 94 | "margin-left": { 95 | lv_name: "MARGIN_LEFT", 96 | type: "number" 97 | }, 98 | "margin-top": { 99 | lv_name: "MARGIN_TOP", 100 | type: "number" 101 | }, 102 | "margin-right": { 103 | lv_name: "MARGIN_RIGHT", 104 | type: "number" 105 | }, 106 | "margin-bottom": { 107 | lv_name: "MARGIN_BOTTOM", 108 | type: "number" 109 | }, 110 | "background-blend-mode": { 111 | lv_name: "BG_BLEND_MODE", 112 | type: "enum_single", 113 | enumValues: blendModes 114 | }, 115 | "border-width": { 116 | type: "int" 117 | }, 118 | "border-side": { 119 | type: "enum_list", 120 | enumValues: { 121 | "top": "LV_BORDER_SIDE_TOP", 122 | "left": "LV_BORDER_SIDE_LEFT", 123 | "bottom": "LV_BORDER_SIDE_BOTTOM", 124 | "right": "LV_BORDER_SIDE_RIGHT", 125 | "internal": "LV_BORDER_SIDE_INTERNAL", 126 | "full": "LV_BORDER_SIDE_FULL", 127 | "all": "LV_BORDER_SIDE_FULL", 128 | } 129 | }, 130 | "border-blend-mode": { 131 | lv_name: "BORDER_BLEND_MODE", 132 | type: "enum_single", 133 | enumValues: blendModes 134 | }, 135 | "border-color": { 136 | type: "color", 137 | alpha: "border_opa" 138 | }, 139 | "outline-width": { 140 | type: "int" 141 | }, 142 | "outline-blend-mode": { 143 | lv_name: "OUTLINE_BLEND_MODE", 144 | type: "enum_single", 145 | enumValues: blendModes 146 | }, 147 | "outline-color": { 148 | type: "color", 149 | alpha: "outline_opa" 150 | }, 151 | "outline-padding": { 152 | type: "int", 153 | lv_name: "OUTLINE_PAD" 154 | }, 155 | "shadow-color": { 156 | type: "color", 157 | alpha: "shadow_opa" 158 | }, 159 | "shadow-width": { 160 | type: "int" 161 | }, 162 | "shadow-spread": { 163 | type: "int" 164 | }, 165 | "shadow-blend-mode": { 166 | lv_name: "SHADOW_BLEND_MODE", 167 | type: "enum_single", 168 | enumValues: blendModes 169 | }, 170 | "shadow-offset-x": { 171 | type: "int", 172 | lv_name: "SHADOW_OFS_X" 173 | }, 174 | "shadow-offset-y": { 175 | type: "int", 176 | lv_name: "SHADOW_OFS_Y" 177 | }, 178 | "pattern-image": { 179 | type: "address" 180 | }, 181 | "pattern-color": { 182 | type: "color", 183 | lv_name: "PATTERN_RECOLOR", 184 | alpha: "pattern_recolor_opa" 185 | }, 186 | "pattern-repeat": { 187 | type: "bool" 188 | }, 189 | "pattern-blend-mode": { 190 | lv_name: "PATTERN_BLEND_MODE", 191 | type: "enum_single", 192 | enumValues: blendModes 193 | }, 194 | "value-str": { 195 | type: "string", 196 | }, 197 | "value-color": { 198 | type: "color", 199 | lv_name: "VALUE_COLOR", 200 | alpha: "value_opa" 201 | }, 202 | "value-font": { 203 | type: "address" 204 | }, 205 | "value-align": { 206 | type: "enum_single", 207 | enumValues: alignments 208 | }, 209 | "value-offset-x": { 210 | type: "int", 211 | lv_name: "VALUE_OFS_X" 212 | }, 213 | "value-offset-y": { 214 | type: "int", 215 | lv_name: "VALUE_OFS_Y" 216 | }, 217 | "value-blend-mode": { 218 | lv_name: "VALUE_BLEND_MODE", 219 | type: "enum_single", 220 | enumValues: blendModes 221 | }, 222 | "value-letter-space": { 223 | type: "int" 224 | }, 225 | "value-line-space": { 226 | type: "int" 227 | }, 228 | "text-color": { 229 | type: "color", 230 | lv_name: "TEXT_COLOR", 231 | alpha: "text_opa" 232 | }, 233 | "text-font": { 234 | type: "address" 235 | }, 236 | "text-blend-mode": { 237 | lv_name: "TEXT_BLEND_MODE", 238 | type: "enum_single", 239 | enumValues: blendModes 240 | }, 241 | "text-letter-space": { 242 | type: "int" 243 | }, 244 | "text-line-space": { 245 | type: "int" 246 | }, 247 | "text-decor": { 248 | type: "enum_list", 249 | enumValues: textDecors 250 | }, 251 | "line-width": { 252 | type: "int" 253 | }, 254 | "line-blend-mode": { 255 | lv_name: "LINE_BLEND_MODE", 256 | type: "enum_single", 257 | enumValues: blendModes 258 | }, 259 | "line-color": { 260 | type: "color", 261 | alpha: "line_opa" 262 | }, 263 | "line-dash-width": { 264 | type: "int" 265 | }, 266 | "line-dash-gap": { 267 | type: "int" 268 | }, 269 | "line-rounded": { 270 | type: "bool" 271 | }, 272 | "image-color": { 273 | type: "color", 274 | alpha: "image_recolor_opa", 275 | lv_name: "IMAGE_RECOLOR" 276 | }, 277 | "image-blend-mode": { 278 | lv_name: "IMAGE_BLEND_MODE", 279 | type: "enum_single", 280 | enumValues: blendModes 281 | }, 282 | "image-opacity": { 283 | type: "percent" 284 | }, 285 | "scale-grad-color": { 286 | type: "color" 287 | }, 288 | "scale-end-color": { 289 | type: "color" 290 | }, 291 | "scale-border-width": { 292 | type: "int" 293 | }, 294 | "scale-end-border-width": { 295 | type: "int" 296 | }, 297 | "scale-end-line-width": { 298 | type: "int" 299 | }, 300 | "font-family": { 301 | alias: "text-font" 302 | }, 303 | "border": { 304 | type: "group", 305 | groupElements: [ 306 | "border-width", 307 | "border-color" 308 | ] 309 | } 310 | } 311 | 312 | function getRealPropertyName(propertyName) { 313 | while(true) { 314 | if(convertable_properties[propertyName] == undefined) { 315 | throw new Error("Unknown LVGL CSS property: " + propertyName); 316 | } 317 | if(convertable_properties[propertyName].alias != undefined) { 318 | propertyName = convertable_properties[propertyName].alias; 319 | continue; 320 | } 321 | return propertyName; 322 | } 323 | } 324 | 325 | function getLvName(propertyName) { 326 | const convertPropertyObj = convertable_properties[propertyName]; 327 | if(typeof convertPropertyObj.lv_name != 'undefined') 328 | return convertPropertyObj.lv_name; 329 | return propertyName.toUpperCase().replace(/-/g, "_"); 330 | } 331 | function validateState(state) { 332 | var validStates = [ 333 | "DEFAULT", 334 | "CHECKED", 335 | "FOCUSED", 336 | "EDITED", 337 | "HOVERED", 338 | "PRESSED", 339 | "DISABLED" 340 | ]; 341 | return validStates.includes(state.toUpperCase()); 342 | } 343 | 344 | function micropythonState(state) { 345 | return "lv." + state.substr(3).toUpperCase().replace(/_/g, '.'); 346 | } 347 | 348 | function get_lv_c_value(propertyName, value, type, className, state, lang) { 349 | if(typeof convertable_properties[propertyName] == 'undefined') 350 | throw new Error("Unknown property"); 351 | var valueType = convertable_properties[propertyName].type; 352 | if(valueType == "color") { 353 | let rgba = parseCSSColor(value); 354 | let color_make; 355 | if(lang == "c") 356 | color_make = "LV_COLOR_MAKE"; 357 | else if(lang == "micropython") 358 | color_make = "lv.color_make"; 359 | else 360 | throw new Error("Unknown language: " + lang); 361 | var color = `${color_make}(${rgba[0]}, ${rgba[1]}, ${rgba[2]})`; 362 | if(typeof convertable_properties[propertyName].alpha != 'undefined') { 363 | if(lang == "c") 364 | color += `); lv_style_set_${convertable_properties[propertyName].alpha}(&lv_style_${type == "tag" ? "css_wid_" : ""}${className}, ${state.toUpperCase()}, ${Math.round(rgba[3] * 255)}`; 365 | else if(lang == "micropython") 366 | color += `)\nstyle_${type == "tag" ? "css_wid_" : ""}${className}.set_${convertable_properties[propertyName].alpha}(${micropythonState(state)}, ${Math.round(rgba[3] * 255)}`; 367 | } 368 | return color; 369 | } else if(valueType == "number" || valueType == "int") { 370 | var n = parseInt(value); 371 | if(isNaN(n)) 372 | throw new Error("Not a number"); 373 | return n; 374 | } else if(valueType == "percent") { 375 | var n = parseFloat(value); 376 | if(isNaN(n)) 377 | throw new Error("Not a percent or number"); 378 | if(value.endsWith("%")) 379 | n /= 100; 380 | if(n < 0 || n > 1) 381 | throw new Error("Not a number between 0 and 1"); 382 | return Math.round(n * 255); 383 | } else if(valueType == "bool") { 384 | if(value == "true" || value == "yes") 385 | return true; 386 | else if(value == "false" || value == "no") 387 | return false; 388 | throw new Error("Not a boolean value"); 389 | } else if(valueType == "enum_single") { 390 | if(!Object.keys(convertable_properties[propertyName].enumValues).includes(value)) { 391 | throw new Error(`'${value}': not a valid value`); 392 | } 393 | return convertable_properties[propertyName].enumValues[value]; 394 | } else if(valueType == "enum_list") { 395 | const value_list = value.split(','); 396 | let finalValue = ""; 397 | for(let value of value_list) { 398 | value = value.trim(); 399 | if(!Object.keys(convertable_properties[propertyName].enumValues).includes(value)) { 400 | throw new Error(`'${value}': not a valid value`); 401 | } 402 | finalValue += `|${convertable_properties[propertyName].enumValues[value]}`; 403 | } 404 | return finalValue.substr(1); 405 | } else if(valueType == "address") { 406 | return "&" + value; 407 | } else 408 | throw new Error("Unable to handle value type '" + valueType + "'"); 409 | } 410 | function flattenStates(states) { 411 | return states.map(state => `LV_STATE_${state.toUpperCase()}`).join(" | "); 412 | } 413 | function processProperty(propertyName, realValue, type, name, states, part, lang) { 414 | let styleObj = null; 415 | if(type == "class") { 416 | styleObj = classes[name] || {}; 417 | classes[name] = styleObj; 418 | } else if(type == "tag") { 419 | name = name + "_part_" + part; 420 | styleObj = objectTypeStyles[name] || {}; 421 | objectTypeStyles[name] = styleObj; 422 | } else 423 | throw new Error("Unexpected selector type"); 424 | propertyName = getRealPropertyName(propertyName); 425 | if(convertable_properties[propertyName].type == "group") { 426 | /* Groups must be broken down into individual properties */ 427 | const groupStack = Array.from(convertable_properties[propertyName].groupElements).reverse(); 428 | const values = realValue.split(" ").reverse(); 429 | while(groupStack.length > 0) { 430 | var value = values.pop(); 431 | do { 432 | try { 433 | var pn = groupStack.pop(); 434 | processProperty(pn, value, type, name, states, part, lang); 435 | break; 436 | } catch(e) { 437 | console.error(e); 438 | } 439 | } while(groupStack.length > 0); 440 | } 441 | return; 442 | } 443 | 444 | const lv_name = getLvName(propertyName); 445 | const valueObj = styleObj[convertable_properties[propertyName]] || {}; 446 | styleObj[lv_name] = valueObj; 447 | var state = flattenStates(states); 448 | try { 449 | styleObj[lv_name][state] = get_lv_c_value(propertyName, realValue, type, name, state, lang); 450 | } catch(e) { 451 | console.error(`Error while parsing property '${propertyName}': ` + e); 452 | throw new Error(); 453 | } 454 | } 455 | /** 456 | * Converts a style sheet to a set of LittlevGL style rules. 457 | * @param {String} css_string CSS stylesheet to convert 458 | */ 459 | function convert(watcher, css_string, lang) { 460 | const csso = cssom.parse(css_string); 461 | for(const rule of csso.cssRules) { 462 | if(rule instanceof cssom.CSSStyleRule) { 463 | var parsedObj = cssSelectorParser.parse(rule.selectorText); 464 | if(typeof parsedObj.selectors == 'undefined') 465 | parsedObj = { selectors: [ parsedObj ]}; 466 | for(const selector of parsedObj.selectors) { 467 | let name = null; 468 | let hasTagName = typeof selector.rule.tagName != 'undefined'; 469 | let hasClassName = typeof selector.rule.classNames != 'undefined'; 470 | if(hasTagName && hasClassName) 471 | throw new Error("Selectors must only have a tag or class name, not both."); 472 | else if(hasTagName) 473 | name = selector.rule.tagName; 474 | else if(hasClassName) 475 | name = selector.rule.classNames[0]; 476 | else 477 | throw new Error("Unexpected selector type"); 478 | let part = "main"; 479 | let states = [ "default" ]; 480 | if(selector.rule.pseudos) { 481 | for(var i = 0; i < selector.rule.pseudos.length; i++) { 482 | var state = selector.rule.pseudos[i].name; 483 | if(state.trim().length == 0) 484 | continue; 485 | if(validateState(state)) { 486 | states.push(state); 487 | } else if(part == "main") 488 | part = state; 489 | else 490 | throw new Error("Unexpected state/part: " + state); 491 | } 492 | 493 | } 494 | if(part != "main" && !hasTagName) 495 | throw new Error("Parts can only be specified when using a tag selector (i.e. btn) not a class selector"); 496 | 497 | for(var i = 0; i < rule.style.length; i++) { 498 | processProperty(rule.style[i], rule.style[rule.style[i]], hasClassName ? "class" : "tag", name, states, part, lang); 499 | } 500 | } 501 | 502 | } else if(rule instanceof cssom.CSSImportRule) { 503 | fs.accessSync(rule.href, fs.constants.R_OK); 504 | if(watcher != null) 505 | watcher.add(rule.href); 506 | convert(watcher, fs.readFileSync(rule.href).toString(), lang); 507 | } 508 | } 509 | } 510 | 511 | function langComment(lang, comment) { 512 | if(lang == "c") 513 | return `/* ${comment} */\n`; 514 | else if(lang == "micropython") 515 | return `# ${comment}\n`; 516 | else 517 | throw new Error("Unknown language"); 518 | } 519 | 520 | function styleGen(lang, lineEnding, obj) { 521 | var file = ""; 522 | for(var cls in obj) { 523 | var clsTerm = cls; 524 | if(obj == classes) 525 | file += "\n" + langComment(lang, `.${cls}`); 526 | else { 527 | file += "\n" + langComment(lang, cls); 528 | clsTerm = "css_wid_" + cls; 529 | } 530 | if(lang == "c") 531 | file += ` lv_style_init(&lv_style_${clsTerm});\n`; 532 | else if(lang == "micropython") 533 | file += `style_${clsTerm} = lv.style_t()\n`; 534 | Object.keys(obj[cls]).forEach((key) => { 535 | const valueObj = obj[cls][key]; 536 | Object.keys(valueObj).forEach(state => { 537 | if(lang == "micropython") 538 | file += `style_${clsTerm}.set_${key.toLowerCase()}(${micropythonState(state)}, ${valueObj[state]})${lineEnding}\n`; 539 | else if(lang == "c") 540 | file += ` lv_style_set_${key.toLowerCase()}(&lv_style_${clsTerm}, ${state.toUpperCase()}, ${valueObj[state]})${lineEnding}\n`; 541 | }); 542 | 543 | }); 544 | } 545 | return file; 546 | } 547 | 548 | /** 549 | * Write a code file containing the styles. 550 | * 551 | */ 552 | function finalize(lang) { 553 | var file = ""; 554 | var lineEnding = ""; 555 | if(lang == "c") 556 | lineEnding = ";"; 557 | 558 | file += langComment(lang, "Autogenerated file; do not edit."); 559 | if(lang == "c") { 560 | file += "#include \n\n"; 561 | } else if(lang == "micropython") { 562 | file += "import lvgl as lv\n\n"; 563 | } 564 | 565 | file += "\n\n"; 566 | file += "# Initialize a custom theme*/\n"; 567 | file += `class style_css_theme(lv.theme_t): 568 | def __init__(self): 569 | super().__init__() 570 | 571 | # This theme is based on active theme 572 | base_theme = lv.theme_get_act() 573 | self.copy(base_theme) 574 | 575 | # This theme will be applied only after base theme is applied 576 | self.set_base(base_theme) 577 | 578 | # Set the "apply" callback of this theme to our custom callback 579 | self.set_apply_cb(self.apply) 580 | 581 | # Activate this theme 582 | self.set_act() 583 | 584 | def apply(self, theme, obj, name): 585 | style_css_apply_cb(theme, obj, name)\n\ntheme = style_css_theme()\n`; 586 | 587 | file += styleGen(lang, lineEnding, classes); 588 | file += styleGen(lang, lineEnding, objectTypeStyles); 589 | 590 | if(lang == "c") 591 | file += "}\n"; 592 | 593 | if(lang == "c") 594 | file += "\nstatic void lv_style_css_apply_cb(lv_theme_t * th, lv_obj_t * obj, lv_theme_style_t name) {\n"; 595 | else if(lang == "micropython") 596 | file += "\ndef style_css_apply_cb(th, obj, name):\n"; 597 | if(lang == "c") { 598 | file += " lv_style_list_t * list;\n\n"; 599 | file += " switch(name) {\n"; 600 | } 601 | 602 | var widgetParts = {}; 603 | var widgetNames = new Set(Object.keys(objectTypeStyles).map(wid => { 604 | var words = wid.split("_part_"); 605 | var widgetName = words[0]; 606 | var widgetPartObj = widgetParts[widgetName] || []; 607 | widgetParts[widgetName] = widgetPartObj; 608 | widgetPartObj.push(words[1]); 609 | return widgetName; 610 | })); 611 | widgetNames.forEach(wid => { 612 | if(lang == "c") 613 | file += ` case LV_THEME_${wid.toUpperCase()}:\n`; 614 | else if(lang == "micropython") 615 | file += ` if name == lv.THEME.${wid.toUpperCase()}:\n`; 616 | widgetParts[wid].forEach(part => { 617 | if(lang == "c") { 618 | file += ` list = lv_obj_get_style_list(obj, LV_${wid.toUpperCase()}_PART_${part.toUpperCase()});\n`; 619 | file += ` _lv_style_list_add_style(list, &lv_style_css_wid_${wid}_part_${part});\n`; 620 | file += ` break;\n`; 621 | } else if(lang == "micropython") 622 | file += ` obj.add_style(lv.${wid}.PART.${part.toUpperCase()}, style_css_wid_${wid}_part_${part})\n`; 623 | }); 624 | }); 625 | if(lang == "c") { 626 | file += ` default:\n`; 627 | file += ` break;\n`; 628 | file += " }\n"; 629 | file += "}\n"; 630 | } 631 | 632 | return file; 633 | } 634 | module.exports = { convert: convert, finalize: finalize }; -------------------------------------------------------------------------------- /css-color-parser.js: -------------------------------------------------------------------------------- 1 | // (c) Dean McNamee , 2012. 2 | // 3 | // https://github.com/deanm/css-color-parser-js 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to 7 | // deal in the Software without restriction, including without limitation the 8 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | // sell copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | // IN THE SOFTWARE. 22 | 23 | // http://www.w3.org/TR/css3-color/ 24 | var kCSSColorTable = { 25 | "transparent": [0,0,0,0], "aliceblue": [240,248,255,1], 26 | "antiquewhite": [250,235,215,1], "aqua": [0,255,255,1], 27 | "aquamarine": [127,255,212,1], "azure": [240,255,255,1], 28 | "beige": [245,245,220,1], "bisque": [255,228,196,1], 29 | "black": [0,0,0,1], "blanchedalmond": [255,235,205,1], 30 | "blue": [0,0,255,1], "blueviolet": [138,43,226,1], 31 | "brown": [165,42,42,1], "burlywood": [222,184,135,1], 32 | "cadetblue": [95,158,160,1], "chartreuse": [127,255,0,1], 33 | "chocolate": [210,105,30,1], "coral": [255,127,80,1], 34 | "cornflowerblue": [100,149,237,1], "cornsilk": [255,248,220,1], 35 | "crimson": [220,20,60,1], "cyan": [0,255,255,1], 36 | "darkblue": [0,0,139,1], "darkcyan": [0,139,139,1], 37 | "darkgoldenrod": [184,134,11,1], "darkgray": [169,169,169,1], 38 | "darkgreen": [0,100,0,1], "darkgrey": [169,169,169,1], 39 | "darkkhaki": [189,183,107,1], "darkmagenta": [139,0,139,1], 40 | "darkolivegreen": [85,107,47,1], "darkorange": [255,140,0,1], 41 | "darkorchid": [153,50,204,1], "darkred": [139,0,0,1], 42 | "darksalmon": [233,150,122,1], "darkseagreen": [143,188,143,1], 43 | "darkslateblue": [72,61,139,1], "darkslategray": [47,79,79,1], 44 | "darkslategrey": [47,79,79,1], "darkturquoise": [0,206,209,1], 45 | "darkviolet": [148,0,211,1], "deeppink": [255,20,147,1], 46 | "deepskyblue": [0,191,255,1], "dimgray": [105,105,105,1], 47 | "dimgrey": [105,105,105,1], "dodgerblue": [30,144,255,1], 48 | "firebrick": [178,34,34,1], "floralwhite": [255,250,240,1], 49 | "forestgreen": [34,139,34,1], "fuchsia": [255,0,255,1], 50 | "gainsboro": [220,220,220,1], "ghostwhite": [248,248,255,1], 51 | "gold": [255,215,0,1], "goldenrod": [218,165,32,1], 52 | "gray": [128,128,128,1], "green": [0,128,0,1], 53 | "greenyellow": [173,255,47,1], "grey": [128,128,128,1], 54 | "honeydew": [240,255,240,1], "hotpink": [255,105,180,1], 55 | "indianred": [205,92,92,1], "indigo": [75,0,130,1], 56 | "ivory": [255,255,240,1], "khaki": [240,230,140,1], 57 | "lavender": [230,230,250,1], "lavenderblush": [255,240,245,1], 58 | "lawngreen": [124,252,0,1], "lemonchiffon": [255,250,205,1], 59 | "lightblue": [173,216,230,1], "lightcoral": [240,128,128,1], 60 | "lightcyan": [224,255,255,1], "lightgoldenrodyellow": [250,250,210,1], 61 | "lightgray": [211,211,211,1], "lightgreen": [144,238,144,1], 62 | "lightgrey": [211,211,211,1], "lightpink": [255,182,193,1], 63 | "lightsalmon": [255,160,122,1], "lightseagreen": [32,178,170,1], 64 | "lightskyblue": [135,206,250,1], "lightslategray": [119,136,153,1], 65 | "lightslategrey": [119,136,153,1], "lightsteelblue": [176,196,222,1], 66 | "lightyellow": [255,255,224,1], "lime": [0,255,0,1], 67 | "limegreen": [50,205,50,1], "linen": [250,240,230,1], 68 | "magenta": [255,0,255,1], "maroon": [128,0,0,1], 69 | "mediumaquamarine": [102,205,170,1], "mediumblue": [0,0,205,1], 70 | "mediumorchid": [186,85,211,1], "mediumpurple": [147,112,219,1], 71 | "mediumseagreen": [60,179,113,1], "mediumslateblue": [123,104,238,1], 72 | "mediumspringgreen": [0,250,154,1], "mediumturquoise": [72,209,204,1], 73 | "mediumvioletred": [199,21,133,1], "midnightblue": [25,25,112,1], 74 | "mintcream": [245,255,250,1], "mistyrose": [255,228,225,1], 75 | "moccasin": [255,228,181,1], "navajowhite": [255,222,173,1], 76 | "navy": [0,0,128,1], "oldlace": [253,245,230,1], 77 | "olive": [128,128,0,1], "olivedrab": [107,142,35,1], 78 | "orange": [255,165,0,1], "orangered": [255,69,0,1], 79 | "orchid": [218,112,214,1], "palegoldenrod": [238,232,170,1], 80 | "palegreen": [152,251,152,1], "paleturquoise": [175,238,238,1], 81 | "palevioletred": [219,112,147,1], "papayawhip": [255,239,213,1], 82 | "peachpuff": [255,218,185,1], "peru": [205,133,63,1], 83 | "pink": [255,192,203,1], "plum": [221,160,221,1], 84 | "powderblue": [176,224,230,1], "purple": [128,0,128,1], 85 | "rebeccapurple": [102,51,153,1], 86 | "red": [255,0,0,1], "rosybrown": [188,143,143,1], 87 | "royalblue": [65,105,225,1], "saddlebrown": [139,69,19,1], 88 | "salmon": [250,128,114,1], "sandybrown": [244,164,96,1], 89 | "seagreen": [46,139,87,1], "seashell": [255,245,238,1], 90 | "sienna": [160,82,45,1], "silver": [192,192,192,1], 91 | "skyblue": [135,206,235,1], "slateblue": [106,90,205,1], 92 | "slategray": [112,128,144,1], "slategrey": [112,128,144,1], 93 | "snow": [255,250,250,1], "springgreen": [0,255,127,1], 94 | "steelblue": [70,130,180,1], "tan": [210,180,140,1], 95 | "teal": [0,128,128,1], "thistle": [216,191,216,1], 96 | "tomato": [255,99,71,1], "turquoise": [64,224,208,1], 97 | "violet": [238,130,238,1], "wheat": [245,222,179,1], 98 | "white": [255,255,255,1], "whitesmoke": [245,245,245,1], 99 | "yellow": [255,255,0,1], "yellowgreen": [154,205,50,1]} 100 | 101 | function clamp_css_byte(i) { // Clamp to integer 0 .. 255. 102 | i = Math.round(i); // Seems to be what Chrome does (vs truncation). 103 | return i < 0 ? 0 : i > 255 ? 255 : i; 104 | } 105 | 106 | function clamp_css_float(f) { // Clamp to float 0.0 .. 1.0. 107 | return f < 0 ? 0 : f > 1 ? 1 : f; 108 | } 109 | 110 | function parse_css_int(str) { // int or percentage. 111 | if (str[str.length - 1] === '%') 112 | return clamp_css_byte(parseFloat(str) / 100 * 255); 113 | return clamp_css_byte(parseInt(str)); 114 | } 115 | 116 | function parse_css_float(str) { // float or percentage. 117 | if (str[str.length - 1] === '%') 118 | return clamp_css_float(parseFloat(str) / 100); 119 | return clamp_css_float(parseFloat(str)); 120 | } 121 | 122 | function css_hue_to_rgb(m1, m2, h) { 123 | if (h < 0) h += 1; 124 | else if (h > 1) h -= 1; 125 | 126 | if (h * 6 < 1) return m1 + (m2 - m1) * h * 6; 127 | if (h * 2 < 1) return m2; 128 | if (h * 3 < 2) return m1 + (m2 - m1) * (2/3 - h) * 6; 129 | return m1; 130 | } 131 | 132 | function parseCSSColor(css_str) { 133 | // Remove all whitespace, not compliant, but should just be more accepting. 134 | var str = css_str.replace(/ /g, '').toLowerCase(); 135 | 136 | // Color keywords (and transparent) lookup. 137 | if (str in kCSSColorTable) return kCSSColorTable[str].slice(); // dup. 138 | 139 | // #abc and #abc123 syntax. 140 | if (str[0] === '#') { 141 | if (str.length === 4) { 142 | var iv = parseInt(str.substr(1), 16); // TODO(deanm): Stricter parsing. 143 | if (!(iv >= 0 && iv <= 0xfff)) return null; // Covers NaN. 144 | return [((iv & 0xf00) >> 4) | ((iv & 0xf00) >> 8), 145 | (iv & 0xf0) | ((iv & 0xf0) >> 4), 146 | (iv & 0xf) | ((iv & 0xf) << 4), 147 | 1]; 148 | } else if (str.length === 7) { 149 | var iv = parseInt(str.substr(1), 16); // TODO(deanm): Stricter parsing. 150 | if (!(iv >= 0 && iv <= 0xffffff)) return null; // Covers NaN. 151 | return [(iv & 0xff0000) >> 16, 152 | (iv & 0xff00) >> 8, 153 | iv & 0xff, 154 | 1]; 155 | } 156 | 157 | return null; 158 | } 159 | 160 | var op = str.indexOf('('), ep = str.indexOf(')'); 161 | if (op !== -1 && ep + 1 === str.length) { 162 | var fname = str.substr(0, op); 163 | var params = str.substr(op+1, ep-(op+1)).split(','); 164 | var alpha = 1; // To allow case fallthrough. 165 | switch (fname) { 166 | case 'rgba': 167 | if (params.length !== 4) return null; 168 | alpha = parse_css_float(params.pop()); 169 | // Fall through. 170 | case 'rgb': 171 | if (params.length !== 3) return null; 172 | return [parse_css_int(params[0]), 173 | parse_css_int(params[1]), 174 | parse_css_int(params[2]), 175 | alpha]; 176 | case 'hsla': 177 | if (params.length !== 4) return null; 178 | alpha = parse_css_float(params.pop()); 179 | // Fall through. 180 | case 'hsl': 181 | if (params.length !== 3) return null; 182 | var h = (((parseFloat(params[0]) % 360) + 360) % 360) / 360; // 0 .. 1 183 | // NOTE(deanm): According to the CSS spec s/l should only be 184 | // percentages, but we don't bother and let float or percentage. 185 | var s = parse_css_float(params[1]); 186 | var l = parse_css_float(params[2]); 187 | var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s; 188 | var m1 = l * 2 - m2; 189 | return [clamp_css_byte(css_hue_to_rgb(m1, m2, h+1/3) * 255), 190 | clamp_css_byte(css_hue_to_rgb(m1, m2, h) * 255), 191 | clamp_css_byte(css_hue_to_rgb(m1, m2, h-1/3) * 255), 192 | alpha]; 193 | default: 194 | return null; 195 | } 196 | } 197 | 198 | return null; 199 | } 200 | 201 | try { exports.parseCSSColor = parseCSSColor } catch(e) { } -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const argv = require('yargs') 3 | .usage('Usage: $0 [optional args] -o ') 4 | .option('outfile', { 5 | alias: 'o', 6 | type: 'string', 7 | nargs: 1, 8 | demandOption: "Must specify output file", 9 | description: 'File to write generated code to', 10 | }) 11 | .option('watch', { 12 | alias: 'w', 13 | type: 'boolean', 14 | description: 'Enable daemon mode (watches for changes to CSS)' 15 | }) 16 | .option('lang', { 17 | description: 'Output code language', 18 | alias: 'l', 19 | type: 'string', 20 | choices: [ "c", "micropython" ], 21 | default: "c" 22 | }) 23 | .demandCommand(1, "Must specify input file") 24 | .argv; 25 | const fs = require('fs'); 26 | const convert = require('./convert'); 27 | 28 | 29 | var file = argv._[0]; 30 | 31 | const doConvert = (watcher) => { 32 | console.log("Converting..."); 33 | convert.convert(watcher, fs.readFileSync(file).toString(), argv.lang); 34 | fs.writeFileSync(argv.outfile, convert.finalize(argv.lang)); 35 | console.log("Finished converting."); 36 | }; 37 | if(argv.watch) { 38 | const chokidar = require('chokidar'); 39 | const watcher = chokidar.watch(file, { 40 | ignored: /(^|[\/\\])\../, // ignore dotfiles 41 | persistent: true 42 | }); 43 | doConvert(watcher); 44 | watcher.on('ready', () => console.log('Ready for changes')); 45 | watcher.on('change', () => { 46 | watcher.unwatch('*'); 47 | watcher.add(file); 48 | doConvert(watcher); 49 | }); 50 | } else { 51 | try { 52 | doConvert(); 53 | } catch(e) { 54 | console.error("Error: could not convert file '" + file + "': "); 55 | console.error(e); 56 | process.exit(1); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lv_css", 3 | "version": "1.0.0", 4 | "description": "CSS-like compiler for LittlevGL styles", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "embeddedt", 10 | "license": "MIT", 11 | "dependencies": { 12 | "chokidar": "^3.4.2", 13 | "css-selector-parser": "^1.4.1", 14 | "cssom": "^0.4.4", 15 | "yargs": "^15.3.1" 16 | } 17 | } 18 | --------------------------------------------------------------------------------