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

rokucommunity / brighterscript / #12930

13 Aug 2024 05:02PM UTC coverage: 86.193% (-1.7%) from 87.933%
#12930

push

web-flow
Merge 58ad447a2 into 0e968f1c3

10630 of 13125 branches covered (80.99%)

Branch coverage included in aggregate %.

6675 of 7284 new or added lines in 99 files covered. (91.64%)

84 existing lines in 18 files now uncovered.

12312 of 13492 relevant lines covered (91.25%)

26865.48 hits per line

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

48.65
/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

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

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

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

38
    public get key() {
NEW
39
        return this.tokens.key.text;
×
40
    }
41

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

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

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

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

96
export class SGElement {
1✔
97

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

344
export class SGProlog extends SGElement { }
1✔
345

346
export class SGNode extends SGElement { }
1✔
347

348
export class SGChildren extends SGElement { }
1✔
349

350
export class SGCustomization extends SGElement { }
1✔
351

352
export class SGScript extends SGElement {
1✔
353

354
    public cdata?: SGToken;
355

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

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

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

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

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

418
export class SGInterfaceField extends SGElement {
1✔
419

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

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

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

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

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

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

486
export class SGInterfaceFunction extends SGElement {
1✔
487
    get name() {
488
        return this.getAttributeValue('name');
173✔
489
    }
490
    set name(value: string) {
NEW
491
        this.setAttributeValue('name', value);
×
492
    }
493
}
494

495
export type SGInterfaceMember = SGInterfaceField | SGInterfaceFunction;
496

497
export class SGInterface extends SGElement {
1✔
498
    public get fields() {
499
        return this.getElementsByTagName<SGInterfaceField>('field');
62✔
500
    }
501

502
    public get functions() {
503
        return this.getElementsByTagName<SGInterfaceFunction>('function');
74✔
504
    }
505

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

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

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

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

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

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

586
    /**
587
     * Get the interface function with the specified name
588
     */
589
    public getFunction(name: string) {
NEW
590
        return this.functions.find(func => func.name === name);
×
591
    }
592

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

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

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

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

640
    public get scriptElements() {
641
        return this.getElementsByTagName<SGScript>('script');
432✔
642
    }
643

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

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

663
    public get customizationElements() {
NEW
664
        return this.getElementsByTagName<SGCustomization>('customization');
×
665
    }
666

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

684
    /**
685
     * Specifies the name of the built-in or extended SceneGraph scene or node class whose functionality is extended by this component.
686
     *
687
     * 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).
688
     *
689
     * By default, a component extends the Group node class.
690
     */
691
    get extends() {
692
        return this.getAttributeValue('extends');
1,188✔
693
    }
694
    set extends(value: string) {
NEW
695
        this.setAttributeValue('extends', value);
×
696
    }
697

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

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

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

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

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

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

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

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

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

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

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

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

831
export class SGAst {
1✔
832

833
    constructor(options: {
834
        prologElement?: SGProlog;
835
        rootElement?: SGElement;
836
        componentElement?: SGComponent;
837
    } = {}) {
453✔
838
        this.prologElement = options.prologElement;
843✔
839
        this.rootElement = options.rootElement;
843✔
840
        this.componentElement = options.componentElement;
843✔
841
    }
842

843
    public readonly prologElement?: SGProlog;
844
    public readonly rootElement?: SGElement;
845
    public readonly componentElement?: SGComponent;
846

847
    public transpile(state: TranspileState): SourceNode {
848
        const chunks = [] as SourceNode[];
27✔
849
        //write XML prolog
850
        if (this.prologElement) {
27✔
851
            chunks.push(
22✔
852
                this.prologElement.transpile(state)
853
            );
854
        }
855
        if (this.componentElement) {
27!
856
            //write content
857
            chunks.push(
27✔
858
                this.componentElement.transpile(state)
859
            );
860
        }
861
        return new SourceNode(null, null, null, chunks);
27✔
862
    }
863
}
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

© 2025 Coveralls, Inc