@ugfoundation/swaralipi-js 0.2.0 → 0.3.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 (37) hide show
  1. package/dist/{batch-CIAXVKP5.js → batch-QPSHVNDS.js} +4 -4
  2. package/dist/{batch-CIAXVKP5.js.map → batch-QPSHVNDS.js.map} +1 -1
  3. package/dist/{chunk-2NBNF6JP.js → chunk-4YTOLN3C.js} +3 -3
  4. package/dist/{chunk-2NBNF6JP.js.map → chunk-4YTOLN3C.js.map} +1 -1
  5. package/dist/chunk-V5ZSUJCA.js +354 -0
  6. package/dist/chunk-V5ZSUJCA.js.map +1 -0
  7. package/dist/{chunk-SH2INDZ2.js → chunk-VCRWS3F3.js} +3 -3
  8. package/dist/{chunk-SH2INDZ2.js.map → chunk-VCRWS3F3.js.map} +1 -1
  9. package/dist/cli.cjs +1 -1
  10. package/dist/cli.cjs.map +1 -1
  11. package/dist/cli.js +2 -2
  12. package/dist/convert/index.cjs +1 -1
  13. package/dist/convert/index.cjs.map +1 -1
  14. package/dist/convert/index.js +2 -2
  15. package/dist/index.cjs +1 -1
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/player/index.cjs +58 -2
  19. package/dist/player/index.cjs.map +1 -1
  20. package/dist/player/index.d.cts +12 -158
  21. package/dist/player/index.d.ts +12 -158
  22. package/dist/player/index.js +31 -324
  23. package/dist/player/index.js.map +1 -1
  24. package/dist/schedule-DhO_Xzz4.d.cts +167 -0
  25. package/dist/schedule-PTHUONrA.d.ts +167 -0
  26. package/dist/spec/index.cjs +1 -1
  27. package/dist/spec/index.cjs.map +1 -1
  28. package/dist/spec/index.d.cts +1 -1
  29. package/dist/spec/index.d.ts +1 -1
  30. package/dist/spec/index.js +1 -1
  31. package/dist/viewer/index.cjs +684 -202
  32. package/dist/viewer/index.cjs.map +1 -1
  33. package/dist/viewer/index.d.cts +217 -64
  34. package/dist/viewer/index.d.ts +217 -64
  35. package/dist/viewer/index.js +421 -177
  36. package/dist/viewer/index.js.map +1 -1
  37. package/package.json +7 -2
@@ -11,6 +11,30 @@ var __export = (target, all) => {
11
11
  for (var name in all)
12
12
  __defProp(target, name, { get: all[name], enumerable: true });
13
13
  };
14
+
15
+ // src/spec/taals.ts
16
+ var TAALS;
17
+ var init_taals = __esm({
18
+ "src/spec/taals.ts"() {
19
+ TAALS = {
20
+ dadra: { id: "dadra", numBeats: 6, bhaags: [3, 3], sam: 0, khaali: [3] },
21
+ kehrwa: { id: "kehrwa", numBeats: 8, bhaags: [4, 4], sam: 0, khaali: [4] },
22
+ rupak: { id: "rupak", numBeats: 7, bhaags: [3, 2, 2], sam: 0, khaali: [0] },
23
+ jhaptaal: { id: "jhaptaal", numBeats: 10, bhaags: [2, 3, 2, 3], sam: 0, khaali: [5] },
24
+ ektaal: { id: "ektaal", numBeats: 12, bhaags: [2, 2, 2, 2, 2, 2], sam: 0, khaali: [2, 6] },
25
+ adachautaal: { id: "adachautaal", numBeats: 14, sam: 0 },
26
+ choutaal: { id: "choutaal", numBeats: 12, bhaags: [2, 2, 2, 2, 2, 2], sam: 0, khaali: [2, 6] },
27
+ // চৌতাল
28
+ shashthi: { id: "shashthi", numBeats: 6, bhaags: [2, 4], sam: 0 },
29
+ // ষষ্ঠী
30
+ teora: { id: "teora", numBeats: 7, bhaags: [3, 2, 2], sam: 0 },
31
+ // টেওরা
32
+ teentaal: { id: "teentaal", numBeats: 16, bhaags: [4, 4, 4, 4], sam: 0, khaali: [8] },
33
+ aratheka: { id: "aratheka", numBeats: 16, bhaags: [4, 4, 4, 4], sam: 0, khaali: [8] }
34
+ // আড়াঠেকা
35
+ };
36
+ }
37
+ });
14
38
  var octaveSchema, svaraLetterSchema, svaraSchema, microtoneSchema, barSchema, bracketSchema, noteSchema, rowSchema, partSchema, taalSchema, metaSchema, swaralipiSchema;
