@k-l-lambda/lilylet 0.1.78 → 0.1.79

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.
@@ -205,6 +205,51 @@ const renderRepeatSpan = (infos, lo, hi) => {
205
205
  parts.push(rangeCode(lastStop + 1, hi));
206
206
  return parts.join(", ");
207
207
  };
208
+ // Render a whole piece that has one OR MORE plain repeat/volta sections in
209
+ // sequence (no navigation). Real scores are usually multi-section (AABB minuets,
210
+ // sonata exposition+recap), which the single-span renderRepeatSpan can't express
211
+ // — it bails when it sees >1 repeat-end. Here we cut the piece at section
212
+ // boundaries (each cut ends a repeat section, voltas included) and render each
213
+ // span independently, joining with commas. Any non-repeated measures between two
214
+ // sections are absorbed as the `pre` of the following span. Returns null if any
215
+ // section isn't a clean single repeat/volta (caller then falls back to flat).
216
+ const renderRepeatSections = (infos, total) => {
217
+ const repeatEnds = infos.filter(i => i.repeatEnd).map(i => i.index).sort((a, b) => a - b);
218
+ const repeatStarts = infos.filter(i => i.repeatStart).map(i => i.index).sort((a, b) => a - b);
219
+ if (repeatEnds.length <= 1)
220
+ return renderRepeatSpan(infos, 1, total);
221
+ const endingStops = infos.filter(i => i.endingStop !== undefined).map(i => i.index);
222
+ const parts = [];
223
+ let prevHi = 0;
224
+ for (const e of repeatEnds) {
225
+ const lo = prevHi + 1;
226
+ // The next section's repeat-start bounds this section's voltas (2nd/3rd
227
+ // endings sit AFTER the repeat-end but BEFORE the next section starts).
228
+ const nextStart = repeatStarts.find(s => s > e);
229
+ const sectionEndingStarts = infos
230
+ .filter(i => i.endingStart !== undefined && i.index >= lo && (nextStart === undefined || i.index < nextStart))
231
+ .map(i => i.index)
232
+ .sort((a, b) => a - b);
233
+ let hi;
234
+ if (sectionEndingStarts.length) {
235
+ const lastStart = sectionEndingStarts[sectionEndingStarts.length - 1];
236
+ // the last ending's stop closes the section; else the ending start itself
237
+ const stop = endingStops.filter(s => s >= lastStart).sort((a, b) => a - b)[0];
238
+ hi = stop ?? lastStart;
239
+ }
240
+ else
241
+ hi = e;
242
+ const code = renderRepeatSpan(infos, lo, hi);
243
+ if (code === null)
244
+ return null;
245
+ parts.push(code);
246
+ prevHi = hi;
247
+ }
248
+ // trailing non-repeated tail
249
+ if (prevHi < total)
250
+ parts.push(rangeCode(prevHi + 1, total));
251
+ return parts.join(", ");
252
+ };
208
253
  const tryStructured = (infos, total) => {
209
254
  const dacapo = infos.find(i => i.dacapo);
210
255
  const dalsegno = infos.some(i => i.dalsegno);
@@ -223,8 +268,10 @@ const tryStructured = (infos, total) => {
223
268
  }
224
269
  if (dalsegno || tocoda || infos.some(i => i.fine || i.segno || i.coda))
225
270
  return null;
226
- // No navigation: a single plain repeat/volta over the whole piece.
227
- return renderRepeatSpan(infos, 1, total);
271
+ // No navigation: one or more plain repeat/volta sections in sequence. Most
272
+ // real pieces (AABB minuets, sonata exposition+recap) have ≥2 repeat sections;
273
+ // render each independently and join with commas.
274
+ return renderRepeatSections(infos, total);
228
275
  };
229
276
  // Construct the ABA form <main, rest> for a D.C. case. main = the volta/repeat
