@ugfoundation/swaralipi-js 0.8.1 → 0.9.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.
Files changed (51) hide show
  1. package/dist/{batch-NDL6CW7Q.js → batch-PYBXQ4OM.js} +5 -5
  2. package/dist/{batch-NDL6CW7Q.js.map → batch-PYBXQ4OM.js.map} +1 -1
  3. package/dist/{chunk-LPCKL3AM.js → chunk-3ZQPM2CE.js} +28 -19
  4. package/dist/chunk-3ZQPM2CE.js.map +1 -0
  5. package/dist/{chunk-4IVUPMRK.js → chunk-7SMRG4IS.js} +14 -9
  6. package/dist/chunk-7SMRG4IS.js.map +1 -0
  7. package/dist/{chunk-4HNT6OAT.js → chunk-MYVIQU4O.js} +12 -3
  8. package/dist/chunk-MYVIQU4O.js.map +1 -0
  9. package/dist/{chunk-Z42M5JAA.js → chunk-RT4SVJMK.js} +5 -4
  10. package/dist/chunk-RT4SVJMK.js.map +1 -0
  11. package/dist/cli.cjs +33 -19
  12. package/dist/cli.cjs.map +1 -1
  13. package/dist/cli.js +3 -3
  14. package/dist/convert/index.cjs +31 -17
  15. package/dist/convert/index.cjs.map +1 -1
  16. package/dist/convert/index.d.cts +1 -1
  17. package/dist/convert/index.d.ts +1 -1
  18. package/dist/convert/index.js +3 -3
  19. package/dist/index.cjs +38 -17
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +2 -2
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.js +2 -2
  24. package/dist/{lay-Csa4CD46.d.cts → lay-CZIzR9cM.d.ts} +16 -2
  25. package/dist/{lay-Ct9ky_EE.d.ts → lay-CfDlPhH2.d.cts} +16 -2
  26. package/dist/player/index.cjs +80 -48
  27. package/dist/player/index.cjs.map +1 -1
  28. package/dist/player/index.d.cts +11 -9
  29. package/dist/player/index.d.ts +11 -9
  30. package/dist/player/index.js +68 -46
  31. package/dist/player/index.js.map +1 -1
  32. package/dist/{schedule-B_UaluBi.d.cts → schedule-9uvjnCG_.d.ts} +15 -10
  33. package/dist/{schedule-CdILTNsk.d.ts → schedule-TxSfHL-T.d.cts} +15 -10
  34. package/dist/{schema-DGA_bNVZ.d.ts → schema-Cf_epUIC.d.cts} +333 -63
  35. package/dist/{schema-DGA_bNVZ.d.cts → schema-Cf_epUIC.d.ts} +333 -63
  36. package/dist/spec/index.cjs +38 -17
  37. package/dist/spec/index.cjs.map +1 -1
  38. package/dist/spec/index.d.cts +26 -8
  39. package/dist/spec/index.d.ts +26 -8
  40. package/dist/spec/index.js +2 -2
  41. package/dist/viewer/index.cjs +306 -178
  42. package/dist/viewer/index.cjs.map +1 -1
  43. package/dist/viewer/index.d.cts +36 -12
  44. package/dist/viewer/index.d.ts +36 -12
  45. package/dist/viewer/index.js +234 -135
  46. package/dist/viewer/index.js.map +1 -1
  47. package/package.json +1 -1
  48. package/dist/chunk-4HNT6OAT.js.map +0 -1
  49. package/dist/chunk-4IVUPMRK.js.map +0 -1
  50. package/dist/chunk-LPCKL3AM.js.map +0 -1
  51. package/dist/chunk-Z42M5JAA.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { isArcRaw, glideTargets, buildSchedule, drumPattern } from '../chunk-4IVUPMRK.js';
2
- import { hasKomal, TAALS, graceNotes } from '../chunk-4HNT6OAT.js';
1
+ import { isArcRaw, glideTargets, buildSchedule, drumPattern } from '../chunk-7SMRG4IS.js';
2
+ import { hasKomal, isTaalFerta, sameTaal, TAALS, graceNotes } from '../chunk-MYVIQU4O.js';
3
3
 
4
4
  // src/viewer/glyphs.ts
