From 5106cb3b87906065a105ae0c7aea98721ba9b3ab Mon Sep 17 00:00:00 2001 From: Caitlye <57263725+LyundipLye@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:24:03 +0100 Subject: [PATCH] Kanbun reader: recover when its frame loads badly On the iOS home-screen app a frame occasionally loaded without its typesetter or never answered, leaving the plain vertical source with no way back short of a reload. The host now asks again, reloads the frame (twice at most) on an error or silence, and a layout choice restarts the attempts. The viewer answers every explicit request. Host script and stylesheet are fingerprinted like the frames. --- assets/kanbun-viewer/viewer.js | 5 +- .../chaozhongcuo-20181023-revised-202610.html | 2 +- .../fengrusong-20181102-revised-202610.html | 2 +- .../kanbun-viewer/works/jia-yi-a1-text.html | 2 +- .../kanbun-viewer/works/jia-yi-a10-text.html | 2 +- .../kanbun-viewer/works/jia-yi-a2-text.html | 2 +- .../kanbun-viewer/works/jia-yi-a3-text.html | 2 +- .../kanbun-viewer/works/jia-yi-a4-text.html | 2 +- .../kanbun-viewer/works/jia-yi-a5-text.html | 2 +- .../kanbun-viewer/works/jia-yi-a6-text.html | 2 +- .../kanbun-viewer/works/jia-yi-a7-text.html | 2 +- .../kanbun-viewer/works/jia-yi-a8-text.html | 2 +- .../kanbun-viewer/works/jia-yi-a9-text.html | 2 +- .../kanbun-viewer/works/jia-yi-b1-text.html | 2 +- .../kanbun-viewer/works/jia-yi-b2-text.html | 2 +- .../kanbun-viewer/works/jia-yi-b3-text.html | 2 +- .../kanbun-viewer/works/jia-yi-b4-text.html | 2 +- .../kanbun-viewer/works/jia-yi-b5-text.html | 2 +- .../kanbun-viewer/works/jia-yi-b6-text.html | 2 +- .../kanbun-viewer/works/jia-yi-b7-text.html | 2 +- .../linjiangxian-20180108-revised-202610.html | 2 +- .../works/linjiangxian-20260909-text.html | 2 +- ...njiangxianyin-20191028-revised-202610.html | 2 +- .../works/manjianghong-2022-echo-202610.html | 2 +- .../works/manjianghong-2022-sewn-202610.html | 2 +- .../manjianghong-rereading-202610-text.html | 2 +- .../mantingfang-20191219-revised-202610.html | 2 +- .../queqiaoxian-20181222-revised-202610.html | 2 +- .../works/yingtianchang-20261008-text.html | 2 +- .../works/zhegutian-20260909-text.html | 2 +- assets/poetry-kunten-host.js | 93 +++++++++++---- ja/poetry/chaozhongcuo-20181023/index.html | 4 +- ja/poetry/fengrusong-20181102/index.html | 4 +- ja/poetry/jia-yi/index.html | 36 +++--- ja/poetry/linjiangxian-20180108/index.html | 4 +- ja/poetry/linjiangxian-20260909/index.html | 4 +- ja/poetry/linjiangxianyin-20191028/index.html | 4 +- ja/poetry/manjianghong-2022/index.html | 6 +- .../manjianghong-rereading-202610/index.html | 4 +- ja/poetry/mantingfang-20191219/index.html | 4 +- ja/poetry/queqiaoxian-20181222/index.html | 4 +- ja/poetry/september-2026/index.html | 6 +- ja/poetry/yingtianchang-20261008/index.html | 4 +- ja/poetry/zhegutian-20260909/index.html | 4 +- package.json | 3 +- tools/poetry_pages.py | 11 +- tools/poetrykuntenrecoverycheck.mjs | 107 ++++++++++++++++++ 47 files changed, 266 insertions(+), 99 deletions(-) create mode 100644 tools/poetrykuntenrecoverycheck.mjs diff --git a/assets/kanbun-viewer/viewer.js b/assets/kanbun-viewer/viewer.js index 470ea5f..fba95fd 100644 --- a/assets/kanbun-viewer/viewer.js +++ b/assets/kanbun-viewer/viewer.js @@ -98,7 +98,10 @@ if (typeof foreground === 'string' && foreground.length < 100 && CSS.supports('color',foreground)) document.body.style.color = foreground; // An offscreen sandboxed iframe may have requestAnimationFrame suspended. - // The first handshake must not depend on animation frames. + // The first handshake must not depend on animation frames. The host also + // sends this message to ask again for a report it never accepted, so an + // explicit request is always answered, even with an unchanged size. + last = ''; measure(); }); document.fonts.ready.then(() => { diff --git a/assets/kanbun-viewer/works/chaozhongcuo-20181023-revised-202610.html b/assets/kanbun-viewer/works/chaozhongcuo-20181023-revised-202610.html index 2be9b9f..63e9bc1 100644 --- a/assets/kanbun-viewer/works/chaozhongcuo-20181023-revised-202610.html +++ b/assets/kanbun-viewer/works/chaozhongcuo-20181023-revised-202610.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/fengrusong-20181102-revised-202610.html b/assets/kanbun-viewer/works/fengrusong-20181102-revised-202610.html index 7ac368a..21fcf64 100644 --- a/assets/kanbun-viewer/works/fengrusong-20181102-revised-202610.html +++ b/assets/kanbun-viewer/works/fengrusong-20181102-revised-202610.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-a1-text.html b/assets/kanbun-viewer/works/jia-yi-a1-text.html index 353cb9d..f0a0513 100644 --- a/assets/kanbun-viewer/works/jia-yi-a1-text.html +++ b/assets/kanbun-viewer/works/jia-yi-a1-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-a10-text.html b/assets/kanbun-viewer/works/jia-yi-a10-text.html index dfdd964..900199a 100644 --- a/assets/kanbun-viewer/works/jia-yi-a10-text.html +++ b/assets/kanbun-viewer/works/jia-yi-a10-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-a2-text.html b/assets/kanbun-viewer/works/jia-yi-a2-text.html index df2dde2..dad0ca3 100644 --- a/assets/kanbun-viewer/works/jia-yi-a2-text.html +++ b/assets/kanbun-viewer/works/jia-yi-a2-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-a3-text.html b/assets/kanbun-viewer/works/jia-yi-a3-text.html index 62832b6..167eb9d 100644 --- a/assets/kanbun-viewer/works/jia-yi-a3-text.html +++ b/assets/kanbun-viewer/works/jia-yi-a3-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-a4-text.html b/assets/kanbun-viewer/works/jia-yi-a4-text.html index ef62dd2..450cccc 100644 --- a/assets/kanbun-viewer/works/jia-yi-a4-text.html +++ b/assets/kanbun-viewer/works/jia-yi-a4-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-a5-text.html b/assets/kanbun-viewer/works/jia-yi-a5-text.html index 3968fb4..413d82c 100644 --- a/assets/kanbun-viewer/works/jia-yi-a5-text.html +++ b/assets/kanbun-viewer/works/jia-yi-a5-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-a6-text.html b/assets/kanbun-viewer/works/jia-yi-a6-text.html index 3787caf..c461129 100644 --- a/assets/kanbun-viewer/works/jia-yi-a6-text.html +++ b/assets/kanbun-viewer/works/jia-yi-a6-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-a7-text.html b/assets/kanbun-viewer/works/jia-yi-a7-text.html index 9e67c76..19c0608 100644 --- a/assets/kanbun-viewer/works/jia-yi-a7-text.html +++ b/assets/kanbun-viewer/works/jia-yi-a7-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-a8-text.html b/assets/kanbun-viewer/works/jia-yi-a8-text.html index 6e7ad59..732433b 100644 --- a/assets/kanbun-viewer/works/jia-yi-a8-text.html +++ b/assets/kanbun-viewer/works/jia-yi-a8-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-a9-text.html b/assets/kanbun-viewer/works/jia-yi-a9-text.html index c7e9e3d..ebc7b9c 100644 --- a/assets/kanbun-viewer/works/jia-yi-a9-text.html +++ b/assets/kanbun-viewer/works/jia-yi-a9-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-b1-text.html b/assets/kanbun-viewer/works/jia-yi-b1-text.html index 71e6ad3..3e33be6 100644 --- a/assets/kanbun-viewer/works/jia-yi-b1-text.html +++ b/assets/kanbun-viewer/works/jia-yi-b1-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-b2-text.html b/assets/kanbun-viewer/works/jia-yi-b2-text.html index cb3e4e8..3e151a0 100644 --- a/assets/kanbun-viewer/works/jia-yi-b2-text.html +++ b/assets/kanbun-viewer/works/jia-yi-b2-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-b3-text.html b/assets/kanbun-viewer/works/jia-yi-b3-text.html index 970fe96..e1b4519 100644 --- a/assets/kanbun-viewer/works/jia-yi-b3-text.html +++ b/assets/kanbun-viewer/works/jia-yi-b3-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-b4-text.html b/assets/kanbun-viewer/works/jia-yi-b4-text.html index e912ce7..fba35b2 100644 --- a/assets/kanbun-viewer/works/jia-yi-b4-text.html +++ b/assets/kanbun-viewer/works/jia-yi-b4-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-b5-text.html b/assets/kanbun-viewer/works/jia-yi-b5-text.html index 29e1073..b008ee6 100644 --- a/assets/kanbun-viewer/works/jia-yi-b5-text.html +++ b/assets/kanbun-viewer/works/jia-yi-b5-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-b6-text.html b/assets/kanbun-viewer/works/jia-yi-b6-text.html index 3375be7..c6aa139 100644 --- a/assets/kanbun-viewer/works/jia-yi-b6-text.html +++ b/assets/kanbun-viewer/works/jia-yi-b6-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/jia-yi-b7-text.html b/assets/kanbun-viewer/works/jia-yi-b7-text.html index 0eb63b4..667f80d 100644 --- a/assets/kanbun-viewer/works/jia-yi-b7-text.html +++ b/assets/kanbun-viewer/works/jia-yi-b7-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/linjiangxian-20180108-revised-202610.html b/assets/kanbun-viewer/works/linjiangxian-20180108-revised-202610.html index c97f04a..efd4636 100644 --- a/assets/kanbun-viewer/works/linjiangxian-20180108-revised-202610.html +++ b/assets/kanbun-viewer/works/linjiangxian-20180108-revised-202610.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/linjiangxian-20260909-text.html b/assets/kanbun-viewer/works/linjiangxian-20260909-text.html index 13ed879..84f82cd 100644 --- a/assets/kanbun-viewer/works/linjiangxian-20260909-text.html +++ b/assets/kanbun-viewer/works/linjiangxian-20260909-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/linjiangxianyin-20191028-revised-202610.html b/assets/kanbun-viewer/works/linjiangxianyin-20191028-revised-202610.html index e719807..013a94c 100644 --- a/assets/kanbun-viewer/works/linjiangxianyin-20191028-revised-202610.html +++ b/assets/kanbun-viewer/works/linjiangxianyin-20191028-revised-202610.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/manjianghong-2022-echo-202610.html b/assets/kanbun-viewer/works/manjianghong-2022-echo-202610.html index 126dd64..48721e8 100644 --- a/assets/kanbun-viewer/works/manjianghong-2022-echo-202610.html +++ b/assets/kanbun-viewer/works/manjianghong-2022-echo-202610.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/manjianghong-2022-sewn-202610.html b/assets/kanbun-viewer/works/manjianghong-2022-sewn-202610.html index 765e597..993eaae 100644 --- a/assets/kanbun-viewer/works/manjianghong-2022-sewn-202610.html +++ b/assets/kanbun-viewer/works/manjianghong-2022-sewn-202610.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/manjianghong-rereading-202610-text.html b/assets/kanbun-viewer/works/manjianghong-rereading-202610-text.html index 09ee9ed..50f5721 100644 --- a/assets/kanbun-viewer/works/manjianghong-rereading-202610-text.html +++ b/assets/kanbun-viewer/works/manjianghong-rereading-202610-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/mantingfang-20191219-revised-202610.html b/assets/kanbun-viewer/works/mantingfang-20191219-revised-202610.html index 4f047b2..6b336a3 100644 --- a/assets/kanbun-viewer/works/mantingfang-20191219-revised-202610.html +++ b/assets/kanbun-viewer/works/mantingfang-20191219-revised-202610.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/queqiaoxian-20181222-revised-202610.html b/assets/kanbun-viewer/works/queqiaoxian-20181222-revised-202610.html index f91c7b5..de67ce6 100644 --- a/assets/kanbun-viewer/works/queqiaoxian-20181222-revised-202610.html +++ b/assets/kanbun-viewer/works/queqiaoxian-20181222-revised-202610.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/yingtianchang-20261008-text.html b/assets/kanbun-viewer/works/yingtianchang-20261008-text.html index c81e589..99a28fc 100644 --- a/assets/kanbun-viewer/works/yingtianchang-20261008-text.html +++ b/assets/kanbun-viewer/works/yingtianchang-20261008-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/kanbun-viewer/works/zhegutian-20260909-text.html b/assets/kanbun-viewer/works/zhegutian-20260909-text.html index f906711..76ea25b 100644 --- a/assets/kanbun-viewer/works/zhegutian-20260909-text.html +++ b/assets/kanbun-viewer/works/zhegutian-20260909-text.html @@ -8,7 +8,7 @@ - +
diff --git a/assets/poetry-kunten-host.js b/assets/poetry-kunten-host.js index dda7851..5fcfb94 100644 --- a/assets/poetry-kunten-host.js +++ b/assets/poetry-kunten-host.js @@ -20,7 +20,45 @@ const frame = stage.querySelector('iframe[data-kanbun-frame]'); const article = stage.closest('.reader-poem'); if (!frame || !article) continue; - byWindow.set(frame.contentWindow, {frame, article, stage}); + byWindow.set(frame.contentWindow, {frame, article, stage, pings:0, reloads:0, timer:0}); + } + // A visual reader that never reports ready leaves the plain vertical + // source in place. That fallback is correct, but on iOS home-screen apps + // a frame occasionally loads without its typesetter (or never answers). + // Ask again, then reload the frame; a layout choice restarts the attempts. + const PING_MS = 1500; + const PINGS_BEFORE_RELOAD = 3; + const MAX_RELOADS = 2; + function reloadFrame(entry) { + if (entry.reloads >= MAX_RELOADS) return false; + entry.reloads += 1; + entry.pings = 0; + const url = new URL(entry.frame.getAttribute('src'), location.href); + url.searchParams.set('retry', String(entry.reloads)); + entry.frame.src = url.pathname + url.search; + return true; + } + function watch(entry) { + clearTimeout(entry.timer); + entry.timer = setTimeout(() => { + if (!isActive() || entry.stage.dataset.kanbunReady === 'true') return; + if (entry.pings >= PINGS_BEFORE_RELOAD) { + if (reloadFrame(entry)) watch(entry); + return; + } + entry.pings += 1; + sendPreferences(entry); + watch(entry); + }, PING_MS); + } + function watchAll({restart = false} = {}) { + for (const entry of byWindow.values()) { + if (restart && entry.stage.dataset.kanbunReady !== 'true') { + entry.pings = 0; + entry.reloads = 0; + } + watch(entry); + } } let currentLayout = root.dataset.poetryLayout; function isActive() { @@ -45,6 +83,7 @@ const match = [...byWindow.values()].find(({frame}) => frame.contentWindow === event.source); if (!match) return; const {frame, article, stage} = match; + const entry = match; const msg = event.data; if (msg?.type !== 'hanpu-kanbun-viewer-v1' || msg.tid !== article.dataset.poetryText || @@ -52,6 +91,8 @@ if (msg.state === 'error') { stage.dataset.kanbunReady = 'false'; apply(); // Canonical Chinese source remains visible. + // A missing typesetter script is usually transient; load the frame again. + if (isActive() && reloadFrame(entry)) watch(entry); return; } if (msg.state !== 'ready' || @@ -63,29 +104,33 @@ frame.style.width = msg.width + 'px'; frame.style.height = msg.height + 'px'; stage.dataset.kanbunReady = 'true'; + clearTimeout(entry.timer); apply(); }); function updateFontSizes() { - for (const {frame,article} of byWindow.values()) { - const reference = article.querySelector('.source .body'); - if (!reference || !frame.contentWindow) continue; - const style = getComputedStyle(reference); - const size = Number.parseFloat(style.fontSize); - const section = article.querySelector('.poem-version.source'); - const display = section?.querySelector('.poetry-kanbun-display'); - const sectionWidth = section?.getBoundingClientRect().width || innerWidth; - const gap = Number.parseFloat(getComputedStyle(display).columnGap) || 12; - // Reserve 38px inside the iframe for glyph bounds/gutters, plus a - // safety margin. The title is typeset inside the frame. - const cap = Math.max(100,Math.floor(sectionWidth - gap - 44)); - if (Number.isFinite(size)) - frame.contentWindow.postMessage({ - type:'hanpu-kanbun-preferences-v1', - fontSize:Math.max(20,Math.min(36,Math.round(size*1.35))), - foreground:style.color, - maxContentWidth:cap, - }, '*'); - } + for (const entry of byWindow.values()) sendPreferences(entry); + } + // The viewer always answers a preferences message with its measurement, + // so this doubles as the request for a ready report. + function sendPreferences({frame,article}) { + const reference = article.querySelector('.source .body'); + if (!reference || !frame.contentWindow) return; + const style = getComputedStyle(reference); + const size = Number.parseFloat(style.fontSize); + const section = article.querySelector('.poem-version.source'); + const display = section?.querySelector('.poetry-kanbun-display'); + const sectionWidth = section?.getBoundingClientRect().width || innerWidth; + const gap = Number.parseFloat(getComputedStyle(display).columnGap) || 12; + // Reserve 38px inside the iframe for glyph bounds/gutters, plus a + // safety margin. The title is typeset inside the frame. + const cap = Math.max(100,Math.floor(sectionWidth - gap - 44)); + if (Number.isFinite(size)) + frame.contentWindow.postMessage({ + type:'hanpu-kanbun-preferences-v1', + fontSize:Math.max(20,Math.min(36,Math.round(size*1.35))), + foreground:style.color, + maxContentWidth:cap, + }, '*'); } // Unlike the failed overlay, the visual iframe never owns source text. // Readers may select within it or use this explicit canonical-copy control. @@ -111,6 +156,7 @@ addEventListener('poetry-layout-applied', () => { apply(); updateFontSizes(); + watchAll({restart: true}); }); addEventListener('resize', () => { updateFontSizes(); @@ -121,8 +167,11 @@ m.attributeName === 'data-reading-contrast')) { apply(); updateFontSizes(); + watchAll({restart: true}); } }).observe(root,{attributes:true,attributeFilter:['data-reading-simple','data-reading-large','data-reading-contrast']}); - for (const {frame} of byWindow.values()) frame.addEventListener('load', updateFontSizes); + for (const entry of byWindow.values()) + entry.frame.addEventListener('load', () => { sendPreferences(entry); watch(entry); }); apply(); + watchAll(); })(); \ No newline at end of file diff --git a/ja/poetry/chaozhongcuo-20181023/index.html b/ja/poetry/chaozhongcuo-20181023/index.html index 33edc16..9d69fce 100644 --- a/ja/poetry/chaozhongcuo-20181023/index.html +++ b/ja/poetry/chaozhongcuo-20181023/index.html @@ -67,7 +67,7 @@ - + @@ -125,7 +125,7 @@

