@ugfoundation/swaralipi-js 0.8.0 → 0.8.2

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.
@@ -260,7 +260,7 @@ var init_spec = __esm({
260
260
  init_grace();
261
261
  init_lay();
262
262
  init_svara();
263
- SPEC_VERSION = "0.8.0";
263
+ SPEC_VERSION = "0.8.2";
264
264
  }
265
265
  });
266
266
 
@@ -642,42 +642,64 @@ function alignRows(rows) {
642
642
  }
643
643
  function resolveArcs(cells) {
644
644
  const arcs = [];
645
- let openStart;
645
+ let open;
646
646
  let sawEnds = false;
647
647
  const fillCells = [];
648
- const openAt = (i) => {
648
+ const openAt = (cell, note) => {
649
649
  sawEnds = true;
650
- if (openStart === void 0) openStart = i;
650
+ open ??= { cell, note };
651
651
  };
652
- const closeAt = (i) => {
652
+ const closeAt = (cell, note) => {
653
653
  sawEnds = true;
654
- if (openStart !== void 0) {
655
- arcs.push({ start: openStart, end: i, openLeft: false, openRight: false });
656
- openStart = void 0;
657
- } else {
658
- arcs.push({ start: 0, end: i, openLeft: true, openRight: false });
659
- }
654
+ const from = open ?? { cell: 0, note: 0 };
655
+ arcs.push({
656
+ start: from.cell,
657
+ startNote: from.note,
658
+ end: cell,
659
+ endNote: note,
660
+ openLeft: open === void 0,
661
+ openRight: false
662
+ });
663
+ open = void 0;
660
664
  };
661
665
  cells.forEach((cell, i) => {
662
- for (const note of cell.notes) {
663
- if (note.meend === "start") openAt(i);
664
- else if (note.meend === "end") closeAt(i);
665
- for (const b of note.below ?? []) {
666
- if (!isArcRaw(b.raw)) continue;
667
- for (const ch of b.raw) {
668
- if (ch === "w" || ch === "W") openAt(i);
669
- else if (ch === "x" || ch === "X") closeAt(i);
670
- else fillCells.push(i);
671
- }
672
- }
666
+ const pitched = cell.notes.flatMap((n, j) => isPitched(n) ? [j] : []);
667
+ const first = pitched[0] ?? 0;
668
+ const last = pitched[pitched.length - 1] ?? cell.notes.length - 1;
669
+ let marks = "";
670
+ cell.notes.forEach((note, j) => {
671
+ if (note.meend === "start") openAt(i, j);
672
+ else if (note.meend === "end") closeAt(i, j);
673
+ for (const b of note.below ?? []) if (isArcRaw(b.raw)) marks += b.raw;
674
+ });
675
+ const reopens = /[xX].*[wW]/.test(marks);
676
+ for (const ch of marks) {
677
+ if (ch === "w" || ch === "W") openAt(i, reopens ? last : first);
678
+ else if (ch === "x" || ch === "X") closeAt(i, reopens ? first : last);
679
+ else if (ch === "$") {
680
+ openAt(i, first);
681
+ closeAt(i, last);
682
+ } else fillCells.push(i);
673
683
  }
674
684
  });
675
- if (openStart !== void 0) {
676
- arcs.push({ start: openStart, end: Math.max(cells.length - 1, 0), openLeft: false, openRight: true });
685
+ const lastCell = Math.max(cells.length - 1, 0);
686
+ const lastNote = (c) => Math.max((cells[c]?.notes.length ?? 1) - 1, 0);
687
+ if (open !== void 0) {
688
+ arcs.push({
689
+ start: open.cell,
690
+ startNote: open.note,
691
+ end: lastCell,
692
+ endNote: lastNote(lastCell),
693
+ openLeft: false,
694
+ openRight: true
695
+ });
677
696
  } else if (!sawEnds && fillCells.length > 0) {
697
+ const end = Math.max(...fillCells);
678
698
  arcs.push({
679
699
  start: Math.min(...fillCells),
680
- end: Math.max(...fillCells),
700
+ startNote: 0,
701
+ end,
702
+ endNote: lastNote(end),
681
703
  openLeft: true,
682
704
  openRight: true,
683
705
  line: true
@@ -685,6 +707,35 @@ function resolveArcs(cells) {
685
707
  }
686
708
  return arcs;
687
709
  }
710
+ function arcInk(cells, arcs) {
711
+ const ink = { cells: cells.map(() => ""), notes: cells.map((c) => c.notes.map(() => "")) };
712
+ const mark = (c, j, cls, clip = false) => {
713
+ ink.notes[c][j] += ` ${cls}`;
714
+ if (clip && !ink.cells[c].includes(" sw-mk")) ink.cells[c] += " sw-mk";
715
+ };
716
+ for (const arc of arcs) {
717
+ const { start: s, end: e, startNote: j, endNote: k } = arc;
718
+ const hookL = arc.line !== true && !arc.openLeft;
719
+ const hookR = arc.line !== true && !arc.openRight;
720
+ if (s === e && hookL && hookR) {
721
+ if (j === k) {
722
+ mark(s, j, "sw-mos");
723
+ mark(s, j, "sw-mcs");
724
+ } else {
725
+ mark(s, j, "sw-mob");
726
+ for (let n = j + 1; n < k; n++) mark(s, n, "sw-mf");
727
+ mark(s, k, "sw-mcb");
728
+ }
729
+ continue;
730
+ }
731
+ for (let c = s; c <= e; c++) {
732
+ if (c === s && hookL) mark(c, j, "sw-mo", true);
733
+ else if (c === e && hookR) mark(c, k, "sw-mc", true);
734
+ else mark(c, 0, "sw-mm", true);
735
+ }
736
+ }
737
+ return ink;
738
+ }
688
739
  var SVARA_CHAR = /[srgmpqndtkuv]/;
689
740
  function rawMarks(raw, count) {
690
741
  if (raw === void 0) return void 0;
@@ -860,21 +911,9 @@ function renderNote(note, kind, opts, ctx) {
860
911
  const raw = rawDisplay(note);
861
912
  return bracketGlyphs(note, "open") + (raw !== void 0 ? `<span class="sw-raw" title="${escapeHtml(raw)}">${escapeHtml(raw)}</span>` : "") + bracketGlyphs(note, "close");
862
913
  }
863
- function renderArc(arc, offset) {
864
- const open = arc.line === true;
865
- const cls = [
866
- "sw-arc",
867
- arc.openLeft || open ? "sw-arc--openl" : "",
868
- arc.openRight || open ? "sw-arc--openr" : ""
869
- ].filter(Boolean).join(" ");
870
- const left = open || arc.openLeft ? "" : '<i class="sw-arc-l">w</i>';
871
- const right = open || arc.openRight ? "" : '<i class="sw-arc-r">x</i>';
872
- const from = offset + arc.start + COL_BASE;
873
- const to = offset + arc.end + COL_BASE + 1;
874
- return `<span class="${cls}" style="grid-column:${from} / ${to}">${left}<i class="sw-arc-m"></i>${right}</span>`;
875
- }
876
914
  function renderRow(row, partIdx, rowIdx, offset, ctx) {
877
915
  const cells = groupRowIntoCells(row.notes);
916
+ const ink = arcInk(cells, resolveArcs(cells));
878
917
  const out = [];
879
918
  if (row.label !== void 0)
880
919
  out.push(`<span class="sw-label">${escapeHtml(ctx.tr(row.label))}</span>`);
@@ -884,11 +923,10 @@ function renderRow(row, partIdx, rowIdx, offset, ctx) {
884
923
  const barCls = cell.kind === "bar" ? " sw-bar-cell" : "";
885
924
  const notes = cell.notes.map((note, j) => {
886
925
  const path = `${partIdx}.${rowIdx}.${cell.start + j}`;
887
- const cls = isPitched(note) ? " sw-p" : "";
888
926
  const bar = note.bar !== void 0 ? ` sw-bar sw-bar--${note.bar}` : "";
889
- return `<span class="sw-n${cls}${bar}" data-note="${path}">${renderNote(note, cell.kind, opts[j], ctx)}</span>`;
927
+ return `<span class="sw-n${bar}${ink.notes[ci][j]}" data-note="${path}">${renderNote(note, cell.kind, opts[j], ctx)}</span>`;
890
928
  }).join("");
891
- out.push(`<span class="sw-cell${barCls}" style="grid-column:${col}">${notes}</span>`);
929
+ out.push(`<span class="sw-cell${barCls}${ink.cells[ci]}" style="grid-column:${col}">${notes}</span>`);
892
930
  const slices = cellLyricSlices(cell);
893
931
  if (slices !== void 0) {
894
932
  const html = slices.map((t) => `<span>${escapeHtml(lyricText(t, ctx))}</span>`).join("");
@@ -901,7 +939,6 @@ function renderRow(row, partIdx, rowIdx, offset, ctx) {
901
939
  if (below.length > 0)
902
940
  out.push(`<span class="sw-below" style="grid-column:${col}">${renderAnnotation(below, ctx)}</span>`);
903
941
  });
904
- for (const arc of resolveArcs(cells)) out.push(renderArc(arc, offset));
905
942
  return `<div class="sw-row">${out.join("")}</div>`;
906
943
  }
907
944
  function renderHeader(meta, ctx) {
@@ -959,7 +996,7 @@ function fitSwaralipi(root, minScale = 0, zoom = 1, minSpacing = 0.35, minSize =
959
996
  sheet.style.removeProperty("--sw-size");
960
997
  sheet.style.removeProperty("--sw-tight");
961
998
  const available = scroll.clientWidth;
962
- let natural = block.scrollWidth;
999
+ let natural = block.offsetWidth;
963
1000
  if (available === 0 || natural === 0) continue;
964
1001
  if (natural > available && minSpacing < 1) {
965
1002
  let loose = minSpacing;
@@ -967,11 +1004,11 @@ function fitSwaralipi(root, minScale = 0, zoom = 1, minSpacing = 0.35, minSize =
967
1004
  for (let i = 0; i < 6; i++) {
968
1005
  const mid = (loose + tight) / 2;
969
1006
  sheet.style.setProperty("--sw-tight", String(mid));
970
- if (block.scrollWidth <= available) loose = mid;
1007
+ if (block.offsetWidth <= available) loose = mid;
971
1008
  else tight = mid;
972
1009
  }
973
1010
  sheet.style.setProperty("--sw-tight", String(loose));
974
- natural = block.scrollWidth;
1011
+ natural = block.offsetWidth;
975
1012
  }
976
1013
  const base = parseFloat(getComputedStyle(sheet).fontSize);
977
1014
  if (!(base > 0)) continue;
@@ -1069,31 +1106,45 @@ ${FONT_FACES}
1069
1106
  column-gap: calc(var(--sw-cell-gap, 0.4em) * var(--sw-tight, 1));
1070
1107
  align-items: end;
1071
1108
  width: max-content;
1109
+ /* The last column's cell spacers overhang half a gap past the grid; clip
1110
+ that sideways so it neither counts as width to fit nor raises a scrollbar. */
1111
+ overflow-x: clip;
1072
1112
  }
1073
1113
  .sw-part { display: contents; }
1114
+ /* Four FIXED bands per row \u2014 above / notation / arc / lyric \u2014 so a beat label,
1115
+ * a \u0995\u09A3 or a \u09AE\u09C0\u09A1\u09BC never changes how far a line sits from its words: the
1116
+ * notation hugs its lyric and the next system starts a little further off,
1117
+ * as the printed \u09B8\u09CD\u09AC\u09B0\u09AC\u09BF\u09A4\u09BE\u09A8 sets it. Content taller than its band overflows
1118
+ * rather than pushing the rhythm apart. */
1074
1119
  .sw-row {
1075
1120
  grid-column: 1 / -1;
1076
1121
  display: grid;
1077
1122
  grid-template-columns: subgrid;
1078
- grid-template-rows: auto auto auto auto;
1123
+ grid-template-rows: 0.95em 1.35em 0.4em 1.35em;
1079
1124
  align-items: end;
1080
- padding: 0.4em 0;
1125
+ padding-bottom: 0.35em;
1126
+ line-height: 1.35;
1081
1127
  }
1082
1128
  .sw-label { grid-column: 1; grid-row: 2; padding-right: 0.5em; font-weight: 600; }
1083
1129
 
1084
- /* One column per cell; sub-notes are equal slices of it (min-width:0 kills
1085
- * flex's min-content floor, else a wide sub-note steals width from its
1086
- * neighbours while the syllable below it does not, and the two bands drift). */
1087
- .sw-cell, .sw-lyr { display: flex; justify-content: center; }
1088
- .sw-cell > .sw-n { white-space: nowrap; }
1089
- .sw-cell > .sw-n.sw-p,
1090
- .sw-lyr > span { flex: 1 1 0; min-width: 0; text-align: center; white-space: nowrap; }
1091
- .sw-cell { grid-row: 2; }
1092
- .sw-above { grid-row: 1; font-size: 0.62em; justify-self: center; white-space: nowrap; }
1093
- .sw-arc,
1094
- .sw-below { grid-row: 3; }
1095
- .sw-lyr { grid-row: 4; padding-top: 0.3em; }
1096
- .sw-below { font-size: 0.62em; justify-self: center; white-space: nowrap; }
1130
+ /* One column per cell, its notes set tight as one word (\u09B0\u09B8\u09BE) and centred by
1131
+ * two flex spacers. Each spacer overhangs half the column gap (a min-width
1132
+ * paid back by a negative margin, so the column is sized by the notes alone),
1133
+ * which lets neighbouring cells' spacers meet mid-gap: a \u09AE\u09C0\u09A1\u09BC crossing cells is
1134
+ * inked by these boxes with no hole between them. */
1135
+ .sw-cell, .sw-lyr { display: flex; justify-content: center; white-space: nowrap; }
1136
+ .sw-cell {
1137
+ --sw-half-gap: calc(var(--sw-cell-gap, 0.4em) * var(--sw-tight, 1) / 2);
1138
+ grid-row: 2;
1139
+ align-self: stretch;
1140
+ }
1141
+ .sw-cell::before, .sw-cell::after { content: ''; flex: 1 0 0; min-width: var(--sw-half-gap); }
1142
+ .sw-cell::before { margin-left: calc(var(--sw-half-gap) * -1); }
1143
+ .sw-cell::after { margin-right: calc(var(--sw-half-gap) * -1); }
1144
+ .sw-cell > .sw-n { position: relative; }
1145
+ .sw-above { grid-row: 1; font-size: 0.62em; line-height: 1; justify-self: center; white-space: nowrap; }
1146
+ .sw-below { grid-row: 3; font-size: 0.62em; line-height: 1; justify-self: center; white-space: nowrap; }
1147
+ .sw-lyr { grid-row: 4; }
1097
1148
 
1098
1149
  /* ---- notation clusters & marks ---- */
1099
1150
  .sw-cl { position: relative; display: inline-block; }
@@ -1114,12 +1165,16 @@ ${FONT_FACES}
1114
1165
  .sw-ati-taar::before { top: -0.3em; }
1115
1166
  .sw-ati-mandra::before { bottom: -0.35em; }
1116
1167
 
1117
- /* grace notes: small raised clusters \u2014 \u0995\u09A3 before the note, \u09B0\u09C7\u09B6 after it */
1168
+ /* grace notes: small raised clusters \u2014 \u0995\u09A3 before the note, \u09B0\u09C7\u09B6 after it.
1169
+ * line-height 0 (here and on .sw-micro): a raised inline box taller than the
1170
+ * strut grows its line box and drops that one note's baseline below its
1171
+ * neighbours' (css2 \xA710.8). */
1118
1172
  .sw-kan,
1119
1173
  .sw-resh {
1120
1174
  position: relative;
1121
1175
  display: inline-block;
1122
1176
  font-size: 0.58em;
1177
+ line-height: 0;
1123
1178
  vertical-align: 0.75em;
1124
1179
  }
1125
1180
  .sw-kan { margin-right: 0.08em; }
@@ -1151,7 +1206,7 @@ ${FONT_FACES}
1151
1206
  .sw-cut::before { top: 0.2em; }
1152
1207
  .sw-cut::after { top: 0.55em; }
1153
1208
  .sw-br { opacity: 0.8; padding: 0 0.06em; }
1154
- .sw-micro { font-size: 0.6em; vertical-align: super; opacity: 0.8; }
1209
+ .sw-micro { font-size: 0.6em; line-height: 0; vertical-align: super; opacity: 0.8; }
1155
1210
 
1156
1211
  /* raw fallback: never dropped, visibly non-notation. Swarabitan first \u2014
1157
1212
  * raw IS that font's encoding, so undecoded cells show NLTR's own glyphs
@@ -1171,32 +1226,46 @@ ${FONT_FACES}
1171
1226
  /* a bar owns a whole column but needs no matra of width */
1172
1227
  .sw-bar-cell { min-width: 0; }
1173
1228
 
1174
- /* ---- \u09AE\u09C0\u09A1\u09BC under-arcs (from w/x segments and spec meend) ----
1175
- * Swarabitan draws the arc as three pieces \u2014 'w' the opening hook, 'y' a
1176
- * flat fill, 'x' the closing hook. We use the font's own hooks and stretch
1177
- * the fill as a CSS bar (the 'y' glyph IS a plain rectangle), so one arc
1178
- * spans any number of columns. An open side = the arc continues onto the
1179
- * next system: drop that hook and leave the fill squared off. */
1180
- .sw-arc {
1181
- display: flex;
1182
- align-items: baseline;
1183
- line-height: 1;
1184
- font-family: Swarabitan, var(--sw-font, sans-serif);
1229
+ /* ---- \u09AE\u09C0\u09A1\u09BC under-arcs (from w/x/$ segments and spec meend) ----
1230
+ * Swarabitan's own arc, all langs: w is the opening hook, curling down from
1231
+ * its tip into a flat, x its mirror, y that flat alone \u2014 and they join
1232
+ * hole-free, so one run of text is a whole end (tools/align-check/glyph.html).
1233
+ * Each end sets its hook tip on its note's centre and runs the flat toward the
1234
+ * other end (render.ts arcInk): across cells it is cut at the mid-gap, where
1235
+ * the next cell's run carries on (.sw-mk clips a cell half a gap past each
1236
+ * side, as far as its spacers reach); inside one cell, at its note's edge. So
1237
+ * a long arc extends the flat and a short one trims it off. line-height 0 sets
1238
+ * every run's baseline the same distance below its note. The clip reaches
1239
+ * 0.5px further so two cells' anti-aliased edges overlap instead of leaving a
1240
+ * hairline seam between their identical flats. */
1241
+ .sw-mk { clip-path: inset(-3em calc(var(--sw-half-gap) * -1 - 0.5px)); }
1242
+ .sw-mo::after, .sw-mob::after, .sw-mos::after,
1243
+ .sw-mc::before, .sw-mcb::before, .sw-mcs::before,
1244
+ .sw-mf::before, .sw-mm::before {
1245
+ content: 'yyyyyyyyyyyy';
1246
+ position: absolute;
1247
+ top: calc(100% + 0.05em);
1248
+ font-family: Swarabitan;
1185
1249
  font-size: 0.9em;
1186
- }
1187
- .sw-arc-l, .sw-arc-r { flex: none; font-style: normal; }
1188
- /* The fill's geometry is measured off the font's own 'y' glyph: ink 0.11em
1189
- tall whose bottom sits 0.01em above the baseline. An empty inline-block
1190
- takes its bottom margin edge as its baseline, so -0.01em lifts the bar to
1191
- exactly where the hooks' flat stroke runs \u2014 no seam at either join. */
1192
- .sw-arc-m {
1193
- flex: 1 1 0;
1194
- min-width: 0;
1195
- align-self: baseline;
1196
- height: 0.11em;
1197
- margin-bottom: -0.01em;
1198
- background: currentColor;
1199
- }
1250
+ line-height: 0;
1251
+ white-space: pre;
1252
+ overflow-x: clip;
1253
+ }
1254
+ .sw-mo::after, .sw-mob::after, .sw-mos::after { content: 'wyyyyyyyyyyyy'; }
1255
+ /* rtl: a closing run cut short keeps its hook on its right edge */
1256
+ .sw-mc::before, .sw-mcb::before, .sw-mcs::before { content: 'yyyyyyyyyyyyx'; direction: rtl; }
1257
+ .sw-mo::after, .sw-mob::after { left: 50%; }
1258
+ .sw-mc::before, .sw-mcb::before { right: 50%; }
1259
+ .sw-mos::after { left: 0; }
1260
+ .sw-mcs::before { right: 0; }
1261
+ .sw-mf::before { left: 0; right: 0; }
1262
+ .sw-mm::before { left: -12em; }
1263
+ /* Inside one cell a hook gets half a note to curl in, about a third of its
1264
+ * curl: squeezed to half width (same stroke) it lands flat at the cut, so the
1265
+ * two flats meet level whatever the notes' widths \u2014 a small arc, not a V. */
1266
+ .sw-mob::after, .sw-mos::after, .sw-mcb::before, .sw-mcs::before { width: 100%; transform: scaleX(0.5); }
1267
+ .sw-mob::after, .sw-mos::after { transform-origin: left; }
1268
+ .sw-mcb::before, .sw-mcs::before { transform-origin: right; }
1200
1269
 
1201
1270
  /* ---- annotation content ---- */
1202
1271
  .sw-beat { position: relative; display: inline-block; padding: 0 0.1em; }