@ugfoundation/swaralipi-js 0.5.0 → 0.6.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.
Files changed (46) hide show
  1. package/dist/{batch-UETOBPHT.js → batch-E76HM4GM.js} +4 -4
  2. package/dist/{batch-UETOBPHT.js.map → batch-E76HM4GM.js.map} +1 -1
  3. package/dist/{chunk-UD2K4UIC.js → chunk-52EPQY47.js} +8 -6
  4. package/dist/chunk-52EPQY47.js.map +1 -0
  5. package/dist/{chunk-C6TRCMPL.js → chunk-GHWURC4U.js} +97 -5
  6. package/dist/chunk-GHWURC4U.js.map +1 -0
  7. package/dist/{chunk-ID44OP2N.js → chunk-MSVF43WQ.js} +7 -6
  8. package/dist/chunk-MSVF43WQ.js.map +1 -0
  9. package/dist/cli.cjs +11 -8
  10. package/dist/cli.cjs.map +1 -1
  11. package/dist/cli.js +2 -2
  12. package/dist/convert/index.cjs +10 -7
  13. package/dist/convert/index.cjs.map +1 -1
  14. package/dist/convert/index.d.cts +4 -3
  15. package/dist/convert/index.d.ts +4 -3
  16. package/dist/convert/index.js +2 -2
  17. package/dist/index.cjs +6 -4
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/player/index.cjs +212 -20
  23. package/dist/player/index.cjs.map +1 -1
  24. package/dist/player/index.d.cts +16 -4
  25. package/dist/player/index.d.ts +16 -4
  26. package/dist/player/index.js +111 -20
  27. package/dist/player/index.js.map +1 -1
  28. package/dist/{schedule-BaYIJf8y.d.ts → schedule-8WFUBJqB.d.ts} +78 -3
  29. package/dist/{schedule-bBvy18MO.d.cts → schedule-CjPcD-Hf.d.cts} +78 -3
  30. package/dist/{schema-D3wyNRqM.d.ts → schema-D_86x4oV.d.cts} +21 -6
  31. package/dist/{schema-D3wyNRqM.d.cts → schema-D_86x4oV.d.ts} +21 -6
  32. package/dist/spec/index.cjs +6 -4
  33. package/dist/spec/index.cjs.map +1 -1
  34. package/dist/spec/index.d.cts +3 -3
  35. package/dist/spec/index.d.ts +3 -3
  36. package/dist/spec/index.js +1 -1
  37. package/dist/viewer/index.cjs +264 -166
  38. package/dist/viewer/index.cjs.map +1 -1
  39. package/dist/viewer/index.d.cts +16 -23
  40. package/dist/viewer/index.d.ts +16 -23
  41. package/dist/viewer/index.js +99 -41
  42. package/dist/viewer/index.js.map +1 -1
  43. package/package.json +1 -1
  44. package/dist/chunk-C6TRCMPL.js.map +0 -1
  45. package/dist/chunk-ID44OP2N.js.map +0 -1
  46. package/dist/chunk-UD2K4UIC.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { O as Octave, a as SvaraLetter, N as Note, M as Meta, S as Swaralipi, T as Taal } from '../schema-D3wyNRqM.cjs';
2
- import { D as DrumVoice, S as Schedule, P as PlayEvent } from '../schedule-bBvy18MO.cjs';
1
+ import { O as Octave, a as SvaraLetter, N as Note, M as Meta, S as Swaralipi, T as Taal } from '../schema-D_86x4oV.cjs';
2
+ import { D as DrumVoice, S as Schedule, P as PlayEvent } from '../schedule-CjPcD-Hf.cjs';
3
3
  import 'zod';
4
4
  import 'tone';
5
5
  import '../taals-CjzBuZ35.cjs';
@@ -77,25 +77,6 @@ declare function composeCluster(letter: SvaraLetter, opts: ComposeOptions): Clus
77
77
  /** Map ASCII digits in `s` to the target script's digits. */
78
78
  declare function localizeDigits(s: string, lang: GridLang): string;
79
79
 
