@ugfoundation/swaralipi-js 0.6.1 → 0.7.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.
@@ -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,52 @@ 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(calc(var(--sw-matra, 1.4em) * var(--sw-tight, 1)),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, minSpacing = 0.35, minSize = 12) {
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
+ sheet.style.removeProperty("--sw-tight");
492
+ const available = scroll.clientWidth;
493
+ let natural = block.scrollWidth;
494
+ if (available === 0 || natural === 0) continue;
495
+ for (const step of [0.75, 0.5, 0.25, 0]) {
496
+ if (natural <= available || minSpacing >= 1) break;
497
+ sheet.style.setProperty("--sw-tight", String(minSpacing + (1 - minSpacing) * step));
498
+ natural = block.scrollWidth;
499
+ }
500
+ const base = parseFloat(getComputedStyle(sheet).fontSize);
501
+ let fit = natural > available ? Math.max(minScale, available / natural) : 1;
502
+ if (base > 0 && minSize > 0) fit = Math.max(fit, Math.min(1, minSize / base));
503
+ const scale = fit * zoom;
504
+ if (scale === 1) continue;
505
+ sheet.style.setProperty("--sw-size", `${(base * scale).toFixed(3)}px`);
506
+ }
507
+ }
508
+ var MIN_ZOOM = 0.5;
509
+ var MAX_ZOOM = 3;
510
+ var ZOOM_STEP = 1.2;
511
+
393
512
  // src/viewer/styles.ts
394
513
  var NOTATION_FONT_URLS = {
395
514
  Swarabitan: "https://lipighor.pages.dev/swarabitan/Swarabitan.ttf",
@@ -419,7 +538,10 @@ ${FONT_FACES}
419
538
  margin: 0 0 0.75em;
420
539
  padding: 0 0 0.5em;
421
540
  border-bottom: 1px solid var(--sw-rule, rgba(128, 128, 128, 0.35));
422
- font-size: 0.95em;
541
+ /* Scales with the sheet, but never below the floor: fitSwaralipi shrinks
542
+ .sw-sheet's font-size to fit the notation, and the metadata is prose \u2014
543
+ it has no columns to align and must stay readable on a phone. */
544
+ font-size: max(0.95em, var(--sw-header-min, 0.95rem));
423
545
  }
424
546
  .sw-meta-row { display: flex; gap: 0.3em; }
425
547
  .sw-meta-label { font-weight: 600; min-width: 4.5em; }
@@ -427,30 +549,42 @@ ${FONT_FACES}
427
549
 
428
550
  .sw-part-name { font-size: 0.85em; font-weight: 600; margin: 1em 0 0.2em; opacity: 0.85; }
429
551
 
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;
552
+ /* ---- block grid: ONE column set for the whole sheet ----
553
+ * NLTR emits a song as a single html table, which is why its lines align
554
+ * down the page. Same here: .sw-block owns the tracks, every .sw-row
555
+ * borrows them with subgrid, and .sw-part is display:contents so section
556
+ * headings sit in the same grid instead of breaking it. */
557
+ .sw-scroll { overflow-x: auto; }
558
+ .sw-block {
559
+ display: grid;
560
+ column-gap: calc(var(--sw-cell-gap, 0.4em) * var(--sw-tight, 1));
561
+ align-items: end;
562
+ width: max-content;
436
563
  }
437
- .sw-label { flex: none; margin-right: 0.8em; font-weight: 600; }
438
- .sw-grid {
564
+ .sw-part { display: contents; }
565
+ .sw-row {
566
+ grid-column: 1 / -1;
439
567
  display: grid;
568
+ grid-template-columns: subgrid;
440
569
  grid-template-rows: auto auto auto auto;
441
- grid-auto-columns: max-content;
442
570
  align-items: end;
571
+ padding: 0.4em 0;
443
572
  }
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; }
573
+ .sw-label { grid-column: 1; grid-row: 2; padding-right: 0.5em; font-weight: 600; }
574
+
575
+ /* One column per cell; sub-notes are equal slices of it (min-width:0 kills
576
+ * flex's min-content floor, else a wide sub-note steals width from its
577
+ * neighbours while the syllable below it does not, and the two bands drift). */
578
+ .sw-cell, .sw-lyr { display: flex; justify-content: center; }
579
+ .sw-cell > .sw-n { white-space: nowrap; }
580
+ .sw-cell > .sw-n.sw-p,
581
+ .sw-lyr > span { flex: 1 1 0; min-width: 0; text-align: center; white-space: nowrap; }
582
+ .sw-cell { grid-row: 2; }
583
+ .sw-above { grid-row: 1; font-size: 0.62em; justify-self: center; white-space: nowrap; }
446
584
  .sw-arc,
447
585
  .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); }
586
+ .sw-lyr { grid-row: 4; padding-top: 0.3em; }
587
+ .sw-below { font-size: 0.62em; justify-self: center; white-space: nowrap; }
454
588
 
