• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / brighterscript / #15226

23 Feb 2026 03:37PM UTC coverage: 87.222% (-0.006%) from 87.228%
#15226

push

web-flow
Merge c2f319ba4 into fd4d8c43f

14852 of 17988 branches covered (82.57%)

Branch coverage included in aggregate %.

31 of 31 new or added lines in 5 files covered. (100.0%)

227 existing lines in 5 files now uncovered.

15518 of 16831 relevant lines covered (92.2%)

25628.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

48.88
/src/parser/SGTypes.ts
1
import { SourceNode } from 'source-map';
1✔
2
import type { Location } from 'vscode-languageserver';
3
import { createSGAttribute, createSGInterface, createSGInterfaceField, createSGInterfaceFunction, createSGToken } from '../astUtils/creators';
1✔
4
import type { FileReference, TranspileResult } from '../interfaces';
5
import util from '../util';
1✔
6
import type { TranspileState } from './TranspileState';
7
import type { BscType } from '../types';
8

9
export interface SGToken {
10
    text: string;
11
    location?: Location;
12
}
13

14
export class SGAttribute {
1✔
15
    public constructor(options: {
16
        key: SGToken;
17
        equals?: SGToken;
18
        openingQuote?: SGToken;
19
        value?: SGToken;
20
        closingQuote?: SGToken;
21
    }) {
22
        this.tokens = {
2,244✔
23
            key: options.key,
24
            equals: options.equals,
25
            openingQuote: options.openingQuote,
26
            value: options.value,
27
            closingQuote: options.closingQuote
28
        };
29
    }
30

31
    public readonly tokens: {
32
        readonly key: SGToken;
33
        equals?: SGToken;
34
        openingQuote?: SGToken;
35
        value?: SGToken;
36
        closingQuote?: SGToken;
37
    };
38

39
    public get key() {
40
        return this.tokens.key.text;
4✔
41
    }
42

43
    /**
44
     * The value of this attribute. This does not including the opening or closing quote
45
     */
46
    public get value(): string | undefined {
47
        return this.tokens.value?.text;
135!
48
    }
49
    public set value(val) {
50
        if (val === null || val === undefined) {
33!
UNCOV
51
            val = '';
×
52
        }
53
        if (!this.tokens.equals) {
33!
UNCOV
54
            this.tokens.equals = { text: '=' };
×
55
        }
56
        if (this.tokens.value) {
33!
57
            this.tokens.value.text = val;
33✔
58
        } else {
UNCOV
59
            this.tokens.value = { text: val };
×
60
        }
61
    }
62

63
    public get location(): Location {
64
        if (!this._location) {
3!
65
            this._location = util.createBoundingLocation(
3✔
66
                this.tokens.key,
67
                this.tokens.equals,
68
                this.tokens.openingQuote,
69
                this.tokens.value,
70
                this.tokens.closingQuote
71
            );
72
        }
73
        return this._location;
3✔
74
    }
75
    private _location = null as Location;
2,244✔
76

77
    public transpile(state: TranspileState) {
78
        const result: TranspileResult = [
228✔
79
            state.transpileToken(this.tokens.key)
80
        ];
81
        if (this.tokens.value) {
228!
82
            result.push(
228✔
83
                state.transpileToken(this.tokens.equals, '='),
84
                state.transpileToken(this.tokens.openingQuote, '"'),
85
                state.transpileToken(this.tokens.value),
86
                state.transpileToken(this.tokens.closingQuote, '"')
87
            );
88
        }
89
        return util.sourceNodeFromTranspileResult(null, null, null, result);
228✔
90
    }
91

92
    public clone() {
UNCOV
93
        return new SGAttribute(this.tokens);
×
94
    }
95
}
96

