🗂 コードツアー
このページはPenguin Language Plusのソースコードを解説しています。
サイトの中にサイトの説明が入る — マトリョーシカ設計。
技術スタック
フレームワークは最小限。Astroのテンプレートエンジン+バニラJSのみで構築しています。
AstroVanilla JSlocalStorageWeb Speech APICSS Custom PropertiesVercel deploy
ビルド:
ページは全て静的HTML。バックエンドなし、DBなし、オフラインでも動く。
npm run build → Vercel に自動デプロイ(git push するだけ)。ページは全て静的HTML。バックエンドなし、DBなし、オフラインでも動く。
⌨️ ローマ字変換エンジン
toRomaji() がひらがな・カタカナをローマ字に変換します。 拗音(きゃ→kya など2文字)を先に照合してから1文字照合する2段階ルックアップです。
// 変換テーブル(拗音を先に定義することが重要) const KR = { 'きゃ':'kya', 'しゃ':'sha', 'ちゃ':'cha', // 拗音(2文字) 'あ':'a', 'い':'i', 'う':'u', // 単文字 'っ':/* 次の子音を重ねる特殊処理 */ }; function hNorm(c) { // カタカナ→ひらがな正規化 const code = c.charCodeAt(0); return (code >= 0x30A1 && code <= 0x30F3) ? String.fromCharCode(code - 0x60) : c; } function toRomaji(text) { let res='', i=0; while (i < text.length) { const c = hNorm(text[i]); // カタカナ→ひらがな正規化 // 拗音(2文字)を先にチェック if (i+1 < text.length) { const two = c + hNorm(text[i+1]); if (KR[two]) { res += KR[two]; i+=2; continue; } } res += KR[c] || c; i++; // 単文字、またはそのまま } return res; }
🎯 タイピング判定とオーバーレイ
透明な <input> をガイド表示 .target-display の上に重ね、 1文字ずつ tc-ok / tc-ng / tc-cur / tc-rest クラスで色分けします。
/* CSS: 透明inputをdisplayの上に絶対配置 */ .type-area { position: relative; } .type-input { position: absolute; inset: 0; background: transparent; color: transparent; caret-color: var(--accent); /* カーソルだけ見える */ border: none; outline: none; } // JS: 1文字ずつクラスで色分け function updateTypeDisplay(input) { const target = getTypeTarget(pairs[typeIdx]); let html = ''; for (let i=0; i < target.length; i++) { const ch = target[i]; if (i < input.length) html += input[i]===ch ? '<span class="tc-ok">'+ch+'</span>' // 緑: 正解 : '<span class="tc-ng">'+ch+'</span>'; // 赤: 誤り else if (i===input.length) html += '<span class="tc-cur">'+ch+'</span>'; else html += '<span class="tc-rest">'+ch+'</span>'; } document.getElementById('type-display').innerHTML = html; }
🗂 タブシステム
setupTabs(groupId) は汎用タブ切替関数です。 グループIDを渡すとその中の .tab ボタンを全て監視し、クリック時に対応する #panel-{名前} を表示します。 2グループ(日本語・自分の練習)に同じ関数を使い回しています。
function setupTabs(groupId) { document.querySelectorAll(`#${groupId} .tab`).forEach(btn => { btn.addEventListener('click', () => { // 全タブ・全パネルを非アクティブに document.querySelectorAll('.tab' ).forEach(b => b.classList.remove('active')); document.querySelectorAll('.panel').forEach(p => p.classList.remove('active')); // クリックされたタブとパネルをアクティブに btn.classList.add('active'); document.getElementById('panel-' + btn.dataset.tab).classList.add('active'); }); }); } setupTabs('tabs-lang'); // 日本語グループ(文法マップ・発音・単語集…) setupTabs('tabs-practice'); // 練習グループ(例文・学習・タイピング…)
💾 データ保存(localStorage)
ユーザーの例文はブラウザの localStorage にJSONで保存します。 サーバー不要、オフライン動作、ページリロードしてもデータが残ります。
const STORAGE_KEY = 'pl_pairs_v2'; function save() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(pairs)); } catch(e) {} // プライベートモードでも落ちないよう try/catch } function load() { try { const d = localStorage.getItem(STORAGE_KEY); if (d) pairs = JSON.parse(d); } catch(e) {} }
このページ自体がドキュメントであり、ドキュメントがこのページである — マトリョーシカ設計。