Skip to content

Commit 0868371

Browse files
aidanpscottEthanFennellMrCars0nAslanRuleschrisvire
authored
Display Lexicon Views
* Create route for lexicon page Create component for reversal view Create reusable alphabet strip component Implement initial lexicon reversal interface Add lexicon page data loading Add DAB program type route handling Add missing imports to lexicon and main routing pages lint format for page.js file for lexicon Create first reversal page draft Converted page to components Dynamically load alphabet from dictionary config Dynamically load alphabet from dictionary config Lift language state to parent component Add reversal lanuage name and letter change detection Lazy load English words and base for XML implementation Create Language Tabs Component and Parse lang Issue #685 Moved the code for the language tabs from LexiconReversalView and created a separate component. Also, parsed lang from writing systems and updated index.d.ts with displayLang Change English wording and fix selectedLetter Name vernacular language correctly and organize code Create new index.ts file for lexicon page Name vernacular language correctly and organize code Fix language tab naming convention Removed duplicate code for parsing lang Issue #685 Removed Unnecessary Code and Lexicon Folder Issue #685 Removed hardcoding Switch order of language tabs * Removed page.ts * Changed use of "slot" to "render" for navbar * List of words * Fix mobile layout * Fix lexicon auto load * Fix word id mismatch * Add margins to alphabet strip * Add first draft of vernacular letter id handling * Restore dictionary functionality while preserving styling * Fixed vernacular letter id handling * Create XML element and reorganize * Fix loading of second letter on tab change * Fix alphabet bar navigation bug * Store selectedLanguage after XMLView * Add word subtexts, indexes, and alphabet strip size change * Add word navigation component * Add XML new lines * Add multi-index XML call and new line formatting * Bug fixes * Moved vernacular query to page.js, simplified page.svelte * Revert to threshold * Fixed lazy loading glitches on firefox * Fixed sqlite fetch issue * add single entry style type * Add hyperlinks to headwords in XML * Remove unused variable and comments * show single entry styles * Added stores. Refactored lexicon code. Included reversal/vernacular mapped words in store to improve efficiency * Merge and formatting * Applied suggestions * Small changes * Small changes * miscellaneous small refactors * Format * Fix test * Fix accessibility issues * Add fallback for summary without matches * Fix comma issue. Fix duplicate listeners * Add proper type definition for vernacularWordsStore * Improve store definitions * Vernacular word hyperlink fix * Fix lint issues * Fix language tab to match native app * Remove padding * Applied feedback for language tabs * fix lint * Fix spacing * Adjust word list spacing * Fix navbar hiding on scroll and div organization * Fix more div organizaiton * Spacing fix * Implement lexicon error handling * Implement fetch logic for reversal view * Fix a potential security vulnerability * fix lint * Remove hardcoded colors * Split LexiconReversalListView into separate components. * Remove hardcoded color * Remove alphabetStrip artifact * Add proper reversal file fetch * Remove phantom endpoint call * Add grid view * Remove HEAD request in file loading * Remove unnecessary comments * Add grid view * Add grid view * Remove redundancy * convertReversalIndex create index.json * Use ReversalIndex to load reversal files * Fix wide screen gap * Fix display on wide screen * Rename AlphabetStrip * Rename LexiconReversalView * Rename LexiconXMLView --------- Co-authored-by: EthanFennell <edfennell@liberty.edu> Co-authored-by: Carson Kramer <carsondkramer@gmail.com> Co-authored-by: AslanRules <39661498+AslanRules@users.noreply.github.com> Co-authored-by: Chris Hubbard <chris@thehubbards.org>
1 parent f289cd9 commit 0868371

17 files changed

Lines changed: 978 additions & 85 deletions

config/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,4 +285,11 @@ export type DictionaryConfig = AppConfig & {
285285
displayed: boolean;
286286
};
287287
};
288+
singleEntryStyles?: {
289+
name: string;
290+
category?: string;
291+
properties: {
292+
[key: string]: string;
293+
};
294+
}[];
288295
};