97
export class SGElement {
1✔
98

99
    constructor(options: {
100
        startTagOpen?: SGToken;
101
        startTagName: SGToken;
102
        attributes?: SGAttribute[];
103
        startTagClose?: SGToken;
104
        elements?: SGElement[];
105
        endTagOpen?: SGToken;
106
        endTagName?: SGToken;
107
        endTagClose?: SGToken;
108
    }) {
109
        this.tokens = {
1,371✔
110
            startTagOpen: options.startTagOpen,
111
            startTagName: options.startTagName,
112
            startTagClose: options.startTagClose,
113
            endTagOpen: options.endTagOpen,
114
            endTagName: options.endTagName,
115
            endTagClose: options.endTagClose
116
        };
117
        this.attributes = options.attributes ?? [];
1,371!
118
        this.elements = options.elements ?? [];
1,371✔
119
    }
120

121
    public readonly tokens: {
122
        /**
123
         * The first portion of the startTag. (i.e. `<` or `<?`)
124
         */
125
        readonly startTagOpen?: SGToken;
126
        /**
127
         * The name of the opening tag (i.e. CoolTag in `<CoolTag>`).
128
         */
129
        readonly startTagName: SGToken;
130
        /**
131
         * The last bit of the startTag (i.e. `/>` for self-closing, `?>` for xml prolog, or `>` for tag with children)
132
         */
133
        readonly startTagClose?: SGToken;
134
        /**
135
         * The endTag opening char `<`
136
         */
137
        readonly endTagOpen?: SGToken;
138
        /**
139
         * The name of the ending tag (i.e. CoolTag in `</CoolTag>`)
140
         */
141
        readonly endTagName?: SGToken;
142
        /**
143
         * The endTag closing char `>`
144
         */
145
        readonly endTagClose?: SGToken;
146
    };
147

148
    /**
149
     * Array of attributes found on this tag
150
     */
151
    public readonly attributes = [] as SGAttribute[];
1,371✔
152

153
    /**
154
     * The array of direct children AST elements of this AST node
155
     */
156
    public readonly elements = [] as SGElement[];
1,371✔
157

158
    public get location() {
159
        if (!this._location) {
1!
160
            this._location = util.createBoundingLocation(
1✔
161
                this.tokens.startTagOpen,
162
                this.tokens.startTagName,
163
                this.attributes?.[this.attributes?.length - 1],
6!
164
                this.tokens.startTagClose,
165
                this.elements?.[this.elements?.length - 1],
6!
166
                this.tokens.endTagOpen,
167
                this.tokens.endTagName,
168
                this.tokens.endTagClose
169
            );
170
        }
171
        return this._location;
1✔
172
    }
173
    private _location = null as Location;
1,371✔
174

175
    /**
176
     * Is this a self-closing tag?
177
     */
178
    get isSelfClosing() {
179
        return this.tokens.startTagClose && this.tokens.startTagClose?.text !== '>';
160!
180
    }
181

182
    get id() {
183
        return this.getAttributeValue('id');
153✔
184
    }
185
    set id(value: string) {
UNCOV
186
        this.setAttributeValue('id', value);
×
187
    }
188

189
    /**
190
     * Get the name of this tag.
191
     */
192
    public get tagName() {
UNCOV
193
        return this.tokens.startTagName?.text;
×
194
    }
195

196
    /**
197
     * Find all direct children by their tag name (case insensitive).
198
     * This does not step into children's children.
199
     *
200
     */
201
    public getElementsByTagName<T extends SGElement>(tagName: string) {
202
        const result = [] as T[];
3,038✔
203
        const lowerTagName = tagName.toLowerCase();
3,038✔
204
        for (const el of this.elements) {
3,038✔
205
            if (el.tokens.startTagName.text.toLowerCase() === lowerTagName) {
2,846✔
206
                result.push(el as T);
1,243✔
207
            }
208
        }
209
        return result as Readonly<Array<T>>;
3,038✔
210
    }
211

212
    /**
213
     * Add a child to the end of the children array
214
     */
215
    public addChild<T extends SGElement>(tag: T) {
UNCOV
216
        this.elements.push(tag);
×
217
        return tag;
×
218
    }
219

220
    /**
221
     * Remove a child from the children array.
222
     * @returns true if node was found and removed, false if the node wasn't there and thus nothing was done
223
     */
224
    public removeChild(tag: SGElement) {
UNCOV
225
        const idx = this.elements.indexOf(tag);
×
226
        if (idx > -1) {
×
227
            this.elements.splice(idx, 1);
×
228
            return true;
×
229
        }
UNCOV
230
        return false;
×
231
    }
232

233
    /**
234
     * Does this node have the specified attribute?
235
     */
236
    public hasAttribute(name: string) {
UNCOV
237
        return !!this.getAttribute(name);
×
238
    }
239

240
    /**
241
     * Get an SGAttribute by its name (case INsensitive)
242
     */
243
    public getAttribute(name: string): SGAttribute | undefined {
244
        const nameLower = name.toLowerCase();
5,427✔
245
        for (const attr of this.attributes) {
5,427✔
246
            if (attr.tokens.key?.text.toLowerCase() === nameLower) {
7,885!
247
                return attr;
5,237✔
248
            }
249
        }
250
    }
251

252
    /**
253
     * Get an attribute value by its name
254
     */
255
    public getAttributeValue(name: string): string | undefined {
256
        return this.getAttribute(name)?.tokens.value?.text;
3,924✔
257
    }
258

259
    /**
260
     * Set an attribute value by its name. If no attribute exists with this name, it is created
261
     */
262
    public setAttributeValue(name: string, value: string) {
UNCOV
263
        if (value === undefined) {
×
264
            this.removeAttribute(name);
×
265
        } else {
UNCOV
266
            let attr = this.getAttribute(name);
×
267
            //create an attribute with this name if we don't have one yet
UNCOV
268
            if (!attr) {
×
269
                attr = createSGAttribute(name, value);
×
270
                this.attributes.push(
×
271
                    attr
272
                );
273
            }
UNCOV
274
            attr.value = value;
×
275
        }
276
    }
277

278
    /**
279
     * Remove an attribute by its name. DO NOT USE this to edit AST (use ASTEditor)
280
     * @returns true if an attribute was found and removed. False if no attribute was found
281
     */
282
    public removeAttribute(name: string) {
UNCOV
283
        const nameLower = name.toLowerCase();
×
284
        for (let i = 0; i < this.attributes.length; i++) {
×
285
            if (this.attributes[i].key?.toLowerCase() === nameLower) {
×
286
                this.attributes.splice(i, 1);
×
287
                return true;
×
288
            }
289
        }
UNCOV
290
        return false;
×
291
    }
292

293
    public transpile(state: TranspileState) {
294
        return util.sourceNodeFromTranspileResult(null, null, null, [
123✔
295
            state.transpileToken(this.tokens.startTagOpen, '<'), // <
296
            state.transpileToken(this.tokens.startTagName),
297
            this.transpileAttributes(state, this.attributes),
298
            this.transpileBody(state)
299
        ]);
300
    }
301

302
    protected transpileBody(state: TranspileState) {
303
        if (this.isSelfClosing && this.elements.length === 0) {
122✔
304
            return util.sourceNodeFromTranspileResult(null, null, null, [
84✔
305
                ' ',
306
                state.transpileToken(this.tokens.startTagClose, '/>'),
307
                state.newline
308
            ]);
309
        } else {
310
            // it is possible that the original tag isSelfClosing, but new elements have been added to it
311
            // in that case, create a new startTagClose token for transpilation.
312
            const startTagClose = this.isSelfClosing ? createSGToken('>', this.tokens.startTagClose.location) : this.tokens.startTagClose;
38✔
313
            const chunks: TranspileResult = [
38✔
314
                state.transpileToken(startTagClose, '>'),
315
                state.newline
316
            ];
317
            state.blockDepth++;
38✔
318
            for (const child of this.elements) {
38✔
319
                chunks.push(
74✔
320
                    state.indentText,
321
                    child.transpile(state)
322
                );
323
            }
324
            state.blockDepth--;
38✔
325
            chunks.push(
38✔
326
                state.indentText,
327
                state.transpileToken(this.tokens.endTagOpen, '</'),
328
                state.transpileToken(this.tokens.endTagName ?? this.tokens.startTagName),
114✔
329
                state.transpileToken(this.tokens.endTagClose, '>'),
330
                state.newline
331
            );
332
            return util.sourceNodeFromTranspileResult(null, null, null, chunks);
38✔
333
        }
334
    }
335

336
    protected transpileAttributes(state: TranspileState, attributes: SGAttribute[]) {
337
        const chunks = [];
123✔
338
        for (const attr of attributes) {
123✔
339
            chunks.push(' ', attr.transpile(state));
228✔
340
        }
341
        return new SourceNode(null, null, null, chunks);
123✔
342
    }
343
}
344

