#!/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 执行写入');