diff --git a/src/i18n_pack.rs b/src/i18n_pack.rs index 5d7ea96..18f9491 100644 --- a/src/i18n_pack.rs +++ b/src/i18n_pack.rs @@ -94,10 +94,10 @@ const EN_END: &str = "\n };"; /// /// ⚠️ `T_LITERAL_COUNT` 是 `T("…")` **调用点**总数,不是键数,也不是去重后的键数 —— /// 三个集合各不相同(坑 99);说「这个数不该变」之前先确认它在数哪个集合。 -const ZH_KEY_COUNT: usize = 812; -const EN_KEY_COUNT: usize = 812; -const STATIC_ATTR_COUNT: usize = 333; -const STATIC_ATTR_DISTINCT: usize = 308; +const ZH_KEY_COUNT: usize = 811; +const EN_KEY_COUNT: usize = 811; +const STATIC_ATTR_COUNT: usize = 332; +const STATIC_ATTR_DISTINCT: usize = 307; const T_LITERAL_COUNT: usize = 542; const T_LITERAL_DISTINCT: usize = 433; @@ -195,6 +195,232 @@ fn scan_static_attributes(src: &str) -> Vec { out } +/// `data-i18n` 的四种静态属性形态(与 `scan_static_attributes` 的后缀集合一致)。 +const I18N_ATTRS: [&str; 4] = [ + "data-i18n", + "data-i18n-ph", + "data-i18n-title", + "data-i18n-label", +]; + +/// 从 `lt`(`<` 的下标)出发,返回该标签 `>` 的下标。 +/// +/// **必须跳过引号内的 `>`**:属性值里出现 `>` 是合法 HTML(本仓真实形态: +/// `title="搜索用户名 / 邮箱…"` 之类倒没有,但 `data-i18n` 的**值**由语言包决定, +/// 未来随时可能含尖括号)。一个被 `>` 提前截断的解析器会把后续标记错位、静默漏检。 +fn html_tag_end(src: &str, lt: usize) -> Option { + let b = src.as_bytes(); + let mut i = lt + 1; + let mut quote: Option = None; + while i < b.len() { + let c = b[i]; + match quote { + Some(q) => { + if c == q { + quote = None; + } + } + None => { + if c == b'"' || c == b'\'' { + quote = Some(c); + } else if c == b'>' { + return Some(i); + } + } + } + i += 1; + } + None +} + +/// 把起始标签体(`<` 与 `>` 之间,不含尖括号)切成 `(属性名, 属性值)`,值已去引号。 +/// +/// 逐属性切分而不是在整段里搜 `data-i18n=`:后者会命中**别的属性值里**的字符串 +/// (如 `title="data-i18n=x"`),把一个装饰性文本当成真属性。 +fn tag_attributes(body: &str) -> Vec<(String, String)> { + fn ws(c: u8) -> bool { + matches!(c, b' ' | b'\t' | b'\n' | b'\r') + } + let b = body.as_bytes(); + let mut out = Vec::new(); + let mut i = 0usize; + // 跳过标签名 + while i < b.len() && !ws(b[i]) && b[i] != b'/' { + i += 1; + } + while i < b.len() { + while i < b.len() && ws(b[i]) { + i += 1; + } + if i >= b.len() || b[i] == b'/' { + break; + } + let ns = i; + while i < b.len() && !ws(b[i]) && b[i] != b'=' { + i += 1; + } + let name = body[ns..i].to_string(); + let mut j = i; + while j < b.len() && ws(b[j]) { + j += 1; + } + if j < b.len() && b[j] == b'=' { + j += 1; + while j < b.len() && ws(b[j]) { + j += 1; + } + if j < b.len() && (b[j] == b'"' || b[j] == b'\'') { + let q = b[j]; + let vs = j + 1; + let mut e = vs; + while e < b.len() && b[e] != q { + e += 1; + } + out.push((name, body[vs..e.min(b.len())].to_string())); + i = (e + 1).min(b.len()); + } else { + let vs = j; + while j < b.len() && !ws(b[j]) { + j += 1; + } + out.push((name, body[vs..j].to_string())); + i = j; + } + } else { + out.push((name, String::new())); + i = j; + } + } + out +} + +/// `scan_i18n_nesting` 的结果。带阳性对照字段,好让「0 违规」不被误读为「扫描器瞎了」。 +struct I18nNestingScan { + /// 违规清单(已格式化成可直接断言的文本) + violations: Vec, + /// 带**文本** `data-i18n` 属性的元素个数(阳性对照:为 0 ⇒ 扫描器没在看) + text_carriers: usize, + /// 解析到的起始标签数(阳性对照:为 0 ⇒ 扫描器没在看) + start_tags: usize, + /// EOF 时仍未闭合的元素名(非空 ⇒ 语料把扫描器弄瞎了,拒绝据此判绿) + leftover: Vec, +} + +/// 扫描 `index.html`:`data-i18n*` 属性**绝不能**落在「带文本 `data-i18n` 的祖先元素」内部。 +/// +/// 为什么(`ui/js/i18n.js::applyStatic`): +/// ```js +/// els = document.querySelectorAll("[data-i18n]"); +/// for (i = 0; i < els.length; i++) { if (key && ZH[key]) els[i].innerHTML = t(key); } +/// ``` +/// 祖先那一步把 `innerHTML` **整体换成语包值** ⇒ 后代元素(连同它自己的 `data-i18n` 属性) +/// 被从文档里摘掉;随后循环再对那个**已分离**的节点设值 —— 无异常、无效果。 +/// 实测(jsdom):`

加额申请

` +/// 里那个 span 的 `isConnected` 为 `false`,两种语言下都不显示。 +/// +/// ⚠️ 只有**文本**属性(`data-i18n`)会砸后代:`data-i18n-title` / `-label` / `-ph` +/// 走 `setAttribute`,只写那一个属性,子标记原样保留。因此 `select#tx-range`(带 +/// `data-i18n-title`)里的五个 `