455
589
  /* ---- notation clusters & marks ---- */
456
590
  .sw-cl { position: relative; display: inline-block; }
@@ -519,31 +653,41 @@ ${FONT_FACES}
519
653
  opacity: 0.55;
520
654
  }
521
655
 
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 */
656
+ /* ---- bars: dari (A) and danda (l/L) Swarabitan glyphs, all langs ---- */
525
657
  .sw-bar {
526
- grid-row: 2;
527
658
  font-family: Swarabitan, var(--sw-font, sans-serif);
528
659
  white-space: nowrap;
529
- padding-right: 0.1em;
530
660
  }
531
661
  .sw-bar--quad { letter-spacing: 0.15em; }
662
+ /* a bar owns a whole column but needs no matra of width */
663
+ .sw-bar-cell { min-width: 0; }
532
664
 
533
- /* ---- under-arcs (slurs decoded from w/x segments and meend) ---- */
665
+ /* ---- \u09AE\u09C0\u09A1\u09BC under-arcs (from w/x segments and spec meend) ----
666
+ * Swarabitan draws the arc as three pieces \u2014 'w' the opening hook, 'y' a
667
+ * flat fill, 'x' the closing hook. We use the font's own hooks and stretch
668
+ * the fill as a CSS bar (the 'y' glyph IS a plain rectangle), so one arc
669
+ * spans any number of columns. An open side = the arc continues onto the
670
+ * next system: drop that hook and leave the fill squared off. */
534
671
  .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; }
672
+ display: flex;
673
+ align-items: baseline;
674
+ line-height: 1;
675
+ font-family: Swarabitan, var(--sw-font, sans-serif);
676
+ font-size: 0.9em;
677
+ }
678
+ .sw-arc-l, .sw-arc-r { flex: none; font-style: normal; }
679
+ /* The fill's geometry is measured off the font's own 'y' glyph: ink 0.11em
680
+ tall whose bottom sits 0.01em above the baseline. An empty inline-block
681
+ takes its bottom margin edge as its baseline, so -0.01em lifts the bar to
682
+ exactly where the hooks' flat stroke runs \u2014 no seam at either join. */
683
+ .sw-arc-m {
684
+ flex: 1 1 0;
685
+ min-width: 0;
686
+ align-self: baseline;
687
+ height: 0.11em;
688
+ margin-bottom: -0.01em;
689
+ background: currentColor;
690
+ }
547
691
 
548
692
  /* ---- annotation content ---- */
549
693
  .sw-beat { position: relative; display: inline-block; padding: 0 0.1em; }
@@ -587,8 +731,8 @@ var SWARALIPI_PRINT_CSS = `
587
731
  @page { margin: 16mm; }
588
732
  @media print {
589
733
  .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; }
734
+ .sw-scroll { overflow: visible !important; }
735
+ .sw-row { break-inside: avoid; page-break-inside: avoid; }
592
736
  .sw-part-name, .sw-header { break-after: avoid-page; page-break-after: avoid; }
593
737
  .sw-hl { background: none !important; }
594
738
  /* Keep each engraved staff system whole across a page break (the abcjs line
@@ -887,12 +1031,17 @@ ${SWARALIPI_STAFF_CSS}
887
1031
  ${SWARALIPI_PRINT_CSS}
888
1032
  ${watermarkCss(watermark)}
889
1033
  html, body { margin: 0; }
890
- body { color: #111; background: #fff; padding: 16px; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
1034
+ /* The printable width of A4 at the @page margin above. The sheet is one grid
1035
+ with one natural width, so the fit script needs a real page-sized box to
1036
+ measure against \u2014 the iframe's own width is not it. */
1037
+ body { width: 178mm; color: #111; background: #fff; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
891
1038
  .sw-sheet { max-width: 100%; }
892
1039
  .sw-footer { margin-top: 12px; padding-top: 6px; border-top: 1px solid #ddd; font-size: 12px; color: #555; text-align: right; }
893
1040
  </style>
894
1041
  </head>
895
- <body>${top}${bodyHtml}${bottom}</body>
1042
+ <body>${top}${bodyHtml}${bottom}
1043
+ <script>window.__swFit=function(){(${fitSwaralipi.toString()})(document,0.45,1,undefined,0)};window.__swFit()</script>
1044
+ </body>
896
1045
  </html>`;
897
1046
  }
