@ugfoundation/swaralipi-js 0.6.1 → 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.
@@ -112,49 +112,120 @@ function groupRowIntoCells(notes) {
112
112
  if (note.halfMatra === true || acc > EPS && isIntegral(acc)) close();
113
113
  });
114
114
  close();
115
- return cells;
115
+ return mergeStructural(absorbBrackets(cells));
116
+ }
117
+ var isStructural = (c) => c.kind === "bar" || c.kind === "carrier";
118
+ function bracketEdges(cell) {
119
+ if (cell.kind !== "carrier") return void 0;
120
+ let open = false;
121
+ let close = false;
122
+ for (const note of cell.notes) {
123
+ if (note.raw !== void 0 && !/^[{}()[\]\s]*$/.test(note.raw)) return void 0;
124
+ for (const b of note.brackets ?? []) {
125
+ if (b.edge === "open") open = true;
126
+ else close = true;
127
+ }
128
+ }
129
+ return open || close ? { open, close } : void 0;
130
+ }
131
+ function absorbBrackets(cells) {
132
+ const out = [];
133
+ for (let i = 0; i < cells.length; i++) {
134
+ const cell = cells[i];
135
+ const edges = bracketEdges(cell);
136
+ if (edges !== void 0 && edges.open !== edges.close) {
137
+ const prev = out[out.length - 1];
138
+ const next = cells[i + 1];
139
+ if (edges.close && prev?.kind === "group") {
140
+ prev.notes = [...prev.notes, ...cell.notes];
141
+ continue;
142
+ }
143
+ if (edges.open && next?.kind === "group") {
144
+ next.notes = [...cell.notes, ...next.notes];
145
+ next.start = cell.start;
146
+ continue;
147
+ }
148
+ }
149
+ out.push(cell);
150
+ }
151
+ return out;
152
+ }
153
+ function mergeStructural(cells) {
154
+ const out = [];
155
+ for (const cell of cells) {
156
+ const prev = out[out.length - 1];
157
+ if (prev !== void 0 && isStructural(prev) && isStructural(cell)) {
158
+ prev.notes = [...prev.notes, ...cell.notes];
159
+ if (cell.kind === "bar") prev.kind = "bar";
160
+ } else out.push(cell);
161
+ }
162
+ return out;
163
+ }
164
+ var SECTION_BARS = /* @__PURE__ */ new Set(["double", "quad"]);
165
+ var isBarCell = (cell) => cell?.kind === "bar";
166
+ var endsSection = (cell) => isBarCell(cell) && SECTION_BARS.has(cell.notes[cell.notes.length - 1].bar);
167
+ function alignRows(rows) {
168
+ const counts = /* @__PURE__ */ new Map();
169
+ for (const cells of rows)
170
+ if (isBarCell(cells[0])) counts.set(cells.length, (counts.get(cells.length) ?? 0) + 1);
171
+ let width = 0;
172
+ let best = 0;
173
+ for (const [len, n] of counts)
174
+ if (n > best || n === best && len > width) {
175
+ width = len;
176
+ best = n;
177
+ }
178
+ if (width === 0) for (const cells of rows) width = Math.max(width, cells.length);
179
+ const offsets = rows.map((cells, i) => {
180
+ if (cells.length === 0 || cells.length >= width) return 0;
181
+ const prev = rows[i - 1];
182
+ const phraseStart = !isBarCell(cells[0]) || i === 0 || endsSection(prev?.[prev.length - 1]);
183
+ if (!phraseStart) return 0;
184
+ return Math.max(0, width - cells.length - (isBarCell(cells[cells.length - 1]) ? 0 : 1));
185
+ });
186
+ rows.forEach((cells, i) => {
187
+ width = Math.max(width, offsets[i] + cells.length);
188
+ });
189
+ return { width, offsets };
116
190
  }
