fix(w05): data-rows 宿主换 <article> —— div 宿主被行模版里第一个 </div> 截断,档案卡当场串行

浏览器走查逮到的真事故:renderRows 的 ROWS 正则按宿主自己的闭合标签非贪婪截取行模版,
<div class="lanes"> 的行模版在 lab 那格的 </div> 处被截半,第二行的展开被浏览器塞进
第一行没闭合的标签里 —— 屏上「候选内部料号 / LM2596S-5.0 / 不在库」三样各来自不同的行。
A2 立的「行容器用行内不会再出现的标签」说的正是宿主(它自己用的就是 <article> 包 <section>),
我在计划里引了这条却把它按在了行标签上。.lanes / .pvgrid 两个宿主同病同修。

防再犯钉:照 ROWS 正则同款截取行模版,断言截出来的内容标签配平(截断必然留下没闭合的
标签)—— 第一版写成「不含宿主闭合标签」是空话:非贪婪截取本来就停在闭合标签之前,
那条断言永远不会红;换配平判法后,两个宿主各自退回 div 的变异都真红、复原绿。

pnpm gates 退出码 0(2496 测);包版本 2.2.0 -> 2.2.1(2.2.0 已装库,安装恒新增)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-08-30 20:39:42 +08:00
co-authored by Claude Opus 5
parent 0f40bae0d1
commit 64fe4b43c9
3 changed files with 32 additions and 10 deletions
@@ -727,14 +727,14 @@ describe('件① 评估输入补全', () => {
});
it('**两条轨道:行源与写向同一元素,行模版是宿主直接子元素**(rowOf 认宿主直接子元素 —— 一条 lane = 一行;行模版里不许再有 <section>,行展开按第一个同名闭合标签截行内容)', () => {
expect(html).toMatch(/<div class="lanes" data-rows="mine:sub_eval\.part" data-edit="parts\.profile">\s*<section class="lane">/);
expect(html).toMatch(/<article class="lanes" data-rows="mine:sub_eval\.part" data-edit="parts\.profile">\s*<section class="lane">/);
const laneBody = /<section class="lane">([\s\S]*?)<\/section>/.exec(html)?.[1] ?? '';
expect(laneBody).not.toBe('');
expect(laneBody).not.toContain('<section');
});
it('**参数瓷贴单块平铺,瓷贴是宿主直接子元素** —— 按料嵌一层分栏会把一颗料的所有参数格并成一条记录(ui-kit view-container-collect-div-rows.test.ts 用真实 renderView+collect 钉死的「必然归并」)', () => {
expect(html).toMatch(/<div class="pvgrid" data-rows="mine:parts\.parameter" data-edit="parts\.parameter"><section class="pv">/);
expect(html).toMatch(/<article class="pvgrid" data-rows="mine:parts\.parameter" data-edit="parts\.parameter"><section class="pv">/);
const pvBody = /<section class="pv">([\s\S]*?)<\/section>/.exec(html)?.[1] ?? '';
expect(pvBody).not.toBe('');
expect(pvBody).not.toContain('<section');
@@ -745,6 +745,24 @@ describe('件① 评估输入补全', () => {
expect(html).toContain('.pv .rowkey{display:none}');
});
it('**data-rows 宿主的闭合标签不许出现在自己的行模版里** —— renderRows 的正则按宿主闭合标签非贪婪截取,撞上就把行模版截半、后一行嵌进前一行没闭合的标签里(2026-08-30 浏览器走查逮到:<div class="lanes"> 被 lab 那格的 </div> 截断,档案卡串行)。判法:照 ROWS 正则同款截取,截出来的行模版必须**标签配平**(截断必然留下没闭合的标签 —— 直接查「不含闭合标签」是空话,非贪婪截取本来就停在闭合标签之前)', () => {
const hosts = [...html.matchAll(/<([a-zA-Z][\w-]*)\b[^>]*\bdata-rows="[^"]+"[^>]*>([\s\S]*?)<\/\1>/g)];
expect(hosts.length).toBeGreaterThanOrEqual(2);
const VOID = new Set(['input', 'br', 'img', 'hr', 'meta', 'link']);
for (const m of hosts) {
const inner = m[2] ?? '';
const opens = new Map<string, number>();
for (const tm of inner.matchAll(/<(\/?)([a-zA-Z][\w-]*)\b/g)) {
const tag = (tm[2] ?? '').toLowerCase();
if (VOID.has(tag)) continue;
opens.set(tag, (opens.get(tag) ?? 0) + (tm[1] === '/' ? -1 : 1));
}
for (const [tag, n] of opens) {
expect(n, `data-rows 宿主 <${m[1]}> 截出的行模版里 <${tag}> 开 ${n} 个没闭合 —— 宿主的闭合标签把模版截半了`).toBe(0);
}
}
});
it('**类型不一致护栏**:distinct 是基座数,红带只在 v-2 档现身,模版不算不判', () => {
expect(html).toMatch(/class="jc-typewarn" data-class-of="sub_eval_part_distinct_chip_type_id"/);
expect(html).toContain('.jc-typewarn{display:none}');