feat(ktb): add markdown parsing, sanitation, and add-lect feature

This commit is contained in:
nikomiko 2026-07-20 15:49:59 -04:00
parent bf8538bbb9
commit 6873a8d920
4 changed files with 116 additions and 53 deletions

View file

@ -81,15 +81,25 @@ document.addEventListener("alpine:init", () => {
this.update_css_variables(); this.update_css_variables();
}, },
async fetch_all_lects() { async fetch_all_lects() {
try { try {
const res = await axios.get("http://localhost:1225/lects"); const res = await axios.get("http://localhost:1225/lects");
console.log(JSON.stringify(res.data)); return res.data;
return res.data; } catch (e) {
} catch (e) { console.error(e);
console.error(e); }
} },
},
async post_lect(lect_name) {
try {
const res = await axios.post("http://localhost:1225/lect", {
lect_name,
});
return res.data.lect;
} catch (e) {
console.error(e);
}
},
async fetch_all_terms(search_term) { async fetch_all_terms(search_term) {
try { try {
@ -180,13 +190,17 @@ document.addEventListener("alpine:init", () => {
filter_results(search_term) { filter_results(search_term) {
if (!search_term) return this.search_results.results; if (!search_term) return this.search_results.results;
return this.search_results.results.filter( return this.search_results.results.filter((item) =>
(item) => item.word_forms
item.word_forms .map((wf) => wf.word_form.toLowerCase())
.map((wf) => wf.word_form.toLowerCase()) .join()
.join() .includes(search_term.toLowerCase()),
.includes(search_term.toLowerCase()),
); );
}, },
md(text){
console.log(marked.parse(text));
return DOMPurify.sanitize(marked.parse(text), {ALLOWED_TAGS: ['br', 'em', 'strong', 'code', '#text']});
},
}); });
}); });

View file