117
- function resolveArcs(cells, noteCount) {
191
+ function resolveArcs(cells) {
118
192
  const arcs = [];
119
193
  let openStart;
120
194
  let sawEnds = false;
121
- const fillCols = [];
122
- const openAt = (col) => {
195
+ const fillCells = [];
196
+ const openAt = (i) => {
123
197
  sawEnds = true;
124
- if (openStart === void 0) openStart = col;
198
+ if (openStart === void 0) openStart = i;
125
199
  };
126
- const closeAt = (col) => {
200
+ const closeAt = (i) => {
127
201
  sawEnds = true;
128
202
  if (openStart !== void 0) {
129
- arcs.push({ startCol: openStart, endCol: col, openLeft: false, openRight: false });
203
+ arcs.push({ start: openStart, end: i, openLeft: false, openRight: false });
130
204
  openStart = void 0;
131
205
  } else {
132
- arcs.push({ startCol: 1, endCol: col, openLeft: true, openRight: false });
206
+ arcs.push({ start: 0, end: i, openLeft: true, openRight: false });
133
207
  }
134
208
  };
135
- for (const cell of cells) {
136
- const firstCol = cell.start + 1;
137
- const lastCol = cell.start + cell.notes.length;
138
- cell.notes.forEach((note, j) => {
139
- const col = cell.start + j + 1;
140
- if (note.meend === "start") openAt(col);
141
- else if (note.meend === "end") closeAt(col);
209
+ cells.forEach((cell, i) => {
210
+ for (const note of cell.notes) {
211
+ if (note.meend === "start") openAt(i);
212
+ else if (note.meend === "end") closeAt(i);
142
213
  for (const b of note.below ?? []) {
143
214
  if (!isArcRaw(b.raw)) continue;
144
215
  for (const ch of b.raw) {
145
- if (ch === "w" || ch === "W") openAt(firstCol);
146
- else if (ch === "x" || ch === "X") closeAt(lastCol);
147
- else fillCols.push(firstCol, lastCol);
216
+ if (ch === "w" || ch === "W") openAt(i);
217
+ else if (ch === "x" || ch === "X") closeAt(i);
218
+ else fillCells.push(i);
148
219
  }
149
220
  }
150
- });
151
- }
221
+ }
222
+ });
152
223
  if (openStart !== void 0) {
153
- arcs.push({ startCol: openStart, endCol: Math.max(noteCount, 1), openLeft: false, openRight: true });
154
- } else if (!sawEnds && fillCols.length > 0) {
224
+ arcs.push({ start: openStart, end: Math.max(cells.length - 1, 0), openLeft: false, openRight: true });
225
+ } else if (!sawEnds && fillCells.length > 0) {
155
226
  arcs.push({
156
- startCol: Math.min(...fillCols),
157
- endCol: Math.max(...fillCols),
227
+ start: Math.min(...fillCells),
228
+ end: Math.max(...fillCells),
158
229
  openLeft: true,
159
230
  openRight: true,
160
231
  line: true
@@ -206,20 +277,43 @@ function cutGlyphs(marks, ctx) {
206
277
  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
278
  }
208
279
  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);
280
+ const pitched = cell.notes.filter(isPitched);
281
+ const total = pitched.reduce((a, n) => a + (n.durationMatra ?? 1), 0);
282
+ const half = pitched.some((n) => n.halfMatra === true);
211
283
  const whole = Math.floor(total + EPS);
212
284
  return { matras: half ? whole : Math.max(1, whole), half };
213
285
  }
214
286
  function cellPitchedOpts(cell) {
215
287
  const { matras, half } = cellMatra(cell);
216
- const marks = rawMarks(cell.notes[0].raw, cell.notes.length);
288
+ const pitched = [];
289
+ cell.notes.forEach((n, j) => {
290
+ if (isPitched(n)) pitched.push(j);
291
+ });
292
+ const first = pitched[0];
293
+ const last = pitched[pitched.length - 1];
294
+ const marks = rawMarks(first === void 0 ? void 0 : cell.notes[first].raw, pitched.length);
217
295
  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
- }));
296
+ return cell.notes.map((note, j) => {
297
+ if (!isPitched(note)) return { matras: 0, pre: "", post: "" };
298
+ return {
299
+ matras: j === last ? aakars : 0,
300
+ pre: j === first ? marks?.pre ?? "" : "",
301
+ post: marks !== void 0 ? marks.post[pitched.indexOf(j)] : half && j === last ? "i" : ""
302
+ };
303
+ });
304
+ }
305
+ function cellLyricSlices(cell) {
306
+ if (!cell.notes.some((n) => n.lyric !== void 0)) return void 0;
307
+ const pitched = cell.notes.filter(isPitched);
308
+ if (pitched.length === 0) return cell.notes.map((n) => n.lyric ?? "");
309
+ const slices = new Array(pitched.length).fill("");
310
+ let k = 0;
311
+ for (const note of cell.notes) {
312
+ const at = Math.min(k, pitched.length - 1);
313
+ slices[at] += note.lyric ?? "";
314
+ if (isPitched(note)) k++;
315
+ }
316
+ return slices;
223
317
  }
224
318
  function renderPitchedInner(note, opts, ctx) {
225
319
  let inner = bracketGlyphs(note, "open");
@@ -297,64 +391,56 @@ function renderAnnotation(notes, ctx) {
297
391
  function lyricText(s, ctx) {
298
392
  return s.split("\u09F9").map((part) => part === "" ? "" : ctx.tr(part)).join(ctx.marks.melisma);
299
393
  }
300
- function renderRow(row, partIdx, rowIdx, ctx) {
394
+ var COL_BASE = 2;
395
+ function renderNote(note, kind, opts, ctx) {
396
+ if (note.bar !== void 0) return renderBarInner(note);
397
+ if (kind === "group" && opts !== void 0) return renderPitchedInner(note, opts, ctx);
398
+ const raw = rawDisplay(note);
399
+ return bracketGlyphs(note, "open") + (raw !== void 0 ? `<span class="sw-raw" title="${escapeHtml(raw)}">${escapeHtml(raw)}</span>` : "") + bracketGlyphs(note, "close");
400
+ }
401
+ function renderArc(arc, offset) {
402
+ const open = arc.line === true;
403
+ const cls = [
404
+ "sw-arc",
405
+ arc.openLeft || open ? "sw-arc--openl" : "",
406
+ arc.openRight || open ? "sw-arc--openr" : ""
407
+ ].filter(Boolean).join(" ");
408
+ const left = open || arc.openLeft ? "" : '<i class="sw-arc-l">w</i>';
409
+ const right = open || arc.openRight ? "" : '<i class="sw-arc-r">x</i>';
410
+ const from = offset + arc.start + COL_BASE;
411
+ const to = offset + arc.end + COL_BASE + 1;
412
+ return `<span class="${cls}" style="grid-column:${from} / ${to}">${left}<i class="sw-arc-m"></i>${right}</span>`;
413
+ }
414
+ function renderRow(row, partIdx, rowIdx, offset, ctx) {
301
415
  const cells = groupRowIntoCells(row.notes);
302
- const arcs = resolveArcs(cells, row.notes.length);
303
- const above = [];
304
- const notation = [];
305
- const below = [];
306
- const lyric = [];
307
- for (const cell of cells) {
308
- const firstCol = cell.start + 1;
309
- const lastCol = cell.start + cell.notes.length;
310
- const cellSpan = lastCol > firstCol ? `grid-column:${firstCol} / ${lastCol + 1}` : `grid-column:${firstCol}`;
416
+ const out = [];
417
+ if (row.label !== void 0)
418
+ out.push(`<span class="sw-label">${escapeHtml(ctx.tr(row.label))}</span>`);
419
+ cells.forEach((cell, ci) => {
420
+ const col = offset + ci + COL_BASE;
311
421
  const opts = cell.kind === "group" ? cellPitchedOpts(cell) : [];
312
- cell.notes.forEach((note, j) => {
313
- const col = cell.start + j + 1;
422
+ const barCls = cell.kind === "bar" ? " sw-bar-cell" : "";
423
+ const notes = cell.notes.map((note, j) => {
314
424
  const path = `${partIdx}.${rowIdx}.${cell.start + j}`;
315
- const cellEdges = `${j === 0 ? " sw-cs" : ""}${j === cell.notes.length - 1 ? " sw-ce" : ""}`;
316
- if (cell.kind === "bar") {
317
- notation.push(
318
- `<span class="sw-bar sw-bar--${note.bar}${cellEdges}" data-note="${path}" style="grid-column:${col}">${renderBarInner(note)}</span>`
319
- );
320
- } else if (cell.kind === "group") {
321
- notation.push(
322
- `<span class="sw-n${cellEdges}" data-note="${path}" style="grid-column:${col}">${renderPitchedInner(note, opts[j], ctx)}</span>`
323
- );
324
- } else {
325
- const raw = rawDisplay(note);
326
- const inner = bracketGlyphs(note, "open") + (raw !== void 0 ? `<span class="sw-raw" title="${escapeHtml(raw)}">${escapeHtml(raw)}</span>` : "") + bracketGlyphs(note, "close");
327
- if (inner !== "") {
328
- notation.push(
329
- `<span class="sw-n sw-carrier${cellEdges}" data-note="${path}" style="grid-column:${col}">${inner}</span>`
330
- );
331
- }
332
- }
333
- if (note.lyric !== void 0) {
334
- lyric.push(
335
- `<span class="sw-lyr${j === 0 ? " sw-cs" : ""}" style="grid-column:${col}">${escapeHtml(lyricText(note.lyric, ctx))}</span>`
336
- );
337
- }
338
- if (note.above !== void 0 && note.above.length > 0) {
339
- above.push(`<span class="sw-above" style="${cellSpan}">${renderAnnotation(note.above, ctx)}</span>`);
340
- }
341
- const nonArc = (note.below ?? []).filter((b) => !isArcRaw(b.raw));
342
- if (nonArc.length > 0) {
343
- below.push(`<span class="sw-below" style="${cellSpan}">${renderAnnotation(nonArc, ctx)}</span>`);
344
- }
345
- });
346
- }
347
- for (const arc of arcs) {
348
- const cls = [
349
- "sw-arc",
350
- arc.openLeft ? "sw-arc--openl" : "",
351
- arc.openRight ? "sw-arc--openr" : "",
352
- arc.line === true ? "sw-arc--line" : ""
353
- ].filter(Boolean).join(" ");
354
- below.push(`<span class="${cls}" style="grid-column:${arc.startCol} / ${arc.endCol + 1}"></span>`);
355
- }
356
- const label = row.label !== void 0 ? `<span class="sw-label">${escapeHtml(ctx.tr(row.label))}</span>` : "";
357
- return `<div class="sw-row">${label}<div class="sw-grid">${above.join("")}${notation.join("")}${below.join("")}${lyric.join("")}</div></div>`;
425
+ const cls = isPitched(note) ? " sw-p" : "";
426
+ const bar = note.bar !== void 0 ? ` sw-bar sw-bar--${note.bar}` : "";
427
+ return `<span class="sw-n${cls}${bar}" data-note="${path}">${renderNote(note, cell.kind, opts[j], ctx)}</span>`;
428
+ }).join("");
429
+ out.push(`<span class="sw-cell${barCls}" style="grid-column:${col}">${notes}</span>`);
430
+ const slices = cellLyricSlices(cell);
431
+ if (slices !== void 0) {
432
+ const html = slices.map((t) => `<span>${escapeHtml(lyricText(t, ctx))}</span>`).join("");
433
+ out.push(`<span class="sw-lyr" style="grid-column:${col}">${html}</span>`);
434
+ }
435
+ const above = cell.notes.flatMap((n) => n.above ?? []);
436
+ if (above.length > 0)
437
+ out.push(`<span class="sw-above" style="grid-column:${col}">${renderAnnotation(above, ctx)}</span>`);
438
+ const below = cell.notes.flatMap((n) => (n.below ?? []).filter((b) => !isArcRaw(b.raw)));
439
+ if (below.length > 0)
440
+ out.push(`<span class="sw-below" style="grid-column:${col}">${renderAnnotation(below, ctx)}</span>`);
441
+ });
442
+ for (const arc of resolveArcs(cells)) out.push(renderArc(arc, offset));
443
+ return `<div class="sw-row">${out.join("")}</div>`;
358
444
  }
359
445
  function renderHeader(meta, ctx) {
360
446
  return renderMetaHeader(meta, ctx.lang, ctx.tr);
@@ -377,19 +463,45 @@ function renderToHtml(doc, opts = {}) {
377
463
  const lang = opts.lang === "hi" || opts.lang === "en" ? opts.lang : "bn";
378
464
  const tr = lang !== "bn" && opts.transliterate ? opts.transliterate : (s) => s;
379
465
  const ctx = { lang, marks: MARKS[lang], tr };
466
+ const layout = alignRows(doc.parts.flatMap((p) => p.rows.map((r) => groupRowIntoCells(r.notes))));
467
+ const tracks = `grid-template-columns:max-content repeat(${layout.width},minmax(var(--sw-matra, 1.4em),max-content))`;
380
468
  const out = [`<div class="sw-sheet" lang="${lang}">`];
381
469
  if (opts.showHeader !== false) out.push(renderHeader(doc.meta, ctx));
470
+ out.push(`<div class="sw-scroll"><div class="sw-block" style="${tracks}">`);
471
+ let n = 0;
382
472
  doc.parts.forEach((part, p) => {
383
473
  out.push('<section class="sw-part">');
384
474
  if (part.name !== void 0)
385
475
  out.push(`<h3 class="sw-part-name">${escapeHtml(ctx.tr(part.name))}</h3>`);
386
- part.rows.forEach((row, r) => out.push(renderRow(row, p, r, ctx)));
476
+ part.rows.forEach((row, r) => out.push(renderRow(row, p, r, layout.offsets[n++], ctx)));
387
477
  out.push("</section>");
388
478
  });
389
- out.push("</div>");
479
+ out.push("</div></div></div>");
390
480
  return out.join("");
391
481
  }
392
482
 
483
+ // src/viewer/fit.ts
484
+ function fitSwaralipi(root, minScale = 0, zoom = 1) {
485
+ const sheets = root.querySelectorAll(".sw-sheet");
486
+ for (const sheet of sheets) {
487
+ const scroll = sheet.querySelector(".sw-scroll");
488
+ const block = sheet.querySelector(".sw-block");
489
+ if (scroll === null || block === null) continue;
490
+ sheet.style.removeProperty("--sw-size");
491
+ const available = scroll.clientWidth;
492
+ const natural = block.scrollWidth;
493
+ if (available === 0 || natural === 0) continue;
494
+ const fit = natural > available ? Math.max(minScale, available / natural) : 1;
495
+ const scale = fit * zoom;
496
+ if (scale === 1) continue;
497
+ const base = parseFloat(getComputedStyle(sheet).fontSize);
498
+ sheet.style.setProperty("--sw-size", `${(base * scale).toFixed(3)}px`);
499
+ }
500
+ }
501
+ var MIN_ZOOM = 0.5;
502
+ var MAX_ZOOM = 3;
503
+ var ZOOM_STEP = 1.2;
504
+
393
505
  // src/viewer/styles.ts
394
506
  var NOTATION_FONT_URLS = {
395
507
  Swarabitan: "https://lipighor.pages.dev/swarabitan/Swarabitan.ttf",
@@ -427,30 +539,42 @@ ${FONT_FACES}
427
539
 
428
540
  .sw-part-name { font-size: 0.85em; font-weight: 600; margin: 1em 0 0.2em; opacity: 0.85; }
429
541
 
430
- /* ---- row grid: above / notation / below(arcs) / lyric ---- */
431
- .sw-row {
432
- display: flex;
433
- align-items: flex-start;
434
- overflow-x: auto;
435
- padding: 0.4em 0;
542
+ /* ---- block grid: ONE column set for the whole sheet ----
543
+ * NLTR emits a song as a single html table, which is why its lines align
544
+ * down the page. Same here: .sw-block owns the tracks, every .sw-row
545
+ * borrows them with subgrid, and .sw-part is display:contents so section
546
+ * headings sit in the same grid instead of breaking it. */
547
+ .sw-scroll { overflow-x: auto; }
548
+ .sw-block {
549
+ display: grid;
550
+ column-gap: var(--sw-cell-gap, 0.4em);
551
+ align-items: end;
552
+ width: max-content;
436
553
  }
437
- .sw-label { flex: none; margin-right: 0.8em; font-weight: 600; }
438
- .sw-grid {
554
+ .sw-part { display: contents; }
555
+ .sw-row {
556
+ grid-column: 1 / -1;
439
557
  display: grid;
558
+ grid-template-columns: subgrid;
440
559
  grid-template-rows: auto auto auto auto;
441
- grid-auto-columns: max-content;
442
560
  align-items: end;
561
+ padding: 0.4em 0;
443
562
  }
444
- .sw-above { grid-row: 1; font-size: 0.62em; justify-self: start; white-space: nowrap; padding-left: 0.2em; }
445
- .sw-n { grid-row: 2; justify-self: center; white-space: nowrap; }
563
+ .sw-label { grid-column: 1; grid-row: 2; padding-right: 0.5em; font-weight: 600; }
564
+
565
+ /* One column per cell; sub-notes are equal slices of it (min-width:0 kills
566
+ * flex's min-content floor, else a wide sub-note steals width from its
567
+ * neighbours while the syllable below it does not, and the two bands drift). */
568
+ .sw-cell, .sw-lyr { display: flex; justify-content: center; }
569
+ .sw-cell > .sw-n { white-space: nowrap; }
570
+ .sw-cell > .sw-n.sw-p,
571
+ .sw-lyr > span { flex: 1 1 0; min-width: 0; text-align: center; white-space: nowrap; }
572
+ .sw-cell { grid-row: 2; }
573
+ .sw-above { grid-row: 1; font-size: 0.62em; justify-self: center; white-space: nowrap; }
446
574
  .sw-arc,
447
575
  .sw-below { grid-row: 3; }
448
- .sw-lyr { grid-row: 4; justify-self: center; white-space: nowrap; padding-top: 0.3em; }
449
- .sw-below { font-size: 0.62em; justify-self: start; white-space: nowrap; padding-left: 0.2em; }
450
-
451
- /* inter-cell gap as padding on each cell's first notation item, so arcs
452
- * and above-spans crossing cells stay continuous */
453
- .sw-cs { padding-left: var(--sw-cell-gap, 0.55em); }
576
+ .sw-lyr { grid-row: 4; padding-top: 0.3em; }
577
+ .sw-below { font-size: 0.62em; justify-self: center; white-space: nowrap; }
454
578
 
455
579
  /* ---- notation clusters & marks ---- */
456
580
  .sw-cl { position: relative; display: inline-block; }
@@ -519,31 +643,41 @@ ${FONT_FACES}
519
643
  opacity: 0.55;
520
644
  }
521
645
 
522
- /* ---- bars: dari (A) and danda (l/L) Swarabitan glyphs ---- */
523
- /* horizontal gap before a bar comes from its own .sw-cs padding-left \u2014
524
- * never reset it with a padding shorthand here */
646
+ /* ---- bars: dari (A) and danda (l/L) Swarabitan glyphs, all langs ---- */
525
647
  .sw-bar {
526
- grid-row: 2;
527
648
  font-family: Swarabitan, var(--sw-font, sans-serif);
528
649
  white-space: nowrap;
529
- padding-right: 0.1em;
530
650
  }
531
651
  .sw-bar--quad { letter-spacing: 0.15em; }
652
+ /* a bar owns a whole column but needs no matra of width */
653
+ .sw-bar-cell { min-width: 0; }
532
654
 
533
- /* ---- under-arcs (slurs decoded from w/x segments and meend) ---- */
655
+ /* ---- \u09AE\u09C0\u09A1\u09BC under-arcs (from w/x segments and spec meend) ----
656
+ * Swarabitan draws the arc as three pieces \u2014 'w' the opening hook, 'y' a
657
+ * flat fill, 'x' the closing hook. We use the font's own hooks and stretch
658
+ * the fill as a CSS bar (the 'y' glyph IS a plain rectangle), so one arc
659
+ * spans any number of columns. An open side = the arc continues onto the
660
+ * next system: drop that hook and leave the fill squared off. */
534
661
  .sw-arc {
535
- align-self: start;
536
- height: 0.4em;
537
- margin: 0.05em 0.25em 0;
538
- border: 0.09em solid currentColor;
539
- border-top: none;
540
- border-bottom-left-radius: 0.6em;
541
- border-bottom-right-radius: 0.6em;
542
- opacity: 0.85;
543
- }
544
- .sw-arc--openl { border-left: none; border-bottom-left-radius: 0; }
545
- .sw-arc--openr { border-right: none; border-bottom-right-radius: 0; }
546
- .sw-arc--line { border-left: none; border-right: none; border-radius: 0; height: 0.22em; }
662
+ display: flex;
663
+ align-items: baseline;
664
+ line-height: 1;
665
+ font-family: Swarabitan, var(--sw-font, sans-serif);
666
+ font-size: 0.9em;
667
+ }
668
+ .sw-arc-l, .sw-arc-r { flex: none; font-style: normal; }
669
+ /* The fill's geometry is measured off the font's own 'y' glyph: ink 0.11em
670
+ tall whose bottom sits 0.01em above the baseline. An empty inline-block
671
+ takes its bottom margin edge as its baseline, so -0.01em lifts the bar to
672
+ exactly where the hooks' flat stroke runs \u2014 no seam at either join. */
673
+ .sw-arc-m {
674
+ flex: 1 1 0;
675
+ min-width: 0;
676
+ align-self: baseline;
677
+ height: 0.11em;
678
+ margin-bottom: -0.01em;
679
+ background: currentColor;
680
+ }
547
681
 
548
682
  /* ---- annotation content ---- */
549
683
  .sw-beat { position: relative; display: inline-block; padding: 0 0.1em; }
@@ -587,8 +721,8 @@ var SWARALIPI_PRINT_CSS = `
587
721
  @page { margin: 16mm; }
588
722
  @media print {
589
723
  .sw-sheet, .sw-staff { -webkit-print-color-adjust: exact; print-color-adjust: exact; color: #000; }
590
- .sw-row { overflow: visible !important; break-inside: avoid; page-break-inside: avoid; }
591
- .sw-part { break-inside: auto; }
724
+ .sw-scroll { overflow: visible !important; }
725
+ .sw-row { break-inside: avoid; page-break-inside: avoid; }
592
726
  .sw-part-name, .sw-header { break-after: avoid-page; page-break-after: avoid; }
593
727
  .sw-hl { background: none !important; }
594
728
  /* Keep each engraved staff system whole across a page break (the abcjs line
@@ -887,12 +1021,17 @@ ${SWARALIPI_STAFF_CSS}
887
1021
  ${SWARALIPI_PRINT_CSS}
888
1022
  ${watermarkCss(watermark)}
889
1023
  html, body { margin: 0; }
890
- body { color: #111; background: #fff; padding: 16px; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
1024
+ /* The printable width of A4 at the @page margin above. The sheet is one grid
1025
+ with one natural width, so the fit script needs a real page-sized box to
1026
+ measure against \u2014 the iframe's own width is not it. */
1027
+ body { width: 178mm; color: #111; background: #fff; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
891
1028
  .sw-sheet { max-width: 100%; }
892
1029
  .sw-footer { margin-top: 12px; padding-top: 6px; border-top: 1px solid #ddd; font-size: 12px; color: #555; text-align: right; }
893
1030
  </style>
894
1031
  </head>
895
- <body>${top}${bodyHtml}${bottom}</body>
1032
+ <body>${top}${bodyHtml}${bottom}
1033
+ <script>window.__swFit=function(){(${fitSwaralipi.toString()})(document,0.45)};window.__swFit()</script>
1034
+ </body>
896
1035
  </html>`;
897
1036
  }
898
1037
  function renderPrintDocument(doc, opts = {}) {
@@ -935,6 +1074,7 @@ function printHtmlInIframe(html) {
935
1074
  if (printed) return;
936
1075
  printed = true;
937
1076
  try {
1077
+ win.__swFit?.();
938
1078
  win.focus();
939
1079
  win.print();
940
1080
  } catch {
@@ -990,6 +1130,9 @@ function createElementClass() {
990
1130
  #translitLang;
991
1131
  #renderSeq = 0;
992
1132
  #staffOptions = {};
1133
+ #resize;
1134
+ #fittedAt = -1;
1135
+ #zoom = 1;
993
1136
  validate = false;
994
1137
  constructor() {
995
1138
  super();
@@ -1039,9 +1182,46 @@ function createElementClass() {
1039
1182
  connectedCallback() {
1040
1183
  this.#ensureTransliterator();
1041
1184
  this.#render();
1185
+ if (this.#resize === void 0 && typeof ResizeObserver !== "undefined") {
1186
+ this.#resize = new ResizeObserver(([entry]) => {
1187
+ const w = entry?.contentRect.width ?? -1;
1188
+ if (w === this.#fittedAt) return;
1189
+ this.#fittedAt = w;
1190
+ this.#fit();
1191
+ });
1192
+ this.#resize.observe(this);
1193
+ }
1042
1194
  }
1043
1195
  disconnectedCallback() {
1044
1196
  this.#abort?.abort();
1197
+ this.#resize?.disconnect();
1198
+ this.#resize = void 0;
1199
+ }
1200
+ /** Scale the notation to fit the available width, times the zoom factor. */
1201
+ #fit() {
1202
+ fitSwaralipi(this.#root, 0, this.#zoom);
1203
+ }
1204
+ /** Re-fit the current render (public for hosts that resize the sheet
1205
+ * themselves, e.g. a tab that was hidden while the document loaded). */
1206
+ fit() {
1207
+ this.#fittedAt = -1;
1208
+ this.#fit();
1209
+ }
1210
+ get zoom() {
1211
+ return this.#zoom;
1212
+ }
1213
+ set zoom(factor) {
1214
+ this.#zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, factor));
1215
+ this.#fit();
1216
+ }
1217
+ zoomIn() {
1218
+ this.zoom = this.#zoom * ZOOM_STEP;
1219
+ }
1220
+ zoomOut() {
1221
+ this.zoom = this.#zoom / ZOOM_STEP;
1222
+ }
1223
+ resetZoom() {
1224
+ this.zoom = 1;
1045
1225
  }
1046
1226
  async #load(url) {
1047
1227
  this.#abort?.abort();
@@ -1073,6 +1253,7 @@ function createElementClass() {
1073
1253
  }
1074
1254
  #render() {
1075
1255
  const seq = ++this.#renderSeq;
1256
+ this.#zoom = 1;
1076
1257
  if (this.#doc === void 0) {
1077
1258
  this.#root.innerHTML = this.#styleFallback();
1078
1259
  return;
@@ -1091,6 +1272,7 @@ function createElementClass() {
1091
1272
  transliterate: this.#translitLang === lang ? this.#translit : void 0
1092
1273
  });
1093
1274
  this.#root.innerHTML = this.#styleFallback() + html;
1275
+ this.#fit();
1094
1276
  }
1095
1277
  #renderError(message) {
1096
1278
  const div = document.createElement("div");
@@ -1135,6 +1317,6 @@ function defineSwaralipiSheet(tagName = "swaralipi-sheet") {
1135
1317
  return ctor;
1136
1318
  }
1137
1319
 
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 };
1320
+ export { DEFAULT_NOTATOR, DEFAULT_WATERMARK, DIGITS, LABELS, MARKS, MAX_ZOOM, MIN_ZOOM, NOTATION_FONT_URLS, SWARALIPI_CSS, SWARALIPI_PRINT_CSS, SWARALIPI_STAFF_CSS, ZOOM_STEP, abcDuration, abcToSvg, alignRows, buildAbc, buildTaalAbc, cellMatra, composeCluster, defineSwaralipiSheet, docToAbc, fitSwaralipi, groupRowIntoCells, loadLyricTransliterator, localizeDigits, midiToAbc, printSwaralipi, renderMetaHeader, renderPrintDocument, renderStaffHtml, renderStaffPrintDocument, renderTaalStaffHtml, renderToHtml, resolveArcs };
1139
1321
  //# sourceMappingURL=index.js.map
1140
1322
  //# sourceMappingURL=index.js.map