trackparse 0.3.2 → 0.4.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
@@ -11,7 +11,7 @@ Parse music track names into structured metadata: artists, featured artists, rem
11
11
 
12
12
  This is the JavaScript/TypeScript reference implementation of the
13
13
  [trackparse spec](https://github.com/4matic/trackparse/blob/main/spec/SPEC.md). It passes all
14
- 12,965 shared fixtures. Zero dependencies, ESM and CommonJS, TypeScript types included, never
14
+ 13,124 shared fixtures. Zero dependencies, ESM and CommonJS, TypeScript types included, never
15
15
  throws.
16
16
 
17
17
  The principle is **extract, don't delete**: everything recognised lands in a field (artists with
@@ -29,6 +29,8 @@ Node 18 or later. No Node-specific APIs are used, so it also runs in browsers an
29
29
 
30
30
  ## Quick start
31
31
 
32
+ **Docs:** [Getting started](https://4matic.github.io/trackparse/start/getting-started/)
33
+
32
34
  ```js
33
35
  import { parse } from "trackparse";
34
36
  // const { parse } = require("trackparse");
@@ -51,12 +53,16 @@ track.mode; // 'youtube' (auto-detected from the junk gro
51
53
 
52
54
  ## API
53
55
 
56
+ **Docs:** [parse and ParsedTrack](https://4matic.github.io/trackparse/reference/parse/) · [Other functions](https://4matic.github.io/trackparse/reference/functions/)
57
+
54
58
  ### `parse(input, options?) → ParsedTrack`
55
59
 
56
60
  Parses one track string. Every key of the result is always present.
57
61
 
58
62
  ### `parseArtists(input, options?) → Artist[]`
59
63
 
64
+ **Docs:** [parseArtists](https://4matic.github.io/trackparse/reference/functions/#parseartistsinput-options)
65
+
60
66
  Splits an artist string (no title) into credits.
61
67
 
62
68
  ```js
@@ -71,6 +77,8 @@ parseArtists("Sub Focus, Wilkinson & Dimension feat. Kojo");
71
77
 
72
78
  ### `format(track, options?) → string`
73
79
 
80
+ **Docs:** [format](https://4matic.github.io/trackparse/reference/functions/#formattrack-options) · [Formatting](https://4matic.github.io/trackparse/guides/formatting/)
81
+
74
82
  Renders a `ParsedTrack` back to `Artists - Title (versions) (prod. ...)`.
75
83
 
76
84
  ```js
@@ -92,6 +100,8 @@ format(t, { feat: "omit", versions: "none" }); // 'Wilkinson - Afterglow'
92
100
 
93
101
  ### `normalize(input) → string`
94
102
 
103
+ **Docs:** [normalize](https://4matic.github.io/trackparse/reference/functions/#normalizeinput)
104
+
95
105
  The Unicode cleanup applied before parsing: NFC, zero-width spaces, BOM and soft hyphens removed, fullwidth
96
106
  brackets, smart quotes and dash variants mapped to ASCII, whitespace collapsed. Idempotent.
97
107
 
@@ -102,6 +112,8 @@ normalize(" Artist​ — Title (Remix) “Quoted” ");
102
112
 
103
113
  ### `allArtists(track) → Artist[]`
104
114
 
115
+ **Docs:** [allArtists](https://4matic.github.io/trackparse/reference/functions/#allartiststrack)
116
+
105
117
  Track credits plus every version's remixers, deduplicated.
106
118
 
107
119
  ```js
@@ -112,6 +124,8 @@ allArtists(parse("Camo & Krooked - Kallisto (Noisia Remix)", { knownArtists: ["C
112
124
 
113
125
  ### `createParser(options) → { parse, parseArtists }`
114
126
 
127
+ **Docs:** [createParser](https://4matic.github.io/trackparse/reference/functions/#createparseroptions)
128
+
115
129
  Compiles options, including extra vocabulary, once. Per-call options are merged over the base.
116
130
 
117
131
  ```js
@@ -133,6 +147,8 @@ The spec version this build implements, e.g. `"0.3.0"`. Always equal to the pack
133
147
 
134
148
  ## Options
135
149
 
150
+ **Docs:** [Options](https://4matic.github.io/trackparse/reference/options/)
151
+
136
152
  | Option | Default | Effect |
137
153
  |---|---|---|
138
154
  | `mode` | `"auto"` | `"clean"` (tags, store metadata), `"youtube"` (video titles), `"filename"`, `"tracklist"` (one line of a tracklist), or `"auto"` to detect. |
@@ -207,9 +223,11 @@ The full field reference, a gallery of examples and the comparison with other li
207
223
 
208
224
  ## Tracklists
209
225
 
226
+ **Docs:** [Tracklists](https://4matic.github.io/trackparse/guides/tracklists/) · [Editing tracklists](https://4matic.github.io/trackparse/guides/editing-tracklists/)
227
+
210
228
  `trackparse/tracklist` parses a whole tracklist: one entry per line with its kind, start time and
211
229
  a full `ParsedTrack`, plus checks with line numbers. Joining `raw + eol` over the entries gives the
212
- input back byte for byte, whatever the line endings. It's a separate entry point (about 7.6 KiB
230
+ input back byte for byte, whatever the line endings. It's a separate entry point (about 8.3 KiB
213
231
  gzipped), so you only pay for it if you import it.
214
232
 
215
233
  ```js
@@ -271,6 +289,13 @@ Warning codes: `time-missing`, `time-out-of-order` (with a one- or two-digit `su
271
289
  exactly one fix fits), `time-duplicate`, `no-start-at-zero`, `time-after-audio-end` (pass
272
290
  `audioSeconds`), `invalid-time`, `position-gap`, `position-duplicate`, `line-unparsed`.
273
291
 
292
+ Lists pasted with list bullets or Markdown emphasis (`* 14:21 Artist - Title`,
293
+ `**[**14:21**] Artist - Title**`) read like plain lines; the decoration stays in `raw` and `parts`.
294
+ A line whose track text (after any time) starts with `w/` is `layered`: a track played over the
295
+ previous one
296
+ ([Markdown](https://4matic.github.io/trackparse/guides/tracklists/#markdown),
297
+ [layered tracks](https://4matic.github.io/trackparse/guides/tracklists/#layered-tracks)).
298
+
274
299
  Options: `mode` (default `"tracklist"`), `knownArtists`, `splitAnd`, `keywords`, `audioSeconds`,
275
300
  `markerWords`. Every type is exported (`import type { Tracklist, Edit } from
276
301
  "trackparse/tracklist"`).
@@ -288,6 +313,8 @@ Field tables and the full list of rules: the [tracklists guide](https://4matic.g
288
313
 
289
314
  ## Limitations
290
315
 
316
+ **Docs:** [Limitations and roadmap](https://4matic.github.io/trackparse/project/roadmap/)
317
+
291
318
  One track string per `parse()` call; tracklists go through `trackparse/tracklist`, which reads
292
319
  start times only (no durations, trailing times or ranges). No label/catalog, key or BPM fields, and
293
320
  no built-in artist database: pass `knownArtists` for names that contain a joiner.
@@ -1,5 +1,5 @@
1
1
  // src/data.generated.ts
2
- var SPEC_VERSION = "0.3.2";
2
+ var SPEC_VERSION = "0.4.0";
3
3
  var descriptorsData = {
4
4
  "descriptors": [
5
5
  "radio",
@@ -327,6 +327,11 @@ var junkData = {
327
327
  "recordings"
328
328
  ]
329
329
  };
330
+ var layeredWordsData = {
331
+ "words": [
332
+ "w/"
333
+ ]
334
+ };
330
335
  var markerWordsData = {
331
336
  "words": [
332
337
  "intro",
@@ -2311,4 +2316,4 @@ function parseArtists(input, options) {
2311
2316
  return defaultParser.parseArtists(input, options);
2312
2317
  }
2313
2318
 
2314
- export { PhraseTable, SPEC_VERSION, asciiLower, createListParser, createParser, dedupKey, digitRun, getDefaultTables, headerWordsData, inVocab, isDigit, isWhitespace, markerWordsData, normalize, parse, parseArtists, renderVersion, splitWords, stringList, wordMatches };
2319
+ export { PhraseTable, SPEC_VERSION, asciiLower, createListParser, createParser, dedupKey, digitRun, getDefaultTables, headerWordsData, inVocab, isDigit, isWhitespace, layeredWordsData, markerWordsData, normalize, parse, parseArtists, renderVersion, splitWords, stringList, wordMatches };
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  // src/data.generated.ts
4
- var SPEC_VERSION = "0.3.2";
4
+ var SPEC_VERSION = "0.4.0";
5
5
  var descriptorsData = {
6
6
  "descriptors": [
7
7
  "radio",
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { P as ParsedTrack, A as Artist, F as FormatOptions, a as ParseOptions } from './types-DqJSn18b.cjs';
2
2
  export { b as ArtistRole, c as ArtistSource, d as Flags, J as Junk, e as JunkKind, K as KeywordOptions, M as Mode, f as ModeOption, g as Position, T as Timestamp, V as Version, h as VersionDelimiter, i as VersionType, W as Warning } from './types-DqJSn18b.cjs';
3
3
 
4
- declare const SPEC_VERSION = "0.3.2";
4
+ declare const SPEC_VERSION = "0.4.0";
5
5
 
6
6
  declare function format(track: ParsedTrack, options?: FormatOptions): string;
7
7
  /** All credited names: track artists plus every version's remixers, deduped by R0.4 key. */
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { P as ParsedTrack, A as Artist, F as FormatOptions, a as ParseOptions } from './types-DqJSn18b.js';
2
2
  export { b as ArtistRole, c as ArtistSource, d as Flags, J as Junk, e as JunkKind, K as KeywordOptions, M as Mode, f as ModeOption, g as Position, T as Timestamp, V as Version, h as VersionDelimiter, i as VersionType, W as Warning } from './types-DqJSn18b.js';
3
3
 
4
- declare const SPEC_VERSION = "0.3.2";
4
+ declare const SPEC_VERSION = "0.4.0";
5
5
 
6
6
  declare function format(track: ParsedTrack, options?: FormatOptions): string;
7
7
  /** All credited names: track artists plus every version's remixers, deduped by R0.4 key. */
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { getDefaultTables, renderVersion, dedupKey, wordMatches } from './chunk-L4WNGZ5H.js';
2
- export { SPEC_VERSION, createParser, normalize, parse, parseArtists } from './chunk-L4WNGZ5H.js';
1
+ import { getDefaultTables, renderVersion, dedupKey, wordMatches } from './chunk-OY536C3I.js';
2
+ export { SPEC_VERSION, createParser, normalize, parse, parseArtists } from './chunk-OY536C3I.js';
3
3
 
4
4
  // src/format.ts
5
5
  function resolve(o) {
@@ -240,6 +240,45 @@ function twoDigitsUpTo59(s, at) {
240
240
  return n <= 59 ? n : null;
241
241
  }
242
242
 
243
+ // src/tracklist/emphasis.ts
244
+ function runLength(s, at) {
245
+ const ch = s[at];
246
+ let j = at;
247
+ if (ch === "*" || ch === "_") while (s[j] === ch) j++;
248
+ return j - at;
249
+ }
250
+ function emphasisRun(s, at) {
251
+ const n = runLength(s, at);
252
+ return n <= 2 ? n : 0;
253
+ }
254
+ var isOuter = (ch) => ch === void 0 || isWhitespace(ch) || "[]()*_".includes(ch);
255
+ var isInner = (ch) => ch !== void 0 && !isWhitespace(ch);
256
+ function withoutMarkers(raw, from, open) {
257
+ const marked = [];
258
+ const stacks = { "*": [], "**": [], _: [], __: [] };
259
+ for (let i = from; i < raw.length; ) {
260
+ const len = runLength(raw, i) || 1;
261
+ const own = stacks[raw.slice(i, i + len)];
262
+ const before = raw[i - 1];
263
+ const after = raw[i + len];
264
+ const at = own && isInner(before) && isOuter(after) ? own.pop() : void 0;
265
+ if (at !== void 0) {
266
+ for (let m = 0; m < len; m++) marked[at + m] = marked[i + m] = true;
267
+ for (const st of Object.values(stacks)) while ((st[st.length - 1] ?? -1) > at) st.pop();
268
+ } else if (own && isOuter(before) && isInner(after)) own.push(i);
269
+ i += len;
270
+ }
271
+ let end = raw.length;
272
+ while (end > from && isWhitespace(raw[end - 1])) end--;
273
+ let start = end;
274
+ while (start > from && raw[start - 1] === open) start--;
275
+ if (open && end - start <= 2 && start > from && raw[start - 1] !== open && isInner(raw[start - 1]))
276
+ for (let m = start; m < end && !marked[m]; m++) marked[m] = true;
277
+ let out = "";
278
+ for (let i = from; i < raw.length; i++) if (!marked[i]) out += raw[i];
279
+ return out;
280
+ }
281
+
243
282
  // src/tracklist/times.ts
244
283
  function isGapSeparator(ch) {
245
284
  return ch === "-" || ch === "\u2013" || ch === "\u2014" || ch === "|" || ch === "." || ch === ":";
@@ -261,9 +300,11 @@ function untimed(raw, invalid) {
261
300
  parts: { lead, time: "", gap: "", rest: raw.slice(lead.length) },
262
301
  time: null,
263
302
  style: null,
264
- invalid
303
+ invalid,
304
+ text: withoutMarkers(raw, lead.length)
265
305
  };
266
306
  }
307
+ var closingRun = (raw, at, open) => open && raw[at] === open ? at + emphasisRun(raw, at) : at;
267
308
  function secondsOf(whole, frac) {
268
309
  return frac.length === 0 ? whole : Number(`${whole}.${frac}`);
269
310
  }
@@ -296,8 +337,22 @@ function shapeSeconds(parts) {
296
337
  }
297
338
  function scanTime(raw) {
298
339
  let i = skipWhitespace(raw, 0);
340
+ if (isBullet(raw[i]) && isWhitespace(raw[i + 1])) i = skipWhitespace(raw, i + 1);
341
+ let outerOpen;
342
+ let n = emphasisRun(raw, i);
343
+ if (n && (raw[i + n] === "[" || raw[i + n] === "(" || isDigit(raw[i + n]))) {
344
+ outerOpen = raw[i];
345
+ i += n;
346
+ }
299
347
  const closer = raw[i] === "[" ? "]" : raw[i] === "(" ? ")" : null;
300
- if (closer) i++;
348
+ let innerOpen;
349
+ if (closer) {
350
+ n = emphasisRun(raw, ++i);
351
+ if (n && isDigit(raw[i + n])) {
352
+ innerOpen = raw[i];
353
+ i += n;
354
+ }
355
+ }
301
356
  const leadEnd = i;
302
357
  const shape = shapeAt(raw, i);
303
358
  if (!shape) return untimed(raw, false);
@@ -308,19 +363,30 @@ function scanTime(raw) {
308
363
  let frac = "";
309
364
  const sep = raw[j];
310
365
  if (sep === "." || sep === ",") {
311
- const n = digitRun(raw, j + 1);
312
- if (n >= 1 && n <= 3) {
366
+ const n2 = digitRun(raw, j + 1);
367
+ if (n2 >= 1 && n2 <= 3) {
313
368
  fracSep = sep;
314
- frac = raw.slice(j + 1, j + 1 + n);
315
- j += 1 + n;
369
+ frac = raw.slice(j + 1, j + 1 + n2);
370
+ j += 1 + n2;
316
371
  }
317
372
  }
318
373
  const timeEnd = j;
374
+ const followsTime = (at) => at >= raw.length || isWhitespace(raw[at]) || isGapSeparator(raw[at]);
375
+ let open;
319
376
  if (closer) {
377
+ let k = closingRun(raw, j, innerOpen);
378
+ if (k === j) open = innerOpen;
379
+ j = k;
320
380
  if (raw[j] !== closer) return untimed(raw, false);
321
381
  j++;
322
- } else if (j < raw.length && !isWhitespace(raw[j]) && !isGapSeparator(raw[j])) {
323
- return untimed(raw, false);
382
+ k = closingRun(raw, j, outerOpen);
383
+ if (k > j && followsTime(k)) j = k;
384
+ else open = outerOpen ?? open;
385
+ } else {
386
+ const k = closingRun(raw, j, outerOpen);
387
+ if (k === j) open = outerOpen;
388
+ j = k;
389
+ if (!followsTime(j)) return untimed(raw, false);
324
390
  }
325
391
  j = skipWhitespace(raw, j);
326
392
  if (isGapSeparator(raw[j])) j = skipWhitespace(raw, j + 1);
@@ -348,7 +414,8 @@ function scanTime(raw) {
348
414
  fracSep,
349
415
  fracDigits: frac.length
350
416
  },
351
- invalid: false
417
+ invalid: false,
418
+ text: withoutMarkers(raw, j, open)
352
419
  };
353
420
  }
354
421
  var pad = (n, width) => String(n).padStart(width, "0");
@@ -642,7 +709,7 @@ function applyEdits(text, edits, options) {
642
709
  }
643
710
 
644
711
  // src/data.generated.ts
645
- var SPEC_VERSION = "0.3.2";
712
+ var SPEC_VERSION = "0.4.0";
646
713
  var descriptorsData = {
647
714
  "descriptors": [
648
715
  "radio",
@@ -970,6 +1037,11 @@ var junkData = {
970
1037
  "recordings"
971
1038
  ]
972
1039
  };
1040
+ var layeredWordsData = {
1041
+ "words": [
1042
+ "w/"
1043
+ ]
1044
+ };
973
1045
  var markerWordsData = {
974
1046
  "words": [
975
1047
  "intro",
@@ -2745,6 +2817,7 @@ createParser();
2745
2817
 
2746
2818
  // src/tracklist/kinds.ts
2747
2819
  var headerWords = new Set(headerWordsData.words);
2820
+ var layeredWords = new Set(layeredWordsData.words);
2748
2821
  var defaultMarkers = null;
2749
2822
  function markerTable(extra) {
2750
2823
  const more = stringList(extra);
@@ -2775,6 +2848,15 @@ function markerWords(text) {
2775
2848
  flush();
2776
2849
  return out;
2777
2850
  }
2851
+ function layeredPrefix(text) {
2852
+ let i = 0;
2853
+ while (i < text.length && isWhitespace(text[i])) i++;
2854
+ let j = i;
2855
+ while (j < text.length && !isWhitespace(text[j])) j++;
2856
+ if (j === i || j === text.length || !inVocab(text.slice(i, j), layeredWords)) return 0;
2857
+ while (j < text.length && isWhitespace(text[j])) j++;
2858
+ return j < text.length ? j : 0;
2859
+ }
2778
2860
  function isBlank(raw) {
2779
2861
  for (let i = 0; i < raw.length; i++) if (!isWhitespace(raw[i])) return false;
2780
2862
  return true;
@@ -2998,7 +3080,8 @@ function tracklistWarnings(input) {
2998
3080
  if (e.time) timed.push(i);
2999
3081
  });
3000
3082
  entries.forEach((e, i) => {
3001
- if (e.kind === "track" && e.time === null && timed.length > 0) add(i, "time-missing");
3083
+ if (e.kind === "track" && e.time === null && !e.layered && timed.length > 0)
3084
+ add(i, "time-missing");
3002
3085
  if (invalid[i]) add(i, "invalid-time");
3003
3086
  if (unparsed[i]) add(i, "line-unparsed");
3004
3087
  });
@@ -3039,9 +3122,12 @@ function parseTracklist(text, options) {
3039
3122
  const rows = lines.map((line) => {
3040
3123
  const scan2 = scanTime(line.raw);
3041
3124
  const blank = isBlank(line.raw);
3125
+ const layered = blank ? 0 : layeredPrefix(scan2.text);
3042
3126
  const row = {
3043
3127
  line,
3044
3128
  scan: scan2,
3129
+ text: scan2.text.slice(layered),
3130
+ layered: layered > 0,
3045
3131
  blank,
3046
3132
  candidate: false,
3047
3133
  dash: false,
@@ -3052,11 +3138,11 @@ function parseTracklist(text, options) {
3052
3138
  marker: false
3053
3139
  };
3054
3140
  if (blank) return row;
3055
- const probe2 = lp.probe(scan2.parts.rest);
3141
+ const probe2 = lp.probe(row.text);
3056
3142
  row.probe = probe2;
3057
3143
  row.dash = probe2.spacedDash;
3058
3144
  row.candidate = scan2.time !== null || row.dash;
3059
- row.bare = bareCandidate(scan2.parts.rest, probe2.position);
3145
+ row.bare = row.layered ? null : bareCandidate(row.text, probe2.position);
3060
3146
  return row;
3061
3147
  });
3062
3148
  let candidates2 = 0;
@@ -3069,13 +3155,13 @@ function parseTracklist(text, options) {
3069
3155
  const listContext = candidates2 > 0 && 2 * dashes >= candidates2;
3070
3156
  const confirmed = confirmPositions(
3071
3157
  rows.map((r) => r.bare),
3072
- rows.map((r) => r.candidate)
3158
+ rows.map((r) => r.candidate && !r.layered)
3073
3159
  );
3074
3160
  rows.forEach((r, i) => {
3075
3161
  if (r.blank || !r.probe) return;
3076
3162
  const bare = confirmed[i] ? r.bare : null;
3077
3163
  const probe2 = bare ? lp.probe(bare.stripped) : r.probe;
3078
- r.marker = hasMarkerWord(bare ? bare.stripped : r.scan.parts.rest, markers);
3164
+ r.marker = hasMarkerWord(bare ? bare.stripped : r.text, markers);
3079
3165
  const track = probe2.finish(listContext && r.scan.time !== null && !r.marker);
3080
3166
  if (bare) track.position = { raw: bare.raw, number: bare.number };
3081
3167
  r.track = track;
@@ -3091,7 +3177,8 @@ function parseTracklist(text, options) {
3091
3177
  raw: r.line.raw,
3092
3178
  parts: r.scan.parts,
3093
3179
  time: r.scan.time,
3094
- track: keep ? r.track : null
3180
+ track: keep ? r.track : null,
3181
+ layered: r.layered
3095
3182
  };
3096
3183
  });
3097
3184
  const warnings = tracklistWarnings({
@@ -3107,9 +3194,8 @@ function kindOf(r) {
3107
3194
  if (r.blank || !r.track) return "blank";
3108
3195
  const marker = !r.separated && r.marker;
3109
3196
  if (r.scan.time) return marker ? "marker" : "track";
3110
- const rest = r.scan.parts.rest;
3111
- if (isLink(rest)) return "link";
3112
- if (isHeader(rest)) return "header";
3197
+ if (isLink(r.text)) return "link";
3198
+ if (isHeader(r.text)) return "header";
3113
3199
  if (marker) return "marker";
3114
3200
  return r.separated ? "track" : "note";
3115
3201
  }
@@ -25,6 +25,8 @@ interface TracklistEntry {
25
25
  parts: EntryParts;
26
26
  time: TracklistTime | null;
27
27
  track: ParsedTrack | null;
28
+ /** R13.13: the line starts with `w/`: played over the previous track. */
29
+ layered: boolean;
28
30
  }
29
31
  type TracklistWarningCode = "time-missing" | "time-out-of-order" | "time-duplicate" | "no-start-at-zero" | "time-after-audio-end" | "invalid-time" | "position-gap" | "position-duplicate" | "line-unparsed";
30
32
  interface TracklistWarning {
@@ -25,6 +25,8 @@ interface TracklistEntry {
25
25
  parts: EntryParts;
26
26
  time: TracklistTime | null;
27
27
  track: ParsedTrack | null;
28
+ /** R13.13: the line starts with `w/`: played over the previous track. */
29
+ layered: boolean;
28
30
  }
29
31
  type TracklistWarningCode = "time-missing" | "time-out-of-order" | "time-duplicate" | "no-start-at-zero" | "time-after-audio-end" | "invalid-time" | "position-gap" | "position-duplicate" | "line-unparsed";
30
32
  interface TracklistWarning {
package/dist/tracklist.js CHANGED
@@ -1,4 +1,4 @@
1
- import { headerWordsData, createListParser, SPEC_VERSION, stringList, PhraseTable, markerWordsData, digitRun, isWhitespace, asciiLower, splitWords, inVocab, isDigit } from './chunk-L4WNGZ5H.js';
1
+ import { headerWordsData, layeredWordsData, createListParser, SPEC_VERSION, stringList, PhraseTable, markerWordsData, isWhitespace, isDigit, digitRun, inVocab, asciiLower, splitWords } from './chunk-OY536C3I.js';
2
2
 
3
3
  // src/tracklist/split.ts
4
4
  function splitLines(text) {
@@ -35,6 +35,45 @@ function hasLineBreak(s) {
35
35
  return false;
36
36
  }
37
37
 
38
+ // src/tracklist/emphasis.ts
39
+ function runLength(s, at) {
40
+ const ch = s[at];
41
+ let j = at;
42
+ if (ch === "*" || ch === "_") while (s[j] === ch) j++;
43
+ return j - at;
44
+ }
45
+ function emphasisRun(s, at) {
46
+ const n = runLength(s, at);
47
+ return n <= 2 ? n : 0;
48
+ }
49
+ var isOuter = (ch) => ch === void 0 || isWhitespace(ch) || "[]()*_".includes(ch);
50
+ var isInner = (ch) => ch !== void 0 && !isWhitespace(ch);
51
+ function withoutMarkers(raw, from, open) {
52
+ const marked = [];
53
+ const stacks = { "*": [], "**": [], _: [], __: [] };
54
+ for (let i = from; i < raw.length; ) {
55
+ const len = runLength(raw, i) || 1;
56
+ const own = stacks[raw.slice(i, i + len)];
57
+ const before = raw[i - 1];
58
+ const after = raw[i + len];
59
+ const at = own && isInner(before) && isOuter(after) ? own.pop() : void 0;
60
+ if (at !== void 0) {
61
+ for (let m = 0; m < len; m++) marked[at + m] = marked[i + m] = true;
62
+ for (const st of Object.values(stacks)) while ((st[st.length - 1] ?? -1) > at) st.pop();
63
+ } else if (own && isOuter(before) && isInner(after)) own.push(i);
64
+ i += len;
65
+ }
66
+ let end = raw.length;
67
+ while (end > from && isWhitespace(raw[end - 1])) end--;
68
+ let start = end;
69
+ while (start > from && raw[start - 1] === open) start--;
70
+ if (open && end - start <= 2 && start > from && raw[start - 1] !== open && isInner(raw[start - 1]))
71
+ for (let m = start; m < end && !marked[m]; m++) marked[m] = true;
72
+ let out = "";
73
+ for (let i = from; i < raw.length; i++) if (!marked[i]) out += raw[i];
74
+ return out;
75
+ }
76
+
38
77
  // src/tracklist/times.ts
39
78
  function isGapSeparator(ch) {
40
79
  return ch === "-" || ch === "\u2013" || ch === "\u2014" || ch === "|" || ch === "." || ch === ":";
@@ -56,9 +95,11 @@ function untimed(raw, invalid) {
56
95
  parts: { lead, time: "", gap: "", rest: raw.slice(lead.length) },
57
96
  time: null,
58
97
  style: null,
59
- invalid
98
+ invalid,
99
+ text: withoutMarkers(raw, lead.length)
60
100
  };
61
101
  }
102
+ var closingRun = (raw, at, open) => open && raw[at] === open ? at + emphasisRun(raw, at) : at;
62
103
  function secondsOf(whole, frac) {
63
104
  return frac.length === 0 ? whole : Number(`${whole}.${frac}`);
64
105
  }
@@ -91,8 +132,22 @@ function shapeSeconds(parts) {
91
132
  }
92
133
  function scanTime(raw) {
93
134
  let i = skipWhitespace(raw, 0);
135
+ if (isBullet(raw[i]) && isWhitespace(raw[i + 1])) i = skipWhitespace(raw, i + 1);
136
+ let outerOpen;
137
+ let n = emphasisRun(raw, i);
138
+ if (n && (raw[i + n] === "[" || raw[i + n] === "(" || isDigit(raw[i + n]))) {
139
+ outerOpen = raw[i];
140
+ i += n;
141
+ }
94
142
  const closer = raw[i] === "[" ? "]" : raw[i] === "(" ? ")" : null;
95
- if (closer) i++;
143
+ let innerOpen;
144
+ if (closer) {
145
+ n = emphasisRun(raw, ++i);
146
+ if (n && isDigit(raw[i + n])) {
147
+ innerOpen = raw[i];
148
+ i += n;
149
+ }
150
+ }
96
151
  const leadEnd = i;
97
152
  const shape = shapeAt(raw, i);
98
153
  if (!shape) return untimed(raw, false);
@@ -103,19 +158,30 @@ function scanTime(raw) {
103
158
  let frac = "";
104
159
  const sep = raw[j];
105
160
  if (sep === "." || sep === ",") {
106
- const n = digitRun(raw, j + 1);
107
- if (n >= 1 && n <= 3) {
161
+ const n2 = digitRun(raw, j + 1);
162
+ if (n2 >= 1 && n2 <= 3) {
108
163
  fracSep = sep;
109
- frac = raw.slice(j + 1, j + 1 + n);
110
- j += 1 + n;
164
+ frac = raw.slice(j + 1, j + 1 + n2);
165
+ j += 1 + n2;
111
166
  }
112
167
  }
113
168
  const timeEnd = j;
169
+ const followsTime = (at) => at >= raw.length || isWhitespace(raw[at]) || isGapSeparator(raw[at]);
170
+ let open;
114
171
  if (closer) {
172
+ let k = closingRun(raw, j, innerOpen);
173
+ if (k === j) open = innerOpen;
174
+ j = k;
115
175
  if (raw[j] !== closer) return untimed(raw, false);
116
176
  j++;
117
- } else if (j < raw.length && !isWhitespace(raw[j]) && !isGapSeparator(raw[j])) {
118
- return untimed(raw, false);
177
+ k = closingRun(raw, j, outerOpen);
178
+ if (k > j && followsTime(k)) j = k;
179
+ else open = outerOpen ?? open;
180
+ } else {
181
+ const k = closingRun(raw, j, outerOpen);
182
+ if (k === j) open = outerOpen;
183
+ j = k;
184
+ if (!followsTime(j)) return untimed(raw, false);
119
185
  }
120
186
  j = skipWhitespace(raw, j);
121
187
  if (isGapSeparator(raw[j])) j = skipWhitespace(raw, j + 1);
@@ -143,7 +209,8 @@ function scanTime(raw) {
143
209
  fracSep,
144
210
  fracDigits: frac.length
145
211
  },
146
- invalid: false
212
+ invalid: false,
213
+ text: withoutMarkers(raw, j, open)
147
214
  };
148
215
  }
149
216
  var pad = (n, width) => String(n).padStart(width, "0");
@@ -438,6 +505,7 @@ function applyEdits(text, edits, options) {
438
505
 
439
506
  // src/tracklist/kinds.ts
440
507
  var headerWords = new Set(headerWordsData.words);
508
+ var layeredWords = new Set(layeredWordsData.words);
441
509
  var defaultMarkers = null;
442
510
  function markerTable(extra) {
443
511
  const more = stringList(extra);
@@ -468,6 +536,15 @@ function markerWords(text) {
468
536
  flush();
469
537
  return out;
470
538
  }
539
+ function layeredPrefix(text) {
540
+ let i = 0;
541
+ while (i < text.length && isWhitespace(text[i])) i++;
542
+ let j = i;
543
+ while (j < text.length && !isWhitespace(text[j])) j++;
544
+ if (j === i || j === text.length || !inVocab(text.slice(i, j), layeredWords)) return 0;
545
+ while (j < text.length && isWhitespace(text[j])) j++;
546
+ return j < text.length ? j : 0;
547
+ }
471
548
  function isBlank(raw) {
472
549
  for (let i = 0; i < raw.length; i++) if (!isWhitespace(raw[i])) return false;
473
550
  return true;
@@ -691,7 +768,8 @@ function tracklistWarnings(input) {
691
768
  if (e.time) timed.push(i);
692
769
  });
693
770
  entries.forEach((e, i) => {
694
- if (e.kind === "track" && e.time === null && timed.length > 0) add(i, "time-missing");
771
+ if (e.kind === "track" && e.time === null && !e.layered && timed.length > 0)
772
+ add(i, "time-missing");
695
773
  if (invalid[i]) add(i, "invalid-time");
696
774
  if (unparsed[i]) add(i, "line-unparsed");
697
775
  });
@@ -732,9 +810,12 @@ function parseTracklist(text, options) {
732
810
  const rows = lines.map((line) => {
733
811
  const scan = scanTime(line.raw);
734
812
  const blank = isBlank(line.raw);
813
+ const layered = blank ? 0 : layeredPrefix(scan.text);
735
814
  const row = {
736
815
  line,
737
816
  scan,
817
+ text: scan.text.slice(layered),
818
+ layered: layered > 0,
738
819
  blank,
739
820
  candidate: false,
740
821
  dash: false,
@@ -745,11 +826,11 @@ function parseTracklist(text, options) {
745
826
  marker: false
746
827
  };
747
828
  if (blank) return row;
748
- const probe = lp.probe(scan.parts.rest);
829
+ const probe = lp.probe(row.text);
749
830
  row.probe = probe;
750
831
  row.dash = probe.spacedDash;
751
832
  row.candidate = scan.time !== null || row.dash;
752
- row.bare = bareCandidate(scan.parts.rest, probe.position);
833
+ row.bare = row.layered ? null : bareCandidate(row.text, probe.position);
753
834
  return row;
754
835
  });
755
836
  let candidates = 0;
@@ -762,13 +843,13 @@ function parseTracklist(text, options) {
762
843
  const listContext = candidates > 0 && 2 * dashes >= candidates;
763
844
  const confirmed = confirmPositions(
764
845
  rows.map((r) => r.bare),
765
- rows.map((r) => r.candidate)
846
+ rows.map((r) => r.candidate && !r.layered)
766
847
  );
767
848
  rows.forEach((r, i) => {
768
849
  if (r.blank || !r.probe) return;
769
850
  const bare = confirmed[i] ? r.bare : null;
770
851
  const probe = bare ? lp.probe(bare.stripped) : r.probe;
771
- r.marker = hasMarkerWord(bare ? bare.stripped : r.scan.parts.rest, markers);
852
+ r.marker = hasMarkerWord(bare ? bare.stripped : r.text, markers);
772
853
  const track = probe.finish(listContext && r.scan.time !== null && !r.marker);
773
854
  if (bare) track.position = { raw: bare.raw, number: bare.number };
774
855
  r.track = track;
@@ -784,7 +865,8 @@ function parseTracklist(text, options) {
784
865
  raw: r.line.raw,
785
866
  parts: r.scan.parts,
786
867
  time: r.scan.time,
787
- track: keep ? r.track : null
868
+ track: keep ? r.track : null,
869
+ layered: r.layered
788
870
  };
789
871
  });
790
872
  const warnings = tracklistWarnings({
@@ -800,9 +882,8 @@ function kindOf(r) {
800
882
  if (r.blank || !r.track) return "blank";
801
883
  const marker = !r.separated && r.marker;
802
884
  if (r.scan.time) return marker ? "marker" : "track";
803
- const rest = r.scan.parts.rest;
804
- if (isLink(rest)) return "link";
805
- if (isHeader(rest)) return "header";
885
+ if (isLink(r.text)) return "link";
886
+ if (isHeader(r.text)) return "header";
806
887
  if (marker) return "marker";
807
888
  return r.separated ? "track" : "note";
808
889
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackparse",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Spec-driven parser for music track strings: artists, feat credits, remixes/versions, junk and more from single track strings (store titles, YouTube titles, filenames) and multi-line tracklists.",
5
5
  "license": "MIT",
6
6
  "author": "Maksim Maksimov",