@ -6,6 +6,8 @@
<title>Dictionary Cards</title> <title>Dictionary Cards</title>
<link rel="stylesheet" href="styles.css" /> <link rel="stylesheet" href="styles.css" />
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked@18.0.6/lib/marked.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.4.12/dist/purify.min.js"></script>
<script src="https://unpkg.com/alpinejs" defer></script> <script src="https://unpkg.com/alpinejs" defer></script>
<script src="script.js"></script> <script src="script.js"></script>
</head> </head>
@ -16,7 +18,10 @@
x-data="{ x-data="{
search_term: '', search_term: '',
showPrefs: false, showPrefs: false,
lects: [] lects: [],
md(text){
return $store.dictionary.md(text);
},
}" }"
x-init="$store.dictionary.fetch_all_lects().then((lects_response)=>{ x-init="$store.dictionary.fetch_all_lects().then((lects_response)=>{
if(!!lects_response.lects) { if(!!lects_response.lects) {
@ -169,6 +174,9 @@
<!-- wfs --> <!-- wfs -->
<h4>Trofal</h4> <h4>Trofal</h4>
<table class="is-fullwidth"> <table class="is-fullwidth">
<tbody>
<template <template
x-for="wf2 in (lemma_detail?.word_forms ?? [])"> x-for="wf2 in (lemma_detail?.word_forms ?? [])">
<tr x-data="{ <tr x-data="{
@ -201,36 +209,44 @@
</td> </td>
</tr> </tr>
</template> </template>
</tbody>
<!-- Add new wf --> <!-- Add new wf -->
<tr x-data="{ <tfoot>
adding_word_form:false, <tr x-data="{
new_word_form_text:'', adding_word_form:false,
selected_lect:'' new_word_form_text:'',
}"> selected_lect:'',
adding_lect:false,
new_lect_name:''
}">
<td colspan="3" x-show="!adding_word_form"> <td colspan="3" x-show="!adding_word_form" style="width: 16em;">
<button class="is-fullwidth hollow primary" <button class="is-fullwidth hollow primary"
@click="adding_word_form = !adding_word_form">+</button> @click="adding_word_form = !adding_word_form">+</button>
</td> </td>
<td x-show="adding_word_form"> <td x-show="adding_word_form">
<select name="vilgovor" x-model="selected_lect"> <select name="vilgovor" x-model="selected_lect" x-show="!adding_lect">
<template x-for="lect in lects"> <template x-for="lect in lects">
<option :value="lect.name" x-text="lect.name"></option> <option :value="lect.name" x-text="lect.name"></option>
</template> </template>
</select> </select>
</td> <input type="text" x-show="adding_lect" x-model="new_lect_name">
<td x-show="adding_word_form"> <button class="float-right tiny primary" x-show="!adding_lect" @click="adding_lect = true" title="neo govor">+</button>
<input type="text" x-model="new_word_form_text"> <button class="float-right tiny edit" x-show="adding_lect" title="jame" @click="adding_lect = false">/</button>
</td> </td>
<td x-show="adding_word_form"> <td x-show="adding_word_form">
<button class="tiny edit" x-show="adding_word_form" title="jame" @click="adding_word_form = false">/</button> <input type="text" x-model="new_word_form_text">
<button class="tiny" x-show="adding_word_form" title="antaa" </td>
@click="await post_new_word_form(item.lemma_name, selected_lect, new_word_form_text); adding_word_form = false">></button> <td x-show="adding_word_form">
</td> <button class="tiny edit" x-show="adding_word_form" title="jame" @click="adding_word_form = false">/</button>
<button class="tiny" x-show="adding_word_form" title="antaa"
@click="await post_new_word_form(item.lemma_name, adding_lect ? new_lect_name : selected_lect, new_word_form_text); adding_word_form = false">></button>
</td>
</tr> </tr>
</tfoot>
</table> </table>
<h4>Imi</h4> <h4>Imi</h4>
@ -241,7 +257,7 @@
editing: false, editing: false,
edited_text: def.definition_text.toString() edited_text: def.definition_text.toString()
}" @click.outside="editing = false"> }" @click.outside="editing = false">
<span x-show="!editing" x-text="def.definition_text" @click="editing = true"></span> <span x-show="!editing" x-html="md(def.definition_text)" @click="editing = true"></span>
<input type="text" x-show="editing" x-model="edited_text"> <input type="text" x-show="editing" x-model="edited_text">
<button class="float-right tiny hollow primary" title="kawari" x-show="!editing" <button class="float-right tiny hollow primary" title="kawari" x-show="!editing"
@click="editing = true">~</button> @click="editing = true">~</button>
@ -292,7 +308,7 @@
edited_text: ex.example_text.toString() edited_text: ex.example_text.toString()
}" }"
@click.outside="editing = false"> @click.outside="editing = false">
<span x-show="!editing" x-text="ex.example_text" @click="editing = true"></span> <span x-show="!editing" x-html="md(ex.example_text)" @click="editing = true"></span>
<input type="text" x-show="editing" x-model="edited_text"> <input type="text" x-show="editing" x-model="edited_text">
<button class="float-right tiny hollow primary" x-show="!editing" @click="editing = true" title="kawari">~</button> <button class="float-right tiny hollow primary" x-show="!editing" @click="editing = true" title="kawari">~</button>
<button class="float-right tiny edit" x-show="editing" @click="editing = false" title="jame">/</button> <button class="float-right tiny edit" x-show="editing" @click="editing = false" title="jame">/</button>

View file

@ -47,7 +47,7 @@
} }
body { body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family: 'Verdana', -apple-system, BlinkMacSystemFont, sans-serif;
background-color: var(--color-main-bg); background-color: var(--color-main-bg);
color: var(--color-text); color: var(--color-text);
line-height: 1.5; line-height: 1.5;
@ -72,15 +72,19 @@ ul {
} }
li { li {
margin-top: calc(var(--space-xs)/2); margin-top: calc(var(--space-xs));
margin-bottom: calc(var(--space-xs)/2); margin-bottom: calc(var(--space-xs)/2);
margin-left: var(--space-md); margin-left: var(--space-md);
} }
table { tbody {
table-layout: fixed; table-layout: fixed;
} }
tfoot {
table-layout: auto;
}
.float-right { .float-right {
float: right; float: right;
} }

View file

@ -79,6 +79,21 @@ function initExpress() {
res.status(200).send({ lect }); res.status(200).send({ lect });
}); });
app.post("/lect", (req, res) => {
const lect_name = req.query.lect_name?.toString();
if (!lect_name) {
return void res.sendStatus(400);
}
const lect = new Lect();
lect.name = lect_name;
lect.save().then((lect)=>{
res.status(200).send({ lect })
});
});
app.get("/lects", async (_req, res) => { app.get("/lects", async (_req, res) => {
const lects = await Lect.find(); const lects = await Lect.find();
res.status(200).send({ lects }); res.status(200).send({ lects });
@ -261,13 +276,27 @@ function initExpress() {
let word_form = new WordForm(); let word_form = new WordForm();
let lemma = await Lemma.findOne({where:{lemma_name}}); let lemma = await Lemma.findOne({where:{lemma_name}});
let lect = await Lect.findOne({where:{name:lect_name}}); let lect;
try{
lect = await Lect.findOne({where:{name:lect_name}});
} catch {
console.info(`Lect ${lect_name} not found.`);
}
if(!lect || !lemma){ if(!lemma){
console.error(`Error: ${JSON.stringify({lect:lect, lemma:lemma})}`); console.error(`Error: ${JSON.stringify({lect:lect, lemma:lemma})}`);
return void res.status(400).send(); return void res.status(400).send();
} }
if(!lect) {
lect = new Lect();
lect.name = lect_name;
await lect
.save()
.then((l)=>{console.info(`Created lect: ${JSON.stringify(l)}`)});
}
word_form.word_form = word_form_text; word_form.word_form = word_form_text;
word_form.lemma = lemma; word_form.lemma = lemma;
word_form.lect = lect; word_form.lect = lect;