diff --git a/convert/convertBooks.ts b/convert/convertBooks.ts index cabbf31aa..2a4821f29 100644 --- a/convert/convertBooks.ts +++ b/convert/convertBooks.ts @@ -10,6 +10,7 @@ import { queries, postQueries, freeze } from '../sab-proskomma-tools'; import { convertMarkdownsToMilestones } from './convertMarkdown'; import { verifyGlossaryEntries } from './verifyGlossaryEntries'; import { hasAudioExtension, hasImageExtension } from './stringUtils'; +import { convertStorybookElements } from './storybook'; /** * Loops through bookCollections property of configData. @@ -22,10 +23,16 @@ function replaceVideoTags(text: string, _bcId: string, _bookId: string): string return text.replace(/\\video (.*)/g, '\\zvideo-s |id="$1"\\*\\zvideo-e\\*'); } -// This is the start of supporting story books, but it still fails if there is no chapter. -function replacePageTags(text: string, _bcId: string, _bookId: string): string { - return text.replace(/\\page (.*)/g, '\\zpage-s |id="$1"\\*\\zpage-e\\*'); +/** + * Replace the USFM book ID with the given book ID. + * + * While uncommon, it is possible to use the same USFM for multiple books. + * In this case, we must use the unique ID specified in config. + */ +function replaceId(text: string, _bcId: string, bookId: string): string { + return text.replace(/\\id \w+/, `\\id ${bookId}`); } + function loadGlossary(collection: any, dataDir: string): string[] { const glossary: string[] = []; for (const book of collection.books) { @@ -101,16 +108,23 @@ function isImageMissing(imageSource: string): boolean { const filterFunctions: ((text: string, bcId: string, bookId: string) => string)[] = [ removeStrongNumberReferences, replaceVideoTags, - replacePageTags, convertMarkdownsToMilestones, - removeMissingFigures + removeMissingFigures, + replaceId ]; -function applyFilters(text: string, bcId: string, bookId: string): string { +function applyFilters(text: string, bcId: string, bookId: string, bookType?: string): string { let filteredText = text; for (const filterFn of filterFunctions) { filteredText = filterFn(filteredText, bcId, bookId); } + if (bookType === 'story') { + filteredText = convertStorybookElements(filteredText); + } + // Debugging + // if (bcId == 'C01') { + // console.log(filteredText.slice(0, 1000)); + // } return filteredText; } @@ -204,7 +218,6 @@ export async function convertBooks( for (const book of collection.books) { let bookConverted = false; switch (book.type) { - case 'story': case 'songs': case 'audio-only': case 'bloom-player': @@ -490,7 +503,7 @@ function convertScriptureBook( function processBookContent(resolve: () => void, err: any, content: string) { //process.stdout.write(`processBookContent: bookId:${book.id}, error:${err}\n`); if (err) throw err; - content = applyFilters(content, context.bcId, book.id); + content = applyFilters(content, context.bcId, book.id, book.type); if (context.configData.traits['has-glossary']) { content = verifyGlossaryEntries(content, bcGlossary); } @@ -567,7 +580,23 @@ function convertScriptureBook( fileContents.push(fs.readFileSync(filePath, 'utf-8')); }); - processBookContent(resolve, null, fileContents.join('')); + // Collect the file contents into a single document + let usfm: string; + + if (book.type == 'story') { + // The first file contains meta-content (id, title, etc) + usfm = fileContents[0]; + + // Subsequent files represent storybook pages. + // SAB deletes the \page tags. Replace them with chapter tags. + for (let i = 1; i < fileContents.length; i++) { + usfm += `\\c ${i} ${fileContents[i]}`; + } + } else { + usfm = fileContents.join(''); + } + + processBookContent(resolve, null, usfm); } }) ); diff --git a/convert/convertConfig.ts b/convert/convertConfig.ts index 6b1f32189..d91b4163d 100644 --- a/convert/convertConfig.ts +++ b/convert/convertConfig.ts @@ -22,6 +22,12 @@ type BookCollectionAudio = { timingFile: string; }; +type StorybookImage = { + page: string; + filename: string; + // TODO: Add motion parameters +}; + type Style = { font: string; textSize: number; @@ -49,6 +55,7 @@ export type Book = { audio: BookCollectionAudio[]; features: any; quizFeatures?: any; + storybookImages?: StorybookImage[]; footer?: HTML; style?: Style; styles?: { @@ -419,6 +426,109 @@ function convertCollectionFooter(collectionTag: Element, document: Document) { return footer; } +function shortenBookCode(id: string, allIds: string[]): string | null { + const short = id.replace(/^(\w)0(\d\d)$/, '$1$2'); + return id === short || allIds.includes(short) ? null : short; +} + +function lengthenBookCode(id: string, allIds: string[]): string | null { + if (id.length === 1) { + id = '00' + id; + } else if (id.length === 2) { + id = '0' + id; + } + return allIds.includes(id) ? null : id; +} + +function convertBookCodes(books: Element[]) { + for (const bk of books) { + bk.setAttribute('fullId', bk.id); + const ids = books.map((b) => b.id); + const shortened = shortenBookCode(bk.id, ids); + const lengthened = lengthenBookCode(bk.id, ids); + if (shortened) { + console.log(` shortening book code: ${bk.id} => ${shortened}`); + bk.id = shortened; + } else if (lengthened) { + console.log(` lengthening book code: ${bk.id} => ${lengthened}`); + bk.id = lengthened; + } + } + checkBookCodes(books); +} + +function checkBookCodes(books: Element[]) { + const invalid = books.map((b) => b.id).filter((id) => id.length !== 3); + if (invalid.length) { + console.log( + '\n WARNING: The following book codes are not 3 characters. Some may not load properly:' + ); + console.log(` ${invalid.join(' ')}`); + } +} + +function getBookAudio(book: Element, verbose: number) { + const audio: BookCollectionAudio[] = []; + for (const page of book.getElementsByTagName('page')) { + if (verbose >= 2) console.log(`.. page: ${page.attributes[0].value}`); + const audioTag = page.getElementsByTagName('audio')[0]; + if (!audioTag) continue; + if (audioTag.attributes.getNamedItem('background')?.value === 'continue') { + // Happens when a storybook uses a single audio file for multiple pages. + // TODO: Implement this feature + continue; + } + const fTag = audioTag.getElementsByTagName('f')[0]; + if (verbose >= 2) + console.log(`... audioTag: ${audioTag.outerHTML}, fTag:${fTag.outerHTML}`); + audio.push({ + num: parseInt(page.attributes.getNamedItem('num')!.value), + filename: fTag.innerHTML, + len: fTag.hasAttribute('len') + ? parseInt(fTag.attributes.getNamedItem('len')!.value) + : undefined, + size: fTag.hasAttribute('size') + ? parseInt(fTag.attributes.getNamedItem('size')!.value) + : undefined, + src: fTag.attributes.getNamedItem('src')!.value, + timingFile: audioTag.getElementsByTagName('y')[0]?.innerHTML + }); + if (verbose >= 3) console.log(`.... audio: `, JSON.stringify(audio[0])); + } + return audio; +} + +function imageFromPage( + page: Element, + collection: string, + book: string, + imageFiles: string[] +): StorybookImage | null { + const filenameElement = page.getElementsByTagName('image-filename')[0]; + + // In testing, the image filename took one of the following two forms + const filename1 = filenameElement?.textContent; + const filename2 = `${collection}-${book}-${filename1}`; + const file = imageFiles.find((f) => [filename1, filename2].includes(f)); + + const num = page.getAttribute('num'); + return file && num + ? { + filename: file, + page: num + } + : null; +} + +function getStorybookImages(book: Element, collection: string, dataDir: string): StorybookImage[] { + const id = book.getAttribute('fullId') ?? book.id; + const pages = Array.from(book.getElementsByTagName('page')); + const imageFiles = readdirSync(path.join(dataDir, 'illustrations')); + return pages + .map((page) => imageFromPage(page, collection, id, imageFiles)) + .filter((image) => image) as StorybookImage[]; +} + function convertConfig(dataDir: string, verbose: number) { const dom = new jsdom.JSDOM(readFileSync(path.join(dataDir, 'appdef.xml')).toString(), { contentType: 'text/xml' @@ -578,30 +688,9 @@ function convertConfig(dataDir: string, verbose: number) { } const books: BookCollection['books'] = []; const bookTags = tag.getElementsByTagName('book'); + convertBookCodes(Array.from(bookTags)); for (const book of bookTags) { if (verbose >= 2) console.log(`. book: ${book.id}`); - const audio: BookCollectionAudio[] = []; - for (const page of book.getElementsByTagName('page')) { - if (verbose >= 2) console.log(`.. page: ${page.attributes[0].value}`); - const audioTag = page.getElementsByTagName('audio')[0]; - if (!audioTag) continue; - const fTag = audioTag.getElementsByTagName('f')[0]; - if (verbose >= 2) - console.log(`... audioTag: ${audioTag.outerHTML}, fTag:${fTag.outerHTML}`); - audio.push({ - num: parseInt(page.attributes.getNamedItem('num')!.value), - filename: fTag.innerHTML, - len: fTag.hasAttribute('len') - ? parseInt(fTag.attributes.getNamedItem('len')!.value) - : undefined, - size: fTag.hasAttribute('size') - ? parseInt(fTag.attributes.getNamedItem('size')!.value) - : undefined, - src: fTag.attributes.getNamedItem('src')!.value, - timingFile: audioTag.getElementsByTagName('y')[0]?.innerHTML - }); - if (verbose >= 3) console.log(`.... audio: `, JSON.stringify(audio[0])); - } const bookFeaturesTag = book .querySelector('features[type=book]') ?.getElementsByTagName('e'); @@ -658,7 +747,8 @@ function convertConfig(dataDir: string, verbose: number) { section: book.getElementsByTagName('sg')[0]?.innerHTML, testament: book.getElementsByTagName('g')[0]?.innerHTML, abbreviation: book.getElementsByTagName('v')[0]?.innerHTML, - audio, + audio: getBookAudio(book, verbose), + storybookImages: getStorybookImages(book, tag.id, dataDir), file: book.getElementsByTagName('f')[0]?.innerHTML.replace(/\.\w*$/, '.usfm'), features: bookFeatures, quizFeatures, diff --git a/convert/storybook.test.ts b/convert/storybook.test.ts new file mode 100644 index 000000000..f223d1e6e --- /dev/null +++ b/convert/storybook.test.ts @@ -0,0 +1,270 @@ +import { expect, test } from 'vitest'; +import { + transformLists, + replacePageTags, + removeImageTags, + transformHeadings, + convertPTags +} from './storybook'; + +function tokensOf(str: string) { + return str.split(/\s+/).filter((token) => token.length); +} + +test('replace page tags', () => { + const input = 'abc \\page 41 xyz \\page 45 122'; + const expected = 'abc \\c 41 xyz \\c 45 122'; + const result = replacePageTags(input); + expect(tokensOf(result)).toEqual(tokensOf(expected)); +}); + +// For now, get image data from config.js (may change this in the future) +test('remove img tags', () => { + const input = 'abc \\img img1.jpeg efg \\img image-2.jpeg \\img image_with_underscores.jpeg'; + const expected = 'abc efg'; + const result = removeImageTags(input); + expect(tokensOf(result)).toEqual(tokensOf(expected)); +}); + +test('convertPTags', () => { + const input = 'abc \\p_Normal hello world \\m \\b'; + const expected = 'abc \\m hello world \\m \\b'; + const result = convertPTags(input); + expect(tokensOf(result)).toEqual(tokensOf(expected)); +}); + +test('convert unordered list to milestones', () => { + const input = ` + \\m Some content + \\zuli1 One + \\zuli1 Two \\zuli1 Three in a row! + \\b + `; + const expected = ` + \\m Some content + \\m + \\zuli1-s |\\* One \\zuli1-e\\* + \\zuli1-s |\\* Two \\zuli1-e\\* + \\zuli1-s |\\* Three in a row! \\zuli1-e\\* + \\b + `; + const result = transformLists(input); + expect(tokensOf(result)).toEqual(tokensOf(expected)); +}); + +test('convert ordered list to milestones', () => { + const input = ` + \\m Some content + \\zon1 10 + \\zoli1 One + \\zoli1 Two \\zoli1 Three in a row! + \\b + `; + const expected = ` + \\m Some content + \\m + \\zon1-s |start="10"\\* + \\zoli1-s |\\* One \\zoli1-e\\* + \\zoli1-s |\\* Two \\zoli1-e\\* + \\zoli1-s |\\* Three in a row! \\zoli1-e\\* + \\zon1-e\\* + \\b + `; + const result = transformLists(input); + expect(tokensOf(result)).toEqual(tokensOf(expected)); +}); + +test('convert unordered list with formatting to milestones', () => { + const input = ` + \\m Some content + \\zuli1 One + \\zuli1 \\bd Two \\bd* + \\zuli1 Three in a row! + \\b + `; + const expected = ` + \\m Some content + \\m + \\zuli1-s |\\* One \\zuli1-e\\* + \\zuli1-s |\\* \\bd Two \\bd* \\zuli1-e\\* + \\zuli1-s |\\* Three in a row! \\zuli1-e\\* + \\b + `; + const result = transformLists(input); + expect(tokensOf(result)).toEqual(tokensOf(expected)); +}); + +test('convert ordered list with formatting to milestones', () => { + const input = ` + \\m Some content + \\zon1 10 + \\zoli1 One + \\zoli1 \\bdit Two \\bdit* + \\zoli1 Three in a row! + \\b + `; + const expected = ` + \\m Some content + \\m + \\zon1-s |start="10"\\* + \\zoli1-s |\\* One \\zoli1-e\\* + \\zoli1-s |\\* \\bdit Two \\bdit* \\zoli1-e\\* + \\zoli1-s |\\* Three in a row! \\zoli1-e\\* + \\zon1-e\\* + \\b + `; + const result = transformLists(input); + expect(tokensOf(result)).toEqual(tokensOf(expected)); +}); + +test('convert multilevel unordered list to milestones', () => { + const input = ` + \\c 2 + \\b + \\zuli1 Old Testament + \\zuli2 Pentateuch + \\zuli3 Genesis + \\zuli3 Exodus + \\zuli3 Leviticus + \\zuli2 Joshua + \\zuli2 Judges + \\zuli1 New Testament + \\zuli2 Matthew + \\zuli2 Mark + \\zuli2 Luke + \\zuli2 John + \\zuli1 Glossary + `; + const expected = ` + \\c 2 + \\b + \\m + \\zuli1-s |\\* Old Testament + \\zuli2-s |\\* Pentateuch + \\zuli3-s |\\* Genesis \\zuli3-e\\* + \\zuli3-s |\\* Exodus \\zuli3-e\\* + \\zuli3-s |\\* Leviticus \\zuli3-e\\* + \\zuli2-e\\* + \\zuli2-s |\\* Joshua \\zuli2-e\\* + \\zuli2-s |\\* Judges \\zuli2-e\\* + \\zuli1-e\\* + \\zuli1-s |\\* New Testament + \\zuli2-s |\\* Matthew \\zuli2-e\\* + \\zuli2-s |\\* Mark \\zuli2-e\\* + \\zuli2-s |\\* Luke \\zuli2-e\\* + \\zuli2-s |\\* John \\zuli2-e\\* + \\zuli1-e\\* + \\zuli1-s |\\* Glossary \\zuli1-e\\* + `; + const result = transformLists(input); + expect(tokensOf(result)).toEqual(tokensOf(expected)); +}); + +test('convert multilevel ordered list to milestones', () => { + const input = ` + \\m My List: + \\b + \\zon1 1 + \\zoli1 Food + \\zon2 1 + \\zoli2 Fruit + \\zon3 1 + \\zoli3 Apples + \\zoli3 Bananas + \\zoli3 Pears + \\zoli2 Dessert + \\zoli3 Pie + \\zoli3 Cake + \\zoli3 Ice Cream + \\zoli1 Drinks + \\zoli2 Coffee + \\zoli2 Water + \\zoli2 Tea + `; + const expected = ` + \\m My List: + \\b + \\m + \\zon1-s |start="1"\\* + \\zoli1-s |\\* Food + \\zon2-s |start="1"\\* + \\zoli2-s |\\* Fruit + \\zon3-s |start="1"\\* + \\zoli3-s |\\* Apples \\zoli3-e\\* + \\zoli3-s |\\* Bananas \\zoli3-e\\* + \\zoli3-s |\\* Pears \\zoli3-e\\* + \\zon3-e\\* + \\zoli2-e\\* + \\zoli2-s |\\* Dessert + \\zon3-s |start="1"\\* + \\zoli3-s |\\* Pie \\zoli3-e\\* + \\zoli3-s |\\* Cake \\zoli3-e\\* + \\zoli3-s |\\* Ice Cream \\zoli3-e\\* + \\zon3-e\\* + \\zoli2-e\\* + \\zon2-e\\* + \\zoli1-e\\* + \\zoli1-s |\\* Drinks + \\zon2-s |start="1"\\* + \\zoli2-s |\\* Coffee \\zoli2-e\\* + \\zoli2-s |\\* Water \\zoli2-e\\* + \\zoli2-s |\\* Tea \\zoli2-e\\* + \\zon2-e\\* + \\zoli1-e\\* + \\zon1-e\\* + `; + const result = transformLists(input); + expect(tokensOf(result)).toEqual(tokensOf(expected)); +}); + +test('convert multilevel ordered list with formatting to milestones', () => { + const input = ` + \\m Some content + \\zon1 10 + \\zoli1 One + \\zon2 3 + \\zoli2 \\it sub-point 1 \\it* + \\zoli2 sub-point 2 + \\zoli1 \\bdit Two \\bdit* + \\zoli1 Three in a row! + \\b + `; + const expected = ` + \\m Some content + \\m + \\zon1-s |start="10"\\* + \\zoli1-s |\\* One + \\zon2-s |start="3"\\* + \\zoli2-s |\\* \\it sub-point 1 \\it* \\zoli2-e\\* + \\zoli2-s |\\* sub-point 2 \\zoli2-e\\* + \\zon2-e\\* + \\zoli1-e\\* + \\zoli1-s |\\* \\bdit Two \\bdit* \\zoli1-e\\* + \\zoli1-s |\\* Three in a row! \\zoli1-e\\* + \\zon1-e\\* + \\b + `; + const result = transformLists(input); + expect(tokensOf(result)).toEqual(tokensOf(expected)); +}); + +test('converts section headings to milestones', () => { + const input = ` + \\ms1 Hello + \\ms2 World + \\b + \\s Some content + \\s2 Some other content + \\m + `; + const expected = ` + \\m \\zusfm-s |class="ms1"\\* Hello \\zusfm-e + \\m \\zusfm-s |class="ms2"\\* World \\zusfm-e + \\b + \\m \\zusfm-s |class="s"\\* Some content \\zusfm-e + \\m \\zusfm-s |class="s2"\\* Some other content \\zusfm-e + \\m + `; + const result = transformHeadings(input); + expect(tokensOf(result)).toEqual(tokensOf(expected)); +}); diff --git a/convert/storybook.ts b/convert/storybook.ts new file mode 100644 index 000000000..ffc6167bf --- /dev/null +++ b/convert/storybook.ts @@ -0,0 +1,143 @@ +/** + * A list of inline character format markers + */ +const characterMarkers = 'em bd it bdit no sc sup'.split(' '); + +/** + * Replace page tags with chapters + */ +export function replacePageTags(usfm: string): string { + return usfm.replace(/\\page\s+(\d+)/g, '\\c $1'); +} + +/** + * Remove img tags + * + * For now, get images from config (may change this later) + */ +export function removeImageTags(usfm: string): string { + return usfm.replace(/\\img\s+\S+/g, ''); +} + +/** + * Replace \p_Normal paragraphs with \m + */ +export function convertPTags(usfm: string) { + return usfm.replace(/\\p_Normal/g, '\\m'); +} + +/** + * Convert list tags to milestones + */ +export function transformLists(usfm: string): string { + usfm = transformUnorderedLists(usfm); + usfm = transformOrderedLists(usfm); + return usfm; +} + +function transformUnorderedLists(usfm: string): string { + const inlineMarkers = [...characterMarkers, 'zuli'].join('|'); + const listPattern = new RegExp(`\\\\zuli([^\\\\]|\\\\(${inlineMarkers}))*`, 'g'); + // Place a paragraph marker (\m) before unordered lists + usfm = usfm.replace(listPattern, '\\m $& '); + let level = 1; + let tag = '\\zuli' + level; + while (usfm.includes(tag)) { + const inlineMarkers = [...characterMarkers, `zuli[^1-${level}]`].join('|'); + const pattern = new RegExp(`\\${tag}\\s(([^\\\\]|\\\\(${inlineMarkers}))*)`, 'g'); + usfm = usfm.replace(pattern, `${tag}-s |\\* $1 ${tag}-e\\* `); + level++; + tag = '\\zuli' + level; + } + return usfm; +} + +/** + * Get the start number for an ordered list + */ +function getStartNumber(usfm: string, level: number) { + const startPattern = new RegExp(`\\\\zon${level} (\\d+)`); + const startMatch = usfm.match(startPattern); + return startMatch ? startMatch[1] : '1'; +} + +function transformOrderedListItems(usfm: string, level: number) { + // A nested list + // * begins with \zon# or \zoli#, where # does not equal the current level + // * ends at the first sfm tag that is not \zon#, \zoli#, or a character marker + const sublistMarkers = [...characterMarkers, `zon[^${level}]`, `zoli[^${level}]`].join('|'); + const sublistPattern = `(\\\\zo(n|li)[^${level}])([^\\\\]|\\\\(${sublistMarkers}))*`; + + // A list item consists of the following parts: + // * \zoli# tag, where # is the current level + // * text that may include only character markers + // * a nested list (optional) + const textMarkers = characterMarkers.join('|'); + const itemPattern = new RegExp( + `\\\\zoli${level}\\s((?:[^\\\\]|\\\\(?:${textMarkers}))*)(${sublistPattern})?`, + 'g' + ); + const items = Array.from(usfm.matchAll(itemPattern)); + return items + .map((item) => { + const sublist = item[2] ? transformOrderedSublist(item[2], level + 1) : ''; + return `\\zoli${level}-s |\\* ${item[1]} ${sublist} \\zoli${level}-e\\*`; + }) + .join(' '); +} + +function transformOrderedSublist(usfm: string, level: number): string { + const start = getStartNumber(usfm, level); + + // A list's contents + // * begins with \zoli#, where # is the current level + // * contains no sfm tags except the following: + // - \zoli#, where # is any number + // - \zon#, where # is not the current level + const allowedMarkers = [...characterMarkers, 'zoli', `zon[^${level}]`].join('|'); + const contentsPattern = new RegExp(`\\\\zoli${level}\\s([^\\\\]|\\\\(${allowedMarkers}))*`); + const contentsMatch = usfm.match(contentsPattern); + if (contentsMatch) { + const contents = transformOrderedListItems(contentsMatch[0], level); + return ` \\zon${level}-s |start="${start}"\\* ${contents} \\zon${level}-e\\* `; + } + throw new Error(`Invalid USFM list: ${usfm}`); +} + +function transformOrderedLists(usfm: string) { + // Each ordered list + // * begins with \zon1 + // * contains no sfm markers except the following: + // - inline character markup + // - \zoli#, where # is any number + // - \zon#, where # is not 1 + const allowedMarkers = [...characterMarkers, 'zoli', 'zon[^1]'].join('|'); + const listPattern = new RegExp(`\\\\zon1\\s([^\\\\]|\\\\(${allowedMarkers}))*`, 'g'); + const lists = usfm.matchAll(listPattern); + let transformed = ''; + let i = 0; + for (const list of lists) { + transformed += usfm.substring(i, list.index); + transformed += ' \\m '; + transformed += transformOrderedSublist(list[0], 1); + if (list.index === undefined) { + throw new Error('Expected regex match index to be defined'); + } else { + i = list.index + list[0].length; + } + } + return transformed + usfm.substring(i); +} + +export function transformHeadings(usfm: string): string { + return usfm.replace(/\\(m?s\d?)([^\\]*)/g, '\\m \\zusfm-s |class="$1"\\* $2 \\zusfm-e '); +} + +export function convertStorybookElements(usfm: string) { + usfm = replacePageTags(usfm); + usfm = removeImageTags(usfm); + usfm = convertPTags(usfm); + usfm = transformLists(usfm); + usfm = transformHeadings(usfm); + return usfm; +} diff --git a/package-lock.json b/package-lock.json index 7936f65db..8e70d7505 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "ts-node": "^10.8.1", "typescript": "^5", "vite": "^4", - "vitest": "^1.0.0" + "vitest": "^1.4.0" } }, "convert": { @@ -3078,9 +3078,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.9.14", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.14.tgz", - "integrity": "sha512-nOpuzZ2G3IuMFN+UPPpKrC6NsLmWsTqSsm66IRfnBt1D4pwTqE27lmbpcPM+l2Ua4gE7PfjRHI6uedAy7hoXUw==", + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", "dev": true, "dependencies": { "@grpc/proto-loader": "^0.7.8", @@ -3445,6 +3445,19 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz", + "integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", @@ -3471,6 +3484,19 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz", + "integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", @@ -3484,6 +3510,19 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz", + "integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", @@ -3935,13 +3974,13 @@ "dev": true }, "node_modules/@vitest/expect": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.1.tgz", - "integrity": "sha512-/bqGXcHfyKgFWYwIgFr1QYDaR9e64pRKxgBNWNXPefPFRhgm+K3+a/dS0cUGEreWngets3dlr8w8SBRw2fCfFQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", + "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", "dev": true, "dependencies": { - "@vitest/spy": "1.2.1", - "@vitest/utils": "1.2.1", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", "chai": "^4.3.10" }, "funding": { @@ -3949,12 +3988,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.1.tgz", - "integrity": "sha512-zc2dP5LQpzNzbpaBt7OeYAvmIsRS1KpZQw4G3WM/yqSV1cQKNKwLGmnm79GyZZjMhQGlRcSFMImLjZaUQvNVZQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", + "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", "dev": true, "dependencies": { - "@vitest/utils": "1.2.1", + "@vitest/utils": "1.6.0", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -3978,9 +4017,9 @@ } }, "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", "dev": true, "engines": { "node": ">=12.20" @@ -3990,9 +4029,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.1.tgz", - "integrity": "sha512-Tmp/IcYEemKaqAYCS08sh0vORLJkMr0NRV76Gl8sHGxXT5151cITJCET20063wk0Yr/1koQ6dnmP6eEqezmd/Q==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", + "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -4004,9 +4043,9 @@ } }, "node_modules/@vitest/spy": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.1.tgz", - "integrity": "sha512-vG3a/b7INKH7L49Lbp0IWrG6sw9j4waWAucwnksPB1r1FTJgV7nkBByd9ufzu6VWya/QTvQW4V9FShZbZIB2UQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", + "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -4016,12 +4055,12 @@ } }, "node_modules/@vitest/ui": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-1.2.1.tgz", - "integrity": "sha512-5kyEDpH18TB13Keutk5VScWG+LUDfPJOL2Yd1hqX+jv6+V74tp4ZYcmTgx//WDngiZA5PvX3qCHQ5KrhGzPbLg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-1.6.0.tgz", + "integrity": "sha512-k3Lyo+ONLOgylctiGovRKy7V4+dIN2yxstX3eY5cWFXH6WP+ooVX79YSyi0GagdTQzLmT43BF27T0s6dOIPBXA==", "dev": true, "dependencies": { - "@vitest/utils": "1.2.1", + "@vitest/utils": "1.6.0", "fast-glob": "^3.3.2", "fflate": "^0.8.1", "flatted": "^3.2.9", @@ -4033,13 +4072,13 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "^1.0.0" + "vitest": "1.6.0" } }, "node_modules/@vitest/utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.1.tgz", - "integrity": "sha512-bsH6WVZYe/J2v3+81M5LDU8kW76xWObKIURpPrOXm2pjBniBu2MERI/XP60GpS4PHU3jyK50LUutOwrx4CyHUg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", + "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", "dev": true, "dependencies": { "diff-sequences": "^29.6.3", @@ -4428,12 +4467,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -4581,9 +4620,9 @@ ] }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", @@ -4592,7 +4631,7 @@ "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "type-detect": "^4.1.0" }, "engines": { "node": ">=4" @@ -5082,9 +5121,9 @@ "dev": true }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, "dependencies": { "type-detect": "^4.0.0" @@ -5867,9 +5906,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -7798,9 +7837,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, "node_modules/picomatch": { @@ -7845,9 +7884,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", "dev": true, "funding": [ { @@ -7865,7 +7904,7 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "source-map-js": "^1.2.0" }, "engines": { @@ -8299,9 +8338,9 @@ ] }, "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, "node_modules/read-cache": { @@ -9162,17 +9201,23 @@ } }, "node_modules/strip-literal": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", - "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz", + "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", "dev": true, "dependencies": { - "acorn": "^8.10.0" + "js-tokens": "^9.0.0" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz", + "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", + "dev": true + }, "node_modules/sucrase": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", @@ -9594,18 +9639,18 @@ "dev": true }, "node_modules/tinypool": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", - "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", - "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", "dev": true, "engines": { "node": ">=14.0.0" @@ -9784,9 +9829,9 @@ } }, "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, "engines": { "node": ">=4" @@ -10053,9 +10098,9 @@ } }, "node_modules/vite-node": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.1.tgz", - "integrity": "sha512-fNzHmQUSOY+y30naohBvSW7pPn/xn3Ib/uqm+5wAJQJiqQsU0NBR78XdRJb04l4bOFKjpTWld0XAfkKlrDbySg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", + "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -10074,10 +10119,585 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz", + "integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/vite-node/node_modules/@rollup/rollup-android-arm64": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz", + "integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/vite-node/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz", + "integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/vite-node/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz", + "integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/vite-node/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz", + "integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite-node/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz", + "integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite-node/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz", + "integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite-node/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz", + "integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite-node/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz", + "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite-node/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz", + "integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite-node/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz", + "integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/vite-node/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz", + "integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/vite-node/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz", + "integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "node_modules/vite-node/node_modules/rollup": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", - "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz", + "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -10090,30 +10710,33 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.13.0", - "@rollup/rollup-android-arm64": "4.13.0", - "@rollup/rollup-darwin-arm64": "4.13.0", - "@rollup/rollup-darwin-x64": "4.13.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", - "@rollup/rollup-linux-arm64-gnu": "4.13.0", - "@rollup/rollup-linux-arm64-musl": "4.13.0", - "@rollup/rollup-linux-riscv64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-musl": "4.13.0", - "@rollup/rollup-win32-arm64-msvc": "4.13.0", - "@rollup/rollup-win32-ia32-msvc": "4.13.0", - "@rollup/rollup-win32-x64-msvc": "4.13.0", + "@rollup/rollup-android-arm-eabi": "4.20.0", + "@rollup/rollup-android-arm64": "4.20.0", + "@rollup/rollup-darwin-arm64": "4.20.0", + "@rollup/rollup-darwin-x64": "4.20.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.20.0", + "@rollup/rollup-linux-arm-musleabihf": "4.20.0", + "@rollup/rollup-linux-arm64-gnu": "4.20.0", + "@rollup/rollup-linux-arm64-musl": "4.20.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.20.0", + "@rollup/rollup-linux-riscv64-gnu": "4.20.0", + "@rollup/rollup-linux-s390x-gnu": "4.20.0", + "@rollup/rollup-linux-x64-gnu": "4.20.0", + "@rollup/rollup-linux-x64-musl": "4.20.0", + "@rollup/rollup-win32-arm64-msvc": "4.20.0", + "@rollup/rollup-win32-ia32-msvc": "4.20.0", + "@rollup/rollup-win32-x64-msvc": "4.20.0", "fsevents": "~2.3.2" } }, "node_modules/vite-node/node_modules/vite": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", - "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", + "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", "dev": true, "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", + "esbuild": "^0.21.3", + "postcss": "^8.4.39", "rollup": "^4.13.0" }, "bin": { @@ -10565,18 +11188,17 @@ } }, "node_modules/vitest": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.1.tgz", - "integrity": "sha512-TRph8N8rnSDa5M2wKWJCMnztCZS9cDcgVTQ6tsTFTG/odHJ4l5yNVqvbeDJYJRZ6is3uxaEpFs8LL6QM+YFSdA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", + "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", "dev": true, "dependencies": { - "@vitest/expect": "1.2.1", - "@vitest/runner": "1.2.1", - "@vitest/snapshot": "1.2.1", - "@vitest/spy": "1.2.1", - "@vitest/utils": "1.2.1", + "@vitest/expect": "1.6.0", + "@vitest/runner": "1.6.0", + "@vitest/snapshot": "1.6.0", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", "acorn-walk": "^8.3.2", - "cac": "^6.7.14", "chai": "^4.3.10", "debug": "^4.3.4", "execa": "^8.0.1", @@ -10585,11 +11207,11 @@ "pathe": "^1.1.1", "picocolors": "^1.0.0", "std-env": "^3.5.0", - "strip-literal": "^1.3.0", + "strip-literal": "^2.0.0", "tinybench": "^2.5.1", - "tinypool": "^0.8.1", + "tinypool": "^0.8.3", "vite": "^5.0.0", - "vite-node": "1.2.1", + "vite-node": "1.6.0", "why-is-node-running": "^2.2.2" }, "bin": { @@ -10604,8 +11226,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "^1.0.0", - "@vitest/ui": "^1.0.0", + "@vitest/browser": "1.6.0", + "@vitest/ui": "1.6.0", "happy-dom": "*", "jsdom": "*" }, @@ -11021,9 +11643,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index ab9f26393..15b8849de 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "ts-node": "^10.8.1", "typescript": "^5", "vite": "^4", - "vitest": "^1.0.0" + "vitest": "^1.4.0" }, "volta": { "node": "20.9.0" diff --git a/src/lib/components/ScriptureViewSofria.svelte b/src/lib/components/ScriptureViewSofria.svelte index d30079740..72bd026df 100644 --- a/src/lib/components/ScriptureViewSofria.svelte +++ b/src/lib/components/ScriptureViewSofria.svelte @@ -39,6 +39,7 @@ LOGGING: export let bodyFontSize: any; export let bodyLineHeight: any; export let bookmarks: any; + export let direction: string; export let notes: any; export let highlights: any; export let maxSelections: any; @@ -1250,8 +1251,11 @@ LOGGING: const refText = generateHTML(text, 'header-ref'); spanV.innerHTML = refText; if (workspace.phraseDiv === null) { - workspace.phraseDiv = startPhrase(workspace, 'keep'); - } + workspace.phraseDiv = startPhrase( + workspace, + 'keep' + ); + } workspace.phraseDiv.appendChild(spanV); } else { addText(workspace, text); @@ -1896,9 +1900,6 @@ LOGGING: $: versePerLine = verseLayout === 'one-per-line'; /**list of books in current docSet*/ $: books = $refs.catalog.documents; - $: direction = config.bookCollections.find((x) => x.id === references.collection).style - .textDirection; - $: (() => { performance.mark('query-start'); const bookHasIntroduction = books.find((x) => x.bookCode === currentBook).hasIntroduction; diff --git a/src/lib/components/StorybookImage.svelte b/src/lib/components/StorybookImage.svelte new file mode 100644 index 000000000..2814dd9ba --- /dev/null +++ b/src/lib/components/StorybookImage.svelte @@ -0,0 +1,21 @@ + + +{#if image} + +{/if} diff --git a/src/lib/components/StorybookText.svelte b/src/lib/components/StorybookText.svelte new file mode 100644 index 000000000..d9ea6349d --- /dev/null +++ b/src/lib/components/StorybookText.svelte @@ -0,0 +1,42 @@ + + +{#await loadSofria(references)} + +{:then sofria} +
+ +
+{/await} diff --git a/src/lib/components/sofria-render-json/RenderBlocks.svelte b/src/lib/components/sofria-render-json/RenderBlocks.svelte new file mode 100644 index 000000000..5ea5b1c9a --- /dev/null +++ b/src/lib/components/sofria-render-json/RenderBlocks.svelte @@ -0,0 +1,21 @@ + + +{#each blocks as block} + {#if isParagraph(block)} + + {:else if blockIsGraft(block)} + + {:else} + {(onInvalidBlockType(block), '')} + {/if} +{/each} diff --git a/src/lib/components/sofria-render-json/RenderContent.svelte b/src/lib/components/sofria-render-json/RenderContent.svelte new file mode 100644 index 000000000..53c2c5c78 --- /dev/null +++ b/src/lib/components/sofria-render-json/RenderContent.svelte @@ -0,0 +1,28 @@ + + +{#each content as element} + {#if typeof element === 'string'} + {element} + {:else if contentIsGraft(element)} + + {:else if isWrapper(element)} + + {:else} + {(onInvalidContent(element), '')} + {/if} +{/each} diff --git a/src/lib/components/sofria-render-json/RenderParagraph.svelte b/src/lib/components/sofria-render-json/RenderParagraph.svelte new file mode 100644 index 000000000..57a3b6089 --- /dev/null +++ b/src/lib/components/sofria-render-json/RenderParagraph.svelte @@ -0,0 +1,27 @@ + + +{#if isUsfmParagraph(paragraph)} +
+ +
+{:else if isListContainer(paragraph)} +
+ +
+{:else if isListItem(paragraph)} +
  • + +
  • +{:else} + {(onInvalidParagraph(), '')} +{/if} diff --git a/src/lib/components/sofria-render-json/RenderSequence.svelte b/src/lib/components/sofria-render-json/RenderSequence.svelte new file mode 100644 index 000000000..30274416f --- /dev/null +++ b/src/lib/components/sofria-render-json/RenderSequence.svelte @@ -0,0 +1,19 @@ + + +{#if isListSequence(sequence)} +
      + +
    +{:else if isOrderedListSequence(sequence)} +
      + +
    +{:else} + +{/if} diff --git a/src/lib/components/sofria-render-json/RenderWrapper.svelte b/src/lib/components/sofria-render-json/RenderWrapper.svelte new file mode 100644 index 000000000..efa0d9e85 --- /dev/null +++ b/src/lib/components/sofria-render-json/RenderWrapper.svelte @@ -0,0 +1,18 @@ + + +{#if bold || italic} + + + +{:else} + +{/if} diff --git a/src/lib/components/sofria-render-json/SofriaRender.svelte b/src/lib/components/sofria-render-json/SofriaRender.svelte new file mode 100644 index 000000000..92c855332 --- /dev/null +++ b/src/lib/components/sofria-render-json/SofriaRender.svelte @@ -0,0 +1,26 @@ + + + diff --git a/src/lib/components/sofria-render-json/proskomma-tools/convert-lists.ts b/src/lib/components/sofria-render-json/proskomma-tools/convert-lists.ts new file mode 100644 index 000000000..01bf70c79 --- /dev/null +++ b/src/lib/components/sofria-render-json/proskomma-tools/convert-lists.ts @@ -0,0 +1,274 @@ +import { + type Content, + type Wrapper, + type ContentModifier, + type ContentElement, + isWrapper, + type Paragraph, + type Block, + type Graft, + isParagraph +} from '../schema/sofria-schema'; +import { replaceContent } from './convert-paragraph'; + +function listItemSubtype(level: number, ordered: boolean) { + return ordered ? `zoli${level}` : `zuli${level}`; +} + +function isListItemStart(element: ContentElement, level: number, ordered: boolean) { + return ( + typeof element !== 'string' && + element.type === 'start_milestone' && + element.subtype === 'usfm:' + listItemSubtype(level, ordered) + ); +} + +function isListItemEnd(element: ContentElement, level: number, ordered: boolean) { + return ( + typeof element !== 'string' && + element.type === 'end_milestone' && + element.subtype === 'usfm:' + listItemSubtype(level, ordered) + ); +} + +function isListItemMilestone(element: ContentElement, level: number, ordered: boolean) { + return isListItemStart(element, level, ordered) || isListItemEnd(element, level, ordered); +} + +function hasItemStart(element: ContentElement, level: number, ordered: boolean) { + return ( + isListItemStart(element, level, ordered) || + (isWrapper(element) && element.content.find((e) => hasItemStart(e, level, ordered))) + ); +} + +function hasList(content: Content, level: number, ordered: boolean) { + // Look for milestones, which may be nested within wrapper elements + const hasNestedList = + content.filter( + (element) => + typeof element !== 'string' && + element.type === 'wrapper' && + element.content && + hasList(element.content, level, ordered) + ).length > 0; + return ( + hasNestedList || + content.filter((element) => isListItemMilestone(element, level, ordered)).length > 0 + ); +} + +/** + * Get content for each bullet item in an unordered list + */ +function listItems(content: Content, listLevel: number, ordered: boolean): Content[] { + const items: Content[] = []; + while (content.length > 0) { + const { item, contentLeft } = nextListItem(content, listLevel, ordered, items.length === 0); + items.push(item); + content = contentLeft; + } + return items; +} + +/** + * Get content for the first bullet point in the given list + * + * @returns an object with the following properties: + * - item: The content for the list item + * - itemDone: Whether the item has terminated with an end milestone + * - contentLeft: Unparsed content containing the rest of the list items + */ +function nextListItem(content: Content, listLevel: number, ordered: boolean, isFirstItem: boolean) { + const listContent: Content = []; + for (let i = 0; i < content.length; i++) { + const { element, itemDone, parseNext } = parseListElement( + content[i], + listLevel, + ordered, + isFirstItem + ); + if (element) { + listContent.push(element); + } + if (itemDone) { + return { + item: listContent, + itemDone: true, + contentLeft: getRemainingContent(content, i, parseNext) + }; + } + } + return { item: listContent, itemDone: false, contentLeft: [] }; +} + +/** + * Get content items after the given index, prepending parseNext if present + */ +function getRemainingContent( + content: Content, + lastParsedIndex: number, + parseNext: null | ContentModifier +) { + const i = lastParsedIndex + 1; + return parseNext?.content.length > 0 ? [parseNext, ...content.slice(i)] : content.slice(i); +} + +/** + * Parse a content element of a list defined by milestones + * + * @returns an object with the following properties: + * - element: If not null, a content element to be appended to the parsed list + * - itemDone: Whether the given element marks the end of the current list item + * - parseNext: If itemDone is true, this element contains the next item to parse + */ +function parseListElement( + element: ContentElement, + listLevel: number, + ordered: boolean, + isFirstItem: boolean +) { + if (isWrapper(element)) { + const { item, itemDone, contentLeft } = nextListItem( + element.content, + listLevel, + ordered, + isFirstItem + ); + return { + element: replaceContent(element, item), + itemDone, + parseNext: replaceContent(element, contentLeft) + }; + } + return { + element: isListItemMilestone(element, listLevel, ordered) ? null : element, + itemDone: + isListItemEnd(element, listLevel, ordered) || + (isFirstItem && isListItemStart(element, listLevel, ordered)), + parseNext: null + }; +} + +function orderedListStart(content: Content, level: number) { + for (const element of content) { + if ( + typeof element !== 'string' && + element.type === 'start_milestone' && + element.subtype === `usfm:zon${level}` && + element.atts?.start + ) { + return element.atts.start[0]; + } else if (isWrapper(element)) { + const start = orderedListStart(element.content, level); + if (start) { + return start; + } + } + } + return '1'; +} + +/** + * Remove \zon milestones from the given content + * + * @param content Content to be filtered + * @param level The list level of the \zon elements + * @param start Whether to remove start milestones instead of end milestones + * @returns The content with \zon milestones removed + */ +function removeOrderedListMarkers(content: Content, level: number, start: boolean): Content { + const milestonType = start ? 'start_milestone' : 'end_milestone'; + return content + .filter( + (element) => + typeof element === 'string' || + element.type !== milestonType || + element.subtype !== `usfm:zon${level}` + ) + .map((element) => + isWrapper(element) + ? replaceContent(element, removeOrderedListMarkers(element.content, level, start)) + : element + ); +} + +function convertListLevel(content: Content, level: number, ordered: boolean) { + content = removeOrderedListMarkers(content, level, false); + const items = listItems(content, level, ordered); + const label = items[0]; + const bullets = items + .slice(1) + .map((item) => convertNestedListContent(item, level + 1, ordered)) + // Clean up nested \zon start milestones + .map((item) => removeOrderedListMarkers(item, level + 1, true)) + // Clean up empty list items + .filter(hasTextContent) + .map((item) => listBulletParagraph(item, level, ordered)); + const sequence = ordered + ? { + type: 'ordered_list', + start: orderedListStart(items[0], level), + blocks: bullets + } + : { + type: 'list', + blocks: bullets + }; + const bulletsGraft: Graft = { + type: 'graft', + new: false, + sequence + }; + return { label, bullets: bulletsGraft }; +} + +function hasTextContent(content: Content): boolean { + const text = content.find( + (element) => + (typeof element === 'string' && element.trim().length) || + (isWrapper(element) && hasTextContent(element.content)) + ); + return text ? true : false; +} + +function listBulletParagraph(itemContent: Content, level: number, ordered: boolean): Paragraph { + return { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: listItemSubtype(level, ordered) + }, + content: itemContent + }; +} + +function convertNestedListContent(content: Content, level: number, ordered: boolean): Content { + const i = content.findIndex((element) => hasItemStart(element, level, ordered)); + if (i < 0) return content; + const { label, bullets } = convertListLevel(content, level, ordered); + return label.concat(bullets); +} + +function convertList(paragraph: Paragraph, ordered: boolean): Paragraph { + const { bullets } = convertListLevel(paragraph.content, 1, ordered); + return { + type: 'paragraph', + subtype: 'list_container', + content: [bullets] + }; +} + +export function maybeConvertList(paragraph: Paragraph): Paragraph { + if (hasList(paragraph.content, 1, false)) { + return convertList(paragraph, false); + } + return paragraph; +} + +export function maybeConvertOrderedList(paragraph: Paragraph): Paragraph { + if (hasList(paragraph.content, 1, true)) { + return convertList(paragraph, true); + } + return paragraph; +} diff --git a/src/lib/components/sofria-render-json/proskomma-tools/convert-paragraph.ts b/src/lib/components/sofria-render-json/proskomma-tools/convert-paragraph.ts new file mode 100644 index 000000000..cb027735e --- /dev/null +++ b/src/lib/components/sofria-render-json/proskomma-tools/convert-paragraph.ts @@ -0,0 +1,102 @@ +import { + type Block, + type ContentElement, + isContentModifier, + isNestedDocument, + isParagraph, + type Content, + type Document, + type Paragraph, + isWrapper, + type Wrapper +} from '../schema/sofria-schema'; +import { maybeConvertList, maybeConvertOrderedList } from './convert-lists'; + +/** + * Return a copy of the wrapper element with different content + */ +export function replaceContent(wrapper: Wrapper, content: Content) { + const copy = JSON.parse(JSON.stringify(wrapper)) as Wrapper; + copy.content = content; + return copy; +} + +export function removeMarks(content: Content): Content { + return content + .filter((element) => typeof element == 'string' || element.type !== 'mark') + .map((element) => + isWrapper(element) && element.content + ? replaceContent(element, removeMarks(element.content)) + : element + ); +} + +export function convertHeading(paragraph: Paragraph) { + if (paragraph.subtype.match(/usfm:\w+1/)) { + paragraph.subtype = paragraph.subtype.slice(0, -1); + } + return paragraph; +} + +function isParagraphMilestone(element: ContentElement, allowEnd: boolean = false) { + const types = ['start_milestone']; + if (allowEnd) types.push('end_milestone'); + return ( + isContentModifier(element) && + types.includes(element.type) && + element.subtype === 'usfm:zusfm' + ); +} + +function paragraphTypeByMilestone(content: Content): string | null { + for (const element of content) { + if (isContentModifier(element)) { + if (isParagraphMilestone(element)) { + return element.atts.class[0]; + } + if (element.content) { + const nestedType = paragraphTypeByMilestone(element.content); + if (nestedType) return nestedType; + } + } + } + return null; +} + +function removeParagraphMilestones(content: Content): Content { + const filtered = content.filter((e) => !isParagraphMilestone(e, true)); + for (const element of filtered) { + if (isContentModifier(element) && element.content) { + element.content = removeParagraphMilestones(element.content); + } + } + return filtered; +} + +export function convertParagraphType(paragraph: Paragraph): Paragraph { + const paragraphType = paragraphTypeByMilestone(paragraph.content); + return { + type: 'paragraph', + subtype: `usfm:${paragraphType}`, + content: removeParagraphMilestones(paragraph.content) + }; +} + +function convertStorybookBlock(block: Block) { + if (isParagraph(block)) { + block.content = removeMarks(block.content); + block = convertParagraphType(block as Paragraph); + block = convertHeading(block as Paragraph); + block = maybeConvertList(block as Paragraph); + block = maybeConvertOrderedList(block as Paragraph); + } + return block; +} + +export function convertStorybook(sofria: Document) { + if (isNestedDocument(sofria)) { + sofria.sequence.blocks = sofria.sequence.blocks.map(convertStorybookBlock); + return sofria; + } + throw new Error(`Document structure not supported: ${sofria.schema.structure}`); +} diff --git a/src/lib/components/sofria-render-json/proskomma-tools/pk-query.ts b/src/lib/components/sofria-render-json/proskomma-tools/pk-query.ts new file mode 100644 index 000000000..3b2fad332 --- /dev/null +++ b/src/lib/components/sofria-render-json/proskomma-tools/pk-query.ts @@ -0,0 +1,58 @@ +import type { SABProskomma } from '$lib/sab-proskomma'; +import type { Document } from '../schema/sofria-schema'; + +export async function sofriaRaw( + proskomma: SABProskomma, + docSet: string, + book: string, + chapter: number +): Promise { + const pkDocuments = await proskomma.gqlQuery(`{ + docSet(id: "${docSet}") { + documents { + id + idParts { + parts + } + } + } + }`); + + if (!pkDocuments.data) { + throw new Error( + `Could not retrieve documents for docset ${docSet}\n` + + `Proskomma response: ${JSON.stringify(pkDocuments)}` + ); + } + + if (!pkDocuments.data.docSet) { + throw new Error( + `Could not load docset ${docSet}\n` + + `Proskomma response: ${JSON.stringify(pkDocuments)}` + ); + } + + const document = pkDocuments.data.docSet.documents.find((doc) => + doc.idParts.parts.includes(book) + ); + + if (!document) { + throw new Error(`Could not find book ${book} in docset ${docSet}`); + } + + const pageContent = await proskomma.gqlQuery(`{ + document(id: "${document.id}") { + sofria(chapter: ${chapter}) + } + }`); + + if (!pageContent.data) { + throw new Error( + `Could not retrieve sofria for chapter ${chapter} ` + + `of document '${docSet}' (id: ${document.id})\n` + + `Proskomma response: ${JSON.stringify(pageContent)}` + ); + } + + return JSON.parse(pageContent.data.document.sofria); +} diff --git a/src/lib/components/sofria-render-json/proskomma-tools/test/convert-lists.test.ts b/src/lib/components/sofria-render-json/proskomma-tools/test/convert-lists.test.ts new file mode 100644 index 000000000..e1240c31f --- /dev/null +++ b/src/lib/components/sofria-render-json/proskomma-tools/test/convert-lists.test.ts @@ -0,0 +1,1271 @@ +import { describe, expect, test } from 'vitest'; +import { maybeConvertList, maybeConvertOrderedList } from '../convert-lists'; +import type { Paragraph } from '../../schema/sofria-schema'; + +describe('convertUnorderedLists', () => { + test('preserves normal paragraph', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:m', + content: [ + { + type: 'start_milestone', + subtype: 'usfm:zusfm', + atts: { class: ['ms2'] } + }, + 'Hello', + 'world', + { type: 'start_milestone' }, + { type: 'end_milestone' }, + { type: 'wrapper' }, + { type: 'end_milestone', subtype: 'usfm:zusfm' } + ] + }; + expect(maybeConvertList(testParagraph)).toEqual(testParagraph); + }); + + test('flat list', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:m', + content: [ + { + type: 'start_milestone', + subtype: 'usfm:zuli1', + atts: { unknownDefault_zuli1: [''] } + }, + 'One ', + { type: 'end_milestone', subtype: 'usfm:zuli1' }, + ' ', + { + type: 'start_milestone', + subtype: 'usfm:zuli1', + atts: { unknownDefault_zuli1: [''] } + }, + ' Two ', + { type: 'end_milestone', subtype: 'usfm:zuli1' }, + ' ', + { + type: 'start_milestone', + subtype: 'usfm:zuli1', + atts: { unknownDefault_zuli1: [''] } + }, + ' Three', + { type: 'end_milestone', subtype: 'usfm:zuli1' } + ] + }; + const transformed = { + type: 'paragraph', + subtype: 'list_container', + content: [ + { + type: 'graft', + new: false, + sequence: { + type: 'list', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zuli1' + }, + content: ['One '] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zuli1' + }, + content: [' ', ' Two '] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zuli1' + }, + content: [' ', ' Three'] + } + ] + } + } + ] + }; + expect(maybeConvertList(testParagraph)).toEqual(transformed); + }); + + test('flat list with wrappers', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:m', + content: [ + { + type: 'wrapper', + subtype: 'test', + content: [ + { + type: 'start_milestone', + subtype: 'usfm:zuli1', + atts: { unknownDefault_zuli1: [''] } + }, + 'One', + { type: 'end_milestone', subtype: 'usfm:zuli1' } + ] + }, + { + type: 'wrapper', + subtype: 'style1', + atts: { value: '1' }, + content: [ + { + type: 'wrapper', + subtype: 'style2', + content: [ + ' ', + { + type: 'start_milestone', + subtype: 'usfm:zuli1', + atts: { unknownDefault_zuli1: [''] } + }, + 'Hello ', + { + type: 'wrapper', + subtype: 'style3', + content: [ + ' World! ', + { type: 'end_milestone', subtype: 'usfm:zuli1' }, + { + type: 'start_milestone', + subtype: 'usfm:zuli1', + atts: { unknownDefault_zuli1: [''] } + }, + 'Three', + { type: 'end_milestone', subtype: 'usfm:zuli1' }, + 'Number ' + ] + }, + { + type: 'start_milestone', + subtype: 'usfm:zuli1', + atts: { unknownDefault_zuli1: [''] } + }, + 'Four', + { type: 'end_milestone', subtype: 'usfm:zuli1' } + ] + } + ] + } + ] + }; + const transformed = { + type: 'paragraph', + subtype: 'list_container', + content: [ + { + type: 'graft', + new: false, + sequence: { + type: 'list', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zuli1' + }, + content: [ + { + type: 'wrapper', + subtype: 'test', + content: ['One'] + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zuli1' + }, + content: [ + { + type: 'wrapper', + subtype: 'style1', + atts: { value: '1' }, + content: [ + { + type: 'wrapper', + subtype: 'style2', + content: [ + ' ', + 'Hello ', + { + type: 'wrapper', + subtype: 'style3', + content: [' World! '] + } + ] + } + ] + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zuli1' + }, + content: [ + { + type: 'wrapper', + subtype: 'style1', + atts: { value: '1' }, + content: [ + { + type: 'wrapper', + subtype: 'style2', + content: [ + { + type: 'wrapper', + subtype: 'style3', + content: ['Three'] + } + ] + } + ] + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zuli1' + }, + content: [ + { + type: 'wrapper', + subtype: 'style1', + atts: { value: '1' }, + content: [ + { + type: 'wrapper', + subtype: 'style2', + content: [ + { + type: 'wrapper', + subtype: 'style3', + content: ['Number '] + }, + 'Four' + ] + } + ] + } + ] + } + ] + } + } + ] + }; + expect(maybeConvertList(testParagraph)).toEqual(transformed); + }); + + test('multilevel list', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:m', + content: [ + { + type: 'start_milestone', + subtype: 'usfm:zuli1' + }, + 'Old Testament', + { + type: 'start_milestone', + subtype: 'usfm:zuli2' + }, + 'Pentateuch', + { + type: 'start_milestone', + subtype: 'usfm:zuli3' + }, + 'Genesis', + { type: 'end_milestone', subtype: 'usfm:zuli3' }, + { + type: 'start_milestone', + subtype: 'usfm:zuli3' + }, + 'Exodus', + { type: 'end_milestone', subtype: 'usfm:zuli3' }, + { type: 'end_milestone', subtype: 'usfm:zuli2' }, + { + type: 'start_milestone', + subtype: 'usfm:zuli2' + }, + 'Joshua', + { type: 'end_milestone', subtype: 'usfm:zuli2' }, + { type: 'end_milestone', subtype: 'usfm:zuli1' }, + { + type: 'start_milestone', + subtype: 'usfm:zuli1' + }, + 'New Testament', + { type: 'end_milestone', subtype: 'usfm:zuli1' } + ] + }; + const transformed = { + type: 'paragraph', + subtype: 'list_container', + content: [ + { + type: 'graft', + new: false, + sequence: { + type: 'list', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { htmlClass: 'zuli1' }, + content: [ + 'Old Testament', + { + type: 'graft', + new: false, + sequence: { + type: 'list', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { htmlClass: 'zuli2' }, + content: [ + 'Pentateuch', + { + type: 'graft', + new: false, + sequence: { + type: 'list', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zuli3' + }, + content: ['Genesis'] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zuli3' + }, + content: ['Exodus'] + } + ] + } + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { htmlClass: 'zuli2' }, + content: ['Joshua'] + } + ] + } + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { htmlClass: 'zuli1' }, + content: ['New Testament'] + } + ] + } + } + ] + }; + + const converted = maybeConvertList(testParagraph); + expect(converted).toEqual(transformed); + }); + + test('multilevel list with wrappers', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:m', + content: [ + { + type: 'wrapper', + subtype: 'some_style', + content: [ + { + type: 'start_milestone', + subtype: 'usfm:zuli1' + }, + 'Old Testament', + { + type: 'start_milestone', + subtype: 'usfm:zuli2' + }, + 'Pentateuch', + { + type: 'start_milestone', + subtype: 'usfm:zuli3' + }, + 'Genesis', + { type: 'end_milestone', subtype: 'usfm:zuli3' }, + { + type: 'start_milestone', + subtype: 'usfm:zuli3' + }, + 'Exodus', + { type: 'end_milestone', subtype: 'usfm:zuli3' }, + { type: 'end_milestone', subtype: 'usfm:zuli2' }, + { + type: 'start_milestone', + subtype: 'usfm:zuli2' + }, + 'Joshua', + { type: 'end_milestone', subtype: 'usfm:zuli2' }, + { type: 'end_milestone', subtype: 'usfm:zuli1' }, + { + type: 'start_milestone', + subtype: 'usfm:zuli1' + }, + 'New Testament', + { type: 'end_milestone', subtype: 'usfm:zuli1' } + ] + } + ] + }; + const transformed = { + type: 'paragraph', + subtype: 'list_container', + content: [ + { + type: 'graft', + new: false, + sequence: { + type: 'list', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { htmlClass: 'zuli1' }, + content: [ + { + type: 'wrapper', + subtype: 'some_style', + content: ['Old Testament'] + }, + { + type: 'graft', + new: false, + sequence: { + type: 'list', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { htmlClass: 'zuli2' }, + content: [ + { + type: 'wrapper', + subtype: 'some_style', + content: ['Pentateuch'] + }, + { + type: 'graft', + new: false, + sequence: { + type: 'list', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zuli3' + }, + content: [ + { + type: 'wrapper', + subtype: + 'some_style', + content: ['Genesis'] + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zuli3' + }, + content: [ + { + type: 'wrapper', + subtype: + 'some_style', + content: ['Exodus'] + } + ] + } + ] + } + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { htmlClass: 'zuli2' }, + content: [ + { + type: 'wrapper', + subtype: 'some_style', + content: ['Joshua'] + } + ] + } + ] + } + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { htmlClass: 'zuli1' }, + content: [ + { + type: 'wrapper', + subtype: 'some_style', + content: ['New Testament'] + } + ] + } + ] + } + } + ] + }; + + expect(maybeConvertList(testParagraph)).toEqual(transformed); + }); +}); + +describe('convert ordered list', () => { + test('preserves normal paragraph', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:m', + content: [ + { + type: 'start_milestone', + subtype: 'usfm:zusfm', + atts: { class: ['ms2'] } + }, + 'Hello', + 'world', + { type: 'start_milestone' }, + { type: 'end_milestone' }, + { type: 'wrapper' }, + { type: 'end_milestone', subtype: 'usfm:zusfm' } + ] + }; + expect(maybeConvertOrderedList(testParagraph)).toEqual(testParagraph); + }); + + test('flat list', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:m', + content: [ + { + type: 'wrapper', + subtype: 'style', + atts: { color: 'red' }, + content: [ + { + type: 'start_milestone', + subtype: 'usfm:zon1', + atts: { start: ['1'] } + }, + { + type: 'start_milestone', + subtype: 'usfm:zoli1', + atts: { unknownDefault_zoli1: [''] } + }, + 'Apples ', + { type: 'end_milestone', subtype: 'usfm:zoli1' }, + ' ', + { + type: 'start_milestone', + subtype: 'usfm:zoli1', + atts: { unknownDefault_zoli1: [''] } + }, + ' Peaches ', + { type: 'end_milestone', subtype: 'usfm:zoli1' }, + ' ', + { + type: 'start_milestone', + subtype: 'usfm:zoli1', + atts: { unknownDefault_zoli1: [''] } + }, + ' Pumpkin Pie', + { type: 'end_milestone', subtype: 'usfm:zoli1' }, + { type: 'end_milestone', subtype: 'usfm:zon1' } + ] + } + ] + }; + const transformed = { + type: 'paragraph', + subtype: 'list_container', + content: [ + { + type: 'graft', + new: false, + sequence: { + type: 'ordered_list', + start: '1', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zoli1' + }, + content: [ + { + type: 'wrapper', + subtype: 'style', + atts: { color: 'red' }, + content: ['Apples '] + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zoli1' + }, + content: [ + { + type: 'wrapper', + subtype: 'style', + atts: { color: 'red' }, + content: [' ', ' Peaches '] + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zoli1' + }, + content: [ + { + type: 'wrapper', + subtype: 'style', + atts: { color: 'red' }, + content: [' ', ' Pumpkin Pie'] + } + ] + } + ] + } + } + ] + }; + expect(maybeConvertOrderedList(testParagraph)).toEqual(transformed); + }); + + test('flat list with custom start', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:m', + content: [ + { + type: 'wrapper', + subtype: 'style', + atts: { color: 'red' }, + content: [ + { + type: 'start_milestone', + subtype: 'usfm:zon1', + atts: { start: ['3'] } + }, + { + type: 'start_milestone', + subtype: 'usfm:zoli1', + atts: { unknownDefault_zoli1: [''] } + }, + 'Apples ', + { type: 'end_milestone', subtype: 'usfm:zoli1' }, + ' ', + { + type: 'start_milestone', + subtype: 'usfm:zoli1', + atts: { unknownDefault_zoli1: [''] } + }, + ' Peaches ', + { type: 'end_milestone', subtype: 'usfm:zoli1' }, + ' ', + { + type: 'start_milestone', + subtype: 'usfm:zoli1', + atts: { unknownDefault_zoli1: [''] } + }, + ' Pumpkin Pie', + { type: 'end_milestone', subtype: 'usfm:zoli1' }, + { type: 'end_milestone', subtype: 'usfm:zon1' } + ] + } + ] + }; + const transformed = { + type: 'paragraph', + subtype: 'list_container', + content: [ + { + type: 'graft', + new: false, + sequence: { + type: 'ordered_list', + start: '3', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zoli1' + }, + content: [ + { + type: 'wrapper', + subtype: 'style', + atts: { color: 'red' }, + content: ['Apples '] + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zoli1' + }, + content: [ + { + type: 'wrapper', + subtype: 'style', + atts: { color: 'red' }, + content: [' ', ' Peaches '] + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zoli1' + }, + content: [ + { + type: 'wrapper', + subtype: 'style', + atts: { color: 'red' }, + content: [' ', ' Pumpkin Pie'] + } + ] + } + ] + } + } + ] + }; + expect(maybeConvertOrderedList(testParagraph)).toEqual(transformed); + }); + + test('flat list with default start', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:m', + content: [ + { + type: 'wrapper', + subtype: 'style', + atts: { color: 'red' }, + content: [ + { + type: 'start_milestone', + subtype: 'usfm:zon1' + }, + { + type: 'start_milestone', + subtype: 'usfm:zoli1', + atts: { unknownDefault_zoli1: [''] } + }, + 'Apples ', + { type: 'end_milestone', subtype: 'usfm:zoli1' }, + ' ', + { + type: 'start_milestone', + subtype: 'usfm:zoli1', + atts: { unknownDefault_zoli1: [''] } + }, + ' Peaches ', + { type: 'end_milestone', subtype: 'usfm:zoli1' }, + ' ', + { + type: 'start_milestone', + subtype: 'usfm:zoli1', + atts: { unknownDefault_zoli1: [''] } + }, + ' Pumpkin Pie', + { type: 'end_milestone', subtype: 'usfm:zoli1' }, + { type: 'end_milestone', subtype: 'usfm:zon1' } + ] + } + ] + }; + const transformed = { + type: 'paragraph', + subtype: 'list_container', + content: [ + { + type: 'graft', + new: false, + sequence: { + type: 'ordered_list', + start: '1', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zoli1' + }, + content: [ + { + type: 'wrapper', + subtype: 'style', + atts: { color: 'red' }, + content: ['Apples '] + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zoli1' + }, + content: [ + { + type: 'wrapper', + subtype: 'style', + atts: { color: 'red' }, + content: [' ', ' Peaches '] + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zoli1' + }, + content: [ + { + type: 'wrapper', + subtype: 'style', + atts: { color: 'red' }, + content: [' ', ' Pumpkin Pie'] + } + ] + } + ] + } + } + ] + }; + expect(maybeConvertOrderedList(testParagraph)).toEqual(transformed); + }); + + test('multi-level list', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:m', + content: [ + { + type: 'wrapper', + subtype: 'test_style', + atts: { color: 'blue' }, + content: [ + { + type: 'start_milestone', + subtype: 'usfm:zon1', + atts: { start: ['1'] } + }, + { + type: 'start_milestone', + subtype: 'usfm:zoli1', + atts: { unknownDefault_zoli1: [''] } + }, + 'Food', + { + type: 'start_milestone', + subtype: 'usfm:zon2', + atts: { start: ['3'] } + }, + { + type: 'start_milestone', + subtype: 'usfm:zoli2', + atts: { unknownDefault_zoli2: [''] } + }, + 'Fruit', + { + type: 'start_milestone', + subtype: 'usfm:zon3', + atts: { start: ['1'] } + }, + { + type: 'start_milestone', + subtype: 'usfm:zoli3', + atts: { unknownDefault_zoli3: [''] } + }, + { + type: 'wrapper', + subtype: 'usfm:bd', + content: ['Apples'], + atts: {} + }, + { type: 'end_milestone', subtype: 'usfm:zoli3' }, + { + type: 'start_milestone', + subtype: 'usfm:zoli3', + atts: { unknownDefault_zoli3: [''] } + }, + 'Bananas', + { type: 'end_milestone', subtype: 'usfm:zoli3' }, + { type: 'end_milestone', subtype: 'usfm:zon3' }, + { type: 'end_milestone', subtype: 'usfm:zoli2' }, + { + type: 'start_milestone', + subtype: 'usfm:zoli2', + atts: { unknownDefault_zoli2: [''] } + }, + 'Dessert', + { type: 'end_milestone', subtype: 'usfm:zoli2' }, + { type: 'end_milestone', subtype: 'usfm:zon2' }, + { type: 'end_milestone', subtype: 'usfm:zoli1' }, + { + type: 'start_milestone', + subtype: 'usfm:zoli1', + atts: { unknownDefault_zoli1: [''] } + }, + 'Drinks', + { type: 'end_milestone', subtype: 'usfm:zoli1' }, + { type: 'end_milestone', subtype: 'usfm:zon1' } + ] + } + ] + }; + const transformed = { + type: 'paragraph', + subtype: 'list_container', + content: [ + { + type: 'graft', + new: false, + sequence: { + type: 'ordered_list', + start: '1', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zoli1' + }, + content: [ + { + type: 'wrapper', + subtype: 'test_style', + atts: { color: 'blue' }, + content: ['Food'] + }, + { + type: 'graft', + new: false, + sequence: { + type: 'ordered_list', + start: '3', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { htmlClass: 'zoli2' }, + content: [ + { + type: 'wrapper', + subtype: 'test_style', + atts: { color: 'blue' }, + content: ['Fruit'] + }, + { + type: 'graft', + new: false, + sequence: { + type: 'ordered_list', + start: '1', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zoli3' + }, + content: [ + { + type: 'wrapper', + subtype: + 'test_style', + atts: { + color: 'blue' + }, + content: [ + { + type: 'wrapper', + subtype: + 'usfm:bd', + atts: {}, + content: [ + 'Apples' + ] + } + ] + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zoli3' + }, + content: [ + { + type: 'wrapper', + subtype: + 'test_style', + atts: { + color: 'blue' + }, + content: ['Bananas'] + } + ] + } + ] + } + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { htmlClass: 'zoli2' }, + content: [ + { + type: 'wrapper', + subtype: 'test_style', + atts: { color: 'blue' }, + content: ['Dessert'] + } + ] + } + ] + } + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { + htmlClass: 'zoli1' + }, + content: [ + { + type: 'wrapper', + subtype: 'test_style', + atts: { color: 'blue' }, + content: ['Drinks'] + } + ] + } + ] + } + } + ] + }; + + expect(maybeConvertOrderedList(testParagraph)).toEqual(transformed); + }); + + test('Trailing whitespace does not create list item', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:m', + content: [ + { + type: 'wrapper', + subtype: 'chapter', + content: [ + { + type: 'start_milestone', + subtype: 'usfm:zon1', + atts: { start: ['1'] } + }, + { + type: 'start_milestone', + subtype: 'usfm:zoli1', + atts: { unknownDefault_zoli1: [''] } + }, + 'Food ', + { + type: 'start_milestone', + subtype: 'usfm:zon2', + atts: { start: ['1'] } + }, + ' ', + { + type: 'start_milestone', + subtype: 'usfm:zoli2', + atts: { unknownDefault_zoli2: [''] } + }, + ' Fruit ', + { type: 'end_milestone', subtype: 'usfm:zoli2' }, + ' ', + { + type: 'start_milestone', + subtype: 'usfm:zoli2', + atts: { unknownDefault_zoli2: [''] } + }, + ' Dessert ', + { type: 'end_milestone', subtype: 'usfm:zoli2' }, + ' ', + { type: 'end_milestone', subtype: 'usfm:zon2' }, + ' ', + { type: 'end_milestone', subtype: 'usfm:zoli1' }, + ' ', + { + type: 'start_milestone', + subtype: 'usfm:zoli1', + atts: { unknownDefault_zoli1: [''] } + }, + ' Drinks ', + { type: 'end_milestone', subtype: 'usfm:zoli1' }, + { type: 'end_milestone', subtype: 'usfm:zon1' } + ], + atts: { number: '1' } + } + ] + }; + const transformed = { + type: 'paragraph', + subtype: 'list_container', + content: [ + { + type: 'graft', + new: false, + sequence: { + type: 'ordered_list', + start: '1', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { htmlClass: 'zoli1' }, + content: [ + { + type: 'wrapper', + subtype: 'chapter', + content: ['Food ', ' '], + atts: { number: '1' } + }, + { + type: 'graft', + new: false, + sequence: { + type: 'ordered_list', + start: '1', + blocks: [ + { + type: 'paragraph', + subtype: 'list_item', + atts: { htmlClass: 'zoli2' }, + content: [ + { + type: 'wrapper', + subtype: 'chapter', + content: [' Fruit '], + atts: { number: '1' } + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { htmlClass: 'zoli2' }, + content: [ + { + type: 'wrapper', + subtype: 'chapter', + content: [' ', ' Dessert '], + atts: { number: '1' } + } + ] + } + ] + } + } + ] + }, + { + type: 'paragraph', + subtype: 'list_item', + atts: { htmlClass: 'zoli1' }, + content: [ + { + type: 'wrapper', + subtype: 'chapter', + content: [' ', ' Drinks '], + atts: { number: '1' } + } + ] + } + ] + } + } + ] + }; + + expect(maybeConvertOrderedList(testParagraph)).toEqual(transformed); + }); +}); diff --git a/src/lib/components/sofria-render-json/proskomma-tools/test/convert-paragraph.test.ts b/src/lib/components/sofria-render-json/proskomma-tools/test/convert-paragraph.test.ts new file mode 100644 index 000000000..5fa5f29e1 --- /dev/null +++ b/src/lib/components/sofria-render-json/proskomma-tools/test/convert-paragraph.test.ts @@ -0,0 +1,209 @@ +import { describe, expect, test } from 'vitest'; +import { convertHeading, convertParagraphType, removeMarks } from '../convert-paragraph'; +import type { Content, Paragraph } from '../../schema/sofria-schema'; + +describe('removeMarks', () => { + test('with strings', () => { + const testContent: Content = [ + { + type: 'mark', + subtype: 'chapter_label', + atts: { number: '1' } + }, + 'Hello', + 'World' + ]; + expect(removeMarks(testContent)).toEqual(['Hello', 'World']); + }); + + test('with wrapper', () => { + const testContent: Content = [ + { + type: 'mark', + subtype: 'chapter_label', + atts: { number: '1' } + }, + { + type: 'wrapper' + }, + 'Hello', + 'World' + ]; + expect(removeMarks(testContent)).toEqual([ + { + type: 'wrapper' + }, + 'Hello', + 'World' + ]); + }); + + test('removes marks within wrappers', () => { + const testContent: Content = [ + { + type: 'wrapper', + subtype: 'chapter', + content: [ + { + type: 'mark', + subtype: 'chapter_label', + atts: { number: '5' } + }, + 'Hello world!' + ], + atts: { number: '5' } + } + ]; + const transformed = [ + { + type: 'wrapper', + subtype: 'chapter', + content: ['Hello world!'], + atts: { number: '5' } + } + ]; + expect(removeMarks(testContent)).toEqual(transformed); + }); +}); + +describe('convertHeading', () => { + test('does not change regular paragraph', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:m', + content: ['hello', ' ', 'world'] + }; + expect(convertHeading(testParagraph)).toEqual(testParagraph); + }); + + test('converts s1 headings to s', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:s1', + content: ['hello', ' ', 'world'] + }; + const transformed: Paragraph = { + type: 'paragraph', + subtype: 'usfm:s', + content: ['hello', ' ', 'world'] + }; + expect(convertHeading(testParagraph)).toEqual(transformed); + }); + + test('does not convert s11 to s1', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:s11', + content: ['hello', ' ', 'world'] + }; + expect(convertHeading(testParagraph)).toEqual(testParagraph); + }); + + test('converts xxx1 headings to xxx', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:xxx1', + content: ['hello', ' ', 'world'] + }; + const transformed: Paragraph = { + type: 'paragraph', + subtype: 'usfm:xxx', + content: ['hello', ' ', 'world'] + }; + expect(convertHeading(testParagraph)).toEqual(transformed); + }); +}); + +describe('convertParagraphType', () => { + test('flat string content', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:m', + content: [ + { + type: 'start_milestone', + subtype: 'usfm:zusfm', + atts: { class: ['ms2'] } + }, + 'Hello', + 'world', + { type: 'end_milestone', subtype: 'usfm:zusfm' } + ] + }; + const transformed = { + type: 'paragraph', + subtype: 'usfm:ms2', + content: ['Hello', 'world'] + }; + expect(convertParagraphType(testParagraph)).toEqual(transformed); + }); + + test('flat content with non-string elements', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:m', + content: [ + { + type: 'start_milestone', + subtype: 'usfm:zusfm', + atts: { class: ['ms2'] } + }, + 'Hello', + 'world', + { type: 'start_milestone' }, + { type: 'end_milestone' }, + { type: 'wrapper' }, + { type: 'end_milestone', subtype: 'usfm:zusfm' } + ] + }; + const transformed = { + type: 'paragraph', + subtype: 'usfm:ms2', + content: [ + 'Hello', + 'world', + { type: 'start_milestone' }, + { type: 'end_milestone' }, + { type: 'wrapper' } + ] + }; + expect(convertParagraphType(testParagraph)).toEqual(transformed); + }); + + test('nested content', () => { + const testParagraph: Paragraph = { + type: 'paragraph', + subtype: 'usfm:m', + content: [ + { + type: 'wrapper', + subtype: 'chapter', + content: [ + { + type: 'start_milestone', + subtype: 'usfm:zusfm', + atts: { class: ['ms2'] } + }, + 'Hello', + 'world', + { type: 'end_milestone', subtype: 'usfm:zusfm' } + ], + atts: { number: '1' } + } + ] + }; + const transformed = { + type: 'paragraph', + subtype: 'usfm:ms2', + content: [ + { + type: 'wrapper', + subtype: 'chapter', + content: ['Hello', 'world'], + atts: { number: '1' } + } + ] + }; + expect(convertParagraphType(testParagraph)).toEqual(transformed); + }); +}); diff --git a/src/lib/components/sofria-render-json/proskomma-tools/test/pk-query.test.ts b/src/lib/components/sofria-render-json/proskomma-tools/test/pk-query.test.ts new file mode 100644 index 000000000..35e106481 --- /dev/null +++ b/src/lib/components/sofria-render-json/proskomma-tools/test/pk-query.test.ts @@ -0,0 +1,33 @@ +import { SABProskomma } from '$lib/sab-proskomma'; +import { expect, test } from 'vitest'; +import { sofriaRaw } from '../pk-query'; + +const usfm1 = ` +\\id 001 +\\c 1 +\\m Hello +\\c 2 +\\m world +`; + +async function addDocument(pk: SABProskomma, usfm: string) { + await pk.gqlQuery( + `mutation { + addDocument( + selectors: [ + {key: "lang", value: "eng"}, + {key: "abbr", value: "C01"} + ], + contentType: "usfm", + content: """${usfm}""", + ) + }` + ); +} + +test('sofriaRaw', async () => { + const pk = new SABProskomma(); + await addDocument(pk, usfm1); + const sofria = await sofriaRaw(pk, 'eng_C01', '001', 2); + expect(sofria.schema.constraints[0].name).toBe('sofria'); +}); diff --git a/src/lib/components/sofria-render-json/schema/paragraphs.ts b/src/lib/components/sofria-render-json/schema/paragraphs.ts new file mode 100644 index 000000000..f9bb111d0 --- /dev/null +++ b/src/lib/components/sofria-render-json/schema/paragraphs.ts @@ -0,0 +1,24 @@ +import type { Paragraph } from './sofria-schema'; + +export function isUsfmParagraph(paragraph: Paragraph) { + return paragraph.subtype.split(':')[0] === 'usfm'; +} + +export function usfmClass(paragraph: Paragraph) { + return paragraph.subtype.split(':')[1]; +} + +export function isListContainer(paragraph: Paragraph) { + return paragraph.subtype === 'list_container'; +} + +export interface ListItem extends Paragraph { + subtype: 'list_item'; + atts: { + htmlClass: string; + }; +} + +export function isListItem(paragraph: Paragraph): paragraph is ListItem { + return paragraph.subtype === 'list_item'; +} diff --git a/src/lib/components/sofria-render-json/schema/sequences.ts b/src/lib/components/sofria-render-json/schema/sequences.ts new file mode 100644 index 000000000..5826e4619 --- /dev/null +++ b/src/lib/components/sofria-render-json/schema/sequences.ts @@ -0,0 +1,18 @@ +import type { Sequence } from './sofria-schema'; + +export interface ListSequence extends Sequence { + type: 'list'; +} + +export function isListSequence(seq: Sequence): seq is ListSequence { + return seq.type === 'list'; +} + +export interface OrderedListSequence extends Sequence { + type: 'ordered_list'; + start: string; +} + +export function isOrderedListSequence(seq: Sequence): seq is OrderedListSequence { + return seq.type === 'ordered_list'; +} diff --git a/src/lib/components/sofria-render-json/schema/sofria-schema.ts b/src/lib/components/sofria-render-json/schema/sofria-schema.ts new file mode 100644 index 000000000..207691a7b --- /dev/null +++ b/src/lib/components/sofria-render-json/schema/sofria-schema.ts @@ -0,0 +1,111 @@ +// Interfaces defining the standard schema for Sofria +// Based on documentation from the proskomma-json-tools package + +export type Attributes = { [name: string]: boolean | string | string[] }; +export type ContentElement = string | ContentModifier | Graft; +export type Content = ContentElement[]; + +export interface Document { + schema: Schema; + metadata: object; +} + +export interface FlatDocument extends Document { + sequences: { [id: string]: Sequence }; + main_sequence_id: string; +} + +export interface NestedDocument extends Document { + sequence: Sequence; +} + +export function isFlatDocument(doc: Document): doc is FlatDocument { + return doc.schema.structure === 'flat'; +} + +export function isNestedDocument(doc: Document): doc is NestedDocument { + return doc.schema.structure === 'nested'; +} + +export interface Schema { + structure: 'flat' | 'nested'; + structure_version: string; + constraints: { + name: 'perf' | 'sofria'; + version: string; + }; +} + +export interface Sequence { + type: string; + preview_text?: string; + blocks: Block[]; +} + +export interface Block { + type: 'paragraph' | 'graft'; +} + +export interface Paragraph extends Block { + type: 'paragraph'; + subtype: string; + atts?: Attributes; + content: Content; +} + +export function isParagraph(block: Block): block is Paragraph { + return block.type === 'paragraph'; +} + +interface GraftBase extends Block { + type: 'graft'; + new: boolean; + atts?: Attributes; +} + +export interface Graft extends GraftBase { + new: false; + + // One of the following must be defined + target?: string; // The ID of the sequence containing the graft content + sequence?: Sequence; // The sequence containing the graft content + + preview_text?: string; +} + +export function blockIsGraft(block: Block): block is Graft { + return block.type === 'graft' && !(block as GraftBase).new; +} + +export function contentIsGraft(element: ContentElement): element is Graft { + return typeof element !== 'string' && blockIsGraft(element as Block); +} + +export interface NewGraft extends GraftBase { + new: true; + + // One of the following must be defined + subtype?: string; + sequence?: Sequence; +} + +export interface ContentModifier { + type: 'mark' | 'wrapper' | 'start_milestone' | 'end_milestone'; + subtype?: string; + atts?: Attributes; + content?: Content; + meta_content?: Content; +} + +export function isContentModifier(element: ContentElement): element is ContentModifier { + return typeof element !== 'string' && !contentIsGraft(element); +} + +export interface Wrapper extends ContentModifier { + type: 'wrapper'; + content: Content; +} + +export function isWrapper(element: ContentElement): element is Wrapper { + return typeof element !== 'string' && element.type === 'wrapper'; +} diff --git a/src/lib/components/sofria-render-json/schema/wrappers.ts b/src/lib/components/sofria-render-json/schema/wrappers.ts new file mode 100644 index 000000000..8197feee2 --- /dev/null +++ b/src/lib/components/sofria-render-json/schema/wrappers.ts @@ -0,0 +1,9 @@ +import type { Wrapper } from './sofria-schema'; + +export function makesBold(wrapper: Wrapper) { + return ['usfm:bd', 'usfm:bdit'].includes(wrapper.subtype); +} + +export function makesItalic(wrapper: Wrapper) { + return ['usfm:it', 'usfm:bdit'].includes(wrapper.subtype); +} diff --git a/src/lib/data/analytics.ts b/src/lib/data/analytics.ts index 700e6e75f..933692964 100644 --- a/src/lib/data/analytics.ts +++ b/src/lib/data/analytics.ts @@ -12,8 +12,9 @@ export function getBook(item: { collection?: string; book: string }) { function getDamId(item: { book: any; chapter: string }) { let damId; if (item.book.audio.length > 0) { + // TODO (Garrett Jones): In a storybook, audio may come from a previous chapter. const audio = item.book.audio.find((x) => x.num === Number(item.chapter)); - const source = audio.src; + const source = audio?.src; if (source) { if (config.audio.sources[source]?.type === 'fcbh') { damId = config.audio.sources[source].damId; diff --git a/src/lib/data/navigation.test.ts b/src/lib/data/navigation.test.ts index 86aa596e6..bd068ce5a 100644 --- a/src/lib/data/navigation.test.ts +++ b/src/lib/data/navigation.test.ts @@ -83,6 +83,51 @@ const config = { features: {} } ] + }, + { + id: 'C03', + languageCode: 'eng', + collectionName: 'World English Bible', + collectionAbbreviation: 'web', + books: [ + { + chapters: 3, + chaptersN: '1-3', + fonts: [], + id: 'MAT', + name: 'Matthew', + section: 'Gospel', + testament: 'NT', + abbreviation: 'Mat' + }, + { + chapters: 9, + chaptersN: '1-9', + fonts: [], + id: '001', + type: 'story', + name: 'Unmerciful Servant', + abbreviation: 'US', + audio: [ + { + num: 1, + filename: 'unmercifulservant.mp3', + len: 145842, + size: 2328449, + src: 'a1', + timingFile: 'C01-01-001-01-timing.txt' + } + ], + file: 'Unmerciful Servant.usfm', + features: { + 'show-chapter-numbers': 'no', + 'lock-orientation': 'none', + 'show-border': 'inherit', + 'audio-goto-next-chapter': 'inherit', + 'story-image-max-height': 44 + } + } + ] } ] }; @@ -179,8 +224,67 @@ const catalog2: CatalogData = { tags: {} }; -function getTestCatalog(docSet: string): Promise { - return Promise.resolve(docSet == 'eng_C01' ? catalog1 : catalog2); +const catalog3: CatalogData = { + id: 'eng_C01', + selectors: { lang: 'eng', abbr: 'C01' }, + hasMapping: false, + documents: [ + { + id: 'MGNjODdlMjIt', + bookCode: 'MAT', + h: 'Matthew', + toc: 'The Good News According to Matthew', + toc2: 'Matthew', + toc3: 'Mat', + sequences: [], + hasIntroduction: false, + versesByChapters: { + 1: { + 1: '1', + 2: '2', + 3: '3' + }, + 2: { + 1: '1', + 2: '2', + 3: '3', + 4: '4' + }, + 3: { + 1: '1', + 2: '2' + } + } + }, + { + id: 'MTg4NTk2ODUt', + bookCode: '001', + h: null, + toc: null, + toc2: 'Unmerciful Servant', + toc3: null, + sequences: [], + hasIntroduction: false, + versesByChapters: { + '1': {}, + '2': {}, + '3': {}, + '4': {}, + '5': {}, + '6': {}, + '7': {}, + '8': {}, + '9': {} + } + } + ], + tags: {} +}; +async function getTestCatalog(docSet: string): Promise { + if (docSet === 'eng_C01') return catalog1; + if (docSet === 'grc_C02') return catalog2; + if (docSet === 'eng_C03') return catalog3; + throw new Error(`Please provide a test catalog for docset ${docSet}`); } describe('goToInitial', async () => { @@ -369,22 +473,6 @@ describe('goTo', () => { expect(navContext.chapterLength).toBe(1); }); - // test('chapter length is 1 when versesByChapter is empty', async () => { - // // Not sure when this may occur, but old code made provision for it. - // const navContext = new TestNavigationContext(getTestCatalog, config); - // await navContext.goToInitial(); - // await navContext.goTo('grc_C02', 'LUK', '1', '1'); - // expect(navContext.chapterLength).toBe(1); - // }); - - // test('chapter is 1 when versesByChapter is empty', async () => { - // // Not sure when this may occur, but old code made provision for it. - // const navContext = new TestNavigationContext(getTestCatalog, config); - // await navContext.goToInitial(); - // await navContext.goTo('grc_C02', 'LUK', '2', '1'); - // expect(navContext.chapter).toBe('1'); - // }); - describe('collection', () => { test('after initialization', async () => { const navContext = new TestNavigationContext(getTestCatalog, config); @@ -526,7 +614,7 @@ describe('goTo', () => { expect(navContext.chapter).toBe('1'); }); - test('at end of collection do not advane', async () => { + test('at end of collection do not advance', async () => { const navContext = new TestNavigationContext(getTestCatalog, config); await navContext.gotoInitial(); await navContext.goto('eng_C01', 'MRK', '3', '1'); @@ -585,4 +673,21 @@ describe('goTo', () => { expect(navContext.reference).toBe('eng_C01.MAT.2'); }); }); + + describe('bookIsStory', () => { + test('false for Scripture', async () => { + const navContext = new TestNavigationContext(getTestCatalog, config); + await navContext.gotoInitial(); + await navContext.goto('eng_C03', 'MAT', '2'); + expect(navContext.bookIsStory).toBe(false); + }); + + test('true for storybook', async () => { + const navContext = new TestNavigationContext(getTestCatalog, config); + await navContext.gotoInitial(); + await navContext.goto('eng_C03', '001', '2'); + console.log(navContext.book); + expect(navContext.bookIsStory).toBe(true); + }); + }); }); diff --git a/src/lib/data/navigation.ts b/src/lib/data/navigation.ts index 5f6b58356..227e2eb54 100644 --- a/src/lib/data/navigation.ts +++ b/src/lib/data/navigation.ts @@ -14,6 +14,7 @@ export class NavigationContext { chapterVerses: string; verse: string; audio: any; + bookIsStory: boolean; title: string; name: string; catalog: CatalogData; @@ -64,6 +65,7 @@ export class NavigationContext { this.updateHeadings(); this.updateNextPrev(); this.updateReference(); + this.updateIfStory(); } private async updateLocation(docSet: string, book: string, chapter: string, verse: string) { @@ -114,7 +116,7 @@ export class NavigationContext { this.config.bookCollections .find((x) => x.id === this.collection) .books.find((x) => x.id === this.book) - ?.audio.find((x) => String(x.num) === this.chapter); + ?.audio?.find((x) => String(x.num) === this.chapter); } private updateHeadings() { @@ -130,6 +132,13 @@ export class NavigationContext { } } + private updateIfStory() { + this.bookIsStory = + this.config.bookCollections + .find((bc) => bc.id === this.collection) + .books.find((bk) => bk.id === this.book).type === 'story'; + } + private updateNextPrev() { const chapters = Object.keys(this.versesByChatper); const c = chapters.indexOf(this.chapter); diff --git a/src/lib/data/stores/reference.ts b/src/lib/data/stores/reference.ts index 17a57306a..5cb7206f7 100644 --- a/src/lib/data/stores/reference.ts +++ b/src/lib/data/stores/reference.ts @@ -3,7 +3,7 @@ import { derived, writable } from 'svelte/store'; import type { CatalogData } from '../catalogData'; import config from '$lib/data/config'; -interface ReferenceStore { +export interface ReferenceStore { docSet: string; collection: string; book: string; @@ -11,6 +11,7 @@ interface ReferenceStore { verse: string; chapterVerses: string; numVerses: number; + isStory: boolean; hasAudio: any; title: string; name: string; @@ -34,6 +35,7 @@ export const referenceStore = () => { chapterVerses: nav.chapterVerses, numVerses: nav.chapterLength, hasAudio: nav.audio, + isStory: nav.bookIsStory, title: nav.title, name: nav.name, next: nav.next, diff --git a/src/routes/text/+page.svelte b/src/routes/text/+page.svelte index 5f6fefa1e..41eb85b1c 100644 --- a/src/routes/text/+page.svelte +++ b/src/routes/text/+page.svelte @@ -56,6 +56,8 @@ import { goto } from '$app/navigation'; import { onDestroy, onMount, afterUpdate } from 'svelte'; import { navigateToTextChapterInDirection } from '$lib/navigate'; + import StorybookText from '$lib/components/StorybookText.svelte'; + import StorybookImage from '$lib/components/StorybookImage.svelte'; let savedScrollPosition = 0; function saveScrollPosition() { @@ -76,9 +78,11 @@ async function doSwipe(event) { const swipeDirection = event.detail.direction; console.log('SWIPE', swipeDirection); - if (swipeBetweenBooks || - ($refs.prev.book === $refs.book && swipeDirection === 'right') || - ($refs.next.book === $refs.book && swipeDirection === 'left')) { + if ( + swipeBetweenBooks || + ($refs.prev.book === $refs.book && swipeDirection === 'right') || + ($refs.next.book === $refs.book && swipeDirection === 'left') + ) { await navigateToTextChapterInDirection(swipeDirection === 'right' ? -1 : 1); } } @@ -130,11 +134,14 @@ $: showBorderSetting = getFeatureValueBoolean('show-border', $refs.collection, $refs.book); $: showBorder = config.traits['has-borders'] && ($userSettings['show-border'] ?? showBorderSetting); + $: textDirection = config.bookCollections.find((x) => x.id === $refs.collection).style + .textDirection; $: viewSettings = { audioPhraseEndChars: audioPhraseEndChars, bodyFontSize: $bodyFontSize, bodyLineHeight: $bodyLineHeight, bookmarks: $bookmarks, + direction: textDirection, notes: $notes, highlights: $highlights, maxSelections: config.mainFeatures['annotation-max-select'], @@ -406,27 +413,34 @@ {/if}
    -
    +
    -
    -
    -
    +
    +
    +
    + {#if viewSettings.references.isStory} + + {/if}
    - + {#if viewSettings.references.isStory} + + {:else} + + {/if}
    @@ -444,7 +469,8 @@