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

graphty-org / graphty-element / 16201911984

10 Jul 2025 05:29PM UTC coverage: 67.128% (+0.9%) from 66.274%
16201911984

push

github

apowers313
refactor: refactor RichTextLabel into multiple classes

469 of 723 branches covered (64.87%)

Branch coverage included in aggregate %.

534 of 950 new or added lines in 7 files covered. (56.21%)

40 existing lines in 2 files now uncovered.

3699 of 5486 relevant lines covered (67.43%)

1232.77 hits per line

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

60.38
/src/RichTextParser.ts
1
import type {RichTextStyle, TextSegment} from "./RichTextLabel.ts";
2

3
export class RichTextParser {
1✔
4
    private readonly defaultStyle: RichTextStyle;
5

6
    constructor(defaultStyle: RichTextStyle) {
1✔
7
        this.defaultStyle = defaultStyle;
2,162✔
8
    }
2,162✔
9

10
    parse(text: string): TextSegment[][] {
1✔
11
        const lines = text.split("\n");
2,162✔
12
        const parsedContent: TextSegment[][] = [];
2,162✔
13

14
        for (const line of lines) {
2,162✔
15
            const segments = this.parseLine(line);
2,701✔
16
            parsedContent.push(segments);
2,701✔
17
        }
2,701✔
18

19
        return parsedContent;
2,162✔
20
    }
2,162✔
21

22
    private parseLine(line: string): TextSegment[] {
1✔
23
        const segments: TextSegment[] = [];
2,701✔
24
        let currentPos = 0;
2,701✔
25

26
        const styleStack: RichTextStyle[] = [
2,701✔
27
            Object.assign({}, this.defaultStyle),
2,701✔
28
        ];
2,701✔
29

30
        const tagRegex = /<(\/?)(bold|italic|color|size|font|bg)(?:='([^']*)')?>/g;
2,701✔
31
        let match;
2,701✔
32

33
        while ((match = tagRegex.exec(line)) !== null) {
2,701!
NEW
34
            if (match.index > currentPos) {
×
NEW
35
                segments.push({
×
NEW
36
                    text: line.substring(currentPos, match.index),
×
NEW
37
                    style: Object.assign({}, styleStack[styleStack.length - 1]),
×
NEW
38
                });
×
NEW
39
            }
×
40

NEW
41
            const isClosing = match[1] === "/";
×
NEW
42
            const tagName = match[2];
×
NEW
43
            const tagValue = match[3];
×
44

NEW
45
            if (isClosing) {
×
NEW
46
                if (styleStack.length > 1) {
×
NEW
47
                    styleStack.pop();
×
NEW
48
                }
×
NEW
49
            } else {
×
NEW
50
                const newStyle = Object.assign({}, styleStack[styleStack.length - 1]);
×
51

NEW
52
                switch (tagName) {
×
NEW
53
                    case "bold":
×
NEW
54
                        newStyle.weight = "bold";
×
NEW
55
                        break;
×
NEW
56
                    case "italic":
×
NEW
57
                        newStyle.style = "italic";
×
NEW
58
                        break;
×
NEW
59
                    case "color":
×
NEW
60
                        newStyle.color = tagValue || this.defaultStyle.color;
×
NEW
61
                        break;
×
NEW
62
                    case "size":
×
NEW
63
                        newStyle.size = parseInt(tagValue || "0") || this.defaultStyle.size;
×
NEW
64
                        break;
×
NEW
65
                    case "font":
×
NEW
66
                        newStyle.font = tagValue || this.defaultStyle.font;
×
NEW
67
                        break;
×
NEW
68
                    case "bg":
×
NEW
69
                        newStyle.background = tagValue || null;
×
NEW
70
                        break;
×
NEW
71
                    default:
×
NEW
72
                        break;
×
NEW
73
                }
×
74

NEW
75
                styleStack.push(newStyle);
×
NEW
76
            }
×
77

NEW
78
            currentPos = match.index + match[0].length;
×
NEW
79
        }
×
80

81
        if (currentPos < line.length) {
2,701✔
82
            segments.push({
2,701✔
83
                text: line.substring(currentPos),
2,701✔
84
                style: Object.assign({}, styleStack[styleStack.length - 1]),
2,701✔
85
            });
2,701✔
86
        }
2,701✔
87

88
        return segments;
2,701✔
89
    }
2,701✔
90

91
    measureText(
1✔
92
        parsedContent: TextSegment[][],
2,162✔
93
        ctx: CanvasRenderingContext2D,
2,162✔
94
        options: {
2,162✔
95
            lineHeight: number;
96
            textOutline: boolean;
97
            textOutlineWidth: number;
98
        },
99
    ): {maxWidth: number, totalHeight: number} {
2,162✔
100
        let maxWidth = 0;
2,162✔
101
        let totalHeight = 0;
2,162✔
102

103
        for (const lineSegments of parsedContent) {
2,162✔
104
            let lineWidth = 0;
2,701✔
105
            let maxLineHeight = 0;
2,701✔
106

107
            for (const segment of lineSegments) {
2,701✔
108
                const {style} = segment;
2,701✔
109

110
                ctx.font = `${style.style} ${style.weight} ${style.size}px ${style.font}`;
2,701✔
111
                const metrics = ctx.measureText(segment.text);
2,701✔
112

113
                lineWidth += metrics.width;
2,701✔
114
                maxLineHeight = Math.max(maxLineHeight, style.size);
2,701✔
115
            }
2,701✔
116

117
            if (options.textOutline) {
2,701✔
118
                lineWidth += options.textOutlineWidth * 2;
154✔
119
                maxLineHeight += options.textOutlineWidth * 2;
154✔
120
            }
154✔
121

122
            maxWidth = Math.max(maxWidth, lineWidth);
2,701✔
123
            totalHeight += maxLineHeight * options.lineHeight;
2,701✔
124
        }
2,701✔
125

126
        return {maxWidth, totalHeight};
2,162✔
127
    }
2,162✔
128
}
1✔
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