convert/convertConfig.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,15 @@ function convertConfig(dataDir: string, verbose: number) {
248248
const mainStyles = document.querySelector('styles')!;
249249
data.styles = parseStyles(mainStyles, verbose);
250250

251+
if (isDictionaryConfig(data)) {
252+
const singleEntryStyles = document.querySelector('styles[type=single-entry]');
253+
if (singleEntryStyles) {
254+
data.singleEntryStyles = parseStyles(singleEntryStyles, verbose);
255+
} else if (verbose) {
256+
console.log('No single-entry styles found in the XML document');
257+
}
258+
}
259+
251260
if (isScriptureConfig(data)) {
252261
data.traits = parseTraits(document, dataDir, verbose);
253262
data.bookCollections = parseBookCollections(document, verbose);

convert/convertReverseIndex.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export function convertReverseIndex(
4949
.filter(([gloss]) => gloss?.length > 0);
5050

5151
const entriesByLetter: { [letter: string]: [string, string][] } = {};
52+
const fileIndexByLetter: { [letter: string]: string[] } = {}; // New index for files by letter
5253

5354
let latestLetter = makeEntryLetter(alphabet[0]);
5455
indexEntries.forEach((entry) => {
@@ -57,14 +58,17 @@ export function convertReverseIndex(
5758

5859
const firstLetter = getBaseLetter(gloss, alphabet);
5960
const entryLetter = firstLetter ?? latestLetter;
61+
const fileEntryLetter = entryLetter.toLowerCase();
6062
if (!entriesByLetter[entryLetter]) {
6163
entriesByLetter[entryLetter] = [];
64+
fileIndexByLetter[fileEntryLetter] = []; // Initialize file index for the letter
6265
}
6366
entriesByLetter[entryLetter].push([entry[0], entry[1]]);
6467
latestLetter = entryLetter;
6568
});
6669

6770
Object.entries(entriesByLetter).forEach(([letter, entries]) => {
71+
const fileLetter = letter.toLowerCase();
6872
entries.sort(([a], [b]) => a.localeCompare(b, language));
6973

7074
let currentChunk: { [key: string]: ReversalEntry[] } = {};
@@ -99,14 +103,16 @@ export function convertReverseIndex(
99103
currentCount++;
100104

101105
if (currentCount >= ENTRIES_PER_CHUNK || i === entries.length - 1) {
102-
const chunkFileName = `${letter.toLowerCase()}-${String(chunkIndex + 1).padStart(3, '0')}.json`;
106+
const chunkFileName = `${fileLetter}-${String(chunkIndex + 1).padStart(3, '0')}.json`;
103107
const chunkPath = path.join(outputDir, chunkFileName);
104108

105109
files.push({
106110
path: chunkPath,
107111
content: JSON.stringify(currentChunk, null, 2)
108112
});
109113

114+
fileIndexByLetter[fileLetter].push(chunkFileName); // Add file to index
115+
110116
currentChunk = {};
111117
currentCount = 0;
112118
chunkIndex++;
@@ -115,6 +121,11 @@ export function convertReverseIndex(
115121
}
116122
});
117123

124+
files.push({
125+
path: path.join(outputDir, 'index.json'),
126+
content: JSON.stringify(fileIndexByLetter, null, 2) // Write the file index to index.json
127+
});
128+
118129
return files;
119130
}
120131

convert/tests/sab/convertConfigSAB.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ if (programType === 'DAB') {
196196

197197
test('convertConfig: parse features', () => {
198198
const result = parseFeatures(document, 1);
199-
expect(Object.keys(result)).toHaveLength(137);
199+
expect(Object.keys(result)).toHaveLength(140);
200200
});
201201

202202
test('convertConfig: parse background images', () => {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script lang="ts">
2+
export let alphabet: string[];
3+
export let activeLetter: string;
4+
export let onLetterSelect: (letter: string) => void;
5+
</script>
6+
7+
<div
8+
class="flex m-2 gap-1 md:gap-2 mb-4 justify-start overflow-x-auto whitespace-nowrap pb-2 snap-x"
9+
>
10+
{#each alphabet as letter}
11+
<button
12+
class="px-3 py-2 text-sm font-bold border rounded-md bg-gray-100 hover:bg-gray-200 cursor-pointer snap-start
13+
sm:px-4 sm:py-3 sm:text-base
14+
md:px-5 md:py-4 md:text-base
15+
lg:px-6 lg:py-4 lg:text-lg"
16+
style={activeLetter === letter
17+
? 'background-color: var(--TitleBackgroundColor); border-color: black;'
18+
: ''}
19+
on:click={() => onLetterSelect(letter)}
20+
>
21+
{letter}
22+
</button>
23+
{/each}
24+
</div>
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
<script>
2+
import { base } from '$app/paths';
3+
import config from '$lib/data/config';
4+
import { convertStyle } from '$lib/data/stores';
5+
import initSqlJs from 'sql.js';
6+
import { afterUpdate, onMount } from 'svelte';
7+
8+
export let selectedWord;
9+
export let vernacularWordsList;
10+
export let vernacularLanguage;
11+
export let onSelectWord;
12+
export let onSwitchLanguage;
13+
14+
let xmlData = '';
15+
16+
let singleEntryStyles = config.singleEntryStyles;
17+
18+
async function queryXmlByWordId(wordId) {
19+
try {
20+
const SQL = await initSqlJs({
21+
locateFile: (file) => `${base}/wasm/sql-wasm.wasm`
22+
});
23+
24+
const response = await fetch(`${base}/data.sqlite`);
25+
if (!response.ok) {
26+
throw new Error(
27+
`Failed to fetch database: ${response.status} ${response.statusText}`
28+
);
29+
}
30+
const buffer = await response.arrayBuffer();
31+
const db = new SQL.Database(new Uint8Array(buffer));
32+
if (!db) {
33+
console.error('Database not initialized');
34+
return null;
35+
}
36+
37+
const stmt = db.prepare('SELECT xml FROM entries WHERE id = ?');
38+
stmt.bind([wordId]);
39+
40+
let result = null;
41+
if (stmt.step()) {
42+
result = stmt.getAsObject().xml;
43+
}
44+
stmt.free();
45+
db.close();
46+
47+
return result;
48+
} catch (error) {
49+
console.error(`Error querying XML for word ID ${wordId}:`, error);
50+
return null;
51+
}
52+
}
53+
54+
function formatXmlByClass(xmlString) {
55+
if (!xmlString) return '';
56+
57+
const parser = new DOMParser();
58+
const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
59+
60+
// Check if parsing failed
61+
const parseError = xmlDoc.querySelector('parsererror');
62+
if (parseError) {
63+
console.error('XML parsing error:', parseError.textContent);
64+
return `<span class="text-error">Error parsing XML: Invalid format</span>`;
65+
}
66+
67+
function processNode(node, parentHasSenseNumber = false) {
68+
let output = '';
69+
70+
if (node.nodeType === Node.TEXT_NODE) {
71+
return node.nodeValue.trim() ? node.nodeValue + ' ' : '';
72+
}
73+
74+
if (node.nodeType === Node.ELEMENT_NODE) {
75+
let className = node.getAttribute('class') || '';
76+
let isSenseNumber = className.includes('sensenumber');
77+
78+
let parentContainsSenseNumber =
79+
parentHasSenseNumber ||
80+
[...node.parentNode.children].some(
81+
(child) =>
82+
child.getAttribute &&
83+
(child.getAttribute('class') || '').includes('sensenumber')
84+
);
85+
86+
if (node.tagName === 'a' && node.hasAttribute('href')) {
87+
const href = node.getAttribute('href');
88+
const match = href.match(/E-(\d+)/); // Extract index number
89+
if (match) {
90+
const index = parseInt(match[1], 10); // Extracted number as integer
91+
const wordObject = vernacularWordsList.find((item) => item.id === index);
92+
const word = wordObject ? wordObject.name : 'Unknown'; // Fallback if not found
93+
const homonymIndex = wordObject ? wordObject.homonym_index : 1; // Default to 1 if not found
94+
95+
let linkText = node.textContent.trim();
96+
97+
// If the text inside the link matches the homonym index, use the homonym index as the text
98+
if (linkText === String(homonymIndex)) {
99+
linkText = homonymIndex.toString();
100+
}
101+
102+
output += `<span class="clickable cursor-pointer" data-word="${word}" data-index="${index}" data-homonym="${homonymIndex}">${linkText}</span>`;
103+
return output;
104+
}
105+
} else {
106+
output += '<' + node.tagName;
107+
for (let attr of node.attributes) {
108+
output += ` ${attr.name}="${attr.value}"`;
109+
}
110+
output += '>';
111+
112+
for (let child of node.childNodes) {
113+
output += processNode(child, parentContainsSenseNumber || isSenseNumber);
114+
}
115+
116+
output += `</${node.tagName}>`;
117+
}
118+
}
119+
120+
return output;
121+
}
122+
123+
return processNode(xmlDoc.documentElement);
124+
}
125+
126+
async function updateXmlData() {
127+
if (
128+
!selectedWord ||
129+
(!selectedWord.index && (!selectedWord.indexes || selectedWord.indexes.length === 0))
130+
) {
131+
xmlData = '';
132+
return;
133+
}
134+
135+
let wordIds = selectedWord.indexes ? selectedWord.indexes : [selectedWord.index];
136+
let xmlResults = await Promise.all(wordIds.map(queryXmlByWordId));
137+
138+
// Insert an `<hr>` tag or a visible separator between entries
139+
xmlData =
140+
xmlResults
141+
.filter((xml) => xml) // Ensure no null values are included
142+
.map(formatXmlByClass)
143+
.join('\n<hr>\n') + '\n<hr>\n'; // `<hr>` adds a visible line between entries
144+
}
145+
146+
function attachEventListeners() {
147+
const spans = document.querySelectorAll('.clickable');
148+
149+
spans.forEach((span) => {
150+
const oldSpan = span.cloneNode(true);
151+
span.parentNode.replaceChild(oldSpan, span);
152+
});
153+
154+
const freshSpans = document.querySelectorAll('.clickable');
155+
freshSpans.forEach((span) => {
156+
span.addEventListener('click', () => {
157+
onSwitchLanguage(vernacularLanguage);
158+
const word = span.getAttribute('data-word');
159+
const index = parseInt(span.getAttribute('data-index'), 10);
160+
const homonym_index = parseInt(span.getAttribute('data-homonym'), 10);
161+
162+
onSelectWord({
163+
word,
164+
index,
165+
homonym_index
166+
});
167+
});
168+
});
169+
}
170+
171+
function applyStyles() {
172+
for (let stl of singleEntryStyles) {
173+
for (let elm of document.querySelectorAll(stl.name)) {
174+
elm.style = convertStyle(stl.properties);
175+
}
176+
}
177+
}
178+
179+
onMount(updateXmlData);
180+
181+
afterUpdate(() => {
182+
updateXmlData();
183+
applyStyles();
184+
attachEventListeners();
185+
});
186+
</script>
187+
188+
<pre class="p-4 whitespace-pre-wrap break-words min-w-[100vw]">{@html xmlData}</pre>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script>
2+
import { expoInOut } from 'svelte/easing';
3+
import { fly } from 'svelte/transition';
4+
5+
export let reversalLanguage;
6+
export let selectedLanguage;
7+
export let onSwitchLanguage;
8+
export let vernacularLanguage;
9+
</script>
10+
11+
<div class="flex w-full" style="background-color: var(--TabBackgroundColor);">
12+
<div
13+
role="button"
14+
tabindex="0"
15+
aria-pressed={selectedLanguage === vernacularLanguage}
16+
on:click={() => onSwitchLanguage(vernacularLanguage)}
17+
on:keydown={(e) => e.key === 'Enter' && onSwitchLanguage(vernacularLanguage)}
18+
class="py-2.5 px-3.5 text-sm uppercase text-center relative dy-tabs dy-tabs-bordered mb-1"
19+
>
20+
{vernacularLanguage}
21+
{#if selectedLanguage === vernacularLanguage}
22+
<div
23+
transition:fly={{ axis: 'x', easing: expoInOut, x: 70 }}
24+
class="absolute -bottom-1 left-0 w-full h-1 bg-black"
25+
></div>
26+
{/if}
27+
</div>
28+
<div
29+
role="button"
30+
tabindex="0"
31+
aria-pressed={selectedLanguage === reversalLanguage}
32+
on:click={() => onSwitchLanguage(reversalLanguage)}
33+
on:keydown={(e) => e.key === 'Enter' && onSwitchLanguage(reversalLanguage)}
34+
class="py-2.5 px-3.5 text-sm uppercase text-center relative dy-tabs dy-tabs-bordered mb-1"
35+
>
36+
{reversalLanguage}
37+
{#if selectedLanguage === reversalLanguage}
38+
<div
39+
transition:fly={{ axis: 'x', easing: expoInOut, x: -70 }}
40+
class="absolute -bottom-1 left-0 w-full h-1 bg-black"
41+
></div>
42+
{/if}
43+
</div>
44+
<div class="flex-1"></div>
45+
</div>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script>
2+
import LexiconAlphabetStrip from './LexiconAlphabetStrip.svelte';
3+
import LexiconLanguageTabs from './LexiconLanguageTabs.svelte';
4+
5+
export let alphabet = [];
6+
export let selectedLanguage;
7+
export let vernacularLanguage;
8+
export let reversalLanguage;
9+
export let selectedLetter;
10+
export let onSwitchLanguage;
11+
export let onLetterChange;
12+
13+
let currentLetter = alphabet[0];
14+
15+
async function handleLetterSelect(letter) {
16+
currentLetter = letter;
17+
onLetterChange(letter);
18+
}
19+
20+
$: if (alphabet && alphabet.length > 0) {
21+
currentLetter = alphabet[0];
22+
}
23+
$: if (selectedLetter !== currentLetter) {
24+
currentLetter = selectedLetter;
25+
}
26+
</script>
27+
28+
<LexiconLanguageTabs
29+
{reversalLanguage}
30+
{selectedLanguage}
31+
{onSwitchLanguage}
32+
{vernacularLanguage}
33+
/>
34+
35+
<LexiconAlphabetStrip {alphabet} activeLetter={currentLetter} onLetterSelect={handleLetterSelect} />

0 commit comments

Comments
 (0)