345
export class SGProlog extends SGElement { }
1✔
346

347
export class SGNode extends SGElement { }
1✔
348

349
export class SGChildren extends SGElement { }
1✔
350

351
export class SGCustomization extends SGElement { }
1✔
352

353
export class SGScript extends SGElement {
1✔
354

355
    public cdata?: SGToken;
356

357
    get type() {
358
        return this.getAttributeValue('type');
4✔
359
    }
360
    set type(value: string) {
UNCOV
361
        this.setAttributeValue('type', value);
×
362
    }
363

364
    get uri() {
365
        return this.getAttributeValue('uri');
64✔
366
    }
367
    set uri(value: string) {
UNCOV
368
        this.setAttributeValue('uri', value);
×
369
    }
370

371
    protected transpileBody(state: TranspileState) {
372
        if (this.cdata) {
51✔
373
            return util.sourceNodeFromTranspileResult(null, null, null, [
1✔
374
                '>',
375
                state.transpileToken(this.cdata),
376
                '</',
377
                this.tokens.startTagName.text,
378
                '>',
379
                state.newline
380
            ]);
381
        } else {
382
            return super.transpileBody(state);
50✔
383
        }
384
    }
385

386
    protected transpileAttributes(state: TranspileState, attributes: SGAttribute[]) {
387
        const modifiedAttributes = [] as SGAttribute[];
51✔
388
        let foundType = false;
51✔
389
        const bsExtensionRegexp = /\.bs$/i;
51✔
390

391
        for (const attr of attributes) {
51✔
392
            const lowerKey = attr.tokens.key?.text.toLowerCase();
100!
393
            if (lowerKey === 'uri' && bsExtensionRegexp.exec(attr.tokens.value?.text)) {
100!
UNCOV
394
                const clone = attr.clone();
×
395
                clone.tokens.value.text.replace(bsExtensionRegexp, '.brs');
×
396
                modifiedAttributes.push(clone);
×
397
            } else if (lowerKey === 'type') {
100✔
398
                foundType = true;
50✔
399
                if (attr.tokens.value?.text.toLowerCase().endsWith('brighterscript')) {
50!
UNCOV
400
                    modifiedAttributes.push(
×
401
                        attr.clone()
402
                    );
403
                } else {
404
                    modifiedAttributes.push(attr);
50✔
405
                }
406
            } else {
407
                modifiedAttributes.push(attr);
50✔
408
            }
409
        }
410
        if (!foundType) {
51✔
411
            modifiedAttributes.push(
1✔
412
                createSGAttribute('type', 'text/brightscript')
413
            );
414
        }
415
        return super.transpileAttributes(state, modifiedAttributes);
51✔
416
    }
417
}
418

