@@ -8,6 +8,13 @@ import {
88 LovelaceCard ,
99 OverlayConfig ,
1010} from "../types" ;
11+ import {
12+ evaluateCssTemplates ,
13+ evaluateOverlayContent ,
14+ extractEntitiesFromTemplate ,
15+ } from "../template" ;
16+ import { detectAllGridAreas , formatAreaName , ensureSectionsForAllAreas } from "../grid-utils" ;
17+ import { sectionConfigToYaml , parseYaml } from "../yaml" ;
1118
1219class GridLayout extends LitElement {
1320 // ── External properties set by HA ───────────────────────────────────────
@@ -437,25 +444,17 @@ class GridLayout extends LitElement {
437444
438445 _extractEntitiesFromTemplates ( ) {
439446 this . _trackedEntities . clear ( ) ;
440- const extract = ( str : string ) => {
441- if ( ! str ) return ;
442- for ( const re of [
443- / i s _ s t a t e \( [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / g,
444- / s t a t e s \( [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / g,
445- / s t a t e _ a t t r \( [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / g,
446- ] ) {
447- let m ;
448- while ( ( m = re . exec ( str ) ) !== null ) this . _trackedEntities . add ( m [ 1 ] ) ;
449- }
447+ const addAll = ( str : string ) => {
448+ for ( const eid of extractEntitiesFromTemplate ( str ) ) this . _trackedEntities . add ( eid ) ;
450449 } ;
451- extract ( this . _config . layout ?. custom_css ) ;
452- extract ( this . _config . layout ?. background_image ) ;
450+ addAll ( this . _config . layout ?. custom_css ) ;
451+ addAll ( this . _config . layout ?. background_image ) ;
453452 const overlays = this . _config . layout ?. overlays ;
454453 if ( overlays ) {
455454 for ( const overlay of overlays ) {
456455 if ( overlay . entity ) this . _trackedEntities . add ( overlay . entity ) ;
457- extract ( overlay . custom_css ) ;
458- extract ( overlay . content ) ;
456+ addAll ( overlay . custom_css ) ;
457+ addAll ( overlay . content ) ;
459458 }
460459 }
461460 }
@@ -464,38 +463,9 @@ class GridLayout extends LitElement {
464463 if ( ! css || ! this . hass ) return css ;
465464 if ( ! css . includes ( "{{" ) && ! css . includes ( "{%" ) ) return css ;
466465 if ( css === this . _lastEvaluatedCss ) return css ;
467- try {
468- let out = css ;
469- out = out . replace (
470- / \{ % \s * i f \s + i s _ s t a t e \( [ ' " ] ( [ ^ ' " ] + ) [ ' " ] , \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] \) \s * % \} ( [ \s \S ] * ?) \{ % \s * e n d i f \s * % \} / g,
471- ( _ , eid , expected , content ) => {
472- this . _trackedEntities . add ( eid ) ;
473- return this . hass . states [ eid ] ?. state === expected ? content : "" ;
474- }
475- ) ;
476- out = out . replace (
477- / \{ % \s * i f \s + n o t \s + i s _ s t a t e \( [ ' " ] ( [ ^ ' " ] + ) [ ' " ] , \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] \) \s * % \} ( [ \s \S ] * ?) \{ % \s * e n d i f \s * % \} / g,
478- ( _ , eid , expected , content ) => {
479- this . _trackedEntities . add ( eid ) ;
480- return this . hass . states [ eid ] ?. state !== expected ? content : "" ;
481- }
482- ) ;
483- out = out . replace ( / \{ \{ \s * s t a t e s \( [ ' " ] ( [ ^ ' " ] + ) [ ' " ] \) \s * \} \} / g, ( m , eid ) => {
484- this . _trackedEntities . add ( eid ) ;
485- return this . hass . states [ eid ] ?. state ?? m ;
486- } ) ;
487- out = out . replace ( / \{ \{ \s * s t a t e _ a t t r \( [ ' " ] ( [ ^ ' " ] + ) [ ' " ] , \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] \) \s * \} \} / g,
488- ( m , eid , attr ) => {
489- this . _trackedEntities . add ( eid ) ;
490- const val = this . hass . states [ eid ] ?. attributes ?. [ attr ] ;
491- return val !== undefined ? val : m ;
492- }
493- ) ;
494- this . _lastEvaluatedCss = out ;
495- return out ;
496- } catch {
497- return css ;
498- }
466+ const out = evaluateCssTemplates ( css , this . hass . states , this . _trackedEntities ) ;
467+ this . _lastEvaluatedCss = out ;
468+ return out ;
499469 }
500470
501471 // ── Overlays ─────────────────────────────────────────────────────────
@@ -648,20 +618,8 @@ class GridLayout extends LitElement {
648618 }
649619
650620 _evaluateOverlayContent ( content : string ) : string {
651- if ( ! content || ! this . hass ) return content || "" ;
652- if ( ! content . includes ( "{{" ) ) return content ;
653- let out = content ;
654- out = out . replace ( / \{ \{ \s * s t a t e s \( [ ' " ] ( [ ^ ' " ] + ) [ ' " ] \) \s * \} \} / g, ( m , eid ) => {
655- return this . hass . states [ eid ] ?. state ?? m ;
656- } ) ;
657- out = out . replace (
658- / \{ \{ \s * s t a t e _ a t t r \( [ ' " ] ( [ ^ ' " ] + ) [ ' " ] , \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] \) \s * \} \} / g,
659- ( m , eid , attr ) => {
660- const val = this . hass . states [ eid ] ?. attributes ?. [ attr ] ;
661- return val !== undefined ? String ( val ) : m ;
662- }
663- ) ;
664- return out ;
621+ if ( ! this . hass ) return content || "" ;
622+ return evaluateOverlayContent ( content , this . hass . states ) ;
665623 }
666624
667625 // ── Background image ─────────────────────────────────────────────────────
@@ -866,31 +824,15 @@ class GridLayout extends LitElement {
866824 }
867825
868826 _detectAllGridAreas ( ) : string [ ] {
869- const areas = this . _config . layout ?. [ "grid-template-areas" ] ;
870- if ( ! areas ) return [ ] ;
871- const found = new Set < string > ( ) ;
872- for ( const line of areas . split ( "\n" ) . map ( l => l . trim ( ) ) . filter ( Boolean ) ) {
873- for ( const area of line . replace ( / [ ' " ] / g, "" ) . split ( / \s + / ) ) {
874- if ( area !== "." && area !== "" ) found . add ( area ) ;
875- }
876- }
877- return Array . from ( found ) ;
827+ return detectAllGridAreas ( this . _config . layout ?. [ "grid-template-areas" ] ) ;
878828 }
879829
880830 _ensureSectionsForAllAreas ( allGridAreas : string [ ] ) : any [ ] {
881- if ( ! allGridAreas . length ) return this . _config . sections || [ ] ;
882- const sections = [ ...( this . _config . sections || [ ] ) ] ;
883- const existing = new Set ( sections . map ( s => s . grid_area ) . filter ( Boolean ) ) ;
884- for ( const area of allGridAreas ) {
885- if ( ! existing . has ( area ) ) {
886- sections . push ( { type : "grid" , title : this . _formatAreaName ( area ) , grid_area : area , cards : [ ] } ) ;
887- }
888- }
889- return sections ;
831+ return ensureSectionsForAllAreas ( allGridAreas , this . _config . sections || [ ] ) ;
890832 }
891833
892834 _formatAreaName ( area : string ) : string {
893- return area . split ( / [ - _ ] / ) . map ( w => w . charAt ( 0 ) . toUpperCase ( ) + w . slice ( 1 ) ) . join ( " " ) ;
835+ return formatAreaName ( area ) ;
894836 }
895837
896838 _createLooseCardsContainer ( ) : HTMLElement {
@@ -998,83 +940,11 @@ class GridLayout extends LitElement {
998940 }
999941
1000942 _sectionConfigToYaml ( config : any ) : string {
1001- const SKIP = new Set ( [ "cards" ] ) ;
1002- const serialize = ( obj : any , indent : number ) : string => {
1003- const pad = " " . repeat ( indent ) ;
1004- const lines : string [ ] = [ ] ;
1005- for ( const [ key , value ] of Object . entries ( obj ) ) {
1006- if ( indent === 0 && SKIP . has ( key ) ) continue ;
1007- if ( value === undefined || value === null ) continue ;
1008- if ( Array . isArray ( value ) ) {
1009- lines . push ( `${ pad } ${ key } :` ) ;
1010- for ( const item of value ) {
1011- if ( typeof item === "object" && item !== null ) {
1012- const inner = serialize ( item , indent + 2 ) . replace ( / ^ \s + / , "" ) ;
1013- lines . push ( `${ pad } - ${ inner } ` ) ;
1014- } else {
1015- lines . push ( `${ pad } - ${ this . _yamlScalar ( item , indent + 1 ) } ` ) ;
1016- }
1017- }
1018- } else if ( typeof value === "object" ) {
1019- lines . push ( `${ pad } ${ key } :` ) ;
1020- lines . push ( serialize ( value , indent + 1 ) ) ;
1021- } else if ( typeof value === "string" && value . includes ( "\n" ) ) {
1022- // Multiline string: use YAML block literal style (|)
1023- const innerPad = " " . repeat ( indent + 1 ) ;
1024- lines . push ( `${ pad } ${ key } : |` ) ;
1025- for ( const sline of value . split ( "\n" ) ) {
1026- lines . push ( sline === "" ? "" : `${ innerPad } ${ sline } ` ) ;
1027- }
1028- } else {
1029- lines . push ( `${ pad } ${ key } : ${ this . _yamlScalar ( value , indent ) } ` ) ;
1030- }
1031- }
1032- return lines . join ( "\n" ) ;
1033- } ;
1034- return serialize ( config , 0 ) ;
1035- }
1036-
1037- _yamlScalar ( value : any , _indent : number = 0 ) : string {
1038- if ( typeof value === "string" ) {
1039- if ( value === "" || value === "true" || value === "false" ||
1040- value === "null" || value === "~" ||
1041- / ^ [ \d . ] + $ / . test ( value ) || value . includes ( ":" ) ||
1042- value . includes ( "#" ) || value . includes ( "{" ) ||
1043- value . includes ( "[" ) || value . startsWith ( "'" ) ||
1044- value . startsWith ( '"' ) || value . startsWith ( "&" ) ||
1045- value . startsWith ( "*" ) ) {
1046- return `"${ value . replace ( / \\ / g, "\\\\" ) . replace ( / " / g, '\\"' ) } "` ;
1047- }
1048- return value ;
1049- }
1050- return String ( value ) ;
943+ return sectionConfigToYaml ( config ) ;
1051944 }
1052945
1053946 _parseYaml ( yaml : string ) : Record < string , any > | null {
1054- // Try HA's bundled js-yaml first
1055- try {
1056- const jsyaml = ( window as any ) . jsyaml ;
1057- if ( jsyaml ?. load ) return jsyaml . load ( yaml ) || { } ;
1058- } catch { /* fall through */ }
1059- // Fallback: simple line-based parser (flat keys only)
1060- try {
1061- const result : Record < string , any > = { } ;
1062- for ( const line of yaml . split ( "\n" ) ) {
1063- const trimmed = line . trim ( ) ;
1064- if ( ! trimmed || trimmed . startsWith ( "#" ) ) continue ;
1065- const colonIdx = trimmed . indexOf ( ":" ) ;
1066- if ( colonIdx < 0 ) continue ;
1067- const key = trimmed . slice ( 0 , colonIdx ) . trim ( ) ;
1068- let val : any = trimmed . slice ( colonIdx + 1 ) . trim ( ) ;
1069- if ( ( val . startsWith ( '"' ) && val . endsWith ( '"' ) ) || ( val . startsWith ( "'" ) && val . endsWith ( "'" ) ) ) {
1070- val = val . slice ( 1 , - 1 ) ;
1071- } else if ( val === "true" ) { val = true ; }
1072- else if ( val === "false" ) { val = false ; }
1073- else if ( val !== "" && ! isNaN ( Number ( val ) ) ) { val = Number ( val ) ; }
1074- if ( val !== "" ) result [ key ] = val ;
1075- }
1076- return result ;
1077- } catch { return null ; }
947+ return parseYaml ( yaml ) ;
1078948 }
1079949
1080950 _openSectionYamlEditor ( gridArea : string ) {
0 commit comments