《朝中措》

唇邊鳳語,挹來便作,銀磬微鳴。 字底分明芒刺,唇間猶護餘聲。
- +

朝中措の調べ

羽の金を梳いて、ほのかな香りの花にする。香りは鏡の中に届いて止まる。 欄干の前、影を伴侶に、並びたいと願う。一筋の朱い髪が先に絡まる。 diff --git a/ja/poetry/fengrusong-20181102/index.html b/ja/poetry/fengrusong-20181102/index.html index 3df2d83..e111299 100644 --- a/ja/poetry/fengrusong-20181102/index.html +++ b/ja/poetry/fengrusong-20181102/index.html @@ -67,7 +67,7 @@ - + @@ -127,7 +127,7 @@

《風入松》

舊山橫影亂爐光,燼處餘香。 筆枯化紙容人念,料經年、應易遷忘。 芍藥更添紅雨,玉筋暗觸龍章。
- +

風入松の調べ

丘の亭では雪が降ろうとし、霜はすでに結ぶ。それでも頑なな陽は残る。 斜陽は煉瓦に落ちてなお去りがたく、金の炉を焚き、長い夜、君を待つ。 diff --git a/ja/poetry/jia-yi/index.html b/ja/poetry/jia-yi/index.html index 67c334b..334e39b 100644 --- a/ja/poetry/jia-yi/index.html +++ b/ja/poetry/jia-yi/index.html @@ -67,7 +67,7 @@ - + @@ -146,7 +146,7 @@

