@braccato/core 0.1.7 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,274 @@
1
+ /* Main container styles */
2
+ .blyrics-container {
3
+ font-family: var(--blyrics-font-family);
4
+ font-size: var(--blyrics-font-size);
5
+ font-weight: var(--blyrics-font-weight);
6
+ isolation: isolate;
7
+ line-height: var(--blyrics-line-height);
8
+ padding-bottom: calc(var(--blyrics-padding-bottom));
9
+ padding-top: 2rem;
10
+ position: relative !important;
11
+ transition: transform var(--blyrics-lyric-scroll-duration)
12
+ var(--blyrics-lyric-scroll-timing-function) 0s;
13
+ will-change: transform;
14
+ z-index: 1;
15
+ }
16
+
17
+ .blyrics-container > div {
18
+ box-sizing: border-box;
19
+ cursor: pointer;
20
+ padding: var(--blyrics-padding) 0.25em !important;
21
+ transform: scale(var(--blyrics-scale));
22
+ transform-origin: left center;
23
+ }
24
+
25
+ .blyrics--active {
26
+ cursor: default;
27
+ }
28
+
29
+ .blyrics--line {
30
+ color: var(--blyrics-lyric-inactive-color);
31
+ max-width: 100%;
32
+ unicode-bidi: plaintext;
33
+ white-space: normal;
34
+ word-break: normal;
35
+ }
36
+
37
+ .blyrics-line-main {
38
+ display: block;
39
+ max-width: 100%;
40
+ overflow-wrap: normal;
41
+ text-align: inherit;
42
+ unicode-bidi: plaintext;
43
+ white-space: normal;
44
+ }
45
+
46
+ .blyrics-background-line {
47
+ display: block;
48
+ margin-top: 0.1em;
49
+ max-width: 100%;
50
+ text-align: inherit;
51
+ unicode-bidi: plaintext;
52
+ white-space: normal;
53
+ }
54
+
55
+ .blyrics-bidi-run {
56
+ display: inline;
57
+ max-width: 100%;
58
+ unicode-bidi: normal;
59
+ white-space: inherit;
60
+ }
61
+
62
+ .blyrics-word-group {
63
+ display: inline-block;
64
+ max-width: 100%;
65
+ unicode-bidi: normal;
66
+ vertical-align: baseline;
67
+ white-space: nowrap;
68
+ }
69
+
70
+ .blyrics-word-group-long {
71
+ display: inline;
72
+ overflow-wrap: anywhere;
73
+ white-space: normal;
74
+ word-break: break-word;
75
+ }
76
+
77
+ .blyrics--word {
78
+ color: var(--blyrics-lyric-inactive-color);
79
+ display: inline;
80
+ position: relative;
81
+ transform: translateY(0);
82
+ unicode-bidi: normal;
83
+ white-space: inherit;
84
+ }
85
+
86
+ .blyrics-word-group:not(.blyrics-word-group-long) .blyrics--word {
87
+ display: inline-block;
88
+ }
89
+
90
+ .blyrics-bidi-sensitive .blyrics-word-group,
91
+ .blyrics-bidi-sensitive .blyrics-word-group-long {
92
+ display: contents;
93
+ max-width: none;
94
+ overflow-wrap: normal;
95
+ unicode-bidi: normal;
96
+ white-space: normal;
97
+ word-break: normal;
98
+ }
99
+
100
+ .blyrics-bidi-sensitive .blyrics--word,
101
+ .blyrics-bidi-sensitive .blyrics-word-group:not(.blyrics-word-group-long) .blyrics--word {
102
+ display: inline;
103
+ unicode-bidi: normal;
104
+ white-space: inherit;
105
+ }
106
+
107
+ @property --lyric-transition-amount-start {
108
+ syntax: "<number>";
109
+ inherits: false;
110
+ initial-value: -0.2;
111
+ }
112
+
113
+ @property --lyric-transition-amount-end {
114
+ syntax: "<number>";
115
+ inherits: false;
116
+ initial-value: -0.1;
117
+ }
118
+
119
+ .blyrics--word::after,
120
+ .blyrics-word-highlight {
121
+ color: transparent;
122
+ background-image: linear-gradient(
123
+ 90deg,
124
+ var(--blyrics-lyric-active-color)
125
+ calc(
126
+ 100% * var(--lyric-transition-amount-start) - 4rem *
127
+ var(--lyric-transition-amount-start) + 2rem
128
+ ),
129
+ #00000000
130
+ calc(
131
+ 100% * var(--lyric-transition-amount-end) - 4rem *
132
+ var(--lyric-transition-amount-end) + 2rem + 1px
133
+ )
134
+ );
135
+ background-clip: text;
136
+ box-sizing: content-box;
137
+ filter: drop-shadow(0 0 0 var(--blyrics-glow-color));
138
+ left: -2rem;
139
+ opacity: 0;
140
+ padding: 2rem;
141
+ pointer-events: none;
142
+ position: absolute;
143
+ top: -2rem;
144
+ white-space: inherit;
145
+ width: 100%;
146
+ --lyric-transition-amount-start: var(
147
+ --blyrics-highlight-swipe-start-from,
148
+ -0.2
149
+ );
150
+ --lyric-transition-amount-end: var(--blyrics-highlight-swipe-end-from, -0.1);
151
+ }
152
+
153
+ .blyrics--word::after {
154
+ content: attr(data-content);
155
+ }
156
+
157
+ .blyrics-word-highlight {
158
+ user-select: none;
159
+ }
160
+
161
+ .blyrics--word:has(.blyrics-word-highlight)::after {
162
+ content: none;
163
+ }
164
+
165
+ .blyrics-rtl.blyrics--word::after,
166
+ .blyrics--word.blyrics-rtl::after,
167
+ .blyrics-rtl .blyrics-word-highlight,
168
+ .blyrics--word.blyrics-rtl .blyrics-word-highlight {
169
+ background-image: linear-gradient(
170
+ 270deg,
171
+ var(--blyrics-lyric-active-color)
172
+ calc(
173
+ 100% * var(--lyric-transition-amount-start) - 4rem *
174
+ var(--lyric-transition-amount-start) + 2rem
175
+ ),
176
+ #00000000
177
+ calc(
178
+ 100% * var(--lyric-transition-amount-end) - 4rem *
179
+ var(--lyric-transition-amount-end) + 2rem + 1px
180
+ )
181
+ );
182
+ }
183
+
184
+ [blyrics-alt-hover] .blyrics--word:hover {
185
+ text-decoration: underline;
186
+ text-underline-offset: 0.15em;
187
+ }
188
+
189
+ .blyrics--line > .blyrics-line-main > .blyrics-background-lyric,
190
+ .blyrics--line > .blyrics-background-line > .blyrics-background-lyric,
191
+ .blyrics--line > .blyrics-line-main > .blyrics-bidi-run > .blyrics-background-lyric,
192
+ .blyrics--line > .blyrics-background-line > .blyrics-bidi-run > .blyrics-background-lyric {
193
+ font-size: 0.75em;
194
+ font-weight: 500;
195
+ }
196
+
197
+ .blyrics--line > .blyrics-line-main > .blyrics-word-group.blyrics-background-lyric,
198
+ .blyrics--line > .blyrics-background-line > .blyrics-word-group.blyrics-background-lyric,
199
+ .blyrics--line > .blyrics-line-main > .blyrics-bidi-run > .blyrics-word-group.blyrics-background-lyric,
200
+ .blyrics--line > .blyrics-background-line > .blyrics-bidi-run > .blyrics-word-group.blyrics-background-lyric {
201
+ color: color-mix(in srgb, var(--blyrics-lyric-inactive-color) 78%, transparent);
202
+ }
203
+
204
+ .blyrics-explicit {
205
+ text-decoration: line-through;
206
+ text-decoration-thickness: 0.08em;
207
+ }
208
+
209
+ .blyrics--translated,
210
+ .blyrics--romanized {
211
+ color: var(--blyrics-translated-color);
212
+ display: block;
213
+ font-family: var(--blyrics-translated-font-family);
214
+ font-size: var(--blyrics-translated-font-size);
215
+ font-weight: var(--blyrics-translated-font-weight);
216
+ line-height: 1.1;
217
+ margin-top: 8px;
218
+ max-width: 100%;
219
+ text-align: inherit;
220
+ unicode-bidi: plaintext;
221
+ white-space: normal;
222
+ }
223
+
224
+ .blyrics--romanized {
225
+ background: rgba(255, 255, 255, 0.05);
226
+ border: 1px solid rgba(255, 255, 255, 0.1);
227
+ border-radius: 1rem;
228
+ box-sizing: border-box;
229
+ display: block;
230
+ font-size: calc(var(--blyrics-translated-font-size) / 1.25);
231
+ padding: 0.375rem 0.75rem;
232
+ width: fit-content;
233
+ }
234
+
235
+ .blyrics--romanized .blyrics-line-main {
236
+ display: inline;
237
+ }
238
+
239
+ .blyrics-rtl {
240
+ direction: rtl;
241
+ }
242
+
243
+ .blyrics--line[data-direction="rtl"] {
244
+ text-align: right;
245
+ transform-origin: right center;
246
+ }
247
+
248
+ /* Secondary and tertiary vocals */
249
+ .blyrics--line[data-agent="v2"],
250
+ .blyrics--line[data-agent="v3"] {
251
+ text-align: right;
252
+ transform-origin: right center !important;
253
+ }
254
+
255
+ /* All vocals together */
256
+ .blyrics--line[data-agent="v1000"] {
257
+ text-align: center;
258
+ transform-origin: center center !important;
259
+ }
260
+
261
+ .blyrics--line[data-agent="v2"] .blyrics--romanized,
262
+ .blyrics--line[data-agent="v3"] .blyrics--romanized,
263
+ .blyrics--line[data-direction="rtl"] .blyrics--romanized {
264
+ margin-left: auto;
265
+ }
266
+
267
+ .blyrics--line[data-agent="v1000"] .blyrics--romanized {
268
+ margin-left: auto;
269
+ margin-right: auto;
270
+ }
271
+
272
+ #blyrics-wrapper {
273
+ margin-top: 0;
274
+ }
@@ -0,0 +1,181 @@
1
+ :root {
2
+ /* Colors */
3
+ --blyrics-ui-text-color: var(
4
+ --blyrics-text-color,
5
+ color(display-p3 1 1 1 / 1)
6
+ );
7
+ --blyrics-glow-color: var(
8
+ --blyrics-highlight-color,
9
+ color(display-p3 1 1 1 / 0.5)
10
+ );
11
+ --blyrics-error-color: color(display-p3 0.992 0.882 0.882);
12
+ --blyrics-footer-bg-color: hsla(0, 0%, 100%, 0.1);
13
+ --blyrics-footer-border-color: hsla(0, 0%, 100%, 0.1);
14
+ --blyrics-footer-text-color: #aaa;
15
+ --blyrics-footer-link-color: #fff;
16
+ --blyrics-discord-hover-color: #5865f2;
17
+ --blyrics-vote-hover-color: hsla(0, 0%, 100%, 0.2);
18
+ --blyrics-lyric-active-color: var(
19
+ --blyrics-text-color,
20
+ color(display-p3 1 1 1 / var(--blyrics-active-opacity, 1))
21
+ );
22
+ --blyrics-lyric-inactive-color: color(
23
+ from var(--blyrics-text-color, color(display-p3 1 1 1)) display-p3 r g b /
24
+ var(--blyrics-inactive-opacity, 0.3)
25
+ );
26
+
27
+ --blyrics-richsync-timing-offset: 0.150s;
28
+ --blyrics-timing-offset: 0.115s;
29
+
30
+ --blyrics-scroll-timing-offset: 0.5s;
31
+
32
+ /* Typography */
33
+ /* The stack behind --noto-sans-universal is 32 Noto families and the extension is what loads
34
+ them, so the declaration stays in misc.css, where two published themes select on it by name.
35
+ Inside the extension the fallback is unreachable. It is one real family rather than a generic
36
+ so that a standalone consumer without it still falls through to the rest of this stack; the
37
+ CJK, Devanagari, Arabic, Hebrew and Thai coverage is what such a consumer gives up either
38
+ way. */
39
+ --blyrics-font-family:
40
+ Satoshi, var(--noto-sans-universal, "Noto Sans"), Avenir, -apple-system,
41
+ BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans,
42
+ Helvetica Neue, sans-serif;
43
+ --blyrics-font-size: 3rem;
44
+ --blyrics-font-weight: 700;
45
+ --blyrics-line-height: 1.333;
46
+
47
+ --blyrics-translated-font-size: 2rem;
48
+ --blyrics-translated-font-weight: 600;
49
+ --blyrics-translated-font-family: var(--blyrics-font-family);
50
+ --blyrics-translated-color: color(
51
+ display-p3 1 1 1 / var(--blyrics-translated-opacity, 0.6)
52
+ );
53
+
54
+ --blyrics-footer-font-family: Satoshi, Roboto, Noto Naskh Arabic UI, Arial, sans-serif;
55
+ --blyrics-footer-font-size: 14px;
56
+ --blyrics-footer-font-weight: 400;
57
+
58
+ /* Animations */
59
+ --blyrics-animate-line-scale: 1;
60
+ --blyrics-animate-word-wobble: 1;
61
+ --blyrics-animate-highlight-swipe: 1;
62
+ --blyrics-animate-highlight-glow: 1;
63
+ --blyrics-animate-highlight-fade: 1;
64
+ --blyrics-animate-scroll: 1;
65
+ --blyrics-animate-instrumental: 1;
66
+
67
+ --blyrics-loader-transition-duration: 0.6s;
68
+ --blyrics-loader-transition-easing: cubic-bezier(0.22, 1, 0.36, 1);
69
+ --blyrics-scale-transition-duration: var(
70
+ --blyrics-transition-duration,
71
+ 0.166s
72
+ );
73
+ --blyrics-line-enter-transform-from: scale(var(--blyrics-scale));
74
+ --blyrics-line-enter-transform-to: scale(var(--blyrics-active-scale));
75
+ --blyrics-line-exit-transform-from: scale(var(--blyrics-active-scale));
76
+ --blyrics-line-exit-transform-to: scale(var(--blyrics-scale));
77
+ --blyrics-line-enter-easing: ease;
78
+ --blyrics-line-exit-easing: ease;
79
+
80
+ --blyrics-lyric-highlight-fade-in-duration: var(
81
+ --blyrics-opacity-transition,
82
+ 0.33s
83
+ );
84
+ --blyrics-lyric-highlight-fade-out-duration: var(
85
+ --blyrics-opacity-transition,
86
+ 0.5s
87
+ );
88
+ --blyrics-lyric-highlight-fade-in-easing: ease;
89
+ --blyrics-lyric-highlight-fade-out-easing: ease;
90
+ --blyrics-highlight-swipe-easing: linear;
91
+ --blyrics-highlight-swipe-start-from: -0.2;
92
+ --blyrics-highlight-swipe-end-from: -0.1;
93
+ --blyrics-highlight-swipe-start-to: 1.4;
94
+ --blyrics-highlight-swipe-end-to: 1.5;
95
+ --blyrics-highlight-glow-radius-from: 0.8rem;
96
+ --blyrics-highlight-glow-radius-to: 0;
97
+ --blyrics-highlight-glow-duration-ratio: 1.2;
98
+ --blyrics-highlight-glow-min-duration: 1.2s;
99
+ --blyrics-highlight-glow-easing: ease;
100
+
101
+ --blyrics-wobble-duration: 1s;
102
+ --blyrics-word-wobble-transform-from: scaleX(1);
103
+ --blyrics-word-wobble-transform-peak: translateX(0.05em) scaleX(1.025);
104
+ --blyrics-word-wobble-transform-settle: translateX(0) scaleX(1);
105
+ --blyrics-word-wobble-transform-to: scaleX(1);
106
+ --blyrics-word-wobble-peak-offset: 0.125;
107
+ --blyrics-word-wobble-settle-offset: 0.75;
108
+ --blyrics-word-wobble-easing: ease;
109
+ --blyrics-word-wobble-peak-easing: ease-in-out;
110
+ --blyrics-word-wobble-end-easing: ease-out;
111
+
112
+ --blyrics-instrumental-fill-fade-duration: 150ms;
113
+ --blyrics-instrumental-fill-fade-easing: ease;
114
+ --blyrics-instrumental-fill-transform-from: translateY(78%);
115
+ --blyrics-instrumental-fill-transform-to: translateY(-4%);
116
+ --blyrics-instrumental-fill-easing: linear;
117
+ --blyrics-instrumental-wave-transform-from: scaleY(1.2);
118
+ --blyrics-instrumental-wave-transform-to: scaleY(0.0001);
119
+ --blyrics-instrumental-wave-easing: ease-in;
120
+ --blyrics-instrumental-wave-oscillation-duration: 1.25s;
121
+ --blyrics-instrumental-wave-oscillation-easing: ease-in-out;
122
+
123
+ /* Layout */
124
+ --blyrics-padding: 2rem;
125
+ --blyrics-margin: 2rem;
126
+ --blyrics-small-border-radius: 1rem;
127
+ --blyrics-border-radius: 1000rem;
128
+
129
+ /* Effects */
130
+ --blyrics-blur-amount: 30px;
131
+ --blyrics-scale: 0.95;
132
+ --blyrics-active-scale: 1;
133
+ --blyrics-error-opacity: 0.33;
134
+ --blyrics-background-blur: 100px;
135
+ --blyrics-background-saturate: 2;
136
+
137
+ /* Lyric Transition Properties */
138
+ --blyrics-lyric-scroll-duration: var(
139
+ --blyrics-lyric-transition-duration,
140
+ 650ms
141
+ );
142
+ --blyrics-lyric-scroll-timing-function: var(
143
+ --blyrics-lyric-transition-timing-function,
144
+ cubic-bezier(0.86, 0, 0.2, 1)
145
+ );
146
+
147
+ /* Gradient stops */
148
+ --blyrics-gradient-stops:
149
+ transparent 0%, rgba(0, 0, 0, 0.013) 8.1%, rgba(0, 0, 0, 0.049) 15.5%,
150
+ rgba(0, 0, 0, 0.104) 22.5%, rgba(0, 0, 0, 0.175) 29%,
151
+ rgba(0, 0, 0, 0.259) 35.3%, rgba(0, 0, 0, 0.352) 41.2%,
152
+ rgba(0, 0, 0, 0.45) 47.1%, rgba(0, 0, 0, 0.55) 52.9%,
153
+ rgba(0, 0, 0, 0.648) 58.8%, rgba(0, 0, 0, 0.741) 64.7%,
154
+ rgba(0, 0, 0, 0.825) 71%, rgba(0, 0, 0, 0.896) 77.5%,
155
+ rgba(0, 0, 0, 0.951) 84.5%, rgba(0, 0, 0, 0.987) 91.9%, #000 100%;
156
+
157
+ --blyrics-shimmer-gradient: linear-gradient(
158
+ 110deg,
159
+ color-mix(in srgb, var(--blyrics-ui-text-color) 50%, transparent) 30%,
160
+ var(--blyrics-ui-text-color) 50%,
161
+ color-mix(in srgb, var(--blyrics-ui-text-color) 50%, transparent) 70%
162
+ );
163
+ }
164
+
165
+ /* Reduced-motion disables the vestibular triggers (zoom-on-active line scale,
166
+ translateX word wobble, and instrumental fill/wave motion) and flattens the
167
+ active/inactive scale delta. Smooth scroll remains enabled, but the animation
168
+ engine suppresses side-specific line-scroll differential effects.
169
+ Everything else is left on by design:
170
+ - karaoke swipe + highlight glow: gradient mask / drop-shadow fills, not movement.
171
+ - highlight fade-in / fade-out: opacity transitions are not motion. */
172
+ @media (prefers-reduced-motion: reduce) {
173
+ :root {
174
+ --blyrics-animate-line-scale: 0;
175
+ --blyrics-animate-word-wobble: 0;
176
+ --blyrics-animate-instrumental: 0;
177
+
178
+ --blyrics-scale: 1;
179
+ --blyrics-active-scale: 1;
180
+ }
181
+ }
package/dist/text.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export declare const testRtl: (text: string) => boolean;
2
+ /**
3
+ * Checks if a given string contains any non-Latin characters.
4
+ * @param text The string to check.
5
+ * @returns True if a non-Latin character is found, otherwise false.
6
+ */
7
+ export declare function containsNonLatin(text: string): boolean;
8
+ declare const SCRIPT_TO_LANG: readonly [readonly [RegExp, "ja"], readonly [RegExp, "ko"], readonly [RegExp, "zh"], readonly [RegExp, "ru"], readonly [RegExp, "hi"], readonly [RegExp, "ar"], readonly [RegExp, "th"], readonly [RegExp, "el"], readonly [RegExp, "he"], readonly [RegExp, "bn"], readonly [RegExp, "ta"], readonly [RegExp, "te"], readonly [RegExp, "ml"], readonly [RegExp, "kn"], readonly [RegExp, "gu"], readonly [RegExp, "pa"], readonly [RegExp, "si"], readonly [RegExp, "my"], readonly [RegExp, "ka"], readonly [RegExp, "km"], readonly [RegExp, "lo"]];
9
+ type NonLatinLanguageCode = (typeof SCRIPT_TO_LANG)[number][1];
10
+ export declare function detectNonLatinLanguage(text: string): NonLatinLanguageCode | null;
11
+ export {};
package/dist/text.js ADDED
@@ -0,0 +1,54 @@
1
+ // Script detection for the text the module renders. Pure string work, no DOM and no host state.
2
+ export const testRtl = (text) => /[\p{Script=Arabic}\p{Script=Hebrew}\p{Script=Syriac}\p{Script=Thaana}]/u.test(text);
3
+ /**
4
+ * This regex is designed to detect any characters that are outside of the
5
+ * standard "Basic Latin" and "Latin-1 Supplement" Unicode blocks, as well
6
+ * as common "smart" punctuation like curved quotes.
7
+ *
8
+ * How it works:
9
+ * [^...] - This is a negated set, which matches any character NOT inside the brackets.
10
+ * \x00-\xFF - This range covers both the "Basic Latin" (ASCII) and "Latin-1 Supplement"
11
+ * blocks. This includes English letters, numbers, common punctuation, and
12
+ * most accented characters used in Western European languages (e.g., á, ö, ñ).
13
+ * ‘-” - This range covers common "smart" or curly punctuation, including single
14
+ * and double quotation marks/apostrophes (‘, ’, “, ”).
15
+ */
16
+ const nonLatinRegex = /[^\p{Script_Extensions=Latin}\p{Script_Extensions=Common}]/u;
17
+ /**
18
+ * Checks if a given string contains any non-Latin characters.
19
+ * @param text The string to check.
20
+ * @returns True if a non-Latin character is found, otherwise false.
21
+ */
22
+ export function containsNonLatin(text) {
23
+ return nonLatinRegex.test(text);
24
+ }
25
+ const SCRIPT_TO_LANG = [
26
+ [/\p{Script=Hiragana}|\p{Script=Katakana}/u, "ja"],
27
+ [/\p{Script=Hangul}/u, "ko"],
28
+ [/\p{Script=Han}/u, "zh"],
29
+ [/\p{Script=Cyrillic}/u, "ru"],
30
+ [/\p{Script=Devanagari}/u, "hi"],
31
+ [/\p{Script=Arabic}/u, "ar"],
32
+ [/\p{Script=Thai}/u, "th"],
33
+ [/\p{Script=Greek}/u, "el"],
34
+ [/\p{Script=Hebrew}/u, "he"],
35
+ [/\p{Script=Bengali}/u, "bn"],
36
+ [/\p{Script=Tamil}/u, "ta"],
37
+ [/\p{Script=Telugu}/u, "te"],
38
+ [/\p{Script=Malayalam}/u, "ml"],
39
+ [/\p{Script=Kannada}/u, "kn"],
40
+ [/\p{Script=Gujarati}/u, "gu"],
41
+ [/\p{Script=Gurmukhi}/u, "pa"],
42
+ [/\p{Script=Sinhala}/u, "si"],
43
+ [/\p{Script=Myanmar}/u, "my"],
44
+ [/\p{Script=Georgian}/u, "ka"],
45
+ [/\p{Script=Khmer}/u, "km"],
46
+ [/\p{Script=Lao}/u, "lo"],
47
+ ];
48
+ export function detectNonLatinLanguage(text) {
49
+ for (const [regex, lang] of SCRIPT_TO_LANG) {
50
+ if (regex.test(text))
51
+ return lang;
52
+ }
53
+ return null;
54
+ }
@@ -0,0 +1,23 @@
1
+ /** @public */
2
+ export declare class Setting {
3
+ readonly type: "number" | "boolean" | "string";
4
+ value: number | boolean | string;
5
+ readonly defaultValue: number | boolean | string;
6
+ readonly requiresLyricReload: boolean;
7
+ private manuallySet;
8
+ constructor(type: "number" | "boolean" | "string", value: number | boolean | string, defaultValue: number | boolean | string, requiresLyricReload: boolean);
9
+ getNumberValue(): number;
10
+ getBooleanValue(): boolean;
11
+ getStringValue(): string;
12
+ isManuallySet(): boolean;
13
+ setManuallySet(manuallySet: boolean): void;
14
+ }
15
+ /**
16
+ * The `blyrics-*` configuration a compiled stylesheet declares, in declaration order, last one
17
+ * winning. Lives here rather than beside the renderer because it depends on nothing and its only
18
+ * consumer is `setThemeSettings`: a consumer that compiles a theme somewhere the renderer does not
19
+ * run can read the settings out of it without pulling the engine in.
20
+ */
21
+ export declare function parseThemeConfig(css: string): Map<string, string>;
22
+ export declare function registerThemeSetting(key: string, defaultValue: number | boolean | string, requiresLyricReload?: boolean): Setting;
23
+ export declare function setThemeSettings(map: Map<string, string>): boolean;
@@ -0,0 +1,126 @@
1
+ // The renderer declares the theme settings its own code reads, so a setting and the code that
2
+ // consumes it stay together. Applying settings reports whether the lyrics need reloading; the host
3
+ // decides what to do about it.
4
+ //
5
+ // What arrives here is compiled CSS rather than theme source. Better Lyrics themes are written in
6
+ // RICS and compiled with the `rics` package first, which is the consumer's dependency and not this
7
+ // module's: nothing under this directory ships with any.
8
+ let keyToSettingMap = new Map();
9
+ // -- Setting --------------------------------------------
10
+ // `registerThemeSetting` returns one of these, so a consumer of this leaf has to be able to spell
11
+ // it. `@public` keeps knip off an export this repo only ever names through inference.
12
+ /** @public */
13
+ export class Setting {
14
+ type;
15
+ value;
16
+ defaultValue;
17
+ requiresLyricReload;
18
+ manuallySet = false;
19
+ constructor(type, value, defaultValue, requiresLyricReload) {
20
+ this.type = type;
21
+ this.value = value;
22
+ this.defaultValue = defaultValue;
23
+ this.requiresLyricReload = requiresLyricReload;
24
+ }
25
+ getNumberValue() {
26
+ return this.value;
27
+ }
28
+ getBooleanValue() {
29
+ return this.value;
30
+ }
31
+ getStringValue() {
32
+ return this.value;
33
+ }
34
+ isManuallySet() {
35
+ return this.manuallySet;
36
+ }
37
+ setManuallySet(manuallySet) {
38
+ this.manuallySet = manuallySet;
39
+ }
40
+ }
41
+ // -- Parsing a theme --------------------------------------------
42
+ // A theme is a stylesheet, and it configures the renderer through comments inside it, in the form
43
+ // `blyrics-some-key = value;`. Only inside comments: everything outside one is CSS the browser is
44
+ // going to read, and a stylesheet must not be able to configure the module by accident.
45
+ const THEME_COMMENT_PATTERN = /\/\*([\s\S]*?)\*\//g;
46
+ const THEME_SETTING_PATTERN = /(blyrics-[\w-]+)\s*=\s*([^;]+);/g;
47
+ /**
48
+ * The `blyrics-*` configuration a compiled stylesheet declares, in declaration order, last one
49
+ * winning. Lives here rather than beside the renderer because it depends on nothing and its only
50
+ * consumer is `setThemeSettings`: a consumer that compiles a theme somewhere the renderer does not
51
+ * run can read the settings out of it without pulling the engine in.
52
+ */
53
+ export function parseThemeConfig(css) {
54
+ const config = new Map();
55
+ // `matchAll` works off a copy of the pattern, so neither of the two shared at module scope can
56
+ // carry a `lastIndex` out of one stylesheet and into the next however these loops are nested.
57
+ // Driving them with `exec` is correct too, as long as every loop is left running to exhaustion:
58
+ // this is written not to depend on that.
59
+ for (const [, comment] of css.matchAll(THEME_COMMENT_PATTERN)) {
60
+ for (const [, key, value] of comment.matchAll(THEME_SETTING_PATTERN)) {
61
+ config.set(key, value.trim());
62
+ }
63
+ }
64
+ return config;
65
+ }
66
+ // -- Registry --------------------------------------------
67
+ // Registration runs at module scope, which evaluates once per bundle however many views that bundle
68
+ // is rendering, so a setting's value is shared by all of them rather than held per instance. The
69
+ // unit is a bundle rather than a document: this extension bundles the module into the isolated world
70
+ // and the page world separately, and each of those registries needs its own theme applied to it or
71
+ // one view renders against defaults while the other renders against the theme.
72
+ export function registerThemeSetting(key, defaultValue, requiresLyricReload = false) {
73
+ let type = typeof defaultValue;
74
+ if (type !== "number" && type !== "boolean" && type !== "string") {
75
+ throw new Error("Invalid type for theme setting");
76
+ }
77
+ let setting = new Setting(type, defaultValue, defaultValue, requiresLyricReload);
78
+ keyToSettingMap.set(key, setting);
79
+ return setting;
80
+ }
81
+ // Returns whether a setting flagged requiresLyricReload changed, so the caller can reload.
82
+ export function setThemeSettings(map) {
83
+ let needsLyricReload = false;
84
+ map.forEach((value, key) => {
85
+ let setting = keyToSettingMap.get(key);
86
+ if (setting) {
87
+ let lastValue = setting.value;
88
+ if (setting.type === "number") {
89
+ const parsed = parseFloat(value);
90
+ if (isNaN(parsed)) {
91
+ setting.value = setting.defaultValue;
92
+ setting.setManuallySet(false);
93
+ }
94
+ else {
95
+ setting.value = parsed;
96
+ setting.setManuallySet(true);
97
+ }
98
+ }
99
+ else if (setting.type === "boolean") {
100
+ setting.value = value.toLowerCase() === "true";
101
+ setting.setManuallySet(true);
102
+ }
103
+ else {
104
+ setting.value = value;
105
+ setting.setManuallySet(true);
106
+ }
107
+ if (setting.requiresLyricReload && lastValue !== setting.value) {
108
+ needsLyricReload = true;
109
+ }
110
+ }
111
+ });
112
+ // second pass reset undefined values to their default values
113
+ for (const [key, setting] of keyToSettingMap.entries()) {
114
+ if (!map.has(key) && setting.value !== setting.defaultValue) {
115
+ setting.value = setting.defaultValue;
116
+ setting.setManuallySet(false);
117
+ if (setting.requiresLyricReload) {
118
+ needsLyricReload = true;
119
+ }
120
+ }
121
+ else if (!map.has(key)) {
122
+ setting.setManuallySet(false);
123
+ }
124
+ }
125
+ return needsLyricReload;
126
+ }