@@ -2,7 +2,7 @@ import type { JsonValue } from 'type-fest'
22import type { ExecuteExpressionResult } from '@companion-app/shared/Expression/ExpressionResult.js'
33import type { HorizontalAlignment , VerticalAlignment } from '@companion-app/shared/Graphics/Util.js'
44import type { ControlLocation } from '@companion-app/shared/Model/Common.js'
5- import type { ExpressionOrValue } from '@companion-app/shared/Model/Options.js'
5+ import { isExpressionOrValue , type ExpressionOrValue } from '@companion-app/shared/Model/Options.js'
66import type { DrawImageBuffer } from '@companion-app/shared/Model/StyleModel.js'
77import {
88 stringifyVariableValue ,
@@ -77,7 +77,7 @@ export class ElementExpressionHelper<T> {
7777
7878 #getValue( propertyName : keyof T ) : ExpressionOrValue < JsonValue | undefined > {
7979 const override = this . #elementOverrides?. get ( String ( propertyName ) )
80- return override ? override : ( this . #element as any ) [ propertyName ]
80+ return override ?? ( this . #element as any ) [ propertyName ] ?? { isExpression : false , value : undefined }
8181 }
8282
8383 getUnknown ( propertyName : keyof T , defaultValue : VariableValue ) : VariableValue | undefined {
@@ -96,7 +96,7 @@ export class ElementExpressionHelper<T> {
9696 getParsedString ( propertyName : keyof T , defaultValue : string ) : string {
9797 const value = this . #getValue( propertyName )
9898 if ( value . isExpression ) {
99- return stringifyVariableValue ( this . getUnknown ( propertyName , defaultValue ) ) ?? ''
99+ return stringifyVariableValue ( this . getUnknown ( propertyName , defaultValue ) ) ?? defaultValue
100100 } else {
101101 return this . parseVariablesInString ( stringifyVariableValue ( value . value ) ?? '' , defaultValue )
102102 }
@@ -105,14 +105,20 @@ export class ElementExpressionHelper<T> {
105105 getNumber ( propertyName : keyof T , defaultValue : number , scale = 1 ) : number {
106106 const value = this . #getValue( propertyName )
107107
108- if ( ! value . isExpression ) return Number ( value . value ) * scale
108+ if ( ! value . isExpression ) {
109+ const num = Number ( value . value )
110+ return isNaN ( num ) ? defaultValue : num * scale
111+ }
109112
110113 const result = this . executeExpressionAndTrackVariables ( value . value , 'number' )
111114 if ( ! result . ok ) {
112115 return defaultValue
113116 }
114117
115- return Number ( result . value ) * scale
118+ // Number(undefined) = NaN and typeof NaN === 'number', so ok:true can still yield NaN
119+ // (e.g. when a referenced variable doesn't exist). Treat NaN as a missing value.
120+ const num = Number ( result . value )
121+ return isNaN ( num ) ? defaultValue : num * scale
116122 }
117123
118124 getString < TVal extends string | null | undefined > ( propertyName : keyof T , defaultValue : TVal ) : TVal {
@@ -123,16 +129,15 @@ export class ElementExpressionHelper<T> {
123129 return stringifyVariableValue ( value . value ) as TVal
124130 }
125131
126- const result = this . executeExpressionAndTrackVariables ( value . value , 'string' )
132+ const result = this . executeExpressionAndTrackVariables ( value . value , undefined )
127133 if ( ! result . ok ) {
128134 return defaultValue
129135 }
130136
131- if ( typeof result . value !== 'string' ) {
132- return defaultValue
133- }
134-
135- return result . value as TVal
137+ // stringifyVariableValue returns undefined only when result.value is undefined
138+ // (e.g. the referenced variable doesn't exist). Treat that as a missing value.
139+ const stringified = stringifyVariableValue ( result . value )
140+ return ( stringified ?? defaultValue ) as TVal
136141 }
137142
138143 getEnum < TVal extends string | number > ( propertyName : keyof T , values : TVal [ ] , defaultValue : TVal ) : TVal {
@@ -157,6 +162,18 @@ export class ElementExpressionHelper<T> {
157162 return actualValue
158163 }
159164
165+ /**
166+ * Like getEnum, but compares the string value to the enum values in a tolerant way:
167+ * Matches only the first non-whitespace character, case-insensitively.
168+ */
169+ getTolerantEnum < TVal extends string > ( propertyName : keyof T , values : readonly TVal [ ] , defaultValue : TVal ) : TVal {
170+ const raw = this . getString ( propertyName , defaultValue )
171+ const trimmed = String ( raw ?? '' )
172+ . trim ( )
173+ . toLowerCase ( )
174+ return values . find ( ( v ) => v . toLowerCase ( ) . startsWith ( trimmed [ 0 ] ) ) ?? defaultValue
175+ }
176+
160177 getBoolean ( propertyName : keyof T , defaultValue : boolean ) : boolean {
161178 const value = this . #getValue( propertyName )
162179
@@ -194,6 +211,21 @@ export class ElementExpressionHelper<T> {
194211 return 'center'
195212 }
196213 }
214+
215+ /**
216+ * Create a helper for a row of an internal:list or internal:table (or other child object)
217+ */
218+ forRow ( row : unknown ) : ElementExpressionHelper < Record < string , ExpressionOrValue < JsonValue | undefined > > > {
219+ const normalised : Record < string , ExpressionOrValue < JsonValue | undefined > > = { }
220+ if ( row && typeof row === 'object' && ! Array . isArray ( row ) ) {
221+ for ( const key of Object . keys ( row ) ) {
222+ const val = ( row as Record < string , unknown > ) [ key ]
223+ normalised [ key ] = isExpressionOrValue ( val ) ? val : { isExpression : false , value : val as JsonValue | undefined }
224+ }
225+ }
226+ return new ElementExpressionHelper ( this . #parser, this . #usedVariables, normalised , undefined )
227+ }
228+
197229 getVerticalAlignment ( propertyName : keyof T ) : VerticalAlignment {
198230 const value = this . #getValue( propertyName )
199231
0 commit comments