chore: sanitize stored quiz exp, kp delimiter outside tags, weak drill exit label, plainText helper
Made-with: Cursor
This commit is contained in:
parent
0f2ae7f332
commit
16547cf0ad
|
|
@ -507,9 +507,9 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
|
||||||
<i data-lucide="zap" style="width:13px;height:13px"></i>
|
<i data-lucide="zap" style="width:13px;height:13px"></i>
|
||||||
弱点集中特訓
|
弱点集中特訓
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-sm" @click="exitWeakDrill()">
|
<button class="btn-sm" type="button" @click="exitWeakDrill()" aria-label="弱点特訓を終了する">
|
||||||
<i data-lucide="arrow-left" style="width:11px;height:11px"></i>
|
<i data-lucide="x" style="width:11px;height:11px"></i>
|
||||||
戻る
|
特訓をやめる
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
43
js/app.js
43
js/app.js
|
|
@ -1,6 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* Alpine アプリ本体。単元データは ./data/categories.js と ./data/drills.js。
|
* Alpine アプリ本体。単元データは ./data/categories.js と ./data/drills.js。
|
||||||
* 表示系 HTML は safeHtml() 経由(DOMPurify)でサニタイズすること。
|
*
|
||||||
|
* HTML 方針:
|
||||||
|
* - マークアップを描画する箇所は x-html="safeHtml(...)" のみ。
|
||||||
|
* - タグを出さない一行表示には plainText(...)(サニタイズ後に textContent 化)。
|
||||||
|
* - 状態に保持する解説文字列は保存時に sanitizeStoredHtml で正規化。
|
||||||
*/
|
*/
|
||||||
import { DRILLS } from './data/drills.js';
|
import { DRILLS } from './data/drills.js';
|
||||||
import { CATEGORIES } from './data/categories.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', () => {
|
document.addEventListener('alpine:init', () => {
|
||||||
Alpine.data('bokiApp', () => ({
|
Alpine.data('bokiApp', () => ({
|
||||||
safeHtml(s) {
|
safeHtml(s) {
|
||||||
return sanitizeTrustedHtml(s);
|
return sanitizeTrustedHtml(s);
|
||||||
},
|
},
|
||||||
|
plainText(s) {
|
||||||
|
return plainTextFromSanitizedHtml(s);
|
||||||
|
},
|
||||||
categories:[],
|
categories:[],
|
||||||
currentUnit:null,
|
currentUnit:null,
|
||||||
quizState:{},
|
quizState:{},
|
||||||
|
|
@ -228,7 +248,7 @@ document.addEventListener('alpine:init', () => {
|
||||||
answered(qi){ return !!this.quizState[qi]; },
|
answered(qi){ return !!this.quizState[qi]; },
|
||||||
doAnswer(qi,ci,correct,exp){
|
doAnswer(qi,ci,correct,exp){
|
||||||
if(this.quizState[qi]) return;
|
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};
|
this.quizState={...this.quizState};
|
||||||
// Check if all questions answered
|
// Check if all questions answered
|
||||||
const total=this.currentUnit?.quiz?.length||0;
|
const total=this.currentUnit?.quiz?.length||0;
|
||||||
|
|
@ -353,7 +373,7 @@ document.addEventListener('alpine:init', () => {
|
||||||
},
|
},
|
||||||
doWeakDrillAnswer(qi,ci,correct,exp){
|
doWeakDrillAnswer(qi,ci,correct,exp){
|
||||||
if(this.weakDrillQuizState[qi]) return;
|
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};
|
this.weakDrillQuizState={...this.weakDrillQuizState};
|
||||||
},
|
},
|
||||||
get weakDrillAllAnswered(){
|
get weakDrillAllAnswered(){
|
||||||
|
|
@ -431,6 +451,7 @@ document.addEventListener('alpine:init', () => {
|
||||||
this.resetFlashRevealState();
|
this.resetFlashRevealState();
|
||||||
}
|
}
|
||||||
} else if(this.stepStep===3){
|
} else if(this.stepStep===3){
|
||||||
|
this.quizState={};
|
||||||
const drills=this.unitDrills;
|
const drills=this.unitDrills;
|
||||||
if(drills.length>0){
|
if(drills.length>0){
|
||||||
this.stepStep=2;
|
this.stepStep=2;
|
||||||
|
|
@ -457,12 +478,16 @@ document.addEventListener('alpine:init', () => {
|
||||||
},
|
},
|
||||||
_kpDelimiterIndex(kp){
|
_kpDelimiterIndex(kp){
|
||||||
if(!kp||typeof kp!=='string') return -1;
|
if(!kp||typeof kp!=='string') return -1;
|
||||||
const iw=kp.indexOf(':');
|
const ja=kp.indexOf(':');
|
||||||
const ia=kp.indexOf(':');
|
if(ja>=0) return ja;
|
||||||
if(iw<0&&ia<0) return -1;
|
let depth=0;
|
||||||
if(iw<0) return ia;
|
for(let i=0;i<kp.length;i++){
|
||||||
if(ia<0) return iw;
|
const ch=kp[i];
|
||||||
return Math.min(iw,ia);
|
if(ch==='<') depth++;
|
||||||
|
else if(ch==='>') depth=Math.max(0,depth-1);
|
||||||
|
else if(ch===':'&&depth===0) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
},
|
},
|
||||||
keypointHasFlip(idx){
|
keypointHasFlip(idx){
|
||||||
const kp=(this.currentUnit?.keypoints||[])[idx];
|
const kp=(this.currentUnit?.keypoints||[])[idx];
|
||||||
|
|
|
||||||
2
sw.js
2
sw.js
|
|
@ -1,5 +1,5 @@
|
||||||
// posimai-boki SW — stale-while-revalidate + skipWaiting
|
// 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'];
|
const STATIC = ['/', '/index.html', '/manifest.json', '/logo.png', '/js/app.js', '/js/data/drills.js', '/js/data/categories.js'];
|
||||||
|
|
||||||
self.addEventListener('install', e => {
|
self.addEventListener('install', e => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue