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

rokucommunity / brs / #299

01 Apr 2024 08:41PM UTC coverage: 90.321% (-0.1%) from 90.465%
#299

push

web-flow
Implemented `roPath` component and fixed Interpreter Comparisons (#50)

* Added option to disable colors and fixed unit tests to not fail with colors

* Implemented `roPath` and fixed comparison to match Roku behavior

* Added unit tests and fixed prettier issues

* Properly compare Number with Boolean

* Added support to AND and OR between Boolean and Numbers

* Updated unit tests and fixed Int64

* Improved URL regex for CLI colorizer

1881 of 2239 branches covered (84.01%)

Branch coverage included in aggregate %.

167 of 181 new or added lines in 11 files covered. (92.27%)

5 existing lines in 3 files now uncovered.

5519 of 5954 relevant lines covered (92.69%)

8637.25 hits per line

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

94.66
/src/brsTypes/components/RoString.ts
1
import { BrsComponent } from "./BrsComponent";
141✔
2
import { RoArray } from "./RoArray";
141✔
3
import { BrsValue, ValueKind, BrsString, BrsBoolean, BrsInvalid, Comparable } from "../BrsType";
141✔
4
import { Callable, StdlibArgument } from "../Callable";
141✔
5
import { Interpreter } from "../../interpreter";
6
import { BrsType, RoList, isStringComp } from "..";
141✔
7
import { Unboxable } from "../Boxing";
8
import { Int32 } from "../Int32";
141✔
9
import { Float } from "../Float";
141✔
10

11
export class RoString extends BrsComponent implements BrsValue, Comparable, Unboxable {
141✔
12
    readonly kind = ValueKind.Object;
175✔
13
    private intrinsic: BrsString = new BrsString("");
175✔
14

15
    public getValue(): string {
16
        return this.intrinsic.value;
26✔
17
    }
18

19
    constructor(initialValue?: BrsString) {
20
        super("roString");
175✔
21

22
        if (initialValue) {
175✔
23
            this.intrinsic = initialValue;
173✔
24
        }
25

26
        this.registerMethods({
175✔
27
            ifString: [this.setString, this.getString],
28
            ifStringOps: [
29
                this.appendString,
30
                this.len,
31
                this.left,
32
                this.right,
33
                this.mid,
34
                this.instr,
35
                this.replace,
36
                this.trim,
37
                this.toInt,
38
                this.toFloat,
39
                this.tokenize,
40
                this.setString,
41
                this.split,
42
                this.getEntityEncode,
43
                this.escape,
44
                this.unescape,
45
                this.encodeUri,
46
                this.decodeUri,
47
                this.encodeUriComponent,
48
                this.decodeUriComponent,
49
                this.startsWith,
50
                this.endsWith,
51
                this.isEmpty,
52
            ],
53
            ifToStr: [this.toStr],
54
        });
55
    }
56

57
    lessThan(other: BrsType): BrsBoolean {
58
        if (isStringComp(other)) {
2✔
59
            return BrsBoolean.from(this.getValue() < other.getValue());
2✔
60
        }
UNCOV
61
        return BrsBoolean.False;
×
62
    }
63

64
    greaterThan(other: BrsType): BrsBoolean {
65
        if (isStringComp(other)) {
2✔
66
            return BrsBoolean.from(this.getValue() > other.getValue());
2✔
67
        }
UNCOV
68
        return BrsBoolean.False;
×
69
    }
70

71
    equalTo(other: BrsType): BrsBoolean {
72
        if (isStringComp(other)) {
8✔
73
            return BrsBoolean.from(this.getValue() === other.getValue());
8✔
74
        }
NEW
75
        return BrsBoolean.False;
×
76
    }
77

78
    concat(other: BrsType): BrsString {
NEW
79
        if (isStringComp(other)) {
×
NEW
80
            return new BrsString(this.intrinsic.value + other.getValue());
×
81
        }
NEW
82
        return new BrsString(this.intrinsic.value + other.toString());
×
83
    }
84

85
    unbox() {
86
        return this.intrinsic;
2✔
87
    }
88

89
    toString(parent?: BrsType): string {
90
        return this.intrinsic.toString(parent);
1✔
91
    }
92

93
    /**
94
     * Sets the string to the first len characters of s.
95
     * Note: this method is implemented in the ifString and ifStringOps interfaces
96
     */
97
    private setString = new Callable(
175✔
98
        "SetString",
99
        {
100
            signature: {
101
                args: [new StdlibArgument("s", ValueKind.String)],
102
                returns: ValueKind.Void,
103
            },
104
            impl: (_interpreter, s: BrsString) => {
105
                this.intrinsic = new BrsString(s.value);
4✔
106
                return BrsInvalid.Instance;
4✔
107
            },
108
        },
109
        {
110
            signature: {
111
                args: [
112
                    new StdlibArgument("s", ValueKind.String),
113
                    new StdlibArgument("len", ValueKind.Int32),
114
                ],
115
                returns: ValueKind.Void,
116
            },
117
            impl: (_interpreter, s: BrsString, len: Int32) => {
118
                this.intrinsic = new BrsString(s.value.substr(0, len.getValue()));
4✔
119
                return BrsInvalid.Instance;
4✔
120
            },
121
        }
122
    );
123

124
    private getString = new Callable("GetString", {
175✔
125
        signature: {
126
            args: [],
127
            returns: ValueKind.String,
128
        },
129
        impl: (_interpreter) => this.intrinsic,
4✔
130
    });
131

132
    // ---------- ifStringOps ----------
133
    /** Appends the first len characters of s to the end of the string. */
134
    private appendString = new Callable("AppendString", {
175✔
135
        signature: {
136
            args: [
137
                new StdlibArgument("s", ValueKind.String),
138
                new StdlibArgument("len", ValueKind.Int32),
139
            ],
140
            returns: ValueKind.Void,
141
        },
142
        impl: (_interpreter, s: BrsString, len: Int32) => {
143
            this.intrinsic = this.intrinsic.concat(
5✔
144
                new BrsString(s.value.substr(0, len.getValue()))
145
            );
146
            return BrsInvalid.Instance;
5✔
147
        },
148
    });
149

150
    /** Returns the number of characters in the string. */
151
    private len = new Callable("Len", {
175✔
152
        signature: {
153
            args: [],
154
            returns: ValueKind.Int32,
155
        },
156
        impl: (_interpreter) => {
157
            return new Int32(this.intrinsic.value.length);
2✔
158
        },
159
    });
160

161
    /** Returns a string consisting of the first len characters of the string. */
162
    private left = new Callable("Left", {
175✔
163
        signature: {
164
            args: [new StdlibArgument("len", ValueKind.Int32)],
165
            returns: ValueKind.String,
166
        },
167
        impl: (_interpreter, len: Int32) => {
168
            return new BrsString(this.intrinsic.value.substr(0, len.getValue()));
4✔
169
        },
170
    });
171

172
    /** Returns a string consisting of the last len characters of the string. */
173
    private right = new Callable("Right", {
175✔
174
        signature: {
175
            args: [new StdlibArgument("len", ValueKind.Int32)],
176
            returns: ValueKind.String,
177
        },
178
        impl: (_interpreter, len: Int32) => {
179
            let source = this.intrinsic.value;
4✔
180
            return new BrsString(source.substr(source.length - len.getValue()));
4✔
181
        },
182
    });
183

184
    private mid = new Callable(
175✔
185
        "Mid",
186
        /**
187
         * Returns a string consisting of the last characters of the string, starting at the
188
         * zero-based start_index.
189
         */
190
        {
191
            signature: {
192
                args: [new StdlibArgument("start_index", ValueKind.Int32)],
193
                returns: ValueKind.String,
194
            },
195
            impl: (_interpreter, startIndex: Int32) => {
196
                return new BrsString(this.intrinsic.value.substr(startIndex.getValue()));
4✔
197
            },
198
        },
199

200
        /**
201
         * Returns a string consisting of num_chars characters of the string, starting at the
202
         * zero-based start_index.
203
         */
204
        {
205
            signature: {
206
                args: [
207
                    new StdlibArgument("start_index", ValueKind.Int32),
208
                    new StdlibArgument("num_chars", ValueKind.Int32),
209
                ],
210
                returns: ValueKind.String,
211
            },
212
            impl: (_interpreter, startIndex: Int32, numChars: Int32) => {
213
                let source = this.intrinsic.value;
4✔
214
                return new BrsString(
4✔
215
                    this.intrinsic.value.substr(startIndex.getValue(), numChars.getValue())
216
                );
217
            },
218
        }
219
    );
220

221
    private instr = new Callable(
175✔
222
        "Instr",
223
        /** Returns the zero-based index of the first occurrence of substring in the string. */
224
        {
225
            signature: {
226
                args: [new StdlibArgument("substring", ValueKind.String)],
227
                returns: ValueKind.Int32,
228
            },
229
            impl: (_interpreter, substring: BrsString) => {
230
                return new Int32(this.intrinsic.value.indexOf(substring.value));
2✔
231
            },
232
        },
233
        /**
234
         * Returns the zero-based index of the first occurrence of substring in the string, starting
235
         * at the specified zero-based start_index.
236
         */
237
        {
238
            signature: {
239
                args: [
240
                    new StdlibArgument("start_index", ValueKind.Int32),
241
                    new StdlibArgument("substring", ValueKind.String),
242
                ],
243
                returns: ValueKind.Int32,
244
            },
245
            impl: (_interpreter, startIndex: Int32, substring: BrsString) => {
246
                return new Int32(
9✔
247
                    this.intrinsic.value.indexOf(substring.value, startIndex.getValue())
248
                );
249
            },
250
        }
251
    );
252

253
    /**
254
     * Returns a copy of the string with all instances of fromStr replaced with toStr. If fromStr is
255
     * empty the return value is the same as the source string.
256
     */
257
    private replace = new Callable("Replace", {
175✔
258
        signature: {
259
            args: [
260
                new StdlibArgument("from", ValueKind.String),
261
                new StdlibArgument("to", ValueKind.String),
262
            ],
263
            returns: ValueKind.String,
264
        },
265
        impl: (_interpreter, from: BrsString, to: BrsString) => {
266
            if (from.value === "") {
3✔
267
                return this.intrinsic;
1✔
268
            }
269

270
            // From Mozilla's guide to escaping regex:
271
            // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
272
            let escapedFrom = from.value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2✔
273
            return new BrsString(
2✔
274
                this.intrinsic.value.replace(new RegExp(escapedFrom, "g"), to.value)
275
            );
276
        },
277
    });
278

279
    /**
280
     * Returns the string with any leading and trailing whitespace characters (space, TAB, LF, CR,
281
     * VT, FF, NO-BREAK SPACE, et al) removed.
282
     */
283
    private trim = new Callable("Trim", {
175✔
284
        signature: {
285
            args: [],
286
            returns: ValueKind.String,
287
        },
288
        impl: (_interpreter) => {
289
            return new BrsString(this.intrinsic.value.trim());
3✔
290
        },
291
    });
292

293
    /** Returns the value of the string interpreted as a decimal number. */
294
    private toInt = new Callable("ToInt", {
175✔
295
        signature: {
296
            args: [],
297
            returns: ValueKind.Int32,
298
        },
299
        impl: (_interpreter) => {
300
            let int = Math.trunc(Number.parseFloat(this.intrinsic.value));
7✔
301

302
            if (Number.isNaN(int)) {
7✔
303
                // non-integers are returned as "0"
304
                return new Int32(0);
2✔
305
            }
306

307
            return new Int32(int);
5✔
308
        },
309
    });
310

311
    /** Returns the value of the string interpreted as a floating point number. */
312
    private toFloat = new Callable("ToFloat", {
175✔
313
        signature: {
314
            args: [],
315
            returns: ValueKind.Float,
316
        },
317
        impl: (_interpreter) => {
318
            let float = Number.parseFloat(this.intrinsic.value);
5✔
319

320
            if (Number.isNaN(float)) {
5✔
321
                // non-integers are returned as "0"
322
                return new Float(0);
2✔
323
            }
324

325
            return new Float(float);
3✔
326
        },
327
    });
328

329
    /**
330
     * Splits the string into separate substrings separated by a single delimiter character. Returns
331
     * an roList containing each of the substrings. The delimiters are not returned.
332
     */
333
    private tokenize = new Callable("Tokenize", {
175✔
334
        signature: {
335
            args: [new StdlibArgument("delim", ValueKind.String)],
336
            returns: ValueKind.Object,
337
        },
338
        impl: (_interpreter, delim: BrsString) => {
339
            let str = this.intrinsic.value;
5✔
340
            let token: string[] = [];
5✔
341
            let tokens: BrsString[] = [];
5✔
342
            for (let char of str) {
5✔
343
                if (delim.value.includes(char)) {
50✔
344
                    if (token.length > 0) {
9✔
345
                        tokens.push(new BrsString(token.join("")));
6✔
346
                        token = [];
6✔
347
                    }
348
                } else {
349
                    token.push(char);
41✔
350
                }
351
            }
352
            if (token.length > 0) {
5✔
353
                tokens.push(new BrsString(token.join("")));
4✔
354
            }
355
            return new RoList(tokens);
5✔
356
        },
357
    });
358

359
    /**
360
     * Splits the input string using the separator string as a delimiter, and returns an array of
361
     * the split token strings (not including the delimiter). An empty separator string indicates
362
     * to split the string by character.
363
     */
364
    private split = new Callable("Split", {
175✔
365
        signature: {
366
            args: [new StdlibArgument("separator", ValueKind.String)],
367
            returns: ValueKind.Object,
368
        },
369
        impl: (_interpreter, separator: BrsString) => {
370
            let parts;
371
            if (separator.value === "") {
5✔
372
                // split characters apart, preserving multi-character unicode structures
373
                parts = Array.from(this.intrinsic.value);
1✔
374
            } else {
375
                parts = this.intrinsic.value.split(separator.value);
4✔
376
            }
377

378
            return new RoArray(parts.map((part) => new BrsString(part)));
19✔
379
        },
380
    });
381

382
    /**
383
     * Returns the string with certain characters ("'<>&) replaced with the corresponding HTML
384
     * entity encoding.
385
     */
386
    private getEntityEncode = new Callable("GetEntityEncode", {
175✔
387
        signature: {
388
            args: [],
389
            returns: ValueKind.String,
390
        },
391
        impl: (_interpreter) => {
392
            return new BrsString(this.intrinsic.value.replace(/(['"<>&])/g, "\\$1"));
1✔
393
        },
394
    });
395

396
    /** URL encodes the specified string per RFC 3986 and returns the encoded string. */
397
    private escape = new Callable("Escape", {
175✔
398
        signature: {
399
            args: [],
400
            returns: ValueKind.String,
401
        },
402
        impl: (_interpreter) => {
403
            return new BrsString(
2✔
404
                // encoding courtesy of
405
                // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Description
406
                encodeURIComponent(this.intrinsic.value).replace(
407
                    /[!'()*]/g,
408
                    (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
2✔
409
                )
410
            );
411
        },
412
    });
413

414
    /** URL decodes the specified string per RFC 3986 and returns the decoded string. */
415
    private unescape = new Callable("Unescape", {
175✔
416
        signature: {
417
            args: [],
418
            returns: ValueKind.String,
419
        },
420
        impl: (_interpreter) => {
421
            return new BrsString(decodeURIComponent(this.intrinsic.value));
2✔
422
        },
423
    });
424

425
    /** returns whether string is empty or not */
426
    private isEmpty = new Callable("isEmpty", {
175✔
427
        signature: {
428
            args: [],
429
            returns: ValueKind.Boolean,
430
        },
431
        impl: (_interpreter) => {
432
            return BrsBoolean.from(this.intrinsic.value.length === 0);
4✔
433
        },
434
    });
435

436
    /**
437
     * Encode the specified string with escape sequences for reserved Uniform Resource Identifier
438
     * (URI) characters.
439
     */
440
    private encodeUri = new Callable("EncodeUri", {
175✔
441
        signature: {
442
            args: [],
443
            returns: ValueKind.String,
444
        },
445
        impl: (_interpreter) => {
446
            return new BrsString(encodeURI(this.intrinsic.value));
2✔
447
        },
448
    });
449

450
    /**
451
     * Decode the specified string with escape sequences for reserved Uniform Resource Identifier
452
     * (URI) characters.
453
     */
454
    private decodeUri = new Callable("DecodeUri", {
175✔
455
        signature: {
456
            args: [],
457
            returns: ValueKind.String,
458
        },
459
        impl: (_interpreter) => {
460
            return new BrsString(decodeURI(this.intrinsic.value));
2✔
461
        },
462
    });
463

464
    /**
465
     * Encode the specified string with escape sequences for reserved Uniform Resource Identifier
466
     * (URI) component characters.
467
     */
468
    private encodeUriComponent = new Callable("EncodeUriComponent", {
175✔
469
        signature: {
470
            args: [],
471
            returns: ValueKind.String,
472
        },
473
        impl: (_interpreter) => {
474
            return new BrsString(encodeURIComponent(this.intrinsic.value));
3✔
475
        },
476
    });
477

478
    private decodeUriComponent = new Callable("DecodeUriCOmponent", {
175✔
479
        signature: {
480
            args: [],
481
            returns: ValueKind.String,
482
        },
483
        impl: (_interpreter) => {
484
            return new BrsString(decodeURIComponent(this.intrinsic.value));
3✔
485
        },
486
    });
487

488
    /** Checks whether the string starts with the substring specified in matchString, starting at the matchPos parameter (0-based character offset). */
489
    private startsWith = new Callable("startsWith", {
175✔
490
        signature: {
491
            args: [
492
                new StdlibArgument("matchString", ValueKind.String),
493
                new StdlibArgument("position", ValueKind.Int32, BrsInvalid.Instance),
494
            ],
495
            returns: ValueKind.Boolean,
496
        },
497
        impl: (_: Interpreter, matchString: BrsString, position: Int32 | BrsInvalid) => {
498
            if (position instanceof BrsInvalid) {
5✔
499
                return BrsBoolean.from(this.intrinsic.value.startsWith(matchString.value));
2✔
500
            }
501
            return BrsBoolean.from(
3✔
502
                this.intrinsic.value.startsWith(matchString.value, position.getValue())
503
            );
504
        },
505
    });
506

507
    /** Checks whether the string ends with the substring specified in matchString, starting at the position specified in the length parameter. */
508
    private endsWith = new Callable("endsWith", {
175✔
509
        signature: {
510
            args: [
511
                new StdlibArgument("matchString", ValueKind.String),
512
                new StdlibArgument("position", ValueKind.Int32, BrsInvalid.Instance),
513
            ],
514
            returns: ValueKind.Boolean,
515
        },
516
        impl: (_: Interpreter, matchString: BrsString, position: Int32 | BrsInvalid) => {
517
            if (position instanceof BrsInvalid) {
5✔
518
                return BrsBoolean.from(this.intrinsic.value.endsWith(matchString.value));
2✔
519
            }
520
            return BrsBoolean.from(
3✔
521
                this.intrinsic.value.endsWith(matchString.value, position.getValue())
522
            );
523
        },
524
    });
525

526
    private toStr = new Callable("toStr", {
175✔
527
        signature: {
528
            args: [],
529
            returns: ValueKind.String,
530
        },
531
        impl: (_interpreter) => this.intrinsic,
62✔
532
    });
533
}
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