@ugfoundation/swaralipi-js 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/{batch-RUZGM2NR.js → batch-UG26T3IE.js} +4 -4
- package/dist/{batch-RUZGM2NR.js.map → batch-UG26T3IE.js.map} +1 -1
- package/dist/{chunk-ULK7NVKH.js → chunk-KMIW7ZBA.js} +3 -3
- package/dist/{chunk-ULK7NVKH.js.map → chunk-KMIW7ZBA.js.map} +1 -1
- package/dist/{chunk-ZNVTOY7J.js → chunk-NFKKXN7S.js} +3 -3
- package/dist/chunk-NFKKXN7S.js.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/convert/index.cjs +1 -1
- package/dist/convert/index.cjs.map +1 -1
- package/dist/convert/index.js +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/spec/index.cjs +1 -1
- package/dist/spec/index.cjs.map +1 -1
- package/dist/spec/index.d.cts +1 -1
- package/dist/spec/index.d.ts +1 -1
- package/dist/spec/index.js +1 -1
- package/dist/viewer/index.cjs +354 -148
- package/dist/viewer/index.cjs.map +1 -1
- package/dist/viewer/index.d.cts +80 -14
- package/dist/viewer/index.d.ts +80 -14
- package/dist/viewer/index.js +349 -148
- package/dist/viewer/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-ZNVTOY7J.js.map +0 -1
package/dist/viewer/index.cjs
CHANGED
|
@@ -185,7 +185,7 @@ var init_spec = __esm({
|
|
|
185
185
|
init_schema();
|
|
186
186
|
init_schema();
|
|
187
187
|
init_taals();
|
|
188
|
-
SPEC_VERSION = "0.
|
|
188
|
+
SPEC_VERSION = "0.7.0";
|
|
189
189
|
}
|
|
190
190
|
});
|
|
191
191
|
|
|
@@ -460,49 +460,120 @@ function groupRowIntoCells(notes) {
|
|
|
460
460
|
if (note.halfMatra === true || acc > EPS && isIntegral(acc)) close();
|
|
461
461
|
});
|
|
462
462
|
close();
|
|
463
|
-
return cells;
|
|
463
|
+
return mergeStructural(absorbBrackets(cells));
|
|
464
|
+
}
|
|
465
|
+
var isStructural = (c) => c.kind === "bar" || c.kind === "carrier";
|
|
466
|
+
function bracketEdges(cell) {
|
|
467
|
+
if (cell.kind !== "carrier") return void 0;
|
|
468
|
+
let open = false;
|
|
469
|
+
let close = false;
|
|
470
|
+
for (const note of cell.notes) {
|
|
471
|
+
if (note.raw !== void 0 && !/^[{}()[\]\s]*$/.test(note.raw)) return void 0;
|
|
472
|
+
for (const b of note.brackets ?? []) {
|
|
473
|
+
if (b.edge === "open") open = true;
|
|
474
|
+
else close = true;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return open || close ? { open, close } : void 0;
|
|
478
|
+
}
|
|
479
|
+
function absorbBrackets(cells) {
|
|
480
|
+
const out = [];
|
|
481
|
+
for (let i = 0; i < cells.length; i++) {
|
|
482
|
+
const cell = cells[i];
|
|
483
|
+
const edges = bracketEdges(cell);
|
|
484
|
+
if (edges !== void 0 && edges.open !== edges.close) {
|
|
485
|
+
const prev = out[out.length - 1];
|
|
486
|
+
const next = cells[i + 1];
|
|
487
|
+
if (edges.close && prev?.kind === "group") {
|
|
488
|
+
prev.notes = [...prev.notes, ...cell.notes];
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (edges.open && next?.kind === "group") {
|
|
492
|
+
next.notes = [...cell.notes, ...next.notes];
|
|
493
|
+
next.start = cell.start;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
out.push(cell);
|
|
498
|
+
}
|
|
499
|
+
return out;
|
|
464
500
|
}
|
|
465
|
-
function
|
|
501
|
+
function mergeStructural(cells) {
|
|
502
|
+
const out = [];
|
|
503
|
+
for (const cell of cells) {
|
|
504
|
+
const prev = out[out.length - 1];
|
|
505
|
+
if (prev !== void 0 && isStructural(prev) && isStructural(cell)) {
|
|
506
|
+
prev.notes = [...prev.notes, ...cell.notes];
|
|
507
|
+
if (cell.kind === "bar") prev.kind = "bar";
|
|
508
|
+
} else out.push(cell);
|
|
509
|
+
}
|
|
510
|
+
return out;
|
|
511
|
+
}
|
|
512
|
+
var SECTION_BARS = /* @__PURE__ */ new Set(["double", "quad"]);
|
|
513
|
+
var isBarCell = (cell) => cell?.kind === "bar";
|
|
514
|
+
var endsSection = (cell) => isBarCell(cell) && SECTION_BARS.has(cell.notes[cell.notes.length - 1].bar);
|
|
515
|
+
function alignRows(rows) {
|
|
516
|
+
const counts = /* @__PURE__ */ new Map();
|
|
517
|
+
for (const cells of rows)
|
|
518
|
+
if (isBarCell(cells[0])) counts.set(cells.length, (counts.get(cells.length) ?? 0) + 1);
|
|
519
|
+
let width = 0;
|
|
520
|
+
let best = 0;
|
|
521
|
+
for (const [len, n] of counts)
|
|
522
|
+
if (n > best || n === best && len > width) {
|
|
523
|
+
width = len;
|
|
524
|
+
best = n;
|
|
525
|
+
}
|
|
526
|
+
if (width === 0) for (const cells of rows) width = Math.max(width, cells.length);
|
|
527
|
+
const offsets = rows.map((cells, i) => {
|
|
528
|
+
if (cells.length === 0 || cells.length >= width) return 0;
|
|
529
|
+
const prev = rows[i - 1];
|
|
530
|
+
const phraseStart = !isBarCell(cells[0]) || i === 0 || endsSection(prev?.[prev.length - 1]);
|
|
531
|
+
if (!phraseStart) return 0;
|
|
532
|
+
return Math.max(0, width - cells.length - (isBarCell(cells[cells.length - 1]) ? 0 : 1));
|
|
533
|
+
});
|
|
534
|
+
rows.forEach((cells, i) => {
|
|
535
|
+
width = Math.max(width, offsets[i] + cells.length);
|
|
536
|
+
});
|
|
537
|
+
return { width, offsets };
|
|
538
|
+
}
|
|
539
|
+
function resolveArcs(cells) {
|
|
466
540
|
const arcs = [];
|
|
467
541
|
let openStart;
|
|
468
542
|
let sawEnds = false;
|
|
469
|
-
const
|
|
470
|
-
const openAt = (
|
|
543
|
+
const fillCells = [];
|
|
544
|
+
const openAt = (i) => {
|
|
471
545
|
sawEnds = true;
|
|
472
|
-
if (openStart === void 0) openStart =
|
|
546
|
+
if (openStart === void 0) openStart = i;
|
|
473
547
|
};
|
|
474
|
-
const closeAt = (
|
|
548
|
+
const closeAt = (i) => {
|
|
475
549
|
sawEnds = true;
|
|
476
550
|
if (openStart !== void 0) {
|
|
477
|
-
arcs.push({
|
|
551
|
+
arcs.push({ start: openStart, end: i, openLeft: false, openRight: false });
|
|
478
552
|
openStart = void 0;
|
|
479
553
|
} else {
|
|
480
|
-
arcs.push({
|
|
554
|
+
arcs.push({ start: 0, end: i, openLeft: true, openRight: false });
|
|
481
555
|
}
|
|
482
556
|
};
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const col = cell.start + j + 1;
|
|
488
|
-
if (note.meend === "start") openAt(col);
|
|
489
|
-
else if (note.meend === "end") closeAt(col);
|
|
557
|
+
cells.forEach((cell, i) => {
|
|
558
|
+
for (const note of cell.notes) {
|
|
559
|
+
if (note.meend === "start") openAt(i);
|
|
560
|
+
else if (note.meend === "end") closeAt(i);
|
|
490
561
|
for (const b of note.below ?? []) {
|
|
491
562
|
if (!isArcRaw(b.raw)) continue;
|
|
492
563
|
for (const ch of b.raw) {
|
|
493
|
-
if (ch === "w" || ch === "W") openAt(
|
|
494
|
-
else if (ch === "x" || ch === "X") closeAt(
|
|
495
|
-
else
|
|
564
|
+
if (ch === "w" || ch === "W") openAt(i);
|
|
565
|
+
else if (ch === "x" || ch === "X") closeAt(i);
|
|
566
|
+
else fillCells.push(i);
|
|
496
567
|
}
|
|
497
568
|
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
569
|
+
}
|
|
570
|
+
});
|
|
500
571
|
if (openStart !== void 0) {
|
|
501
|
-
arcs.push({
|
|
502
|
-
} else if (!sawEnds &&
|
|
572
|
+
arcs.push({ start: openStart, end: Math.max(cells.length - 1, 0), openLeft: false, openRight: true });
|
|
573
|
+
} else if (!sawEnds && fillCells.length > 0) {
|
|
503
574
|
arcs.push({
|
|
504
|
-
|
|
505
|
-
|
|
575
|
+
start: Math.min(...fillCells),
|
|
576
|
+
end: Math.max(...fillCells),
|
|
506
577
|
openLeft: true,
|
|
507
578
|
openRight: true,
|
|
508
579
|
line: true
|
|
@@ -510,9 +581,22 @@ function resolveArcs(cells, noteCount) {
|
|
|
510
581
|
}
|
|
511
582
|
return arcs;
|
|
512
583
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
584
|
+
var SVARA_CHAR = /[srgmpqndtkuv]/;
|
|
585
|
+
function rawMarks(raw, count) {
|
|
586
|
+
if (raw === void 0) return void 0;
|
|
587
|
+
const marks = { aakar: false, pre: "", post: new Array(count).fill("") };
|
|
588
|
+
const leads = SVARA_CHAR.test(raw);
|
|
589
|
+
let seen = 0;
|
|
590
|
+
for (const ch of raw) {
|
|
591
|
+
if (SVARA_CHAR.test(ch)) seen = Math.min(seen + 1, count);
|
|
592
|
+
else if (ch === "a") marks.aakar = true;
|
|
593
|
+
else if (ch === "i" || ch === ":") {
|
|
594
|
+
if (seen > 0) marks.post[seen - 1] += ch;
|
|
595
|
+
else if (leads) marks.pre += ch;
|
|
596
|
+
else marks.post[0] += ch;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return marks;
|
|
516
600
|
}
|
|
517
601
|
function parseBeatLabel(raw) {
|
|
518
602
|
const m = raw.match(/^([0-9]{1,2})([fF])?:?$/);
|
|
@@ -536,16 +620,54 @@ function renderGrace(svara, cls, ctx) {
|
|
|
536
620
|
const c = composeCluster(letter, { octave, withMatra: false, lang: ctx.lang });
|
|
537
621
|
return `<span class="${[cls, ...c.classes].join(" ")}">${c.text}</span>`;
|
|
538
622
|
}
|
|
623
|
+
function cutGlyphs(marks, ctx) {
|
|
624
|
+
if (marks === "") return "";
|
|
625
|
+
return ctx.lang === "bn" ? `<span class="sw-cl" title="${ctx.marks.cut}">${marks}</span>` : `<span class="sw-cut" title="${ctx.marks.cut}"></span>`.repeat(marks.length);
|
|
626
|
+
}
|
|
539
627
|
function cellMatra(cell) {
|
|
540
|
-
const
|
|
541
|
-
const
|
|
628
|
+
const pitched = cell.notes.filter(isPitched);
|
|
629
|
+
const total = pitched.reduce((a, n) => a + (n.durationMatra ?? 1), 0);
|
|
630
|
+
const half = pitched.some((n) => n.halfMatra === true);
|
|
542
631
|
const whole = Math.floor(total + EPS);
|
|
543
632
|
return { matras: half ? whole : Math.max(1, whole), half };
|
|
544
633
|
}
|
|
634
|
+
function cellPitchedOpts(cell) {
|
|
635
|
+
const { matras, half } = cellMatra(cell);
|
|
636
|
+
const pitched = [];
|
|
637
|
+
cell.notes.forEach((n, j) => {
|
|
638
|
+
if (isPitched(n)) pitched.push(j);
|
|
639
|
+
});
|
|
640
|
+
const first = pitched[0];
|
|
641
|
+
const last = pitched[pitched.length - 1];
|
|
642
|
+
const marks = rawMarks(first === void 0 ? void 0 : cell.notes[first].raw, pitched.length);
|
|
643
|
+
const aakars = marks !== void 0 && !marks.aakar ? 0 : matras;
|
|
644
|
+
return cell.notes.map((note, j) => {
|
|
645
|
+
if (!isPitched(note)) return { matras: 0, pre: "", post: "" };
|
|
646
|
+
return {
|
|
647
|
+
matras: j === last ? aakars : 0,
|
|
648
|
+
pre: j === first ? marks?.pre ?? "" : "",
|
|
649
|
+
post: marks !== void 0 ? marks.post[pitched.indexOf(j)] : half && j === last ? "i" : ""
|
|
650
|
+
};
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
function cellLyricSlices(cell) {
|
|
654
|
+
if (!cell.notes.some((n) => n.lyric !== void 0)) return void 0;
|
|
655
|
+
const pitched = cell.notes.filter(isPitched);
|
|
656
|
+
if (pitched.length === 0) return cell.notes.map((n) => n.lyric ?? "");
|
|
657
|
+
const slices = new Array(pitched.length).fill("");
|
|
658
|
+
let k = 0;
|
|
659
|
+
for (const note of cell.notes) {
|
|
660
|
+
const at = Math.min(k, pitched.length - 1);
|
|
661
|
+
slices[at] += note.lyric ?? "";
|
|
662
|
+
if (isPitched(note)) k++;
|
|
663
|
+
}
|
|
664
|
+
return slices;
|
|
665
|
+
}
|
|
545
666
|
function renderPitchedInner(note, opts, ctx) {
|
|
546
667
|
let inner = bracketGlyphs(note, "open");
|
|
547
668
|
inner += renderGrace(note.kan, "sw-kan", ctx);
|
|
548
669
|
if (note.sustain === true) inner += `<span class="sw-hy">${ctx.marks.hyphen}</span>`;
|
|
670
|
+
inner += cutGlyphs(opts.pre, ctx);
|
|
549
671
|
if (note.svara !== void 0) {
|
|
550
672
|
const [octave, letter] = note.svara;
|
|
551
673
|
const c = composeCluster(letter, {
|
|
@@ -571,8 +693,7 @@ function renderPitchedInner(note, opts, ctx) {
|
|
|
571
693
|
if (note.svara !== void 0 && opts.matras > 1) {
|
|
572
694
|
inner += matraGlyph(ctx).repeat(opts.matras - 1);
|
|
573
695
|
}
|
|
574
|
-
|
|
575
|
-
if (opts.cuts > 0) inner += `<span class="sw-cut" title="${ctx.marks.cut}"></span>`.repeat(opts.cuts);
|
|
696
|
+
inner += cutGlyphs(opts.post, ctx);
|
|
576
697
|
inner += bracketGlyphs(note, "close");
|
|
577
698
|
return inner;
|
|
578
699
|
}
|
|
@@ -589,12 +710,9 @@ function renderAnnotation(notes, ctx) {
|
|
|
589
710
|
continue;
|
|
590
711
|
}
|
|
591
712
|
if (cell.kind === "group") {
|
|
592
|
-
const
|
|
593
|
-
const cuts = half ? 0 : cutMarkCount(cell.notes[0].raw);
|
|
713
|
+
const opts = cellPitchedOpts(cell);
|
|
594
714
|
cell.notes.forEach((note2, j) => {
|
|
595
|
-
|
|
596
|
-
const opts = last ? { matras, half, cuts } : { matras: 0, half: false, cuts: 0 };
|
|
597
|
-
parts.push(`<span class="sw-n">${renderPitchedInner(note2, opts, ctx)}</span>`);
|
|
715
|
+
parts.push(`<span class="sw-n">${renderPitchedInner(note2, opts[j], ctx)}</span>`);
|
|
598
716
|
});
|
|
599
717
|
continue;
|
|
600
718
|
}
|
|
@@ -621,67 +739,56 @@ function renderAnnotation(notes, ctx) {
|
|
|
621
739
|
function lyricText(s, ctx) {
|
|
622
740
|
return s.split("\u09F9").map((part) => part === "" ? "" : ctx.tr(part)).join(ctx.marks.melisma);
|
|
623
741
|
}
|
|
624
|
-
|
|
742
|
+
var COL_BASE = 2;
|
|
743
|
+
function renderNote(note, kind, opts, ctx) {
|
|
744
|
+
if (note.bar !== void 0) return renderBarInner(note);
|
|
745
|
+
if (kind === "group" && opts !== void 0) return renderPitchedInner(note, opts, ctx);
|
|
746
|
+
const raw = rawDisplay(note);
|
|
747
|
+
return bracketGlyphs(note, "open") + (raw !== void 0 ? `<span class="sw-raw" title="${escapeHtml(raw)}">${escapeHtml(raw)}</span>` : "") + bracketGlyphs(note, "close");
|
|
748
|
+
}
|
|
749
|
+
function renderArc(arc, offset) {
|
|
750
|
+
const open = arc.line === true;
|
|
751
|
+
const cls = [
|
|
752
|
+
"sw-arc",
|
|
753
|
+
arc.openLeft || open ? "sw-arc--openl" : "",
|
|
754
|
+
arc.openRight || open ? "sw-arc--openr" : ""
|
|
755
|
+
].filter(Boolean).join(" ");
|
|
756
|
+
const left = open || arc.openLeft ? "" : '<i class="sw-arc-l">w</i>';
|
|
757
|
+
const right = open || arc.openRight ? "" : '<i class="sw-arc-r">x</i>';
|
|
758
|
+
const from = offset + arc.start + COL_BASE;
|
|
759
|
+
const to = offset + arc.end + COL_BASE + 1;
|
|
760
|
+
return `<span class="${cls}" style="grid-column:${from} / ${to}">${left}<i class="sw-arc-m"></i>${right}</span>`;
|
|
761
|
+
}
|
|
762
|
+
function renderRow(row, partIdx, rowIdx, offset, ctx) {
|
|
625
763
|
const cells = groupRowIntoCells(row.notes);
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
const
|
|
634
|
-
const cellSpan = lastCol > firstCol ? `grid-column:${firstCol} / ${lastCol + 1}` : `grid-column:${firstCol}`;
|
|
635
|
-
const { matras, half } = cellMatra(cell);
|
|
636
|
-
const cuts = cell.kind === "group" && !half ? cutMarkCount(cell.notes[0].raw) : 0;
|
|
637
|
-
cell.notes.forEach((note, j) => {
|
|
638
|
-
const col = cell.start + j + 1;
|
|
764
|
+
const out = [];
|
|
765
|
+
if (row.label !== void 0)
|
|
766
|
+
out.push(`<span class="sw-label">${escapeHtml(ctx.tr(row.label))}</span>`);
|
|
767
|
+
cells.forEach((cell, ci) => {
|
|
768
|
+
const col = offset + ci + COL_BASE;
|
|
769
|
+
const opts = cell.kind === "group" ? cellPitchedOpts(cell) : [];
|
|
770
|
+
const barCls = cell.kind === "bar" ? " sw-bar-cell" : "";
|
|
771
|
+
const notes = cell.notes.map((note, j) => {
|
|
639
772
|
const path = `${partIdx}.${rowIdx}.${cell.start + j}`;
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
660
|
-
if (note.lyric !== void 0) {
|
|
661
|
-
lyric.push(
|
|
662
|
-
`<span class="sw-lyr${j === 0 ? " sw-cs" : ""}" style="grid-column:${col}">${escapeHtml(lyricText(note.lyric, ctx))}</span>`
|
|
663
|
-
);
|
|
664
|
-
}
|
|
665
|
-
if (note.above !== void 0 && note.above.length > 0) {
|
|
666
|
-
above.push(`<span class="sw-above" style="${cellSpan}">${renderAnnotation(note.above, ctx)}</span>`);
|
|
667
|
-
}
|
|
668
|
-
const nonArc = (note.below ?? []).filter((b) => !isArcRaw(b.raw));
|
|
669
|
-
if (nonArc.length > 0) {
|
|
670
|
-
below.push(`<span class="sw-below" style="${cellSpan}">${renderAnnotation(nonArc, ctx)}</span>`);
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
for (const arc of arcs) {
|
|
675
|
-
const cls = [
|
|
676
|
-
"sw-arc",
|
|
677
|
-
arc.openLeft ? "sw-arc--openl" : "",
|
|
678
|
-
arc.openRight ? "sw-arc--openr" : "",
|
|
679
|
-
arc.line === true ? "sw-arc--line" : ""
|
|
680
|
-
].filter(Boolean).join(" ");
|
|
681
|
-
below.push(`<span class="${cls}" style="grid-column:${arc.startCol} / ${arc.endCol + 1}"></span>`);
|
|
682
|
-
}
|
|
683
|
-
const label = row.label !== void 0 ? `<span class="sw-label">${escapeHtml(ctx.tr(row.label))}</span>` : "";
|
|
684
|
-
return `<div class="sw-row">${label}<div class="sw-grid">${above.join("")}${notation.join("")}${below.join("")}${lyric.join("")}</div></div>`;
|
|
773
|
+
const cls = isPitched(note) ? " sw-p" : "";
|
|
774
|
+
const bar = note.bar !== void 0 ? ` sw-bar sw-bar--${note.bar}` : "";
|
|
775
|
+
return `<span class="sw-n${cls}${bar}" data-note="${path}">${renderNote(note, cell.kind, opts[j], ctx)}</span>`;
|
|
776
|
+
}).join("");
|
|
777
|
+
out.push(`<span class="sw-cell${barCls}" style="grid-column:${col}">${notes}</span>`);
|
|
778
|
+
const slices = cellLyricSlices(cell);
|
|
779
|
+
if (slices !== void 0) {
|
|
780
|
+
const html = slices.map((t) => `<span>${escapeHtml(lyricText(t, ctx))}</span>`).join("");
|
|
781
|
+
out.push(`<span class="sw-lyr" style="grid-column:${col}">${html}</span>`);
|
|
782
|
+
}
|
|
783
|
+
const above = cell.notes.flatMap((n) => n.above ?? []);
|
|
784
|
+
if (above.length > 0)
|
|
785
|
+
out.push(`<span class="sw-above" style="grid-column:${col}">${renderAnnotation(above, ctx)}</span>`);
|
|
786
|
+
const below = cell.notes.flatMap((n) => (n.below ?? []).filter((b) => !isArcRaw(b.raw)));
|
|
787
|
+
if (below.length > 0)
|
|
788
|
+
out.push(`<span class="sw-below" style="grid-column:${col}">${renderAnnotation(below, ctx)}</span>`);
|
|
789
|
+
});
|
|
790
|
+
for (const arc of resolveArcs(cells)) out.push(renderArc(arc, offset));
|
|
791
|
+
return `<div class="sw-row">${out.join("")}</div>`;
|
|
685
792
|
}
|
|
686
793
|
function renderHeader(meta, ctx) {
|
|
687
794
|
return renderMetaHeader(meta, ctx.lang, ctx.tr);
|
|
@@ -704,19 +811,45 @@ function renderToHtml(doc, opts = {}) {
|
|
|
704
811
|
const lang = opts.lang === "hi" || opts.lang === "en" ? opts.lang : "bn";
|
|
705
812
|
const tr = lang !== "bn" && opts.transliterate ? opts.transliterate : (s) => s;
|
|
706
813
|
const ctx = { lang, marks: MARKS[lang], tr };
|
|
814
|
+
const layout = alignRows(doc.parts.flatMap((p) => p.rows.map((r) => groupRowIntoCells(r.notes))));
|
|
815
|
+
const tracks = `grid-template-columns:max-content repeat(${layout.width},minmax(var(--sw-matra, 1.4em),max-content))`;
|
|
707
816
|
const out = [`<div class="sw-sheet" lang="${lang}">`];
|
|
708
817
|
if (opts.showHeader !== false) out.push(renderHeader(doc.meta, ctx));
|
|
818
|
+
out.push(`<div class="sw-scroll"><div class="sw-block" style="${tracks}">`);
|
|
819
|
+
let n = 0;
|
|
709
820
|
doc.parts.forEach((part, p) => {
|
|
710
821
|
out.push('<section class="sw-part">');
|
|
711
822
|
if (part.name !== void 0)
|
|
712
823
|
out.push(`<h3 class="sw-part-name">${escapeHtml(ctx.tr(part.name))}</h3>`);
|
|
713
|
-
part.rows.forEach((row, r) => out.push(renderRow(row, p, r, ctx)));
|
|
824
|
+
part.rows.forEach((row, r) => out.push(renderRow(row, p, r, layout.offsets[n++], ctx)));
|
|
714
825
|
out.push("</section>");
|
|
715
826
|
});
|
|
716
|
-
out.push("</div>");
|
|
827
|
+
out.push("</div></div></div>");
|
|
717
828
|
return out.join("");
|
|
718
829
|
}
|
|
719
830
|
|
|
831
|
+
// src/viewer/fit.ts
|
|
832
|
+
function fitSwaralipi(root, minScale = 0, zoom = 1) {
|
|
833
|
+
const sheets = root.querySelectorAll(".sw-sheet");
|
|
834
|
+
for (const sheet of sheets) {
|
|
835
|
+
const scroll = sheet.querySelector(".sw-scroll");
|
|
836
|
+
const block = sheet.querySelector(".sw-block");
|
|
837
|
+
if (scroll === null || block === null) continue;
|
|
838
|
+
sheet.style.removeProperty("--sw-size");
|
|
839
|
+
const available = scroll.clientWidth;
|
|
840
|
+
const natural = block.scrollWidth;
|
|
841
|
+
if (available === 0 || natural === 0) continue;
|
|
842
|
+
const fit = natural > available ? Math.max(minScale, available / natural) : 1;
|
|
843
|
+
const scale = fit * zoom;
|
|
844
|
+
if (scale === 1) continue;
|
|
845
|
+
const base = parseFloat(getComputedStyle(sheet).fontSize);
|
|
846
|
+
sheet.style.setProperty("--sw-size", `${(base * scale).toFixed(3)}px`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
var MIN_ZOOM = 0.5;
|
|
850
|
+
var MAX_ZOOM = 3;
|
|
851
|
+
var ZOOM_STEP = 1.2;
|
|
852
|
+
|
|
720
853
|
// src/viewer/styles.ts
|
|
721
854
|
var NOTATION_FONT_URLS = {
|
|
722
855
|
Swarabitan: "https://lipighor.pages.dev/swarabitan/Swarabitan.ttf",
|
|
@@ -754,30 +887,42 @@ ${FONT_FACES}
|
|
|
754
887
|
|
|
755
888
|
.sw-part-name { font-size: 0.85em; font-weight: 600; margin: 1em 0 0.2em; opacity: 0.85; }
|
|
756
889
|
|
|
757
|
-
/* ----
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
890
|
+
/* ---- block grid: ONE column set for the whole sheet ----
|
|
891
|
+
* NLTR emits a song as a single html table, which is why its lines align
|
|
892
|
+
* down the page. Same here: .sw-block owns the tracks, every .sw-row
|
|
893
|
+
* borrows them with subgrid, and .sw-part is display:contents so section
|
|
894
|
+
* headings sit in the same grid instead of breaking it. */
|
|
895
|
+
.sw-scroll { overflow-x: auto; }
|
|
896
|
+
.sw-block {
|
|
897
|
+
display: grid;
|
|
898
|
+
column-gap: var(--sw-cell-gap, 0.4em);
|
|
899
|
+
align-items: end;
|
|
900
|
+
width: max-content;
|
|
763
901
|
}
|
|
764
|
-
.sw-
|
|
765
|
-
.sw-
|
|
902
|
+
.sw-part { display: contents; }
|
|
903
|
+
.sw-row {
|
|
904
|
+
grid-column: 1 / -1;
|
|
766
905
|
display: grid;
|
|
906
|
+
grid-template-columns: subgrid;
|
|
767
907
|
grid-template-rows: auto auto auto auto;
|
|
768
|
-
grid-auto-columns: max-content;
|
|
769
908
|
align-items: end;
|
|
909
|
+
padding: 0.4em 0;
|
|
770
910
|
}
|
|
771
|
-
.sw-
|
|
772
|
-
|
|
911
|
+
.sw-label { grid-column: 1; grid-row: 2; padding-right: 0.5em; font-weight: 600; }
|
|
912
|
+
|
|
913
|
+
/* One column per cell; sub-notes are equal slices of it (min-width:0 kills
|
|
914
|
+
* flex's min-content floor, else a wide sub-note steals width from its
|
|
915
|
+
* neighbours while the syllable below it does not, and the two bands drift). */
|
|
916
|
+
.sw-cell, .sw-lyr { display: flex; justify-content: center; }
|
|
917
|
+
.sw-cell > .sw-n { white-space: nowrap; }
|
|
918
|
+
.sw-cell > .sw-n.sw-p,
|
|
919
|
+
.sw-lyr > span { flex: 1 1 0; min-width: 0; text-align: center; white-space: nowrap; }
|
|
920
|
+
.sw-cell { grid-row: 2; }
|
|
921
|
+
.sw-above { grid-row: 1; font-size: 0.62em; justify-self: center; white-space: nowrap; }
|
|
773
922
|
.sw-arc,
|
|
774
923
|
.sw-below { grid-row: 3; }
|
|
775
|
-
.sw-lyr { grid-row: 4;
|
|
776
|
-
.sw-below { font-size: 0.62em; justify-self:
|
|
777
|
-
|
|
778
|
-
/* inter-cell gap as padding on each cell's first notation item, so arcs
|
|
779
|
-
* and above-spans crossing cells stay continuous */
|
|
780
|
-
.sw-cs { padding-left: var(--sw-cell-gap, 0.55em); }
|
|
924
|
+
.sw-lyr { grid-row: 4; padding-top: 0.3em; }
|
|
925
|
+
.sw-below { font-size: 0.62em; justify-self: center; white-space: nowrap; }
|
|
781
926
|
|
|
782
927
|
/* ---- notation clusters & marks ---- */
|
|
783
928
|
.sw-cl { position: relative; display: inline-block; }
|
|
@@ -811,21 +956,19 @@ ${FONT_FACES}
|
|
|
811
956
|
|
|
812
957
|
.sw-hy { padding-right: 0.02em; }
|
|
813
958
|
.sw-iso { display: inline-block; margin-left: -0.18em; }
|
|
814
|
-
/* The \u0985\u09B0\u09CD\u09A7\u09AE\u09BE\u09A4\u09CD\u09B0\u09BE / \u099B\u09C7\u09A6 mark: two stacked rings (legend p. \u09E7\u09E9\u09EB)
|
|
815
|
-
*
|
|
816
|
-
* a dotted circle (an NBSP carrier
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
.sw-cut
|
|
820
|
-
.sw-half {
|
|
959
|
+
/* The \u0985\u09B0\u09CD\u09A7\u09AE\u09BE\u09A4\u09CD\u09B0\u09BE / \u099B\u09C7\u09A6 mark: two stacked rings (legend p. \u09E7\u09E9\u09EB). bn types
|
|
960
|
+
* Swarabitan's own glyph (the ASCII i key) \u2014 an isolated Unicode \u0983 has no base and
|
|
961
|
+
* every Bengali text font shapes it with a dotted circle (an NBSP carrier
|
|
962
|
+
* does not help), but the notation font has the mark itself. hi/en fall back
|
|
963
|
+
* to these drawn rings: Bhatkhande has no such sign. */
|
|
964
|
+
.sw-cut {
|
|
821
965
|
position: relative;
|
|
822
966
|
display: inline-block;
|
|
823
967
|
width: 0.3em;
|
|
824
968
|
height: 1em;
|
|
825
969
|
vertical-align: baseline;
|
|
826
970
|
}
|
|
827
|
-
.sw-cut::before, .sw-cut::after
|
|
828
|
-
.sw-half::before, .sw-half::after {
|
|
971
|
+
.sw-cut::before, .sw-cut::after {
|
|
829
972
|
content: '';
|
|
830
973
|
position: absolute;
|
|
831
974
|
left: 0.08em;
|
|
@@ -834,8 +977,8 @@ ${FONT_FACES}
|
|
|
834
977
|
border-radius: 50%;
|
|
835
978
|
background: currentColor;
|
|
836
979
|
}
|
|
837
|
-
.sw-cut::before
|
|
838
|
-
.sw-cut::after
|
|
980
|
+
.sw-cut::before { top: 0.2em; }
|
|
981
|
+
.sw-cut::after { top: 0.55em; }
|
|
839
982
|
.sw-br { opacity: 0.8; padding: 0 0.06em; }
|
|
840
983
|
.sw-micro { font-size: 0.6em; vertical-align: super; opacity: 0.8; }
|
|
841
984
|
|
|
@@ -848,31 +991,41 @@ ${FONT_FACES}
|
|
|
848
991
|
opacity: 0.55;
|
|
849
992
|
}
|
|
850
993
|
|
|
851
|
-
/* ---- bars: dari (A) and danda (l/L) Swarabitan glyphs ---- */
|
|
852
|
-
/* horizontal gap before a bar comes from its own .sw-cs padding-left \u2014
|
|
853
|
-
* never reset it with a padding shorthand here */
|
|
994
|
+
/* ---- bars: dari (A) and danda (l/L) Swarabitan glyphs, all langs ---- */
|
|
854
995
|
.sw-bar {
|
|
855
|
-
grid-row: 2;
|
|
856
996
|
font-family: Swarabitan, var(--sw-font, sans-serif);
|
|
857
997
|
white-space: nowrap;
|
|
858
|
-
padding-right: 0.1em;
|
|
859
998
|
}
|
|
860
999
|
.sw-bar--quad { letter-spacing: 0.15em; }
|
|
1000
|
+
/* a bar owns a whole column but needs no matra of width */
|
|
1001
|
+
.sw-bar-cell { min-width: 0; }
|
|
861
1002
|
|
|
862
|
-
/* ---- under-arcs (
|
|
1003
|
+
/* ---- \u09AE\u09C0\u09A1\u09BC under-arcs (from w/x segments and spec meend) ----
|
|
1004
|
+
* Swarabitan draws the arc as three pieces \u2014 'w' the opening hook, 'y' a
|
|
1005
|
+
* flat fill, 'x' the closing hook. We use the font's own hooks and stretch
|
|
1006
|
+
* the fill as a CSS bar (the 'y' glyph IS a plain rectangle), so one arc
|
|
1007
|
+
* spans any number of columns. An open side = the arc continues onto the
|
|
1008
|
+
* next system: drop that hook and leave the fill squared off. */
|
|
863
1009
|
.sw-arc {
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
.sw-arc
|
|
1010
|
+
display: flex;
|
|
1011
|
+
align-items: baseline;
|
|
1012
|
+
line-height: 1;
|
|
1013
|
+
font-family: Swarabitan, var(--sw-font, sans-serif);
|
|
1014
|
+
font-size: 0.9em;
|
|
1015
|
+
}
|
|
1016
|
+
.sw-arc-l, .sw-arc-r { flex: none; font-style: normal; }
|
|
1017
|
+
/* The fill's geometry is measured off the font's own 'y' glyph: ink 0.11em
|
|
1018
|
+
tall whose bottom sits 0.01em above the baseline. An empty inline-block
|
|
1019
|
+
takes its bottom margin edge as its baseline, so -0.01em lifts the bar to
|
|
1020
|
+
exactly where the hooks' flat stroke runs \u2014 no seam at either join. */
|
|
1021
|
+
.sw-arc-m {
|
|
1022
|
+
flex: 1 1 0;
|
|
1023
|
+
min-width: 0;
|
|
1024
|
+
align-self: baseline;
|
|
1025
|
+
height: 0.11em;
|
|
1026
|
+
margin-bottom: -0.01em;
|
|
1027
|
+
background: currentColor;
|
|
1028
|
+
}
|
|
876
1029
|
|
|
877
1030
|
/* ---- annotation content ---- */
|
|
878
1031
|
.sw-beat { position: relative; display: inline-block; padding: 0 0.1em; }
|
|
@@ -916,8 +1069,8 @@ var SWARALIPI_PRINT_CSS = `
|
|
|
916
1069
|
@page { margin: 16mm; }
|
|
917
1070
|
@media print {
|
|
918
1071
|
.sw-sheet, .sw-staff { -webkit-print-color-adjust: exact; print-color-adjust: exact; color: #000; }
|
|
919
|
-
.sw-
|
|
920
|
-
.sw-
|
|
1072
|
+
.sw-scroll { overflow: visible !important; }
|
|
1073
|
+
.sw-row { break-inside: avoid; page-break-inside: avoid; }
|
|
921
1074
|
.sw-part-name, .sw-header { break-after: avoid-page; page-break-after: avoid; }
|
|
922
1075
|
.sw-hl { background: none !important; }
|
|
923
1076
|
/* Keep each engraved staff system whole across a page break (the abcjs line
|
|
@@ -1335,12 +1488,17 @@ ${SWARALIPI_STAFF_CSS}
|
|
|
1335
1488
|
${SWARALIPI_PRINT_CSS}
|
|
1336
1489
|
${watermarkCss(watermark)}
|
|
1337
1490
|
html, body { margin: 0; }
|
|
1338
|
-
|
|
1491
|
+
/* The printable width of A4 at the @page margin above. The sheet is one grid
|
|
1492
|
+
with one natural width, so the fit script needs a real page-sized box to
|
|
1493
|
+
measure against \u2014 the iframe's own width is not it. */
|
|
1494
|
+
body { width: 178mm; color: #111; background: #fff; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
1339
1495
|
.sw-sheet { max-width: 100%; }
|
|
1340
1496
|
.sw-footer { margin-top: 12px; padding-top: 6px; border-top: 1px solid #ddd; font-size: 12px; color: #555; text-align: right; }
|
|
1341
1497
|
</style>
|
|
1342
1498
|
</head>
|
|
1343
|
-
<body>${top}${bodyHtml}${bottom}
|
|
1499
|
+
<body>${top}${bodyHtml}${bottom}
|
|
1500
|
+
<script>window.__swFit=function(){(${fitSwaralipi.toString()})(document,0.45)};window.__swFit()</script>
|
|
1501
|
+
</body>
|
|
1344
1502
|
</html>`;
|
|
1345
1503
|
}
|
|
1346
1504
|
function renderPrintDocument(doc, opts = {}) {
|
|
@@ -1383,6 +1541,7 @@ function printHtmlInIframe(html) {
|
|
|
1383
1541
|
if (printed) return;
|
|
1384
1542
|
printed = true;
|
|
1385
1543
|
try {
|
|
1544
|
+
win.__swFit?.();
|
|
1386
1545
|
win.focus();
|
|
1387
1546
|
win.print();
|
|
1388
1547
|
} catch {
|
|
@@ -1438,6 +1597,9 @@ function createElementClass() {
|
|
|
1438
1597
|
#translitLang;
|
|
1439
1598
|
#renderSeq = 0;
|
|
1440
1599
|
#staffOptions = {};
|
|
1600
|
+
#resize;
|
|
1601
|
+
#fittedAt = -1;
|
|
1602
|
+
#zoom = 1;
|
|
1441
1603
|
validate = false;
|
|
1442
1604
|
constructor() {
|
|
1443
1605
|
super();
|
|
@@ -1487,9 +1649,46 @@ function createElementClass() {
|
|
|
1487
1649
|
connectedCallback() {
|
|
1488
1650
|
this.#ensureTransliterator();
|
|
1489
1651
|
this.#render();
|
|
1652
|
+
if (this.#resize === void 0 && typeof ResizeObserver !== "undefined") {
|
|
1653
|
+
this.#resize = new ResizeObserver(([entry]) => {
|
|
1654
|
+
const w = entry?.contentRect.width ?? -1;
|
|
1655
|
+
if (w === this.#fittedAt) return;
|
|
1656
|
+
this.#fittedAt = w;
|
|
1657
|
+
this.#fit();
|
|
1658
|
+
});
|
|
1659
|
+
this.#resize.observe(this);
|
|
1660
|
+
}
|
|
1490
1661
|
}
|
|
1491
1662
|
disconnectedCallback() {
|
|
1492
1663
|
this.#abort?.abort();
|
|
1664
|
+
this.#resize?.disconnect();
|
|
1665
|
+
this.#resize = void 0;
|
|
1666
|
+
}
|
|
1667
|
+
/** Scale the notation to fit the available width, times the zoom factor. */
|
|
1668
|
+
#fit() {
|
|
1669
|
+
fitSwaralipi(this.#root, 0, this.#zoom);
|
|
1670
|
+
}
|
|
1671
|
+
/** Re-fit the current render (public for hosts that resize the sheet
|
|
1672
|
+
* themselves, e.g. a tab that was hidden while the document loaded). */
|
|
1673
|
+
fit() {
|
|
1674
|
+
this.#fittedAt = -1;
|
|
1675
|
+
this.#fit();
|
|
1676
|
+
}
|
|
1677
|
+
get zoom() {
|
|
1678
|
+
return this.#zoom;
|
|
1679
|
+
}
|
|
1680
|
+
set zoom(factor) {
|
|
1681
|
+
this.#zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, factor));
|
|
1682
|
+
this.#fit();
|
|
1683
|
+
}
|
|
1684
|
+
zoomIn() {
|
|
1685
|
+
this.zoom = this.#zoom * ZOOM_STEP;
|
|
1686
|
+
}
|
|
1687
|
+
zoomOut() {
|
|
1688
|
+
this.zoom = this.#zoom / ZOOM_STEP;
|
|
1689
|
+
}
|
|
1690
|
+
resetZoom() {
|
|
1691
|
+
this.zoom = 1;
|
|
1493
1692
|
}
|
|
1494
1693
|
async #load(url) {
|
|
1495
1694
|
this.#abort?.abort();
|
|
@@ -1521,6 +1720,7 @@ function createElementClass() {
|
|
|
1521
1720
|
}
|
|
1522
1721
|
#render() {
|
|
1523
1722
|
const seq = ++this.#renderSeq;
|
|
1723
|
+
this.#zoom = 1;
|
|
1524
1724
|
if (this.#doc === void 0) {
|
|
1525
1725
|
this.#root.innerHTML = this.#styleFallback();
|
|
1526
1726
|
return;
|
|
@@ -1539,6 +1739,7 @@ function createElementClass() {
|
|
|
1539
1739
|
transliterate: this.#translitLang === lang ? this.#translit : void 0
|
|
1540
1740
|
});
|
|
1541
1741
|
this.#root.innerHTML = this.#styleFallback() + html;
|
|
1742
|
+
this.#fit();
|
|
1542
1743
|
}
|
|
1543
1744
|
#renderError(message) {
|
|
1544
1745
|
const div = document.createElement("div");
|
|
@@ -1588,18 +1789,23 @@ exports.DEFAULT_WATERMARK = DEFAULT_WATERMARK;
|
|
|
1588
1789
|
exports.DIGITS = DIGITS;
|
|
1589
1790
|
exports.LABELS = LABELS;
|
|
1590
1791
|
exports.MARKS = MARKS;
|
|
1792
|
+
exports.MAX_ZOOM = MAX_ZOOM;
|
|
1793
|
+
exports.MIN_ZOOM = MIN_ZOOM;
|
|
1591
1794
|
exports.NOTATION_FONT_URLS = NOTATION_FONT_URLS;
|
|
1592
1795
|
exports.SWARALIPI_CSS = SWARALIPI_CSS;
|
|
1593
1796
|
exports.SWARALIPI_PRINT_CSS = SWARALIPI_PRINT_CSS;
|
|
1594
1797
|
exports.SWARALIPI_STAFF_CSS = SWARALIPI_STAFF_CSS;
|
|
1798
|
+
exports.ZOOM_STEP = ZOOM_STEP;
|
|
1595
1799
|
exports.abcDuration = abcDuration;
|
|
1596
1800
|
exports.abcToSvg = abcToSvg;
|
|
1801
|
+
exports.alignRows = alignRows;
|
|
1597
1802
|
exports.buildAbc = buildAbc;
|
|
1598
1803
|
exports.buildTaalAbc = buildTaalAbc;
|
|
1599
1804
|
exports.cellMatra = cellMatra;
|
|
1600
1805
|
exports.composeCluster = composeCluster;
|
|
1601
1806
|
exports.defineSwaralipiSheet = defineSwaralipiSheet;
|
|
1602
1807
|
exports.docToAbc = docToAbc;
|
|
1808
|
+
exports.fitSwaralipi = fitSwaralipi;
|
|
1603
1809
|
exports.groupRowIntoCells = groupRowIntoCells;
|
|
1604
1810
|
exports.loadLyricTransliterator = loadLyricTransliterator;
|
|
1605
1811
|
exports.localizeDigits = localizeDigits;
|