419
export class SGInterfaceField extends SGElement {
1✔
420

421
    get type() {
422
        return this.getAttributeValue('type');
152✔
423
    }
424
    set type(value: string) {
UNCOV
425
        this.setAttributeValue('type', value);
×
426
    }
427

428
    get alias() {
429
        return this.getAttributeValue('alias');
3✔
430
    }
431
    set alias(value: string) {
UNCOV
432
        this.setAttributeValue('alias', value);
×
433
    }
434

435
    get value() {
UNCOV
436
        return this.getAttributeValue('value');
×
437
    }
438
    set value(value: string) {
UNCOV
439
        this.setAttributeValue('value', value);
×
440
    }
441

442
    get onChange() {
443
        return this.getAttributeValue('onChange');
70✔
444
    }
445
    set onChange(value: string) {
UNCOV
446
        this.setAttributeValue('onChange', value);
×
447
    }
448

449
    get alwaysNotify() {
UNCOV
450
        return this.getAttributeValue('alwaysNotify');
×
451
    }
452
    set alwaysNotify(value: string) {
UNCOV
453
        this.setAttributeValue('alwaysNotify', value);
×
454
    }
455

456
    bscType: BscType;
457
}
458

459
export enum SGFieldType {
1✔
460
    integer = 'integer',
1✔
461
    int = 'int',
1✔
462
    longinteger = 'longinteger',
1✔
463
    float = 'float',
1✔
464
    string = 'string',
1✔
465
    str = 'str',
1✔
466
    boolean = 'boolean',
1✔
467
    bool = 'bool',
1✔
468
    vector2d = 'vector2d',
1✔
469
    color = 'color',
1✔
470
    time = 'time',
1✔
471
    uri = 'uri',
1✔
472
    node = 'node',
1✔
473
    floatarray = 'floatarray',
1✔
474
    intarray = 'intarray',
1✔
475
    boolarray = 'boolarray',
1✔
476
    stringarray = 'stringarray',
1✔
477
    vector2darray = 'vector2darray',
1✔
478
    colorarray = 'colorarray',
1✔
479
    timearray = 'timearray',
1✔
480
    nodearray = 'nodearray',
1✔
481
    assocarray = 'assocarray',
1✔
482
    array = 'array',
1✔
483
    roarray = 'roarray',
1✔
484
    rect2d = 'rect2d',
1✔
485
    rect2darray = 'rect2darray'
1✔
486
}
487
export const SGFieldTypes = Object.keys(SGFieldType);
1✔
488