5
5
  var SWARABITAN_SHUDDHA = {
@@ -49,6 +49,11 @@ var LAY_LABELS = {
49
49
  hi: { drut: "\u0926\u094D\u0930\u0941\u0924 \u0932\u092F", madhya: "\u092E\u0927\u094D\u092F \u0932\u092F", vilambit: "\u0935\u093F\u0932\u0902\u092C\u093F\u0924 \u0932\u092F" },
50
50
  en: { drut: "Fast (drut)", madhya: "Medium (madhya)", vilambit: "Slow (vilambit)" }
51
51
  };
52
+ var FREE_TAAL_LABELS = {
53
+ bn: "\u09AE\u09C1\u0995\u09CD\u09A4\u099B\u09A8\u09CD\u09A6",
54
+ hi: "\u092E\u0941\u0915\u094D\u0924\u091B\u0902\u0926",
55
+ en: "Free rhythm"
56
+ };
52
57
  function composeCluster(letter, opts) {
53
58
  const classes = [];
54
59
  if (opts.lang === "bn") {
@@ -194,42 +199,64 @@ function alignRows(rows) {
194
199
  }
195
200
  function resolveArcs(cells) {
196
201
  const arcs = [];
197
- let openStart;
202
+ let open;
198
203
  let sawEnds = false;
199
204
  const fillCells = [];
200
- const openAt = (i) => {
205
+ const openAt = (cell, note) => {
201
206
  sawEnds = true;
202
- if (openStart === void 0) openStart = i;
207
+ open ??= { cell, note };
203
208
  };
204
- const closeAt = (i) => {
209
+ const closeAt = (cell, note) => {
205
210
  sawEnds = true;
206
- if (openStart !== void 0) {
207
- arcs.push({ start: openStart, end: i, openLeft: false, openRight: false });
208
- openStart = void 0;
209
- } else {
210
- arcs.push({ start: 0, end: i, openLeft: true, openRight: false });
211
- }
211
+ const from = open ?? { cell: 0, note: 0 };
212
+ arcs.push({
213
+ start: from.cell,
214
+ startNote: from.note,
215
+ end: cell,
216
+ endNote: note,
217
+ openLeft: open === void 0,
218
+ openRight: false
219
+ });
220
+ open = void 0;
212
221
  };
213
222
  cells.forEach((cell, i) => {
214
- for (const note of cell.notes) {
215
- if (note.meend === "start") openAt(i);
216
- else if (note.meend === "end") closeAt(i);
217
- for (const b of note.below ?? []) {
218
- if (!isArcRaw(b.raw)) continue;
219
- for (const ch of b.raw) {
220
- if (ch === "w" || ch === "W") openAt(i);
221
- else if (ch === "x" || ch === "X") closeAt(i);
222
- else fillCells.push(i);
223
- }
224
- }
223
+ const pitched = cell.notes.flatMap((n, j) => isPitched(n) ? [j] : []);
224
+ const first = pitched[0] ?? 0;
225
+ const last = pitched[pitched.length - 1] ?? cell.notes.length - 1;
226
+ let marks = "";
227
+ cell.notes.forEach((note, j) => {
228
+ if (note.meend === "start") openAt(i, j);
229
+ else if (note.meend === "end") closeAt(i, j);
230
+ for (const b of note.below ?? []) if (isArcRaw(b.raw)) marks += b.raw;
231
+ });
232
+ const reopens = /[xX].*[wW]/.test(marks);
233
+ for (const ch of marks) {
234
+ if (ch === "w" || ch === "W") openAt(i, reopens ? last : first);
235
+ else if (ch === "x" || ch === "X") closeAt(i, reopens ? first : last);
236
+ else if (ch === "$") {
237
+ openAt(i, first);
238
+ closeAt(i, last);
239
+ } else fillCells.push(i);
225
240
  }
226
241
  });
227
- if (openStart !== void 0) {
228
- arcs.push({ start: openStart, end: Math.max(cells.length - 1, 0), openLeft: false, openRight: true });
242
+ const lastCell = Math.max(cells.length - 1, 0);
243
+ const lastNote = (c) => Math.max((cells[c]?.notes.length ?? 1) - 1, 0);
244
+ if (open !== void 0) {
245
+ arcs.push({
246
+ start: open.cell,
247
+ startNote: open.note,
248
+ end: lastCell,
249
+ endNote: lastNote(lastCell),
250
+ openLeft: false,
251
+ openRight: true
252
+ });
229
253
  } else if (!sawEnds && fillCells.length > 0) {
254
+ const end = Math.max(...fillCells);
230
255
  arcs.push({
231
256
  start: Math.min(...fillCells),
232
- end: Math.max(...fillCells),
257
+ startNote: 0,
258
+ end,
259
+ endNote: lastNote(end),
233
260
  openLeft: true,
234
261
  openRight: true,
235
262
  line: true
@@ -237,6 +264,35 @@ function resolveArcs(cells) {
237
264
  }
238
265
  return arcs;
239
266
  }
267
+ function arcInk(cells, arcs) {
268
+ const ink = { cells: cells.map(() => ""), notes: cells.map((c) => c.notes.map(() => "")) };
269
+ const mark = (c, j, cls, clip = false) => {
270
+ ink.notes[c][j] += ` ${cls}`;
271
+ if (clip && !ink.cells[c].includes(" sw-mk")) ink.cells[c] += " sw-mk";
272
+ };
273
+ for (const arc of arcs) {
274
+ const { start: s, end: e, startNote: j, endNote: k } = arc;
275
+ const hookL = arc.line !== true && !arc.openLeft;
276
+ const hookR = arc.line !== true && !arc.openRight;
277
+ if (s === e && hookL && hookR) {
278
+ if (j === k) {
279
+ mark(s, j, "sw-mos");
280
+ mark(s, j, "sw-mcs");
281
+ } else {
282
+ mark(s, j, "sw-mob");
283
+ for (let n = j + 1; n < k; n++) mark(s, n, "sw-mf");
284
+ mark(s, k, "sw-mcb");
285
+ }
286
+ continue;
287
+ }
288
+ for (let c = s; c <= e; c++) {
289
+ if (c === s && hookL) mark(c, j, "sw-mo", true);
290
+ else if (c === e && hookR) mark(c, k, "sw-mc", true);
291
+ else mark(c, 0, "sw-mm", true);
292
+ }
293
+ }
294
+ return ink;
295
+ }
240
296
  var SVARA_CHAR = /[srgmpqndtkuv]/;
241
297
  function rawMarks(raw, count) {
242
298
  if (raw === void 0) return void 0;
@@ -412,25 +468,9 @@ function renderNote(note, kind, opts, ctx) {
412
468
  const raw = rawDisplay(note);
413
469
  return bracketGlyphs(note, "open") + (raw !== void 0 ? `<span class="sw-raw" title="${escapeHtml(raw)}">${escapeHtml(raw)}</span>` : "") + bracketGlyphs(note, "close");
414
470
  }
415
- function renderArc(arc, offset) {
416
- const open = arc.line === true;
417
- const openL = arc.openLeft || open;
418
- const openR = arc.openRight || open;
419
- const fill = '<i class="sw-arc-m"></i>';
420
- const hookL = openL ? "" : '<i class="sw-arc-l">w</i>';
421
- const hookR = openR ? "" : '<i class="sw-arc-r">x</i>';
422
- const piece = (col, cls, inner) => `<span class="sw-arc${cls}" style="grid-column:${col}">${inner}</span>`;
423
- const f = offset + arc.start + COL_BASE;
424
- const t = offset + arc.end + COL_BASE;
425
- const openCls = (openL ? " sw-arc--openl" : "") + (openR ? " sw-arc--openr" : "");
426
- if (t === f) return piece(`${f}`, openCls, hookL + fill + hookR);
427
- const head = piece(`${f}`, openL ? " sw-arc--openl" : " sw-arc--head", hookL + fill);
428
- const tail = piece(`${t}`, openR ? " sw-arc--openr" : " sw-arc--tail", fill + hookR);
429
- const mid = t > f + 1 ? piece(`${f + 1} / ${t}`, " sw-arc--mid", fill) : "";
430
- return head + mid + tail;
431
- }
432
471
  function renderRow(row, partIdx, rowIdx, offset, ctx) {
433
472
  const cells = groupRowIntoCells(row.notes);
473
+ const ink = arcInk(cells, resolveArcs(cells));
434
474
  const out = [];
435
475
  if (row.label !== void 0)
436
476
  out.push(`<span class="sw-label">${escapeHtml(ctx.tr(row.label))}</span>`);
@@ -440,11 +480,10 @@ function renderRow(row, partIdx, rowIdx, offset, ctx) {
440
480
  const barCls = cell.kind === "bar" ? " sw-bar-cell" : "";
441
481
  const notes = cell.notes.map((note, j) => {
442
482
  const path = `${partIdx}.${rowIdx}.${cell.start + j}`;
443
- const cls = isPitched(note) ? " sw-p" : "";
444
483
  const bar = note.bar !== void 0 ? ` sw-bar sw-bar--${note.bar}` : "";
445
- return `<span class="sw-n${cls}${bar}" data-note="${path}">${renderNote(note, cell.kind, opts[j], ctx)}</span>`;
484
+ return `<span class="sw-n${bar}${ink.notes[ci][j]}" data-note="${path}">${renderNote(note, cell.kind, opts[j], ctx)}</span>`;
446
485
  }).join("");
447
- out.push(`<span class="sw-cell${barCls}" style="grid-column:${col}">${notes}</span>`);
486
+ out.push(`<span class="sw-cell${barCls}${ink.cells[ci]}" style="grid-column:${col}">${notes}</span>`);
448
487
  const slices = cellLyricSlices(cell);
449
488
  if (slices !== void 0) {
450
489
  const html = slices.map((t) => `<span>${escapeHtml(lyricText(t, ctx))}</span>`).join("");
@@ -457,7 +496,6 @@ function renderRow(row, partIdx, rowIdx, offset, ctx) {
457
496
  if (below.length > 0)
458
497
  out.push(`<span class="sw-below" style="grid-column:${col}">${renderAnnotation(below, ctx)}</span>`);
459
498
  });
460
- for (const arc of resolveArcs(cells)) out.push(renderArc(arc, offset));
461
499
  return `<div class="sw-row">${out.join("")}</div>`;
462
500
  }
463
501
  function renderHeader(meta, ctx) {
@@ -488,15 +526,25 @@ function renderToHtml(doc, opts = {}) {
488
526
  out.push(`<div class="sw-scroll"><div class="sw-block" style="${tracks}">`);
489
527
  let n = 0;
490
528
  let lay;
529
+ let taal = doc.meta.taal;
530
+ let named = !isTaalFerta(doc.meta.taalName);
491
531
  doc.parts.forEach((part, p) => {
492
532
  out.push('<section class="sw-part">');
493
533
  if (part.name !== void 0)
494
534
  out.push(`<h3 class="sw-part-name">${escapeHtml(ctx.tr(part.name))}</h3>`);
495
535
  part.rows.forEach((row, r) => {
536
+ const marks = [];
537
+ if (row.taal !== void 0 && (!named || !sameTaal(row.taal, taal))) {
538
+ taal = row.taal;
539
+ named = true;
540
+ const name = row.taalName ?? (taal === null ? FREE_TAAL_LABELS[ctx.lang] : taal.id);
541
+ marks.push(`<span class="sw-taal" data-taal="${escapeHtml(taal?.id ?? "free")}">${escapeHtml(ctx.tr(name))}</span>`);
542
+ }
496
543
  if (row.lay !== void 0 && row.lay !== lay) {
497
544
  lay = row.lay;
498
- out.push(`<div class="sw-lay" data-lay="${lay}">${escapeHtml(LAY_LABELS[ctx.lang][lay])}</div>`);
545
+ marks.push(`<span class="sw-lay" data-lay="${lay}">${escapeHtml(LAY_LABELS[ctx.lang][lay])}</span>`);
499
546
  }
547
+ if (marks.length > 0) out.push(`<div class="sw-change">${marks.join("")}</div>`);
500
548
  out.push(renderRow(row, p, r, layout.offsets[n++], ctx));
501
549
  });
502
550
  out.push("</section>");
@@ -515,7 +563,7 @@ function fitSwaralipi(root, minScale = 0, zoom = 1, minSpacing = 0.35, minSize =
515
563
  sheet.style.removeProperty("--sw-size");
516
564
  sheet.style.removeProperty("--sw-tight");
517
565
  const available = scroll.clientWidth;
518
- let natural = block.scrollWidth;
566
+ let natural = block.offsetWidth;
519
567
  if (available === 0 || natural === 0) continue;
520
568
  if (natural > available && minSpacing < 1) {
521
569
  let loose = minSpacing;
@@ -523,11 +571,11 @@ function fitSwaralipi(root, minScale = 0, zoom = 1, minSpacing = 0.35, minSize =
523
571
  for (let i = 0; i < 6; i++) {
524
572
  const mid = (loose + tight) / 2;
525
573
  sheet.style.setProperty("--sw-tight", String(mid));
526
- if (block.scrollWidth <= available) loose = mid;
574
+ if (block.offsetWidth <= available) loose = mid;
527
575
  else tight = mid;
528
576
  }
529
577
  sheet.style.setProperty("--sw-tight", String(loose));
530
- natural = block.scrollWidth;
578
+ natural = block.offsetWidth;
531
579
  }
532
580
  const base = parseFloat(getComputedStyle(sheet).fontSize);
533
581
  if (!(base > 0)) continue;
@@ -590,10 +638,12 @@ ${FONT_FACES}
590
638
 
591
639
  .sw-part-name { font-size: 0.85em; font-weight: 600; margin: 1em 0 0.2em; opacity: 0.85; }
592
640
 
593
- /* \u09B2\u09AF\u09BC marker: the line where the tempo tier turns over. A full-width rule
594
- rather than a row label \u2014 the label track is what the notation columns are
595
- measured against, and a six-em word in it would shove the whole sheet right. */
596
- .sw-lay {
641
+ /* \u09A4\u09BE\u09B2 / \u09B2\u09AF\u09BC change: the line where the \u09A4\u09BE\u09B2\u09AB\u09C7\u09B0\u09A4\u09BE or the tempo tier turns over,
642
+ centred between two systems the way \u09B8\u09CD\u09AC\u09B0\u09AC\u09BF\u09A4\u09BE\u09A8 prints "\u099D\u09BE\u0981\u09AA\u09A4\u09BE\u09B2" or
643
+ "\u09A6\u09CD\u09B0\u09C1\u09A4 \u09B2\u09AF\u09BC". A full-width band rather than a row label \u2014 the label track is
644
+ what the notation columns are measured against, and a six-em word in it
645
+ would shove the whole sheet right. */
646
+ .sw-change {
597
647
  grid-column: 1 / -1;
598
648
  display: flex;
599
649
  align-items: center;
@@ -607,12 +657,14 @@ ${FONT_FACES}
607
657
  opacity: 0.75;
608
658
  break-after: avoid;
609
659
  }
610
- .sw-lay::after {
660
+ .sw-change::before,
661
+ .sw-change::after {
611
662
  content: '';
612
663
  flex: 1;
613
664
  height: 1px;
614
665
  background: var(--sw-rule, rgba(128, 128, 128, 0.35));
615
666
  }
667
+ .sw-taal + .sw-lay::before { content: '\xB7'; margin-right: 0.6em; }
616
668
 
617
669
  /* ---- block grid: ONE column set for the whole sheet ----
618
670
  * NLTR emits a song as a single html table, which is why its lines align
@@ -625,31 +677,45 @@ ${FONT_FACES}
625
677
  column-gap: calc(var(--sw-cell-gap, 0.4em) * var(--sw-tight, 1));
626
678
  align-items: end;
627
679
  width: max-content;
680
+ /* The last column's cell spacers overhang half a gap past the grid; clip
681
+ that sideways so it neither counts as width to fit nor raises a scrollbar. */
682
+ overflow-x: clip;
628
683
  }
629
684
  .sw-part { display: contents; }
685
+ /* Four FIXED bands per row \u2014 above / notation / arc / lyric \u2014 so a beat label,
686
+ * a \u0995\u09A3 or a \u09AE\u09C0\u09A1\u09BC never changes how far a line sits from its words: the
687
+ * notation hugs its lyric and the next system starts a little further off,
688
+ * as the printed \u09B8\u09CD\u09AC\u09B0\u09AC\u09BF\u09A4\u09BE\u09A8 sets it. Content taller than its band overflows
689
+ * rather than pushing the rhythm apart. */
630
690
  .sw-row {
631
691
  grid-column: 1 / -1;
632
692
  display: grid;
633
693
  grid-template-columns: subgrid;
634
- grid-template-rows: auto auto auto auto;
694
+ grid-template-rows: 0.95em 1.35em 0.4em 1.35em;
635
695
  align-items: end;
636
- padding: 0.4em 0;
696
+ padding-bottom: 0.35em;
697
+ line-height: 1.35;
637
698
  }
638
699
  .sw-label { grid-column: 1; grid-row: 2; padding-right: 0.5em; font-weight: 600; }
639
700
 
640
- /* One column per cell; sub-notes are equal slices of it (min-width:0 kills
641
- * flex's min-content floor, else a wide sub-note steals width from its
642
- * neighbours while the syllable below it does not, and the two bands drift). */
643
- .sw-cell, .sw-lyr { display: flex; justify-content: center; }
644
- .sw-cell > .sw-n { white-space: nowrap; }
645
- .sw-cell > .sw-n.sw-p,
646
- .sw-lyr > span { flex: 1 1 0; min-width: 0; text-align: center; white-space: nowrap; }
647
- .sw-cell { grid-row: 2; }
648
- .sw-above { grid-row: 1; font-size: 0.62em; justify-self: center; white-space: nowrap; }
649
- .sw-arc,
650
- .sw-below { grid-row: 3; }
651
- .sw-lyr { grid-row: 4; padding-top: 0.3em; }
652
- .sw-below { font-size: 0.62em; justify-self: center; white-space: nowrap; }
701
+ /* One column per cell, its notes set tight as one word (\u09B0\u09B8\u09BE) and centred by
702
+ * two flex spacers. Each spacer overhangs half the column gap (a min-width
703
+ * paid back by a negative margin, so the column is sized by the notes alone),
704
+ * which lets neighbouring cells' spacers meet mid-gap: a \u09AE\u09C0\u09A1\u09BC crossing cells is
705
+ * inked by these boxes with no hole between them. */
706
+ .sw-cell, .sw-lyr { display: flex; justify-content: center; white-space: nowrap; }
707
+ .sw-cell {
708
+ --sw-half-gap: calc(var(--sw-cell-gap, 0.4em) * var(--sw-tight, 1) / 2);
709
+ grid-row: 2;
710
+ align-self: stretch;
711
+ }
712
+ .sw-cell::before, .sw-cell::after { content: ''; flex: 1 0 0; min-width: var(--sw-half-gap); }
713
+ .sw-cell::before { margin-left: calc(var(--sw-half-gap) * -1); }
714
+ .sw-cell::after { margin-right: calc(var(--sw-half-gap) * -1); }
715
+ .sw-cell > .sw-n { position: relative; }
716
+ .sw-above { grid-row: 1; font-size: 0.62em; line-height: 1; justify-self: center; white-space: nowrap; }
717
+ .sw-below { grid-row: 3; font-size: 0.62em; line-height: 1; justify-self: center; white-space: nowrap; }
718
+ .sw-lyr { grid-row: 4; }
653
719
 
654
720
  /* ---- notation clusters & marks ---- */
655
721
  .sw-cl { position: relative; display: inline-block; }
@@ -670,12 +736,16 @@ ${FONT_FACES}
670
736
  .sw-ati-taar::before { top: -0.3em; }
671
737
  .sw-ati-mandra::before { bottom: -0.35em; }
672
738
 
673
- /* grace notes: small raised clusters \u2014 \u0995\u09A3 before the note, \u09B0\u09C7\u09B6 after it */
739
+ /* grace notes: small raised clusters \u2014 \u0995\u09A3 before the note, \u09B0\u09C7\u09B6 after it.
740
+ * line-height 0 (here and on .sw-micro): a raised inline box taller than the
741
+ * strut grows its line box and drops that one note's baseline below its
742
+ * neighbours' (css2 \xA710.8). */
674
743
  .sw-kan,
675
744
  .sw-resh {
676
745
  position: relative;
677
746
  display: inline-block;
678
747
  font-size: 0.58em;
748
+ line-height: 0;
679
749
  vertical-align: 0.75em;
680
750
  }
681
751
  .sw-kan { margin-right: 0.08em; }
@@ -707,7 +777,7 @@ ${FONT_FACES}
707
777
  .sw-cut::before { top: 0.2em; }
708
778
  .sw-cut::after { top: 0.55em; }
709
779
  .sw-br { opacity: 0.8; padding: 0 0.06em; }
710
- .sw-micro { font-size: 0.6em; vertical-align: super; opacity: 0.8; }
780
+ .sw-micro { font-size: 0.6em; line-height: 0; vertical-align: super; opacity: 0.8; }
711
781
 
712
782
  /* raw fallback: never dropped, visibly non-notation. Swarabitan first \u2014
713
783
  * raw IS that font's encoding, so undecoded cells show NLTR's own glyphs
@@ -727,49 +797,46 @@ ${FONT_FACES}
727
797
  /* a bar owns a whole column but needs no matra of width */
728
798
  .sw-bar-cell { min-width: 0; }
729
799
 
730
- /* ---- \u09AE\u09C0\u09A1\u09BC under-arcs (from w/x segments and spec meend) ----
731
- * Swarabitan draws the arc as three pieces \u2014 'w' the opening hook, 'y' a
732
- * flat fill, 'x' the closing hook. We use the font's own hooks and stretch
733
- * the fill as a CSS bar (the 'y' glyph IS a plain rectangle), so one arc
734
- * spans any number of columns. An open side = the arc continues onto the
735
- * next system: drop that hook and leave the fill squared off. */
736
- .sw-arc {
737
- display: flex;
738
- align-items: baseline;
739
- line-height: 1;
740
- font-family: Swarabitan, var(--sw-font, sans-serif);
741
- /* The 0.9em sits on the children, not here: the end pieces below size
742
- themselves off the block's own column-gap, which is in the block's em. */
743
- }
744
- .sw-arc > i, .sw-arc::before { font-size: 0.9em; }
745
- .sw-arc-l, .sw-arc-r { flex: none; font-style: normal; }
746
- /* Every piece needs the same baseline \u2014 the fill rides it, so a piece holding
747
- only fill would otherwise drop to the bottom of the row. A zero-width copy
748
- of the hook glyph is the strut (same font, same metrics). */
749
- .sw-arc::before { content: 'w'; flex: none; width: 0; overflow: hidden; }
750
- /* The ends hang from the two notes' CENTRES, as the print sets them, so the
751
- arc is three grid items: each end piece takes half of its own column plus
752
- the column-gap it has to bridge, and the negative margin on the same side
753
- lets that bridge overhang the grid area instead of shifting the piece. */
754
- .sw-arc--head, .sw-arc--tail {
755
- --sw-arc-gap: calc(var(--sw-cell-gap, 0.4em) * var(--sw-tight, 1));
756
- width: calc(50% + var(--sw-arc-gap));
757
- }
758
- .sw-arc--head { justify-self: end; margin-right: calc(var(--sw-arc-gap) * -1); }
759
- .sw-arc--tail { justify-self: start; margin-left: calc(var(--sw-arc-gap) * -1);
760
- justify-content: flex-end; }
761
- /* The fill's geometry is measured off the font's own 'y' glyph: ink 0.11em
762
- tall whose bottom sits 0.01em above the baseline. An empty inline-block
763
- takes its bottom margin edge as its baseline, so -0.01em lifts the bar to
764
- exactly where the hooks' flat stroke runs \u2014 no seam at either join. */
765
- .sw-arc-m {
766
- flex: 1 1 0;
767
- min-width: 0;
768
- align-self: baseline;
769
- height: 0.11em;
770
- margin-bottom: -0.01em;
771
- background: currentColor;
772
- }
800
+ /* ---- \u09AE\u09C0\u09A1\u09BC under-arcs (from w/x/$ segments and spec meend) ----
801
+ * Swarabitan's own arc, all langs: w is the opening hook, curling down from
802
+ * its tip into a flat, x its mirror, y that flat alone \u2014 and they join
803
+ * hole-free, so one run of text is a whole end (tools/align-check/glyph.html).
804
+ * Each end sets its hook tip on its note's centre and runs the flat toward the
805
+ * other end (render.ts arcInk): across cells it is cut at the mid-gap, where
806
+ * the next cell's run carries on (.sw-mk clips a cell half a gap past each
807
+ * side, as far as its spacers reach); inside one cell, at its note's edge. So
808
+ * a long arc extends the flat and a short one trims it off. line-height 0 sets
809
+ * every run's baseline the same distance below its note. The clip reaches
810
+ * 0.5px further so two cells' anti-aliased edges overlap instead of leaving a
811
+ * hairline seam between their identical flats. */
812
+ .sw-mk { clip-path: inset(-3em calc(var(--sw-half-gap) * -1 - 0.5px)); }
813
+ .sw-mo::after, .sw-mob::after, .sw-mos::after,
814
+ .sw-mc::before, .sw-mcb::before, .sw-mcs::before,
815
+ .sw-mf::before, .sw-mm::before {
816
+ content: 'yyyyyyyyyyyy';
817
+ position: absolute;
818
+ top: calc(100% + 0.05em);
819
+ font-family: Swarabitan;
820
+ font-size: 0.9em;
821
+ line-height: 0;
822
+ white-space: pre;
823
+ overflow-x: clip;
824
+ }
825
+ .sw-mo::after, .sw-mob::after, .sw-mos::after { content: 'wyyyyyyyyyyyy'; }
826
+ /* rtl: a closing run cut short keeps its hook on its right edge */
827
+ .sw-mc::before, .sw-mcb::before, .sw-mcs::before { content: 'yyyyyyyyyyyyx'; direction: rtl; }
828
+ .sw-mo::after, .sw-mob::after { left: 50%; }
829
+ .sw-mc::before, .sw-mcb::before { right: 50%; }
830
+ .sw-mos::after { left: 0; }
831
+ .sw-mcs::before { right: 0; }
832
+ .sw-mf::before { left: 0; right: 0; }
833
+ .sw-mm::before { left: -12em; }
834
+ /* Inside one cell a hook gets half a note to curl in, about a third of its
835
+ * curl: squeezed to half width (same stroke) it lands flat at the cut, so the
836
+ * two flats meet level whatever the notes' widths \u2014 a small arc, not a V. */
837
+ .sw-mob::after, .sw-mos::after, .sw-mcb::before, .sw-mcs::before { width: 100%; transform: scaleX(0.5); }
838
+ .sw-mob::after, .sw-mos::after { transform-origin: left; }
839
+ .sw-mcb::before, .sw-mcs::before { transform-origin: right; }
773
840
 
774
841
  /* ---- annotation content ---- */
775
842
  .sw-beat { position: relative; display: inline-block; padding: 0 0.1em; }
@@ -815,7 +882,7 @@ var SWARALIPI_PRINT_CSS = `
815
882
  .sw-sheet, .sw-staff { -webkit-print-color-adjust: exact; print-color-adjust: exact; color: #000; }
816
883
  .sw-scroll { overflow: visible !important; }
817
884
  .sw-row { break-inside: avoid; page-break-inside: avoid; }
818
- .sw-part-name, .sw-header, .sw-lay { break-after: avoid-page; page-break-after: avoid; }
885
+ .sw-part-name, .sw-header, .sw-change { break-after: avoid-page; page-break-after: avoid; }
819
886
  .sw-hl { background: none !important; }
820
887
  /* Keep each engraved staff system whole across a page break (the abcjs line
821
888
  is a <div> wrapping the <svg>, so the break rule must sit on the div). */
@@ -872,11 +939,15 @@ function eventPaths(schedule) {
872
939
  }
873
940
  return paths;
874
941
  }
942
+ var barLen = (m) => m.beatsPerBar > 0 ? m.beatsPerBar : 4;
875
943
  function abcText(s) {
876
944
  return s.replace(/[\r\n]+/g, " ").trim();
877
945
  }
878
946
  function buildAbc(schedule, lyricOf, opts) {
879
- const beatsPerBar = opts.beatsPerBar > 0 ? opts.beatsPerBar : 4;
947
+ const meters = opts.meters?.length ? opts.meters : [{ beat: 0, beatsPerBar: opts.beatsPerBar, drums: opts.drums }];
948
+ const withDrums = opts.drums !== void 0 || meters.some((m2) => m2.drums !== void 0);
949
+ let m = 0;
950
+ let beatsPerBar = barLen(meters[0]);
880
951
  const tokens = [];
881
952
  const words = [];
882
953
  let barPos = 0;
@@ -890,6 +961,17 @@ function buildAbc(schedule, lyricOf, opts) {
890
961
  }
891
962
  };
892
963
  schedule.events.forEach((ev, i) => {
964
+ while (m + 1 < meters.length && ev.beat >= meters[m + 1].beat - EPS2) {
965
+ m++;
966
+ if (barPos > EPS2) {
967
+ tokens.push("|");
968
+ words.push("|");
969
+ }
970
+ barPos = 0;
971
+ const next = barLen(meters[m]);
972
+ if (next !== beatsPerBar) tokens.push(`[M:${ratioMeter(next)}]`);
973
+ beatsPerBar = next;
974
+ }
893
975
  if (ev.midi === null) {
894
976
  if (ev.kind === "rest") {
895
977
  tokens.push(`z${abcDuration(ev.durationBeats)}`);
@@ -908,16 +990,16 @@ function buildAbc(schedule, lyricOf, opts) {
908
990
  });
909
991
  if (tokens.length && tokens[tokens.length - 1] !== "|") tokens.push("|]");
910
992
  else if (tokens.length) tokens[tokens.length - 1] = "|]";
911
- const meter = ratioMeter(beatsPerBar);
993
+ const meter = ratioMeter(barLen(meters[0]));
912
994
  const head = ["X:1", `M:${meter}`, "L:1/4", `Q:1/4=${Math.round(opts.bpm)}`];
913
- if (opts.drums) head.push(...percMapLines(opts.drums));
995
+ if (withDrums) head.push(...percMapLines(meters.flatMap((x) => x.drums ?? [])));
914
996
  head.push("K:C treble");
915
997
  const melodyBody = tokens.length ? tokens.join(" ") : "z |]";
916
998
  const lines = [...head];
917
- if (opts.drums) {
999
+ if (withDrums) {
918
1000
  lines.push("V:1", melodyBody);
919
1001
  if (opts.lyrics && words.some((w) => w !== "*" && w !== "|")) lines.push(`w: ${words.join(" ")}`);
920
- lines.push("V:2 clef=perc stem=up", buildDrumVoice(schedule.totalBeats, beatsPerBar, opts.drums));
1002
+ lines.push("V:2 clef=perc stem=up", buildDrumVoice(schedule.totalBeats, meters));
921
1003
  } else {
922
1004
  lines.push(melodyBody);
923
1005
  if (opts.lyrics && words.some((w) => w !== "*" && w !== "|")) lines.push(`w: ${words.join(" ")}`);
@@ -946,13 +1028,21 @@ function percMapLines(voices) {
946
1028
  }
947
1029
  return lines;
948
1030
  }
949
- function buildDrumVoice(totalBeats, beatsPerBar, pattern) {
950
- const n = Math.max(1, Math.ceil(totalBeats));
1031
+ function buildDrumVoice(totalBeats, meters) {
951
1032
  const tokens = [];
952
- for (let beat = 0; beat < n; beat++) {
953
- tokens.push(DRUM_ABC[pattern[beat % pattern.length]].note);
954
- if ((beat + 1) % beatsPerBar === 0) tokens.push("|");
955
- }
1033
+ meters.forEach((m, i) => {
1034
+ const bpb = barLen(m);
1035
+ if (i > 0) {
1036
+ if (tokens.length > 0 && tokens[tokens.length - 1] !== "|") tokens.push("|");
1037
+ if (bpb !== barLen(meters[i - 1])) tokens.push(`[M:${ratioMeter(bpb)}]`);
1038
+ }
1039
+ const end = meters[i + 1]?.beat ?? Math.max(1, Math.ceil(totalBeats));
1040
+ const n = Math.ceil(end - m.beat - 1e-6);
1041
+ for (let beat = 0; beat < n; beat++) {
1042
+ tokens.push(m.drums?.length ? DRUM_ABC[m.drums[beat % m.drums.length]].note : "z");
1043
+ if ((beat + 1) % bpb === 0) tokens.push("|");
1044
+ }
1045
+ });
956
1046
  if (tokens.length === 0 || tokens[tokens.length - 1] !== "|") tokens.push("|]");
957
1047
  else tokens[tokens.length - 1] = "|]";
958
1048
  return tokens.join(" ");
@@ -963,16 +1053,25 @@ function lyricLookup(doc) {
963
1053
  return doc.parts[p]?.rows[r]?.notes[i]?.lyric;
964
1054
  };
965
1055
  }
966
- function resolveDrumTaal(doc) {
967
- const id = doc.meta.taal?.id;
1056
+ function resolveDrumTaal(taal) {
1057
+ const id = taal?.id;
968
1058
  return id !== void 0 && id in TAALS ? TAALS[id] : { numBeats: 4, sam: 0 };
969
1059
  }
970
1060
  function docToAbc(doc, opts = {}) {
971
1061
  const schedule = buildSchedule(doc, { tonic: opts.tonic, baseOctave: opts.baseOctave, lay: false });
972
1062
  const bpm = opts.bpm ?? doc.meta.bpm ?? 90;
973
- const beatsPerBar = opts.beatsPerBar ?? doc.meta.taal?.numBeats ?? 4;
974
- const drums = opts.drums ? drumPattern(resolveDrumTaal(doc)) : void 0;
975
- return buildAbc(schedule, lyricLookup(doc), { bpm, lyrics: opts.lyrics !== false, beatsPerBar, drums });
1063
+ const meters = schedule.tempo.map((seg) => ({
1064
+ beat: seg.beat,
1065
+ beatsPerBar: opts.beatsPerBar ?? seg.taal?.numBeats ?? 4,
1066
+ drums: opts.drums && seg.taal !== null ? drumPattern(resolveDrumTaal(seg.taal)) : void 0
1067
+ }));
1068
+ return buildAbc(schedule, lyricLookup(doc), {
1069
+ bpm,
1070
+ lyrics: opts.lyrics !== false,
1071
+ beatsPerBar: meters[0].beatsPerBar,
1072
+ drums: opts.drums ? meters[0].drums ?? [] : void 0,
1073
+ meters
1074
+ });
976
1075
  }
977
1076
  async function abcToSvg(abc, opts = {}, notePaths) {
978
1077
  if (typeof document === "undefined") {
@@ -1051,7 +1150,7 @@ function buildTaalAbc(taal, opts = {}) {
1051
1150
  const voices = Array.from({ length: n }, (_, i) => src[i % src.length]);
1052
1151
  const bpm = Math.round(opts.bpm ?? 90);
1053
1152
  const head = ["X:1", `M:${ratioMeter(n)}`, "L:1/4", `Q:1/4=${bpm}`, ...percMapLines(voices), "K:C"];
1054
- const lines = [...head, "V:1 clef=perc stem=up", buildDrumVoice(n, n, voices)];
1153
+ const lines = [...head, "V:1 clef=perc stem=up", buildDrumVoice(n, [{ beat: 0, beatsPerBar: n, drums: voices }])];
1055
1154
  if (opts.bols?.length) {
1056
1155
  const w = opts.bols.slice(0, n).map((b) => b.trim() ? abcText(b).replace(/\s+/g, "~") : "*");
1057
1156
  while (w.length < n) w.push("*");
@@ -1439,6 +1538,6 @@ function defineSwaralipiSheet(tagName = "swaralipi-sheet") {
1439
1538
  return ctor;
1440
1539
  }
1441
1540
 
1442
- export { DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, LABELS, LAY_LABELS, MARKS, MAX_ZOOM, MIN_ZOOM, NOTATION_FONT_URLS, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, ZOOM_STEP, abcDuration, abcToSvg, alignRows, buildAbc, buildTaalAbc, cellMatra, composeCluster, defineSwaralipiSheet, docToAbc, fitSwaralipi, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderTaalStaffHtml, renderToHtml, resolveArcs };
1541
+ export { DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, FREE_TAAL_LABELS, LABELS, LAY_LABELS, MARKS, MAX_ZOOM, MIN_ZOOM, NOTATION_FONT_URLS, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, ZOOM_STEP, abcDuration, abcToSvg, alignRows, buildAbc, buildTaalAbc, cellMatra, composeCluster, defineSwaralipiSheet, docToAbc, fitSwaralipi, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderTaalStaffHtml, renderToHtml, resolveArcs };
1443
1542
  //# sourceMappingURL=index.js.map
1444
1543
  //# sourceMappingURL=index.js.map