不語各含無限思,而今惟道相期。 夜闌暗忘漏聲移。 慵來還指日,明日又今時。

- +

A1 · 臨江仙の調べ · 学友と灯の下で夜を過ごし、明け方になって別れる

幾度、夜更けまで二人で黙って座ったことだろう。 ひとつきりの灯、その赤い涙がゆっくり垂れる。 @@ -182,7 +182,7 @@

乙一《無題 · 臨江仙 客散非因誰獨在,他人皆自相依。 席旁偶坐共相棲。 漏殘晨露上,風過落花時。

- +

B1 · 無題 · 臨江仙の調べ

ひっそりした廊下に月が明るく、人の気配がようやく静まった。 二人で花の小径を、ゆっくり歩く。 @@ -218,7 +218,7 @@
+

A2 · 臨江仙の調べ · 酒のあと、頬に手を添えて階上へ上がり、夜更けに寄り添い、人に問われても避けなかった

深く酔い、支え合って並んで寄りかかった。 階上へ上がり、人には知られたくなかった。 @@ -254,7 +254,7 @@

乙二《無題 · 踏莎行 欲喚還休,歡猶未省。 分明一水無舟艇。 殘灰散盡各歸時,餘釭獨照空庭靜。

- +

B2 · 無題 · 踏莎行の調べ