489
export class SGInterfaceFunction extends SGElement {
1✔
490
    get name() {
491
        return this.getAttributeValue('name');
616✔
492
    }
493
    set name(value: string) {
UNCOV
494
        this.setAttributeValue('name', value);
×
495
    }
496
}
497

498
export type SGInterfaceMember = SGInterfaceField | SGInterfaceFunction;
499

500
export class SGInterface extends SGElement {
1✔
501
    public get fields() {
502
        return this.getElementsByTagName<SGInterfaceField>('field');
203✔
503
    }
504

505
    public get functions() {
506
        return this.getElementsByTagName<SGInterfaceFunction>('function');
219✔
507
    }
508

509
    public get members() {
UNCOV
510
        const result = [] as Array<SGInterfaceMember>;
×
UNCOV
511
        for (const node of this.elements) {
×
UNCOV
512
            const tagName = node.tagName?.toLowerCase();
×
513
            if (tagName === 'field' || tagName === 'function') {
×
514
                result.push(node as SGInterfaceMember);
×
515
            }
516
        }
517
        return result as Readonly<typeof result>;
×
518
    }
519

520
    /**
521
     * Check if there's an SGField with the specified name
522
     */
523
    public hasField(id: string) {
UNCOV
524
        for (const node of this.elements) {
×
UNCOV
525
            const tagName = node.tagName?.toLowerCase();
×
UNCOV
526
            if (tagName === 'field' && (node as SGInterfaceField).id === id) {
×
527
                return true;
×
528
            }
529
        }
530
        return false;
×
531
    }
532

533
    /**
534
     * Check if there's an SGFunction with the specified name
535
     */
536
    public hasFunction(name: string) {
UNCOV
537
        for (const node of this.elements) {
×
UNCOV
538
            const tagName = node.tagName?.toLowerCase();
×
UNCOV
539
            if (tagName === 'function' && (node as SGInterfaceFunction).name === name) {
×
540
                return true;
×
541
            }
542
        }
543
        return false;
×
544
    }
545

546
    /**
547
     * Find a field by its ID
548
     */
549
    public getField(id: string) {
UNCOV
550
        return this.fields.find(field => field.id === id);
×
551
    }
552

553
    /**
554
     * Set the value of a field. Creates a new field if one does not already exist with this ID
555
     */
556
    public setField(id: string, type: string, onChange?: string, alwaysNotify?: boolean, alias?: string) {
UNCOV
557
        let field = this.getField(id);
×
UNCOV
558
        if (!field) {
×
UNCOV
559
            field = this.addChild(
×
560
                createSGInterfaceField(id)
561
            );
562
        }
UNCOV
563
        field.type = type;
×
UNCOV
564
        field.onChange = onChange;
×
UNCOV
565
        if (alwaysNotify === undefined) {
×
566
            field.alwaysNotify = undefined;
×
567
        } else {
568
            field.alwaysNotify = alwaysNotify ? 'true' : 'false';
×
569
        }
UNCOV
570
        field.alias = alias;
×
571
        return field;
×
572
    }
573

574
    /**
575
     * Remove a field from the interface
576
     * @returns true if a field was found and removed. Returns false if no field was found with that name
577
     */
578
    public removeField(id: string) {
UNCOV
579
        for (let i = 0; i < this.elements.length; i++) {
×
UNCOV
580
            const node = this.elements[i];
×
UNCOV
581
            if (node.tagName?.toLowerCase() === 'field' && node.id === id) {
×
582
                this.elements.splice(i, 1);
×
583
                return true;
×
584
            }
585
        }
586
        return false;
×
587
    }
588

589
    /**
590
     * Get the interface function with the specified name
591
     */
592
    public getFunction(name: string) {
UNCOV
593
        return this.functions.find(func => func.name === name);
×
594
    }
595

596
    /**
597
     * Add or replace a function on the interface
598
     */
599
    public setFunction(name: string) {
UNCOV
600
        let func = this.getFunction(name);
×
UNCOV
601
        if (!func) {
×
UNCOV
602
            func = this.addChild(
×
603
                createSGInterfaceFunction(name)
604
            );
605
        }
UNCOV
606
        return func;
×
607
    }
608

609
    /**
610
     * Remove a function from the interface
611
     * @returns true if a function was found and removed. Returns false if no function was found with that name
612
     */
613
    public removeFunction(name: string) {
UNCOV
614
        for (let i = 0; i < this.elements.length; i++) {
×
UNCOV
615
            const node = this.elements[i];
×
UNCOV
616
            if (node.tagName?.toLowerCase() === 'function' && node.getAttributeValue('name') === name) {
×
617
                this.elements.splice(i, 1);
×
618
                return true;
×
619
            }
620
        }
621
        return false;
×
622
    }
623
}
624

