-
-
Notifications
You must be signed in to change notification settings - Fork 34
Feature/display reversals/685 #802
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f943d5d
3dffb58
a94997e
702d50b
e4fade1
1df629e
395f895
775a0c0
d033725
7a60099
acc6751
ba1bf5d
c5d9837
c4a298e
65230fe
ff8ade0
8e8e9db
00f5160
06fa8a2
b88233d
0ad9475
17909f8
586f27d
177eace
ae26814
4ff134a
c0664e7
3b36c1b
9461d04
d9ad301
c95120b
c616955
4d70312
f1392b7
0f55156
071ec65
b88c6c3
ef9aaf7
ba77bdc
4531462
7a5cb54
1585440
fc0081c
9dc61e4
facc036
0300807
4bed4c7
e2fcdb6
c9b048c
d0a9562
2dccab3
e848ba0
a791eeb
d2f7d05
1d141aa
e82a119
0fa7c90
40e9333
bdf07c1
52859d0
c2f7e40
da80dab
23fa6e5
6ff83a3
843b73a
92eedb1
248cb73
cc26d25
a0c31de
4231b70
3ae4202
ab03d14
14c5e45
be9693a
144d55e
a41d30a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| <script lang="ts"> | ||
| export let alphabet: string[]; | ||
| export let activeLetter: string; | ||
| export let onLetterSelect: (letter: string) => void; | ||
| </script> | ||
|
|
||
| <div | ||
| class="flex m-2 gap-1 md:gap-2 mb-4 justify-start overflow-x-auto whitespace-nowrap pb-2 snap-x" | ||
| > | ||
| {#each alphabet as letter} | ||
| <button | ||
| class="px-3 py-2 text-sm font-bold border rounded-md bg-gray-100 hover:bg-gray-200 cursor-pointer snap-start | ||
| sm:px-4 sm:py-3 sm:text-base | ||
| md:px-5 md:py-4 md:text-base | ||
| lg:px-6 lg:py-4 lg:text-lg" | ||
| style={activeLetter === letter | ||
| ? 'background-color: var(--TitleBackgroundColor); border-color: black;' | ||
| : ''} | ||
| on:click={() => onLetterSelect(letter)} | ||
| > | ||
| {letter} | ||
| </button> | ||
| {/each} | ||
| </div> |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,188 @@ | ||||||||||||||||||||||||||||||||||
| <script> | ||||||||||||||||||||||||||||||||||
| import { base } from '$app/paths'; | ||||||||||||||||||||||||||||||||||
| import config from '$lib/data/config'; | ||||||||||||||||||||||||||||||||||
| import { convertStyle } from '$lib/data/stores'; | ||||||||||||||||||||||||||||||||||
| import initSqlJs from 'sql.js'; | ||||||||||||||||||||||||||||||||||
| import { afterUpdate, onMount } from 'svelte'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export let selectedWord; | ||||||||||||||||||||||||||||||||||
| export let vernacularWordsList; | ||||||||||||||||||||||||||||||||||
| export let vernacularLanguage; | ||||||||||||||||||||||||||||||||||
| export let onSelectWord; | ||||||||||||||||||||||||||||||||||
| export let onSwitchLanguage; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| let xmlData = ''; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| let singleEntryStyles = config.singleEntryStyles; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| async function queryXmlByWordId(wordId) { | ||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| const SQL = await initSqlJs({ | ||||||||||||||||||||||||||||||||||
| locateFile: (file) => `${base}/wasm/sql-wasm.wasm` | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const response = await fetch(`${base}/data.sqlite`); | ||||||||||||||||||||||||||||||||||
| if (!response.ok) { | ||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||
| `Failed to fetch database: ${response.status} ${response.statusText}` | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| const buffer = await response.arrayBuffer(); | ||||||||||||||||||||||||||||||||||
| const db = new SQL.Database(new Uint8Array(buffer)); | ||||||||||||||||||||||||||||||||||
| if (!db) { | ||||||||||||||||||||||||||||||||||
| console.error('Database not initialized'); | ||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const stmt = db.prepare('SELECT xml FROM entries WHERE id = ?'); | ||||||||||||||||||||||||||||||||||
| stmt.bind([wordId]); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| let result = null; | ||||||||||||||||||||||||||||||||||
| if (stmt.step()) { | ||||||||||||||||||||||||||||||||||
| result = stmt.getAsObject().xml; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| stmt.free(); | ||||||||||||||||||||||||||||||||||
| db.close(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return result; | ||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||
| console.error(`Error querying XML for word ID ${wordId}:`, error); | ||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| function formatXmlByClass(xmlString) { | ||||||||||||||||||||||||||||||||||
| if (!xmlString) return ''; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const parser = new DOMParser(); | ||||||||||||||||||||||||||||||||||
| const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Check if parsing failed | ||||||||||||||||||||||||||||||||||
| const parseError = xmlDoc.querySelector('parsererror'); | ||||||||||||||||||||||||||||||||||
| if (parseError) { | ||||||||||||||||||||||||||||||||||
| console.error('XML parsing error:', parseError.textContent); | ||||||||||||||||||||||||||||||||||
| return `<span class="text-error">Error parsing XML: Invalid format</span>`; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| function processNode(node, parentHasSenseNumber = false) { | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+54
to
+67
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential XSS – XML → HTML is injected without sanitisation
Please run the output through an HTML sanitizer (e.g. DOMPurify) or explicitly strip/escape unsafe tags and attributes before exposing them to the DOM. |
||||||||||||||||||||||||||||||||||
| let output = ''; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (node.nodeType === Node.TEXT_NODE) { | ||||||||||||||||||||||||||||||||||
| return node.nodeValue.trim() ? node.nodeValue + ' ' : ''; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (node.nodeType === Node.ELEMENT_NODE) { | ||||||||||||||||||||||||||||||||||
| let className = node.getAttribute('class') || ''; | ||||||||||||||||||||||||||||||||||
| let isSenseNumber = className.includes('sensenumber'); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| let parentContainsSenseNumber = | ||||||||||||||||||||||||||||||||||
| parentHasSenseNumber || | ||||||||||||||||||||||||||||||||||
| [...node.parentNode.children].some( | ||||||||||||||||||||||||||||||||||
| (child) => | ||||||||||||||||||||||||||||||||||
| child.getAttribute && | ||||||||||||||||||||||||||||||||||
| (child.getAttribute('class') || '').includes('sensenumber') | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (node.tagName === 'a' && node.hasAttribute('href')) { | ||||||||||||||||||||||||||||||||||
| const href = node.getAttribute('href'); | ||||||||||||||||||||||||||||||||||
| const match = href.match(/E-(\d+)/); // Extract index number | ||||||||||||||||||||||||||||||||||
| if (match) { | ||||||||||||||||||||||||||||||||||
| const index = parseInt(match[1], 10); // Extracted number as integer | ||||||||||||||||||||||||||||||||||
| const wordObject = vernacularWordsList.find((item) => item.id === index); | ||||||||||||||||||||||||||||||||||
| const word = wordObject ? wordObject.name : 'Unknown'; // Fallback if not found | ||||||||||||||||||||||||||||||||||
| const homonymIndex = wordObject ? wordObject.homonym_index : 1; // Default to 1 if not found | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| let linkText = node.textContent.trim(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // If the text inside the link matches the homonym index, use the homonym index as the text | ||||||||||||||||||||||||||||||||||
| if (linkText === String(homonymIndex)) { | ||||||||||||||||||||||||||||||||||
| linkText = homonymIndex.toString(); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| output += `<span class="clickable cursor-pointer" data-word="${word}" data-index="${index}" data-homonym="${homonymIndex}">${linkText}</span>`; | ||||||||||||||||||||||||||||||||||
| return output; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| output += '<' + node.tagName; | ||||||||||||||||||||||||||||||||||
| for (let attr of node.attributes) { | ||||||||||||||||||||||||||||||||||
| output += ` ${attr.name}="${attr.value}"`; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| output += '>'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| for (let child of node.childNodes) { | ||||||||||||||||||||||||||||||||||
| output += processNode(child, parentContainsSenseNumber || isSenseNumber); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| output += `</${node.tagName}>`; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return output; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return processNode(xmlDoc.documentElement); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| async function updateXmlData() { | ||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||
| !selectedWord || | ||||||||||||||||||||||||||||||||||
| (!selectedWord.index && (!selectedWord.indexes || selectedWord.indexes.length === 0)) | ||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||
| xmlData = ''; | ||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
Comment on lines
+126
to
+134
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
-if (
- !selectedWord ||
- (!selectedWord.index && (!selectedWord.indexes || selectedWord.indexes.length === 0))
-)
+if (
+ !selectedWord ||
+ (selectedWord.index == null && (!selectedWord.indexes || selectedWord.indexes.length === 0))
+)📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||
| let wordIds = selectedWord.indexes ? selectedWord.indexes : [selectedWord.index]; | ||||||||||||||||||||||||||||||||||
| let xmlResults = await Promise.all(wordIds.map(queryXmlByWordId)); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Insert an `<hr>` tag or a visible separator between entries | ||||||||||||||||||||||||||||||||||
| xmlData = | ||||||||||||||||||||||||||||||||||
| xmlResults | ||||||||||||||||||||||||||||||||||
| .filter((xml) => xml) // Ensure no null values are included | ||||||||||||||||||||||||||||||||||
| .map(formatXmlByClass) | ||||||||||||||||||||||||||||||||||
| .join('\n<hr>\n') + '\n<hr>\n'; // `<hr>` adds a visible line between entries | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| function attachEventListeners() { | ||||||||||||||||||||||||||||||||||
| const spans = document.querySelectorAll('.clickable'); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| spans.forEach((span) => { | ||||||||||||||||||||||||||||||||||
| const oldSpan = span.cloneNode(true); | ||||||||||||||||||||||||||||||||||
| span.parentNode.replaceChild(oldSpan, span); | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const freshSpans = document.querySelectorAll('.clickable'); | ||||||||||||||||||||||||||||||||||
| freshSpans.forEach((span) => { | ||||||||||||||||||||||||||||||||||
| span.addEventListener('click', () => { | ||||||||||||||||||||||||||||||||||
| onSwitchLanguage(vernacularLanguage); | ||||||||||||||||||||||||||||||||||
| const word = span.getAttribute('data-word'); | ||||||||||||||||||||||||||||||||||
| const index = parseInt(span.getAttribute('data-index'), 10); | ||||||||||||||||||||||||||||||||||
| const homonym_index = parseInt(span.getAttribute('data-homonym'), 10); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| onSelectWord({ | ||||||||||||||||||||||||||||||||||
| word, | ||||||||||||||||||||||||||||||||||
| index, | ||||||||||||||||||||||||||||||||||
| homonym_index | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| function applyStyles() { | ||||||||||||||||||||||||||||||||||
| for (let stl of singleEntryStyles) { | ||||||||||||||||||||||||||||||||||
| for (let elm of document.querySelectorAll(stl.name)) { | ||||||||||||||||||||||||||||||||||
| elm.style = convertStyle(stl.properties); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+172
to
+176
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inline‑style assignment is wrong – use
-elm.style = convertStyle(stl.properties);
+elm.style.cssText = convertStyle(stl.properties); // or elm.setAttribute('style', …)Without this fix none of the configured styles are actually applied. 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| onMount(updateXmlData); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| afterUpdate(() => { | ||||||||||||||||||||||||||||||||||
| updateXmlData(); | ||||||||||||||||||||||||||||||||||
| applyStyles(); | ||||||||||||||||||||||||||||||||||
| attachEventListeners(); | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+181
to
+185
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion
Prefer a reactive statement that depends only on $: if (selectedWord) {
await updateXmlData();
await tick(); // wait for DOM update
applyStyles();
attachEventListeners();
}This fires exactly when required and avoids thrashing. |
||||||||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| <pre class="p-4 whitespace-pre-wrap break-words min-w-[100vw]">{@html xmlData}</pre> | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| <script> | ||
| import { expoInOut } from 'svelte/easing'; | ||
| import { fly } from 'svelte/transition'; | ||
|
|
||
| export let reversalLanguage; | ||
| export let selectedLanguage; | ||
| export let onSwitchLanguage; | ||
| export let vernacularLanguage; | ||
| </script> | ||
|
|
||
| <div class="flex w-full" style="background-color: var(--TabBackgroundColor);"> | ||
| <div | ||
| role="button" | ||
| tabindex="0" | ||
| aria-pressed={selectedLanguage === vernacularLanguage} | ||
| on:click={() => onSwitchLanguage(vernacularLanguage)} | ||
| on:keydown={(e) => e.key === 'Enter' && onSwitchLanguage(vernacularLanguage)} | ||
| class="py-2.5 px-3.5 text-sm uppercase text-center relative dy-tabs dy-tabs-bordered mb-1" | ||
| > | ||
| {vernacularLanguage} | ||
| {#if selectedLanguage === vernacularLanguage} | ||
| <div | ||
| transition:fly={{ axis: 'x', easing: expoInOut, x: 70 }} | ||
| class="absolute -bottom-1 left-0 w-full h-1 bg-black" | ||
| ></div> | ||
| {/if} | ||
| </div> | ||
| <div | ||
| role="button" | ||
| tabindex="0" | ||
| aria-pressed={selectedLanguage === reversalLanguage} | ||
| on:click={() => onSwitchLanguage(reversalLanguage)} | ||
| on:keydown={(e) => e.key === 'Enter' && onSwitchLanguage(reversalLanguage)} | ||
| class="py-2.5 px-3.5 text-sm uppercase text-center relative dy-tabs dy-tabs-bordered mb-1" | ||
| > | ||
| {reversalLanguage} | ||
| {#if selectedLanguage === reversalLanguage} | ||
| <div | ||
| transition:fly={{ axis: 'x', easing: expoInOut, x: -70 }} | ||
| class="absolute -bottom-1 left-0 w-full h-1 bg-black" | ||
| ></div> | ||
| {/if} | ||
| </div> | ||
| <div class="flex-1"></div> | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| <script> | ||
| import LexiconAlphabetStrip from './LexiconAlphabetStrip.svelte'; | ||
| import LexiconLanguageTabs from './LexiconLanguageTabs.svelte'; | ||
|
|
||
| export let alphabet = []; | ||
| export let selectedLanguage; | ||
| export let vernacularLanguage; | ||
| export let reversalLanguage; | ||
| export let selectedLetter; | ||
| export let onSwitchLanguage; | ||
| export let onLetterChange; | ||
|
|
||
| let currentLetter = alphabet[0]; | ||
|
|
||
| async function handleLetterSelect(letter) { | ||
| currentLetter = letter; | ||
| onLetterChange(letter); | ||
| } | ||
|
|
||
| $: if (alphabet && alphabet.length > 0) { | ||
| currentLetter = alphabet[0]; | ||
| } | ||
| $: if (selectedLetter !== currentLetter) { | ||
| currentLetter = selectedLetter; | ||
| } | ||
| </script> | ||
|
|
||
| <LexiconLanguageTabs | ||
| {reversalLanguage} | ||
| {selectedLanguage} | ||
| {onSwitchLanguage} | ||
| {vernacularLanguage} | ||
| /> | ||
|
|
||
| <LexiconAlphabetStrip {alphabet} activeLetter={currentLetter} onLetterSelect={handleLetterSelect} /> | ||
|
chrisvire marked this conversation as resolved.
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid re‑initialising
sql.jsand re‑downloading the DB on every word lookupinitSqlJsand thefetchfordata.sqliteare executed insidequeryXmlByWordId, which means a full WASM initialisation and DB download happens for each word ID (and for every component update).This is extremely expensive (multi‑MB download + WASM compile) and becomes noticeable as soon as a user clicks through several entries.
Create a module‑level (or store‑level) singleton that initialises
SQLand keeps aDatabaseinstance open for the life‑time of the session, then reuse it:This reduces network traffic, CPU time, and memory dramatically.