898
1047
  function renderPrintDocument(doc, opts = {}) {
@@ -935,6 +1084,7 @@ function printHtmlInIframe(html) {
935
1084
  if (printed) return;
936
1085
  printed = true;
937
1086
  try {
1087
+ win.__swFit?.();
938
1088
  win.focus();
939
1089
  win.print();
940
1090
  } catch {
@@ -981,7 +1131,7 @@ function ensureNotationFonts() {
981
1131
  var sharedSheet;
982
1132
  function createElementClass() {
983
1133
  return class SwaralipiSheet extends HTMLElement {
984
- static observedAttributes = ["src", "lang", "hide-header"];
1134
+ static observedAttributes = ["src", "lang", "hide-header", "min-spacing", "min-size"];
985
1135
  #doc;
986
1136
  #root;
987
1137
  #adopted = false;
@@ -990,6 +1140,9 @@ function createElementClass() {
990
1140
  #translitLang;
991
1141
  #renderSeq = 0;
992
1142
  #staffOptions = {};
1143
+ #resize;
1144
+ #fittedAt = -1;
1145
+ #zoom = 1;
993
1146
  validate = false;
994
1147
  constructor() {
995
1148
  super();
@@ -1033,15 +1186,77 @@ function createElementClass() {
1033
1186
  if (newValue !== null) void this.#load(newValue);
1034
1187
  return;
1035
1188
  }
1189
+ if (name === "min-spacing" || name === "min-size") {
1190
+ this.fit();
1191
+ return;
1192
+ }
1036
1193
  if (name === "lang") this.#ensureTransliterator();
1037
1194
  this.#render();
1038
1195
  }
1039
1196
  connectedCallback() {
1040
1197
  this.#ensureTransliterator();
1041
1198
  this.#render();
1199
+ if (this.#resize === void 0 && typeof ResizeObserver !== "undefined") {
1200
+ this.#resize = new ResizeObserver(([entry]) => {
1201
+ const w = entry?.contentRect.width ?? -1;
1202
+ if (w === this.#fittedAt) return;
1203
+ this.#fittedAt = w;
1204
+ this.#fit();
1205
+ });
1206
+ this.#resize.observe(this);
1207
+ }
1042
1208
  }
1043
1209
  disconnectedCallback() {
1044
1210
  this.#abort?.abort();
1211
+ this.#resize?.disconnect();
1212
+ this.#resize = void 0;
1213
+ }
1214
+ /** Scale the notation to fit the available width, times the zoom factor. */
1215
+ #fit() {
1216
+ fitSwaralipi(this.#root, 0, this.#zoom, this.minSpacing, this.minSize);
1217
+ }
1218
+ /** Numeric attribute, or undefined for fitSwaralipi's own default. */
1219
+ #attrNum(name) {
1220
+ const value = parseFloat(this.getAttribute(name) ?? "");
1221
+ return Number.isFinite(value) ? value : void 0;
1222
+ }
1223
+ #setAttrNum(name, value) {
1224
+ if (value === void 0) this.removeAttribute(name);
1225
+ else this.setAttribute(name, String(value));
1226
+ }
1227
+ get minSpacing() {
1228
+ return this.#attrNum("min-spacing");
1229
+ }
1230
+ set minSpacing(value) {
1231
+ this.#setAttrNum("min-spacing", value);
1232
+ }
1233
+ get minSize() {
1234
+ return this.#attrNum("min-size");
1235
+ }
1236
+ set minSize(value) {
1237
+ this.#setAttrNum("min-size", value);
1238
+ }
1239
+ /** Re-fit the current render (public for hosts that resize the sheet
1240
+ * themselves, e.g. a tab that was hidden while the document loaded). */
1241
+ fit() {
1242
+ this.#fittedAt = -1;
1243
+ this.#fit();
1244
+ }
1245
+ get zoom() {
1246
+ return this.#zoom;
1247
+ }
1248
+ set zoom(factor) {
1249
+ this.#zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, factor));
1250
+ this.#fit();
1251
+ }
1252
+ zoomIn() {
1253
+ this.zoom = this.#zoom * ZOOM_STEP;
1254
+ }
1255
+ zoomOut() {
1256
+ this.zoom = this.#zoom / ZOOM_STEP;
1257
+ }
1258
+ resetZoom() {
1259
+ this.zoom = 1;
1045
1260
  }
1046
1261
  async #load(url) {
1047
1262
  this.#abort?.abort();
@@ -1073,6 +1288,7 @@ function createElementClass() {
1073
1288
  }
1074
1289
  #render() {
1075
1290
  const seq = ++this.#renderSeq;
1291
+ this.#zoom = 1;
1076
1292
  if (this.#doc === void 0) {
1077
1293
  this.#root.innerHTML = this.#styleFallback();
1078
1294
  return;
@@ -1091,6 +1307,7 @@ function createElementClass() {
1091
1307
  transliterate: this.#translitLang === lang ? this.#translit : void 0
1092
1308
  });
1093
1309
  this.#root.innerHTML = this.#styleFallback() + html;
1310
+ this.#fit();
1094
1311
  }
1095
1312
  #renderError(message) {
1096
1313
  const div = document.createElement("div");
@@ -1135,6 +1352,6 @@ function defineSwaralipiSheet(tagName = "swaralipi-sheet") {
1135
1352
  return ctor;
1136
1353
  }
1137
1354
 
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 };
1355
+ 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
1356
  //# sourceMappingURL=index.js.map
1140
1357
  //# sourceMappingURL=index.js.map