| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """门户内嵌文档 (iframe srcdoc) 的章节锚点修复 — 幂等注入器 (xzy 测试报告 2026-09-08 P1-3).
- 病灶: 报告经 `ovf.srcdoc = 模板` 挂进 iframe。srcdoc 文档的**基地址继承父页**, 所以报告里的
- `<a href="#section-3">` 解析成 `http://127.0.0.1:28084/#section-3` —— 与 iframe 当前文档 (about:srcdoc)
- 不同源同址, 浏览器判为**导航**, 于是 iframe 整个跳去门户首页 (门户路由 route() 认不出 section-3 → 回落 index)。
- 修法: iframe 载入后 (srcdoc 同源, 父页可访问其 document) 拦截 `a[href^="#"]` 点击, 改为在报告文档内 scrollIntoView。
- 不改 srcdoc 挂载方式 (换 blob: 会让相对资源失效; 加 <base> 又会把锚点解析回父页)。
- 用法: python scripts/guanlan_portal_fix_anchors.py [--portal <path>] [--check]
- --check 只报是否已注入 (exit 0 已注入 / 1 未注入); 不带则注入 (已注入时不重复写)。"""
- from __future__ import annotations
- import argparse, hashlib, os, shutil, sys, time
- from pathlib import Path
- ROOT = Path(__file__).resolve().parents[1]
- MARK = "/*guanlan-anchor-fix-v2*/"
- MARK_ANY = "/*guanlan-anchor-fix-v" # 旧版本先剥再注 (升级用)
- ANCHOR = "document.getElementById('ovf').srcdoc=t.content.textContent;"
- ANCHOR_ALT = "document.getElementById('ovf').srcdoc = t.content.textContent;"
- # v2: 用**即时**跳 (原生锚点行为), 不用 behavior:'smooth' —— 平滑滚动在隐藏/零尺寸视口里不执行
- # (2026-09-08 浏览器实测: smooth 后 scrollTop 仍 0, 即时跳到位 14173), 且长文档平滑滚动本身也不是文档阅读的习惯。
- PATCH = (MARK + "(function(){var f=document.getElementById('ovf');if(!f||f._afix)return;f._afix=1;"
- "f.addEventListener('load',function(){try{var d=f.contentDocument;if(!d||d._afix)return;d._afix=1;"
- "d.addEventListener('click',function(ev){var a=ev.target&&ev.target.closest?ev.target.closest('a[href^=\"#\"]'):null;if(!a)return;"
- "ev.preventDefault();var id=decodeURIComponent((a.getAttribute('href')||'#').slice(1));"
- "if(!id){d.defaultView.scrollTo(0,0);return}"
- "var el=d.getElementById(id)||(d.getElementsByName(id)||[])[0];"
- "if(el&&el.scrollIntoView)el.scrollIntoView({block:'start'})},true)}catch(e){}})})();")
- def portal_path(a=None) -> Path:
- if a: return Path(a)
- return Path(os.environ.get("GUANLAN_PORTAL") or (ROOT / "release/portal.html"))
- def _strip_old(s: str) -> tuple[str, int]:
- """剥掉旧版本注入块 (从旧标记到该块结尾 `})();`), 用于升级; 返回 (新串, 剥掉几块)."""
- n = 0
- while True:
- i = s.find(MARK_ANY)
- if i < 0 or s[i:i + len(MARK)] == MARK: break
- j = s.find("})();", i)
- if j < 0: break
- s = s[:i] + s[j + len("})();"):]; n += 1
- return s, n
- def inject(p: Path) -> tuple[bool, str]:
- s = p.read_text(encoding="utf-8", errors="strict")
- if MARK in s: return False, "已注入 (幂等, 不重复写)"
- s, n_old = _strip_old(s)
- for a in (ANCHOR, ANCHOR_ALT):
- if a in s:
- bak = p.with_suffix(p.suffix + ".bak_anchor_" + time.strftime("%Y%m%d%H%M%S")); shutil.copy2(p, bak)
- s2 = s.replace(a, a + PATCH, 1)
- assert MARK in s2 and len(s2) > len(s), "注入后自检失败"
- p.write_text(s2, encoding="utf-8", newline="") # 保持 LF: Windows 上默认会把整份门户改成 CRLF
- return True, f"已注入 ({p.name}, 备份 {bak.name}, 剥旧版 {n_old} 块, sha16 {hashlib.sha256(s2.encode()).hexdigest()[:16]})"
- return False, "未找到挂载点 (门户结构变了: 找 ovf.srcdoc 赋值处) → 未改动"
- def main():
- ap = argparse.ArgumentParser(); ap.add_argument("--portal"); ap.add_argument("--check", action="store_true"); a = ap.parse_args()
- p = portal_path(a.portal)
- if not p.exists(): print(f"门户不存在: {p}"); return 2
- if a.check:
- ok = MARK in p.read_text(encoding="utf-8", errors="replace"); print(("已注入" if ok else "未注入") + f": {p}"); return 0 if ok else 1
- changed, msg = inject(p); print(msg); return 0 if (changed or "已注入" in msg) else 2
- if __name__ == "__main__":
- sys.exit(main())
|