香りの温かな一室。並ぶ二つの影の跡。 笑い声は簾のあいだの闇へ沈む。 @@ -288,7 +288,7 @@

乙三《無題 · 臨江仙 不是東風先折取,恐花為我辭根。 新痕舊鑰莫輕論。 相逢原是客,客受主家恩。

- +

B3 · 無題 · 臨江仙の調べ

蝋燭は暗く、こおろぎは冷え、簾の影は細い。 酔いの名残にはまだわずかな温もり。 @@ -327,7 +327,7 @@

乙四《無題 · 渡江雲 謄復塗、深言怕誤,淺字偏安。 明朝銅色經他手,便認作、一諾窮年。 燈滅後、丁寧只是加餐。

- +

B4 · 無題 · 渡江雲の調べ

明るい銅、新しく刻まれた歯。 封じようとして、手を止める。 @@ -369,7 +369,7 @@

乙四《無題 · 渡江雲

乙五《無題 · 十六字令》

銅。鑰齒新裁付掌中。天寒了,加飯且從容。
- +

B5 · 無題 · 十六字令の調べ

銅。 歯を新しく刻んだ鍵を、あなたの掌へ。 @@ -391,7 +391,7 @@
+

A3 · 鷓鴣天の調べ · あなたから鍵を贈られ、数語の書きつけが添えられていた。灯の下で広げて読む