80
- /**
81
- * Pure spec-JSON → HTML renderer. No DOM, no side effects: usable for SSR
82
- * (Astro/static sites emit `renderToHtml(doc)` next to a
83
- * `<style>${SWARALIPI_CSS}</style>`) and unit-testable in plain Node.
84
- *
85
- * Layout model (mirrors NLTR's 4-row systems): each spec `Row` becomes one
86
- * CSS grid with **one column per top-level note** and four grid rows —
87
- * above-annotations / notation / below (slur arcs, marks) / lyric. Source
88
- * cells (groups of sub-notes sharing one matra) become column *spans*, so
89
- * per-sub-note lyric alignment and P2.3's per-note highlight targets fall
90
- * out of the column model for free.
91
- *
92
- * Rendering is spec-driven (semantics, not `raw`): grouping derives from
93
- * `durationMatra` accumulation so hand-authored documents without `raw`
94
- * render identically. `raw` is consulted only for (a) raw-only fallback
95
- * notes (never dropped — dimmed, in Swarabitan when loaded), (b) the ছেদ
96
- * cut mark and beat labels, whose grammar lives in raw (see codec.ts).
97
- */
98
-
99
80
  interface RenderOptions {
100
81
  /** Notation + UI language. Default `bn`. */
101
82
  lang?: ViewerLang;
@@ -119,7 +100,9 @@ interface Cell {
119
100
  * - bar / raw-only / carrier notes force-close and form singleton cells;
120
101
  * - full-matra notes are singleton groups;
121
102
  * - fractional notes accumulate until the sum reaches an integer matra
122
- * boundary (epsilon for 1/3, 0.2 chains), then the group closes.
103
+ * boundary (epsilon for 1/3, 0.2 chains), then the group closes;
104
+ * - `halfMatra` (the অর্ধমাত্রা ঃ) closes the cell on a half boundary — the
105
+ * only case durations alone cannot resolve (`সঃ গঃ` vs `সরা`).
123
106
  * The converter emits k notes × 1/k per source cell, so groups coincide
124
107
  * with source cells; an incomplete trailing group still closes at row end
125
108
  * (the matra carrier lands on its last note).
@@ -147,6 +130,16 @@ interface Arc {
147
130
  * A row with fill segments but no ends at all renders one straight line.
148
131
  */
149
132
  declare function resolveArcs(cells: Cell[], noteCount: number): Arc[];
133
+ /**
134
+ * The matra marks a cell's final note carries: its total duration as whole
135
+ * আ-কার matras plus an optional অর্ধমাত্রা ঃ (legend `রাঃ` = 1½, `সঃ` = ½).
136
+ * Only a `halfMatra` cell may show zero matras — every other cell keeps at
137
+ * least one, so an incomplete trailing group still gets its carrier.
138
+ */
139
+ declare function cellMatra(cell: Cell): {
140
+ matras: number;
141
+ half: boolean;
142
+ };
150
143
  /**
151
144
  * The নাম / তাল / রাগ / আবর্তন / স্বরলিপিকার header block, shared by the
152
145
  * akarmatrik grid and the staff view so both show the same metadata (localized
@@ -467,4 +460,4 @@ type Transliterator = (bengaliText: string) => string;
467
460
  */
468
461
  declare function loadLyricTransliterator(lang: ViewerLang): Promise<Transliterator | undefined>;
469
462
 
470
- export { type Arc, type Cell, type Cluster, type ComposeOptions, DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, type GridLang, LABELS, MARKS, NOTATION_FONT_URLS, type PrintOptions, type RenderOptions, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, type StaffOptions, type SwaralipiSheetElement, type TaalStaffOptions, type Transliterator, type ViewerLang, abcDuration, abcToSvg, buildAbc, buildTaalAbc, composeCluster, defineSwaralipiSheet, docToAbc, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderTaalStaffHtml, renderToHtml, resolveArcs };
463
+ export { type Arc, type Cell, type Cluster, type ComposeOptions, DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, type GridLang, LABELS, MARKS, NOTATION_FONT_URLS, type PrintOptions, type RenderOptions, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, type StaffOptions, type SwaralipiSheetElement, type TaalStaffOptions, type Transliterator, type ViewerLang, abcDuration, abcToSvg, buildAbc, buildTaalAbc, cellMatra, composeCluster, defineSwaralipiSheet, docToAbc, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderTaalStaffHtml, renderToHtml, resolveArcs };
@@ -1,5 +1,5 @@
1
- import { O as Octave, a as SvaraLetter, N as Note, M as Meta, S as Swaralipi, T as Taal } from '../schema-D3wyNRqM.js';
2
- import { D as DrumVoice, S as Schedule, P as PlayEvent } from '../schedule-BaYIJf8y.js';
1
+ import { O as Octave, a as SvaraLetter, N as Note, M as Meta, S as Swaralipi, T as Taal } from '../schema-D_86x4oV.js';
2
+ import { D as DrumVoice, S as Schedule, P as PlayEvent } from '../schedule-8WFUBJqB.js';
3
3
  import 'zod';
4
4
  import 'tone';
5
5
  import '../taals-CjzBuZ35.js';
@@ -77,25 +77,6 @@ declare function composeCluster(letter: SvaraLetter, opts: ComposeOptions): Clus
77
77
  /** Map ASCII digits in `s` to the target script's digits. */
78
78
  declare function localizeDigits(s: string, lang: GridLang): string;
79
79
 
80
- /**
81
- * Pure spec-JSON → HTML renderer. No DOM, no side effects: usable for SSR
82
- * (Astro/static sites emit `renderToHtml(doc)` next to a
83
- * `<style>${SWARALIPI_CSS}</style>`) and unit-testable in plain Node.
84
- *
85
- * Layout model (mirrors NLTR's 4-row systems): each spec `Row` becomes one
86
- * CSS grid with **one column per top-level note** and four grid rows —
87
- * above-annotations / notation / below (slur arcs, marks) / lyric. Source
88
- * cells (groups of sub-notes sharing one matra) become column *spans*, so
89
- * per-sub-note lyric alignment and P2.3's per-note highlight targets fall
90
- * out of the column model for free.
91
- *
92
- * Rendering is spec-driven (semantics, not `raw`): grouping derives from
93
- * `durationMatra` accumulation so hand-authored documents without `raw`
94
- * render identically. `raw` is consulted only for (a) raw-only fallback
95
- * notes (never dropped — dimmed, in Swarabitan when loaded), (b) the ছেদ
96
- * cut mark and beat labels, whose grammar lives in raw (see codec.ts).
97
- */
98
-
99
80
  interface RenderOptions {
100
81
  /** Notation + UI language. Default `bn`. */
101
82
  lang?: ViewerLang;
@@ -119,7 +100,9 @@ interface Cell {
119
100
  * - bar / raw-only / carrier notes force-close and form singleton cells;
120
101
  * - full-matra notes are singleton groups;
121
102
  * - fractional notes accumulate until the sum reaches an integer matra
122
- * boundary (epsilon for 1/3, 0.2 chains), then the group closes.
103
+ * boundary (epsilon for 1/3, 0.2 chains), then the group closes;
104
+ * - `halfMatra` (the অর্ধমাত্রা ঃ) closes the cell on a half boundary — the
105
+ * only case durations alone cannot resolve (`সঃ গঃ` vs `সরা`).
123
106
  * The converter emits k notes × 1/k per source cell, so groups coincide
124
107
  * with source cells; an incomplete trailing group still closes at row end
125
108
  * (the matra carrier lands on its last note).
@@ -147,6 +130,16 @@ interface Arc {
147
130
  * A row with fill segments but no ends at all renders one straight line.
148
131
  */
149
132
  declare function resolveArcs(cells: Cell[], noteCount: number): Arc[];
133
+ /**
134
+ * The matra marks a cell's final note carries: its total duration as whole
135
+ * আ-কার matras plus an optional অর্ধমাত্রা ঃ (legend `রাঃ` = 1½, `সঃ` = ½).
136
+ * Only a `halfMatra` cell may show zero matras — every other cell keeps at
137
+ * least one, so an incomplete trailing group still gets its carrier.
138
+ */
139
+ declare function cellMatra(cell: Cell): {
140
+ matras: number;
141
+ half: boolean;
142
+ };
150
143
  /**
151
144
  * The নাম / তাল / রাগ / আবর্তন / স্বরলিপিকার header block, shared by the
152
145
  * akarmatrik grid and the staff view so both show the same metadata (localized
@@ -467,4 +460,4 @@ type Transliterator = (bengaliText: string) => string;
467
460
  */
468
461
  declare function loadLyricTransliterator(lang: ViewerLang): Promise<Transliterator | undefined>;
469
462
 
470
- export { type Arc, type Cell, type Cluster, type ComposeOptions, DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, type GridLang, LABELS, MARKS, NOTATION_FONT_URLS, type PrintOptions, type RenderOptions, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, type StaffOptions, type SwaralipiSheetElement, type TaalStaffOptions, type Transliterator, type ViewerLang, abcDuration, abcToSvg, buildAbc, buildTaalAbc, composeCluster, defineSwaralipiSheet, docToAbc, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderTaalStaffHtml, renderToHtml, resolveArcs };
463
+ export { type Arc, type Cell, type Cluster, type ComposeOptions, DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, type GridLang, LABELS, MARKS, NOTATION_FONT_URLS, type PrintOptions, type RenderOptions, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, type StaffOptions, type SwaralipiSheetElement, type TaalStaffOptions, type Transliterator, type ViewerLang, abcDuration, abcToSvg, buildAbc, buildTaalAbc, cellMatra, composeCluster, defineSwaralipiSheet, docToAbc, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderTaalStaffHtml, renderToHtml, resolveArcs };
@@ -1,4 +1,4 @@
1
- import { buildSchedule, drumPattern } from '../chunk-C6TRCMPL.js';
1
+ import { isArcRaw, glideTargets, buildSchedule, drumPattern } from '../chunk-GHWURC4U.js';
2
2
  import { TAALS } from '../chunk-6ETPFHZ6.js';
3
3
 
4
4
  // src/viewer/glyphs.ts
@@ -67,7 +67,6 @@ function localizeDigits(s, lang) {
67
67
  }
68
68
 
69
69
  // src/viewer/render.ts
70
- var NBSP = "\xA0";
71
70
  var EPS = 1e-6;
72
71
  var isIntegral = (x) => Math.abs(x - Math.round(x)) < EPS;
73
72
  function escapeHtml(s) {
@@ -102,7 +101,7 @@ function groupRowIntoCells(notes) {
102
101
  return;
103
102
  }
104
103
  const d = note.durationMatra ?? 1;
105
- if (isIntegral(d)) {
104
+ if (isIntegral(d) && note.halfMatra !== true) {
106
105
  close();
107
106
  cells.push({ kind: "group", start: i, notes: [note] });
108
107
  return;
@@ -110,13 +109,11 @@ function groupRowIntoCells(notes) {
110
109
  cur ??= { kind: "group", start: i, notes: [] };
111
110
  cur.notes.push(note);
112
111
  acc += d;
113
- if (acc > EPS && isIntegral(acc)) close();
112
+ if (note.halfMatra === true || acc > EPS && isIntegral(acc)) close();
114
113
  });
115
114
  close();
116
115
  return cells;
117
116
  }
118
- var ARC_RAW_RE = /^[wWxXyYjJeEzZ$=_<>`\\]+$/;
119
- var isArcRaw = (raw) => raw !== void 0 && ARC_RAW_RE.test(raw);
120
117
  function resolveArcs(cells, noteCount) {
121
118
  const arcs = [];
122
119
  let openStart;
@@ -165,9 +162,22 @@ function resolveArcs(cells, noteCount) {
165
162
  }
166
163
  return arcs;
167
164
  }
168
- function cutMarkCount(raw) {
169
- if (raw === void 0) return 0;
170
- return (raw.match(/[i:]/g) ?? []).length;
165
+ var SVARA_CHAR = /[srgmpqndtkuv]/;
166
+ function rawMarks(raw, count) {
167
+ if (raw === void 0) return void 0;
168
+ const marks = { aakar: false, pre: "", post: new Array(count).fill("") };
169
+ const leads = SVARA_CHAR.test(raw);
170
+ let seen = 0;
171
+ for (const ch of raw) {
172
+ if (SVARA_CHAR.test(ch)) seen = Math.min(seen + 1, count);
173
+ else if (ch === "a") marks.aakar = true;
174
+ else if (ch === "i" || ch === ":") {
175
+ if (seen > 0) marks.post[seen - 1] += ch;
176
+ else if (leads) marks.pre += ch;
177
+ else marks.post[0] += ch;
178
+ }
179
+ }
180
+ return marks;
171
181
  }
172
182
  function parseBeatLabel(raw) {
173
183
  const m = raw.match(/^([0-9]{1,2})([fF])?:?$/);
@@ -185,38 +195,63 @@ function bracketGlyphs(note, edge) {
185
195
  function matraGlyph(ctx) {
186
196
  return ctx.lang === "bn" ? '<span class="sw-cl">a</span>' : '<span class="sw-iso">\u2013</span>';
187
197
  }
188
- function renderKan(note, ctx) {
189
- if (!note.kan) return "";
190
- const [octave, letter] = note.kan;
198
+ function renderGrace(svara, cls, ctx) {
199
+ if (svara === void 0) return "";
200
+ const [octave, letter] = svara;
191
201
  const c = composeCluster(letter, { octave, withMatra: false, lang: ctx.lang });
192
- return `<span class="${["sw-kan", ...c.classes].join(" ")}">${c.text}</span>`;
202
+ return `<span class="${[cls, ...c.classes].join(" ")}">${c.text}</span>`;
203
+ }
204
+ function cutGlyphs(marks, ctx) {
205
+ if (marks === "") return "";
206
+ 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);
207
+ }
208
+ function cellMatra(cell) {
209
+ const total = cell.notes.reduce((a, n) => a + (n.durationMatra ?? 1), 0);
210
+ const half = cell.notes.some((n) => n.halfMatra === true);
211
+ const whole = Math.floor(total + EPS);
212
+ return { matras: half ? whole : Math.max(1, whole), half };
213
+ }
214
+ function cellPitchedOpts(cell) {
215
+ const { matras, half } = cellMatra(cell);
216
+ const marks = rawMarks(cell.notes[0].raw, cell.notes.length);
217
+ const aakars = marks !== void 0 && !marks.aakar ? 0 : matras;
218
+ return cell.notes.map((_, j) => ({
219
+ matras: j === cell.notes.length - 1 ? aakars : 0,
220
+ pre: j === 0 ? marks?.pre ?? "" : "",
221
+ post: marks !== void 0 ? marks.post[j] : half && j === cell.notes.length - 1 ? "i" : ""
222
+ }));
193
223
  }
194
224
  function renderPitchedInner(note, opts, ctx) {
195
225
  let inner = bracketGlyphs(note, "open");
196
- inner += renderKan(note, ctx);
226
+ inner += renderGrace(note.kan, "sw-kan", ctx);
197
227
  if (note.sustain === true) inner += `<span class="sw-hy">${ctx.marks.hyphen}</span>`;
228
+ inner += cutGlyphs(opts.pre, ctx);
198
229
  if (note.svara !== void 0) {
199
230
  const [octave, letter] = note.svara;
200
231
  const c = composeCluster(letter, {
201
232
  komal: note.komal,
202
233
  tivra: note.tivra,
203
234
  octave,
204
- withMatra: opts.withMatra,
235
+ // The first matra is composed into the cluster itself (bn: the font's
236
+ // trailing `a`); any further ones are separate glyphs below.
237
+ withMatra: opts.matras >= 1,
205
238
  lang: ctx.lang
206
239
  });
207
240
  inner += `<span class="${["sw-cl", ...c.classes].join(" ")}">${c.text}</span>`;
208
241
  } else if (note.rest === true) {
209
- inner += matraGlyph(ctx);
210
- } else if (note.sustain === true && opts.withMatra) {
211
- inner += matraGlyph(ctx);
242
+ inner += matraGlyph(ctx).repeat(Math.max(1, opts.matras));
243
+ } else if (note.sustain === true) {
244
+ inner += matraGlyph(ctx).repeat(opts.matras);
212
245
  }
213
246
  if (note.microtone !== void 0) {
214
247
  const mark = localizeDigits(note.microtone === "atikomal" ? "2" : "1", ctx.lang);
215
248
  inner += `<span class="sw-micro" title="${note.microtone}">${mark}</span>`;
216
249
  }
217
- const d = note.durationMatra ?? 1;
218
- if (isIntegral(d) && d > 1 + EPS) inner += matraGlyph(ctx).repeat(Math.round(d) - 1);
219
- if (opts.cuts > 0) inner += `<span class="sw-cut">${NBSP}${ctx.marks.cut}</span>`.repeat(opts.cuts);
250
+ inner += renderGrace(note.resh, "sw-resh", ctx);
251
+ if (note.svara !== void 0 && opts.matras > 1) {
252
+ inner += matraGlyph(ctx).repeat(opts.matras - 1);
253
+ }
254
+ inner += cutGlyphs(opts.post, ctx);
220
255
  inner += bracketGlyphs(note, "close");
221
256
  return inner;
222
257
  }
@@ -233,12 +268,9 @@ function renderAnnotation(notes, ctx) {
233
268
  continue;
234
269
  }
235
270
  if (cell.kind === "group") {
236
- const cuts = cutMarkCount(cell.notes[0].raw);
271
+ const opts = cellPitchedOpts(cell);
237
272
  cell.notes.forEach((note2, j) => {
238
- const last = j === cell.notes.length - 1;
239
- parts.push(
240
- `<span class="sw-n">${renderPitchedInner(note2, { withMatra: last, cuts: last ? cuts : 0 }, ctx)}</span>`
241
- );
273
+ parts.push(`<span class="sw-n">${renderPitchedInner(note2, opts[j], ctx)}</span>`);
242
274
  });
243
275
  continue;
244
276
  }
@@ -276,7 +308,7 @@ function renderRow(row, partIdx, rowIdx, ctx) {
276
308
  const firstCol = cell.start + 1;
277
309
  const lastCol = cell.start + cell.notes.length;
278
310
  const cellSpan = lastCol > firstCol ? `grid-column:${firstCol} / ${lastCol + 1}` : `grid-column:${firstCol}`;
279
- const cuts = cell.kind === "group" ? cutMarkCount(cell.notes[0].raw) : 0;
311
+ const opts = cell.kind === "group" ? cellPitchedOpts(cell) : [];
280
312
  cell.notes.forEach((note, j) => {
281
313
  const col = cell.start + j + 1;
282
314
  const path = `${partIdx}.${rowIdx}.${cell.start + j}`;
@@ -286,9 +318,8 @@ function renderRow(row, partIdx, rowIdx, ctx) {
286
318
  `<span class="sw-bar sw-bar--${note.bar}${cellEdges}" data-note="${path}" style="grid-column:${col}">${renderBarInner(note)}</span>`
287
319
  );
288
320
  } else if (cell.kind === "group") {
289
- const last = j === cell.notes.length - 1;
290
321
  notation.push(
291
- `<span class="sw-n${cellEdges}" data-note="${path}" style="grid-column:${col}">${renderPitchedInner(note, { withMatra: last, cuts: last ? cuts : 0 }, ctx)}</span>`
322
+ `<span class="sw-n${cellEdges}" data-note="${path}" style="grid-column:${col}">${renderPitchedInner(note, opts[j], ctx)}</span>`
292
323
  );
293
324
  } else {
294
325
  const raw = rawDisplay(note);
@@ -379,9 +410,9 @@ ${FONT_FACES}
379
410
  }
380
411
 
381
412
  /* ---- notation font, per language (the note clusters + grace notes) ---- */
382
- .sw-sheet[lang="bn"] .sw-cl, .sw-sheet[lang="bn"] .sw-kan { font-family: Swarabitan, var(--sw-font, sans-serif); }
383
- .sw-sheet[lang="hi"] .sw-cl, .sw-sheet[lang="hi"] .sw-kan { font-family: ome_bhatkhande_hindi, var(--sw-font, sans-serif); }
384
- .sw-sheet[lang="en"] .sw-cl, .sw-sheet[lang="en"] .sw-kan { font-family: ome_bhatkhande_english, var(--sw-font, sans-serif); }
413
+ .sw-sheet[lang="bn"] :is(.sw-cl, .sw-kan, .sw-resh) { font-family: Swarabitan, var(--sw-font, sans-serif); }
414
+ .sw-sheet[lang="hi"] :is(.sw-cl, .sw-kan, .sw-resh) { font-family: ome_bhatkhande_hindi, var(--sw-font, sans-serif); }
415
+ .sw-sheet[lang="en"] :is(.sw-cl, .sw-kan, .sw-resh) { font-family: ome_bhatkhande_english, var(--sw-font, sans-serif); }
385
416
 
386
417
  /* ---- header ---- */
387
418
  .sw-header {
@@ -440,18 +471,42 @@ ${FONT_FACES}
440
471
  .sw-ati-taar::before { top: -0.3em; }
441
472
  .sw-ati-mandra::before { bottom: -0.35em; }
442
473
 
443
- /* kan grace: small raised cluster left of the main note */
444
- .sw-kan {
474
+ /* grace notes: small raised clusters \u2014 \u0995\u09A3 before the note, \u09B0\u09C7\u09B6 after it */
475
+ .sw-kan,
476
+ .sw-resh {
445
477
  position: relative;
446
478
  display: inline-block;
447
479
  font-size: 0.58em;
448
480
  vertical-align: 0.75em;
449
- margin-right: 0.08em;
450
481
  }
482
+ .sw-kan { margin-right: 0.08em; }
483
+ .sw-resh { margin-left: 0.08em; }
451
484
 
452
485
  .sw-hy { padding-right: 0.02em; }
453
486
  .sw-iso { display: inline-block; margin-left: -0.18em; }
454
- .sw-cut { font-size: 0.8em; display: inline-block; margin-left: -0.15em; }
487
+ /* 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
488
+ * Swarabitan's own glyph (the ASCII i key) \u2014 an isolated Unicode \u0983 has no base and
489
+ * every Bengali text font shapes it with a dotted circle (an NBSP carrier
490
+ * does not help), but the notation font has the mark itself. hi/en fall back
491
+ * to these drawn rings: Bhatkhande has no such sign. */
492
+ .sw-cut {
493
+ position: relative;
494
+ display: inline-block;
495
+ width: 0.3em;
496
+ height: 1em;
497
+ vertical-align: baseline;
498
+ }
499
+ .sw-cut::before, .sw-cut::after {
500
+ content: '';
501
+ position: absolute;
502
+ left: 0.08em;
503
+ width: 0.15em;
504
+ height: 0.15em;
505
+ border-radius: 50%;
506
+ background: currentColor;
507
+ }
508
+ .sw-cut::before { top: 0.2em; }
509
+ .sw-cut::after { top: 0.55em; }
455
510
  .sw-br { opacity: 0.8; padding: 0 0.06em; }
456
511
  .sw-micro { font-size: 0.6em; vertical-align: super; opacity: 0.8; }
457
512
 
@@ -608,20 +663,23 @@ function buildAbc(schedule, lyricOf, opts) {
608
663
  barPos -= beatsPerBar;
609
664
  }
610
665
  };
611
- for (const ev of schedule.events) {
666
+ schedule.events.forEach((ev, i) => {
612
667
  if (ev.midi === null) {
613
668
  if (ev.kind === "rest") {
614
669
  tokens.push(`z${abcDuration(ev.durationBeats)}`);
615
670
  words.push("*");
616
671
  advance(ev.durationBeats);
617
672
  }
618
- continue;
673
+ return;
619
674
  }
620
- tokens.push(abcNote(ev));
675
+ const more = glideTargets(schedule.events, i).length > 0;
676
+ const slurOpen = ev.glide !== true && more ? "(" : "";
677
+ const slurClose = ev.glide === true && !more ? ")" : "";
678
+ tokens.push(`${slurOpen}${abcNote(ev)}${slurClose}`);
621
679
  const lyr = lyricOf(ev);
622
680
  words.push(lyr ? abcText(lyr).replace(/\s+/g, "~") : "*");
623
681
  advance(ev.durationBeats);
624
- }
682
+ });
625
683
  if (tokens.length && tokens[tokens.length - 1] !== "|") tokens.push("|]");
626
684
  else if (tokens.length) tokens[tokens.length - 1] = "|]";
627
685
  const meter = ratioMeter(beatsPerBar);
@@ -1077,6 +1135,6 @@ function defineSwaralipiSheet(tagName = "swaralipi-sheet") {
1077
1135
  return ctor;
1078
1136
  }
1079
1137
 
1080
- export { DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, LABELS, MARKS, NOTATION_FONT_URLS, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, abcDuration, abcToSvg, buildAbc, buildTaalAbc, composeCluster, defineSwaralipiSheet, docToAbc, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderTaalStaffHtml, renderToHtml, resolveArcs };
1138
+ export { DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, LABELS, MARKS, NOTATION_FONT_URLS, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, abcDuration, abcToSvg, buildAbc, buildTaalAbc, cellMatra, composeCluster, defineSwaralipiSheet, docToAbc, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderTaalStaffHtml, renderToHtml, resolveArcs };
1081
1139
  //# sourceMappingURL=index.js.map
1082
1140
  //# sourceMappingURL=index.js.map