fix: HTML 预览子页面改用 JS 切换,避免沙箱 blob 导航被拦截

上一版通过设置 location.hash 触发 CSS :target 切页,但沙箱 blob
iframe 禁止导航到自身 blob URL,浏览器报 Not allowed to load local
resource,且 :target 仅在真实片段导航时更新,注定失效。改为完全
不动 hash,用 JS 模拟 html-multipage 技能的 .page/.pages 显示隐藏
(内联 display 覆盖类级 CSS),普通目录锚点仍平滑滚动。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Fable 5 committed 2026-06-16 09:19:15 +08:00
1 parent 21e53310f8
commit 2f414c6e08
1 file changed
+31 -21
+31 -21
View File
@@ -102,14 +102,18 @@ function candidateTextFromHref(href: string): string {
* Injects a small runtime script into generated HTML so that clicking an
* in-page anchor (`<a href="#section">`) works inside the preview iframe.
*
* The hash MUST really change on click: multi-page documents (the
* html-multipage skill) show/hide their sub-page `<section class="page">`
* blocks with CSS `:target` rules, which only fire on a fragment navigation.
* Intercepting with preventDefault + scrollIntoView alone leaves the hash
* untouched, so sub-pages stay `display:none` and clicks look dead. The
* handler therefore syncs `location.hash` (a same-document navigation —
* safe in the sandboxed blob: frame) and only smooth-scrolls afterwards when
* the target is actually rendered.
* The preview loads from a sandboxed `blob:` URL (opaque origin). Such a frame
* is FORBIDDEN from navigating to its own `blob:` URL, so changing the hash
* throws "Not allowed to load local resource: blob:…#page-x". And because CSS
* `:target` only updates on a real fragment navigation, the html-multipage
* skill's `:target`-based sub-page switching can never fire here.
*
* So we never touch the hash. Instead the handler emulates the skill's
* show/hide in JS: a "page" is a `.page` inside a `.pages` container; clicking
* a link to one shows it and hides its siblings (mirroring the
* `.page:target` + `~ #page-home` rules via inline `display`, which outranks
* the skill's class-level CSS). Plain in-page anchors (TOC headings, etc.) just
* smooth-scroll as before.
*/
export function injectInPageAnchorHandler(html: string): string {
const script = `<script>(function(){
@@ -119,6 +123,22 @@ export function injectInPageAnchorHandler(html: string): string {
var esc=window.CSS&&CSS.escape?CSS.escape(id):id.replace(/["\\\\]/g,"\\\\$&");
try{return document.querySelector("#"+esc+",[name=\\""+esc+"\\"]");}catch(e){return null;}
}
function find(raw){
var id=decode(raw);
return document.getElementById(id)||document.getElementById(raw)||
document.getElementsByName(id)[0]||document.getElementsByName(raw)[0]||
bySelector(id)||bySelector(raw);
}
function switchPage(target){
var container=target.parentElement;
while(container&&!(container.classList&&container.classList.contains("pages"))){container=container.parentElement;}
var pages=(container||document).querySelectorAll(".page");
if(!pages.length)return false;
for(var i=0;i<pages.length;i++){pages[i].style.display="none";}
target.style.display="block";
if(window.scrollTo){window.scrollTo(0,0);}
return true;
}
document.addEventListener("click",function(e){
var el=e.target;
while(el&&el.tagName!=="A"){el=el.parentElement;}
@@ -126,20 +146,10 @@ export function injectInPageAnchorHandler(html: string): string {
var href=el.getAttribute("href");
if(!href||href.charAt(0)!=="#"||href.length<2)return;
e.preventDefault();
var raw=href.slice(1);
var id=decode(raw);
var target=
document.getElementById(id)||
document.getElementById(raw)||
document.getElementsByName(id)[0]||
document.getElementsByName(raw)[0]||
bySelector(id)||
bySelector(raw);
try{if(location.hash!=="#"+raw){location.hash=raw;}}catch(err){}
var target=find(href.slice(1));
if(!target)return;
requestAnimationFrame(function(){
if(target.getClientRects().length){target.scrollIntoView({behavior:"smooth",block:"start"});}
});
if(target.classList&&target.classList.contains("page")&&switchPage(target))return;
target.scrollIntoView({behavior:"smooth",block:"start"});
},true);
})();</script>`;
if (/<\/body>/i.test(html)) {