@jxsuite/compiler 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/compiler",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Jx static HTML compiler, island detector, and site builder",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -37,8 +37,8 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@apidevtools/json-schema-ref-parser": "^15.3.5",
40
- "@jxsuite/parser": "^0.15.0",
41
- "@jxsuite/runtime": "^0.15.0",
40
+ "@jxsuite/parser": "^0.18.0",
41
+ "@jxsuite/runtime": "^0.18.0",
42
42
  "remark-gfm": "^4.0.1",
43
43
  "remark-stringify": "^11.0.0",
44
44
  "sharp": "^0.34.5",
package/src/shared.js CHANGED
@@ -488,10 +488,45 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
488
488
  // everything else on body. Project-level style is implicitly :root, so a
489
489
  // flat object like { "--bg": "#000", "margin": "0" } is the expected format.
490
490
  if (projectStyle && typeof projectStyle === "object") {
491
+ function emitProjectRules(
492
+ /** @type {string} */ selector,
493
+ /** @type {Record<string, any>} */ obj,
494
+ ) {
495
+ const props = toCSSText(/** @type {any} */ (obj));
496
+ if (props) rules.push(`${selector} { ${props} }`);
497
+ for (const [key, val] of Object.entries(obj)) {
498
+ if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
499
+ if (key.startsWith("@")) {
500
+ const query = key.startsWith("@--")
501
+ ? (mediaQueries[key.slice(1)] ?? key.slice(1))
502
+ : key.slice(1);
503
+ const mProps = toCSSText(/** @type {any} */ (val));
504
+ if (mProps) rules.push(`@media ${query} { ${selector} { ${mProps} } }`);
505
+ for (const [sel, sub] of Object.entries(/** @type {Record<string, any>} */ (val))) {
506
+ if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
507
+ if (sel.startsWith("@")) continue;
508
+ const resolved = sel.startsWith("&")
509
+ ? sel.replace("&", selector)
510
+ : sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
511
+ ? `${selector}${sel}`
512
+ : `${selector} ${sel}`;
513
+ const subProps = toCSSText(/** @type {any} */ (sub));
514
+ if (subProps) rules.push(`@media ${query} { ${resolved} { ${subProps} } }`);
515
+ }
516
+ continue;
517
+ }
518
+ const resolved = key.startsWith("&")
519
+ ? key.replace("&", selector)
520
+ : key.startsWith(":") || key.startsWith(".") || key.startsWith("[")
521
+ ? `${selector}${key}`
522
+ : `${selector} ${key}`;
523
+ emitProjectRules(resolved, val);
524
+ }
525
+ }
526
+
491
527
  for (const [key, val] of Object.entries(projectStyle)) {
492
528
  if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[")) {
493
- // Standalone selector (e.g. `.dark`, `:focus-visible`)
494
- rules.push(`${key} { ${toCSSText(/** @type {any} */ (val))} }`);
529
+ emitProjectRules(key, /** @type {any} */ (val));
495
530
  } else if (
496
531
  val !== null &&
497
532
  typeof val === "object" &&
@@ -499,10 +534,9 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
499
534
  !key.startsWith("@") &&
500
535
  !key.startsWith("--")
501
536
  ) {
502
- // Element selector with object value (e.g. `html`, `*`)
503
- rules.push(`${key} { ${toCSSText(/** @type {any} */ (val))} }`);
537
+ emitProjectRules(key, val);
504
538
  } else if (key.startsWith("@")) {
505
- // @media block
539
+ // @media block at top level
506
540
  const query = key.startsWith("@--")
507
541
  ? (mediaQueries[key.slice(1)] ?? key.slice(1))
508
542
  : key.slice(1);
@@ -541,6 +575,47 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
541
575
  return `<style>\n${rules.join("\n")}\n</style>`;
542
576
  }
543
577
 
578
+ /**
579
+ * Recursively emit CSS rules for a nested element selector.
580
+ *
581
+ * @param {string} selector
582
+ * @param {Record<string, any>} obj
583
+ * @param {string[]} rules
584
+ * @param {Record<string, any>} mediaQueries
585
+ */
586
+ function emitNestedElement(selector, obj, rules, mediaQueries) {
587
+ const props = toCSSText(/** @type {any} */ (obj));
588
+ if (props) rules.push(`${selector} { ${props} }`);
589
+ for (const [key, val] of Object.entries(obj)) {
590
+ if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
591
+ if (key.startsWith("@")) {
592
+ const query = key.startsWith("@--")
593
+ ? (mediaQueries[key.slice(1)] ?? key.slice(1))
594
+ : key.slice(1);
595
+ const mProps = toCSSText(/** @type {any} */ (val));
596
+ if (mProps) rules.push(`@media ${query} { ${selector} { ${mProps} } }`);
597
+ for (const [sel, sub] of Object.entries(/** @type {Record<string, any>} */ (val))) {
598
+ if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
599
+ if (sel.startsWith("@")) continue;
600
+ const resolved = sel.startsWith("&")
601
+ ? sel.replace("&", selector)
602
+ : sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
603
+ ? `${selector}${sel}`
604
+ : `${selector} ${sel}`;
605
+ const subProps = toCSSText(/** @type {any} */ (sub));
606
+ if (subProps) rules.push(`@media ${query} { ${resolved} { ${subProps} } }`);
607
+ }
608
+ continue;
609
+ }
610
+ const resolved = key.startsWith("&")
611
+ ? key.replace("&", selector)
612
+ : key.startsWith(":") || key.startsWith(".") || key.startsWith("[")
613
+ ? `${selector}${key}`
614
+ : `${selector} ${key}`;
615
+ emitNestedElement(resolved, val, rules, mediaQueries);
616
+ }
617
+ }
618
+
544
619
  /**
545
620
  * @param {any} def
546
621
  * @param {string[]} rules
@@ -590,34 +665,32 @@ export function collectStyles(
590
665
  }
591
666
 
592
667
  for (const [prop, val] of Object.entries(def.style)) {
668
+ if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
593
669
  if (prop.startsWith("@")) {
594
670
  const query = prop.startsWith("@--")
595
671
  ? (mediaQueries[prop.slice(1)] ?? prop.slice(1))
596
672
  : prop.slice(1);
597
673
  rules.push(`@media ${query} { ${selector} { ${toCSSText(/** @type {any} */ (val))} } }`);
598
674
  for (const [sel, nestedRules] of Object.entries(/** @type {Record<string, any>} */ (val))) {
599
- if (
600
- !(
601
- sel.startsWith(":") ||
602
- sel.startsWith(".") ||
603
- sel.startsWith("&") ||
604
- sel.startsWith("[")
605
- )
606
- )
675
+ if (nestedRules === null || typeof nestedRules !== "object" || Array.isArray(nestedRules))
607
676
  continue;
608
- const resolved = sel.startsWith("&") ? sel.replace("&", selector) : `${selector}${sel}`;
677
+ if (sel.startsWith("@")) continue;
678
+ const resolved = sel.startsWith("&")
679
+ ? sel.replace("&", selector)
680
+ : sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
681
+ ? `${selector}${sel}`
682
+ : `${selector} ${sel}`;
609
683
  rules.push(
610
684
  `@media ${query} { ${resolved} { ${toCSSText(/** @type {any} */ (nestedRules))} } }`,
611
685
  );
612
686
  }
613
- } else if (
614
- prop.startsWith(":") ||
615
- prop.startsWith(".") ||
616
- prop.startsWith("&") ||
617
- prop.startsWith("[")
618
- ) {
619
- const resolved = prop.startsWith("&") ? prop.replace("&", selector) : `${selector}${prop}`;
620
- rules.push(`${resolved} { ${toCSSText(/** @type {any} */ (val))} }`);
687
+ } else {
688
+ const resolved = prop.startsWith("&")
689
+ ? prop.replace("&", selector)
690
+ : prop.startsWith(":") || prop.startsWith(".") || prop.startsWith("[")
691
+ ? `${selector}${prop}`
692
+ : `${selector} ${prop}`;
693
+ emitNestedElement(resolved, val, rules, mediaQueries);
621
694
  }
622
695
  }
623
696
  }