Skip to content

Convert Storybooks#870

Open
TheNonPirate wants to merge 20 commits intosillsdev:mainfrom
TheNonPirate:feature/convertStorybook
Open

Convert Storybooks#870
TheNonPirate wants to merge 20 commits intosillsdev:mainfrom
TheNonPirate:feature/convertStorybook

Conversation

@TheNonPirate
Copy link
Contributor

@TheNonPirate TheNonPirate commented Jul 21, 2025

Per #596:
This PR is intended to modify storybook files into a form that the Proskomma USFM parser can parse. I took some code from #645.
I've gotten the basics of storybooks with text and images implemented.

Summary by CodeRabbit

  • New Features

    • Added support for chapter-specific illustrations in story-type books, with images displayed above the text and swipe navigation.
    • Enhanced rendering of styled and nested lists, as well as inline styled text spans in scripture views.
    • Improved handling and display of paragraph and character style milestones in scripture content.
    • Introduced new USFM transformations for paragraph and character style tags and list formatting.
    • Extended book configuration to include page illustrations metadata.
  • Bug Fixes

    • Improved support for multi-file story books, including correct chapter marker insertion and illustration handling.
  • Tests

    • Introduced end-to-end tests for Storybook pages using Playwright.
    • Added and updated unit tests for list transformation and storybook parsing.
  • Chores

    • Updated test scripts to run both Playwright and Vitest, and added Playwright as a development dependency.
    • Modified .gitignore to exclude test result files.

Started taking code from PR sillsdev#645. Discovered that storybook files
seem to be modified before the code in convertBooks runs.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 21, 2025

Walkthrough

This update introduces support for chapter-specific illustrations in story-type books, enhances USFM parsing and rendering for various styled and list elements, and adds Playwright-based end-to-end testing. It modifies configuration parsing, Svelte components, test scripts, and introduces new transformation logic for USFM tags, while updating related tests and scripts to accommodate these features.

Changes

Cohort / File(s) Change Summary
BookConfig Type & Config Parsing
config/index.d.ts, convert/convertConfig.ts
Adds an optional pageIllustrations array to BookConfig and updates config parsing to populate this with chapter illustration metadata.
USFM Transformation & Book Conversion
convert/convertBooks.ts
Adds functions to transform paragraph/character style tags and list tags into milestone markup; updates filter application and book type handling, especially for 'story' books.
Storybook Illustration & Layout Rendering
src/routes/text/+page.svelte, src/lib/components/ScriptureViewSofria.svelte
Adds logic to display chapter-specific illustrations for story books, modifies layout, and extends rendering logic for styled milestones and list formatting.
Test Infrastructure & Scripts
.gitignore, convert/runTests.ts, package.json, playwright.config.ts
Adds Playwright for E2E testing, updates test scripts to run both Playwright and Vitest, ignores test results, and configures test runner scripts.
USFM List Transformation Tests
convert/tests/sab/storybook.test.ts
Adds tests for new list transformation logic, verifying milestone conversion for various list structures and inline formatting.
Config Parsing Tests
convert/tests/sab/convertConfigSAB.test.ts
Updates assertions to skip certain checks for books of type 'story'.
Storybook E2E UI Tests
playwright/storybookTest.e2e.ts
Adds Playwright E2E tests for verifying formatting, list styles, headings, alignment, and illustration display in storybook pages.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant SveltePage as +page.svelte
    participant ScriptureView as ScriptureViewSofria.svelte
    participant Config as convertConfig.ts
    participant USFM as convertBooks.ts

    User->>SveltePage: Navigates to story book page
    SveltePage->>Config: Loads BookConfig (with pageIllustrations)
    SveltePage->>SveltePage: Determines bookType, gets current illustration
    SveltePage->>ScriptureView: Renders content with illustration (if story)
    ScriptureView->>USFM: Parses USFM, applies new milestone and list transformations
    USFM-->>ScriptureView: Returns transformed USFM tokens
    ScriptureView->>ScriptureView: Renders styled lists, paragraphs, and inline spans
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • chrisvire