625
/**
626
 * The `<component>` element in SceneGraph. Not to be confused about usages of components like `<Rectangle>`, those are considered `SGNode` instances.
627
 */
628
export class SGComponent extends SGElement {
1✔
629

630
    /**
631
     * Get all the <Field> and <Function> elements across all <Interface> nodes in this component
632
     */
633
    public get interfaceMembers() {
UNCOV
634
        const members = [] as Array<SGInterfaceMember>;
×
UNCOV
635
        for (const ifaceNode of this.getElementsByTagName<SGInterface>('interface')) {
×
UNCOV
636
            members.push(
×
637
                ...ifaceNode.members
638
            );
639
        }
UNCOV
640
        return members as Readonly<typeof members>;
×
641
    }
642

643
    public get scriptElements() {
644
        return this.getElementsByTagName<SGScript>('script');
531✔
645
    }
646

647
    /**
648
     * Get the <interface> element from this component (if present), or undefined if not.
649
     * NOTE: Roku supports and merges multiple <interface> elements in a component, but this
650
     * property points to the FIRST one. If you need to check whether a member exists,
651
     * look through `this.interfaceMemebers` instead.
652
     */
653
    public get interfaceElement(): SGInterface | undefined {
654
        return this.getElementsByTagName<SGInterface>('interface')[0];
1,927✔
655
    }
656

657
    /**
658
     * Get the `<children>` element of this component. (not to be confused with the AST `childTags` property).
659
     * If there are multiope `<children>` elements, this function will return the last `<children>` tag because that's what Roku devices do.
660
     */
661
    public get childrenElement() {
UNCOV
662
        const children = this.getElementsByTagName<SGChildren>('children');
×
UNCOV
663
        return children[children.length - 1];
×
664
    }
665

666
    public get customizationElements() {
UNCOV
667
        return this.getElementsByTagName<SGCustomization>('customization');
×
668
    }
669

670
    /**
671
     * Specifies the name of the component, that allows you to create the component in your application.
672
     * For example, if the name of the component is `CastMemberInfo`, you could create instances of the component declaratively
673
     * in a child node element of a component `<children>` element (`<CastMemberInfo/>`), or using BrightScript in a `<script>`
674
     * element (`createObject("roSGNode","CastMemberInfo")`).
675
     *
676
     * The name attribute is case-sensitive. You cannot successfully create or declare a component unless the component name exactly
677
     *  matches the name attribute, including case. Also be aware that two components with the exact same name in the same application
678
     * components directory will have undefined and generally undesirable results if you attempt to create a component object with that name in the application.
679
     */
680
    get name() {
681
        return this.getAttributeValue('name');
1,251✔
682
    }
683
    set name(value: string) {
UNCOV
684
        this.setAttributeValue('name', value);
×
685
    }
686

687
    /**
688
     * Specifies the name of the built-in or extended SceneGraph scene or node class whose functionality is extended by this component.
689
     *
690
     * For example, `extends="Group"` specifies that the component has all of the functionality of the Group node class (it can have child nodes, has translation/scale/rotation fields, and so forth).
691
     *
692
     * By default, a component extends the Group node class.
693
     */
694
    get extends() {
695
        return this.getAttributeValue('extends');
1,611✔
696
    }
697
    set extends(value: string) {
UNCOV
698
        this.setAttributeValue('extends', value);
×
699
    }
700

701
    /**
702
     * Specifies the ID of a node declared in the XML file to have the initial remote control focus when the component is instantiated.
703
     */
704
    get initialFocus() {
UNCOV
705
        return this.getAttributeValue('initialFocus');
×
706
    }
707
    set initialFocus(value: string) {
708
        this.setAttributeValue('initialFocus', value);
×
709
    }
710

711
    /**
712
     * Specifies the version of the SceneGraph API. The default is 1.0 if not specified.
713
     */
714
    get version() {
UNCOV
715
        return this.getAttributeValue('version');
×
716
    }
717
    set version(value: string) {
718
        this.setAttributeValue('version', value);
×
719
    }
720

721
    /**
722
     * Does the specified field exist in the component interface?
723
     */
724
    public hasInterfaceField(id: string) {
UNCOV
725
        for (const ifaceNode of this.getElementsByTagName<SGInterface>('interface')) {
×
UNCOV
726
            if (ifaceNode.hasField(id)) {
×
UNCOV
727
                return true;
×
728
            }
729
        }
730
        return false;
×
731
    }
732

733
    /**
734
     * Does the specified function exist in the component interface?
735
     */
736
    public hasInterfaceFunction(name: string) {
UNCOV
737
        for (const ifaceNode of this.getElementsByTagName<SGInterface>('interface')) {
×
UNCOV
738
            if (ifaceNode.hasFunction(name)) {
×
UNCOV
739
                return true;
×
740
            }
741
        }
742
        return false;
×
743
    }
744

745
    /**
746
     * Get an interface field with the specified name
747
     */
748
    public getInterfaceField(name: string): SGInterfaceField | undefined {
UNCOV
749
        for (const ifaceNode of this.getElementsByTagName<SGInterface>('interface')) {
×
UNCOV
750
            const field = ifaceNode.getField(name);
×
UNCOV
751
            if (field) {
×
752
                return field;
×
753
            }
754
        }
755
    }
756

757
    /**
758
     * Return the first SGInterface node found, or insert a new one then return it
759
     */
760
    private ensureInterfaceNode(): SGInterface {
UNCOV
761
        for (const el of this.elements) {
×
UNCOV
762
            if (el.tokens.startTagName.text.toLowerCase() === 'interface') {
×
UNCOV
763
                return el as SGInterface;
×
764
            }
765
        }
766
        return this.addChild(
×
767
            createSGInterface()
768
        );
769
    }
770

771
    /**
772
     * Create or update a <field> interface element.
773
     * This will create a new `<interface>` element if there are none on the component already
774
     */
775
    public setInterfaceField(id: string, type: string, onChange?: string, alwaysNotify?: boolean, alias?: string) {
UNCOV
776
        let ifaceNode = this.ensureInterfaceNode();
×
UNCOV
777
        return ifaceNode.setField(id, type, onChange, alwaysNotify, alias);
×
778
    }
779

780
    /**
781
     * Create or update a <function> interface element.
782
     * This will create a new `<interface>` element if there are none on the component already
783
     */
784
    public setInterfaceFunction(name: string) {
UNCOV
785
        let ifaceNode = this.ensureInterfaceNode();
×
UNCOV
786
        return ifaceNode.setFunction(name);
×
787
    }
788

789
    /**
790
     * Remove an interface field.
791
     * @returns true if a field was found and removed. Returns false if no field was found with that name
792
     */
793
    public removeInterfaceField(id: string) {
UNCOV
794
        for (const ifaceNode of this.getElementsByTagName<SGInterface>('interface')) {
×
UNCOV
795
            if (ifaceNode.removeField(id)) {
×
UNCOV
796
                return true;
×
797
            }
798
        }
799
        return false;
×
800
    }
801

802
    /**
803
     * Get an interface field with the specified name
804
     */
805
    public getInterfaceFunction(name: string): SGInterfaceFunction | undefined {
UNCOV
806
        for (const ifaceNode of this.getElementsByTagName<SGInterface>('interface')) {
×
UNCOV
807
            const func = ifaceNode.getFunction(name);
×
UNCOV
808
            if (func) {
×
809
                return func;
×
810
            }
811
        }
812
    }
813

814
    /**
815
     * Remove an interface function.
816
     * @returns true if a function was found and removed. Returns false if no function was found with that name
817
     */
818
    public removeInterfaceFunction(name: string) {
UNCOV
819
        for (const ifaceNode of this.getElementsByTagName<SGInterface>('interface')) {
×
UNCOV
820
            if (ifaceNode.removeFunction(name)) {
×
UNCOV
821
                return true;
×
822
            }
823
        }
824
        return false;
×
825
    }
826
}
827