新しく刻まれた鍵の歯。私はそれを大切にする。 筆を取り上げる。まだ一字も書かぬうちから、もうためらう。 @@ -424,7 +424,7 @@
+

A4 · 臨江仙の調べ · 甲一を回想して。かつて学友と幾晩か黙って座った。別れてから便りはしだいに薄れ、長い時を経てようやく心を定めた

かつて、ひっそりした廊下で二人は黙って座った。 あのときは、ただありふれたことだと思っていた。 @@ -460,7 +460,7 @@
+

A5 · 臨江仙の調べ · 甲二を回想して。酒のあと、頬に手を添えて階上へ上がり、夜更けに人に問われ、初めて隠し立てをしなかった

あの夜、誰が客と主人を分けただろう。 肩を並べ、夜更けに寄りかかっていた。 @@ -495,7 +495,7 @@
+

A6 · 鷓鴣天の調べ · 甲三を回想して。箱の中の古い鍵は緑青に覆われていた。往復した手紙を思い返し、もの寂しく書く

小さな鍵。年を経て、錆は暗く広がった。 古い手紙を読み終えても、目はまだ冴えている。 @@ -528,7 +528,7 @@
+

A7 · 臨江仙の調べ · 蘭を株分けしてある人へ送る。かつて香草を贈られた時を思い、もの寂しく書く

手で古い蘭を分け、新しく植える。 あの春の温もりを、いまも覚えているだろうか。 @@ -577,7 +577,7 @@

乙六《得某所寄》

- +

B6 · 送られてきたものを受け取って

古い鉢に、移し植えた蘭。 春の痕が幾度かずれた。 @@ -653,7 +653,7 @@

甲八《夜聞舊曲》

- +

A8 · 夜、昔の曲を聞く

あのときは、窓の下で別れた。 いまは、軒先の雨になった。 @@ -696,7 +696,7 @@
+

B7 · 忘川、忘却の川 · 箱を探して古い草稿を見つけた。これ以上持ち歩かず、すべてある人に送り、この詩を添える

灯の下で、昔の詞を読む。 夜も深くなって、ようやく眠れた。 @@ -731,7 +731,7 @@
+

A9 · 鷓鴣天の調べ · 送られてきた古い草稿を受け取り、灯の下で読み終え、箱に残していたものと合わせて甲・乙として編んだ。綴じ上げてからは客が来るたび見せ、この末尾に記す

古びた紙束を二つ、机上にひろげる。 昔の灯影のもと、それぞれが一篇をなしていた。 @@ -766,7 +766,7 @@

甲十《中 遲,是誤了; 歸,也是誤了。

- +

A10 · 中呂 · 山坡羊の調べ · 旧居の前をまた通る

風が顔に当たる。ここで立ち止まる。 鼻先が先に動く。魂のほうが、私より先に着く。 diff --git a/ja/poetry/linjiangxian-20180108/index.html b/ja/poetry/linjiangxian-20180108/index.html index 7732ec3..15fca1c 100644 --- a/ja/poetry/linjiangxian-20180108/index.html +++ b/ja/poetry/linjiangxian-20180108/index.html @@ -67,7 +67,7 @@ - + @@ -127,7 +127,7 @@

《臨江仙》

閬苑蓬萊身已遠,青驄且繫書樓。 別期未至不題愁。 炭冰摧過後,吟捻憶君眸。
- +

臨江仙の調べ

麹と麦芽、塩と梅に、古い調合法はない。塵が来ても、芳しい流れにはまだ波が立たない。 簾の前、去ろうとすれば、影が先に留まる。 diff --git a/ja/poetry/linjiangxian-20260909/index.html b/ja/poetry/linjiangxian-20260909/index.html index 8812ddf..205015f 100644 --- a/ja/poetry/linjiangxian-20260909/index.html +++ b/ja/poetry/linjiangxian-20260909/index.html @@ -67,7 +67,7 @@ - + @@ -127,7 +127,7 @@

