diff --git a/index.html b/index.html index 6d4ed4d..f62f676 100644 --- a/index.html +++ b/index.html @@ -507,9 +507,9 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1 弱点集中特訓 - diff --git a/js/app.js b/js/app.js index 7266c16..4aacfba 100644 --- a/js/app.js +++ b/js/app.js @@ -1,6 +1,10 @@ /** * Alpine アプリ本体。単元データは ./data/categories.js と ./data/drills.js。 - * 表示系 HTML は safeHtml() 経由(DOMPurify)でサニタイズすること。 + * + * HTML 方針: + * - マークアップを描画する箇所は x-html="safeHtml(...)" のみ。 + * - タグを出さない一行表示には plainText(...)(サニタイズ後に textContent 化)。 + * - 状態に保持する解説文字列は保存時に sanitizeStoredHtml で正規化。 */ import { DRILLS } from './data/drills.js'; import { CATEGORIES } from './data/categories.js'; @@ -19,11 +23,27 @@ function sanitizeTrustedHtml(dirty) { }); } +/** localStorage / 画面状態に載せる HTML 断片の正規化(表示前と同じルール) */ +function sanitizeStoredHtml(html) { + return sanitizeTrustedHtml(html); +} + +/** サニタイズ済み HTML からプレーン文字列(x-text 向け) */ +function plainTextFromSanitizedHtml(html) { + if (html == null || html === '') return ''; + const d = document.createElement('div'); + d.innerHTML = sanitizeTrustedHtml(String(html)); + return (d.textContent || '').replace(/\s+/g, ' ').trim(); +} + document.addEventListener('alpine:init', () => { Alpine.data('bokiApp', () => ({ safeHtml(s) { return sanitizeTrustedHtml(s); }, + plainText(s) { + return plainTextFromSanitizedHtml(s); + }, categories:[], currentUnit:null, quizState:{}, @@ -228,7 +248,7 @@ document.addEventListener('alpine:init', () => { answered(qi){ return !!this.quizState[qi]; }, doAnswer(qi,ci,correct,exp){ if(this.quizState[qi]) return; - this.quizState[qi]={selected:ci,correct:ci===correct,exp}; + this.quizState[qi]={selected:ci,correct:ci===correct,exp:sanitizeStoredHtml(exp)}; this.quizState={...this.quizState}; // Check if all questions answered const total=this.currentUnit?.quiz?.length||0; @@ -353,7 +373,7 @@ document.addEventListener('alpine:init', () => { }, doWeakDrillAnswer(qi,ci,correct,exp){ if(this.weakDrillQuizState[qi]) return; - this.weakDrillQuizState[qi]={selected:ci,correct:ci===correct,exp}; + this.weakDrillQuizState[qi]={selected:ci,correct:ci===correct,exp:sanitizeStoredHtml(exp)}; this.weakDrillQuizState={...this.weakDrillQuizState}; }, get weakDrillAllAnswered(){ @@ -431,6 +451,7 @@ document.addEventListener('alpine:init', () => { this.resetFlashRevealState(); } } else if(this.stepStep===3){ + this.quizState={}; const drills=this.unitDrills; if(drills.length>0){ this.stepStep=2; @@ -457,12 +478,16 @@ document.addEventListener('alpine:init', () => { }, _kpDelimiterIndex(kp){ if(!kp||typeof kp!=='string') return -1; - const iw=kp.indexOf(':'); - const ia=kp.indexOf(':'); - if(iw<0&&ia<0) return -1; - if(iw<0) return ia; - if(ia<0) return iw; - return Math.min(iw,ia); + const ja=kp.indexOf(':'); + if(ja>=0) return ja; + let depth=0; + for(let i=0;i') depth=Math.max(0,depth-1); + else if(ch===':'&&depth===0) return i; + } + return -1; }, keypointHasFlip(idx){ const kp=(this.currentUnit?.keypoints||[])[idx]; diff --git a/sw.js b/sw.js index 6df9d67..aa7c8db 100644 --- a/sw.js +++ b/sw.js @@ -1,5 +1,5 @@ // posimai-boki SW — stale-while-revalidate + skipWaiting -const CACHE = 'posimai-boki-v15'; +const CACHE = 'posimai-boki-v16'; const STATIC = ['/', '/index.html', '/manifest.json', '/logo.png', '/js/app.js', '/js/data/drills.js', '/js/data/categories.js']; self.addEventListener('install', e => {