feat: cache kanji request, add kklc tools
This commit is contained in:
parent
b984c9fadb
commit
588a0d1480
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -32,5 +32,7 @@ src/**/*.webp
|
||||||
.obsidian/
|
.obsidian/
|
||||||
.stfolder/
|
.stfolder/
|
||||||
|
|
||||||
# generated kanji
|
# Kanji generator
|
||||||
public/static/kanji/*
|
tools/kklc/kklc.csv
|
||||||
|
tools/kklc/target/
|
||||||
|
public/static/kanji/
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
|
type Ruby = Array<[string, string]>;
|
||||||
|
|
||||||
export interface KKLCEntry {
|
export interface KKLCEntry {
|
||||||
entry: number;
|
id: number;
|
||||||
char: string;
|
char: string;
|
||||||
meanings: string[];
|
keys: string[];
|
||||||
on: string[];
|
|
||||||
kun: string[];
|
|
||||||
examples: Array<
|
|
||||||
[Array<[string, string]>, string]
|
|
||||||
>;
|
|
||||||
senses: string[];
|
senses: string[];
|
||||||
|
onyomi: string[];
|
||||||
|
kunyomi: string[];
|
||||||
|
examples: Array<[string, Ruby]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function chooseKanji(): Promise<number> {
|
async function chooseId(): Promise<number> {
|
||||||
const date = new Date().toLocaleDateString('en');
|
const date = new Date().toLocaleDateString('en');
|
||||||
console.log(date);
|
|
||||||
const data = new TextEncoder().encode(date);
|
const data = new TextEncoder().encode(date);
|
||||||
|
|
||||||
const hash = await crypto.subtle.digest('SHA-256', data);
|
const hash = await crypto.subtle.digest('SHA-256', data);
|
||||||
|
@ -23,10 +22,30 @@ async function chooseKanji(): Promise<number> {
|
||||||
|
|
||||||
const min = 1;
|
const min = 1;
|
||||||
const max = 2300;
|
const max = 2300;
|
||||||
const id = min + (hashValue % (max - min + 1));
|
return min + (hashValue % (max - min + 1));
|
||||||
return id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchKanji(): Promise<KKLCEntry> {
|
function tryGetCache(id: number) {
|
||||||
return await fetch(`/static/kanji/${await chooseKanji()}.json`).then(res => res.json());
|
const item = localStorage.getItem('kanji');
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const cache = JSON.parse(item);
|
||||||
|
if (cache.id === id) {
|
||||||
|
return cache.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertCache(id: number, data: KKLCEntry) {
|
||||||
|
localStorage.setItem('kanji', JSON.stringify({ id, data }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getKanji(): Promise<KKLCEntry> {
|
||||||
|
const id = await chooseId();
|
||||||
|
|
||||||
|
const cache = tryGetCache(id);
|
||||||
|
if (cache) return cache;
|
||||||
|
|
||||||
|
const data = await fetch(`/static/kanji/${id}.json`).then(res => res.json());
|
||||||
|
insertCache(id, data);
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fetchKanji, type KKLCEntry } from './data.svelte.ts';
|
import { getKanji, type KKLCEntry } from './data.svelte.ts';
|
||||||
|
|
||||||
let state = $state<Promise<KKLCEntry>>(new Promise(() => {}));
|
let state = $state<Promise<KKLCEntry>>(new Promise(() => {}));
|
||||||
|
|
||||||
$effect(() => void (state = fetchKanji()));
|
$effect(() => void (state = getKanji()));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,26 +19,26 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="info-meta">
|
<div class="info-meta">
|
||||||
<div class="info-key">
|
<div class="info-key">
|
||||||
{state.meanings.join(', ')}
|
{state.keys.join(', ')}
|
||||||
</div>
|
</div>
|
||||||
<div class="info-on">
|
<div class="info-on">
|
||||||
{state.on.join(', ')}
|
{state.onyomi.join(', ')}
|
||||||
</div>
|
</div>
|
||||||
<div class="info-kun">
|
<div class="info-kun">
|
||||||
{state.kun.join(', ')}
|
{state.kunyomi.join(', ')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table>
|
<table class="examples">
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each state.examples as [example, meaning]}
|
{#each state.examples as [meaning, example]}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td class="examples-ja">
|
||||||
<ruby>
|
<ruby>
|
||||||
{#each example as [kanji, furigana]}{kanji}<rt>{furigana || ''}</rt>{/each}
|
{#each example as [expr, ruby]}{expr}<rt>{ruby||''}</rt>{/each}
|
||||||
</ruby>
|
</ruby>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="examples-en">
|
||||||
{meaning}
|
{meaning}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-char {
|
.info-char {
|
||||||
|
align-self: flex-start;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0.1em;
|
padding: 0.1em;
|
||||||
border: 1px dashed black;
|
border: 1px dashed black;
|
||||||
|
@ -42,8 +43,10 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ruby-cell {
|
|
||||||
min-width: 5em;
|
.examples-ja {
|
||||||
|
padding-right: 0.5em;
|
||||||
|
max-width: 3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
$side: 5em;
|
$side: 5em;
|
||||||
|
|
124
tools/kklc/Cargo.lock
generated
Normal file
124
tools/kklc/Cargo.lock
generated
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
|
||||||
|
dependencies = [
|
||||||
|
"csv-core",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv-core"
|
||||||
|
version = "0.1.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "furigana"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f66a30b0d76741b8ddf54f5cd6bd80c635810ce9dcef147c14ee545e1b07bcd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kklc"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"csv",
|
||||||
|
"furigana",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.78"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.196"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.196"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.113"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.48"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
12
tools/kklc/Cargo.toml
Normal file
12
tools/kklc/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "kklc"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0.196", features = ["derive"] }
|
||||||
|
serde_json = "1.0.113"
|
||||||
|
csv = "1.3"
|
||||||
|
furigana = "0.1.1"
|
163
tools/kklc/src/main.rs
Normal file
163
tools/kklc/src/main.rs
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
use std::{collections::HashMap, error::Error};
|
||||||
|
|
||||||
|
use std::fs::File;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use csv::{ReaderBuilder, StringRecord};
|
||||||
|
|
||||||
|
|
||||||
|
const PATH_CSV: &str = "kklc.csv";
|
||||||
|
|
||||||
|
|
||||||
|
type Readings = HashMap<String, Vec<String>>;
|
||||||
|
type Ruby = Vec<(String, Option<String>)>;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct KKLCEntry {
|
||||||
|
id: i32,
|
||||||
|
char: String,
|
||||||
|
keys: Vec<String>,
|
||||||
|
senses: Vec<String>,
|
||||||
|
onyomi: Vec<String>,
|
||||||
|
kunyomi: Vec<String>,
|
||||||
|
examples: Vec<(String, Ruby)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn get_readings() -> HashMap<String, Vec<String>> {
|
||||||
|
ReaderBuilder::new()
|
||||||
|
.from_path(PATH_CSV)
|
||||||
|
.expect("Failed opening CSV")
|
||||||
|
.records()
|
||||||
|
.map(|record| {
|
||||||
|
let record = record.expect("Error reading CSV record");
|
||||||
|
|
||||||
|
let kanji = record.get(1).unwrap();
|
||||||
|
let onyomi = record.get(3)
|
||||||
|
.unwrap()
|
||||||
|
.split("\n\n")
|
||||||
|
.map(|s| s.split('(').next().unwrap().to_owned());
|
||||||
|
let kunyomi = record.get(4)
|
||||||
|
.unwrap()
|
||||||
|
.split("\n\n")
|
||||||
|
.map(|s| s.split('(').next().unwrap().to_owned());
|
||||||
|
|
||||||
|
(kanji.to_owned(), kunyomi.chain(onyomi).collect())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_vocab(str: &str) -> (String, String, String) {
|
||||||
|
let mut iter = str.split(" ・ ").map(String::from);
|
||||||
|
(
|
||||||
|
iter.next().unwrap()
|
||||||
|
.replace("¹", "")
|
||||||
|
.replace("²", "")
|
||||||
|
.replace("…", "")
|
||||||
|
.replace("*", "")
|
||||||
|
.trim()
|
||||||
|
.into(),
|
||||||
|
iter.next().unwrap()
|
||||||
|
.split('[').next().unwrap()
|
||||||
|
.split('(').next().unwrap()
|
||||||
|
.replace(" ", "")
|
||||||
|
.replace("¹", "")
|
||||||
|
.replace("²", "")
|
||||||
|
.replace("…", "")
|
||||||
|
.trim()
|
||||||
|
.into(),
|
||||||
|
iter.next().unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_ruby(fst: &str, snd: &str, readings: &Readings) -> Ruby {
|
||||||
|
if !fst.contains("○") {
|
||||||
|
let mapping = furigana::map(fst, snd, &readings)
|
||||||
|
.into_iter()
|
||||||
|
.max_by_key(|f| f.accuracy);
|
||||||
|
|
||||||
|
if let Some(mapping) = mapping {
|
||||||
|
if mapping.accuracy > 0 {
|
||||||
|
return mapping.furigana
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| (x.segment.to_owned(), x.furigana.map(String::from)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![(fst.to_owned(), Some(snd.to_owned()))]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_example(vocab: &[(String, String, String)], readings: &Readings) -> Vec<(String, Ruby)> {
|
||||||
|
vocab.iter()
|
||||||
|
.map(|(fst, snd, word)| (word.to_owned(), map_ruby(fst, snd, readings)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_record(record: StringRecord, readings: &Readings) -> KKLCEntry {
|
||||||
|
let id = record.get(0)
|
||||||
|
.unwrap()
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let char: String = record.get(1)
|
||||||
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
let keys = record.get(2)
|
||||||
|
.unwrap_or("")
|
||||||
|
.split("\n\n")
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
let onyomi = record.get(3)
|
||||||
|
.unwrap_or("")
|
||||||
|
.split("\n\n")
|
||||||
|
.map(String::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let kunyomi = record.get(4)
|
||||||
|
.unwrap_or("")
|
||||||
|
.split("\n\n")
|
||||||
|
.map(String::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let vocab = record.get(5)
|
||||||
|
.unwrap_or("")
|
||||||
|
.split("\n\n")
|
||||||
|
.map(map_vocab)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let senses = record.get(6)
|
||||||
|
.unwrap_or("")
|
||||||
|
.split("\n\n")
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let examples = map_example(&vocab, readings);
|
||||||
|
|
||||||
|
KKLCEntry { id, char, keys, senses, onyomi, kunyomi, examples }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_entries(readings: &HashMap<String, Vec<String>>) -> Vec<KKLCEntry> {
|
||||||
|
let mut reader = ReaderBuilder::new()
|
||||||
|
.from_path(PATH_CSV)
|
||||||
|
.expect("Error opening file");
|
||||||
|
|
||||||
|
reader.records()
|
||||||
|
.map(|record| {
|
||||||
|
let record = record.expect("Error reading CSV record");
|
||||||
|
map_record(record, readings)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let readings = get_readings();
|
||||||
|
let entries = get_entries(&readings);
|
||||||
|
|
||||||
|
for (i, entry) in entries.iter().enumerate() {
|
||||||
|
let path = format!("../../public/static/kanji/{}.json", i + 1);
|
||||||
|
let file = File::create(path).expect("Error creating file");
|
||||||
|
|
||||||
|
serde_json::to_writer(&file, entry).expect("Error writing JSON to file");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in a new issue