kashi 3.0.2 → 4.0.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.
package/README.md CHANGED
@@ -20,9 +20,9 @@ This project is a dependency-free library that aims to provide a way to correctl
20
20
  - [x] ✉️ Implements the Observer pattern and emits events at each step of the process whenever something changes
21
21
  - [x] ✏️ Allows you to enter custom text when the lyrics line is empty
22
22
  - [x] 🎤 Synchronizes the lyrics with the music that is playing
23
- - [ ] 🎩 Supports multiple lyrics for the same song (useful to keep track of the original lyrics and their translation)
24
- - [ ] 🧞 Supports the [Walaoke extension](https://en.wikipedia.org/wiki/LRC_(file_format)#Walaoke_extension)
25
- - [ ] 🕖️ Supports the [A2 extension](https://en.wikipedia.org/wiki/LRC_(file_format)#A2_extension_(Enhanced_LRC_format))
23
+ - [x] 🎩 Supports multiple lyrics for the same song (useful to keep track of the original lyrics and their translation)
24
+ - [ ] 🧞 Supports the [Walaoke extension](<https://en.wikipedia.org/wiki/LRC_(file_format)#Walaoke_extension>)
25
+ - [ ] 🕖️ Supports the [A2 extension](<https://en.wikipedia.org/wiki/LRC_(file_format)#A2_extension_(Enhanced_LRC_format)>)
26
26
 
27
27
  > **Note:** Support for fetching lyrics using a URL has been removed because it only supported public URLs.
28
28
  >
@@ -58,7 +58,7 @@ The project is also exported using [UMD](https://github.com/umdjs/umd), which me
58
58
  "use strict";
59
59
 
60
60
  new Kashi({
61
- file. // Loaded from an input[type="file"] or anywhere else
61
+ files. // Loaded from some input[type="file"] or anywhere else
62
62
  container: document.getElementById("kashi"),
63
63
  });
64
64
  ```
@@ -83,7 +83,7 @@ import { Kashi } from "kashi";
83
83
  // Usage is essentially the same as in the previous section
84
84
 
85
85
  new Kashi({
86
- file. // Loaded from an input[type="file"] or anywhere else
86
+ files. // Loaded from some input[type="file"] or anywhere else
87
87
  container: document.getElementById("kashi"),
88
88
  });
89
89
  ```
@@ -129,7 +129,7 @@ export const KashiWrapper = memo(
129
129
 
130
130
  new Kashi({
131
131
  ...rest,
132
- file: response.data,
132
+ files: [response.data],
133
133
  container: ref.current,
134
134
  });
135
135
  } catch (error) {
@@ -150,6 +150,14 @@ export const KashiWrapper = memo(
150
150
  return <div className="kashi-wrapper" ref={ref} />;
151
151
  },
152
152
  (prevProps, nextProps) => {
153
+ function compareArrays(arr1: unknown[], arr2: unknown[]) {
154
+ if (arr1.length !== arr2.length) {
155
+ return false;
156
+ }
157
+
158
+ return arr1.every((item, index) => item === arr2[index]);
159
+ }
160
+
153
161
  function compareObjects(
154
162
  obj1: Omit<KashiProps, "container">,
155
163
  obj2: Omit<KashiProps, "container">,
@@ -166,7 +174,14 @@ export const KashiWrapper = memo(
166
174
  }
167
175
 
168
176
  for (const key of keys1) {
169
- if (obj1[key] !== obj2[key]) {
177
+ const val1 = obj1[key];
178
+ const val2 = obj2[key];
179
+
180
+ if (Array.isArray(val1) && Array.isArray(val2)) {
181
+ if (!compareArrays(val1, val2)) {
182
+ return false;
183
+ }
184
+ } else if (val1 !== val2) {
170
185
  return false;
171
186
  }
172
187
  }
@@ -183,49 +198,46 @@ export const KashiWrapper = memo(
183
198
 
184
199
  You must pass some properties to Kashi to define what lyrics display and where. Here are its specifications:
185
200
 
186
- | Property | Type | Default value | Is required? | Description |
187
- | --------------- | ------------------ | ------------- | ------------ | ----------------------------------------- |
188
- | `file` | `Blob` (or `File`) | - | Yes | Lyrics file |
189
- | `container` | `HTMLDivElement` | - | Yes | Element where the lyrics will be inserted |
190
- | `emptyLineText` | `string` | `...` | No | Custom text for empty lines of the lyrics |
191
- | `noLyricsText` | `string` | - | No | Custom text for when there are no lyrics |
201
+ | Property | Type | Default value | Is required? | Description |
202
+ | --------------- | ---------------- | ------------- | ------------ | ----------------------------------------- |
203
+ | `files` | `Blob[]` | - | Yes | Lyrics files |
204
+ | `container` | `HTMLDivElement` | - | Yes | Element where the lyrics will be inserted |
205
+ | `emptyLineText` | `string` | `...` | No | Custom text for empty lines of the lyrics |
206
+ | `noLyricsText` | `string` | - | No | Custom text for when there are no lyrics |
192
207
 
193
208
  ## 👾 Generated HTML structure
194
209
 
195
210
  The `div#kashi` represents the `container` passed to `Kashi` where the song lyrics will be inserted.
196
211
 
197
- Each line of lyrics present in the lrc file will be wrapped by a `<p></p>` tag and inserted into the `container`.
212
+ Each line of lyrics present in the lrc files will be wrapped by a `<div></div>` tag and inserted into the `container`.
198
213
 
199
214
  Here's an example:
200
215
 
201
216
  ```html
202
217
  <div id="kashi">
203
- <p>
204
- <span data-time="00:17.55" data-ms-time="17550" data-empty="false" data-aria-current="false">
205
- Telling myself, "I won't go there"
206
- </span>
207
- <br/>
208
- <span data-time="00:21.24" data-ms-time="21240" data-empty="false" data-aria-current="false">
209
- Oh, but I know that I won't care
210
- </span>
211
- <br/>
212
- <span data-time="00:24.71" data-ms-time="24710" data-empty="false" data-aria-current="false">
213
- Tryna wash away all the blood I've spilt
214
- </span>
215
- <br/>
216
- <span data-time="00:31.95" data-ms-time="31950" data-empty="false" data-aria-current="false">
217
- This lust is a burden that we both share
218
- </span>
219
- <br/>
220
- <span data-time="00:35.60" data-ms-time="35600" data-empty="false" data-aria-current="false">
221
- Two sinners can't atone from a lone prayer
222
- </span>
223
- <br/>
224
- <span data-time="00:39.28" data-ms-time="39280" data-empty="false" data-aria-current="false">
225
- Souls tied, intertwined by our pride and guilt
226
- </span>
227
- <!-- ... -->
228
- </p>
218
+ <div
219
+ data-time="00:17.55"
220
+ data-ms-time="17550"
221
+ data-empty="false"
222
+ data-aria-current="false"
223
+ >
224
+ <span>Telling myself, "I won't go there"</span>
225
+ <br />
226
+ <span> Dizendo a mim mesmo, "eu não vou lá" </span>
227
+ </div>
228
+
229
+ <div
230
+ data-time="00:21.24"
231
+ data-ms-time="21240"
232
+ data-empty="false"
233
+ data-aria-current="false"
234
+ >
235
+ <span>Oh, but I know that I won't care</span>
236
+ <br />
237
+ <span>Oh, mas eu sei que não vou me importar</span>
238
+ </div>
239
+
240
+ <!-- ... -->
229
241
  </div>
230
242
  ```
231
243
 
@@ -233,17 +245,17 @@ Here's an example:
233
245
 
234
246
  The instance generated by `Kashi` has some public methods and attributes that can be used to query or change properties on the fly.
235
247
 
236
- | Name | Type | Description |
237
- | ------------------ | --------- | ------------------------------------------------------------ |
238
- | `file` | Attribute | Returns the file from the current lyrics |
239
- | `emptyLineText` | Attribute | Returns the text set for empty lines |
240
- | `noLyricsText` | Attribute | Returns the text set for when there are no lyrics |
241
- | `setFile` | Method | Function capable of changing the current lyrics file by passing the the new **file** |
242
- | `setEmptyLineText` | Method | Function capable of changing the text defined for empty lines |
243
- | `setNoLyricsText` | Method | Function capable of changing the text defined for when there are no lyrics |
248
+ | Name | Type | Description |
249
+ | ------------------ | --------- | -------------------------------------------------------------------------------------- |
250
+ | `files` | Attribute | Returns the files from the current lyrics |
251
+ | `emptyLineText` | Attribute | Returns the text set for empty lines |
252
+ | `noLyricsText` | Attribute | Returns the text set for when there are no lyrics |
253
+ | `setFiles` | Method | Function capable of changing the current lyrics files by passing the the new **files** |
254
+ | `setEmptyLineText` | Method | Function capable of changing the text defined for empty lines |
255
+ | `setNoLyricsText` | Method | Function capable of changing the text defined for when there are no lyrics |
244
256
  | `subscribe` | Method | Function capable of defining a callback to be executed when a given event is triggered |
245
- | `unsubscribe` | Method | Function capable of making a callback to stop listening to an event |
246
- | `notify` | Method | Function capable of triggering an event |
257
+ | `unsubscribe` | Method | Function capable of making a callback to stop listening to an event |
258
+ | `notify` | Method | Function capable of triggering an event |
247
259
 
248
260
  ## 🍾 Events
249
261
 
@@ -251,7 +263,7 @@ When creating a new instance using `Kashi` you will have access to the `subscrib
251
263
 
252
264
  | Event | Data | Trigger |
253
265
  | ------------------- | --------------------------- | ------------------------------------------ |
254
- | `fileSet` | `{ file: Blob }` | When calling the `setFile` method |
266
+ | `filesSet` | `{ files: Blob[] }` | When calling the `setFiles` method |
255
267
  | `emptyLineTextSet` | `{ emptyLineText: string }` | When calling the `setEmptyLineText` method |
256
268
  | `noLyricsTextSet` | `{ noLyricsText: text }` | When calling the `setNoLyricsText` method |
257
269
  | `lyricLinesUpdated` | `{ lyricLines: string[] }` | When inserting/updating lyrics in HTML |
package/kashi.d.ts CHANGED
@@ -5,7 +5,7 @@ export type EventName = string;
5
5
  export type EventData = any;
6
6
  export type EventCallback = (data: EventData) => void;
7
7
  export interface KashiProps {
8
- file?: KashiFile;
8
+ files?: KashiFile[];
9
9
  container: HTMLDivElement;
10
10
  emptyLineText?: string;
11
11
  audioPlayer?: HTMLAudioElement;
@@ -14,10 +14,10 @@ export interface KashiProps {
14
14
  export declare class Kashi {
15
15
  #private;
16
16
  constructor(props: KashiProps);
17
- get file(): KashiFile;
17
+ get files(): KashiFile[];
18
18
  get emptyLineText(): string;
19
19
  get noLyricsText(): string;
20
- setFile(file: KashiFile): Promise<void>;
20
+ setFiles(files?: KashiFile[]): Promise<void>;
21
21
  setEmptyLineText(text: string): void;
22
22
  setNoLyricsText(text: string): void;
23
23
  subscribe(event: EventName, fn: EventCallback): void;
package/kashi.js CHANGED
@@ -1 +1 @@
1
- !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var i=t();for(var n in i)("object"==typeof exports?exports:e)[n]=i[n]}}(self,()=>(()=>{"use strict";var e={d:(t,i)=>{for(var n in i)e.o(i,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:i[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{Kashi:()=>T});const i=/\[\d{2}:\d{2}.\d{2}\]/,n=/^\[\d{2}:\d{2}.\d{2}\](.*)$/;var r,s,a,o,l,f,h,c,d,u,p,y,m,w=function(e,t,i,n,r){if("m"===n)throw new TypeError("Private method is not writable");if("a"===n&&!r)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof t?e!==t||!r:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===n?r.call(e,i):r?r.value=i:t.set(e,i),i},b=function(e,t,i,n){if("a"===i&&!n)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!n:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===i?n:"a"===i?n.call(e):n?n.value:t.get(e)};class T{constructor(e){if(r.add(this),s.set(this,void 0),a.set(this,[]),o.set(this,void 0),l.set(this,new Map),f.set(this,void 0),h.set(this,null),c.set(this,!1),d.set(this,void 0),!(e.container&&e.container instanceof HTMLDivElement))throw new Error("Container must be an instance of HTMLDivElement.");if(e.emptyLineText&&this.setEmptyLineText(e.emptyLineText),e.audioPlayer&&!(e.audioPlayer instanceof HTMLAudioElement))throw new Error("Audio player must be an instance of HTMLAudioElement.");e.audioPlayer&&(w(this,h,e.audioPlayer,"f"),b(this,h,"f").addEventListener("timeupdate",()=>{b(this,r,"m",m).call(this)}),b(this,h,"f").addEventListener("seeked",()=>{b(this,o,"f").querySelectorAll("p > span").forEach(e=>{e instanceof HTMLSpanElement&&(e.dataset.ariaCurrent="false")}),b(this,r,"m",m).call(this)})),e.enableAutoScroll&&w(this,c,e.enableAutoScroll,"f"),w(this,o,e.container,"f"),this.subscribe("fileSet",b(this,r,"m",y).bind(this)),this.subscribe("emptyLineTextSet",b(this,r,"m",y).bind(this)),this.subscribe("lyricLinesUpdated",b(this,r,"m",m).bind(this)),this.setFile(e.file)}get file(){return b(this,s,"f")}get emptyLineText(){return b(this,f,"f")??"..."}get noLyricsText(){return b(this,d,"f")}async setFile(e){if(e){const t=await b(this,r,"m",u).call(this,e);w(this,a,b(this,r,"m",p).call(this,t),"f")}else w(this,a,[],"f");w(this,s,e,"f"),this.notify("fileSet",{file:e})}setEmptyLineText(e){w(this,f,e,"f"),this.notify("emptyLineTextSet",{emptyLineText:e})}setNoLyricsText(e){w(this,d,e,"f"),this.notify("noLyricsTextSet",{noLyricsText:e})}subscribe(e,t){b(this,l,"f").set(e,[...b(this,l,"f").get(e)??[],t])}unsubscribe(e,t){const i=b(this,l,"f").get(e);i&&i.length>1?b(this,l,"f").set(e,[...i.filter(e=>e!==t)]):b(this,l,"f").delete(e)}notify(e,t){[...b(this,l,"f").get(e)??[]].forEach(e=>{e(t)})}}return s=new WeakMap,a=new WeakMap,o=new WeakMap,l=new WeakMap,f=new WeakMap,h=new WeakMap,c=new WeakMap,d=new WeakMap,r=new WeakSet,u=async function(e){return new Promise((t,i)=>{const n=new FileReader;n.onload=e=>{"string"==typeof e.target?.result&&e.target?.result.trim().length?t(e.target.result):i(new Error("Failed to read file content."))},n.onerror=()=>i(new Error("Error reading file.")),n.readAsText(e)})},p=function(e){const t=e.split("\n").reduce((e,t)=>{const i=t.trim();return n.test(i)?[...e,i]:e},[]);if(0===t.length)throw new Error("No valid lyric lines found in the file.");return t},y=function(){const e=b(this,a,"f").map(e=>{const t=e.replace(i,""),n=e.match(i)?.[0]?.slice(1,-1);if(!n)return"";const r=n.split(":"),s=r[1].split(".");return`\n <span \n data-time="${n}" \n data-ms-time="${60*parseInt(r[0],10)*1e3+1e3*parseInt(s[0],10)+10*parseInt(s[1],10)}"\n data-empty="${0===t.length}"\n data-aria-current="false"\n >\n ${t||(b(this,f,"f")??"...")}\n </span>\n `}).join("<br/>");b(this,o,"f").innerHTML=`<p>${e.length?e:`\n <span data-no-lyrics="true">${b(this,d,"f")??""}</span>\n `}</p>`,this.notify("lyricLinesUpdated",{lyricLines:b(this,a,"f")})},m=function(){if(!b(this,h,"f")||!b(this,a,"f").length)return;const e=1e3*b(this,h,"f").currentTime,t=b(this,o,"f").querySelectorAll("p > span");let i=null;for(let n=0;n<t.length;n++){const r=t[n],s=parseInt(r.getAttribute("data-ms-time")||"0",10);if(r.dataset.ariaCurrent="false",!(e>=s))break;i=r}i&&(i.dataset.ariaCurrent="true",b(this,c,"f")&&i.scrollIntoView({behavior:"smooth",block:"center"}))},t})());
1
+ !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var i=t();for(var s in i)("object"==typeof exports?exports:e)[s]=i[s]}}(self,()=>(()=>{"use strict";var e={d:(t,i)=>{for(var s in i)e.o(i,s)&&!e.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:i[s]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{Kashi:()=>v});const i=/\[\d{2}:\d{2}.\d{2}\]/,s=/^\[\d{2}:\d{2}.\d{2}\](.*)$/;var n,r,a,o,l,f,h,c,d,u,m,p,y,w=function(e,t,i,s,n){if("m"===s)throw new TypeError("Private method is not writable");if("a"===s&&!n)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof t?e!==t||!n:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===s?n.call(e,i):n?n.value=i:t.set(e,i),i},b=function(e,t,i,s){if("a"===i&&!s)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!s:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===i?s:"a"===i?s.call(e):s?s.value:t.get(e)};class v{constructor(e){if(n.add(this),r.set(this,[]),a.set(this,[]),o.set(this,void 0),l.set(this,new Map),f.set(this,void 0),h.set(this,null),c.set(this,!1),d.set(this,void 0),!(e.container&&e.container instanceof HTMLDivElement))throw new Error("Container must be an instance of HTMLDivElement.");if(e.emptyLineText&&this.setEmptyLineText(e.emptyLineText),e.audioPlayer&&!(e.audioPlayer instanceof HTMLAudioElement))throw new Error("Audio player must be an instance of HTMLAudioElement.");e.audioPlayer&&(w(this,h,e.audioPlayer,"f"),b(this,h,"f").addEventListener("timeupdate",()=>{b(this,n,"m",y).call(this)}),b(this,h,"f").addEventListener("seeked",()=>{b(this,o,"f").querySelectorAll("div[data-ms-time]").forEach(e=>{e instanceof HTMLDivElement&&(e.dataset.ariaCurrent="false")}),b(this,n,"m",y).call(this)})),e.enableAutoScroll&&w(this,c,e.enableAutoScroll,"f"),w(this,o,e.container,"f"),this.subscribe("filesSet",b(this,n,"m",p).bind(this)),this.subscribe("emptyLineTextSet",b(this,n,"m",p).bind(this)),this.subscribe("lyricsLinesUpdated",b(this,n,"m",y).bind(this)),this.setFiles(e.files)}get files(){return b(this,r,"f")}get emptyLineText(){return b(this,f,"f")??"..."}get noLyricsText(){return b(this,d,"f")}async setFiles(e=[]){if(e.length>0){const t=e.map(e=>e?b(this,n,"m",u).call(this,e):Promise.resolve("")),i=await Promise.all(t);w(this,a,i.map(e=>b(this,n,"m",m).call(this,e)),"f")}else w(this,a,[],"f");w(this,r,e,"f"),this.notify("filesSet",{files:e})}setEmptyLineText(e){w(this,f,e,"f"),this.notify("emptyLineTextSet",{emptyLineText:e})}setNoLyricsText(e){w(this,d,e,"f"),this.notify("noLyricsTextSet",{noLyricsText:e})}subscribe(e,t){b(this,l,"f").set(e,[...b(this,l,"f").get(e)??[],t])}unsubscribe(e,t){const i=b(this,l,"f").get(e);i&&i.length>1?b(this,l,"f").set(e,[...i.filter(e=>e!==t)]):b(this,l,"f").delete(e)}notify(e,t){[...b(this,l,"f").get(e)??[]].forEach(e=>{e(t)})}}return r=new WeakMap,a=new WeakMap,o=new WeakMap,l=new WeakMap,f=new WeakMap,h=new WeakMap,c=new WeakMap,d=new WeakMap,n=new WeakSet,u=async function(e){return new Promise((t,i)=>{const s=new FileReader;s.onload=e=>{"string"==typeof e.target?.result&&e.target?.result.trim().length?t(e.target.result):i(new Error("Failed to read file content."))},s.onerror=()=>i(new Error("Error reading file.")),s.readAsText(e)})},m=function(e){const t=e.split("\n").reduce((e,t)=>{const i=t.trim();return s.test(i)?[...e,i]:e},[]);if(0===t.length)throw new Error("No valid lyric lines found in the file.");return t},p=function(){const e=b(this,a,"f").length,t=b(this,a,"f")[0]?.length??0;if(!b(this,a,"f").every(e=>e.length===t))throw new Error("All lyric files must have the same number of lines.");let s="";for(let n=0;n<t;n++){const t=[],r=[];for(let s=0;s<e;s++){const e=b(this,a,"f")[s][n],o=e.replace(i,"");r.push(o);const l=e.match(i)?.[0]?.slice(1,-1);t.push(l)}if(new Set(t).size>1)throw new Error(`Timestamp mismatch at line ${n+1} across files}`);const o=t[0],l=o.split(":"),h=l[1].split("."),c=60*parseInt(l[0],10)*1e3+1e3*parseInt(h[0],10)+10*parseInt(h[1],10),d=r.every(e=>0===e.length);s+=`\n <div\n data-time="${o}"\n data-ms-time="${c}"\n data-empty="${d}"\n data-aria-current="false"\n >\n ${(d?"":r.map(e=>`<span>${e}</span>`).join("<br/>"))||(b(this,f,"f")??"...")}\n </div>\n `}b(this,o,"f").innerHTML=s.length?s:`\n <div>\n <span data-no-lyrics="true">\n ${b(this,d,"f")??""}\n </span>\n </div>\n `,this.notify("lyricsLinesUpdated",{lyricsLines:b(this,a,"f")})},y=function(){if(!b(this,h,"f")||!b(this,a,"f").length||b(this,a,"f").some(e=>0===e.length))return;const e=1e3*b(this,h,"f").currentTime,t=b(this,o,"f").querySelectorAll("div[data-ms-time]");let i=null;for(let s=0;s<t.length;s++){const n=t[s],r=parseInt(n.getAttribute("data-ms-time")||"0",10);if(n.dataset.ariaCurrent="false",!(e>=r))break;i=n}i&&(i.dataset.ariaCurrent="true",b(this,c,"f")&&i.scrollIntoView({behavior:"smooth",block:"center"}))},t})());
package/kashi.mjs CHANGED
@@ -1 +1 @@
1
- const t=/\[\d{2}:\d{2}.\d{2}\]/,e=/^\[\d{2}:\d{2}.\d{2}\](.*)$/;var i,n,s,r,a,o,l,h,f,c,d,u,p,m=function(t,e,i,n,s){if("m"===n)throw new TypeError("Private method is not writable");if("a"===n&&!s)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof e?t!==e||!s:!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===n?s.call(t,i):s?s.value=i:e.set(t,i),i},w=function(t,e,i,n){if("a"===i&&!n)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?t!==e||!n:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===i?n:"a"===i?n.call(t):n?n.value:e.get(t)};class y{constructor(t){if(i.add(this),n.set(this,void 0),s.set(this,[]),r.set(this,void 0),a.set(this,new Map),o.set(this,void 0),l.set(this,null),h.set(this,!1),f.set(this,void 0),!(t.container&&t.container instanceof HTMLDivElement))throw new Error("Container must be an instance of HTMLDivElement.");if(t.emptyLineText&&this.setEmptyLineText(t.emptyLineText),t.audioPlayer&&!(t.audioPlayer instanceof HTMLAudioElement))throw new Error("Audio player must be an instance of HTMLAudioElement.");t.audioPlayer&&(m(this,l,t.audioPlayer,"f"),w(this,l,"f").addEventListener("timeupdate",()=>{w(this,i,"m",p).call(this)}),w(this,l,"f").addEventListener("seeked",()=>{w(this,r,"f").querySelectorAll("p > span").forEach(t=>{t instanceof HTMLSpanElement&&(t.dataset.ariaCurrent="false")}),w(this,i,"m",p).call(this)})),t.enableAutoScroll&&m(this,h,t.enableAutoScroll,"f"),m(this,r,t.container,"f"),this.subscribe("fileSet",w(this,i,"m",u).bind(this)),this.subscribe("emptyLineTextSet",w(this,i,"m",u).bind(this)),this.subscribe("lyricLinesUpdated",w(this,i,"m",p).bind(this)),this.setFile(t.file)}get file(){return w(this,n,"f")}get emptyLineText(){return w(this,o,"f")??"..."}get noLyricsText(){return w(this,f,"f")}async setFile(t){if(t){const e=await w(this,i,"m",c).call(this,t);m(this,s,w(this,i,"m",d).call(this,e),"f")}else m(this,s,[],"f");m(this,n,t,"f"),this.notify("fileSet",{file:t})}setEmptyLineText(t){m(this,o,t,"f"),this.notify("emptyLineTextSet",{emptyLineText:t})}setNoLyricsText(t){m(this,f,t,"f"),this.notify("noLyricsTextSet",{noLyricsText:t})}subscribe(t,e){w(this,a,"f").set(t,[...w(this,a,"f").get(t)??[],e])}unsubscribe(t,e){const i=w(this,a,"f").get(t);i&&i.length>1?w(this,a,"f").set(t,[...i.filter(t=>t!==e)]):w(this,a,"f").delete(t)}notify(t,e){[...w(this,a,"f").get(t)??[]].forEach(t=>{t(e)})}}n=new WeakMap,s=new WeakMap,r=new WeakMap,a=new WeakMap,o=new WeakMap,l=new WeakMap,h=new WeakMap,f=new WeakMap,i=new WeakSet,c=async function(t){return new Promise((e,i)=>{const n=new FileReader;n.onload=t=>{"string"==typeof t.target?.result&&t.target?.result.trim().length?e(t.target.result):i(new Error("Failed to read file content."))},n.onerror=()=>i(new Error("Error reading file.")),n.readAsText(t)})},d=function(t){const i=t.split("\n").reduce((t,i)=>{const n=i.trim();return e.test(n)?[...t,n]:t},[]);if(0===i.length)throw new Error("No valid lyric lines found in the file.");return i},u=function(){const e=w(this,s,"f").map(e=>{const i=e.replace(t,""),n=e.match(t)?.[0]?.slice(1,-1);if(!n)return"";const s=n.split(":"),r=s[1].split(".");return`\n <span \n data-time="${n}" \n data-ms-time="${60*parseInt(s[0],10)*1e3+1e3*parseInt(r[0],10)+10*parseInt(r[1],10)}"\n data-empty="${0===i.length}"\n data-aria-current="false"\n >\n ${i||(w(this,o,"f")??"...")}\n </span>\n `}).join("<br/>");w(this,r,"f").innerHTML=`<p>${e.length?e:`\n <span data-no-lyrics="true">${w(this,f,"f")??""}</span>\n `}</p>`,this.notify("lyricLinesUpdated",{lyricLines:w(this,s,"f")})},p=function(){if(!w(this,l,"f")||!w(this,s,"f").length)return;const t=1e3*w(this,l,"f").currentTime,e=w(this,r,"f").querySelectorAll("p > span");let i=null;for(let n=0;n<e.length;n++){const s=e[n],r=parseInt(s.getAttribute("data-ms-time")||"0",10);if(s.dataset.ariaCurrent="false",!(t>=r))break;i=s}i&&(i.dataset.ariaCurrent="true",w(this,h,"f")&&i.scrollIntoView({behavior:"smooth",block:"center"}))};export{y as Kashi};
1
+ const e=/\[\d{2}:\d{2}.\d{2}\]/,t=/^\[\d{2}:\d{2}.\d{2}\](.*)$/;var i,s,n,r,a,o,l,h,f,c,d,u,m,p=function(e,t,i,s,n){if("m"===s)throw new TypeError("Private method is not writable");if("a"===s&&!n)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof t?e!==t||!n:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===s?n.call(e,i):n?n.value=i:t.set(e,i),i},w=function(e,t,i,s){if("a"===i&&!s)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!s:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===i?s:"a"===i?s.call(e):s?s.value:t.get(e)};class y{constructor(e){if(i.add(this),s.set(this,[]),n.set(this,[]),r.set(this,void 0),a.set(this,new Map),o.set(this,void 0),l.set(this,null),h.set(this,!1),f.set(this,void 0),!(e.container&&e.container instanceof HTMLDivElement))throw new Error("Container must be an instance of HTMLDivElement.");if(e.emptyLineText&&this.setEmptyLineText(e.emptyLineText),e.audioPlayer&&!(e.audioPlayer instanceof HTMLAudioElement))throw new Error("Audio player must be an instance of HTMLAudioElement.");e.audioPlayer&&(p(this,l,e.audioPlayer,"f"),w(this,l,"f").addEventListener("timeupdate",()=>{w(this,i,"m",m).call(this)}),w(this,l,"f").addEventListener("seeked",()=>{w(this,r,"f").querySelectorAll("div[data-ms-time]").forEach(e=>{e instanceof HTMLDivElement&&(e.dataset.ariaCurrent="false")}),w(this,i,"m",m).call(this)})),e.enableAutoScroll&&p(this,h,e.enableAutoScroll,"f"),p(this,r,e.container,"f"),this.subscribe("filesSet",w(this,i,"m",u).bind(this)),this.subscribe("emptyLineTextSet",w(this,i,"m",u).bind(this)),this.subscribe("lyricsLinesUpdated",w(this,i,"m",m).bind(this)),this.setFiles(e.files)}get files(){return w(this,s,"f")}get emptyLineText(){return w(this,o,"f")??"..."}get noLyricsText(){return w(this,f,"f")}async setFiles(e=[]){if(e.length>0){const t=e.map(e=>e?w(this,i,"m",c).call(this,e):Promise.resolve("")),s=await Promise.all(t);p(this,n,s.map(e=>w(this,i,"m",d).call(this,e)),"f")}else p(this,n,[],"f");p(this,s,e,"f"),this.notify("filesSet",{files:e})}setEmptyLineText(e){p(this,o,e,"f"),this.notify("emptyLineTextSet",{emptyLineText:e})}setNoLyricsText(e){p(this,f,e,"f"),this.notify("noLyricsTextSet",{noLyricsText:e})}subscribe(e,t){w(this,a,"f").set(e,[...w(this,a,"f").get(e)??[],t])}unsubscribe(e,t){const i=w(this,a,"f").get(e);i&&i.length>1?w(this,a,"f").set(e,[...i.filter(e=>e!==t)]):w(this,a,"f").delete(e)}notify(e,t){[...w(this,a,"f").get(e)??[]].forEach(e=>{e(t)})}}s=new WeakMap,n=new WeakMap,r=new WeakMap,a=new WeakMap,o=new WeakMap,l=new WeakMap,h=new WeakMap,f=new WeakMap,i=new WeakSet,c=async function(e){return new Promise((t,i)=>{const s=new FileReader;s.onload=e=>{"string"==typeof e.target?.result&&e.target?.result.trim().length?t(e.target.result):i(new Error("Failed to read file content."))},s.onerror=()=>i(new Error("Error reading file.")),s.readAsText(e)})},d=function(e){const i=e.split("\n").reduce((e,i)=>{const s=i.trim();return t.test(s)?[...e,s]:e},[]);if(0===i.length)throw new Error("No valid lyric lines found in the file.");return i},u=function(){const t=w(this,n,"f").length,i=w(this,n,"f")[0]?.length??0;if(!w(this,n,"f").every(e=>e.length===i))throw new Error("All lyric files must have the same number of lines.");let s="";for(let r=0;r<i;r++){const i=[],a=[];for(let s=0;s<t;s++){const t=w(this,n,"f")[s][r],o=t.replace(e,"");a.push(o);const l=t.match(e)?.[0]?.slice(1,-1);i.push(l)}if(new Set(i).size>1)throw new Error(`Timestamp mismatch at line ${r+1} across files}`);const l=i[0],h=l.split(":"),f=h[1].split("."),c=60*parseInt(h[0],10)*1e3+1e3*parseInt(f[0],10)+10*parseInt(f[1],10),d=a.every(e=>0===e.length);s+=`\n <div\n data-time="${l}"\n data-ms-time="${c}"\n data-empty="${d}"\n data-aria-current="false"\n >\n ${(d?"":a.map(e=>`<span>${e}</span>`).join("<br/>"))||(w(this,o,"f")??"...")}\n </div>\n `}w(this,r,"f").innerHTML=s.length?s:`\n <div>\n <span data-no-lyrics="true">\n ${w(this,f,"f")??""}\n </span>\n </div>\n `,this.notify("lyricsLinesUpdated",{lyricsLines:w(this,n,"f")})},m=function(){if(!w(this,l,"f")||!w(this,n,"f").length||w(this,n,"f").some(e=>0===e.length))return;const e=1e3*w(this,l,"f").currentTime,t=w(this,r,"f").querySelectorAll("div[data-ms-time]");let i=null;for(let s=0;s<t.length;s++){const n=t[s],r=parseInt(n.getAttribute("data-ms-time")||"0",10);if(n.dataset.ariaCurrent="false",!(e>=r))break;i=n}i&&(i.dataset.ariaCurrent="true",w(this,h,"f")&&i.scrollIntoView({behavior:"smooth",block:"center"}))};export{y as Kashi};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kashi",
3
- "version": "3.0.2",
3
+ "version": "4.0.0",
4
4
  "description": "Singing at the top of my lungs",
5
5
  "type": "module",
6
6
  "author": "lucasmc64",