@k-l-lambda/lilylet 0.1.74 → 0.1.75

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.
@@ -21,6 +21,7 @@ export const HIGHLIGHT_RULES = [
21
21
  { re: /(?:\[instrument\b)/iy, scope: "header" },
22
22
  { re: /(?:\[genre\b)/iy, scope: "header" },
23
23
  { re: /(?:\[staves\b)/iy, scope: "header" },
24
+ { re: /(?:\[measures\b)/iy, scope: "header" },
24
25
  { re: /(?:\[auto-beam\b)/iy, scope: "header" },
25
26
  { re: /(?:\])/iy, scope: "squareBracket" },
26
27
  { re: /(?:"[^"]*")/iy, scope: "string" },
@@ -2,6 +2,7 @@ export * from "./types";
2
2
  export * from "./parser";
3
3
  export * from "./serializer";
4
4
  export * from "./staffLayout";
5
+ export * from "./measureLayout";
5
6
  import * as meiEncoder from "./meiEncoder";
6
7
  import * as musicXmlDecoder from "./musicXmlDecoder";
7
8
  import * as lilypondEncoder from "./lilypondEncoder";
@@ -2,6 +2,7 @@ export * from "./types.js";
2
2
  export * from "./parser.js";
3
3
  export * from "./serializer.js";
4
4
  export * from "./staffLayout.js";
5
+ export * from "./measureLayout.js";
5
6
  import * as meiEncoder from "./meiEncoder.js";
6
7
  import * as musicXmlDecoder from "./musicXmlDecoder.js";
7
8
  import * as lilypondEncoder from "./lilypondEncoder.js";
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Measure-layout (performance / repeat order) parser + expander, ported from
3
+ * lotus (`jison/measureLayout.jison` + `inc/measureLayout.ts`).
4
+ *
5
+ * A measure-layout string encodes the order in which notated measures are
6
+ * PLAYED — repeats, voltas, da-capo / ABA — independently of the notated
7
+ * sequence. It compiles to MEI `<expansion plist="#m1 #m2 #m1 …">`, which is
8
+ * what verovio's `expand` option unfolds for MIDI / time-map output.
9
+ *
10
+ * Two modes:
11
+ * - index-wise (`i:`, the default): leaves are 1-based measure INDICES.
12
+ * e.g. "2*[1..8]{9,10}, 11..16"
13
+ * - segment-wise (`s:`): leaves are segment LENGTHS; measures are
14
+ * auto-numbered left-to-right from 1. e.g. "s: 4 <2 6> 2"
15
+ *
16
+ * Constructs (both modes): `N` single, `A..B` inclusive range (index-wise
17
+ * only), `[ … ]` block, `N*[ … ]{ alt1, alt2 }` volta with alternate endings,
18
+ * `< main, rest >` ABA / da-capo (play `main rest main`).
19
+ *
20
+ * Expansion semantics mirror lotus `LayoutType`: `Full` unfolds every repeat
21
+ * (volta body repeated once per pass with each alternate ending; ABA → A B A) —
22
+ * this is the form used for MIDI unfolding.
23
+ */
24
+ export declare enum LayoutType {
25
+ Ordinary = "ordinary",// volta body once + all alternates appended; ABA → A B
26
+ Full = "full",// every repeat unfolded; ABA → A B A
27
+ Conservative = "conservative",
28
+ Once = "once"
29
+ }
30
+ export interface SingleMLayout {
31
+ kind: "single";
32
+ measure: number;
33
+ }
34
+ export interface BlockMLayout {
35
+ kind: "block";
36
+ seq: MeasureLayout[];
37
+ }
38
+ export interface VoltaMLayout {
39
+ kind: "volta";
40
+ times: number;
41
+ body: MeasureLayout[];
42
+ alternates: MeasureLayout[][] | null;
43
+ }
44
+ export interface ABAMLayout {
45
+ kind: "aba";
46
+ main: MeasureLayout;
47
+ rest: MeasureLayout[];
48
+ }
49
+ export type MeasureLayout = SingleMLayout | BlockMLayout | VoltaMLayout | ABAMLayout;
50
+ /**
51
+ * Parse a measure-layout string into its AST. The optional `i:` / `s:` prefix
52
+ * selects index-wise (default) or segment-wise mode; segment-wise leaves are
53
+ * resolved to 1-based indices here, so the returned AST is mode-agnostic.
54
+ */
55
+ export declare const parseMeasureLayout: (code: string) => MeasureLayout;
56
+ /**
57
+ * Expand a measure-layout AST to the performed order as 1-based measure
58
+ * indices. `LayoutType.Full` (the default) unfolds every repeat — the form
59
+ * used to build an MEI `<expansion>` for MIDI playback.
60
+ */
61
+ export declare const expandMeasureLayout: (layout: MeasureLayout, type?: LayoutType) => number[];
62
+ export interface VoltaEndingSpan {
63
+ number: number;
64
+ measures: number[];
65
+ }
66
+ export declare const collectVoltaSpans: (layout: MeasureLayout) => VoltaEndingSpan[];
67
+ /** Serialize an AST back to the canonical index-wise DSL string. */
68
+ export declare const serializeMeasureLayout: (layout: MeasureLayout) => string;
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Measure-layout (performance / repeat order) parser + expander, ported from
3
+ * lotus (`jison/measureLayout.jison` + `inc/measureLayout.ts`).
4
+ *
5
+ * A measure-layout string encodes the order in which notated measures are
6
+ * PLAYED — repeats, voltas, da-capo / ABA — independently of the notated
7
+ * sequence. It compiles to MEI `<expansion plist="#m1 #m2 #m1 …">`, which is
8
+ * what verovio's `expand` option unfolds for MIDI / time-map output.
9
+ *
10
+ * Two modes:
11
+ * - index-wise (`i:`, the default): leaves are 1-based measure INDICES.
12
+ * e.g. "2*[1..8]{9,10}, 11..16"
13
+ * - segment-wise (`s:`): leaves are segment LENGTHS; measures are
14
+ * auto-numbered left-to-right from 1. e.g. "s: 4 <2 6> 2"
15
+ *
16
+ * Constructs (both modes): `N` single, `A..B` inclusive range (index-wise
17
+ * only), `[ … ]` block, `N*[ … ]{ alt1, alt2 }` volta with alternate endings,
18
+ * `< main, rest >` ABA / da-capo (play `main rest main`).
19
+ *
20
+ * Expansion semantics mirror lotus `LayoutType`: `Full` unfolds every repeat
21
+ * (volta body repeated once per pass with each alternate ending; ABA → A B A) —
22
+ * this is the form used for MIDI unfolding.
23
+ */
24
+ export var LayoutType;
25
+ (function (LayoutType) {
26
+ LayoutType["Ordinary"] = "ordinary";
27
+ LayoutType["Full"] = "full";
28
+ LayoutType["Conservative"] = "conservative";
29
+ LayoutType["Once"] = "once";
30
+ })(LayoutType || (LayoutType = {}));
31
+ const single = (measure) => ({ kind: "single", measure });
32
+ const block = (seq) => ({ kind: "block", seq: seq });
33
+ const volta = (times, body, alternates) => ({ kind: "volta", times, body: body, alternates: alternates });
34
+ const aba = (main, rest) => ({ kind: "aba", main: main, rest: rest });
35
+ const segment = (length) => ({ kind: "segment", length });
36
+ const SYMS = new Set(["*", ",", "[", "]", "<", ">", "{", "}"]);
37
+ const tokenize = (code) => {
38
+ const tokens = [];
39
+ let i = 0;
40
+ const n = code.length;
41
+ while (i < n) {
42
+ const c = code[i];
43
+ if (/\s/.test(c)) {
44
+ i++;
45
+ continue;
46
+ }
47
+ if (c === "." && code[i + 1] === ".") {
48
+ tokens.push({ t: "range" });
49
+ i += 2;
50
+ continue;
51
+ }
52
+ if (c >= "1" && c <= "9") {
53
+ let j = i + 1;
54
+ while (j < n && code[j] >= "0" && code[j] <= "9")
55
+ j++;
56
+ tokens.push({ t: "num", v: Number(code.slice(i, j)) });
57
+ i = j;
58
+ continue;
59
+ }
60
+ if (SYMS.has(c)) {
61
+ tokens.push({ t: "sym", v: c });
62
+ i++;
63
+ continue;
64
+ }
65
+ // mode prefix: letters followed by ':'
66
+ if (/[a-z]/i.test(c)) {
67
+ let j = i;
68
+ while (j < n && /[a-z]/i.test(code[j]))
69
+ j++;
70
+ const word = code.slice(i, j);
71
+ if (code[j] === ":") {
72
+ if (word === "i" || word === "s") {
73
+ tokens.push({ t: "mode", v: word });
74
+ i = j + 1;
75
+ continue;
76
+ }
77
+ throw new Error(`unknown measure-layout mode prefix: "${word}:"`);
78
+ }
79
+ throw new Error(`unexpected token in measure-layout: "${word}"`);
80
+ }
81
+ throw new Error(`unexpected character in measure-layout: "${c}" at ${i}`);
82
+ }
83
+ tokens.push({ t: "eof" });
84
+ return tokens;
85
+ };
86
+ // ── Recursive-descent parser ─────────────────────────────────────────────
87
+ // Faithful to lotus's jison grammar. The two modes share the same construct
88
+ // shapes; the only differences are: segment-wise leaves are SEGMENT LENGTHS
89
+ // (not indices), there are no ranges in segment-wise, and the ABA separator is
90
+ // a comma in index-wise but plain juxtaposition in segment-wise.
91
+ class Parser {
92
+ tokens;
93
+ pos = 0;
94
+ segMode;
95
+ constructor(tokens, segMode) {
96
+ this.tokens = tokens;
97
+ this.segMode = segMode;
98
+ }
99
+ peek() { return this.tokens[this.pos]; }
100
+ isSym(v) {
101
+ const tk = this.peek();
102
+ return tk.t === "sym" && tk.v === v;
103
+ }
104
+ expectSym(v) {
105
+ if (!this.isSym(v))
106
+ throw new Error(`measure-layout: expected "${v}", got ${JSON.stringify(this.peek())}`);
107
+ this.pos++;
108
+ }
109
+ takeNum() {
110
+ const tk = this.peek();
111
+ if (tk.t !== "num")
112
+ throw new Error(`measure-layout: expected number, got ${JSON.stringify(tk)}`);
113
+ this.pos++;
114
+ return tk.v;
115
+ }
116
+ // top-level sequence → a Block (or the single bare block if it's the only item)
117
+ parseTop() {
118
+ const seq = this.parseSequence();
119
+ if (seq.length === 1 && seq[0].kind === "block")
120
+ return seq[0];
121
+ return block(seq);
122
+ }
123
+ // A comma-separated (index-wise) or juxtaposed (segment-wise) sequence of
124
+ // items, terminated by EOF or a closing ] > } token.
125
+ parseSequence() {
126
+ const seq = [];
127
+ for (;;) {
128
+ const tk = this.peek();
129
+ if (tk.t === "eof" || this.isSym("]") || this.isSym(">") || this.isSym("}"))
130
+ break;
131
+ if (!this.segMode && tk.t === "num" && this.tokens[this.pos + 1]?.t === "range") {
132
+ // index-wise range A..B
133
+ const start = this.takeNum();
134
+ this.pos++; // consume ".."
135
+ const end = this.takeNum();
136
+ if (!(end >= start))
137
+ throw new Error(`invalid measure range: ${start}..${end}`);
138
+ for (let m = start; m <= end; m++)
139
+ seq.push(single(m));
140
+ }
141
+ else {
142
+ seq.push(this.parseItem());
143
+ }
144
+ // index-wise items are comma-separated; consume an optional comma
145
+ if (!this.segMode && this.isSym(","))
146
+ this.pos++;
147
+ }
148
+ return seq;
149
+ }
150
+ parseItem() {
151
+ // volta: N *[ … ]{ … }
152
+ if (this.peek().t === "num" && this.tokens[this.pos + 1]?.t === "sym" && this.tokens[this.pos + 1].v === "*") {
153
+ const times = this.takeNum();
154
+ this.pos++; // consume '*'
155
+ const body = this.parseBlock();
156
+ let alternates = null;
157
+ if (this.isSym("{"))
158
+ alternates = this.parseAlternates();
159
+ return volta(times, body, alternates);
160
+ }
161
+ // block
162
+ if (this.isSym("["))
163
+ return block(this.parseBlock());
164
+ // ABA < main rest >
165
+ if (this.isSym("<"))
166
+ return this.parseAba();
167
+ // leaf
168
+ const v = this.takeNum();
169
+ return this.segMode ? segment(v) : single(v);
170
+ }
171
+ parseBlock() {
172
+ this.expectSym("[");
173
+ const seq = this.parseSequence();
174
+ this.expectSym("]");
175
+ return seq;
176
+ }
177
+ parseAlternates() {
178
+ this.expectSym("{");
179
+ const seq = this.parseSequence();
180
+ this.expectSym("}");
181
+ // each item becomes an ending: unwrap a block into its inner seq, wrap a
182
+ // bare item into a 1-element seq (lotus's `alternates` helper)
183
+ return seq.map(item => item.kind === "block" ? item.seq : [item]);
184
+ }
185
+ parseAba() {
186
+ this.expectSym("<");
187
+ const main = this.parseItem();
188
+ if (!this.segMode)
189
+ this.expectSym(","); // index-wise: comma between main and rest
190
+ const rest = this.parseSequence();
191
+ this.expectSym(">");
192
+ return aba(main, rest);
193
+ }
194
+ }
195
+ // ── Segment-wise → index assignment ──────────────────────────────────────
196
+ // In segment-wise mode the leaves are segment lengths; measures auto-number
197
+ // left-to-right from 1. This walk replaces each SegmentMLayout with the right
198
+ // run of SingleMLayout, threading a shared running index (lotus's `serialize`).
199
+ const assignIndices = (item, state) => {
200
+ switch (item.kind) {
201
+ case "single":
202
+ return item;
203
+ case "segment": {
204
+ // a lone segment (e.g. the `main` of a segment-wise ABA) → run of singles
205
+ const seq = [];
206
+ for (let k = 0; k < item.length; k++)
207
+ seq.push(single(state.index + k));
208
+ state.index += item.length;
209
+ return seq.length === 1 ? seq[0] : { kind: "block", seq };
210
+ }
211
+ case "block":
212
+ return { kind: "block", seq: spreadSegments(item.seq, state) };
213
+ case "volta":
214
+ return {
215
+ kind: "volta",
216
+ times: item.times,
217
+ body: spreadSegments(item.body, state),
218
+ alternates: item.alternates ? item.alternates.map(seq => spreadSegments(seq, state)) : null,
219
+ };
220
+ case "aba":
221
+ return {
222
+ kind: "aba",
223
+ main: assignIndices(item.main, state),
224
+ rest: spreadSegments(item.rest, state),
225
+ };
226
+ }
227
+ };
228
+ const spreadSegments = (seq, state) => {
229
+ const out = [];
230
+ for (const item of seq) {
231
+ if (item.kind === "segment") {
232
+ for (let k = 0; k < item.length; k++)
233
+ out.push(single(state.index + k));
234
+ state.index += item.length;
235
+ }
236
+ else {
237
+ out.push(assignIndices(item, state));
238
+ }
239
+ }
240
+ return out;
241
+ };
242
+ /**
243
+ * Parse a measure-layout string into its AST. The optional `i:` / `s:` prefix
244
+ * selects index-wise (default) or segment-wise mode; segment-wise leaves are
245
+ * resolved to 1-based indices here, so the returned AST is mode-agnostic.
246
+ */
247
+ export const parseMeasureLayout = (code) => {
248
+ const tokens = tokenize(code);
249
+ let segMode = false;
250
+ let start = 0;
251
+ if (tokens[0].t === "mode") {
252
+ segMode = tokens[0].v === "s";
253
+ start = 1;
254
+ }
255
+ const parser = new Parser(tokens.slice(start), segMode);
256
+ const ast = parser.parseTop();
257
+ if (segMode) {
258
+ const state = { index: 1 };
259
+ return assignIndices(ast, state);
260
+ }
261
+ return ast;
262
+ };
263
+ // ── Expander ─────────────────────────────────────────────────────────────
264
+ // Port of lotus's MeasureLayout.serialize(type). Returns the performed order
265
+ // as a flat list of 1-based measure indices.
266
+ const spreadSeq = (seq, type) => [].concat(...seq.map(item => expand(item, type)));
267
+ const expand = (item, type) => {
268
+ switch (item.kind) {
269
+ case "single":
270
+ return [item.measure];
271
+ case "block":
272
+ return spreadSeq(item.seq, type);
273
+ case "volta": {
274
+ const bodySeq = spreadSeq(item.body, LayoutType.Ordinary);
275
+ if (item.alternates) {
276
+ const alternateSeqs = item.alternates.map(seq => spreadSeq(seq, LayoutType.Ordinary));
277
+ const lastAlternateSeq = alternateSeqs[alternateSeqs.length - 1];
278
+ switch (type) {
279
+ case LayoutType.Ordinary:
280
+ return [].concat(bodySeq, ...alternateSeqs);
281
+ case LayoutType.Conservative:
282
+ case LayoutType.Full: {
283
+ const prior = [].concat(...Array(item.times - 1).fill(null).map((_, i) => [
284
+ ...bodySeq,
285
+ ...alternateSeqs[i % (item.times - 1)],
286
+ ]));
287
+ return [...prior, ...bodySeq, ...lastAlternateSeq];
288
+ }
289
+ case LayoutType.Once:
290
+ return [...bodySeq, ...lastAlternateSeq];
291
+ }
292
+ }
293
+ else {
294
+ switch (type) {
295
+ case LayoutType.Ordinary:
296
+ case LayoutType.Conservative:
297
+ case LayoutType.Once:
298
+ return bodySeq;
299
+ case LayoutType.Full:
300
+ return [].concat(...Array(item.times).fill(null).map(() => bodySeq));
301
+ }
302
+ }
303
+ return bodySeq; // unreachable; keeps the type checker happy
304
+ }
305
+ case "aba": {
306
+ const seqA = expand(item.main, type);
307
+ const seqA_ = expand(item.main, LayoutType.Once);
308
+ const seqB = spreadSeq(item.rest, type);
309
+ switch (type) {
310
+ case LayoutType.Ordinary: // A B
311
+ return [...seqA, ...seqB];
312
+ case LayoutType.Once: // B A'
313
+ return [...seqB, ...seqA_];
314
+ case LayoutType.Conservative: // A B A'
315
+ case LayoutType.Full: // A B A'
316
+ return [...seqA, ...seqB, ...seqA_];
317
+ }
318
+ return [...seqA, ...seqB, ...seqA_];
319
+ }
320
+ }
321
+ };
322
+ /**
323
+ * Expand a measure-layout AST to the performed order as 1-based measure
324
+ * indices. `LayoutType.Full` (the default) unfolds every repeat — the form
325
+ * used to build an MEI `<expansion>` for MIDI playback.
326
+ */
327
+ export const expandMeasureLayout = (layout, type = LayoutType.Full) => expand(layout, type);
328
+ export const collectVoltaSpans = (layout) => {
329
+ const spans = [];
330
+ // Indices a node covers in notation order (each measure once, ascending).
331
+ const indicesOf = (node) => {
332
+ switch (node.kind) {
333
+ case "single": return [node.measure];
334
+ case "block": return [].concat(...node.seq.map(indicesOf));
335
+ case "volta": return [].concat(...node.body.map(indicesOf), ...(node.alternates ? node.alternates.flat().map(indicesOf).flat() : []));
336
+ case "aba": return [].concat(indicesOf(node.main), ...node.rest.map(indicesOf));
337
+ }
338
+ };
339
+ const walk = (node) => {
340
+ switch (node.kind) {
341
+ case "block":
342
+ node.seq.forEach(walk);
343
+ break;
344
+ case "aba":
345
+ walk(node.main);
346
+ node.rest.forEach(walk);
347
+ break;
348
+ case "volta":
349
+ node.body.forEach(walk);
350
+ if (node.alternates) {
351
+ node.alternates.forEach((alt, ai) => {
352
+ const measures = [].concat(...alt.map(indicesOf)).sort((a, b) => a - b);
353
+ if (measures.length)
354
+ spans.push({ number: ai + 1, measures });
355
+ alt.forEach(walk);
356
+ });
357
+ }
358
+ break;
359
+ default: break;
360
+ }
361
+ };
362
+ walk(layout);
363
+ return spans;
364
+ };
365
+ // ── Serializer ───────────────────────────────────────────────────────────
366
+ // Canonical index-wise form, collapsing runs of ≥3 consecutive singles into
367
+ // "A..B" ranges (lotus's seqToCode).
368
+ const seqToCode = (seq, withBrackets = false) => {
369
+ let code = "";
370
+ let inRange = false;
371
+ for (let i = 0; i < seq.length; i++) {
372
+ const prev = seq[i - 1], cur = seq[i], next = seq[i + 1];
373
+ const middle = prev?.kind === "single" && cur.kind === "single" && next?.kind === "single"
374
+ && prev.measure + 1 === cur.measure
375
+ && cur.measure + 1 === next.measure;
376
+ if (middle) {
377
+ if (!inRange) {
378
+ code += "..";
379
+ inRange = true;
380
+ }
381
+ }
382
+ else {
383
+ if (i > 0 && !inRange)
384
+ code += ", ";
385
+ inRange = false;
386
+ code += layoutToCode(cur);
387
+ }
388
+ }
389
+ return withBrackets ? `[${code}]` : code;
390
+ };
391
+ const layoutToCode = (item) => {
392
+ switch (item.kind) {
393
+ case "single":
394
+ return item.measure.toString();
395
+ case "block":
396
+ return seqToCode(item.seq, true);
397
+ case "volta": {
398
+ let code = `${item.times}*${seqToCode(item.body, true)}`;
399
+ if (item.alternates)
400
+ code += "{" + item.alternates.map(seq => seqToCode(seq, seq.length > 1)).join(", ") + "}";
401
+ return code;
402
+ }
403
+ case "aba":
404
+ return "<" + layoutToCode(item.main) + ", " + seqToCode(item.rest) + ">";
405
+ }
406
+ };
407
+ /** Serialize an AST back to the canonical index-wise DSL string. */
408
+ export const serializeMeasureLayout = (layout) => {
409
+ if (layout.kind === "block")
410
+ return seqToCode(layout.seq);
411
+ return layoutToCode(layout);
412
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Derive a performance-order measure-layout string from a MusicXML part's
3
+ * repeat / volta / navigation markup.
4
+ *
5
+ * Input: the raw per-measure repeat structure collected from `<barline>`
6
+ * (forward/backward repeats, 1st/2nd-ending voltas) and `<direction><sound>`
7
+ * (D.C./D.S./Coda/Fine jumps), in 1-based measure index order (one entry per
8
+ * decoded lilylet measure — no merging of split bars, so indices map 1:1).
9
+ *
10
+ * Output: a lotus-style measure-layout string (e.g. "2*[1..4]",
11
+ * "1, 2*[2..7]{[8,9], 10}, 11..15", "1..19, 3..10, 20, 21") suitable for
12
+ * `metadata.measureLayout`, which the MEI encoder turns into an <expansion> and
13
+ * verovio unfolds for MIDI. See [[lilylet-measure-layout]].
14
+ *
15
+ * Strategy: produce the flat performed-order index sequence by simulating the
16
+ * repeats/jumps (this is always correct, even for D.C.-al-Fine-over-a-volta
17
+ * which the structured DSL can't compactly express), then COMPACT it back into
18
+ * the DSL via the shared serializer when the structure is a clean single
19
+ * repeat/volta/segno-jump. Compaction is best-effort; the flat form is the
20
+ * always-valid fallback.
21
+ */
22
+ export interface MeasureRepeatInfo {
23
+ index: number;
24
+ repeatStart?: boolean;
25
+ repeatEnd?: boolean;
26
+ repeatTimes?: number;
27
+ endingStart?: number;
28
+ endingStop?: number;
29
+ segno?: boolean;
30
+ coda?: boolean;
31
+ tocoda?: boolean;
32
+ dacapo?: boolean;
33
+ dalsegno?: boolean;
34
+ fine?: boolean;
35
+ }
36
+ /**
37
+ * Build a measure-layout string from a part's repeat structure, or undefined if
38
+ * there is nothing to unfold (no repeats/voltas/navigation). The result is
39
+ * guaranteed to expand (via measureLayout.ts) to the simulated performed order.
40
+ */
41
+ export declare const buildMeasureLayout: (infos: MeasureRepeatInfo[], totalMeasures: number) => string | undefined;
42
+ export declare const collectRepeatInfo: (partEl: Element) => MeasureRepeatInfo[];
43
+ /**
44
+ * Derive metadata.measureLayout from a <part> element, or undefined if no
45
+ * repeats/voltas/navigation are present. Convenience wrapper combining the
46
+ * collector + builder; never throws (returns undefined on any failure).
47
+ */
48
+ export declare const measureLayoutFromPart: (partEl: Element) => string | undefined;