230
277
  // structure; rest = the post-volta tail before the D.C. measure. The Once
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k-l-lambda/lilylet",
3
- "version": "0.1.78",
3
+ "version": "0.1.79",
4
4
  "description": "Lilylet is a lilyopnd-like sheet music language designed for Markdown rendering and symbolic music representation in AIGC applications.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -214,6 +214,52 @@ const renderRepeatSpan = (infos: MeasureRepeatInfo[], lo: number, hi: number): s
214
214
  return parts.join(", ");
215
215
  };
216
216
 
217
+ // Render a whole piece that has one OR MORE plain repeat/volta sections in
218
+ // sequence (no navigation). Real scores are usually multi-section (AABB minuets,
219
+ // sonata exposition+recap), which the single-span renderRepeatSpan can't express
220
+ // — it bails when it sees >1 repeat-end. Here we cut the piece at section
221
+ // boundaries (each cut ends a repeat section, voltas included) and render each
222
+ // span independently, joining with commas. Any non-repeated measures between two
223
+ // sections are absorbed as the `pre` of the following span. Returns null if any
224
+ // section isn't a clean single repeat/volta (caller then falls back to flat).
225
+ const renderRepeatSections = (infos: MeasureRepeatInfo[], total: number): string | null => {
226
+ const repeatEnds = infos.filter(i => i.repeatEnd).map(i => i.index).sort((a, b) => a - b);
227
+ const repeatStarts = infos.filter(i => i.repeatStart).map(i => i.index).sort((a, b) => a - b);
228
+ if (repeatEnds.length <= 1) return renderRepeatSpan(infos, 1, total);
229
+
230
+ const endingStops = infos.filter(i => i.endingStop !== undefined).map(i => i.index);
231
+
232
+ const parts: string[] = [];
233
+ let prevHi = 0;
234
+ for (const e of repeatEnds) {
235
+ const lo = prevHi + 1;
236
+ // The next section's repeat-start bounds this section's voltas (2nd/3rd
237
+ // endings sit AFTER the repeat-end but BEFORE the next section starts).
238
+ const nextStart = repeatStarts.find(s => s > e);
239
+ const sectionEndingStarts = infos
240
+ .filter(i => i.endingStart !== undefined && i.index >= lo && (nextStart === undefined || i.index < nextStart))
241
+ .map(i => i.index)
242
+ .sort((a, b) => a - b);
243
+
244
+ let hi: number;
245
+ if (sectionEndingStarts.length) {
246
+ const lastStart = sectionEndingStarts[sectionEndingStarts.length - 1];
247
+ // the last ending's stop closes the section; else the ending start itself
248
+ const stop = endingStops.filter(s => s >= lastStart).sort((a, b) => a - b)[0];
249
+ hi = stop ?? lastStart;
250
+ }
251
+ else hi = e;
252
+
253
+ const code = renderRepeatSpan(infos, lo, hi);
254
+ if (code === null) return null;
255
+ parts.push(code);
256
+ prevHi = hi;
257
+ }
258
+ // trailing non-repeated tail
259
+ if (prevHi < total) parts.push(rangeCode(prevHi + 1, total));
260
+ return parts.join(", ");
261
+ };
262
+
217
263
  const tryStructured = (infos: MeasureRepeatInfo[], total: number): string | null => {
218
264
  const dacapo = infos.find(i => i.dacapo);
219
265
  const dalsegno = infos.some(i => i.dalsegno);
@@ -233,8 +279,10 @@ const tryStructured = (infos: MeasureRepeatInfo[], total: number): string | null
233
279
  }
234
280
  if (dalsegno || tocoda || infos.some(i => i.fine || i.segno || i.coda)) return null;
235
281
 
236
- // No navigation: a single plain repeat/volta over the whole piece.
237
- return renderRepeatSpan(infos, 1, total);
282
+ // No navigation: one or more plain repeat/volta sections in sequence. Most
283
+ // real pieces (AABB minuets, sonata exposition+recap) have ≥2 repeat sections;
284
+ // render each independently and join with commas.
285
+ return renderRepeatSections(infos, total);
238
286
  };
239
287
 
240
288
  // Construct the ABA form <main, rest> for a D.C. case. main = the volta/repeat