@ugfoundation/swaralipi-js 0.8.1 → 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.1";
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,25 +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 openL = arc.openLeft || open;
866
- const openR = arc.openRight || open;
867
- const fill = '<i class="sw-arc-m"></i>';
868
- const hookL = openL ? "" : '<i class="sw-arc-l">w</i>';
869
- const hookR = openR ? "" : '<i class="sw-arc-r">x</i>';
870
- const piece = (col, cls, inner) => `<span class="sw-arc${cls}" style="grid-column:${col}">${inner}</span>`;
871
- const f = offset + arc.start + COL_BASE;
872
- const t = offset + arc.end + COL_BASE;
873
- const openCls = (openL ? " sw-arc--openl" : "") + (openR ? " sw-arc--openr" : "");
874
- if (t === f) return piece(`${f}`, openCls, hookL + fill + hookR);
875
- const head = piece(`${f}`, openL ? " sw-arc--openl" : " sw-arc--head", hookL + fill);
876
- const tail = piece(`${t}`, openR ? " sw-arc--openr" : " sw-arc--tail", fill + hookR);
877
- const mid = t > f + 1 ? piece(`${f + 1} / ${t}`, " sw-arc--mid", fill) : "";
878
- return head + mid + tail;
879
- }
880
914
  function renderRow(row, partIdx, rowIdx, offset, ctx) {
881
915
  const cells = groupRowIntoCells(row.notes);
916
+ const ink = arcInk(cells, resolveArcs(cells));
882
917
  const out = [];
883
918
  if (row.label !== void 0)
884
919
  out.push(`<span class="sw-label">${escapeHtml(ctx.tr(row.label))}</span>`);
@@ -888,11 +923,10 @@ function renderRow(row, partIdx, rowIdx, offset, ctx) {
888
923
  const barCls = cell.kind === "bar" ? " sw-bar-cell" : "";
889
924
  const notes = cell.notes.map((note, j) => {
890
925
  const path = `${partIdx}.${rowIdx}.${cell.start + j}`;
891
- const cls = isPitched(note) ? " sw-p" : "";
892
926
  const bar = note.bar !== void 0 ? ` sw-bar sw-bar--${note.bar}` : "";
893
- 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>`;
894
928
  }).join("");
895
- 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>`);
896
930
  const slices = cellLyricSlices(cell);
897
931
  if (slices !== void 0) {
898
932
  const html = slices.map((t) => `<span>${escapeHtml(lyricText(t, ctx))}</span>`).join("");
@@ -905,7 +939,6 @@ function renderRow(row, partIdx, rowIdx, offset, ctx) {
905
939
  if (below.length > 0)
906
940
  out.push(`<span class="sw-below" style="grid-column:${col}">${renderAnnotation(below, ctx)}</span>`);
907
941
  });
908
- for (const arc of resolveArcs(cells)) out.push(renderArc(arc, offset));
909
942
  return `<div class="sw-row">${out.join("")}</div>`;
910
943
  }
911
944
  function renderHeader(meta, ctx) {
@@ -963,7 +996,7 @@ function fitSwaralipi(root, minScale = 0, zoom = 1, minSpacing = 0.35, minSize =
963
996
  sheet.style.removeProperty("--sw-size");
964
997
  sheet.style.removeProperty("--sw-tight");
965
998
  const available = scroll.clientWidth;
966
- let natural = block.scrollWidth;
999
+ let natural = block.offsetWidth;
967
1000
  if (available === 0 || natural === 0) continue;
968
1001
  if (natural > available && minSpacing < 1) {
969
1002
  let loose = minSpacing;
@@ -971,11 +1004,11 @@ function fitSwaralipi(root, minScale = 0, zoom = 1, minSpacing = 0.35, minSize =
971
1004
  for (let i = 0; i < 6; i++) {
972
1005
  const mid = (loose + tight) / 2;
973
1006
  sheet.style.setProperty("--sw-tight", String(mid));
974
- if (block.scrollWidth <= available) loose = mid;
1007
+ if (block.offsetWidth <= available) loose = mid;
975
1008
  else tight = mid;
976
1009
  }
977
1010
  sheet.style.setProperty("--sw-tight", String(loose));
978
- natural = block.scrollWidth;
1011
+ natural = block.offsetWidth;
979
1012
  }
980
1013
  const base = parseFloat(getComputedStyle(sheet).fontSize);
981
1014
  if (!(base > 0)) continue;
@@ -1073,31 +1106,45 @@ ${FONT_FACES}
1073
1106
  column-gap: calc(var(--sw-cell-gap, 0.4em) * var(--sw-tight, 1));
1074
1107
  align-items: end;
1075
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;
1076
1112
  }
1077
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. */
1078
1119
  .sw-row {
1079
1120
  grid-column: 1 / -1;
1080
1121
  display: grid;
1081
1122
  grid-template-columns: subgrid;
1082
- grid-template-rows: auto auto auto auto;
1123
+ grid-template-rows: 0.95em 1.35em 0.4em 1.35em;
1083
1124
  align-items: end;
1084
- padding: 0.4em 0;
1125
+ padding-bottom: 0.35em;
1126
+ line-height: 1.35;
1085
1127
  }
1086
1128
  .sw-label { grid-column: 1; grid-row: 2; padding-right: 0.5em; font-weight: 600; }
1087
1129
 
1088
- /* One column per cell; sub-notes are equal slices of it (min-width:0 kills
1089
- * flex's min-content floor, else a wide sub-note steals width from its
1090
- * neighbours while the syllable below it does not, and the two bands drift). */
1091
- .sw-cell, .sw-lyr { display: flex; justify-content: center; }
1092
- .sw-cell > .sw-n { white-space: nowrap; }
1093
- .sw-cell > .sw-n.sw-p,
1094
- .sw-lyr > span { flex: 1 1 0; min-width: 0; text-align: center; white-space: nowrap; }
1095
- .sw-cell { grid-row: 2; }
1096
- .sw-above { grid-row: 1; font-size: 0.62em; justify-self: center; white-space: nowrap; }
1097
- .sw-arc,
1098
- .sw-below { grid-row: 3; }
1099
- .sw-lyr { grid-row: 4; padding-top: 0.3em; }
1100
- .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; }
1101
1148
 
1102
1149
  /* ---- notation clusters & marks ---- */
1103
1150
  .sw-cl { position: relative; display: inline-block; }
@@ -1118,12 +1165,16 @@ ${FONT_FACES}
1118
1165
  .sw-ati-taar::before { top: -0.3em; }
1119
1166
  .sw-ati-mandra::before { bottom: -0.35em; }
1120
1167
 
1121
- /* 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). */
1122
1172
  .sw-kan,
1123
1173
  .sw-resh {
1124
1174
  position: relative;
1125
1175
  display: inline-block;
1126
1176
  font-size: 0.58em;
1177
+ line-height: 0;
1127
1178
  vertical-align: 0.75em;
1128
1179
  }
1129
1180
  .sw-kan { margin-right: 0.08em; }
@@ -1155,7 +1206,7 @@ ${FONT_FACES}
1155
1206
  .sw-cut::before { top: 0.2em; }
1156
1207
  .sw-cut::after { top: 0.55em; }
1157
1208
  .sw-br { opacity: 0.8; padding: 0 0.06em; }
1158
- .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; }
1159
1210
 
1160
1211
  /* raw fallback: never dropped, visibly non-notation. Swarabitan first \u2014
1161
1212
  * raw IS that font's encoding, so undecoded cells show NLTR's own glyphs
@@ -1175,49 +1226,46 @@ ${FONT_FACES}
1175
1226
  /* a bar owns a whole column but needs no matra of width */
1176
1227
  .sw-bar-cell { min-width: 0; }
1177
1228
 
1178
- /* ---- \u09AE\u09C0\u09A1\u09BC under-arcs (from w/x segments and spec meend) ----
1179
- * Swarabitan draws the arc as three pieces \u2014 'w' the opening hook, 'y' a
1180
- * flat fill, 'x' the closing hook. We use the font's own hooks and stretch
1181
- * the fill as a CSS bar (the 'y' glyph IS a plain rectangle), so one arc
1182
- * spans any number of columns. An open side = the arc continues onto the
1183
- * next system: drop that hook and leave the fill squared off. */
1184
- .sw-arc {
1185
- display: flex;
1186
- align-items: baseline;
1187
- line-height: 1;
1188
- font-family: Swarabitan, var(--sw-font, sans-serif);
1189
- /* The 0.9em sits on the children, not here: the end pieces below size
1190
- themselves off the block's own column-gap, which is in the block's em. */
1191
- }
1192
- .sw-arc > i, .sw-arc::before { font-size: 0.9em; }
1193
- .sw-arc-l, .sw-arc-r { flex: none; font-style: normal; }
1194
- /* Every piece needs the same baseline \u2014 the fill rides it, so a piece holding
1195
- only fill would otherwise drop to the bottom of the row. A zero-width copy
1196
- of the hook glyph is the strut (same font, same metrics). */
1197
- .sw-arc::before { content: 'w'; flex: none; width: 0; overflow: hidden; }
1198
- /* The ends hang from the two notes' CENTRES, as the print sets them, so the
1199
- arc is three grid items: each end piece takes half of its own column plus
1200
- the column-gap it has to bridge, and the negative margin on the same side
1201
- lets that bridge overhang the grid area instead of shifting the piece. */
1202
- .sw-arc--head, .sw-arc--tail {
1203
- --sw-arc-gap: calc(var(--sw-cell-gap, 0.4em) * var(--sw-tight, 1));
1204
- width: calc(50% + var(--sw-arc-gap));
1205
- }
1206
- .sw-arc--head { justify-self: end; margin-right: calc(var(--sw-arc-gap) * -1); }
1207
- .sw-arc--tail { justify-self: start; margin-left: calc(var(--sw-arc-gap) * -1);
1208
- justify-content: flex-end; }
1209
- /* The fill's geometry is measured off the font's own 'y' glyph: ink 0.11em
1210
- tall whose bottom sits 0.01em above the baseline. An empty inline-block
1211
- takes its bottom margin edge as its baseline, so -0.01em lifts the bar to
1212
- exactly where the hooks' flat stroke runs \u2014 no seam at either join. */
1213
- .sw-arc-m {
1214
- flex: 1 1 0;
1215
- min-width: 0;
1216
- align-self: baseline;
1217
- height: 0.11em;
1218
- margin-bottom: -0.01em;
1219
- background: currentColor;
1220
- }
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;
1249
+ font-size: 0.9em;
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; }
1221
1269
 
1222
1270
  /* ---- annotation content ---- */
1223
1271
  .sw-beat { position: relative; display: inline-block; padding: 0 0.1em; }