@scorelabs/core 1.0.5 → 1.0.7

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
@@ -36,7 +36,7 @@ console.log(`Number of Parts: ${score.parts.length}`);
36
36
  The core model uses `NoteSet` to represent a collection of notes at a specific point data (e.g., a chord or a single note).
37
37
 
38
38
  ```typescript
39
- import { Note, NoteSet, Pitch, Duration } from '@scorelabs/core';
39
+ import { Duration, Note, NoteSet, Pitch } from '@scorelabs/core';
40
40
 
41
41
  // Create a C4 quarter note
42
42
  const c4 = new Note(new Pitch('C', 4), Duration.Quarter);
@@ -12,8 +12,9 @@ export declare class MusicXMLParser {
12
12
  private currentSymbol;
13
13
  private instrumentPitchMap;
14
14
  private _domParser;
15
- constructor(domParserInstance?: any);
15
+ constructor(domParserInstance?: DOMParser | any);
16
16
  private parseFromString;
17
+ static getXMLFromBinary(data: ArrayBuffer, domParser?: DOMParser | any): Promise<string>;
17
18
  parseBinary(data: ArrayBuffer): Promise<ScoreJSON>;
18
19
  parse(xmlString: string): ScoreJSON;
19
20
  private parseSubtitle;
@@ -1,6 +1,6 @@
1
1
  import JSZip from 'jszip';
2
2
  import { getInstrumentByProgram } from '../models/Instrument';
3
- import { Clef, Duration, Accidental, Articulation, BarlineStyle, HairpinType, OttavaType, Ornament, Bowing, StemDirection, GlissandoType, NoteheadShape, Arpeggio, } from '../models/types';
3
+ import { Accidental, Arpeggio, Articulation, BarlineStyle, Bowing, Clef, Duration, GlissandoType, HairpinType, NoteheadShape, Ornament, OttavaType, StemDirection, } from '../models/types';
4
4
  /**
5
5
  * MusicXML Parser
6
6
  *
@@ -24,14 +24,17 @@ export class MusicXMLParser {
24
24
  }
25
25
  return this._domParser.parseFromString(xmlString, 'application/xml');
26
26
  }
27
- async parseBinary(data) {
27
+ static async getXMLFromBinary(data, domParser) {
28
28
  const uint8 = new Uint8Array(data);
29
29
  if (uint8[0] === 0x50 && uint8[1] === 0x4b) {
30
30
  const zip = await JSZip.loadAsync(data);
31
31
  const containerXml = await zip.file('META-INF/container.xml')?.async('text');
32
32
  if (!containerXml)
33
33
  throw new Error('Invalid MXL: Missing META-INF/container.xml');
34
- const containerDoc = this.parseFromString(containerXml);
34
+ const _parser = domParser || (typeof DOMParser !== 'undefined' ? new DOMParser() : null);
35
+ if (!_parser)
36
+ throw new Error('No DOMParser available');
37
+ const containerDoc = _parser.parseFromString(containerXml, 'application/xml');
35
38
  const rootfile = containerDoc.querySelector('rootfile');
36
39
  const fullPath = rootfile?.getAttribute('full-path');
37
40
  if (!fullPath)
@@ -39,13 +42,17 @@ export class MusicXMLParser {
39
42
  const scoreXml = await zip.file(fullPath)?.async('text');
40
43
  if (!scoreXml)
41
44
  throw new Error(`Invalid MXL: Could not find file ${fullPath}`);
42
- return this.parse(scoreXml);
45
+ return scoreXml;
43
46
  }
44
47
  else {
45
48
  const decoder = new TextDecoder();
46
- return this.parse(decoder.decode(data));
49
+ return decoder.decode(data);
47
50
  }
48
51
  }
52
+ async parseBinary(data) {
53
+ const scoreXml = await MusicXMLParser.getXMLFromBinary(data, this._domParser);
54
+ return this.parse(scoreXml);
55
+ }
49
56
  parse(xmlString) {
50
57
  this.instrumentPitchMap.clear();
51
58
  // Strip potential BOM and leading garbage
@@ -788,22 +795,30 @@ export class MusicXMLParser {
788
795
  else if (barStyle === 'dotted')
789
796
  barlineStyle = BarlineStyle.Dotted;
790
797
  }
791
- if (barline.querySelector('repeat'))
798
+ const repeatEl = barline.querySelector('repeat');
799
+ if (repeatEl) {
800
+ const direction = repeatEl.getAttribute('direction');
801
+ const times = repeatEl.getAttribute('times');
792
802
  repeats.push({
793
- type: barline.querySelector('repeat')?.getAttribute('direction') === 'forward'
794
- ? 'start'
795
- : 'end',
803
+ type: direction === 'forward' ? 'start' : 'end',
804
+ times: times ? parseInt(times) : undefined,
796
805
  });
806
+ }
797
807
  const ending = barline.querySelector('ending');
798
808
  if (ending) {
799
809
  const type = ending.getAttribute('type');
800
- const number = parseInt(ending.getAttribute('number') || '1');
810
+ const numberAttr = ending.getAttribute('number') || '1';
811
+ // Handle "1,2" or "1 2"
812
+ const numbers = numberAttr
813
+ .split(/[,\s]+/)
814
+ .map((n) => parseInt(n))
815
+ .filter((n) => !isNaN(n));
801
816
  if (type === 'start')
802
- volta = { type: 'start', number };
817
+ volta = { type: 'start', numbers };
803
818
  else if (type === 'stop')
804
- volta = { type: 'stop', number };
819
+ volta = { type: 'stop', numbers };
805
820
  else if (type === 'discontinue')
806
- volta = { type: 'both', number };
821
+ volta = { type: 'both', numbers };
807
822
  }
808
823
  });
809
824
  return { repeats, volta, barlineStyle };
@@ -1,6 +1,6 @@
1
1
  import { Note, NoteJSON } from './Note';
2
2
  import { NoteSet, NoteSetJSON } from './NoteSet';
3
- import { TimeSignature, KeySignature, Duration, Repeat, Volta, Clef, Tempo, BarlineStyle } from './types';
3
+ import { BarlineStyle, Clef, Duration, KeySignature, Repeat, Tempo, TimeSignature, Volta } from './types';
4
4
  /**
5
5
  * Represents a single measure containing note sets, potentially in multiple voices.
6
6
  */
@@ -1,6 +1,6 @@
1
1
  import { Note } from './Note';
2
2
  import { NoteSet } from './NoteSet';
3
- import { DURATION_VALUES, decomposeDuration, BarlineStyle, } from './types';
3
+ import { BarlineStyle, DURATION_VALUES, decomposeDuration, } from './types';
4
4
  /**
5
5
  * Represents a single measure containing note sets, potentially in multiple voices.
6
6
  */
@@ -1,5 +1,5 @@
1
- import { Duration, Accidental, Articulation, Dynamic, Slur, Tuplet, Hairpin, Glissando, Arpeggio, Ottava, Pedal, Ornament, FretboardDiagram, NoteheadShape, Bowing, Lyric, StemDirection } from './types';
2
1
  import { Pitch } from './Pitch';
2
+ import { Accidental, Arpeggio, Articulation, Bowing, Duration, Dynamic, FretboardDiagram, Glissando, Hairpin, Lyric, NoteheadShape, Ornament, Ottava, Pedal, Slur, StemDirection, Tuplet } from './types';
3
3
  /**
4
4
  * Represents a single note or rest in a measure.
5
5
  */
@@ -1,5 +1,5 @@
1
- import { Duration, Accidental, DURATION_VALUES, } from './types';
2
1
  import { Pitch } from './Pitch';
2
+ import { Accidental, DURATION_VALUES, Duration, } from './types';
3
3
  /**
4
4
  * Represents a single note or rest in a measure.
5
5
  */
@@ -1,7 +1,7 @@
1
- import { Staff, StaffJSON } from './Staff';
2
1
  import { Instrument } from './Instrument';
3
- import { Note } from './Note';
4
2
  import { Measure } from './Measure';
3
+ import { Note } from './Note';
4
+ import { Staff, StaffJSON } from './Staff';
5
5
  import { Duration } from './types';
6
6
  /**
7
7
  * Represents a musical part (instrument/voice) which can have multiple staves.
@@ -1,5 +1,5 @@
1
- import { Staff } from './Staff';
2
1
  import { PRESET_INSTRUMENTS } from './Instrument';
2
+ import { Staff } from './Staff';
3
3
  /**
4
4
  * Represents a musical part (instrument/voice) which can have multiple staves.
5
5
  */
@@ -1,8 +1,8 @@
1
- import { TimeSignature, KeySignature, Duration, Genre } from './types';
1
+ import { Measure } from './Measure';
2
+ import { NoteSet } from './NoteSet';
2
3
  import { Part, PartJSON } from './Part';
3
4
  import { Staff } from './Staff';
4
- import { NoteSet } from './NoteSet';
5
- import { Measure } from './Measure';
5
+ import { Duration, Genre, KeySignature, TimeSignature } from './types';
6
6
  /**
7
7
  * Represents a complete musical score.
8
8
  */
@@ -1,5 +1,5 @@
1
- import { Duration, decomposeDuration, Genre } from './types';
2
1
  import { Part } from './Part';
2
+ import { Duration, Genre, decomposeDuration } from './types';
3
3
  /**
4
4
  * Represents a complete musical score.
5
5
  */
@@ -275,7 +275,10 @@ export class Score {
275
275
  let i = 0;
276
276
  let blockStart = 0;
277
277
  const repeatCount = new Map();
278
- while (i < measures.length) {
278
+ let safetyCounter = 0;
279
+ const MAX_SEQUENCE = 10000;
280
+ while (i < measures.length && safetyCounter < MAX_SEQUENCE) {
281
+ safetyCounter++;
279
282
  const m = measures[i];
280
283
  if (m.repeats.some((r) => r.type === 'start'))
281
284
  blockStart = i;
@@ -294,7 +297,7 @@ export class Score {
294
297
  if (nextEndIdx !== -1)
295
298
  iteration = (repeatCount.get(nextEndIdx) || 0) + 1;
296
299
  }
297
- if (m.volta && m.volta.number !== iteration) {
300
+ if (m.volta && !m.volta.numbers.includes(iteration)) {
298
301
  i++;
299
302
  continue;
300
303
  }
@@ -1,7 +1,7 @@
1
- import { Clef, Duration } from './types';
2
1
  import { Measure, MeasureJSON } from './Measure';
3
- import { Pitch } from './Pitch';
4
2
  import { Note } from './Note';
3
+ import { Pitch } from './Pitch';
4
+ import { Clef, Duration } from './types';
5
5
  /**
6
6
  * Represents a single staff (one set of 5 lines with a clef).
7
7
  */
@@ -140,7 +140,7 @@ export interface Repeat {
140
140
  }
141
141
  export interface Volta {
142
142
  type: 'start' | 'stop' | 'both';
143
- number: number;
143
+ numbers: number[];
144
144
  }
145
145
  export declare enum BarlineStyle {
146
146
  Regular = "regular",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scorelabs/core",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Core logic and models for ScoreLabs music notation",
5
5
  "type": "module",
6
6
  "files": [
@@ -11,8 +11,8 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "exports": {
13
13
  ".": {
14
- "import": "./dist/index.js",
15
- "types": "./dist/index.d.ts"
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
16
  }
17
17
  },
18
18
  "scripts": {
@@ -23,7 +23,8 @@
23
23
  "release:minor": "npm version minor && npm publish --access public",
24
24
  "release:major": "npm version major && npm publish --access public",
25
25
  "release": "npm run release:patch",
26
- "test": "vitest"
26
+ "test": "vitest",
27
+ "lint": "eslint ."
27
28
  },
28
29
  "dependencies": {
29
30
  "jszip": "^3.10.1"
@@ -1,36 +0,0 @@
1
- export declare const TOKEN_MODULUS = 17;
2
- export declare const EXPECTED_REMAINDER = 2;
3
- export declare const EXPIRATION_MS: number;
4
- export type UserTier = 'free' | 'premium';
5
- /**
6
- * Obfuscates the user's tier into a numeric token for the viewer.
7
- * The viewer expects token % 17 === 2 for premium access.
8
- */
9
- export declare function generateObfuscatedToken(tier: UserTier): number;
10
- /**
11
- * Packs token and timestamp into an obfuscated string.
12
- */
13
- export declare function packToken(token: number, timestamp: number, salt: string): string;
14
- /**
15
- * Unpacks the token string and validates the signature.
16
- */
17
- export declare function unpackToken(packed: string, salt: string): {
18
- token: number;
19
- timestamp: number;
20
- } | null;
21
- /**
22
- * Returns the effective numeric token, checking for expiration.
23
- */
24
- export declare function getEffectiveToken(user: any): number;
25
- /**
26
- * Returns the effective tier from the obfuscated token
27
- */
28
- export declare function getTierFromUser(user: any): UserTier;
29
- /**
30
- * Returns true only if the user has a valid, non-expired premium token.
31
- */
32
- export declare function isUserPremium(user: any): boolean;
33
- /**
34
- * Returns true only if the user has a valid, non-expired free token.
35
- */
36
- export declare function isUserFree(user: any): boolean;
@@ -1,112 +0,0 @@
1
- export const TOKEN_MODULUS = 17;
2
- export const EXPECTED_REMAINDER = 2;
3
- export const EXPIRATION_MS = 24 * 60 * 60 * 1000;
4
- /**
5
- * Obfuscates the user's tier into a numeric token for the viewer.
6
- * The viewer expects token % 17 === 2 for premium access.
7
- */
8
- export function generateObfuscatedToken(tier) {
9
- const isPremium = tier === 'premium';
10
- let base = Math.floor(Math.random() * 8999) + 1000;
11
- if (isPremium) {
12
- // Ensure token % 17 === 2
13
- return base - (base % TOKEN_MODULUS) + EXPECTED_REMAINDER;
14
- }
15
- else {
16
- // Ensure token % 17 !== 2
17
- const result = base;
18
- return result % TOKEN_MODULUS === EXPECTED_REMAINDER ? result + 1 : result;
19
- }
20
- }
21
- /**
22
- * Packs token and timestamp into an obfuscated string.
23
- */
24
- export function packToken(token, timestamp, salt) {
25
- // Use a more robust numeric salt
26
- const saltNum = salt
27
- .split('')
28
- .reduce((acc, char, i) => acc + char.charCodeAt(0) * (i + 1), 0);
29
- // Pack components
30
- const tHex = token.toString(16);
31
- // Obfuscate time using XOR with salt and token
32
- const timeKey = (saltNum ^ token) % 1000000;
33
- const oTime = (BigInt(timestamp) ^ BigInt(timeKey)).toString(16);
34
- // Create a signature that binds token, time and salt
35
- const sigPayload = `${token}:${timestamp}:${salt}`;
36
- let sig = 0;
37
- for (let i = 0; i < sigPayload.length; i++) {
38
- sig = (sig << 5) - sig + sigPayload.charCodeAt(i);
39
- sig |= 0; // Convert to 32bit integer
40
- }
41
- const sHex = Math.abs(sig).toString(16);
42
- return `${tHex}-${oTime}-${sHex}`;
43
- }
44
- /**
45
- * Unpacks the token string and validates the signature.
46
- */
47
- export function unpackToken(packed, salt) {
48
- try {
49
- const [tHex, oTime, sHex] = packed.split('-');
50
- if (!tHex || !oTime || !sHex)
51
- return null;
52
- const token = parseInt(tHex, 16);
53
- const saltNum = salt
54
- .split('')
55
- .reduce((acc, char, i) => acc + char.charCodeAt(0) * (i + 1), 0);
56
- const timeKey = (saltNum ^ token) % 1000000;
57
- const timestamp = Number(BigInt(parseInt(oTime, 16)) ^ BigInt(timeKey));
58
- // Validate signature
59
- const sigPayload = `${token}:${timestamp}:${salt}`;
60
- let sig = 0;
61
- for (let i = 0; i < sigPayload.length; i++) {
62
- sig = (sig << 5) - sig + sigPayload.charCodeAt(i);
63
- sig |= 0;
64
- }
65
- const expectedSig = Math.abs(sig).toString(16);
66
- if (sHex !== expectedSig)
67
- return null;
68
- return { token, timestamp };
69
- }
70
- catch (e) {
71
- return null;
72
- }
73
- }
74
- /**
75
- * Returns the effective numeric token, checking for expiration.
76
- */
77
- export function getEffectiveToken(user) {
78
- if (!user)
79
- return 0;
80
- const accessToken = user.accessToken || user.access_token;
81
- if (!accessToken) {
82
- return 0;
83
- }
84
- const salt = (user.uuid || user.id || 'default-salt').toString();
85
- const unpacked = unpackToken(accessToken, salt);
86
- if (!unpacked)
87
- return 0;
88
- const now = Date.now();
89
- if (now - unpacked.timestamp >= EXPIRATION_MS || now < unpacked.timestamp) {
90
- return 0; // Expired
91
- }
92
- return unpacked.token;
93
- }
94
- /**
95
- * Returns the effective tier from the obfuscated token
96
- */
97
- export function getTierFromUser(user) {
98
- const token = getEffectiveToken(user);
99
- return token % TOKEN_MODULUS === EXPECTED_REMAINDER ? 'premium' : 'free';
100
- }
101
- /**
102
- * Returns true only if the user has a valid, non-expired premium token.
103
- */
104
- export function isUserPremium(user) {
105
- return getTierFromUser(user) === 'premium';
106
- }
107
- /**
108
- * Returns true only if the user has a valid, non-expired free token.
109
- */
110
- export function isUserFree(user) {
111
- return getTierFromUser(user) === 'free';
112
- }