@ugfoundation/swaralipi-js 0.1.6 → 0.2.1

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.
@@ -1,43 +1,39 @@
1
1
  // src/viewer/glyphs.ts
2
- var LETTERS = {
3
- bn: {
4
- s: { shuddha: "\u09B8" },
5
- r: { shuddha: "\u09B0", komal: "\u098B" },
6
- g: { shuddha: "\u0997", komal: "\u099C\u09CD\u099E" },
7
- m: { shuddha: "\u09AE", tivra: "\u0995\u09CD\u09B7" },
8
- p: { shuddha: "\u09AA" },
9
- d: { shuddha: "\u09A7", komal: "\u09A6" },
10
- n: { shuddha: "\u09A8", komal: "\u09A3" }
11
- },
12
- hi: {
13
- s: { shuddha: "\u0938" },
14
- r: { shuddha: "\u0930", komal: "\u090B" },
15
- g: { shuddha: "\u0917", komal: "\u091C\u094D\u091E" },
16
- m: { shuddha: "\u092E", tivra: "\u0915\u094D\u0937" },
17
- p: { shuddha: "\u092A" },
18
- d: { shuddha: "\u0927", komal: "\u0926" },
19
- n: { shuddha: "\u0928", komal: "\u0923" }
20
- },
21
- en: {
22
- s: { shuddha: "S" },
23
- r: { shuddha: "R" },
24
- g: { shuddha: "G" },
25
- m: { shuddha: "M" },
26
- p: { shuddha: "P" },
27
- d: { shuddha: "D" },
28
- n: { shuddha: "N" }
29
- }
2
+ var SWARABITAN_SHUDDHA = {
3
+ s: "s",
4
+ r: "r",
5
+ g: "g",
6
+ m: "m",
7
+ p: "p",
8
+ d: "q",
9
+ n: "n"
10
+ // q = ধ
11
+ };
12
+ var SWARABITAN_KOMAL = {
13
+ r: "v",
14
+ g: "t",
15
+ d: "d",
16
+ n: "u"
17
+ // ঋ জ্ঞ দ ণ
18
+ };
19
+ var SWARABITAN_TIVRA = { m: "k" };
20
+ var BHATKHANDE_OCTAVE = {
21
+ madhyam: "",
22
+ taar: "u",
23
+ mandra: "l",
24
+ "ati-taar": "U",
25
+ "ati-mandra": "L"
30
26
  };
27
+ var BHATKHANDE_HAS_KOMAL = /* @__PURE__ */ new Set(["r", "g", "d", "n"]);
31
28
  var DIGITS = {
32
29
  bn: "\u09E6\u09E7\u09E8\u09E9\u09EA\u09EB\u09EC\u09ED\u09EE\u09EF",
33
30
  hi: "\u0966\u0967\u0968\u0969\u096A\u096B\u096C\u096D\u096E\u096F",
34
31
  en: "0123456789"
35
32
  };
36
- var VOWEL_LETTER_GLYPHS = /* @__PURE__ */ new Set(["\u098B", "\u090B"]);
37
33
  var MARKS = {
38
- bn: { matra: "\u09BE", hyphen: "-", melisma: "\u09F9", cut: "\u0983", reph: "\u09B0\u09CD" },
39
- hi: { matra: "\u093E", hyphen: "-", melisma: "\u0970", cut: "\u0903", reph: "\u0930\u094D" },
40
- en: { matra: "", hyphen: "-", melisma: "\xB7", cut: ":", reph: "" }
34
+ bn: { hyphen: "-", melisma: "\u09F9", cut: "\u0983" },
35
+ hi: { hyphen: "-", melisma: "\u0970", cut: "\u0903" },
36
+ en: { hyphen: "-", melisma: "\xB7", cut: ":" }
41
37
  };
42
38
  var LABELS = {
43
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" },
@@ -45,40 +41,20 @@ var LABELS = {
45
41
  en: { title: "Song", taal: "Taal", raag: "Raag", avartan: "Avartan", notationBy: "Notation" }
46
42
  };
47
43
  function composeCluster(letter, opts) {
48
- const forms = LETTERS[opts.lang][letter];
49
- const marks = MARKS[opts.lang];
50
44
  const classes = [];
51
- let base = forms.shuddha;
52
- if (opts.komal) {
53
- if (forms.komal !== void 0) base = forms.komal;
54
- else classes.push("sw-komal");
55
- }
56
- if (opts.tivra) {
57
- if (forms.tivra !== void 0) base = forms.tivra;
58
- else classes.push("sw-tivra");
59
- }
60
- const isVowelLetter = VOWEL_LETTER_GLYPHS.has(base);
61
- let text = base;
62
- switch (opts.octave) {
63
- case "taar":
64
- case "ati-taar":
65
- if (marks.reph === "") classes.push("sw-dot-above");
66
- else if (isVowelLetter) classes.push("sw-reph-css");
67
- else text = marks.reph + text;
68
- if (opts.octave === "ati-taar") classes.push("sw-ati-taar");
69
- break;
70
- case "mandra":
71
- case "ati-mandra":
72
- classes.push(marks.reph === "" ? "sw-dot-below" : "sw-mandra");
73
- if (opts.octave === "ati-mandra") classes.push("sw-ati-mandra");
74
- break;
45
+ if (opts.lang === "bn") {
46
+ let text = opts.tivra && SWARABITAN_TIVRA[letter] || opts.komal && SWARABITAN_KOMAL[letter] || SWARABITAN_SHUDDHA[letter];
47
+ if (opts.octave === "taar" || opts.octave === "ati-taar") text += "f";
48
+ else if (opts.octave === "mandra" || opts.octave === "ati-mandra") text += "h";
49
+ if (opts.octave === "ati-taar") classes.push("sw-ati-taar");
50
+ if (opts.octave === "ati-mandra") classes.push("sw-ati-mandra");
51
+ if (opts.withMatra) text += "a";
52
+ return { text, classes };
75
53
  }
76
- let matraSeparate = false;
77
- if (opts.withMatra && marks.matra !== "") {
78
- if (isVowelLetter) matraSeparate = true;
79
- else text += marks.matra;
80
- }
81
- return { text, classes, matraSeparate };
54
+ let base = letter;
55
+ if (opts.tivra && letter === "m") base = "M";
56
+ else if (opts.komal && BHATKHANDE_HAS_KOMAL.has(letter)) base = base.toUpperCase();
57
+ return { text: base + BHATKHANDE_OCTAVE[opts.octave], classes };
82
58
  }
83
59
  function localizeDigits(s, lang) {
84
60
  const digits = DIGITS[lang];
@@ -201,11 +177,8 @@ function bracketGlyphs(note, edge) {
201
177
  if (!note.brackets) return "";
202
178
  return note.brackets.filter((b) => b.edge === edge).map((b) => `<span class="sw-br">${BRACKET_GLYPHS[edge][b.kind]}</span>`).join("");
203
179
  }
204
- function isoMatra(ctx) {
205
- return ctx.marks.matra === "" ? "" : `<span class="sw-iso">${NBSP}${ctx.marks.matra}</span>`;
206
- }
207
- function restGlyph(ctx) {
208
- return ctx.marks.matra === "" ? '<span class="sw-iso">\u2013</span>' : isoMatra(ctx);
180
+ function matraGlyph(ctx) {
181
+ return ctx.lang === "bn" ? '<span class="sw-cl">a</span>' : '<span class="sw-iso">\u2013</span>';
209
182
  }
210
183
  function renderKan(note, ctx) {
211
184
  if (!note.kan) return "";
@@ -227,26 +200,24 @@ function renderPitchedInner(note, opts, ctx) {
227
200
  lang: ctx.lang
228
201
  });
229
202
  inner += `<span class="${["sw-cl", ...c.classes].join(" ")}">${c.text}</span>`;
230
- if (c.matraSeparate) inner += isoMatra(ctx);
231
203
  } else if (note.rest === true) {
232
- inner += restGlyph(ctx);
204
+ inner += matraGlyph(ctx);
233
205
  } else if (note.sustain === true && opts.withMatra) {
234
- inner += isoMatra(ctx);
206
+ inner += matraGlyph(ctx);
235
207
  }
236
208
  if (note.microtone !== void 0) {
237
- const mark = note.microtone === "atikomal" ? "\u21CA" : "\u2193";
209
+ const mark = localizeDigits(note.microtone === "atikomal" ? "2" : "1", ctx.lang);
238
210
  inner += `<span class="sw-micro" title="${note.microtone}">${mark}</span>`;
239
211
  }
240
212
  const d = note.durationMatra ?? 1;
241
- if (isIntegral(d) && d > 1 + EPS) inner += isoMatra(ctx).repeat(Math.round(d) - 1);
213
+ if (isIntegral(d) && d > 1 + EPS) inner += matraGlyph(ctx).repeat(Math.round(d) - 1);
242
214
  if (opts.cuts > 0) inner += `<span class="sw-cut">${NBSP}${ctx.marks.cut}</span>`.repeat(opts.cuts);
243
215
  inner += bracketGlyphs(note, "close");
244
216
  return inner;
245
217
  }
246
- var BAR_STROKES = { dari: 1, single: 1, double: 2, quad: 4 };
218
+ var BAR_GLYPHS = { dari: "A", single: "l", double: "L", quad: "LL" };
247
219
  function renderBarInner(note) {
248
- const strokes = "<i></i>".repeat(BAR_STROKES[note.bar]);
249
- return `${bracketGlyphs(note, "open")}${strokes}${bracketGlyphs(note, "close")}`;
220
+ return `${bracketGlyphs(note, "open")}${BAR_GLYPHS[note.bar]}${bracketGlyphs(note, "close")}`;
250
221
  }
251
222
  function renderAnnotation(notes, ctx) {
252
223
  const parts = [];
@@ -325,7 +296,7 @@ function renderRow(row, partIdx, rowIdx, ctx) {
325
296
  }
326
297
  if (note.lyric !== void 0) {
327
298
  lyric.push(
328
- `<span class="sw-lyr" style="grid-column:${col}">${escapeHtml(lyricText(note.lyric, ctx))}</span>`
299
+ `<span class="sw-lyr${j === 0 ? " sw-cs" : ""}" style="grid-column:${col}">${escapeHtml(lyricText(note.lyric, ctx))}</span>`
329
300
  );
330
301
  }
331
302
  if (note.above !== void 0 && note.above.length > 0) {
@@ -381,13 +352,29 @@ function renderToHtml(doc, opts = {}) {
381
352
  }
382
353
 
383
354
  // src/viewer/styles.ts
355
+ 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"
359
+ };
360
+ NOTATION_FONT_URLS.Swarabitan;
361
+ var FONT_FACES = Object.entries(NOTATION_FONT_URLS).map(
362
+ ([family, url]) => `@font-face { font-family: ${family}; src: url('${url}') format('truetype'); font-display: block; }`
363
+ ).join("\n");
384
364
  var SWARALIPI_CSS = `
365
+ ${FONT_FACES}
366
+
385
367
  .sw-sheet {
386
368
  font-family: var(--sw-font, 'Noto Sans Bengali', 'Kohinoor Bangla', 'Bangla Sangam MN', 'Nirmala UI', 'Noto Sans Devanagari', sans-serif);
387
369
  font-size: var(--sw-size, 1.125rem);
388
370
  line-height: 1.5;
389
371
  }
390
372
 
373
+ /* ---- notation font, per language (the note clusters + grace notes) ---- */
374
+ .sw-sheet[lang="bn"] .sw-cl, .sw-sheet[lang="bn"] .sw-kan { font-family: Swarabitan, var(--sw-font, sans-serif); }
375
+ .sw-sheet[lang="hi"] .sw-cl, .sw-sheet[lang="hi"] .sw-kan { font-family: ome_bhatkhande_hindi, var(--sw-font, sans-serif); }
376
+ .sw-sheet[lang="en"] .sw-cl, .sw-sheet[lang="en"] .sw-kan { font-family: ome_bhatkhande_english, var(--sw-font, sans-serif); }
377
+
391
378
  /* ---- header ---- */
392
379
  .sw-header {
393
380
  margin: 0 0 0.75em;
@@ -416,10 +403,10 @@ var SWARALIPI_CSS = `
416
403
  align-items: end;
417
404
  }
418
405
  .sw-above { grid-row: 1; font-size: 0.62em; justify-self: start; white-space: nowrap; padding-left: 0.2em; }
419
- .sw-n { grid-row: 2; white-space: nowrap; }
406
+ .sw-n { grid-row: 2; justify-self: center; white-space: nowrap; }
420
407
  .sw-arc,
421
408
  .sw-below { grid-row: 3; }
422
- .sw-lyr { grid-row: 4; white-space: nowrap; padding-top: 0.3em; }
409
+ .sw-lyr { grid-row: 4; justify-self: center; white-space: nowrap; padding-top: 0.3em; }
423
410
  .sw-below { font-size: 0.62em; justify-self: start; white-space: nowrap; padding-left: 0.2em; }
424
411
 
425
412
  /* inter-cell gap as padding on each cell's first notation item, so arcs
@@ -429,71 +416,21 @@ var SWARALIPI_CSS = `
429
416
  /* ---- notation clusters & marks ---- */
430
417
  .sw-cl { position: relative; display: inline-block; }
431
418
 
432
- /* mandra: hasanta-shaped understroke (a real \u09CD before \u09BE is invalid Unicode) */
433
- .sw-mandra::after {
434
- content: '';
435
- position: absolute;
436
- left: 0.1em;
437
- bottom: -0.08em;
438
- width: 0.45em;
439
- height: 0.09em;
440
- background: currentColor;
441
- transform: rotate(-32deg);
442
- }
443
- /* ati-mandra: an extra dot below */
419
+ /* bn ati-octave: Swarabitan's f/h give a single reph/hasanta, so ati adds
420
+ * one extra dot above (ati-taar) / below (ati-mandra). bn-only \u2014 hi/en get
421
+ * the two-dot glyph natively from the U/L octave suffix. */
422
+ .sw-ati-taar::before,
444
423
  .sw-ati-mandra::before {
445
424
  content: '';
446
425
  position: absolute;
447
426
  left: 0.25em;
448
- bottom: -0.35em;
449
427
  width: 0.16em;
450
428
  height: 0.16em;
451
429
  border-radius: 50%;
452
430
  background: currentColor;
453
431
  }
454
- /* ati-taar (reph already in the text): an extra dot above */
455
- .sw-ati-taar::before {
456
- content: '';
457
- position: absolute;
458
- left: 0.25em;
459
- top: -0.3em;
460
- width: 0.16em;
461
- height: 0.16em;
462
- border-radius: 50%;
463
- background: currentColor;
464
- }
465
- /* CSS reph fallback for vowel letters \u098B/\u090B that reject a real reph */
466
- .sw-reph-css::before {
467
- content: '';
468
- position: absolute;
469
- top: -0.12em;
470
- left: 0.12em;
471
- width: 0.3em;
472
- height: 0.3em;
473
- border: 0.09em solid currentColor;
474
- border-right: none;
475
- border-bottom: none;
476
- border-top-left-radius: 0.3em;
477
- }
478
- /* en octave dots (Bhatkhande-style) */
479
- .sw-dot-above::before,
480
- .sw-dot-below::before {
481
- content: '';
482
- position: absolute;
483
- left: 50%;
484
- margin-left: -0.07em;
485
- width: 0.14em;
486
- height: 0.14em;
487
- border-radius: 50%;
488
- background: currentColor;
489
- }
490
- .sw-dot-above::before { top: -0.25em; }
491
- .sw-dot-below::before { bottom: -0.2em; }
492
- .sw-ati-taar.sw-dot-above::before,
493
- .sw-ati-mandra.sw-dot-below::before { box-shadow: 0.24em 0 0 currentColor; margin-left: -0.19em; }
494
- /* en accidentals (bn/hi use distinct letters instead) */
495
- .sw-komal { text-decoration: underline; text-underline-offset: 0.18em; }
496
- .sw-tivra::after { content: '\\2032'; font-size: 0.7em; vertical-align: super; }
432
+ .sw-ati-taar::before { top: -0.3em; }
433
+ .sw-ati-mandra::before { bottom: -0.35em; }
497
434
 
498
435
  /* kan grace: small raised cluster left of the main note */
499
436
  .sw-kan {
@@ -510,29 +447,25 @@ var SWARALIPI_CSS = `
510
447
  .sw-br { opacity: 0.8; padding: 0 0.06em; }
511
448
  .sw-micro { font-size: 0.6em; vertical-align: super; opacity: 0.8; }
512
449
 
513
- /* raw fallback: never dropped, visibly non-notation */
450
+ /* raw fallback: never dropped, visibly non-notation. Swarabitan first \u2014
451
+ * raw IS that font's encoding, so undecoded cells show NLTR's own glyphs
452
+ * (dimmed: semantics unverified); monospace ASCII when the font is out. */
514
453
  .sw-raw {
515
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
454
+ font-family: Swarabitan, ui-monospace, SFMono-Regular, Menlo, monospace;
516
455
  font-size: 0.72em;
517
456
  opacity: 0.55;
518
457
  }
519
458
 
520
- /* ---- bars: dari (thin) and danda strokes, notation-row height ---- */
459
+ /* ---- bars: dari (A) and danda (l/L) Swarabitan glyphs ---- */
521
460
  /* horizontal gap before a bar comes from its own .sw-cs padding-left \u2014
522
461
  * never reset it with a padding shorthand here */
523
462
  .sw-bar {
524
463
  grid-row: 2;
525
- align-self: stretch;
526
- display: inline-flex;
527
- align-items: stretch;
528
- gap: 0.16em;
464
+ font-family: Swarabitan, var(--sw-font, sans-serif);
465
+ white-space: nowrap;
529
466
  padding-right: 0.1em;
530
467
  }
531
- .sw-bar i { display: inline-block; width: 0.14em; min-height: 1.25em; background: currentColor; }
532
- .sw-bar--dari i { width: 0.05em; opacity: 0.9; }
533
- .sw-bar--quad i:nth-child(3) { margin-left: 0.18em; }
534
- .sw-above .sw-bar, .sw-below .sw-bar { display: inline-flex; vertical-align: baseline; }
535
- .sw-above .sw-bar i, .sw-below .sw-bar i { min-height: 1em; }
468
+ .sw-bar--quad { letter-spacing: 0.15em; }
536
469
 
537
470
  /* ---- under-arcs (slurs decoded from w/x segments and meend) ---- */
538
471
  .sw-arc {
@@ -641,7 +574,10 @@ function printSwaralipi(doc, opts = {}) {
641
574
  }
642
575
  const cleanup = () => setTimeout(() => iframe.remove(), 1e3);
643
576
  win.addEventListener("afterprint", cleanup, { once: true });
577
+ let printed = false;
644
578
  const go = () => {
579
+ if (printed) return;
580
+ printed = true;
645
581
  try {
646
582
  win.focus();
647
583
  win.print();
@@ -649,8 +585,14 @@ function printSwaralipi(doc, opts = {}) {
649
585
  }
650
586
  };
651
587
  const fonts = iframe.contentDocument?.fonts;
652
- if (fonts?.ready) void fonts.ready.then(go).catch(go);
653
- else setTimeout(go, 250);
588
+ if (fonts && typeof fonts.forEach === "function") {
589
+ const loads = [Promise.resolve(fonts.ready)];
590
+ fonts.forEach((face) => loads.push(face.load().catch(() => void 0)));
591
+ void Promise.all(loads).then(go);
592
+ setTimeout(go, 3e3);
593
+ } else {
594
+ setTimeout(go, 250);
595
+ }
654
596
  };
655
597
  iframe.srcdoc = html;
656
598
  document.body.appendChild(iframe);
@@ -671,6 +613,15 @@ async function loadLyricTransliterator(lang) {
671
613
 
672
614
  // src/viewer/element.ts
673
615
  var LANGS = /* @__PURE__ */ new Set(["bn", "hi", "en"]);
616
+ function ensureNotationFonts() {
617
+ if (typeof FontFace === "undefined" || typeof document === "undefined") return;
618
+ const present = /* @__PURE__ */ new Set();
619
+ for (const f of document.fonts) present.add(f.family.replace(/['"]/g, ""));
620
+ for (const [family, url] of Object.entries(NOTATION_FONT_URLS)) {
621
+ if (present.has(family)) continue;
622
+ document.fonts.add(new FontFace(family, `url(${url})`, { display: "block" }));
623
+ }
624
+ }
674
625
  var sharedSheet;
675
626
  function createElementClass() {
676
627
  return class SwaralipiSheet extends HTMLElement {
@@ -803,12 +754,13 @@ function defineSwaralipiSheet(tagName = "swaralipi-sheet") {
803
754
  }
804
755
  const existing = customElements.get(tagName);
805
756
  if (existing) return existing;
757
+ ensureNotationFonts();
806
758
  const ctor = registered.get(tagName) ?? createElementClass();
807
759
  registered.set(tagName, ctor);
808
760
  customElements.define(tagName, ctor);
809
761
  return ctor;
810
762
  }
811
763
 
812
- export { DIGITS, LABELS, LETTERS, MARKS, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, composeCluster, defineSwaralipiSheet, groupRowIntoCells, loadLyricTransliterator, localizeDigits, printSwaralipi, renderPrintDocument, renderToHtml, resolveArcs };
764
+ export { DIGITS, LABELS, MARKS, NOTATION_FONT_URLS, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, composeCluster, defineSwaralipiSheet, groupRowIntoCells, loadLyricTransliterator, localizeDigits, printSwaralipi, renderPrintDocument, renderToHtml, resolveArcs };
813
765
  //# sourceMappingURL=index.js.map
814
766
  //# sourceMappingURL=index.js.map