@ugfoundation/swaralipi-js 0.2.1 → 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-SCSIUDB3.js → batch-QPSHVNDS.js} +4 -4
  2. package/dist/{batch-SCSIUDB3.js.map → batch-QPSHVNDS.js.map} +1 -1
  3. package/dist/{chunk-DINRYGRI.js → chunk-4YTOLN3C.js} +3 -3
  4. package/dist/{chunk-DINRYGRI.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-KYNCQ7OF.js → chunk-VCRWS3F3.js} +3 -3
  8. package/dist/{chunk-KYNCQ7OF.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 +584 -54
  32. package/dist/viewer/index.cjs.map +1 -1
  33. package/dist/viewer/index.d.cts +177 -18
  34. package/dist/viewer/index.d.ts +177 -18
  35. package/dist/viewer/index.js +322 -30
  36. package/dist/viewer/index.js.map +1 -1
  37. package/package.json +7 -2
@@ -1,3 +1,6 @@
1
+ import { buildSchedule, drumPattern } from '../chunk-V5ZSUJCA.js';
2
+ import { TAALS } from '../chunk-H6HC44PZ.js';
3
+
1
4
  // src/viewer/glyphs.ts
2
5
  var SWARABITAN_SHUDDHA = {
3
6
  s: "s",
@@ -36,9 +39,9 @@ var MARKS = {
36
39
  en: { hyphen: "-", melisma: "\xB7", cut: ":" }
37
40
  };
38
41
  var LABELS = {
39
- 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" },
40
- 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" },
41
- en: { title: "Song", taal: "Taal", raag: "Raag", avartan: "Avartan", notationBy: "Notation" }
42
+ 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" },
43
+ 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" },
44
+ en: { title: "Song", taal: "Taal", raag: "Raag", avartan: "Avartan", notationBy: "Notator" }
42
45
  };
43
46
  function composeCluster(letter, opts) {
44
47
  const classes = [];
@@ -321,21 +324,24 @@ function renderRow(row, partIdx, rowIdx, ctx) {
321
324
  return `<div class="sw-row">${label}<div class="sw-grid">${above.join("")}${notation.join("")}${below.join("")}${lyric.join("")}</div></div>`;
322
325
  }
323
326
  function renderHeader(meta, ctx) {
324
- const L = LABELS[ctx.lang];
327
+ return renderMetaHeader(meta, ctx.lang, ctx.tr);
328
+ }
329
+ function renderMetaHeader(meta, lang, tr = (s) => s) {
330
+ const L = LABELS[lang];
325
331
  const rows = [];
326
332
  const add = (label, value) => rows.push(
327
333
  `<div class="sw-meta-row"><span class="sw-meta-label">${escapeHtml(label)}</span><span class="sw-meta-value">${escapeHtml(value)}</span></div>`
328
334
  );
329
- add(L.title, ctx.tr(meta.title));
335
+ add(L.title, tr(meta.title));
330
336
  const taal = meta.taalName ?? meta.taal?.id;
331
- if (taal !== void 0) add(L.taal, ctx.tr(taal));
332
- if (meta.raag !== void 0) add(L.raag, ctx.tr(meta.raag));
333
- if (meta.avartan !== void 0) add(L.avartan, localizeDigits(String(meta.avartan), ctx.lang));
334
- if (meta.notationBy !== void 0) add(L.notationBy, ctx.tr(meta.notationBy));
337
+ if (taal !== void 0) add(L.taal, tr(taal));
338
+ if (meta.raag !== void 0) add(L.raag, tr(meta.raag));
339
+ if (meta.avartan !== void 0) add(L.avartan, localizeDigits(String(meta.avartan), lang));
340
+ if (meta.notationBy !== void 0) add(L.notationBy, tr(meta.notationBy));
335
341
  return `<header class="sw-header">${rows.join("")}</header>`;
336
342
  }
337
343
  function renderToHtml(doc, opts = {}) {
338
- const lang = opts.lang ?? "bn";
344
+ const lang = opts.lang === "hi" || opts.lang === "en" ? opts.lang : "bn";
339
345
  const tr = lang !== "bn" && opts.transliterate ? opts.transliterate : (s) => s;
340
346
  const ctx = { lang, marks: MARKS[lang], tr };
341
347
  const out = [`<div class="sw-sheet" lang="${lang}">`];
@@ -353,9 +359,9 @@ function renderToHtml(doc, opts = {}) {
353
359
 
354
360
  // src/viewer/styles.ts
355
361
  var NOTATION_FONT_URLS = {
356
- Swarabitan: "https://cdn.daserve.in/webfonts/swarabitan/Swarabitan.ttf",
357
- ome_bhatkhande_hindi: "https://cdn.daserve.in/webfonts/ome-bhatkhande/OmeBhatkhandeHindi.ttf",
358
- ome_bhatkhande_english: "https://cdn.daserve.in/webfonts/ome-bhatkhande/OmeBhatkhandeEnglish.ttf"
362
+ Swarabitan: "https://webfonts-4qq.pages.dev/swarabitan/Swarabitan.ttf",
363
+ ome_bhatkhande_hindi: "https://webfonts-4qq.pages.dev/ome-bhatkhande/OmeBhatkhandeHindi.ttf",
364
+ ome_bhatkhande_english: "https://webfonts-4qq.pages.dev/ome-bhatkhande/OmeBhatkhandeEnglish.ttf"
359
365
  };
360
366
  NOTATION_FONT_URLS.Swarabitan;
361
367
  var FONT_FACES = Object.entries(NOTATION_FONT_URLS).map(
@@ -505,6 +511,9 @@ ${FONT_FACES}
505
511
 
506
512
  /* ---- player sync (P2.3) ---- */
507
513
  .sw-hl { background: var(--sw-hl, rgba(255, 196, 64, 0.45)); border-radius: 0.15em; }
514
+ /* Staff notes are SVG <g> \u2014 background doesn't paint there, so recolour the
515
+ glyph fill/stroke instead (abcjs draws note heads/stems as filled paths). */
516
+ .sw-staff .sw-hl, .sw-staff .sw-hl * { fill: var(--sw-hl-note, #d68400); stroke: var(--sw-hl-note, #d68400); }
508
517
 
509
518
  /* ---- element error box ---- */
510
519
  .sw-error {
@@ -520,27 +529,270 @@ ${FONT_FACES}
520
529
  var SWARALIPI_PRINT_CSS = `
521
530
  @page { margin: 16mm; }
522
531
  @media print {
523
- .sw-sheet { -webkit-print-color-adjust: exact; print-color-adjust: exact; color: #000; }
532
+ .sw-sheet, .sw-staff { -webkit-print-color-adjust: exact; print-color-adjust: exact; color: #000; }
524
533
  .sw-row { overflow: visible !important; break-inside: avoid; page-break-inside: avoid; }
525
534
  .sw-part { break-inside: auto; }
526
535
  .sw-part-name, .sw-header { break-after: avoid-page; page-break-after: avoid; }
527
536
  .sw-hl { background: none !important; }
537
+ /* Keep each engraved staff system whole across a page break (the abcjs line
538
+ is a <div> wrapping the <svg>, so the break rule must sit on the div). */
539
+ .sw-staff-lines > div { break-inside: avoid; page-break-inside: avoid; }
540
+ .sw-staff-meta { break-after: avoid-page; page-break-after: avoid; }
528
541
  }
529
542
  `;
543
+ var SWARALIPI_STAFF_CSS = `
544
+ .sw-staff { color: currentColor; }
545
+ .sw-staff-meta { display: flex; gap: 1.25em; align-items: baseline; margin: 0 0 .5em; font-size: .95em; opacity: .85; }
546
+ .sw-staff-instrument { font-weight: 600; }
547
+ .sw-staff-drums { opacity: .75; }
548
+ .sw-staff-tempo { font-variant-numeric: tabular-nums; }
549
+ /* abcjs (oneSvgPerLine) wraps the music into one <div><svg> per system; stack
550
+ them as short, swaralipi-style lines. abcjs sets a fixed pixel height +
551
+ overflow:hidden on each wrapper div \u2014 neutralise it so the viewBox'd SVG can
552
+ scale to the container (and to the print page) instead of being clipped. */
553
+ .sw-staff-lines > div { margin: 0 0 .35em; height: auto !important; overflow: visible !important; }
554
+ /* Fill up to the engraved (staffwidth) size \u2014 shrink to fit a narrow container,
555
+ but never stretch past natural width (abcToSvg pins per-SVG max-width in px),
556
+ else a short last system balloons. */
557
+ .sw-staff-lines svg { display: block; width: 100%; height: auto; }
558
+ .sw-staff-missing { padding: 1em; border: 1px dashed currentColor; border-radius: 6px; opacity: .8; font-size: .9em; }
559
+ .sw-staff-title { font-size: 1.15em; font-weight: 700; }
560
+ .sw-staff-subtitle { opacity: .8; font-size: .95em; margin-top: .15em; }
561
+ `;
562
+
563
+ // src/viewer/staff.ts
564
+ var ABC_SHARP = ["C", "^C", "D", "^D", "E", "F", "^F", "G", "^G", "A", "^A", "B"];
565
+ function midiToAbc(midi) {
566
+ const pc = (midi % 12 + 12) % 12;
567
+ const octave = Math.floor(midi / 12) - 1;
568
+ const token = ABC_SHARP[pc];
569
+ const acc = token[0] === "^" ? "^" : "";
570
+ const letter = acc ? token[1] : token[0];
571
+ return octave >= 5 ? acc + letter.toLowerCase() + "'".repeat(octave - 5) : acc + letter + ",".repeat(4 - octave);
572
+ }
573
+ function abcDuration(beats) {
574
+ if (Math.abs(beats - 1) < 1e-6) return "";
575
+ if (Math.abs(beats - Math.round(beats)) < 1e-6) return String(Math.round(beats));
576
+ const inv = 1 / beats;
577
+ if (Math.abs(inv - Math.round(inv)) < 1e-6) return `/${Math.round(inv)}`;
578
+ const sixteenths = Math.max(1, Math.round(beats * 4));
579
+ return `${sixteenths}/4`;
580
+ }
581
+ function abcNote(ev) {
582
+ const grace = ev.kan !== null ? `{${midiToAbc(ev.kan)}}` : "";
583
+ return `${grace}${midiToAbc(ev.midi)}${abcDuration(ev.durationBeats)}`;
584
+ }
585
+ function eventPaths(schedule) {
586
+ const paths = [];
587
+ for (const ev of schedule.events) {
588
+ if (ev.midi !== null || ev.kind === "rest") paths.push(ev.path);
589
+ }
590
+ return paths;
591
+ }
592
+ function abcText(s) {
593
+ return s.replace(/[\r\n]+/g, " ").trim();
594
+ }
595
+ function buildAbc(schedule, lyricOf, opts) {
596
+ const beatsPerBar = opts.beatsPerBar > 0 ? opts.beatsPerBar : 4;
597
+ const tokens = [];
598
+ const words = [];
599
+ let barPos = 0;
600
+ const EPS2 = 1e-6;
601
+ const advance = (dur) => {
602
+ barPos += dur;
603
+ while (barPos >= beatsPerBar - EPS2) {
604
+ tokens.push("|");
605
+ words.push("|");
606
+ barPos -= beatsPerBar;
607
+ }
608
+ };
609
+ for (const ev of schedule.events) {
610
+ if (ev.midi === null) {
611
+ if (ev.kind === "rest") {
612
+ tokens.push(`z${abcDuration(ev.durationBeats)}`);
613
+ words.push("*");
614
+ advance(ev.durationBeats);
615
+ }
616
+ continue;
617
+ }
618
+ tokens.push(abcNote(ev));
619
+ const lyr = lyricOf(ev);
620
+ words.push(lyr ? abcText(lyr).replace(/\s+/g, "~") : "*");
621
+ advance(ev.durationBeats);
622
+ }
623
+ if (tokens.length && tokens[tokens.length - 1] !== "|") tokens.push("|]");
624
+ else if (tokens.length) tokens[tokens.length - 1] = "|]";
625
+ const meter = ratioMeter(beatsPerBar);
626
+ const head = ["X:1", `M:${meter}`, "L:1/4", `Q:1/4=${Math.round(opts.bpm)}`];
627
+ if (opts.drums) head.push(...percMapLines(opts.drums));
628
+ head.push("K:C treble");
629
+ const melodyBody = tokens.length ? tokens.join(" ") : "z |]";
630
+ const lines = [...head];
631
+ if (opts.drums) {
632
+ lines.push("V:1", melodyBody);
633
+ if (opts.lyrics && words.some((w) => w !== "*" && w !== "|")) lines.push(`w: ${words.join(" ")}`);
634
+ lines.push("V:2 clef=perc stem=up", buildDrumVoice(schedule.totalBeats, beatsPerBar, opts.drums));
635
+ } else {
636
+ lines.push(melodyBody);
637
+ if (opts.lyrics && words.some((w) => w !== "*" && w !== "|")) lines.push(`w: ${words.join(" ")}`);
638
+ }
639
+ return lines.join("\n");
640
+ }
641
+ function ratioMeter(beats) {
642
+ return `${Math.max(1, Math.round(beats))}/4`;
643
+ }
644
+ var DRUM_ABC = {
645
+ kick: { note: "F,", gm: "bass-drum-1" },
646
+ snare: { note: "C", gm: "acoustic-snare" },
647
+ hihat: { note: "g", gm: "closed-hi-hat" },
648
+ tom1: { note: "E", gm: "high-tom" },
649
+ tom2: { note: "A,", gm: "low-mid-tom" },
650
+ tom3: { note: "D,", gm: "low-floor-tom" }
651
+ };
652
+ function percMapLines(voices) {
653
+ const seen = /* @__PURE__ */ new Set();
654
+ const lines = [];
655
+ for (const v of voices) {
656
+ if (seen.has(v)) continue;
657
+ seen.add(v);
658
+ const { note, gm } = DRUM_ABC[v];
659
+ lines.push(`%%percmap ${note} ${gm}`);
660
+ }
661
+ return lines;
662
+ }
663
+ function buildDrumVoice(totalBeats, beatsPerBar, pattern) {
664
+ const n = Math.max(1, Math.ceil(totalBeats));
665
+ const tokens = [];
666
+ for (let beat = 0; beat < n; beat++) {
667
+ tokens.push(DRUM_ABC[pattern[beat % pattern.length]].note);
668
+ if ((beat + 1) % beatsPerBar === 0) tokens.push("|");
669
+ }
670
+ if (tokens.length === 0 || tokens[tokens.length - 1] !== "|") tokens.push("|]");
671
+ else tokens[tokens.length - 1] = "|]";
672
+ return tokens.join(" ");
673
+ }
674
+ function lyricLookup(doc) {
675
+ return (ev) => {
676
+ const [p, r, i] = ev.path.split(".").map(Number);
677
+ return doc.parts[p]?.rows[r]?.notes[i]?.lyric;
678
+ };
679
+ }
680
+ function resolveDrumTaal(doc) {
681
+ const id = doc.meta.taal?.id;
682
+ return id !== void 0 && id in TAALS ? TAALS[id] : { numBeats: 4, sam: 0 };
683
+ }
684
+ function docToAbc(doc, opts = {}) {
685
+ const schedule = buildSchedule(doc, { tonic: opts.tonic, baseOctave: opts.baseOctave });
686
+ const bpm = opts.bpm ?? doc.meta.bpm ?? 90;
687
+ const beatsPerBar = opts.beatsPerBar ?? doc.meta.taal?.numBeats ?? 4;
688
+ const drums = opts.drums ? drumPattern(resolveDrumTaal(doc)) : void 0;
689
+ return buildAbc(schedule, lyricLookup(doc), { bpm, lyrics: opts.lyrics !== false, beatsPerBar, drums });
690
+ }
691
+ async function abcToSvg(abc, opts = {}, notePaths) {
692
+ if (typeof document === "undefined") {
693
+ throw new Error("abcToSvg requires a DOM; render staff notation in the browser or print iframe");
694
+ }
695
+ let ABCJS;
696
+ try {
697
+ const mod = await import('abcjs');
698
+ ABCJS = mod.default ?? mod;
699
+ } catch {
700
+ return `<div class="sw-staff-missing">Staff notation needs the optional <code>abcjs</code> package.</div>`;
701
+ }
702
+ const host = document.createElement("div");
703
+ host.style.cssText = "position:fixed; left:-10000px; top:0; width:720px; visibility:hidden;";
704
+ const mount = document.body ?? document.documentElement;
705
+ mount.appendChild(host);
706
+ try {
707
+ ABCJS.renderAbc(host, abc, {
708
+ add_classes: true,
709
+ staffwidth: opts.staffwidth ?? 680,
710
+ scale: opts.scale ?? 1,
711
+ // Reflow into short lines. staffwidth is the width budget;
712
+ // preferredMeasuresPerLine keeps systems swaralipi-short.
713
+ // (Not `responsive: resize` — its absolute-positioned aspect-ratio wrapper
714
+ // overlaps the header and mis-sizes on the print page.)
715
+ wrap: { minSpacing: 1.8, maxSpacing: 2.7, preferredMeasuresPerLine: opts.measuresPerLine ?? 4 },
716
+ // CRUCIAL: put each wrapped system in its OWN <svg> (default is all systems
717
+ // stacked in one tall SVG, which is atomic and can't page-break — that was
718
+ // the near-blank-first-page / mid-staff-cutoff print bug). One SVG per line
719
+ // lets `break-inside: avoid` keep each staff whole while pages fill.
720
+ oneSvgPerLine: true
721
+ });
722
+ for (const svg of host.querySelectorAll("svg")) {
723
+ const w = parseFloat(svg.getAttribute("width") ?? "");
724
+ const h = parseFloat(svg.getAttribute("height") ?? "");
725
+ if (w > 0 && h > 0) {
726
+ if (!svg.getAttribute("viewBox")) svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
727
+ svg.setAttribute("preserveAspectRatio", "xMinYMin meet");
728
+ svg.removeAttribute("width");
729
+ svg.removeAttribute("height");
730
+ svg.setAttribute("style", `max-width:${w}px`);
731
+ }
732
+ }
733
+ if (notePaths) {
734
+ const melody = host.querySelectorAll(".abcjs-v0.abcjs-note, .abcjs-v0.abcjs-rest");
735
+ let i = 0;
736
+ for (const el of melody) {
737
+ const path = notePaths[i++];
738
+ if (path !== void 0) el.setAttribute("data-note", path);
739
+ }
740
+ }
741
+ return host.innerHTML;
742
+ } finally {
743
+ host.remove();
744
+ }
745
+ }
746
+ async function renderStaffHtml(doc, opts = {}) {
747
+ const bpm = opts.bpm ?? doc.meta.bpm ?? 90;
748
+ const abc = docToAbc(doc, { ...opts, bpm });
749
+ const schedule = buildSchedule(doc, { tonic: opts.tonic, baseOctave: opts.baseOctave });
750
+ const engraved = await abcToSvg(abc, opts, eventPaths(schedule));
751
+ const lines = engraved.includes("sw-staff-missing") ? engraved : `<div class="sw-staff-lines">${engraved}</div>`;
752
+ const header = opts.showHeader === false ? "" : renderMetaHeader(doc.meta, opts.headerLang ?? "en", opts.transliterate);
753
+ const metaParts = [];
754
+ if (opts.instrument) metaParts.push(`<span class="sw-staff-instrument">${escapeAttr(opts.instrument)}</span>`);
755
+ if (opts.drums) metaParts.push(`<span class="sw-staff-drums">+ drums</span>`);
756
+ const meta = metaParts.length ? `<div class="sw-staff-meta">${metaParts.join("")}</div>` : "";
757
+ return `<div class="sw-staff">${header}${meta}${lines}</div>`;
758
+ }
759
+ function escapeAttr(s) {
760
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
761
+ }
530
762
 
531
763
  // src/viewer/print.ts
764
+ var DEFAULT_WATERMARK = "www.gitabitan.io";
765
+ var DEFAULT_NOTATOR = "gitabitan.io Team";
532
766
  function escapeHtml2(s) {
533
767
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
534
768
  }
535
- function renderPrintDocument(doc, opts = {}) {
536
- const lang = opts.lang ?? "bn";
537
- const tr = lang !== "bn" && opts.transliterate ? opts.transliterate : (s) => s;
538
- const title = opts.title ?? tr(doc.meta.title);
539
- const body = renderToHtml(doc, {
540
- lang,
541
- showHeader: opts.showHeader,
542
- transliterate: opts.transliterate
543
- });
769
+ function watermarkCss(text) {
770
+ if (!text) return "";
771
+ return `
772
+ .sw-watermark {
773
+ position: fixed; inset: 0; z-index: 0; pointer-events: none;
774
+ display: flex; align-items: center; justify-content: center;
775
+ overflow: hidden;
776
+ -webkit-print-color-adjust: exact; print-color-adjust: exact;
777
+ }
778
+ .sw-watermark span {
779
+ font-size: 11vw; font-weight: 700; letter-spacing: .08em;
780
+ color: rgba(0,0,0,0.07); transform: rotate(-35deg); white-space: nowrap;
781
+ }
782
+ body > .sw-sheet, body > .sw-footer { position: relative; z-index: 1; }
783
+ `;
784
+ }
785
+ function chrome(text, notator) {
786
+ const top = text ? `<div class="sw-watermark" aria-hidden="true"><span>${escapeHtml2(text)}</span></div>` : "";
787
+ const bottom = notator ? `<footer class="sw-footer">${escapeHtml2(notator)}</footer>` : "";
788
+ return { top, bottom };
789
+ }
790
+ function resolveNotator(doc, opts) {
791
+ return doc.meta.notationBy ?? opts.notator ?? DEFAULT_NOTATOR;
792
+ }
793
+ function pageHtml(lang, title, bodyHtml, opts, doc) {
794
+ const watermark = opts.watermark ?? DEFAULT_WATERMARK;
795
+ const { top, bottom } = chrome(watermark, resolveNotator(doc, opts));
544
796
  return `<!doctype html>
545
797
  <html lang="${escapeHtml2(lang)}">
546
798
  <head>
@@ -549,20 +801,42 @@ function renderPrintDocument(doc, opts = {}) {
549
801
  <title>${escapeHtml2(title)}</title>
550
802
  <style>
551
803
  ${SWARALIPI_CSS}
804
+ ${SWARALIPI_STAFF_CSS}
552
805
  ${SWARALIPI_PRINT_CSS}
806
+ ${watermarkCss(watermark)}
553
807
  html, body { margin: 0; }
554
808
  body { color: #111; background: #fff; padding: 16px; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
555
809
  .sw-sheet { max-width: 100%; }
810
+ .sw-footer { margin-top: 12px; padding-top: 6px; border-top: 1px solid #ddd; font-size: 12px; color: #555; text-align: right; }
556
811
  </style>
557
812
  </head>
558
- <body>${body}</body>
813
+ <body>${top}${bodyHtml}${bottom}</body>
559
814
  </html>`;
560
815
  }
816
+ function renderPrintDocument(doc, opts = {}) {
817
+ const lang = opts.lang === "staff" ? "bn" : opts.lang ?? "bn";
818
+ const tr = lang !== "bn" && opts.transliterate ? opts.transliterate : (s) => s;
819
+ const title = opts.title ?? tr(doc.meta.title);
820
+ const body = renderToHtml(doc, { lang, showHeader: opts.showHeader, transliterate: opts.transliterate });
821
+ return pageHtml(lang, title, body, opts, doc);
822
+ }
823
+ async function renderStaffPrintDocument(doc, opts = {}) {
824
+ if (opts.lang !== "staff") return renderPrintDocument(doc, opts);
825
+ const title = opts.title ?? doc.meta.title;
826
+ const staff = await renderStaffHtml(doc, opts);
827
+ return pageHtml("en", title, staff, opts, doc);
828
+ }
561
829
  function printSwaralipi(doc, opts = {}) {
562
830
  if (typeof document === "undefined") {
563
831
  throw new Error("printSwaralipi requires a DOM; use renderPrintDocument() for server-side PDF");
564
832
  }
565
- const html = renderPrintDocument(doc, opts);
833
+ if (opts.lang === "staff") {
834
+ void renderStaffPrintDocument(doc, opts).then(printHtmlInIframe);
835
+ } else {
836
+ printHtmlInIframe(renderPrintDocument(doc, opts));
837
+ }
838
+ }
839
+ function printHtmlInIframe(html) {
566
840
  const iframe = document.createElement("iframe");
567
841
  iframe.setAttribute("aria-hidden", "true");
568
842
  iframe.style.cssText = "position:fixed; left:-10000px; top:0; width:800px; height:600px; border:0;";
@@ -604,7 +878,7 @@ var TARGET = {
604
878
  en: "iast"
605
879
  };
606
880
  async function loadLyricTransliterator(lang) {
607
- if (lang === "bn") return void 0;
881
+ if (lang === "bn" || lang === "staff") return void 0;
608
882
  const mod = await import('@indic-transliteration/sanscript');
609
883
  const sanscript = mod.default ?? mod;
610
884
  const target = TARGET[lang];
@@ -612,7 +886,7 @@ async function loadLyricTransliterator(lang) {
612
886
  }
613
887
 
614
888
  // src/viewer/element.ts
615
- var LANGS = /* @__PURE__ */ new Set(["bn", "hi", "en"]);
889
+ var LANGS = /* @__PURE__ */ new Set(["bn", "hi", "en", "staff"]);
616
890
  function ensureNotationFonts() {
617
891
  if (typeof FontFace === "undefined" || typeof document === "undefined") return;
618
892
  const present = /* @__PURE__ */ new Set();
@@ -632,6 +906,8 @@ function createElementClass() {
632
906
  #abort;
633
907
  #translit;
634
908
  #translitLang;
909
+ #renderSeq = 0;
910
+ #staffOptions = {};
635
911
  validate = false;
636
912
  constructor() {
637
913
  super();
@@ -639,7 +915,7 @@ function createElementClass() {
639
915
  if (typeof CSSStyleSheet !== "undefined" && "replaceSync" in CSSStyleSheet.prototype && "adoptedStyleSheets" in this.#root) {
640
916
  if (!sharedSheet) {
641
917
  sharedSheet = new CSSStyleSheet();
642
- sharedSheet.replaceSync(SWARALIPI_CSS);
918
+ sharedSheet.replaceSync(SWARALIPI_CSS + SWARALIPI_STAFF_CSS);
643
919
  }
644
920
  this.#root.adoptedStyleSheets = [...this.#root.adoptedStyleSheets, sharedSheet];
645
921
  this.#adopted = true;
@@ -658,6 +934,13 @@ function createElementClass() {
658
934
  }
659
935
  this.#render();
660
936
  }
937
+ get staffOptions() {
938
+ return this.#staffOptions;
939
+ }
940
+ set staffOptions(value) {
941
+ this.#staffOptions = value;
942
+ if (this.#lang === "staff") this.#render();
943
+ }
661
944
  get #lang() {
662
945
  const attr = this.getAttribute("lang");
663
946
  return attr !== null && LANGS.has(attr) ? attr : "bn";
@@ -704,14 +987,22 @@ function createElementClass() {
704
987
  }).catch(() => void 0);
705
988
  }
706
989
  #styleFallback() {
707
- return this.#adopted ? "" : `<style>${SWARALIPI_CSS}</style>`;
990
+ return this.#adopted ? "" : `<style>${SWARALIPI_CSS}${SWARALIPI_STAFF_CSS}</style>`;
708
991
  }
709
992
  #render() {
993
+ const seq = ++this.#renderSeq;
710
994
  if (this.#doc === void 0) {
711
995
  this.#root.innerHTML = this.#styleFallback();
712
996
  return;
713
997
  }
714
998
  const lang = this.#lang;
999
+ if (lang === "staff") {
1000
+ const doc = this.#doc;
1001
+ void renderStaffHtml(doc, this.staffOptions).then((html2) => {
1002
+ if (seq === this.#renderSeq) this.#root.innerHTML = this.#styleFallback() + html2;
1003
+ }).catch(() => void 0);
1004
+ return;
1005
+ }
715
1006
  const html = renderToHtml(this.#doc, {
716
1007
  lang,
717
1008
  showHeader: !this.hasAttribute("hide-header"),
@@ -742,6 +1033,7 @@ function createElementClass() {
742
1033
  lang,
743
1034
  showHeader: !this.hasAttribute("hide-header"),
744
1035
  transliterate: this.#translitLang === lang ? this.#translit : void 0,
1036
+ ...lang === "staff" ? this.staffOptions : {},
745
1037
  ...options
746
1038
  });
747
1039
  }
@@ -761,6 +1053,6 @@ function defineSwaralipiSheet(tagName = "swaralipi-sheet") {
761
1053
  return ctor;
762
1054
  }
763
1055
 
764
- export { DIGITS, LABELS, MARKS, NOTATION_FONT_URLS, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, composeCluster, defineSwaralipiSheet, groupRowIntoCells, loadLyricTransliterator, localizeDigits, printSwaralipi, renderPrintDocument, renderToHtml, resolveArcs };
1056
+ export { DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, LABELS, MARKS, NOTATION_FONT_URLS, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, abcDuration, abcToSvg, buildAbc, composeCluster, defineSwaralipiSheet, docToAbc, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderToHtml, resolveArcs };
765
1057
  //# sourceMappingURL=index.js.map
766
1058
  //# sourceMappingURL=index.js.map