《臨江仙》

他日重簾垂到地,垂來一似從前。 手臨燈畔數迴旋。 明朝風或緊,窗且掩周全。
- +

臨江仙の調べ

簾の影、幾晩も続いた長い話。 降りはじめた露が欄干を濡らす。 diff --git a/ja/poetry/linjiangxianyin-20191028/index.html b/ja/poetry/linjiangxianyin-20191028/index.html index f7d56e7..57f1bab 100644 --- a/ja/poetry/linjiangxianyin-20191028/index.html +++ b/ja/poetry/linjiangxianyin-20191028/index.html @@ -67,7 +67,7 @@ - + @@ -128,7 +128,7 @@

《臨江仙引》

+

臨江仙引の調べ

囲いは裂け、宴は散る。鐘は夕べに垂れる。しばし、安らぎを盗みとる。 星の街に、あのよい歳月はもうない。 diff --git a/ja/poetry/manjianghong-2022/index.html b/ja/poetry/manjianghong-2022/index.html index 1f006c6..bb124c7 100644 --- a/ja/poetry/manjianghong-2022/index.html +++ b/ja/poetry/manjianghong-2022/index.html @@ -67,7 +67,7 @@ - + @@ -134,7 +134,7 @@

《滿江紅 · 更何須揀擇,過門皆璧。 初景寒柯違業識,豐城元禮宜同席。 看收場、針腳又周全,無痕跡。

- +

満江紅の調べ · 引き止めたくとも、こうなるとわかっている

古い裂け目を縫ったばかりなのに、春風はまた、しきりに客を引き止める。 扉は閉じようとし、かえって簾の影が翻り、春を忍び込ませる。 @@ -172,7 +172,7 @@

《 縱衣冠屢換,總稱圭璧。 初景寒柯違業識,豐城元禮宜同席。 曉山塗,晝影祭寒華,猶無息。

- +

満江紅の調べ · 引き止めたくとも、こうなるとわかっている · 別稿

水時計は尽き、鐘は返る。夜明けを促すところ、余韻はなお塞がれている。 扉は閉じようとし、粗末な襟は去ろうとする。それでもまた、よい客を迎える。 diff --git a/ja/poetry/manjianghong-rereading-202610/index.html b/ja/poetry/manjianghong-rereading-202610/index.html index e288de9..d55cf8d 100644 --- a/ja/poetry/manjianghong-rereading-202610/index.html +++ b/ja/poetry/manjianghong-rereading-202610/index.html @@ -67,7 +67,7 @@ - + @@ -129,7 +129,7 @@
+

満江紅の調べ · 旧作を読み返し、年月を経て再び消息を交わしたことを思う

古い詞をまた開けば、まず、あの頃注いだ力がわかる。 かつて細かく解きほぐし、わずかな言葉を千たび転じた。息はかすか、声は渋い。 diff --git a/ja/poetry/mantingfang-20191219/index.html b/ja/poetry/mantingfang-20191219/index.html index 504e473..7707cd4 100644 --- a/ja/poetry/mantingfang-20191219/index.html +++ b/ja/poetry/mantingfang-20191219/index.html @@ -67,7 +67,7 @@ - + @@ -129,7 +129,7 @@

《滿庭芳 · 遙思 把千里關山,盡斂襟前。 幾月光陰忽薄,回眸際、指過眉山。 從頭認,詩還半闋,雙影壓箋邊。

- +

満庭芳の調べ · 遥かに楚潭を思う

風の佩玉がいま静まり、雨が朝の木槿を梳く。文は先に眉の弧を知っていた。 ひと声、低く呼ぶ。夢の弦が驚いて散る。 diff --git a/ja/poetry/queqiaoxian-20181222/index.html b/ja/poetry/queqiaoxian-20181222/index.html index cb41f1d..7520695 100644 --- a/ja/poetry/queqiaoxian-20181222/index.html +++ b/ja/poetry/queqiaoxian-20181222/index.html @@ -67,7 +67,7 @@ - + @@ -125,7 +125,7 @@

《鵲橋仙》

聞人道好,隨人道好,瓦釜雷鳴何故。 逢君仍問近來安,說罷了、回身讓路。
- +

鵲橋仙の調べ

手紙は要らない。古い品は大切にしている。紙にはありふれた言葉ばかり。 柔らかな手の傷を、思いがけず唇が濡らした。幾年を経て、なおどれほど残るのか。 diff --git a/ja/poetry/september-2026/index.html b/ja/poetry/september-2026/index.html index 9b8408a..1e846cc 100644 --- a/ja/poetry/september-2026/index.html +++ b/ja/poetry/september-2026/index.html @@ -67,7 +67,7 @@ - + @@ -129,7 +129,7 @@

《鷓鴣 言漸杳,露初零。獨餘一語未分明。 頻頻蘸得毫端黑,墨至濃時偏不成。

- +

鷓鴣天の調べ · 幾晩か灯の下で書こうとした、未完

幾晩か、簾の前で一緒に灯芯を切った。 一生ぶんの話をして、また三更まで来た。 @@ -161,7 +161,7 @@

《臨江仙》

他日重簾垂到地,垂來一似從前。 手臨燈畔數迴旋。 明朝風或緊,窗且掩周全。
- +

臨江仙の調べ