Poem

A rabbit hops through code so bright,
With lists and styles now set just right!
Storybooks gain their artful flair,
Images and tests appear with care.
Milestones march and Playwright runs,
New features shine—reviewing’s fun!
🐇✨

Note

🔌 MCP (Model Context Protocol) integration is now available in Early Access!

Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a25055d and 7763c09.

📒 Files selected for processing (1)
  • convert/runTests.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • convert/runTests.ts
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

for (const book of collection.books) {
let bookConverted = false;
switch (book.type) {
case 'story':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that it will fall to the default case, but I think it would be better to include case 'story': above default:

@chrisvire chrisvire linked an issue Jul 23, 2025 that may be closed by this pull request
@TheNonPirate TheNonPirate marked this pull request as ready for review August 1, 2025 20:47
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
convert/convertBooks.ts (1)

532-533: Add explicit 'story' case above default

As suggested in the previous review, it would be clearer to include case 'story': explicitly above the default: case for better code readability and maintenance.

🧹 Nitpick comments (5)
convert/convertConfig.ts (1)

533-545: Good implementation with minor suggestion for robustness.

The logic correctly extracts image filenames and constructs appropriate paths with collection/book prefixes. The conditional prefix logic based on the presence of an <images> tag is well thought out.

Consider adding error handling for cases where required attributes might be missing:

const imageFileTag = page.getElementsByTagName('image-filename')[0];
if (imageFileTag) {
+   const pageNum = page.attributes.getNamedItem('num')?.value;
+   if (!pageNum) continue; // Skip if page number is missing
    pageIllustrations.push({
-       num: Number(page.attributes.getNamedItem('num')?.value),
+       num: Number(pageNum),
        filename: book.getElementsByTagName('images')[0]
            ? tag.id +
              '-' +
              book.attributes.getNamedItem('id')!.value +
              '-' +
              imageFileTag.innerHTML
            : imageFileTag.innerHTML
    });
}
src/routes/text/+page.svelte (1)

489-494: Consider consolidating swipe gesture logic.

The swipe gesture on the illustration image duplicates the existing doSwipe function logic. This is functional but could lead to maintenance issues if the swipe behavior needs to be modified.

Consider extracting the swipe configuration to a shared constant:

+const swipeConfig = {
+    timeframe: 300,
+    minSwipeDistance: 60,
+    touchAction: 'pan-y'
+};

// Then use it in both places:
use:swipe={swipeConfig}
src/lib/components/ScriptureViewSofria.svelte (2)

2336-2365: Enhance list milestone handling for better maintainability

The list handling logic is functional but has a few areas for improvement:

  1. The padding calculation 2 * match[1] - 1 + 'rem' could result in excessive indentation for deeply nested lists
  2. Consider extracting magic numbers into constants for better maintainability

Consider these improvements:

+                           const LIST_INDENT_BASE = 1.5; // rem
+                           const LIST_INDENT_MULTIPLIER = 1.5; // rem per level
                            let match;
                            if ((match = element.subType.match(/^usfm:zon(\d+)$/))) {
                                workspace['level' + match[1] + 'ListNum'] =
                                    element.atts['start'][0];
                            } else if ((match = element.subType.match(/^usfm:zoli(\d+)$/))) {
                                workspace.paragraphDiv.classList.add('list-item');
                                workspace.paragraphDiv.classList.add('list-decimal');
                                workspace.paragraphDiv.classList.add('list-inside');
                                workspace.paragraphDiv.style.counterSet =
                                    'list-item ' + workspace['level' + match[1] + 'ListNum'];
                                workspace['level' + match[1] + 'ListNum']++;

                                workspace.paragraphDiv.style.paddingInlineStart =
-                                   2 * match[1] - 1 + 'rem';
+                                   LIST_INDENT_BASE + (match[1] - 1) * LIST_INDENT_MULTIPLIER + 'rem';
                            } else if ((match = element.subType.match(/^usfm:zuli(\d+)$/))) {
                                workspace.paragraphDiv.classList.add('list-item');
                                workspace.paragraphDiv.classList.add('list-inside');

                                workspace.paragraphDiv.style.paddingInlineStart =
-                                   2 * match[1] - 1 + 'rem';
+                                   LIST_INDENT_BASE + (match[1] - 1) * LIST_INDENT_MULTIPLIER + 'rem';
                                if (match[1] === 2) {
                                    workspace.paragraphDiv.classList.add(
                                        '[list-style-type:circle]'
                                    );
                                } else if (match[1] >= 3) {
                                    workspace.paragraphDiv.classList.add(
                                        '[list-style-type:square]'
                                    );
                                }
                            }

2402-2403: Remove debug console.log statements

Debug logging should be conditional or removed for production code.

                                case 'usfm:zcstyle': {
                                    const style = element.atts['id'][0];
-                                   console.log('Element: ');
-                                   console.log(element);
+                                   if (scriptureLogs?.milestone) {
+                                       console.log('Character style milestone:', element);
+                                   }
                                    workspace.encloseInSpanTag = document.createElement('span');
                                    workspace.encloseInSpanTag.classList.add(style);
                                    break;
                                }
convert/convertBooks.ts (1)

78-150: Well-implemented ordered list transformation

The ordered list transformation correctly handles nested lists, start numbers, and inline formatting. The recursive approach is appropriate for handling arbitrary nesting.

Consider adding JSDoc comments to document the complex regex patterns and transformation logic for future maintainability.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8002e5f and 373c260.

📒 Files selected for processing (5)
  • config/index.d.ts (1 hunks)
  • convert/convertBooks.ts (6 hunks)
  • convert/convertConfig.ts (3 hunks)
  • src/lib/components/ScriptureViewSofria.svelte (3 hunks)
  • src/routes/text/+page.svelte (3 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: chrisvire
PR: sillsdev/appbuilder-pwa#808
File: convert/convertMarkdown.ts:216-222
Timestamp: 2025-05-07T13:16:04.481Z
Learning: The USFM attribute encoding workflow in appbuilder-pwa has two distinct steps: (1) Markdown to USFM conversion in convertMarkdown.ts should NOT encode attributes like title or href, (2) The encoding of these attributes should happen only in the convertBooks.ts process via the encodeJmpLinks function to prevent double-encoding.
Learnt from: chrisvire
PR: sillsdev/appbuilder-pwa#808
File: convert/convertMarkdown.ts:216-222
Timestamp: 2025-05-07T13:16:04.481Z
Learning: All USFM attribute encoding (href, title, etc.) should happen in the convertBooks process via the encodeJmpLinks function, not during the markdown to USFM conversion in convertMarkdown.ts.
📚 Learning: in svelte 5, the `page` store is imported from `$app/state` instead of `$app/stores`. this is part o...
Learnt from: chrisvire
PR: sillsdev/appbuilder-pwa#845
File: src/routes/text/+page.svelte:3-3
Timestamp: 2025-07-04T18:51:27.821Z
Learning: In Svelte 5, the `page` store is imported from `$app/state` instead of `$app/stores`. This is part of the migration from Svelte 4 to Svelte 5 where `$app/stores` is being deprecated in favor of `$app/state`.

Applied to files:

  • src/routes/text/+page.svelte
📚 Learning: in sveltekit 2.12 and svelte 5, `$app/stores` was deprecated in favor of `$app/state`. when migratin...
Learnt from: chrisvire
PR: sillsdev/appbuilder-pwa#845
File: src/routes/text/+page.svelte:3-3
Timestamp: 2025-07-04T18:51:27.821Z
Learning: In SvelteKit 2.12 and Svelte 5, `$app/stores` was deprecated in favor of `$app/state`. When migrating from Svelte 4 to Svelte 5, imports should be changed from `import { page } from '$app/stores';` to `import { page } from '$app/state';`. This change aligns with Svelte 5's reactivity model and provides more granular control over state updates.

Applied to files:

  • src/routes/text/+page.svelte
📚 Learning: in svelte 5, `$state` creates a reactive rune, not a store. stores are created with functions like `...
Learnt from: chrisvire
PR: sillsdev/appbuilder-pwa#845
File: src/routes/text/+page.svelte:201-209
Timestamp: 2025-07-04T18:53:08.369Z
Learning: In Svelte 5, `$state` creates a reactive rune, not a store. Stores are created with functions like `writable()`, `readable()`, and `derived()`. When using `bind:this` to bind DOM elements, use plain variables rather than reactive runes to ensure direct DOM property access works correctly.

Applied to files:

  • src/routes/text/+page.svelte
📚 Learning: the usfm attribute encoding workflow in appbuilder-pwa has two distinct steps: (1) markdown to usfm ...
Learnt from: chrisvire
PR: sillsdev/appbuilder-pwa#808
File: convert/convertMarkdown.ts:216-222
Timestamp: 2025-05-07T13:16:04.481Z
Learning: The USFM attribute encoding workflow in appbuilder-pwa has two distinct steps: (1) Markdown to USFM conversion in convertMarkdown.ts should NOT encode attributes like title or href, (2) The encoding of these attributes should happen only in the convertBooks.ts process via the encodeJmpLinks function to prevent double-encoding.

Applied to files:

  • src/lib/components/ScriptureViewSofria.svelte
  • convert/convertBooks.ts
📚 Learning: all usfm attribute encoding (href, title, etc.) should happen in the convertbooks process via the en...
Learnt from: chrisvire
PR: sillsdev/appbuilder-pwa#808
File: convert/convertMarkdown.ts:216-222
Timestamp: 2025-05-07T13:16:04.481Z
Learning: All USFM attribute encoding (href, title, etc.) should happen in the convertBooks process via the encodeJmpLinks function, not during the markdown to USFM conversion in convertMarkdown.ts.

Applied to files:

  • src/lib/components/ScriptureViewSofria.svelte
  • convert/convertBooks.ts
🔇 Additional comments (10)
config/index.d.ts (1)

75-79: LGTM! Well-structured type definition for page illustrations.

The addition of the optional pageIllustrations property to BookConfig is well-designed. The structure with num and filename fields provides a clear mapping between chapter numbers and illustration files, which aligns perfectly with the story book enhancement objectives.

convert/convertConfig.ts (2)

521-521: LGTM! Proper initialization of pageIllustrations array.

The initialization follows the established pattern in the codebase for collecting page-specific data.


728-729: LGTM! Proper inclusion of pageIllustrations in book object.

The pageIllustrations property is correctly added to the book object, maintaining consistency with the TypeScript definition.

src/routes/text/+page.svelte (2)

105-109: LGTM! Clean derivation of book type.

The bookType derived value correctly extracts the book type from the configuration, following Svelte's reactivity patterns.


497-560: LGTM! Well-structured layout changes.

The restructuring to use flexbox with a conditional illustration above the scrollable content is well-implemented. The grow class ensures the scrollable area takes up remaining space, and existing functionality like borders, scroll position saving, and navigation buttons are preserved.

convert/convertBooks.ts (5)

43-45: LGTM!

The paragraph style tag transformation correctly converts \p_style tags to milestone markers with appropriate id attributes.


52-56: LGTM!

Clean orchestration function that properly delegates list transformations.


61-76: LGTM!

The unordered list transformation correctly handles nested lists with dynamic level processing and appropriate milestone markers.


898-913: LGTM! Story book handling is well-implemented

The special handling for story books correctly treats the first file as metadata and subsequent files as chapter pages, with appropriate chapter marker insertion.


386-388: Filter functions properly integrated

The new transformation functions are correctly added to the USFM filter pipeline in the appropriate order.

Comment on lines +459 to +468
if (workspace.encloseInSpanTag) {
const textNode = workspace.phraseDiv.firstChild;
workspace.encloseInSpanTag.appendChild(textNode);
workspace.phraseDiv.innerHTML = workspace.encloseInSpanTag;
workspace.phraseDiv.replaceChild(
workspace.encloseInSpanTag,
workspace.phraseDiv.firstChild
);
workspace.encloseInSpanTag = undefined;
} //Enclose the text in a previously-created span tag
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential null reference issue in span wrapping logic

The code attempts to wrap text in a span tag but has several issues:

  1. Line 462 incorrectly assigns the span element as innerHTML instead of appending it as a child element
  2. The subsequent replaceChild call (lines 463-466) attempts to replace the first child that was just set as innerHTML
  3. No null checks for workspace.phraseDiv.firstChild before manipulation

Apply this fix to properly wrap the text node:

                if (workspace.encloseInSpanTag) {
                    const textNode = workspace.phraseDiv.firstChild;
-                   workspace.encloseInSpanTag.appendChild(textNode);
-                   workspace.phraseDiv.innerHTML = workspace.encloseInSpanTag;
-                   workspace.phraseDiv.replaceChild(
-                       workspace.encloseInSpanTag,
-                       workspace.phraseDiv.firstChild
-                   );
+                   if (textNode) {
+                       workspace.encloseInSpanTag.appendChild(textNode);
+                       workspace.phraseDiv.replaceChild(
+                           workspace.encloseInSpanTag,
+                           textNode
+                       );
+                   }
                    workspace.encloseInSpanTag = undefined;
                } //Enclose the text in a previously-created span tag
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (workspace.encloseInSpanTag) {
const textNode = workspace.phraseDiv.firstChild;
workspace.encloseInSpanTag.appendChild(textNode);
workspace.phraseDiv.innerHTML = workspace.encloseInSpanTag;
workspace.phraseDiv.replaceChild(
workspace.encloseInSpanTag,
workspace.phraseDiv.firstChild
);
workspace.encloseInSpanTag = undefined;
} //Enclose the text in a previously-created span tag
if (workspace.encloseInSpanTag) {
const textNode = workspace.phraseDiv.firstChild;
if (textNode) {
workspace.encloseInSpanTag.appendChild(textNode);
workspace.phraseDiv.replaceChild(
workspace.encloseInSpanTag,
textNode
);
}
workspace.encloseInSpanTag = undefined;
} //Enclose the text in a previously-created span tag
🤖 Prompt for AI Agents
In src/lib/components/ScriptureViewSofria.svelte lines 459 to 468, the code
incorrectly assigns a span element to innerHTML and attempts to replace a child
node without null checks, risking runtime errors. To fix this, remove the
innerHTML assignment, ensure workspace.phraseDiv.firstChild is not null before
manipulation, and properly append or replace child nodes by directly
manipulating DOM elements rather than using innerHTML.

Comment on lines +369 to +378
function getCurrentIllustrationFile() {
let illustrations = config?.bookCollections
.find((x) => x.id === $refs.collection)
.books.find((x) => x.id === $refs.book)?.pageIllustrations;
for (let i = 0; i < illustrations.length; i++) {
if (illustrations[i].num === Number($refs.chapter)) {
return base + '/illustrations/' + illustrations[i].filename;
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical: Handle undefined illustrations array.

The function will throw a runtime error if pageIllustrations is undefined or null, which is likely for books that don't have illustrations.

function getCurrentIllustrationFile() {
    let illustrations = config?.bookCollections
        .find((x) => x.id === $refs.collection)
        .books.find((x) => x.id === $refs.book)?.pageIllustrations;
+   if (!illustrations || illustrations.length === 0) {
+       return null;
+   }
    for (let i = 0; i < illustrations.length; i++) {
        if (illustrations[i].num === Number($refs.chapter)) {
            return base + '/illustrations/' + illustrations[i].filename;
        }
    }
+   return null; // No illustration found for this chapter
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getCurrentIllustrationFile() {
let illustrations = config?.bookCollections
.find((x) => x.id === $refs.collection)
.books.find((x) => x.id === $refs.book)?.pageIllustrations;
for (let i = 0; i < illustrations.length; i++) {
if (illustrations[i].num === Number($refs.chapter)) {
return base + '/illustrations/' + illustrations[i].filename;
}
}
}
function getCurrentIllustrationFile() {
let illustrations = config?.bookCollections
.find((x) => x.id === $refs.collection)
.books.find((x) => x.id === $refs.book)?.pageIllustrations;
if (!illustrations || illustrations.length === 0) {
return null;
}
for (let i = 0; i < illustrations.length; i++) {
if (illustrations[i].num === Number($refs.chapter)) {
return base + '/illustrations/' + illustrations[i].filename;
}
}
return null; // No illustration found for this chapter
}
🤖 Prompt for AI Agents
In src/routes/text/+page.svelte around lines 369 to 378, the function
getCurrentIllustrationFile assumes pageIllustrations is always defined, which
can cause runtime errors if it is undefined or null. To fix this, add a check to
ensure pageIllustrations exists before iterating over it. If it is undefined or
null, handle that case gracefully by returning undefined or an appropriate
fallback value.

Comment on lines +483 to +496
<div class="flex flex-col overflow-y-auto">
{#if bookType === 'story'}
<!-- svelte-ignore a11y_missing_attribute -->
<img
src={getCurrentIllustrationFile()}
class="w-screen object-cover"
use:swipe={{
timeframe: 300,
minSwipeDistance: 60,
touchAction: 'pan-y'
}}
onswipe={doSwipe}
/>
{/if}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify image rendering with undefined src.

The image element should handle cases where getCurrentIllustrationFile() returns null or undefined to prevent broken image displays.

Consider adding a conditional check:

{#if bookType === 'story'}
+   {@const illustrationFile = getCurrentIllustrationFile()}
+   {#if illustrationFile}
        <!-- svelte-ignore a11y_missing_attribute -->
        <img
-           src={getCurrentIllustrationFile()}
+           src={illustrationFile}
            class="w-screen object-cover"
            use:swipe={{
                timeframe: 300,
                minSwipeDistance: 60,
                touchAction: 'pan-y'
            }}
            onswipe={doSwipe}
        />
+   {/if}
{/if}

🏁 Script executed:

#!/bin/bash
# Search for similar image handling patterns in the codebase
ast-grep --pattern $'<img
  $$$
  src={$_}
  $$$
/>'

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Locate the definition and all usages of getCurrentIllustrationFile to verify its return values
rg -n "getCurrentIllustrationFile" -C3 .

Length of output: 1096


🏁 Script executed:

#!/bin/bash
# Display the full body of getCurrentIllustrationFile to determine its return behavior
sed -n '369,400p' src/routes/text/+page.svelte

Length of output: 1493


Guard image rendering when illustrationFile is undefined

To avoid broken images and potential runtime errors, assign the result of getCurrentIllustrationFile() to a local variable and only render the <img> when it’s truthy.

• File: src/routes/text/+page.svelte (around line 483)
• Steps:

  1. Extract illustrationFile: {@const illustrationFile = getCurrentIllustrationFile()}
  2. Wrap the <img> in {#if illustrationFile}{/if}

Suggested patch:

{#if bookType === 'story'}
+   {@const illustrationFile = getCurrentIllustrationFile()}
+   {#if illustrationFile}
        <!-- svelte-ignore a11y_missing_attribute -->
        <img
-           src={getCurrentIllustrationFile()}
+           src={illustrationFile}
            class="w-screen object-cover"
            use:swipe={{
                timeframe: 300,
                minSwipeDistance: 60,
                touchAction: 'pan-y'
            }}
            onswipe={doSwipe}
        />
+   {/if}
{/if}

You may also harden getCurrentIllustrationFile() against missing data:

 function getCurrentIllustrationFile() {
-    let illustrations = config?.bookCollections
+    const illustrations = config?.bookCollections
         .find(x => x.id === $refs.collection)
         .books.find(x => x.id === $refs.book)
         ?.pageIllustrations;
+   if (!illustrations) return;
     for (let i = 0; i < illustrations.length; i++) {
       if (illustrations[i].num === Number($refs.chapter)) {
         return base + '/illustrations/' + illustrations[i].filename;
       }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="flex flex-col overflow-y-auto">
{#if bookType === 'story'}
<!-- svelte-ignore a11y_missing_attribute -->
<img
src={getCurrentIllustrationFile()}
class="w-screen object-cover"
use:swipe={{
timeframe: 300,
minSwipeDistance: 60,
touchAction: 'pan-y'
}}
onswipe={doSwipe}
/>
{/if}
<div class="flex flex-col overflow-y-auto">
{#if bookType === 'story'}
{@const illustrationFile = getCurrentIllustrationFile()}
{#if illustrationFile}
<!-- svelte-ignore a11y_missing_attribute -->
<img
src={illustrationFile}
class="w-screen object-cover"
use:swipe={{
timeframe: 300,
minSwipeDistance: 60,
touchAction: 'pan-y'
}}
onswipe={doSwipe}
/>
{/if}
{/if}
</div>
🤖 Prompt for AI Agents
In src/routes/text/+page.svelte around lines 483 to 496, the image rendering
uses getCurrentIllustrationFile() directly, which can cause broken images if it
returns undefined. To fix this, assign the result of
getCurrentIllustrationFile() to a local variable named illustrationFile using
{@const illustrationFile = getCurrentIllustrationFile()} before the conditional
block, then wrap the <img> element inside an {#if illustrationFile}...{/if}
block so the image only renders when illustrationFile is truthy.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
convert/tests/sab/convertConfigSAB.test.ts (1)

112-116: Consider also asserting the expected emptiness for story books

You now skip the assertions for section, testament, and abbreviation when bkk.type === 'story'.
For extra safety (and clearer documentation of intent) add a short else branch that explicitly checks these fields are undefined/empty for story books.
This prevents silent drift if the parser later starts populating them.

package.json (1)

23-26: Document the combined test runner

"test": "ts-node convert/runTests.ts" now does both e2e and unit tests.
Consider a short comment in the script section (or README) so contributors realise that Playwright runs first and can use --grep etc. through the wrapper.

convert/tests/sab/storybook.test.ts (1)

4-6: Tokenisation hides whitespace-sensitive regressions

tokensOf splits on \\s+, so ordering is preserved but all whitespace is discarded.
If the transformer later introduces unintended line-breaks/indentation you will not detect it.
Consider comparing the raw strings after normalising only leading indentation, or use expect(result.trim()).toBe(expected.trim()).

playwright/storybookTest.e2e.ts (1)

32-36: Prefer toHaveCSS over manual evaluate

Instead of

expect(
    container.evaluate((el) => el.style.getPropertyValue('padding-inline-start'))
).resolves.toBe('1rem');

simply use

await expect(container.first()).toHaveCSS('padding-inline-start', '1rem');

This keeps assertions consistent and avoids mixing locator evaluation with test-runner expectations.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a45467e and a25055d.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • test_data/projects/sab/web_gospels.zip is excluded by !**/*.zip
📒 Files selected for processing (7)
  • .gitignore (1 hunks)
  • convert/runTests.ts (1 hunks)
  • convert/tests/sab/convertConfigSAB.test.ts (1 hunks)
  • convert/tests/sab/storybook.test.ts (1 hunks)
  • package.json (1 hunks)
  • playwright.config.ts (1 hunks)
  • playwright/storybookTest.e2e.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • .gitignore
  • playwright.config.ts
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: chrisvire
PR: sillsdev/appbuilder-pwa#808
File: convert/convertMarkdown.ts:216-222
Timestamp: 2025-05-07T13:16:04.481Z
Learning: The USFM attribute encoding workflow in appbuilder-pwa has two distinct steps: (1) Markdown to USFM conversion in convertMarkdown.ts should NOT encode attributes like title or href, (2) The encoding of these attributes should happen only in the convertBooks.ts process via the encodeJmpLinks function to prevent double-encoding.
Learnt from: chrisvire
PR: sillsdev/appbuilder-pwa#808
File: convert/convertMarkdown.ts:216-222
Timestamp: 2025-05-07T13:16:04.481Z
Learning: All USFM attribute encoding (href, title, etc.) should happen in the convertBooks process via the encodeJmpLinks function, not during the markdown to USFM conversion in convertMarkdown.ts.
📚 Learning: 2025-05-07T13:16:04.481Z
Learnt from: chrisvire
PR: sillsdev/appbuilder-pwa#808
File: convert/convertMarkdown.ts:216-222
Timestamp: 2025-05-07T13:16:04.481Z
Learning: The USFM attribute encoding workflow in appbuilder-pwa has two distinct steps: (1) Markdown to USFM conversion in convertMarkdown.ts should NOT encode attributes like title or href, (2) The encoding of these attributes should happen only in the convertBooks.ts process via the encodeJmpLinks function to prevent double-encoding.

Applied to files:

  • convert/tests/sab/convertConfigSAB.test.ts
🧬 Code Graph Analysis (1)
convert/tests/sab/storybook.test.ts (1)
convert/convertBooks.ts (1)
  • transformLists (52-57)

Comment on lines 3 to 17
let args = '';
for (let i = 2; i < process.argv.length; i++) {
args += process.argv[i];
}
console.log('playwright test ' + args);
spawn('playwright test ' + args, {
shell: true,
stdio: 'inherit'
}).on('close', (code) => {
console.log('vitest ' + args);
spawn('vitest ' + args, {
shell: true,
stdio: 'inherit'
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix CLI-arg handling and propagate failures

Current runner concatenates args without spaces (--run--reporter) and always runs Vitest even if Playwright fails, masking CI errors.

-let args = '';
-for (let i = 2; i < process.argv.length; i++) {
-    args += process.argv[i];
-}
+const args = process.argv.slice(2).join(' ');
-spawn('playwright test ' + args, { … }).on('close', (code) => {
-    console.log('vitest ' + args);
-    spawn('vitest ' + args, { … });
-});
+const pw = spawn(`playwright test ${args}`, { shell: true, stdio: 'inherit' });
+pw.on('close', (code) => {
+    if (code !== 0) process.exit(code);
+    console.log(`vitest ${args}`);
+    const vt = spawn(`vitest ${args}`, { shell: true, stdio: 'inherit' });
+    vt.on('close', (vc) => process.exit(vc));
+});

This preserves spacing, halts on the first failing suite, and forwards the exit code to the parent process.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let args = '';
for (let i = 2; i < process.argv.length; i++) {
args += process.argv[i];
}
console.log('playwright test ' + args);
spawn('playwright test ' + args, {
shell: true,
stdio: 'inherit'
}).on('close', (code) => {
console.log('vitest ' + args);
spawn('vitest ' + args, {
shell: true,
stdio: 'inherit'
});
});
// replace the old args loop with a single join
const args = process.argv.slice(2).join(' ');
console.log('playwright test ' + args);
const pw = spawn(`playwright test ${args}`, {
shell: true,
stdio: 'inherit'
});
pw.on('close', (code) => {
if (code !== 0) process.exit(code);
console.log(`vitest ${args}`);
const vt = spawn(`vitest ${args}`, {
shell: true,
stdio: 'inherit'
});
vt.on('close', (vc) => process.exit(vc));
});
🤖 Prompt for AI Agents
In convert/runTests.ts around lines 3 to 17, the code concatenates CLI arguments
without spaces, causing merged arguments, and it runs Vitest regardless of
Playwright's success, hiding failures. Fix this by joining process.argv elements
with spaces to preserve argument separation, modify the Playwright spawn to
check its exit code, and only run Vitest if Playwright succeeds. Also, ensure
the script exits with the same code as the first failing process to properly
propagate errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Basic storybook - No audio

2 participants