P7: tests/_behaviors.js新建(8动词+@input/@doc定位+同步pump+page-load
幂等缓存); _runtime聚合+重载清缓存; _template引入; 6演示页标注;
79测试页重生成。关键修:runner兜底重跑致行为二轮误报,加缓存解决。
P8: tools/migrate-rtl.mjs新建(白名单dry-run/--write); 28个CSS转逻辑
属性;物理清零(排除auto/拼接/居中14处人工例外);三页RTL零溢出。
回归:100%(79/79页,1009/1009断言,N/A 35)。
75 lines
3.3 KiB
JavaScript
75 lines
3.3 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* migrate-rtl.mjs — S3-P8 RTL 支持:物理属性 → 逻辑属性批量迁移
|
||
*
|
||
* 只做语义安全的替换,例外一律跳过并报告(由人复核):
|
||
* - margin-left/right: auto → 跳过(flex 推送语义,inline-start: auto 不等价)
|
||
* - border-left-width: 0 / 按钮组拼接 → 跳过(顺序语义,需人工镜像 border-radius)
|
||
* - left:0 + right:0 同规则并存 → 跳过(居中语义,inset-inline:0 等价但保持原样降噪)
|
||
* - text-align: left/right → start/end(安全)
|
||
* - margin/padding/border-inline → 直接映射(安全)
|
||
* - inset-inline-start/end → 只处理纯 left/right 定位(有例外表)
|
||
*
|
||
* 幂等:已含逻辑属性的行不再处理。输出变更清单 + 跳过清单。
|
||
* 用法:node tools/migrate-rtl.mjs [--write](默认 dry-run 只报告)
|
||
*/
|
||
import { readFileSync, writeFileSync, readdirSync } from 'fs';
|
||
import { fileURLToPath } from 'url';
|
||
import { dirname, join } from 'path';
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const ROOT = join(__dirname, '..');
|
||
const WRITE = process.argv.includes('--write');
|
||
|
||
const FW = join(ROOT, 'frameworks');
|
||
const files = readdirSync(FW).filter((f) => f.endsWith('.css'));
|
||
|
||
const changed = [];
|
||
const skipped = [];
|
||
|
||
for (const f of files) {
|
||
const p = join(FW, f);
|
||
const src = readFileSync(p, 'utf8');
|
||
const lines = src.split('\n');
|
||
let dirty = false;
|
||
const out = lines.map((line) => {
|
||
let s = line;
|
||
const note = (why) => skipped.push(`${f}: ${line.trim().slice(0, 80)} — ${why}`);
|
||
|
||
// 已有逻辑属性:跳过整行
|
||
if (/margin-inline|padding-inline|inset-inline|border-inline|text-align:\s*(start|end)/.test(s)) return s;
|
||
|
||
// auto 例外
|
||
if (/margin-(left|right):\s*auto/.test(s)) { note('auto 推送语义,人工确认'); return s; }
|
||
// 按钮组/拼接边框例外
|
||
if (/border-(left|right)-(width|color)/.test(s)) { note('拼接边框,需人工镜像'); return s; }
|
||
// left:0 + right:0 同行并存(居中/撑满)
|
||
if (/left:\s*0/.test(s) && /right:\s*0/.test(s)) { note('left:0+right:0 并存,保持原样'); return s; }
|
||
|
||
const before = s;
|
||
s = s.replace(/margin-left(?!\s*:\s*auto)/g, 'margin-inline-start')
|
||
.replace(/margin-right(?!\s*:\s*auto)/g, 'margin-inline-end')
|
||
.replace(/padding-left/g, 'padding-inline-start')
|
||
.replace(/padding-right/g, 'padding-inline-end')
|
||
.replace(/text-align:\s*left/g, 'text-align: start')
|
||
.replace(/text-align:\s*right/g, 'text-align: end');
|
||
// 纯 border-left/right(含 border-left: 2px solid 这类简写):转 inline-start/end
|
||
// 但 border-left-width/color 已在上游跳过
|
||
s = s.replace(/border-left(\s*:)/g, 'border-inline-start$1')
|
||
.replace(/border-right(\s*:)/g, 'border-inline-end$1');
|
||
if (s !== before) dirty = true;
|
||
return s;
|
||
});
|
||
if (dirty) {
|
||
changed.push(f);
|
||
if (WRITE) writeFileSync(p, out.join('\n'));
|
||
}
|
||
}
|
||
|
||
console.log(`扫描 CSS: ${files.length} 个`);
|
||
console.log(`可迁移: ${changed.length} 个${WRITE ? '(已写入)' : '(dry-run,未写入)'}`);
|
||
changed.forEach((f) => console.log(' M ' + f));
|
||
console.log(`跳过(需人工): ${skipped.length} 处`);
|
||
skipped.slice(0, 20).forEach((s) => console.log(' S ' + s));
|
||
if (!WRITE) console.log('加 --write 执行写入');
|