簾の影、幾晩も続いた長い話。 降りはじめた露が欄干を濡らす。 diff --git a/ja/poetry/yingtianchang-20261008/index.html b/ja/poetry/yingtianchang-20261008/index.html index c5a6bde..aa4f1ad 100644 --- a/ja/poetry/yingtianchang-20261008/index.html +++ b/ja/poetry/yingtianchang-20261008/index.html @@ -67,7 +67,7 @@ - + @@ -129,7 +129,7 @@

《應天長·喚回》

- +

應天長の調べ · 呼び戻す

呼び戻してなお、誰も着ていない衣を整える。 袖口の余温は指に残る。 diff --git a/ja/poetry/zhegutian-20260909/index.html b/ja/poetry/zhegutian-20260909/index.html index 5a475ad..05a7acb 100644 --- a/ja/poetry/zhegutian-20260909/index.html +++ b/ja/poetry/zhegutian-20260909/index.html @@ -67,7 +67,7 @@ - + @@ -125,7 +125,7 @@

《鷓鴣 言漸杳,露初零。獨餘一語未分明。 頻頻蘸得毫端黑,墨至濃時偏不成。

- +

鷓鴣天の調べ · 幾晩か灯の下で書こうとした、未完

幾晩か、簾の前で一緒に灯芯を切った。 一生ぶんの話をして、また三更まで来た。 diff --git a/package.json b/package.json index a5e2d67..00808ad 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "qa:first-love": "python3 tools/firstlovecheck.py", "qa:voucher": "node tools/vouchertypecheck.mjs", "qa:404": "node tools/notfoundcheck.mjs", - "qa": "npm run qa:poetry-library && npm run qa:poetry-kunten-manuscript && npm run qa:poetry-kunten-reader && npm run qa:poetry-kunten-lines && npm run qa:poetry-layout && npm run qa:poetry-kunten && npm run qa:poetry-kunten-multiline && npm run qa:poetry-kunten-zoom && npm run qa:summer-translations && npm run qa:html && npm run qa:first-love && npm run qa:voucher && npm run qa:order-core && npm run qa:shop && npm run qa:pv-configurator && npm run qa:pv-edition && npm run qa:shop-locales && npm run qa:poetry-migration && npm run qa:author-shop && npm run qa:h10-print && npm run qa:tuner && npm run qa:browser && npm run qa:a11y-functional && npm run qa:editorial && npm run qa:404 && npm run qa:about && npm run qa:fonts && npm run qa:wrap", + "qa": "npm run qa:poetry-library && npm run qa:poetry-kunten-manuscript && npm run qa:poetry-kunten-reader && npm run qa:poetry-kunten-lines && npm run qa:poetry-layout && npm run qa:poetry-kunten && npm run qa:poetry-kunten-recovery && npm run qa:poetry-kunten-multiline && npm run qa:poetry-kunten-zoom && npm run qa:summer-translations && npm run qa:html && npm run qa:first-love && npm run qa:voucher && npm run qa:order-core && npm run qa:shop && npm run qa:pv-configurator && npm run qa:pv-edition && npm run qa:shop-locales && npm run qa:poetry-migration && npm run qa:author-shop && npm run qa:h10-print && npm run qa:tuner && npm run qa:browser && npm run qa:a11y-functional && npm run qa:editorial && npm run qa:404 && npm run qa:about && npm run qa:fonts && npm run qa:wrap", "qa:editorial": "node tools/editorialcheck.mjs", "qa:shop": "node tools/shopcheck.mjs", "qa:pv-configurator": "node tools/pv-configuratorcheck.mjs", @@ -33,6 +33,7 @@ "qa:poetry-kunten-reader": "python3 tools/poetrykuntenreadercheck.py", "qa:poetry-kunten-lines": "python3 tools/poetrykuntenlinecheck.py", "qa:poetry-kunten": "node tools/poetrykuntencheck.mjs", + "qa:poetry-kunten-recovery": "node tools/poetrykuntenrecoverycheck.mjs", "qa:poetry-kunten-multiline": "node tools/poetrykuntenmultilinecheck.mjs", "qa:poetry-kunten-zoom": "node tools/poetrykuntenzoomcheck.mjs" }, diff --git a/tools/poetry_pages.py b/tools/poetry_pages.py index 4a999c1..4c16055 100644 --- a/tools/poetry_pages.py +++ b/tools/poetry_pages.py @@ -1,6 +1,7 @@ """Static catalogue, chronology, collection readers and independent work pages.""" from __future__ import annotations +import hashlib import html import json import re @@ -38,6 +39,10 @@ def __init__(self, base: Any): self.ui = read(base.CONTENT / 'poetry/ui.json') self.template = (base.TEMPLATES / 'poetry.html').read_text(encoding='utf-8') + def fingerprinted(self, name: str) -> str: + data = (self.base.ROOT / 'assets' / name).read_bytes() + return f'/assets/{name}?v={hashlib.sha256(data).hexdigest()[:12]}' + def languages(self, route: str, locale: str) -> str: result = [] for language in self.base.LANGUAGES: @@ -71,8 +76,10 @@ def shell(self, *, route: str, locale: str, copy: dict[str, Any], heading: str, social = b.social_meta_html(locale_id=locale,title=title,description=description,url=canonical, image_url=image,image_alt=title,width=1200,height=630,image_type='image/png',og_type='article') if locale == 'ja' and 'data-poetry-kanbun-viewer=' in content: - extra_head += ('' - '') + # Fingerprinted like the frames it drives: an installed iOS web app + # must not pair a fresh frame with a stale controller. + extra_head += (f'' + f'') values = { 'HTML_LANG':b.LANG_BY_ID[locale]['html_lang'], 'LOCALE':locale, 'POETRY_TITLE':esc(title), 'POETRY_DESCRIPTION':esc(description), diff --git a/tools/poetrykuntenrecoverycheck.mjs b/tools/poetrykuntenrecoverycheck.mjs new file mode 100644 index 0000000..ed40a26 --- /dev/null +++ b/tools/poetrykuntenrecoverycheck.mjs @@ -0,0 +1,107 @@ +#!/usr/bin/env node +// The kanbun reader recovers when its frame loads badly: a missing +// typesetter (the frame reports an error), a missing viewer (the frame never +// answers), or a ready report that never reaches the host. Each must end with +// the visual reader shown, not the plain vertical fallback. +import assert from 'node:assert/strict'; +import {createServer} from 'node:http'; +import {readFile} from 'node:fs/promises'; +import path from 'node:path'; +import {chromium, webkit} from 'playwright'; + +const root = process.cwd(); +const page_url = '/ja/poetry/linjiangxian-20260909/'; +const id = 'linjiangxian-20260909-text'; +const mime = {'.html':'text/html; charset=utf-8','.js':'text/javascript','.css':'text/css', + '.woff2':'font/woff2','.json':'application/json','.png':'image/png'}; +const server = createServer(async(req,res) => { + try { + let u = decodeURIComponent(new URL(req.url,'http://localhost').pathname); + if (u.endsWith('/')) u += 'index.html'; + const file = path.resolve(root,'.'+u); + if (!file.startsWith(root + path.sep)) throw Error('Path outside project'); + res.writeHead(200,{'Content-Type':mime[path.extname(file)]||'application/octet-stream'}); + res.end(await readFile(file)); + } catch { res.writeHead(404);res.end('Not found'); } +}); +await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve)); +const base = 'http://127.0.0.1:' + server.address().port; + +// Fail only the first request for a viewer asset; later reloads succeed. +async function failFirst(context, pattern) { + let failed = 0; + await context.route(pattern, route => { + if (failed++ === 0) return route.abort(); + return route.continue(); + }); +} + +const scenarios = { + 'typesetter missing on first load': context => failFirst(context, /kanbun-render\.js/), + 'viewer missing on first load': context => failFirst(context, /kanbun-viewer\/viewer\.js/), + 'first ready report lost': async context => { + // Swallow the frame's first ready message before the host sees it. + await context.addInitScript(() => { + if (window.top !== window) return; + let dropped = false; + addEventListener('message', event => { + if (!dropped && event.data?.type === 'hanpu-kanbun-viewer-v1' && event.data.state === 'ready') { + dropped = true; + event.stopImmediatePropagation(); + } + }, true); + }); + }, +}; + +let count = 0; +try { + for (const [name,engine] of [['chromium',chromium],['webkit',webkit]]) { + const browser = await engine.launch(); + try { + for (const [label, inject] of Object.entries(scenarios)) { + const context = await browser.newContext({viewport:{width:390,height:844}}); + await inject(context); + const page = await context.newPage(); + const errors = []; + page.on('pageerror', e=>errors.push(e.message)); + await page.goto(base+page_url+'?layout=vertical&kunten=on',{waitUntil:'load'}); + const stage = page.locator(`[data-poetry-kanbun-viewer="${id}"]`); + await page.locator(`[data-poetry-kanbun-viewer="${id}"][data-kanbun-ready="true"]`) + .waitFor({state:'visible',timeout:20000}) + .catch(() => { throw Error(`${name}: ${label}: reader never became ready`); }); + assert.ok(await page.locator('.poem-version.source').first() + .evaluate(el=>el.classList.contains('poetry-kanbun-showing')), `${name}: ${label}: source not replaced`); + const glyphs = await stage.locator('iframe[data-kanbun-frame]').contentFrame() + .locator('#kanbun-display .kaeriten').count(); + assert.ok(glyphs > 0, `${name}: ${label}: no return marks rendered`); + assert.deepEqual(errors, [], `${name}: ${label}: page errors`); + count++; + await context.close(); + } + + // A reader stuck before ready restarts when the reader chooses + // 縦書き+訓点 again: block the frame entirely, then let it through. + const context = await browser.newContext({viewport:{width:390,height:844}}); + let blocked = true; + await context.route(/kanbun-viewer\/works\//, route => blocked ? route.abort() : route.continue()); + const page = await context.newPage(); + await page.goto(base+page_url+'?layout=vertical&kunten=on',{waitUntil:'load'}); + // Exhaust the automatic attempts (3 pings + 2 reloads at 1.5s). + await page.waitForTimeout(14000); + const stage = page.locator(`[data-poetry-kanbun-viewer="${id}"]`); + assert.notEqual(await stage.getAttribute('data-kanbun-ready'),'true',`${name}: blocked frame reported ready`); + assert.ok(await page.locator('.poem-version.source .body').first().isVisible(), + `${name}: plain source must remain while the reader is unavailable`); + blocked = false; + await page.locator('[data-poetry-kunten-choice="off"]').click(); + await page.locator('[data-poetry-kunten-choice="on"]').click(); + await page.locator(`[data-poetry-kanbun-viewer="${id}"][data-kanbun-ready="true"]`) + .waitFor({state:'visible',timeout:20000}) + .catch(() => { throw Error(`${name}: layout choice did not retry the reader`); }); + count++; + await context.close(); + } finally { await browser.close(); } + } +} finally { server.close(); } +console.log(`kanbun recovery: ${count} scenarios passed`);