828
export interface SGReferences {
829
    name?: SGToken;
830
    extends?: SGToken;
831
    scriptTagImports: Pick<FileReference, 'destPath' | 'text' | 'filePathRange'>[];
832
}
833

834
export class SGAst {
1✔
835

836
    constructor(options: {
837
        prologElement?: SGProlog;
838
        rootElement?: SGElement;
839
        componentElement?: SGComponent;
840
    } = {}) {
551✔
841
        this.prologElement = options.prologElement;
1,040✔
842
        this.rootElement = options.rootElement;
1,040✔
843
        this.componentElement = options.componentElement;
1,040✔
844
    }
845

846
    public readonly prologElement?: SGProlog;
847
    public readonly rootElement?: SGElement;
848
    public readonly componentElement?: SGComponent;
849

850
    public transpile(state: TranspileState): SourceNode {
851
        const chunks = [] as SourceNode[];
27✔
852
        //write XML prolog
853
        if (this.prologElement) {
27✔
854
            chunks.push(
22✔
855
                this.prologElement.transpile(state)
856
            );
857
        }
858
        if (this.componentElement) {
27!
859
            //write content
860
            chunks.push(
27✔
861
                this.componentElement.transpile(state)
862
            );
863
        }
864
        return new SourceNode(null, null, null, chunks);
27✔
865
    }
866
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc