|
221 | 221 | return { r, splitter, pred: safeCompile(r.expr), extract: splitter ? GB.tryExtract(r.expr) : null }; |
222 | 222 | }); |
223 | 223 |
|
224 | | - const raw = []; // { name, unmatched, single, order, seq } |
| 224 | + const raw = []; // { name, unmatched, single, order, sortKey, seq } |
225 | 225 | const idToIdx = new Map(); // bucket id -> raw index |
226 | 226 | let seq = 0; |
227 | | - function add(name, id, order, unmatched, single) { |
228 | | - if (!idToIdx.has(id)) { idToIdx.set(id, raw.length); raw.push({ name, id, unmatched: !!unmatched, single: !!single, order, seq: seq++ }); } |
| 227 | + function add(name, id, order, sortKey, unmatched, single) { |
| 228 | + if (!idToIdx.has(id)) { idToIdx.set(id, raw.length); raw.push({ name, id, unmatched: !!unmatched, single: !!single, order, sortKey, seq: seq++ }); } |
229 | 229 | return idToIdx.get(id); |
230 | 230 | } |
231 | 231 |
|
232 | 232 | // Pre-create fixed (non-splitter) buckets so empty columns/lanes still show. |
233 | | - compiled.forEach((c, ri) => { if (!c.splitter && c.pred) add(c.r.name, "f" + ri, ri, false, false); }); |
| 233 | + compiled.forEach((c, ri) => { if (!c.splitter && c.pred) add(c.r.name, "f" + ri, ri, "", false, false); }); |
234 | 234 |
|
235 | 235 | const itemIdx = new Map(); |
236 | 236 | items.forEach((it) => { |
|
241 | 241 | if (c.splitter) { |
242 | 242 | const ex = c.extract ? c.extract(it) : null; |
243 | 243 | if (!ex) continue; // matched but no capture -> fall through to next rule / unmatched |
244 | | - idx = add(templName(c.r.name, ex), "d" + ri + "|" + ex.key, ri, false, false); |
| 244 | + idx = add(templName(c.r.name, ex), "d" + ri + "|" + ex.key, ri, String(ex.key), false, false); |
245 | 245 | } else { |
246 | | - idx = add(c.r.name, "f" + ri, ri, false, false); |
| 246 | + idx = add(c.r.name, "f" + ri, ri, "", false, false); |
247 | 247 | } |
248 | 248 | break; |
249 | 249 | } |
250 | | - if (idx == null && !hideUnmatched) idx = add("Unmatched", "u", 1e9, true, false); |
| 250 | + if (idx == null && !hideUnmatched) idx = add("Unmatched", "u", 1e9, "", true, false); |
251 | 251 | if (idx != null) itemIdx.set(it, idx); |
252 | 252 | }); |
253 | 253 |
|
254 | | - // Order buckets by rule position, then first-seen; Unmatched always last. |
255 | | - const order = raw.map((_, i) => i).sort((a, b) => raw[a].order - raw[b].order || raw[a].seq - raw[b].seq); |
256 | | - const buckets = order.map((i) => { const b = raw[i]; delete b.id; delete b.order; delete b.seq; return b; }); |
| 254 | + // Order buckets by rule position; within a splitter rule, alphabetically by |
| 255 | + // captured key (case-insensitive); Unmatched always last. |
| 256 | + const alpha = (a, b) => String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: "base" }); |
| 257 | + const order = raw.map((_, i) => i).sort((a, b) => |
| 258 | + raw[a].order - raw[b].order || alpha(raw[a].sortKey, raw[b].sortKey) || raw[a].seq - raw[b].seq); |
| 259 | + const buckets = order.map((i) => { const b = raw[i]; delete b.id; delete b.order; delete b.sortKey; delete b.seq; return b; }); |
257 | 260 | const remap = new Map(); order.forEach((oldIdx, newIdx) => remap.set(oldIdx, newIdx)); |
258 | 261 | const index = new Map(); itemIdx.forEach((v, k) => index.set(k, remap.get(v))); |
259 | 262 |
|
|
0 commit comments