diff --git a/_config.ts b/_config.ts index f2f778e..b25c6d0 100644 --- a/_config.ts +++ b/_config.ts @@ -42,7 +42,7 @@ const site = lume({ site.use(esbuild({ extensions: [".ts", ".tsx"], options: { - target: "es2020", + target: "es2022", minify: false, keepNames: true, plugins: [yamlPlugin], diff --git a/src/_components/GameMain.tsx b/src/_components/GameMain.tsx index 71dac2c..ab1fab7 100644 --- a/src/_components/GameMain.tsx +++ b/src/_components/GameMain.tsx @@ -1,8 +1,7 @@ import RomajiField from "./RomajiField.tsx"; -import { GameSettings } from "./_lib.ts"; +import { GameSettings, QA } from "../_lib/types.ts"; import { Signal, useEffect, useSignal } from "../_deps.ts"; -type QandA = { q: string; a: string }; type GameMainState = "ready" | "playing"; function Timer({ timer }: { timer: Signal }) { @@ -46,7 +45,7 @@ function addScoreGetAnimation(pt: number) { } export default function GameMain( - { problems, settings }: { problems: QandA[]; settings: GameSettings }, + { problems, settings }: { problems: QA[]; settings: GameSettings }, ) { const currentNum = useSignal(0); const state = useSignal("ready"); @@ -150,7 +149,7 @@ export default function GameMain( <>
{question}
diff --git a/src/_components/RomajiField.tsx b/src/_components/RomajiField.tsx index d5b0c4a..47abf7b 100644 --- a/src/_components/RomajiField.tsx +++ b/src/_components/RomajiField.tsx @@ -1,6 +1,6 @@ import RomajiYaml_ from "../_data/romaji.yaml" with { type: "json" }; -import { loadRomajiDict, matchInput } from "./_engine.ts"; -import { Hankaku } from "./_lib.ts"; +import { loadRomajiDict, matchInput } from "../_lib/engine.ts"; +import { Hankaku } from "../_lib/types.ts"; import { signal, useEffect } from "../_deps.ts"; import { hint } from "./Keyboard.tsx"; diff --git a/src/_components/_engine.ts b/src/_lib/engine.ts similarity index 100% rename from src/_components/_engine.ts rename to src/_lib/engine.ts diff --git a/src/_components/_engine_test.ts b/src/_lib/engine_test.ts similarity index 99% rename from src/_components/_engine_test.ts rename to src/_lib/engine_test.ts index 4a7ce64..070982a 100644 --- a/src/_components/_engine_test.ts +++ b/src/_lib/engine_test.ts @@ -2,7 +2,7 @@ import { loadRomajiDict, matchInput, test -} from "./_engine.ts"; +} from "./engine.ts"; import { assertEquals } from "@std/assert"; import { parse } from "@std/yaml/parse"; diff --git a/src/_lib/fetchProblems.ts b/src/_lib/fetchProblems.ts new file mode 100644 index 0000000..0234420 --- /dev/null +++ b/src/_lib/fetchProblems.ts @@ -0,0 +1,61 @@ +import type { GameSettings, QA } from "./types.ts"; +import { PCG } from "./pcg.ts"; + +function parseCsv(csv: string, separator = ","): QA[] { + return csv.split("\n") + .map((x) => x.split(separator)) + .map(([q, a]) => ({ q, a })); +} +function parseSettings(problems: QA[]) { + const settings: GameSettings = { + title: "", + timelimit: 60, + shuffle: true, + voice: false, + }; + problems.forEach(({ q, a }) => { + if (!q.startsWith(":")) return; + if(a === null) return; + switch (q) { + case ":title": + settings.title = a; + break; + case ":timelimit": + settings.timelimit = parseInt(a); + break; + case ":shuffle": + settings.shuffle = a !== "false"; + break; + case ":voice": + settings.voice = a === "true"; + break; + } + }); + return settings; +} +function shuffleArray(array: unknown[], rng: PCG) { + for (let i = array.length - 1; i > 1; i--) { + const j = rng.nextInt(i); + [array[i], array[j]] = [array[j], array[i]]; + } +} + +export async function fetchProblems(url: string) { + const errors: string[] = []; + const separator = url.endsWith(".tsv") ? "\t" : ","; + const response = await fetch(url) + if (!response.ok) { + return { ok: false as const, errors: [`Failed to fetch ${url}: ${response.statusText}`] }; + } + const data = parseCsv(await response.text(), separator); + const settings = parseSettings(data); + const problems = data.filter(({ q }) => !q.startsWith(":")); + + if (settings.shuffle) { + const today = Math.floor(Date.now() / 86400000); + const rng = new PCG(BigInt(today)); + shuffleArray(problems, rng); + } + if (errors.length === 0) return { ok: true as const, problems, settings } + return { ok: false as const, errors }; +} diff --git a/src/_components/_lib.ts b/src/_lib/types.ts similarity index 84% rename from src/_components/_lib.ts rename to src/_lib/types.ts index efc8051..ec2be52 100644 --- a/src/_components/_lib.ts +++ b/src/_lib/types.ts @@ -7,3 +7,4 @@ export type GameSettings = { shuffle: boolean; voice: boolean; }; +export type QA = { q: string; a: string | null }; diff --git a/src/csv/typescript.csv b/src/csv/typescript.csv deleted file mode 100644 index fd1f371..0000000 --- a/src/csv/typescript.csv +++ /dev/null @@ -1,12 +0,0 @@ -:title,typescript -Record -addEventListener('click', ()=>{}) -let isDone = false -const list: number[] = [] -interface Person { name: string; age: number } -type StringOrNumber = string | number -function greet(name: string): string { return `Hello, ${name}` } -enum Direction { Up, Down, Left, Right } -const tuple: [string, number] = ['hello', 10] -class Animal { constructor(public name: string) {} } -const readOnlyArray: ReadonlyArray = [1, 2, 3] diff --git a/src/csv/typescript.tsv b/src/csv/typescript.tsv new file mode 100644 index 0000000..91ed3f7 --- /dev/null +++ b/src/csv/typescript.tsv @@ -0,0 +1,13 @@ +:title typescript +let isDone=false +Record +addEventListener('click',()=>{}) +const list:number[]=[] +interface Person{name:string;age:number} +type StringOrNumber=string|number +return`Hello, ${name}` +enum Direction{Up,Down,Left,Right} +const tuple:[string,number]=['hello',10] +class Animal{constructor(public name:string){}} +const readOnlyArray:ReadonlyArray=[1,2,3] +function greet(name:string):string{ diff --git a/src/inst.md b/src/inst.md index fc0fda6..6005d3a 100644 --- a/src/inst.md +++ b/src/inst.md @@ -15,7 +15,7 @@ layout: layout.vto - [都道府県](/#/csv/todoufuken.csv) - [県庁所在地](/#/csv/kenchou.csv) - [百人一首](/#/csv/hyakunin.csv) -- [typescript](/#/csv/typescript.csv) +- [typescript](/#csv=/csv/typescript.tsv) # 問題の作り方 diff --git a/src/main.tsx b/src/main.tsx index 2504016..f9e7467 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,72 +1,34 @@ import GameMain from "./_components/GameMain.tsx"; import Keyboard from "./_components/Keyboard.tsx"; -import { GameSettings } from "./_components/_lib.ts"; +import { fetchProblems } from "./_lib/fetchProblems.ts"; import { render } from "./_deps.ts"; -import { PCG } from "./_lib/pcg.ts"; -function parseCsv(csv: string) { - return csv.split("\n") - .map((x) => x.split(",")) - .map(([q, a]) => ({ q, a: a ? a.trim() : q })); -} - -(async () => { - const hash = location.hash.slice(1); - let problems_; - if (hash.length > 0) { - problems_ = await fetch(hash).then((x) => x.text()).then(parseCsv).catch( - console.error, - ); - } - problems_ = problems_ || - await fetch("/csv/default.csv").then((x) => x.text()).then(parseCsv); - - const settings: GameSettings = { - title: "", - timelimit: 60, - shuffle: true, - voice: false, - }; - problems_.forEach(({ q, a }) => { - if (!q.startsWith(":")) return; - switch (q) { - case ":title": - settings.title = a; - break; - case ":timelimit": - settings.timelimit = parseInt(a); - break; - case ":shuffle": - settings.shuffle = a !== "false"; - break; - case ":voice": - settings.voice = a === "true"; - break; - } - }); - - function shuffleArray(array: unknown[], rng: PCG) { - for (let i = array.length - 1; i > 1; i--) { - const j = rng.nextInt(i); - [array[i], array[j]] = [array[j], array[i]]; - } +const hash = location.hash.slice(1); +let csv = "/csv/default.csv" +if (hash.length > 0) { + if (!hash.includes("csv=")) { + csv = hash + } else { + const params = new URLSearchParams(hash); + csv = params.get("csv")! } +} - const problems = problems_.filter(({ q }) => !q.startsWith(":")); - if (settings.shuffle) { - const today = Math.floor(Date.now() / 86400000); - const rng = new PCG(BigInt(today)); - shuffleArray(problems, rng); - } - const App = ( - <> - -
- きーぼーど - -
- - ); +const appElem = document.getElementById("app")!; +const probsSets = await fetchProblems(csv) +if (!probsSets.ok) { + appElem.insertAdjacentHTML("beforeend", probsSets.errors.join("
")); + throw new Error('failed to init'); +} - render(App, document.getElementById("app")!); -})(); +const { problems, settings } = probsSets; +const App = ( + <> + +
+ きーぼーど + +
+ +); +render(App, appElem);