diff --git a/.gitignore b/.gitignore index 9c77790c9..9649d4ca5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,10 +17,8 @@ static/illustrations static/icons static/manifest*.json static/assets +static/collections static/contents src/lib/data/catalog.js -src/lib/data/firebase-config.js -src/lib/data/config.js -src/lib/data/contents.js src/gen-assets vite.config.js.timestamp* diff --git a/.prettierignore b/.prettierignore index bdc6382e9..c2d989f59 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,7 +7,7 @@ node_modules .env.* !.env.example /data -src/lib/data/config.js +src/gen-assets src/lib/data/catalog.js src/lib/data/firebase-config.js static diff --git a/config/index.d.ts b/config/index.d.ts index 5529359a8..5f87de873 100644 --- a/config/index.d.ts +++ b/config/index.d.ts @@ -62,8 +62,8 @@ export type BookConfig = { file: string; hashedFileName?: string; // currently just for HTML books audio: BookCollectionAudioConfig[]; - features: any; - quizFeatures?: Record; + features: FeatureConfig; + quizFeatures?: FeatureConfig; footer?: HTML; style?: StyleConfig; styles?: { @@ -157,13 +157,17 @@ export type MenuItemConfig = { file: string; }[]; }; + +export type FeatureValue = string | boolean | number; +export type FeatureConfig = Record; + export type AppConfig = { name?: string; package?: string; version?: string; - programVersion?: string; - programType?: string; - mainFeatures?: any; + programVersion: string; + programType: string; + mainFeatures: FeatureConfig; audio?: AudioConfig; fonts?: { name?: string; @@ -379,3 +383,50 @@ export type Quiz = { }[]; passScore?: number; //\pm }; + +export type LangContainer = { [lang: string]: string }; + +export type LinkMeta = { + // intended to pass between functions so that there is one object passed + linkType?: string; + linkTarget?: string; + linkLocation?: string; +}; + +export type ContentItem = { + id: number; + heading?: boolean; + features?: any; + title: LangContainer; + subtitle?: LangContainer; + audioFilename?: LangContainer; + imageFilename?: string; + itemType?: string; + contentItemContainer: boolean; + linkType?: string; + linkTarget?: string; + linkLocation?: string; + layoutMode?: string; + layoutCollection?: string[]; + children?: ContentItem[]; +}; + +export type ContentScreen = { + id: number; + title?: { + [lang: string]: string; + }; + items?: { + id: number; + }[]; +}; + +export type ContentsData = { + title?: { + [lang: string]: string; + }; + features?: any; + items?: ContentItem[]; + nestedItems?: boolean; + screens?: ContentScreen[]; +}; diff --git a/convert/convertConfig.ts b/convert/convertConfig.ts index 5e8fd05b1..295bd527b 100644 --- a/convert/convertConfig.ts +++ b/convert/convertConfig.ts @@ -1,4 +1,4 @@ -import { existsSync, readdirSync, readFileSync, type PathLike } from 'fs'; +import { existsSync, mkdirSync, readdirSync, readFileSync, type PathLike } from 'fs'; import path, { basename, extname, join } from 'path'; import type { AppConfig, @@ -219,6 +219,10 @@ function isDictionaryConfig(data: ScriptureConfig | DictionaryConfig): data is D } function convertConfig(dataDir: string, verbose: number) { + const genAssets = path.join('src/gen-assets'); + if (!existsSync(genAssets)) { + mkdirSync(genAssets, { recursive: true }); + } const dom = new jsdom.JSDOM(readFileSync(path.join(dataDir, 'appdef.xml')).toString(), { contentType: 'text/xml' }); @@ -1613,7 +1617,7 @@ export interface ConfigTaskOutput extends TaskOutput { /** * Converts appdef.xml into a config object which is passed to other conversion functions - * and is also written to src/config.js. + * and is also written to src/gen-assets/config.ts. */ export class ConvertConfig extends Task { public triggerFiles: string[] = ['appdef.xml']; @@ -1624,8 +1628,14 @@ export class ConvertConfig extends Task { data, files: [ { - path: 'src/lib/data/config.js', - content: `export default ${JSON.stringify(data, null, 2)};` + path: 'src/gen-assets/config.ts', + content: [ + `import type { AppConfig, DictionaryConfig, ScriptureConfig } from '$config';`, + `export const config = ${JSON.stringify(data, null, 2)} as Readonly;`, + `export const dictionaryConfig = config as Readonly;`, + `export const scriptureConfig = config as Readonly;`, + `export default config;\n` + ].join('\n') } ] }; diff --git a/convert/convertContents.ts b/convert/convertContents.ts index 441303255..e9e9749df 100644 --- a/convert/convertContents.ts +++ b/convert/convertContents.ts @@ -1,60 +1,11 @@ import { existsSync, readFileSync } from 'fs'; import path from 'path'; -import { ScriptureConfig } from '$config'; +import { ContentItem, ContentsData, LangContainer, LinkMeta, ScriptureConfig } from '$config'; import jsdom from 'jsdom'; import { ConfigTaskOutput, parseLangAttribute } from './convertConfig'; import { createHashedFile, createOutputDir, deleteOutputDir, joinUrlPath } from './fileUtils'; import { Task, TaskOutput } from './Task'; -export type LangContainer = { [lang: string]: string }; - -export type LinkMeta = { - // intended to pass between functions so that there is one object passed - linkType?: string; - linkTarget?: string; - linkLocation?: string; -}; - -type ContentItem = { - id: number; - heading?: boolean; - features?: any; - title: LangContainer; - subtitle?: LangContainer; - audioFilename?: LangContainer; - imageFilename?: string; - itemType?: string; - contentItemContainer: boolean; - linkType?: string; - linkTarget?: string; - linkLocation?: string; - layoutMode?: string; - layoutCollection?: string[]; - children?: ContentItem[]; -}; - -type ContentScreen = { - id: number; - title?: { - [lang: string]: string; - }; - items?: { - id: number; - }[]; -}; - -export type ContentsData = { - title?: { - [lang: string]: string; - }; - features?: any; - items?: ContentItem[]; - nestedItems?: boolean; - screens?: ContentScreen[]; -}; - -const data: ContentsData = {}; - export interface ContentsTaskOutput extends TaskOutput { taskName: 'ConvertContents'; } @@ -367,6 +318,8 @@ export function convertContents( deleteOutputDir(destDir); } + const data: ContentsData = {}; + const contentsFile = path.join(dataDir, 'contents.xml'); if (!existsSync(contentsFile)) { return data; @@ -602,8 +555,12 @@ export class ConvertContents extends Task { data, files: [ { - path: 'src/lib/data/contents.js', - content: `export default ${JSON.stringify(data, null, 2)}` + path: 'src/gen-assets/contents.ts', + content: [ + `import type { ContentsData } from '$config';`, + `export const contents = ${JSON.stringify(data, null, 2)} as Readonly;`, + `export default contents;\n` + ].join('\n') } ] }; diff --git a/convert/convertFirebase.ts b/convert/convertFirebase.ts index a83b0c474..952d67ffe 100644 --- a/convert/convertFirebase.ts +++ b/convert/convertFirebase.ts @@ -11,16 +11,17 @@ export interface FirebaseTaskOutput extends TaskOutput { export function convertFirebase(dataDir: string, verbose: number) { const srcFile = path.join(dataDir, 'firebase-config.js'); const srcExists = existsSync(srcFile); - const dstFile = path.join('src', 'lib', 'data', 'firebase-config.js'); + const dstFile = path.join('src', 'gen-assets', 'firebase-config.ts'); if (verbose) { console.log(`FirebaseConfig: path=${srcFile}, exists=${srcExists}`); } + const prefix = `import type { FirebaseOptions } from 'firebase/app';\nexport const firebaseConfig: Readonly | null`; if (srcExists) { let content = readFileSync(srcFile, 'utf-8'); - content = content.replace('const firebaseConfig', 'export const firebaseConfig'); + content = content.replace('const firebaseConfig', prefix); writeFileSync(dstFile, content, 'utf-8'); } else { - const firebaseConfig = 'export const firebaseConfig = null;'; + const firebaseConfig = `${prefix} = null`; writeFileSync(dstFile, firebaseConfig); } } diff --git a/convert/index.ts b/convert/index.ts index aef76daa6..57fdf0ea0 100644 --- a/convert/index.ts +++ b/convert/index.ts @@ -1,4 +1,4 @@ -import { writeFile } from 'fs'; +import { writeFile } from 'fs/promises'; import path from 'path'; import { watch } from 'chokidar'; import { ConvertAbout } from './convertAbout'; @@ -87,14 +87,7 @@ async function fullConvert(printDetails: boolean): Promise { // step may be async, in which case it should be awaited const out = await step.run(verbose, outputs, step.triggerFiles); outputs.set(step.constructor.name, out); - await Promise.all( - out.files.map( - (f) => - new Promise((r) => { - writeFile(f.path, f.content, r); - }) - ) - ); + await Promise.all(out.files.map((f) => writeFile(f.path, f.content))); } catch (e) { oldConsoleLog(lastStepOutput); oldConsoleLog(e); @@ -157,11 +150,7 @@ if (process.argv.includes('--watch')) { outputs.set(step.constructor.name, out); // Write all files to disk /*await*/ // We don't need to await the file writes; next steps can continue running while writes occur - Promise.all( - out.files.map((f) => { - new Promise((r) => writeFile(f.path, f.content, r)); - }) - ); + Promise.all(out.files.map((f) => writeFile(f.path, f.content))); } catch (e) { oldConsoleLog(lastStepOutput); oldConsoleLog(e); diff --git a/convert/tests/dab/convertConfigDAB.test.ts b/convert/tests/dab/convertConfigDAB.test.ts index 3023c5623..21162e822 100644 --- a/convert/tests/dab/convertConfigDAB.test.ts +++ b/convert/tests/dab/convertConfigDAB.test.ts @@ -1,6 +1,6 @@ import { readFileSync } from 'fs'; import path from 'path'; -import type { DictionaryConfig, DictionaryWritingSystemConfig } from '$config'; +import type { DictionaryWritingSystemConfig } from '$config'; import jsdom from 'jsdom'; import { expect, test } from 'vitest'; import { parseDictionaryWritingSystem, parseFeatures } from '../../convertConfig'; diff --git a/eslint.config.js b/eslint.config.js index d12c73508..1a806fec9 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -73,7 +73,6 @@ export default ts.config( '.env.*', '!env.example', 'data', - 'src/config.js', 'static', 'example_data', 'test_data', diff --git a/package.json b/package.json index fb8248c1b..5ae7cb0d0 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "convert": "ts-node convert/index.ts", "convert:watch": "ts-node convert/index.ts --watch", "sandbox": "ts-node data-sandbox/index.ts", - "clean": "rimraf .svelte-kit build src/gen-assets src/lib/data/config.js src/lib/data/catalog.js src/lib/data/firebase-config.js src/lib/data/contents.js static/illustrations static/icons static/contents static/manifest*.json && echo 🔔 Reminder: The project cannot be built until the conversion scripts are run again.", + "clean": "rimraf .svelte-kit build src/gen-assets src/lib/data/catalog.js static/illustrations static/icons static/collections static/contents static/manifest*.json && echo 🔔 Reminder: The project cannot be built until the conversion scripts are run again.", "clean:data": "rimraf --glob data/*", "clean:all": "npm run clean && npm run clean:data", "test": "vitest", diff --git a/src/app.d.ts b/src/app.d.ts index 9c33c8f75..4fd9e41a5 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,5 +1,6 @@ /// /// +/// // See https://kit.svelte.dev/docs/types#app // for information about these interfaces @@ -64,11 +65,11 @@ declare namespace App { interface CollectionEntry { id: string; - name: string; + name?: string; // boolean value for if a collection is allowed // to be shown in single pane view singlePane: boolean; - description: string; + description?: string; image?: string; } @@ -86,8 +87,8 @@ declare namespace App { key: string; entries?: string[]; values?: string[]; - value?: string | boolean; - defaultValue?: string | boolean; + value?: FeatureValue; + defaultValue?: FeatureValue; } } diff --git a/src/lib/components/AudioBar.svelte b/src/lib/components/AudioBar.svelte index 406a9a704..dd6a9bf56 100644 --- a/src/lib/components/AudioBar.svelte +++ b/src/lib/components/AudioBar.svelte @@ -6,6 +6,7 @@ TODO: - display audio not found message in UI when audio is not found -->
diff --git a/src/lib/components/LayoutOptions.svelte b/src/lib/components/LayoutOptions.svelte index 02d549e9b..94ef569df 100644 --- a/src/lib/components/LayoutOptions.svelte +++ b/src/lib/components/LayoutOptions.svelte @@ -3,7 +3,7 @@ Displays the three different layout option menus. --> diff --git a/src/lib/components/ScriptureViewSofria.svelte b/src/lib/components/ScriptureViewSofria.svelte index ee052ac75..e5b90594f 100644 --- a/src/lib/components/ScriptureViewSofria.svelte +++ b/src/lib/components/ScriptureViewSofria.svelte @@ -13,8 +13,8 @@ LOGGING: /* eslint-disable svelte/no-dom-manipulating */ import { base } from '$app/paths'; + import { scriptureConfig } from '$assets/config'; import { hasAudioPlayed, seekToVerse } from '$lib/data/audio'; - import config from '$lib/data/config'; import { addPlanProgressItem, deleteAllProgressItemsForPlan, @@ -591,7 +591,7 @@ LOGGING: div.style.float = direction.toLowerCase() === 'ltr' ? 'left' : 'right'; div.innerText = workspace.chapterNumText; workspace.paragraphDiv.appendChild(div); - if (!config.mainFeatures['hide-verse-number-1']) { + if (!scriptureConfig.mainFeatures['hide-verse-number-1']) { addVerseNumber(workspace, element, showVerseNumbers); } } else { @@ -642,7 +642,7 @@ LOGGING: const splitVerse = splitRef[3]; let refDocSet = currentDocSet; - const refBc = config.bookCollections.find((x) => x.id === splitSet); + const refBc = scriptureConfig.bookCollections?.find((x) => x.id === splitSet); if (refBc) { refDocSet = refBc.languageCode + '_' + refBc.id; } else { @@ -659,7 +659,7 @@ LOGGING: event.target.getAttribute('data-end-ref') === 'undefined' ? undefined : JSON.parse(event.target.getAttribute('data-end-ref')); - if (config.mainFeatures['scripture-refs-display'] === 'viewer') { + if (scriptureConfig.mainFeatures['scripture-refs-display'] === 'viewer') { navigate(start); } else { const footnoteHTML = await handleHeaderLinkPressed(start, end, themeColors); @@ -1207,7 +1207,7 @@ LOGGING: const figureImg = document.createElement('img'); figureImg.setAttribute('src', imageSource); figureImg.style.display = 'inline-block'; - if (config.mainFeatures['zoom-illustrations']) { + if (scriptureConfig.mainFeatures['zoom-illustrations']) { figureImg.addEventListener('click', () => showFullscreenPopup(imageSource)); } spanFigure.appendChild(figureImg); @@ -1251,10 +1251,10 @@ LOGGING: function addFooter(document: Document, container: HTMLElement, docSet: string) { const collection = docSet.split('_')[1]; - let footer = config.bookCollections.find((x) => x.id === collection)?.footer; - const bookFooter = config.bookCollections - .find((x) => x.id === collection) - .books.find((x) => x.id === currentBook).footer; + let footer = scriptureConfig.bookCollections?.find((x) => x.id === collection)?.footer; + const bookFooter = scriptureConfig.bookCollections + ?.find((x) => x.id === collection) + ?.books.find((x) => x.id === currentBook)?.footer; if (bookFooter) { footer = bookFooter; } @@ -2430,7 +2430,7 @@ LOGGING: switch (element.subType) { case 'usfm:zvideo': { const id = element.atts['id'][0]; - const video = config.videos.find((x) => x.id === id); + const video = scriptureConfig.videos?.find((x) => x.id === id); if (video) { workspace.videoDiv = createVideoBlock( document, @@ -2443,7 +2443,7 @@ LOGGING: workspace.videoDiv = createVideoBlockFromUrl( document, videoUrl, - config.mainFeatures + scriptureConfig.mainFeatures ); } break; @@ -2494,7 +2494,7 @@ LOGGING: workspace.milestoneTitle, workspace.audioClips.length, element.subType, - config.audio, + scriptureConfig.audio, onClick ); workspace.milestoneLink = ''; @@ -2578,7 +2578,7 @@ LOGGING: function videosForChapter(docSet: string, bookCode: string, chapter: string) { let collection = docSet.split('_')[1]; - let videos = config.videos?.filter( + let videos = scriptureConfig.videos?.filter( (x) => x.placement && x.placement.collection === collection && @@ -2590,7 +2590,7 @@ LOGGING: function illustrationsForChapter(docSet: string, bookCode: string, chapter: string) { let collection = docSet.split('_')[1]; - let illustrations = config.illustrations?.filter( + let illustrations = scriptureConfig.illustrations?.filter( (x) => x.placement && x.placement.collection === collection && @@ -2610,9 +2610,9 @@ LOGGING: const currentDocSet = $derived(references.docSet); const bookTabs = $derived( - config?.bookCollections - .find((x) => x.id === references.collection) - .books.find((x) => x.id === references.book)?.bookTabs + scriptureConfig.bookCollections + ?.find((x) => x.id === references.collection) + ?.books.find((x) => x.id === references.book)?.bookTabs ); const bookTabSelected = $derived(bookTabs && references.bookTab > 0); @@ -2626,17 +2626,18 @@ LOGGING: const currentIsBibleBook = $derived(isBibleBook(references)); const numeralSystem = $derived( - numerals.systemForBook(config, references.collection, currentBook) + numerals.systemForBook(scriptureConfig, references.collection, currentBook) ); const versePerLine = $derived(verseLayout === 'one-per-line'); /**list of books in current docSet*/ const books = $derived($refs.catalog.documents); const direction = $derived( - config.bookCollections.find((x) => x.id === references.collection).style.textDirection + scriptureConfig.bookCollections?.find((x) => x.id === references.collection)?.style + ?.textDirection ); const verseRangeSeparator = $derived( - config.bookCollections.find((x) => x.id === references.collection).features[ + scriptureConfig.bookCollections?.find((x) => x.id === references.collection)?.features[ 'ref-verse-range-separator' ] ); diff --git a/src/lib/components/SearchForm.svelte b/src/lib/components/SearchForm.svelte index fe14efa9f..eaceaa219 100644 --- a/src/lib/components/SearchForm.svelte +++ b/src/lib/components/SearchForm.svelte @@ -1,5 +1,5 @@