15
39
  var init_schema = __esm({
16
40
  "src/spec/schema.ts"() {
@@ -123,30 +147,6 @@ var init_schema = __esm({
123
147
  }
124
148
  });
125
149
 
126
- // src/spec/taals.ts
127
- var TAALS;
128
- var init_taals = __esm({
129
- "src/spec/taals.ts"() {
130
- TAALS = {
131
- dadra: { id: "dadra", numBeats: 6, bhaags: [3, 3], sam: 0, khaali: [3] },
132
- kehrwa: { id: "kehrwa", numBeats: 8, bhaags: [4, 4], sam: 0, khaali: [4] },
133
- rupak: { id: "rupak", numBeats: 7, bhaags: [3, 2, 2], sam: 0, khaali: [0] },
134
- jhaptaal: { id: "jhaptaal", numBeats: 10, bhaags: [2, 3, 2, 3], sam: 0, khaali: [5] },
135
- ektaal: { id: "ektaal", numBeats: 12, bhaags: [2, 2, 2, 2, 2, 2], sam: 0, khaali: [2, 6] },
136
- adachautaal: { id: "adachautaal", numBeats: 14, sam: 0 },
137
- choutaal: { id: "choutaal", numBeats: 12, bhaags: [2, 2, 2, 2, 2, 2], sam: 0, khaali: [2, 6] },
138
- // চৌতাল
139
- shashthi: { id: "shashthi", numBeats: 6, bhaags: [2, 4], sam: 0 },
140
- // ষষ্ঠী
141
- teora: { id: "teora", numBeats: 7, bhaags: [3, 2, 2], sam: 0 },
142
- // টেওরা
143
- teentaal: { id: "teentaal", numBeats: 16, bhaags: [4, 4, 4, 4], sam: 0, khaali: [8] },
144
- aratheka: { id: "aratheka", numBeats: 16, bhaags: [4, 4, 4, 4], sam: 0, khaali: [8] }
145
- // আড়াঠেকা
146
- };
147
- }
148
- });
149
-
150
150
  // src/spec/index.ts
151
151
  var spec_exports = {};
152
152
  __export(spec_exports, {
@@ -179,91 +179,67 @@ var init_spec = __esm({
179
179
  init_schema();
180
180
  init_schema();
181
181
  init_taals();
182
- SPEC_VERSION = "0.2.0";
182
+ SPEC_VERSION = "0.3.0";
183
183
  }
184
184
  });
185
185
 
186
186
  // src/viewer/glyphs.ts
187
- var LETTERS = {
188
- bn: {
189
- s: { shuddha: "\u09B8" },
190
- r: { shuddha: "\u09B0", komal: "\u098B" },
191
- g: { shuddha: "\u0997", komal: "\u099C\u09CD\u099E" },
192
- m: { shuddha: "\u09AE", tivra: "\u0995\u09CD\u09B7" },
193
- p: { shuddha: "\u09AA" },
194
- d: { shuddha: "\u09A7", komal: "\u09A6" },
195
- n: { shuddha: "\u09A8", komal: "\u09A3" }
196
- },
197
- hi: {
198
- s: { shuddha: "\u0938" },
199
- r: { shuddha: "\u0930", komal: "\u090B" },
200
- g: { shuddha: "\u0917", komal: "\u091C\u094D\u091E" },
201
- m: { shuddha: "\u092E", tivra: "\u0915\u094D\u0937" },
202
- p: { shuddha: "\u092A" },
203
- d: { shuddha: "\u0927", komal: "\u0926" },
204
- n: { shuddha: "\u0928", komal: "\u0923" }
205
- },
206
- en: {
207
- s: { shuddha: "S" },
208
- r: { shuddha: "R" },
209
- g: { shuddha: "G" },
210
- m: { shuddha: "M" },
211
- p: { shuddha: "P" },
212
- d: { shuddha: "D" },
213
- n: { shuddha: "N" }
214
- }
187
+ var SWARABITAN_SHUDDHA = {
188
+ s: "s",
189
+ r: "r",
190
+ g: "g",
191
+ m: "m",
192
+ p: "p",
193
+ d: "q",
194
+ n: "n"
195
+ // q = ধ
215
196
  };
197
+ var SWARABITAN_KOMAL = {
198
+ r: "v",
199
+ g: "t",
200
+ d: "d",
201
+ n: "u"
202
+ // ঋ জ্ঞ দ ণ
203
+ };
204
+ var SWARABITAN_TIVRA = { m: "k" };
205
+ var BHATKHANDE_OCTAVE = {
206
+ madhyam: "",
207
+ taar: "u",
208
+ mandra: "l",
209
+ "ati-taar": "U",
210
+ "ati-mandra": "L"
211
+ };
212
+ var BHATKHANDE_HAS_KOMAL = /* @__PURE__ */ new Set(["r", "g", "d", "n"]);
216
213
  var DIGITS = {
217
214
  bn: "\u09E6\u09E7\u09E8\u09E9\u09EA\u09EB\u09EC\u09ED\u09EE\u09EF",
218
215
  hi: "\u0966\u0967\u0968\u0969\u096A\u096B\u096C\u096D\u096E\u096F",
219
216
  en: "0123456789"
220
217
  };
221
- var VOWEL_LETTER_GLYPHS = /* @__PURE__ */ new Set(["\u098B", "\u090B"]);
222
218
  var MARKS = {
223
- bn: { matra: "\u09BE", hyphen: "-", melisma: "\u09F9", cut: "\u0983", reph: "\u09B0\u09CD" },
224
- hi: { matra: "\u093E", hyphen: "-", melisma: "\u0970", cut: "\u0903", reph: "\u0930\u094D" },
225
- en: { matra: "", hyphen: "-", melisma: "\xB7", cut: ":", reph: "" }
219
+ bn: { hyphen: "-", melisma: "\u09F9", cut: "\u0983" },
220
+ hi: { hyphen: "-", melisma: "\u0970", cut: "\u0903" },
221
+ en: { hyphen: "-", melisma: "\xB7", cut: ":" }
226
222
  };
227
223
  var LABELS = {
228
- bn: { title: "\u09A8\u09BE\u09AE", taal: "\u09A4\u09BE\u09B2", raag: "\u09B0\u09BE\u0997", avartan: "\u0986\u09AC\u09B0\u09CD\u09A4\u09A8", notationBy: "\u09B8\u09CD\u09AC\u09B0\u09B2\u09BF\u09AA\u09BF" },
229
- hi: { title: "\u0928\u093E\u092E", taal: "\u0924\u093E\u0932", raag: "\u0930\u093E\u0917", avartan: "\u0906\u0935\u0930\u094D\u0924\u0928", notationBy: "\u0938\u094D\u0935\u0930\u0932\u093F\u092A\u093F" },
230
- en: { title: "Song", taal: "Taal", raag: "Raag", avartan: "Avartan", notationBy: "Notation" }
224
+ bn: { title: "\u09A8\u09BE\u09AE", taal: "\u09A4\u09BE\u09B2", raag: "\u09B0\u09BE\u0997", avartan: "\u0986\u09AC\u09B0\u09CD\u09A4\u09A8", notationBy: "\u09B8\u09CD\u09AC\u09B0\u09B2\u09BF\u09AA\u09BF\u0995\u09BE\u09B0" },
225
+ hi: { title: "\u0928\u093E\u092E", taal: "\u0924\u093E\u0932", raag: "\u0930\u093E\u0917", avartan: "\u0906\u0935\u0930\u094D\u0924\u0928", notationBy: "\u0938\u094D\u0935\u0930\u0932\u093F\u092A\u093F\u0915\u093E\u0930" },
226
+ en: { title: "Song", taal: "Taal", raag: "Raag", avartan: "Avartan", notationBy: "Notator" }
231
227
  };
232
228
  function composeCluster(letter, opts) {
233
- const forms = LETTERS[opts.lang][letter];
234
- const marks = MARKS[opts.lang];
235
229
  const classes = [];
236
- let base = forms.shuddha;
237
- if (opts.komal) {
238
- if (forms.komal !== void 0) base = forms.komal;
239
- else classes.push("sw-komal");
240
- }
241
- if (opts.tivra) {
242
- if (forms.tivra !== void 0) base = forms.tivra;
243
- else classes.push("sw-tivra");
244
- }
245
- const isVowelLetter = VOWEL_LETTER_GLYPHS.has(base);
246
- let text = base;
247
- switch (opts.octave) {
248
- case "taar":
249
- case "ati-taar":
250
- if (marks.reph === "") classes.push("sw-dot-above");
251
- else if (isVowelLetter) classes.push("sw-reph-css");
252
- else text = marks.reph + text;
253
- if (opts.octave === "ati-taar") classes.push("sw-ati-taar");
254
- break;
255
- case "mandra":
256
- case "ati-mandra":
257
- classes.push(marks.reph === "" ? "sw-dot-below" : "sw-mandra");
258
- if (opts.octave === "ati-mandra") classes.push("sw-ati-mandra");
259
- break;
260
- }
261
- let matraSeparate = false;
262
- if (opts.withMatra && marks.matra !== "") {
263
- if (isVowelLetter) matraSeparate = true;
264
- else text += marks.matra;
230
+ if (opts.lang === "bn") {
231
+ let text = opts.tivra && SWARABITAN_TIVRA[letter] || opts.komal && SWARABITAN_KOMAL[letter] || SWARABITAN_SHUDDHA[letter];
232
+ if (opts.octave === "taar" || opts.octave === "ati-taar") text += "f";
233
+ else if (opts.octave === "mandra" || opts.octave === "ati-mandra") text += "h";
234
+ if (opts.octave === "ati-taar") classes.push("sw-ati-taar");
235
+ if (opts.octave === "ati-mandra") classes.push("sw-ati-mandra");
236
+ if (opts.withMatra) text += "a";
237
+ return { text, classes };
265
238
  }
266
- return { text, classes, matraSeparate };
239
+ let base = letter;
240
+ if (opts.tivra && letter === "m") base = "M";
241
+ else if (opts.komal && BHATKHANDE_HAS_KOMAL.has(letter)) base = base.toUpperCase();
242
+ return { text: base + BHATKHANDE_OCTAVE[opts.octave], classes };
267
243
  }
268
244
  function localizeDigits(s, lang) {
269
245
  const digits = DIGITS[lang];
@@ -386,11 +362,8 @@ function bracketGlyphs(note, edge) {
386
362
  if (!note.brackets) return "";
387
363
  return note.brackets.filter((b) => b.edge === edge).map((b) => `<span class="sw-br">${BRACKET_GLYPHS[edge][b.kind]}</span>`).join("");
388
364
  }
389
- function isoMatra(ctx) {
390
- return ctx.marks.matra === "" ? "" : `<span class="sw-iso">${NBSP}${ctx.marks.matra}</span>`;
391
- }
392
- function restGlyph(ctx) {
393
- return ctx.marks.matra === "" ? '<span class="sw-iso">\u2013</span>' : isoMatra(ctx);
365
+ function matraGlyph(ctx) {
366
+ return ctx.lang === "bn" ? '<span class="sw-cl">a</span>' : '<span class="sw-iso">\u2013</span>';
394
367
  }
395
368
  function renderKan(note, ctx) {
396
369
  if (!note.kan) return "";
@@ -412,26 +385,24 @@ function renderPitchedInner(note, opts, ctx) {
412
385
  lang: ctx.lang
413
386
  });
414
387
  inner += `<span class="${["sw-cl", ...c.classes].join(" ")}">${c.text}</span>`;
415
- if (c.matraSeparate) inner += isoMatra(ctx);
416
388
  } else if (note.rest === true) {
417
- inner += restGlyph(ctx);
389
+ inner += matraGlyph(ctx);
418
390
  } else if (note.sustain === true && opts.withMatra) {
419
- inner += isoMatra(ctx);
391
+ inner += matraGlyph(ctx);
420
392
  }
421
393
  if (note.microtone !== void 0) {
422
- const mark = note.microtone === "atikomal" ? "\u21CA" : "\u2193";
394
+ const mark = localizeDigits(note.microtone === "atikomal" ? "2" : "1", ctx.lang);
423
395
  inner += `<span class="sw-micro" title="${note.microtone}">${mark}</span>`;
424
396
  }
425
397
  const d = note.durationMatra ?? 1;
426
- if (isIntegral(d) && d > 1 + EPS) inner += isoMatra(ctx).repeat(Math.round(d) - 1);
398
+ if (isIntegral(d) && d > 1 + EPS) inner += matraGlyph(ctx).repeat(Math.round(d) - 1);
427
399
  if (opts.cuts > 0) inner += `<span class="sw-cut">${NBSP}${ctx.marks.cut}</span>`.repeat(opts.cuts);
428
400
  inner += bracketGlyphs(note, "close");
429
401
  return inner;
430
402
  }
431
- var BAR_STROKES = { dari: 1, single: 1, double: 2, quad: 4 };
403
+ var BAR_GLYPHS = { dari: "A", single: "l", double: "L", quad: "LL" };
432
404
  function renderBarInner(note) {
433
- const strokes = "<i></i>".repeat(BAR_STROKES[note.bar]);
434
- return `${bracketGlyphs(note, "open")}${strokes}${bracketGlyphs(note, "close")}`;
405
+ return `${bracketGlyphs(note, "open")}${BAR_GLYPHS[note.bar]}${bracketGlyphs(note, "close")}`;
435
406
  }
436
407
  function renderAnnotation(notes, ctx) {
437
408
  const parts = [];
@@ -510,7 +481,7 @@ function renderRow(row, partIdx, rowIdx, ctx) {
510
481
  }
511
482
  if (note.lyric !== void 0) {
512
483
  lyric.push(
513
- `<span class="sw-lyr" style="grid-column:${col}">${escapeHtml(lyricText(note.lyric, ctx))}</span>`
484
+ `<span class="sw-lyr${j === 0 ? " sw-cs" : ""}" style="grid-column:${col}">${escapeHtml(lyricText(note.lyric, ctx))}</span>`
514
485
  );
515
486
  }
516
487
  if (note.above !== void 0 && note.above.length > 0) {
@@ -535,21 +506,24 @@ function renderRow(row, partIdx, rowIdx, ctx) {
535
506
  return `<div class="sw-row">${label}<div class="sw-grid">${above.join("")}${notation.join("")}${below.join("")}${lyric.join("")}</div></div>`;
536
507
  }
537
508
  function renderHeader(meta, ctx) {
538
- const L = LABELS[ctx.lang];
509
+ return renderMetaHeader(meta, ctx.lang, ctx.tr);
510
+ }
511
+ function renderMetaHeader(meta, lang, tr = (s) => s) {
512
+ const L = LABELS[lang];
539
513
  const rows = [];
540
514
  const add = (label, value) => rows.push(
541
515
  `<div class="sw-meta-row"><span class="sw-meta-label">${escapeHtml(label)}</span><span class="sw-meta-value">${escapeHtml(value)}</span></div>`
542
516
  );
543
- add(L.title, ctx.tr(meta.title));
517
+ add(L.title, tr(meta.title));
544
518
  const taal = meta.taalName ?? meta.taal?.id;
545
- if (taal !== void 0) add(L.taal, ctx.tr(taal));
546
- if (meta.raag !== void 0) add(L.raag, ctx.tr(meta.raag));
547
- if (meta.avartan !== void 0) add(L.avartan, localizeDigits(String(meta.avartan), ctx.lang));
548
- if (meta.notationBy !== void 0) add(L.notationBy, ctx.tr(meta.notationBy));
519
+ if (taal !== void 0) add(L.taal, tr(taal));
520
+ if (meta.raag !== void 0) add(L.raag, tr(meta.raag));
521
+ if (meta.avartan !== void 0) add(L.avartan, localizeDigits(String(meta.avartan), lang));
522
+ if (meta.notationBy !== void 0) add(L.notationBy, tr(meta.notationBy));
549
523
  return `<header class="sw-header">${rows.join("")}</header>`;
550
524
  }
551
525
  function renderToHtml(doc, opts = {}) {
552
- const lang = opts.lang ?? "bn";
526
+ const lang = opts.lang === "hi" || opts.lang === "en" ? opts.lang : "bn";
553
527
  const tr = lang !== "bn" && opts.transliterate ? opts.transliterate : (s) => s;
554
528
  const ctx = { lang, marks: MARKS[lang], tr };
555
529
  const out = [`<div class="sw-sheet" lang="${lang}">`];
@@ -566,13 +540,29 @@ function renderToHtml(doc, opts = {}) {
566
540
  }
567
541
 
568
542
  // src/viewer/styles.ts
543
+ var NOTATION_FONT_URLS = {
544
+ Swarabitan: "https://webfonts-4qq.pages.dev/swarabitan/Swarabitan.ttf",
545
+ ome_bhatkhande_hindi: "https://webfonts-4qq.pages.dev/ome-bhatkhande/OmeBhatkhandeHindi.ttf",
546
+ ome_bhatkhande_english: "https://webfonts-4qq.pages.dev/ome-bhatkhande/OmeBhatkhandeEnglish.ttf"
547
+ };
548
+ NOTATION_FONT_URLS.Swarabitan;
549
+ var FONT_FACES = Object.entries(NOTATION_FONT_URLS).map(
550
+ ([family, url]) => `@font-face { font-family: ${family}; src: url('${url}') format('truetype'); font-display: block; }`
551
+ ).join("\n");
569
552
  var SWARALIPI_CSS = `
553
+ ${FONT_FACES}
554
+
570
555
  .sw-sheet {
571
556
  font-family: var(--sw-font, 'Noto Sans Bengali', 'Kohinoor Bangla', 'Bangla Sangam MN', 'Nirmala UI', 'Noto Sans Devanagari', sans-serif);
572
557
  font-size: var(--sw-size, 1.125rem);
573
558
  line-height: 1.5;
574
559
  }
575
560
 
561
+ /* ---- notation font, per language (the note clusters + grace notes) ---- */
562
+ .sw-sheet[lang="bn"] .sw-cl, .sw-sheet[lang="bn"] .sw-kan { font-family: Swarabitan, var(--sw-font, sans-serif); }
563
+ .sw-sheet[lang="hi"] .sw-cl, .sw-sheet[lang="hi"] .sw-kan { font-family: ome_bhatkhande_hindi, var(--sw-font, sans-serif); }
564
+ .sw-sheet[lang="en"] .sw-cl, .sw-sheet[lang="en"] .sw-kan { font-family: ome_bhatkhande_english, var(--sw-font, sans-serif); }
565
+
576
566
  /* ---- header ---- */
577
567
  .sw-header {
578
568
  margin: 0 0 0.75em;
@@ -601,10 +591,10 @@ var SWARALIPI_CSS = `
601
591
  align-items: end;
602
592
  }
603
593
  .sw-above { grid-row: 1; font-size: 0.62em; justify-self: start; white-space: nowrap; padding-left: 0.2em; }
604
- .sw-n { grid-row: 2; white-space: nowrap; }
594
+ .sw-n { grid-row: 2; justify-self: center; white-space: nowrap; }
605
595
  .sw-arc,
606
596
  .sw-below { grid-row: 3; }
607
- .sw-lyr { grid-row: 4; white-space: nowrap; padding-top: 0.3em; }
597
+ .sw-lyr { grid-row: 4; justify-self: center; white-space: nowrap; padding-top: 0.3em; }
608
598
  .sw-below { font-size: 0.62em; justify-self: start; white-space: nowrap; padding-left: 0.2em; }
609
599
 
610
600
  /* inter-cell gap as padding on each cell's first notation item, so arcs
@@ -614,71 +604,21 @@ var SWARALIPI_CSS = `
614
604
  /* ---- notation clusters & marks ---- */
615
605
  .sw-cl { position: relative; display: inline-block; }
616
606
 
617
- /* mandra: hasanta-shaped understroke (a real \u09CD before \u09BE is invalid Unicode) */
618
- .sw-mandra::after {
619
- content: '';
620
- position: absolute;
621
- left: 0.1em;
622
- bottom: -0.08em;
623
- width: 0.45em;
624
- height: 0.09em;
625
- background: currentColor;
626
- transform: rotate(-32deg);
627
- }
628
- /* ati-mandra: an extra dot below */
607
+ /* bn ati-octave: Swarabitan's f/h give a single reph/hasanta, so ati adds
608
+ * one extra dot above (ati-taar) / below (ati-mandra). bn-only \u2014 hi/en get
609
+ * the two-dot glyph natively from the U/L octave suffix. */
610
+ .sw-ati-taar::before,
629
611
  .sw-ati-mandra::before {
630
612
  content: '';
631
613
  position: absolute;
632
614
  left: 0.25em;
633
- bottom: -0.35em;
634
615
  width: 0.16em;
635
616
  height: 0.16em;
636
617
  border-radius: 50%;
637
618
  background: currentColor;
638
619
  }
639
- /* ati-taar (reph already in the text): an extra dot above */
640
- .sw-ati-taar::before {
641
- content: '';
642
- position: absolute;
643
- left: 0.25em;
644
- top: -0.3em;
645
- width: 0.16em;
646
- height: 0.16em;
647
- border-radius: 50%;
648
- background: currentColor;
649
- }
650
- /* CSS reph fallback for vowel letters \u098B/\u090B that reject a real reph */
651
- .sw-reph-css::before {
652
- content: '';
653
- position: absolute;
654
- top: -0.12em;
655
- left: 0.12em;
656
- width: 0.3em;
657
- height: 0.3em;
658
- border: 0.09em solid currentColor;
659
- border-right: none;
660
- border-bottom: none;
661
- border-top-left-radius: 0.3em;
662
- }
663
- /* en octave dots (Bhatkhande-style) */
664
- .sw-dot-above::before,
665
- .sw-dot-below::before {
666
- content: '';
667
- position: absolute;
668
- left: 50%;
669
- margin-left: -0.07em;
670
- width: 0.14em;
671
- height: 0.14em;
672
- border-radius: 50%;
673
- background: currentColor;
674
- }
675
- .sw-dot-above::before { top: -0.25em; }
676
- .sw-dot-below::before { bottom: -0.2em; }
677
- .sw-ati-taar.sw-dot-above::before,
678
- .sw-ati-mandra.sw-dot-below::before { box-shadow: 0.24em 0 0 currentColor; margin-left: -0.19em; }
679
- /* en accidentals (bn/hi use distinct letters instead) */
680
- .sw-komal { text-decoration: underline; text-underline-offset: 0.18em; }
681
- .sw-tivra::after { content: '\\2032'; font-size: 0.7em; vertical-align: super; }
620
+ .sw-ati-taar::before { top: -0.3em; }
621
+ .sw-ati-mandra::before { bottom: -0.35em; }
682
622
 
683
623
  /* kan grace: small raised cluster left of the main note */
684
624
  .sw-kan {
@@ -695,29 +635,25 @@ var SWARALIPI_CSS = `
695
635
  .sw-br { opacity: 0.8; padding: 0 0.06em; }
696
636
  .sw-micro { font-size: 0.6em; vertical-align: super; opacity: 0.8; }
697
637
 
698
- /* raw fallback: never dropped, visibly non-notation */
638
+ /* raw fallback: never dropped, visibly non-notation. Swarabitan first \u2014
639
+ * raw IS that font's encoding, so undecoded cells show NLTR's own glyphs
640
+ * (dimmed: semantics unverified); monospace ASCII when the font is out. */
699
641
  .sw-raw {
700
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
642
+ font-family: Swarabitan, ui-monospace, SFMono-Regular, Menlo, monospace;
701
643
  font-size: 0.72em;
702
644
  opacity: 0.55;
703
645
  }
704
646
 
705
- /* ---- bars: dari (thin) and danda strokes, notation-row height ---- */
647
+ /* ---- bars: dari (A) and danda (l/L) Swarabitan glyphs ---- */
706
648
  /* horizontal gap before a bar comes from its own .sw-cs padding-left \u2014
707
649
  * never reset it with a padding shorthand here */
708
650
  .sw-bar {
709
651
  grid-row: 2;
710
- align-self: stretch;
711
- display: inline-flex;
712
- align-items: stretch;
713
- gap: 0.16em;
652
+ font-family: Swarabitan, var(--sw-font, sans-serif);
653
+ white-space: nowrap;
714
654
  padding-right: 0.1em;
715
655
  }
716
- .sw-bar i { display: inline-block; width: 0.14em; min-height: 1.25em; background: currentColor; }
717
- .sw-bar--dari i { width: 0.05em; opacity: 0.9; }
718
- .sw-bar--quad i:nth-child(3) { margin-left: 0.18em; }
719
- .sw-above .sw-bar, .sw-below .sw-bar { display: inline-flex; vertical-align: baseline; }
720
- .sw-above .sw-bar i, .sw-below .sw-bar i { min-height: 1em; }
656
+ .sw-bar--quad { letter-spacing: 0.15em; }
721
657
 
722
658
  /* ---- under-arcs (slurs decoded from w/x segments and meend) ---- */
723
659
  .sw-arc {
@@ -757,6 +693,9 @@ var SWARALIPI_CSS = `
757
693
 
758
694
  /* ---- player sync (P2.3) ---- */
759
695
  .sw-hl { background: var(--sw-hl, rgba(255, 196, 64, 0.45)); border-radius: 0.15em; }
696
+ /* Staff notes are SVG <g> \u2014 background doesn't paint there, so recolour the
697
+ glyph fill/stroke instead (abcjs draws note heads/stems as filled paths). */
698
+ .sw-staff .sw-hl, .sw-staff .sw-hl * { fill: var(--sw-hl-note, #d68400); stroke: var(--sw-hl-note, #d68400); }
760
699
 
761
700
  /* ---- element error box ---- */
762
701
  .sw-error {
@@ -772,27 +711,500 @@ var SWARALIPI_CSS = `
772
711
  var SWARALIPI_PRINT_CSS = `
773
712
  @page { margin: 16mm; }
774
713
  @media print {
775
- .sw-sheet { -webkit-print-color-adjust: exact; print-color-adjust: exact; color: #000; }
714
+ .sw-sheet, .sw-staff { -webkit-print-color-adjust: exact; print-color-adjust: exact; color: #000; }
776
715
  .sw-row { overflow: visible !important; break-inside: avoid; page-break-inside: avoid; }
777
716
  .sw-part { break-inside: auto; }
778
717
  .sw-part-name, .sw-header { break-after: avoid-page; page-break-after: avoid; }
779
718
  .sw-hl { background: none !important; }
719
+ /* Keep each engraved staff system whole across a page break (the abcjs line
720
+ is a <div> wrapping the <svg>, so the break rule must sit on the div). */
721
+ .sw-staff-lines > div { break-inside: avoid; page-break-inside: avoid; }
722
+ .sw-staff-meta { break-after: avoid-page; page-break-after: avoid; }
780
723
  }
781
724
  `;
725
+ var SWARALIPI_STAFF_CSS = `
726
+ .sw-staff { color: currentColor; }
727
+ .sw-staff-meta { display: flex; gap: 1.25em; align-items: baseline; margin: 0 0 .5em; font-size: .95em; opacity: .85; }
728
+ .sw-staff-instrument { font-weight: 600; }
729
+ .sw-staff-drums { opacity: .75; }
730
+ .sw-staff-tempo { font-variant-numeric: tabular-nums; }
731
+ /* abcjs (oneSvgPerLine) wraps the music into one <div><svg> per system; stack
732
+ them as short, swaralipi-style lines. abcjs sets a fixed pixel height +
733
+ overflow:hidden on each wrapper div \u2014 neutralise it so the viewBox'd SVG can
734
+ scale to the container (and to the print page) instead of being clipped. */
735
+ .sw-staff-lines > div { margin: 0 0 .35em; height: auto !important; overflow: visible !important; }
736
+ /* Fill up to the engraved (staffwidth) size \u2014 shrink to fit a narrow container,
737
+ but never stretch past natural width (abcToSvg pins per-SVG max-width in px),
738
+ else a short last system balloons. */
739
+ .sw-staff-lines svg { display: block; width: 100%; height: auto; }
740
+ .sw-staff-missing { padding: 1em; border: 1px dashed currentColor; border-radius: 6px; opacity: .8; font-size: .9em; }
741
+ .sw-staff-title { font-size: 1.15em; font-weight: 700; }
742
+ .sw-staff-subtitle { opacity: .8; font-size: .95em; margin-top: .15em; }
743
+ `;
744
+
745
+ // src/player/pitch.ts
746
+ var OCTAVE_OFFSET = {
747
+ "ati-mandra": -2,
748
+ mandra: -1,
749
+ madhyam: 0,
750
+ taar: 1,
751
+ "ati-taar": 2
752
+ };
753
+ var SHUDDHA = {
754
+ s: 0,
755
+ r: 2,
756
+ g: 4,
757
+ m: 5,
758
+ p: 7,
759
+ d: 9,
760
+ n: 11
761
+ };
762
+ var SHARP_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
763
+ var PITCH_CLASS = {
764
+ c: 0,
765
+ "c#": 1,
766
+ db: 1,
767
+ d: 2,
768
+ "d#": 3,
769
+ eb: 3,
770
+ e: 4,
771
+ "e#": 5,
772
+ fb: 4,
773
+ f: 5,
774
+ "f#": 6,
775
+ gb: 6,
776
+ g: 7,
777
+ "g#": 8,
778
+ ab: 8,
779
+ a: 9,
780
+ "a#": 10,
781
+ bb: 10,
782
+ b: 11,
783
+ "b#": 0,
784
+ cb: 11
785
+ };
786
+ function parseTonic(tonic) {
787
+ if (!tonic) return 0;
788
+ const key = tonic.trim().toLowerCase().replace(/s/g, "#");
789
+ return PITCH_CLASS[key] ?? 0;
790
+ }
791
+ function svaraSemitone(note) {
792
+ if (note.svara === void 0) return null;
793
+ const [, letter] = note.svara;
794
+ let semi = SHUDDHA[letter];
795
+ if (note.komal === true || note.microtone !== void 0) semi -= 1;
796
+ if (note.tivra === true) semi += 1;
797
+ return semi;
798
+ }
799
+ function svaraToMidi(note, tonicPc, baseOctave = 4) {
800
+ const semi = svaraSemitone(note);
801
+ if (semi === null || note.svara === void 0) return null;
802
+ const [octave] = note.svara;
803
+ return (baseOctave + 1 + OCTAVE_OFFSET[octave]) * 12 + tonicPc + semi;
804
+ }
805
+ function kanToMidi(svara, tonicPc, baseOctave = 4) {
806
+ if (svara === void 0) return null;
807
+ const [octave, letter] = svara;
808
+ return (baseOctave + 1 + OCTAVE_OFFSET[octave]) * 12 + tonicPc + SHUDDHA[letter];
809
+ }
810
+ function midiToSampleName(midi) {
811
+ const pc = (midi % 12 + 12) % 12;
812
+ const octave = Math.floor(midi / 12) - 1;
813
+ return `${SHARP_NAMES[pc]}${octave}`;
814
+ }
815
+ function sampleFileName(midi) {
816
+ return midiToSampleName(midi).toLowerCase().replace("#", "s");
817
+ }
818
+
819
+ // src/player/schedule.ts
820
+ var isSilent = (n) => n.svara === void 0 && n.rest !== true && n.sustain !== true;
821
+ function buildSchedule(doc, opts = {}) {
822
+ const tonicPc = parseTonic(opts.tonic ?? doc.meta.tonic);
823
+ const baseOctave = opts.baseOctave ?? 4;
824
+ const events = [];
825
+ let cursor = 0;
826
+ let lastPitched = -1;
827
+ doc.parts.forEach((part, p) => {
828
+ part.rows.forEach((row, r) => {
829
+ row.notes.forEach((note, i) => {
830
+ if (note.bar !== void 0 || isSilent(note)) return;
831
+ const path = `${p}.${r}.${i}`;
832
+ const dur = note.durationMatra ?? 1;
833
+ if (note.svara !== void 0) {
834
+ events.push({
835
+ beat: cursor,
836
+ durationBeats: dur,
837
+ midi: svaraToMidi(note, tonicPc, baseOctave),
838
+ kan: kanToMidi(note.kan, tonicPc, baseOctave),
839
+ path,
840
+ kind: note.sustain === true ? "sustain" : "note"
841
+ });
842
+ lastPitched = events.length - 1;
843
+ } else if (note.sustain === true) {
844
+ if (lastPitched >= 0) events[lastPitched].durationBeats += dur;
845
+ events.push({ beat: cursor, durationBeats: dur, midi: null, kan: null, path, kind: "sustain" });
846
+ } else {
847
+ events.push({ beat: cursor, durationBeats: dur, midi: null, kan: null, path, kind: "rest" });
848
+ }
849
+ cursor += dur;
850
+ });
851
+ });
852
+ });
853
+ return { events, totalBeats: cursor };
854
+ }
855
+
856
+ // src/viewer/staff.ts
857
+ init_taals();
858
+
859
+ // src/player/instruments.ts
860
+ init_taals();
861
+ function keyMap(keys) {
862
+ const m = {};
863
+ for (const k of keys) m[k] = `${k.replace("#", "s")}.mp3`;
864
+ return m;
865
+ }
866
+ var PIANO_KEYS = [
867
+ "A0",
868
+ "C1",
869
+ "D#1",
870
+ "F#1",
871
+ "A1",
872
+ "C2",
873
+ "D#2",
874
+ "F#2",
875
+ "A2",
876
+ "C3",
877
+ "D#3",
878
+ "F#3",
879
+ "A3",
880
+ "C4",
881
+ "D#4",
882
+ "F#4",
883
+ "A4",
884
+ "C5",
885
+ "D#5",
886
+ "F#5",
887
+ "A5",
888
+ "C6",
889
+ "D#6",
890
+ "F#6",
891
+ "A6",
892
+ "C7",
893
+ "D#7",
894
+ "F#7",
895
+ "A7",
896
+ "C8"
897
+ ];
898
+ var HARMONIUM_KEYS = [
899
+ "C2",
900
+ "C#2",
901
+ "D2",
902
+ "D#2",
903
+ "E2",
904
+ "F2",
905
+ "F#2",
906
+ "G2",
907
+ "G#2",
908
+ "A2",
909
+ "A#2",
910
+ "C3",
911
+ "C#3",
912
+ "D3",
913
+ "D#3",
914
+ "E3",
915
+ "F3",
916
+ "F#3",
917
+ "G3",
918
+ "G#3",
919
+ "A3",
920
+ "A#3",
921
+ "C4",
922
+ "C#4",
923
+ "D4",
924
+ "D#4",
925
+ "E4",
926
+ "F4",
927
+ "F#4",
928
+ "G4",
929
+ "G#4",
930
+ "A4",
931
+ "A#4",
932
+ "C5",
933
+ "C#5",
934
+ "D5"
935
+ ];
936
+ var VIOLIN_KEYS = ["A3", "C4", "E4", "G4", "A4", "C5", "E5", "G5", "A5", "C6", "E6", "G6", "A6", "C7"];
937
+ var FLUTE_KEYS = ["C4", "E4", "A4", "C5", "E5", "A5", "C6", "E6", "A6", "C7"];
938
+ ({
939
+ piano: { urls: keyMap(PIANO_KEYS)},
940
+ santoor: { urls: santoorUrls()},
941
+ harmonium: { urls: keyMap(HARMONIUM_KEYS)},
942
+ violin: { urls: keyMap(VIOLIN_KEYS)},
943
+ flute: { urls: keyMap(FLUTE_KEYS)}
944
+ });
945
+ new Set(
946
+ Object.keys(TAALS)
947
+ );
948
+ Array.from({ length: 17 }, (_, i) => 60 + i * 15);
949
+ function santoorUrls() {
950
+ const urls = {};
951
+ const add = (midi) => {
952
+ const stem = sampleFileName(midi);
953
+ const name = stem.replace("s", "#").replace(/^(.)/, (c) => c.toUpperCase());
954
+ urls[name] = `${stem}.mp3`;
955
+ };
956
+ for (let midi = 60; midi <= 95; midi++) add(midi);
957
+ add(96);
958
+ return urls;
959
+ }
960
+ function drumPattern(taal) {
961
+ const n = taal.numBeats;
962
+ const sam = taal.sam ?? 0;
963
+ const khaali = new Set(taal.khaali ?? []);
964
+ const bhaags = taal.bhaags ?? [n];
965
+ const pattern = new Array(n).fill("hihat");
966
+ let pos = 0;
967
+ for (const len of bhaags) {
968
+ pattern[pos] = khaali.has(pos) ? "hihat" : "snare";
969
+ pos += len;
970
+ }
971
+ pattern[sam] = "kick";
972
+ return pattern;
973
+ }
974
+
975
+ // src/viewer/staff.ts
976
+ var ABC_SHARP = ["C", "^C", "D", "^D", "E", "F", "^F", "G", "^G", "A", "^A", "B"];
977
+ function midiToAbc(midi) {
978
+ const pc = (midi % 12 + 12) % 12;
979
+ const octave = Math.floor(midi / 12) - 1;
980
+ const token = ABC_SHARP[pc];
981
+ const acc = token[0] === "^" ? "^" : "";
982
+ const letter = acc ? token[1] : token[0];
983
+ return octave >= 5 ? acc + letter.toLowerCase() + "'".repeat(octave - 5) : acc + letter + ",".repeat(4 - octave);
984
+ }
985
+ function abcDuration(beats) {
986
+ if (Math.abs(beats - 1) < 1e-6) return "";
987
+ if (Math.abs(beats - Math.round(beats)) < 1e-6) return String(Math.round(beats));
988
+ const inv = 1 / beats;
989
+ if (Math.abs(inv - Math.round(inv)) < 1e-6) return `/${Math.round(inv)}`;
990
+ const sixteenths = Math.max(1, Math.round(beats * 4));
991
+ return `${sixteenths}/4`;
992
+ }
993
+ function abcNote(ev) {
994
+ const grace = ev.kan !== null ? `{${midiToAbc(ev.kan)}}` : "";
995
+ return `${grace}${midiToAbc(ev.midi)}${abcDuration(ev.durationBeats)}`;
996
+ }
997
+ function eventPaths(schedule) {
998
+ const paths = [];
999
+ for (const ev of schedule.events) {
1000
+ if (ev.midi !== null || ev.kind === "rest") paths.push(ev.path);
1001
+ }
1002
+ return paths;
1003
+ }
1004
+ function abcText(s) {
1005
+ return s.replace(/[\r\n]+/g, " ").trim();
1006
+ }
1007
+ function buildAbc(schedule, lyricOf, opts) {
1008
+ const beatsPerBar = opts.beatsPerBar > 0 ? opts.beatsPerBar : 4;
1009
+ const tokens = [];
1010
+ const words = [];
1011
+ let barPos = 0;
1012
+ const EPS2 = 1e-6;
1013
+ const advance = (dur) => {
1014
+ barPos += dur;
1015
+ while (barPos >= beatsPerBar - EPS2) {
1016
+ tokens.push("|");
1017
+ words.push("|");
1018
+ barPos -= beatsPerBar;
1019
+ }
1020
+ };
1021
+ for (const ev of schedule.events) {
1022
+ if (ev.midi === null) {
1023
+ if (ev.kind === "rest") {
1024
+ tokens.push(`z${abcDuration(ev.durationBeats)}`);
1025
+ words.push("*");
1026
+ advance(ev.durationBeats);
1027
+ }
1028
+ continue;
1029
+ }
1030
+ tokens.push(abcNote(ev));
1031
+ const lyr = lyricOf(ev);
1032
+ words.push(lyr ? abcText(lyr).replace(/\s+/g, "~") : "*");
1033
+ advance(ev.durationBeats);
1034
+ }
1035
+ if (tokens.length && tokens[tokens.length - 1] !== "|") tokens.push("|]");
1036
+ else if (tokens.length) tokens[tokens.length - 1] = "|]";
1037
+ const meter = ratioMeter(beatsPerBar);
1038
+ const head = ["X:1", `M:${meter}`, "L:1/4", `Q:1/4=${Math.round(opts.bpm)}`];
1039
+ if (opts.drums) head.push(...percMapLines(opts.drums));
1040
+ head.push("K:C treble");
1041
+ const melodyBody = tokens.length ? tokens.join(" ") : "z |]";
1042
+ const lines = [...head];
1043
+ if (opts.drums) {
1044
+ lines.push("V:1", melodyBody);
1045
+ if (opts.lyrics && words.some((w) => w !== "*" && w !== "|")) lines.push(`w: ${words.join(" ")}`);
1046
+ lines.push("V:2 clef=perc stem=up", buildDrumVoice(schedule.totalBeats, beatsPerBar, opts.drums));
1047
+ } else {
1048
+ lines.push(melodyBody);
1049
+ if (opts.lyrics && words.some((w) => w !== "*" && w !== "|")) lines.push(`w: ${words.join(" ")}`);
1050
+ }
1051
+ return lines.join("\n");
1052
+ }
1053
+ function ratioMeter(beats) {
1054
+ return `${Math.max(1, Math.round(beats))}/4`;
1055
+ }
1056
+ var DRUM_ABC = {
1057
+ kick: { note: "F,", gm: "bass-drum-1" },
1058
+ snare: { note: "C", gm: "acoustic-snare" },
1059
+ hihat: { note: "g", gm: "closed-hi-hat" },
1060
+ tom1: { note: "E", gm: "high-tom" },
1061
+ tom2: { note: "A,", gm: "low-mid-tom" },
1062
+ tom3: { note: "D,", gm: "low-floor-tom" }
1063
+ };
1064
+ function percMapLines(voices) {
1065
+ const seen = /* @__PURE__ */ new Set();
1066
+ const lines = [];
1067
+ for (const v of voices) {
1068
+ if (seen.has(v)) continue;
1069
+ seen.add(v);
1070
+ const { note, gm } = DRUM_ABC[v];
1071
+ lines.push(`%%percmap ${note} ${gm}`);
1072
+ }
1073
+ return lines;
1074
+ }
1075
+ function buildDrumVoice(totalBeats, beatsPerBar, pattern) {
1076
+ const n = Math.max(1, Math.ceil(totalBeats));
1077
+ const tokens = [];
1078
+ for (let beat = 0; beat < n; beat++) {
1079
+ tokens.push(DRUM_ABC[pattern[beat % pattern.length]].note);
1080
+ if ((beat + 1) % beatsPerBar === 0) tokens.push("|");
1081
+ }
1082
+ if (tokens.length === 0 || tokens[tokens.length - 1] !== "|") tokens.push("|]");
1083
+ else tokens[tokens.length - 1] = "|]";
1084
+ return tokens.join(" ");
1085
+ }
1086
+ function lyricLookup(doc) {
1087
+ return (ev) => {
1088
+ const [p, r, i] = ev.path.split(".").map(Number);
1089
+ return doc.parts[p]?.rows[r]?.notes[i]?.lyric;
1090
+ };
1091
+ }
1092
+ function resolveDrumTaal(doc) {
1093
+ const id = doc.meta.taal?.id;
1094
+ return id !== void 0 && id in TAALS ? TAALS[id] : { numBeats: 4, sam: 0 };
1095
+ }
1096
+ function docToAbc(doc, opts = {}) {
1097
+ const schedule = buildSchedule(doc, { tonic: opts.tonic, baseOctave: opts.baseOctave });
1098
+ const bpm = opts.bpm ?? doc.meta.bpm ?? 90;
1099
+ const beatsPerBar = opts.beatsPerBar ?? doc.meta.taal?.numBeats ?? 4;
1100
+ const drums = opts.drums ? drumPattern(resolveDrumTaal(doc)) : void 0;
1101
+ return buildAbc(schedule, lyricLookup(doc), { bpm, lyrics: opts.lyrics !== false, beatsPerBar, drums });
1102
+ }
1103
+ async function abcToSvg(abc, opts = {}, notePaths) {
1104
+ if (typeof document === "undefined") {
1105
+ throw new Error("abcToSvg requires a DOM; render staff notation in the browser or print iframe");
1106
+ }
1107
+ let ABCJS;
1108
+ try {
1109
+ const mod = await import('abcjs');
1110
+ ABCJS = mod.default ?? mod;
1111
+ } catch {
1112
+ return `<div class="sw-staff-missing">Staff notation needs the optional <code>abcjs</code> package.</div>`;
1113
+ }
1114
+ const host = document.createElement("div");
1115
+ host.style.cssText = "position:fixed; left:-10000px; top:0; width:720px; visibility:hidden;";
1116
+ const mount = document.body ?? document.documentElement;
1117
+ mount.appendChild(host);
1118
+ try {
1119
+ ABCJS.renderAbc(host, abc, {
1120
+ add_classes: true,
1121
+ staffwidth: opts.staffwidth ?? 680,
1122
+ scale: opts.scale ?? 1,
1123
+ // Reflow into short lines. staffwidth is the width budget;
1124
+ // preferredMeasuresPerLine keeps systems swaralipi-short.
1125
+ // (Not `responsive: resize` — its absolute-positioned aspect-ratio wrapper
1126
+ // overlaps the header and mis-sizes on the print page.)
1127
+ wrap: { minSpacing: 1.8, maxSpacing: 2.7, preferredMeasuresPerLine: opts.measuresPerLine ?? 4 },
1128
+ // CRUCIAL: put each wrapped system in its OWN <svg> (default is all systems
1129
+ // stacked in one tall SVG, which is atomic and can't page-break — that was
1130
+ // the near-blank-first-page / mid-staff-cutoff print bug). One SVG per line
1131
+ // lets `break-inside: avoid` keep each staff whole while pages fill.
1132
+ oneSvgPerLine: true
1133
+ });
1134
+ for (const svg of host.querySelectorAll("svg")) {
1135
+ const w = parseFloat(svg.getAttribute("width") ?? "");
1136
+ const h = parseFloat(svg.getAttribute("height") ?? "");
1137
+ if (w > 0 && h > 0) {
1138
+ if (!svg.getAttribute("viewBox")) svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
1139
+ svg.setAttribute("preserveAspectRatio", "xMinYMin meet");
1140
+ svg.removeAttribute("width");
1141
+ svg.removeAttribute("height");
1142
+ svg.setAttribute("style", `max-width:${w}px`);
1143
+ }
1144
+ }
1145
+ if (notePaths) {
1146
+ const melody = host.querySelectorAll(".abcjs-v0.abcjs-note, .abcjs-v0.abcjs-rest");
1147
+ let i = 0;
1148
+ for (const el of melody) {
1149
+ const path = notePaths[i++];
1150
+ if (path !== void 0) el.setAttribute("data-note", path);
1151
+ }
1152
+ }
1153
+ return host.innerHTML;
1154
+ } finally {
1155
+ host.remove();
1156
+ }
1157
+ }
1158
+ async function renderStaffHtml(doc, opts = {}) {
1159
+ const bpm = opts.bpm ?? doc.meta.bpm ?? 90;
1160
+ const abc = docToAbc(doc, { ...opts, bpm });
1161
+ const schedule = buildSchedule(doc, { tonic: opts.tonic, baseOctave: opts.baseOctave });
1162
+ const engraved = await abcToSvg(abc, opts, eventPaths(schedule));
1163
+ const lines = engraved.includes("sw-staff-missing") ? engraved : `<div class="sw-staff-lines">${engraved}</div>`;
1164
+ const header = opts.showHeader === false ? "" : renderMetaHeader(doc.meta, opts.headerLang ?? "en", opts.transliterate);
1165
+ const metaParts = [];
1166
+ if (opts.instrument) metaParts.push(`<span class="sw-staff-instrument">${escapeAttr(opts.instrument)}</span>`);
1167
+ if (opts.drums) metaParts.push(`<span class="sw-staff-drums">+ drums</span>`);
1168
+ const meta = metaParts.length ? `<div class="sw-staff-meta">${metaParts.join("")}</div>` : "";
1169
+ return `<div class="sw-staff">${header}${meta}${lines}</div>`;
1170
+ }
1171
+ function escapeAttr(s) {
1172
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1173
+ }
782
1174
 
783
1175
  // src/viewer/print.ts
1176
+ var DEFAULT_WATERMARK = "www.gitabitan.io";
1177
+ var DEFAULT_NOTATOR = "gitabitan.io Team";
784
1178
  function escapeHtml2(s) {
785
1179
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
786
1180
  }
787
- function renderPrintDocument(doc, opts = {}) {
788
- const lang = opts.lang ?? "bn";
789
- const tr = lang !== "bn" && opts.transliterate ? opts.transliterate : (s) => s;
790
- const title = opts.title ?? tr(doc.meta.title);
791
- const body = renderToHtml(doc, {
792
- lang,
793
- showHeader: opts.showHeader,
794
- transliterate: opts.transliterate
795
- });
1181
+ function watermarkCss(text) {
1182
+ if (!text) return "";
1183
+ return `
1184
+ .sw-watermark {
1185
+ position: fixed; inset: 0; z-index: 0; pointer-events: none;
1186
+ display: flex; align-items: center; justify-content: center;
1187
+ overflow: hidden;
1188
+ -webkit-print-color-adjust: exact; print-color-adjust: exact;
1189
+ }
1190
+ .sw-watermark span {
1191
+ font-size: 11vw; font-weight: 700; letter-spacing: .08em;
1192
+ color: rgba(0,0,0,0.07); transform: rotate(-35deg); white-space: nowrap;
1193
+ }
1194
+ body > .sw-sheet, body > .sw-footer { position: relative; z-index: 1; }
1195
+ `;
1196
+ }
1197
+ function chrome(text, notator) {
1198
+ const top = text ? `<div class="sw-watermark" aria-hidden="true"><span>${escapeHtml2(text)}</span></div>` : "";
1199
+ const bottom = notator ? `<footer class="sw-footer">${escapeHtml2(notator)}</footer>` : "";
1200
+ return { top, bottom };
1201
+ }
1202
+ function resolveNotator(doc, opts) {
1203
+ return doc.meta.notationBy ?? opts.notator ?? DEFAULT_NOTATOR;
1204
+ }
1205
+ function pageHtml(lang, title, bodyHtml, opts, doc) {
1206
+ const watermark = opts.watermark ?? DEFAULT_WATERMARK;
1207
+ const { top, bottom } = chrome(watermark, resolveNotator(doc, opts));
796
1208
  return `<!doctype html>
797
1209
  <html lang="${escapeHtml2(lang)}">
798
1210
  <head>
@@ -801,20 +1213,42 @@ function renderPrintDocument(doc, opts = {}) {
801
1213
  <title>${escapeHtml2(title)}</title>
802
1214
  <style>
803
1215
  ${SWARALIPI_CSS}
1216
+ ${SWARALIPI_STAFF_CSS}
804
1217
  ${SWARALIPI_PRINT_CSS}
1218
+ ${watermarkCss(watermark)}
805
1219
  html, body { margin: 0; }
806
1220
  body { color: #111; background: #fff; padding: 16px; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
807
1221
  .sw-sheet { max-width: 100%; }
1222
+ .sw-footer { margin-top: 12px; padding-top: 6px; border-top: 1px solid #ddd; font-size: 12px; color: #555; text-align: right; }
808
1223
  </style>
809
1224
  </head>
810
- <body>${body}</body>
1225
+ <body>${top}${bodyHtml}${bottom}</body>
811
1226
  </html>`;
812
1227
  }
1228
+ function renderPrintDocument(doc, opts = {}) {
1229
+ const lang = opts.lang === "staff" ? "bn" : opts.lang ?? "bn";
1230
+ const tr = lang !== "bn" && opts.transliterate ? opts.transliterate : (s) => s;
1231
+ const title = opts.title ?? tr(doc.meta.title);
1232
+ const body = renderToHtml(doc, { lang, showHeader: opts.showHeader, transliterate: opts.transliterate });
1233
+ return pageHtml(lang, title, body, opts, doc);
1234
+ }
1235
+ async function renderStaffPrintDocument(doc, opts = {}) {
1236
+ if (opts.lang !== "staff") return renderPrintDocument(doc, opts);
1237
+ const title = opts.title ?? doc.meta.title;
1238
+ const staff = await renderStaffHtml(doc, opts);
1239
+ return pageHtml("en", title, staff, opts, doc);
1240
+ }
813
1241
  function printSwaralipi(doc, opts = {}) {
814
1242
  if (typeof document === "undefined") {
815
1243
  throw new Error("printSwaralipi requires a DOM; use renderPrintDocument() for server-side PDF");
816
1244
  }
817
- const html = renderPrintDocument(doc, opts);
1245
+ if (opts.lang === "staff") {
1246
+ void renderStaffPrintDocument(doc, opts).then(printHtmlInIframe);
1247
+ } else {
1248
+ printHtmlInIframe(renderPrintDocument(doc, opts));
1249
+ }
1250
+ }
1251
+ function printHtmlInIframe(html) {
818
1252
  const iframe = document.createElement("iframe");
819
1253
  iframe.setAttribute("aria-hidden", "true");
820
1254
  iframe.style.cssText = "position:fixed; left:-10000px; top:0; width:800px; height:600px; border:0;";
@@ -826,7 +1260,10 @@ function printSwaralipi(doc, opts = {}) {
826
1260
  }
827
1261
  const cleanup = () => setTimeout(() => iframe.remove(), 1e3);
828
1262
  win.addEventListener("afterprint", cleanup, { once: true });
1263
+ let printed = false;
829
1264
  const go = () => {
1265
+ if (printed) return;
1266
+ printed = true;
830
1267
  try {
831
1268
  win.focus();
832
1269
  win.print();
@@ -834,8 +1271,14 @@ function printSwaralipi(doc, opts = {}) {
834
1271
  }
835
1272
  };
836
1273
  const fonts = iframe.contentDocument?.fonts;
837
- if (fonts?.ready) void fonts.ready.then(go).catch(go);
838
- else setTimeout(go, 250);
1274
+ if (fonts && typeof fonts.forEach === "function") {
1275
+ const loads = [Promise.resolve(fonts.ready)];
1276
+ fonts.forEach((face) => loads.push(face.load().catch(() => void 0)));
1277
+ void Promise.all(loads).then(go);
1278
+ setTimeout(go, 3e3);
1279
+ } else {
1280
+ setTimeout(go, 250);
1281
+ }
839
1282
  };
840
1283
  iframe.srcdoc = html;
841
1284
  document.body.appendChild(iframe);
@@ -847,7 +1290,7 @@ var TARGET = {
847
1290
  en: "iast"
848
1291
  };
849
1292
  async function loadLyricTransliterator(lang) {
850
- if (lang === "bn") return void 0;
1293
+ if (lang === "bn" || lang === "staff") return void 0;
851
1294
  const mod = await import('@indic-transliteration/sanscript');
852
1295
  const sanscript = mod.default ?? mod;
853
1296
  const target = TARGET[lang];
@@ -855,7 +1298,16 @@ async function loadLyricTransliterator(lang) {
855
1298
  }
856
1299
 
857
1300
  // src/viewer/element.ts
858
- var LANGS = /* @__PURE__ */ new Set(["bn", "hi", "en"]);
1301
+ var LANGS = /* @__PURE__ */ new Set(["bn", "hi", "en", "staff"]);
1302
+ function ensureNotationFonts() {
1303
+ if (typeof FontFace === "undefined" || typeof document === "undefined") return;
1304
+ const present = /* @__PURE__ */ new Set();
1305
+ for (const f of document.fonts) present.add(f.family.replace(/['"]/g, ""));
1306
+ for (const [family, url] of Object.entries(NOTATION_FONT_URLS)) {
1307
+ if (present.has(family)) continue;
1308
+ document.fonts.add(new FontFace(family, `url(${url})`, { display: "block" }));
1309
+ }
1310
+ }
859
1311
  var sharedSheet;
860
1312
  function createElementClass() {
861
1313
  return class SwaralipiSheet extends HTMLElement {
@@ -866,6 +1318,8 @@ function createElementClass() {
866
1318
  #abort;
867
1319
  #translit;
868
1320
  #translitLang;
1321
+ #renderSeq = 0;
1322
+ #staffOptions = {};
869
1323
  validate = false;
870
1324
  constructor() {
871
1325
  super();
@@ -873,7 +1327,7 @@ function createElementClass() {
873
1327
  if (typeof CSSStyleSheet !== "undefined" && "replaceSync" in CSSStyleSheet.prototype && "adoptedStyleSheets" in this.#root) {
874
1328
  if (!sharedSheet) {
875
1329
  sharedSheet = new CSSStyleSheet();
876
- sharedSheet.replaceSync(SWARALIPI_CSS);
1330
+ sharedSheet.replaceSync(SWARALIPI_CSS + SWARALIPI_STAFF_CSS);
877
1331
  }
878
1332
  this.#root.adoptedStyleSheets = [...this.#root.adoptedStyleSheets, sharedSheet];
879
1333
  this.#adopted = true;
@@ -892,6 +1346,13 @@ function createElementClass() {
892
1346
  }
893
1347
  this.#render();
894
1348
  }
1349
+ get staffOptions() {
1350
+ return this.#staffOptions;
1351
+ }
1352
+ set staffOptions(value) {
1353
+ this.#staffOptions = value;
1354
+ if (this.#lang === "staff") this.#render();
1355
+ }
895
1356
  get #lang() {
896
1357
  const attr = this.getAttribute("lang");
897
1358
  return attr !== null && LANGS.has(attr) ? attr : "bn";
@@ -938,14 +1399,22 @@ function createElementClass() {
938
1399
  }).catch(() => void 0);
939
1400
  }
940
1401
  #styleFallback() {
941
- return this.#adopted ? "" : `<style>${SWARALIPI_CSS}</style>`;
1402
+ return this.#adopted ? "" : `<style>${SWARALIPI_CSS}${SWARALIPI_STAFF_CSS}</style>`;
942
1403
  }
943
1404
  #render() {
1405
+ const seq = ++this.#renderSeq;
944
1406
  if (this.#doc === void 0) {
945
1407
  this.#root.innerHTML = this.#styleFallback();
946
1408
  return;
947
1409
  }
948
1410
  const lang = this.#lang;
1411
+ if (lang === "staff") {
1412
+ const doc = this.#doc;
1413
+ void renderStaffHtml(doc, this.staffOptions).then((html2) => {
1414
+ if (seq === this.#renderSeq) this.#root.innerHTML = this.#styleFallback() + html2;
1415
+ }).catch(() => void 0);
1416
+ return;
1417
+ }
949
1418
  const html = renderToHtml(this.#doc, {
950
1419
  lang,
951
1420
  showHeader: !this.hasAttribute("hide-header"),
@@ -976,6 +1445,7 @@ function createElementClass() {
976
1445
  lang,
977
1446
  showHeader: !this.hasAttribute("hide-header"),
978
1447
  transliterate: this.#translitLang === lang ? this.#translit : void 0,
1448
+ ...lang === "staff" ? this.staffOptions : {},
979
1449
  ...options
980
1450
  });
981
1451
  }
@@ -988,25 +1458,37 @@ function defineSwaralipiSheet(tagName = "swaralipi-sheet") {
988
1458
  }
989
1459
  const existing = customElements.get(tagName);
990
1460
  if (existing) return existing;
1461
+ ensureNotationFonts();
991
1462
  const ctor = registered.get(tagName) ?? createElementClass();
992
1463
  registered.set(tagName, ctor);
993
1464
  customElements.define(tagName, ctor);
994
1465
  return ctor;
995
1466
  }
996
1467
 
1468
+ exports.DEFAULT_NOTATOR = DEFAULT_NOTATOR;
1469
+ exports.DEFAULT_WATERMARK = DEFAULT_WATERMARK;
997
1470
  exports.DIGITS = DIGITS;
998
1471
  exports.LABELS = LABELS;
999
- exports.LETTERS = LETTERS;
1000
1472
  exports.MARKS = MARKS;
1473
+ exports.NOTATION_FONT_URLS = NOTATION_FONT_URLS;
1001
1474
  exports.SWARALIPI_CSS = SWARALIPI_CSS;
1002
1475
  exports.SWARALIPI_PRINT_CSS = SWARALIPI_PRINT_CSS;
1476
+ exports.SWARALIPI_STAFF_CSS = SWARALIPI_STAFF_CSS;
1477
+ exports.abcDuration = abcDuration;
1478
+ exports.abcToSvg = abcToSvg;
1479
+ exports.buildAbc = buildAbc;
1003
1480
  exports.composeCluster = composeCluster;
1004
1481
  exports.defineSwaralipiSheet = defineSwaralipiSheet;
1482
+ exports.docToAbc = docToAbc;
1005
1483
  exports.groupRowIntoCells = groupRowIntoCells;
1006
1484
  exports.loadLyricTransliterator = loadLyricTransliterator;
1007
1485
  exports.localizeDigits = localizeDigits;
1486
+ exports.midiToAbc = midiToAbc;
1008
1487
  exports.printSwaralipi = printSwaralipi;
1488
+ exports.renderMetaHeader = renderMetaHeader;
1009
1489
  exports.renderPrintDocument = renderPrintDocument;
1490
+ exports.renderStaffHtml = renderStaffHtml;
1491
+ exports.renderStaffPrintDocument = renderStaffPrintDocument;
1010
1492
  exports.renderToHtml = renderToHtml;
1011
1493
  exports.resolveArcs = resolveArcs;
1012
1494
  //# sourceMappingURL=index.cjs.map