diff --git a/Makefile b/Makefile index 0f059a665..6558eb7e3 100644 --- a/Makefile +++ b/Makefile @@ -10,19 +10,25 @@ endif .PHONY: pipupgrade pipupgrade: checkvenv - pip install --upgrade pip + # pip-tools is not ready for pip 26 + pip install pip==25.2 + # pip install --upgrade pip .PHONY: pyupgrade pyupgrade: checkvenv pipupgrade # checks if pip-tools is installed ifeq ("$(wildcard .venv/bin/pip-compile)","") @echo "Installing Pip-tools..." - @pip install --no-cache-dir pip-tools + # @pip install --no-cache-dir pip-tools + # pip-tools is not ready for pip 26 + @pip install --no-cache-dir pip-tools==7.5.2 endif ifeq ("$(wildcard .venv/bin/pip-sync)","") @echo "Installing Pip-tools..." - @pip install --no-cache-dir pip-tools + # @pip install --no-cache-dir pip-tools + # pip-tools is not ready for pip 26 + @pip install --no-cache-dir pip-tools==7.5.2 endif .PHONY: install-cython diff --git a/backend/doc/domain/resource_lookup.py b/backend/doc/domain/resource_lookup.py index 20878848d..d7fae1a4c 100644 --- a/backend/doc/domain/resource_lookup.py +++ b/backend/doc/domain/resource_lookup.py @@ -48,7 +48,6 @@ from filelock import FileLock from pydantic import HttpUrl, ValidationError - logger = settings.logger(__name__) fetch_source_data_cache: TTLCache[str, SourceData] = TTLCache( @@ -821,24 +820,16 @@ def get_book_codes_for_lang_( manifest_name = book_codes_and_names_localized_from_manifest.get( code, "" ) - if not name and manifest_name: - book_codes_and_names_localized.append( - ( - code, - maybe_correct_book_name( - lang_code, normalize_localized_book_name(manifest_name) - ), - ) - ) - else: - book_codes_and_names_localized.append( - ( - code, - maybe_correct_book_name( - lang_code, normalize_localized_book_name(name) - ), - ) + chosen_name = name or manifest_name + book_codes_and_names_localized.append( + ( + code, + maybe_correct_book_name( + lang_code, + normalize_localized_book_name(chosen_name), + ), ) + ) elif ( use_localized_book_name and len(repo_components) > 2 @@ -862,7 +853,8 @@ def get_book_codes_for_lang_( ( repo_components[1], maybe_correct_book_name( - lang_code, normalize_localized_book_name(book_name_) + lang_code, + normalize_localized_book_name(book_name_), ), ) ) @@ -882,14 +874,22 @@ def get_book_codes_for_lang_( ) logger.debug("book_codes_and_names: %s", book_codes_and_names) logger.debug("book_codes_and_names_localized: %s", book_codes_and_names_localized) - if not book_codes_and_names_localized or any( - name == "" for _, name in book_codes_and_names_localized - ): - unique_values = unique_book_codes(book_codes_and_names) - else: - unique_values = unique_book_codes(book_codes_and_names_localized) + localized_map: dict[str, str] = { + code: name for code, name in book_codes_and_names_localized + } + non_localized_map: dict[str, str] = { + code: name for code, name in book_codes_and_names + } + merged: list[tuple[str, str]] = [] + for code in set(localized_map) | set(non_localized_map): + name = localized_map.get(code, "") + if not name: + name = non_localized_map.get(code, "") + merged.append((code, name)) + unique_values = unique_book_codes(merged) return sorted( - unique_values, key=lambda book_code_and_name: book_id_map[book_code_and_name[0]] + unique_values, + key=lambda book_code_and_name: book_id_map[book_code_and_name[0]], ) @@ -1212,11 +1212,14 @@ def nt_survey_rg_passages( resource_dir: str = settings.EN_RG_DIR, ) -> list[BibleReference]: """ - >>> from doc.domain import resource_lookup - >>> ();rg_books = resource_lookup.nt_survey_rg_passages() ;() # doctest: +ELLIPSIS - (...) - >>> rg_books[0] - BibleReference(book_code='mat', book_name='Matthew', start_chapter=2, start_chapter_verse_ref='1-12', end_chapter=None, end_chapter_verse_ref=None) + Returns the list of all NT RG passages from the docx_file_path, but with + book names localized for language chosen. + + >>> from doc.domain import resource_lookup + >>> ();rg_books = resource_lookup.nt_survey_rg_passages() ;() # doctest: +ELLIPSIS + (...) + >>> rg_books[0] + BibleReference(book_code='mat', book_name='Matthew', start_chapter=2, start_chapter_verse_ref='1-12', end_chapter=None, end_chapter_verse_ref=None) """ path = join(resource_dir, docx_file_path) rg_books = get_rg_books( @@ -1241,6 +1244,7 @@ def nt_survey_rg_passages( maybe_localized_book_name = book_name_map.get( bible_reference.book_code, bible_reference.book_name ) + bible_reference.lang_code = lang_code bible_reference.book_name = maybe_localized_book_name return bible_references @@ -1283,6 +1287,7 @@ def ot_survey_rg1_passages( maybe_localized_book_name = book_name_map.get( bible_reference.book_code, bible_reference.book_name ) + bible_reference.lang_code = lang_code bible_reference.book_name = maybe_localized_book_name return bible_references @@ -1325,6 +1330,7 @@ def ot_survey_rg2_passages( maybe_localized_book_name = book_name_map.get( bible_reference.book_code, bible_reference.book_name ) + bible_reference.lang_code = lang_code bible_reference.book_name = maybe_localized_book_name return bible_references @@ -1367,6 +1373,7 @@ def ot_survey_rg3_passages( maybe_localized_book_name = book_name_map.get( bible_reference.book_code, bible_reference.book_name ) + bible_reference.lang_code = lang_code bible_reference.book_name = maybe_localized_book_name return bible_references @@ -1409,6 +1416,7 @@ def ot_survey_rg4_passages( maybe_localized_book_name = book_name_map.get( bible_reference.book_code, bible_reference.book_name ) + bible_reference.lang_code = lang_code bible_reference.book_name = maybe_localized_book_name return bible_references diff --git a/backend/doc/entrypoints/routes.py b/backend/doc/entrypoints/routes.py index b4f0f308e..426ad3d3d 100644 --- a/backend/doc/entrypoints/routes.py +++ b/backend/doc/entrypoints/routes.py @@ -1,4 +1,4 @@ -from typing import Sequence +from typing import Sequence, cast import celery.states from celery.result import AsyncResult @@ -113,13 +113,17 @@ async def resource_types( Return the list of available resource types tuples for lang_code with book_codes. """ - return resource_lookup.resource_types(lang_code, book_codes) + return cast( + Sequence[tuple[str, str]], resource_lookup.resource_types(lang_code, book_codes) + ) @router.get("/book_codes_for_lang/{lang_code}") async def book_codes_for_lang(lang_code: str) -> Sequence[tuple[str, str]]: """Return list of all available resource codes.""" - return resource_lookup.book_codes_for_lang(lang_code) + return cast( + Sequence[tuple[str, str]], resource_lookup.book_codes_for_lang(lang_code) + ) @router.get("/book_codes_for_lang_from_usfm_only/{lang_code}") @@ -127,7 +131,10 @@ async def book_codes_for_lang_from_usfm_only( lang_code: str, ) -> Sequence[tuple[str, str]]: """Return list of all available resource codes.""" - return resource_lookup.book_codes_for_lang_from_usfm_only(lang_code) + return cast( + Sequence[tuple[str, str]], + resource_lookup.book_codes_for_lang_from_usfm_only(lang_code), + ) @router.get("/chapters_in_books") diff --git a/backend/doc/reviewers_guide/model.py b/backend/doc/reviewers_guide/model.py index 2f1b48100..6c2e5ce51 100644 --- a/backend/doc/reviewers_guide/model.py +++ b/backend/doc/reviewers_guide/model.py @@ -22,6 +22,7 @@ class Part2Item(BaseModel): @final class BibleReference(BaseModel): + lang_code: Optional[str] book_code: str book_name: str start_chapter: ChapterNum @@ -32,6 +33,7 @@ class BibleReference(BaseModel): def __hash__(self: "BibleReference") -> int: return hash( ( + self.lang_code, self.book_code, self.book_name, self.start_chapter, @@ -45,6 +47,7 @@ def __eq__(self: "BibleReference", other: object) -> bool: if not isinstance(other, BibleReference): return NotImplemented return ( + self.lang_code, self.book_code, self.book_name, self.start_chapter, @@ -52,6 +55,7 @@ def __eq__(self: "BibleReference", other: object) -> bool: self.end_chapter, self.end_chapter_verse_ref, ) == ( + other.lang_code, other.book_code, other.book_name, other.start_chapter, diff --git a/backend/doc/reviewers_guide/parser.py b/backend/doc/reviewers_guide/parser.py index 5f570b4b6..557898c14 100644 --- a/backend/doc/reviewers_guide/parser.py +++ b/backend/doc/reviewers_guide/parser.py @@ -147,6 +147,7 @@ def get_ordinal_bible_reference( start_chapter = chapter_verse_components[0] start_chapter_verse_ref = chapter_verse_components[1] bible_reference = BibleReference( + lang_code=None, book_code=book_code, book_name=book_name, start_chapter=int(start_chapter), @@ -176,6 +177,7 @@ def get_bible_reference_spanning_chapter_boundary( chapter_verse_components[2] if len(chapter_verse_components) >= 3 else None ) bible_reference = BibleReference( + lang_code=None, book_code=book_code, book_name=book_name, start_chapter=int(start_chapter), diff --git a/backend/doc/utils/docx_util.py b/backend/doc/utils/docx_util.py index fa3e30783..703a20952 100644 --- a/backend/doc/utils/docx_util.py +++ b/backend/doc/utils/docx_util.py @@ -3,6 +3,7 @@ from docx import Document from docx.document import Document as DocxDocument +from docx.enum.style import WD_STYLE_TYPE from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import RGBColor @@ -217,3 +218,57 @@ def style_superscripts( position = OxmlElement("w:position") position.set(qn("w:val"), str(lift_half_points)) rPr.append(position) + + +def _ensure_character_style_based_on_default( + doc: DocxDocument, + name: str, + *, + color: RGBColor | None = None, + italic: bool | None = None, +) -> None: + styles = doc.styles + if name in styles: + return + style = styles.add_style(name, WD_STYLE_TYPE.CHARACTER) + # ---- Base on Default Paragraph Font ---- + style_elm = style._element + based_on = OxmlElement("w:basedOn") + based_on.set(qn("w:val"), "DefaultParagraphFont") + style_elm.insert(0, based_on) + # ---- Only override what is explicitly requested ---- + font = style.font + if color is not None: + font.color.rgb = color + if italic is not None: + font.italic = italic + + +def ensure_reference_styles( + doc: DocxDocument, + *, + available_color: RGBColor | None = None, + unavailable_color: RGBColor, +) -> None: + """ + Create semantic character styles for passage references. + + AvailableReference: + - Based on Default Paragraph Font + - Usually no overrides (inherits document defaults) + + UnavailableReference: + - Based on Default Paragraph Font + - Lighter color + italics to signal intentional absence + """ + _ensure_character_style_based_on_default( + doc, + "AvailableReference", + color=available_color, # usually None + ) + _ensure_character_style_based_on_default( + doc, + "UnavailableReference", + color=unavailable_color, + italic=True, + ) diff --git a/backend/doc/utils/file_utils.py b/backend/doc/utils/file_utils.py index 3c39b72c3..ecaeb8827 100644 --- a/backend/doc/utils/file_utils.py +++ b/backend/doc/utils/file_utils.py @@ -154,7 +154,9 @@ def epub_filepath( def docx_filepath( - document_request_key: str, output_dir: str = settings.DOCUMENT_OUTPUT_DIR + document_request_key: str, + prefix: str = "", + output_dir: str = settings.DOCUMENT_OUTPUT_DIR, ) -> str: """Given document_request_key, return the docx output file path.""" - return join(output_dir, "{}.docx".format(document_request_key)) + return join(output_dir, "{}{}.docx".format(prefix, document_request_key)) diff --git a/backend/passages/domain/document_generator.py b/backend/passages/domain/document_generator.py index 6981955fb..a55b046b0 100644 --- a/backend/passages/domain/document_generator.py +++ b/backend/passages/domain/document_generator.py @@ -1,13 +1,13 @@ import json import time -from typing import Mapping, Sequence, TYPE_CHECKING +from typing import Mapping, Optional, Sequence, TYPE_CHECKING from celery import current_task from doc.config import settings from doc.domain import worker from doc.domain.bible_books import BOOK_NAMES from doc.domain.email_utils import send_email_with_attachment, should_send_email -from doc.domain.model import Attachment +from doc.domain.model import Attachment, USFMBook from doc.domain.parsing import split_chapter_into_verses, usfm_book_content from doc.domain.resource_lookup import ( book_codes_for_lang_from_usfm_only, @@ -19,19 +19,21 @@ from doc.reviewers_guide.model import BibleReference from doc.utils.file_utils import docx_filepath, file_needs_update from doc.utils.text_utils import maybe_correct_book_name +from doc.utils.docx_util import ensure_reference_styles from docx import Document -from docx.oxml import OxmlElement, parse_xml -from docx.shared import Inches - +from docx.oxml import parse_xml +from docx.shared import Inches, RGBColor +from docx.table import _Cell, _Row from htmldocx import HtmlToDocx # type: ignore -from passages.domain.model import Passage, BibleReference as PassageReference +from passages.domain.model import ( + Passage, + BibleReferenceWithAvailability, +) from passages.domain.parser import verse_text_html from passages.domain.stet_verse_list_parser import BOOK_INDEX, parse_bible_blocks from passages.utils.docx_utils import add_footer, add_header from pydantic import Json -from docx.table import _Cell, _Row - if TYPE_CHECKING: from typing import TypeAlias @@ -44,28 +46,68 @@ logger = settings.logger(__name__) +UNAVAILABLE_COLOR = RGBColor(200, 200, 200) -def generate_docx_document( - lang_code: str, - lang_name: str, - passage_reference_dtos: list[PassageReference], - document_request_key_: str, - docx_filepath_: str, - working_dir: str = settings.WORKING_DIR, - output_dir: str = settings.DOCUMENT_OUTPUT_DIR, - usfm_resource_types: Sequence[str] = settings.USFM_RESOURCE_TYPES, + +def format_reference_suffix(reference: BibleReference) -> str: + if ( + reference.end_chapter + and reference.end_chapter > 0 + and reference.end_chapter_verse_ref + ): + return ( + f"{reference.start_chapter}:" + f"{reference.start_chapter_verse_ref}-" + f"{reference.end_chapter}:" + f"{reference.end_chapter_verse_ref}" + ) + return f"{reference.start_chapter}:{reference.start_chapter_verse_ref}" + + +def get_passages( + bible_references_with_availability: list[BibleReferenceWithAvailability], + usfm_resource_type: str, + usfm_books: list[USFMBook], resource_type_codes_and_names: Mapping[ str, str ] = settings.RESOURCE_TYPE_CODES_AND_NAMES, -) -> str: - """ - Generate the scriptural terms evaluation document. +) -> list[Passage]: + passages: list[Passage] = [] + if not usfm_resource_type: + resource_type_name = "" + else: + resource_type_name = resource_type_codes_and_names[usfm_resource_type] + usfm_book_index: dict[tuple[str, str], USFMBook] = { + (b.book_code, b.resource_type_name): b for b in usfm_books + } + for bible_reference_with_availability in bible_references_with_availability: + reference = bible_reference_with_availability.reference + selected_usfm_book = usfm_book_index.get( + (reference.book_code, resource_type_name) + ) + verse_text_html_ = ( + verse_text_html(reference, selected_usfm_book) if selected_usfm_book else "" + ) + passage = Passage( + reference=reference, + localized_reference=f"{reference.book_name} {format_reference_suffix(reference)}", + passage_text=verse_text_html_, + is_available=bible_reference_with_availability.is_available, + ) + passages.append(passage) + return passages - >>> from passages.domain.document_generator import generate_docx_document - >>> generate_docx_document("en", list[PassageReferenceDto(lang_code="en", book_code="mat", book_name="Matthew", chapter_num=1, verse_reference="3-6"), PassageReferenceDto(lang_code="en", book_code="mat", book_name="Matthew", chapter_num=1, verse_reference="9-10"), PassageReferenceDto(lang_code="en", book_code="mat", book_name="Matthew", chapter_num=1, verse_reference="15")]) - """ + +def get_usfm_books_and_usfm_resource_type( + bible_references_with_availability: list[BibleReferenceWithAvailability], + lang_code: str, + usfm_resource_types: Sequence[str] = settings.USFM_RESOURCE_TYPES, +) -> tuple[list[USFMBook], str]: + # Invariant: book codes are only those that were available from USFM resources book_codes = list( - {passage_ref_dto.book_code for passage_ref_dto in passage_reference_dtos} + dict.fromkeys( + ref.reference.book_code for ref in bible_references_with_availability + ) ) resource_types_ = resource_types(lang_code, ",".join(book_codes)) resource_types_codes = list( @@ -113,105 +155,154 @@ def generate_docx_document( chapter_ ) usfm_books.append(usfm_book) - current_task.update_state(state="Assembling content") - passages = [] - for passage_ref_dto in passage_reference_dtos: - selected_usfm_books = [ - usfm_book_ - for usfm_book_ in usfm_books - if usfm_book_.lang_code == lang_code - and usfm_book_.book_code == passage_ref_dto.book_code - and usfm_book_.resource_type_name - == resource_type_codes_and_names[usfm_resource_type] + return usfm_books, usfm_resource_type + + +def generate_docx_document( + lang0_code: str, + lang0_name: str, + lang1_code: Optional[str], + lang1_name: Optional[str], + bible_references_with_availability: list[BibleReferenceWithAvailability], + document_request_key_: str, + docx_filepath_: str, + working_dir: str = settings.WORKING_DIR, + output_dir: str = settings.DOCUMENT_OUTPUT_DIR, + usfm_resource_types: Sequence[str] = settings.USFM_RESOURCE_TYPES, + book_index: dict[str, int] = BOOK_INDEX, +) -> str: + """Generate the content for the Passages document""" + bible_references_with_availability_lang0: list[BibleReferenceWithAvailability] = [ + b + for b in bible_references_with_availability + if b.reference.lang_code == lang0_code + ] + usfm_books_lang0, usfm_resource_type_lang0 = get_usfm_books_and_usfm_resource_type( + bible_references_with_availability_lang0, lang0_code + ) + bible_references_with_availability_lang1: list[BibleReferenceWithAvailability] = ( + [ + b + for b in bible_references_with_availability + if b.reference.lang_code == lang1_code ] - verse_text_html_ = "" - selected_usfm_book = None - if selected_usfm_books: - selected_usfm_book = selected_usfm_books[0] - if selected_usfm_book: - verse_text_html_ = verse_text_html(passage_ref_dto, selected_usfm_book) - else: - verse_text_html_ = "" - non_book_name_portion_of_reference = "" - if ( - passage_ref_dto.end_chapter - and passage_ref_dto.end_chapter > 0 - and passage_ref_dto.end_chapter_verse_ref - ): - non_book_name_portion_of_reference = f"{passage_ref_dto.start_chapter}:{passage_ref_dto.start_chapter_verse_ref}-{passage_ref_dto.end_chapter}:{passage_ref_dto.end_chapter_verse_ref}" - else: - non_book_name_portion_of_reference = f"{passage_ref_dto.start_chapter}:{passage_ref_dto.start_chapter_verse_ref}" - # NOTE We want the document to show references even if there is no - # content for it. - # localized_reference = ( - # f"{selected_usfm_book.national_book_name} {non_book_name_portion_of_reference}" - # if selected_usfm_book and non_book_name_portion_of_reference - # else passage_ref_dto.start_chapter_verse_ref - # ) - localized_reference = ( - f"{passage_ref_dto.book_name} {non_book_name_portion_of_reference}" - if non_book_name_portion_of_reference - else passage_ref_dto.start_chapter_verse_ref + if lang1_code + else [] + ) + usfm_books_lang1: list[USFMBook] = [] + usfm_resource_type_lang1 = "" + if lang1_code: + usfm_books_lang1, usfm_resource_type_lang1 = ( + get_usfm_books_and_usfm_resource_type( + bible_references_with_availability_lang1, lang1_code + ) ) - passage = Passage( - bible_reference=localized_reference, - passage_text=verse_text_html_, + current_task.update_state(state="Assembling content") + passages_lang0 = get_passages( + bible_references_with_availability_lang0, + usfm_resource_type_lang0, + usfm_books_lang0, + ) + passages_lang1 = [] + if lang1_code: + passages_lang1 = get_passages( + bible_references_with_availability_lang1, + usfm_resource_type_lang1, + usfm_books_lang1, ) - passages.append(passage) current_task.update_state(state="Converting to Docx") - generate_docx(passages, docx_filepath_, lang_code, lang_name) + generate_docx( + passages_lang0, + passages_lang1, + docx_filepath_, + lang0_code, + lang0_name, + lang1_code, + lang1_name, + ) return docx_filepath_ def generate_docx( - passage_dtos: list[Passage], + passages_lang0: list[Passage], + passages_lang1: list[Passage], docx_filepath: str, - lang_code: str, - lang_name: str, - show_notes_column: bool = False, # TODO make a UI option, for now default to False + lang0_code: str, + lang0_name: str, + lang1_code: Optional[str], + lang1_name: Optional[str], + show_notes_column: bool = False, + available_reference_style_name: str = "AvailableReference", + unavailable_reference_style_name: str = "UnavailableReference", + unavailable_color: RGBColor = UNAVAILABLE_COLOR, + total_width: int = Inches(7.0), + document_margin_width: float = Inches(0.75), ) -> None: - TOTAL_WIDTH = Inches(6.0) doc = Document() + section = doc.sections[0] + section.left_margin = Inches(0.75) + section.right_margin = Inches(0.75) + ensure_reference_styles(doc, unavailable_color=unavailable_color) html_to_docx = HtmlToDocx() - for passage_dto in passage_dtos: - if show_notes_column: - table = doc.add_table(rows=1, cols=2) - left_col_width = Inches(4.0) - right_col_width = Inches(2.0) - col_widths = [left_col_width, right_col_width] - else: - table = doc.add_table(rows=1, cols=1) - col_widths = [TOTAL_WIDTH] - table.autofit = False - table.allow_autofit = False - # Apply column + cell widths explicitly - for i, width in enumerate(col_widths): - table.columns[i].width = width - cell = table.cell(0, i) - cell.width = width - tc = cell._tc - tcPr = tc.get_or_add_tcPr() - tcW = OxmlElement("w:tcW") - tcW.set(f"{{{WORD_NAMESPACE}}}w", str(int(width.inches * 1440))) - tcW.set(f"{{{WORD_NAMESPACE}}}type", "dxa") - tcPr.append(tcW) - # Fill left (or only) cell - cell_left = table.cell(0, 0) - html_to_docx.add_html_to_document( - passage_dto.bible_reference, - cell_left, + has_lang1 = lang1_code is not None and lang1_name is not None + if has_lang1: + assert len(passages_lang0) == len(passages_lang1), ( + f"Passage count mismatch: " + f"{len(passages_lang0)} vs {len(passages_lang1)}" ) - html_to_docx.add_html_to_document( - passage_dto.passage_text, - cell_left, + columns: list[str] = ["lang0"] + if has_lang1: + columns.append("lang1") + if show_notes_column: + columns.append("notes") + if columns == ["lang0"]: + col_widths = [total_width] + elif columns == ["lang0", "lang1"]: + col_widths = [Inches(3.5), Inches(3.5)] + elif columns == ["lang0", "notes"]: + col_widths = [Inches(4.5), Inches(2.5)] + elif columns == ["lang0", "lang1", "notes"]: + col_widths = [Inches(3.0), Inches(3.0), Inches(1.0)] + else: + logger.warning(f"Unexpected column configuration: {columns}") + table = doc.add_table(rows=0, cols=len(columns)) + table.autofit = False + table.allow_autofit = False + for i, w in enumerate(col_widths): + table.columns[i].width = w + col_index = {name: i for i, name in enumerate(columns)} + pairs = ( + zip(passages_lang0, passages_lang1) + if has_lang1 + else ((p, None) for p in passages_lang0) + ) + for p0, p1 in pairs: + row = table.add_row() + cell = row.cells[col_index["lang0"]] + run = cell.add_paragraph().add_run(p0.localized_reference) + run.style = ( + available_reference_style_name + if p0.is_available + else unavailable_reference_style_name ) - # Fill right notes column only if enabled + if p0.is_available: + html_to_docx.add_html_to_document(p0.passage_text, cell) + if has_lang1 and p1 is not None: + cell = row.cells[col_index["lang1"]] + run = cell.add_paragraph().add_run(p1.localized_reference) + run.style = ( + available_reference_style_name + if p1.is_available + else unavailable_reference_style_name + ) + if p1.is_available: + html_to_docx.add_html_to_document(p1.passage_text, cell) if show_notes_column: - cell_right = table.cell(0, 1) - cell_right.text = "" - add_vertical_line(cell_right) + cell = row.cells[col_index["notes"]] + cell.text = "" + add_vertical_line(cell) doc = add_footer(doc) - doc = add_header(doc, lang_name, header_text="Passages") + doc = add_header(doc, lang0_name, lang1_name, header_text="Passages") doc.save(docx_filepath) @@ -237,8 +328,9 @@ def add_vertical_line(cell: Cell) -> None: def document_request_key( - lang_code: str, - passage_reference_dtos: list[PassageReference], + lang0_code: str, + lang1_code: Optional[str], + passage_reference_dtos: list[BibleReferenceWithAvailability], max_filename_len: int = 240, underscore: str = "_", hyphen: str = "-", @@ -262,11 +354,15 @@ def document_request_key( translation_table = str.maketrans(":;,-", "____") passages_key = underscore.join( [ - f"{passage_reference.book_code}_{passage_reference.start_chapter}_{passage_reference.start_chapter_verse_ref.translate(translation_table)}" + f"{passage_reference.reference.book_code}_{passage_reference.reference.start_chapter}_{passage_reference.reference.start_chapter_verse_ref.translate(translation_table)}" for passage_reference in passage_reference_dtos ] ) - document_request_key_ = f"{lang_code}_{passages_key}_passages" + document_request_key_ = ( + f"{lang0_code}_{lang1_code}_{passages_key}_passages" + if lang1_code + else f"{lang0_code}_{passages_key}_passages" + ) if len(document_request_key_) >= max_filename_len: # Likely the generated filename was too long for the OS where this is # running. In that case, use the current time as a document_request_key @@ -281,8 +377,10 @@ def document_request_key( @worker.app.task def generate_passages_docx_document( - lang_code: str, - lang_name: str, + lang0_code: str, + lang0_name: str, + lang1_code: Optional[str], + lang1_name: Optional[str], passage_reference_dtos_json: str, email_address: str, docx_filepath_prefix: str = "passages_", @@ -290,20 +388,26 @@ def generate_passages_docx_document( ) -> Json[str]: passage_reference_dtos_list = json.loads(passage_reference_dtos_json) passage_reference_dtos = [ - PassageReference(**d) for d in passage_reference_dtos_list + BibleReferenceWithAvailability(**d) for d in passage_reference_dtos_list ] - logger.debug( - "passed args: lang_code: %s, passage_references: %s, email_adress: %s", - lang_code, - passage_reference_dtos, - email_address, + # logger.debug( + # "passed args: lang0_code: %s, lang1_code: %s, passage_references: %s, email_adress: %s", + # lang0_code, + # lang1_code, + # passage_reference_dtos, + # email_address, + # ) + document_request_key_ = document_request_key( + lang0_code, lang1_code, passage_reference_dtos ) - document_request_key_ = document_request_key(lang_code, passage_reference_dtos) - docx_filepath_ = f"{docx_filepath_prefix}{docx_filepath(document_request_key_)}" + docx_filepath_ = f"{docx_filepath(document_request_key_, docx_filepath_prefix)}" + logger.debug("docx_filepath_: %s", docx_filepath_) if file_needs_update(docx_filepath_): generate_docx_document( - lang_code, - lang_name, + lang0_code, + lang0_name, + lang1_code, + lang1_name, passage_reference_dtos, document_request_key_, docx_filepath_, @@ -333,6 +437,7 @@ def generate_passages_docx_document( def stet_exhaustive_verse_list( lang_code: str = "en", filepath: str = "backend/passages/data/Spiritual_Terms_Evaluation_Exhaustive_Verse_List.txt", + book_index: dict[str, int] = BOOK_INDEX, ) -> Sequence[BibleReference]: """ >>> from passages.domain.document_generator import stet_exhaustive_verse_list @@ -352,7 +457,7 @@ def stet_exhaustive_verse_list( unique_bible_references = sorted( set(bible_references), key=lambda ref: ( - BOOK_INDEX[ref.book_code], + book_index[ref.book_code], ref.start_chapter, ref.start_chapter_verse_ref, ), @@ -389,6 +494,7 @@ def parse_bible_reference(book_and_reference_raw: str) -> BibleReference: chapter = int(chapter_reference.split(":")[0]) chapter_verse_ref = chapter_reference.split(":")[1] bible_reference = BibleReference( + lang_code=None, book_code=get_book_code(book_name), book_name=book_name, start_chapter=chapter, diff --git a/backend/passages/domain/model.py b/backend/passages/domain/model.py index 13dce47ae..a40f77862 100644 --- a/backend/passages/domain/model.py +++ b/backend/passages/domain/model.py @@ -1,29 +1,31 @@ -from typing import Optional, NamedTuple, final +from typing import Optional, NamedTuple, final, TypeAlias from doc.domain.model import ChapterNum +from doc.reviewers_guide.model import BibleReference from pydantic import BaseModel, EmailStr + + @final -class BibleReference(BaseModel): - lang_code: str - book_code: str - book_name: str - start_chapter: ChapterNum - start_chapter_verse_ref: str - end_chapter: Optional[ChapterNum] - end_chapter_verse_ref: Optional[str] +class BibleReferenceWithAvailability(BaseModel): + reference: BibleReference + is_available: bool @final class Passage(NamedTuple): + reference: BibleReference passage_text: str # HTML of passage - bible_reference: str + localized_reference: str + is_available: bool @final class PassagesDocumentRequest(BaseModel): - lang_code: str - lang_name: str - bible_references: list[BibleReference] + lang0_code: str + lang0_name: str + lang1_code: Optional[str] + lang1_name: Optional[str] + bible_references: list[BibleReferenceWithAvailability] email_address: Optional[EmailStr] diff --git a/backend/passages/domain/parser.py b/backend/passages/domain/parser.py index 455d9ec24..455f33661 100644 --- a/backend/passages/domain/parser.py +++ b/backend/passages/domain/parser.py @@ -4,7 +4,7 @@ from doc.domain.bible_books import BOOK_CHAPTER_VERSES from doc.domain.model import USFMBook from doc.domain.parsing import lookup_verse_text -from passages.domain.model import BibleReference +from doc.reviewers_guide.model import BibleReference logger = settings.logger(__name__) diff --git a/backend/passages/domain/stet_verse_list_parser.py b/backend/passages/domain/stet_verse_list_parser.py index bb983937f..55d017c12 100644 --- a/backend/passages/domain/stet_verse_list_parser.py +++ b/backend/passages/domain/stet_verse_list_parser.py @@ -4,7 +4,7 @@ from doc.domain.bible_books import BOOK_NAMES -BOOK_INDEX = dict((id, pos) for pos, id in enumerate(BOOK_NAMES.keys())) +BOOK_INDEX = dict((key, pos) for pos, key in enumerate(BOOK_NAMES.keys())) BOOK_VERSE_PATTERN = re.compile(r"(?:([1-3]?\s?[A-Z][a-z]+)\s)?(\d+:\d+)") HEADER_PATTERN = re.compile(r"^(\w+)\s+\(([\d,; ]+)\)") diff --git a/backend/passages/entrypoints/routes.py b/backend/passages/entrypoints/routes.py index 1cbb66c4f..f6d04024b 100644 --- a/backend/passages/entrypoints/routes.py +++ b/backend/passages/entrypoints/routes.py @@ -1,5 +1,5 @@ import json -from typing import Sequence +from typing import Sequence, cast import celery.states from celery.result import AsyncResult @@ -20,16 +20,17 @@ async def generate_passages_docx_document( passages_document_request: model.PassagesDocumentRequest, ) -> JSONResponse: - # logger.debug( - # "passages_document_request.bible_references: %s", - # passages_document_request.bible_references, - # ) - # Top level exception handler + logger.debug( + "passages_document_request: %s", + passages_document_request, + ) try: task = document_generator.generate_passages_docx_document.apply_async( args=( - passages_document_request.lang_code, - passages_document_request.lang_name, + passages_document_request.lang0_code, + passages_document_request.lang0_name, + passages_document_request.lang1_code, + passages_document_request.lang1_name, # Serialize the list of objects to a JSON string json.dumps( passages_document_request.bible_references, @@ -70,4 +71,4 @@ async def task_status(task_id: str) -> JSONResponse: @router.get("/passages/stet_verse_list/{lang_code}") async def stet_verse_list(lang_code: str) -> Sequence[BibleReference]: - return stet_exhaustive_verse_list(lang_code) + return cast(Sequence[BibleReference], stet_exhaustive_verse_list(lang_code)) diff --git a/backend/passages/utils/docx_utils.py b/backend/passages/utils/docx_utils.py index 22b977991..744676010 100644 --- a/backend/passages/utils/docx_utils.py +++ b/backend/passages/utils/docx_utils.py @@ -119,7 +119,8 @@ def add_checkbox_to_cell(cell: Cell) -> None: def add_header( doc: DocxDocument, - lang_name: str, + lang0_name: str, + lang1_name: Optional[str], header_text: str = "Passages", ) -> DocxDocument: """ @@ -131,9 +132,16 @@ def add_header( header_paragraph = header.add_paragraph() header_paragraph.style = doc.styles["Header"] header_paragraph.style.font.size = Pt(12) # Optional: Adjust font size - # Add the header text with grey color - run1 = header_paragraph.add_run(header_text + ": " + lang_name) - run1.font.color.rgb = RGBColor(169, 169, 169) # Grey color + if lang1_name: + # Add the header text with grey color + run1 = header_paragraph.add_run( + header_text + ": " + lang0_name + "/" + lang1_name + ) + run1.font.color.rgb = RGBColor(169, 169, 169) # Grey color + else: + # Add the header text with grey color + run1 = header_paragraph.add_run(header_text + ": " + lang0_name) + run1.font.color.rgb = RGBColor(169, 169, 169) # Grey color return doc diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index 65c426eff..1de9fba39 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -8,21 +8,21 @@ amqp==5.3.1 # via # -c backend/requirements.txt # kombu -astroid==4.0.2 +astroid==4.0.4 # via pylint billiard==4.2.4 # via # -c backend/requirements.txt # celery -black==25.12.0 +black==26.1.0 # via # -r backend/requirements-dev.in # python-lsp-server -celery==5.6.0 +celery==5.6.2 # via # -c backend/requirements.txt # pytest-celery -certifi==2025.11.12 +certifi==2026.1.4 # via # -c backend/requirements.txt # requests @@ -56,9 +56,9 @@ cohesion==1.2.0 # via -r backend/requirements-dev.in colorama==0.4.6 # via radon -debugpy==1.8.18 +debugpy==1.8.20 # via pytest-celery -dill==0.4.0 +dill==0.4.1 # via pylint dlint==0.16.0 # via -r backend/requirements-dev.in @@ -68,10 +68,6 @@ docker==7.1.0 # pytest-docker-tools docstring-to-markdown==0.17 # via python-lsp-server -exceptiongroup==1.3.1 - # via - # -c backend/requirements.txt - # celery execnet==2.1.2 # via pytest-xdist flake8==7.3.0 @@ -82,15 +78,15 @@ flake8-fixme==1.1.1 # via -r backend/requirements-dev.in gitdb==4.0.12 # via gitpython -gitpython==3.1.45 +gitpython==3.1.46 # via pygount -icdiff==2.0.7 +icdiff==2.0.10 # via pytest-icdiff idna==3.11 # via # -c backend/requirements.txt # requests -importlib-metadata==8.7.0 +importlib-metadata==8.7.1 # via docstring-to-markdown iniconfig==2.3.0 # via pytest @@ -100,12 +96,12 @@ isort==7.0.0 # pylint jedi==0.19.2 # via python-lsp-server -kombu==5.6.1 +kombu==5.6.2 # via # -c backend/requirements.txt # celery # pytest-celery -librt==0.7.3 +librt==0.8.0 # via mypy mando==0.7.1 # via radon @@ -121,7 +117,7 @@ mdurl==0.1.2 # via # -c backend/requirements.txt # markdown-it-py -mypy==1.19.0 +mypy==1.19.1 # via # -r backend/requirements-dev.in # pylsp-mypy @@ -129,19 +125,19 @@ mypy-extensions==1.1.0 # via # black # mypy -packaging==25.0 +packaging==26.0 # via # -c backend/requirements.txt # black # kombu # pytest -parso==0.8.5 +parso==0.8.6 # via jedi -pathspec==0.12.1 +pathspec==1.0.4 # via # black # mypy -platformdirs==4.5.1 +platformdirs==4.7.0 # via # black # pylint @@ -155,7 +151,7 @@ prompt-toolkit==3.0.52 # via # -c backend/requirements.txt # click-repl -psutil==7.1.3 +psutil==7.2.2 # via # -r backend/requirements-dev.in # pytest-celery @@ -173,7 +169,7 @@ pygount==3.1.0 # via -r backend/requirements-dev.in pylint==4.0.4 # via -r backend/requirements-dev.in -pylsp-mypy==0.7.0 +pylsp-mypy==0.7.1 # via -r backend/requirements-dev.in pytest==9.0.2 # via @@ -185,7 +181,7 @@ pytest==9.0.2 # pytest-xdist pytest-celery==1.2.1 # via -r backend/requirements-dev.in -pytest-datafiles==3.0.0 +pytest-datafiles==3.0.1 # via -r backend/requirements-dev.in pytest-docker-tools==3.1.9 # via pytest-celery @@ -203,7 +199,7 @@ python-lsp-jsonrpc==1.1.2 # via python-lsp-server python-lsp-server==1.14.0 # via pylsp-mypy -pytokens==0.3.0 +pytokens==0.4.1 # via black radon==6.0.1 # via -r backend/requirements-dev.in @@ -211,7 +207,7 @@ requests==2.32.5 # via # -c backend/requirements.txt # docker -rich==14.2.0 +rich==14.3.2 # via # -c backend/requirements.txt # pygount @@ -222,9 +218,9 @@ six==1.17.0 # python-dateutil smmap==5.0.2 # via gitdb -tenacity==9.1.2 +tenacity==9.1.4 # via pytest-celery -tomlkit==0.13.3 +tomlkit==0.14.0 # via pylint typing-extensions==4.15.0 # via @@ -244,7 +240,7 @@ ujson==5.11.0 # -c backend/requirements.txt # python-lsp-jsonrpc # python-lsp-server -urllib3==2.6.2 +urllib3==2.6.3 # via # -c backend/requirements.txt # docker @@ -257,7 +253,7 @@ vine==5.1.0 # kombu vulture==2.14 # via -r backend/requirements-dev.in -wcwidth==0.2.14 +wcwidth==0.6.0 # via # -c backend/requirements.txt # prompt-toolkit diff --git a/backend/requirements-prod.txt b/backend/requirements-prod.txt index 77e48255c..85beef556 100644 --- a/backend/requirements-prod.txt +++ b/backend/requirements-prod.txt @@ -6,21 +6,21 @@ # execnet==2.1.2 # via pytest-xdist -icdiff==2.0.7 +icdiff==2.0.10 # via pytest-icdiff iniconfig==2.3.0 # via pytest -librt==0.7.3 +librt==0.8.0 # via mypy -mypy==1.19.0 +mypy==1.19.1 # via -r backend/requirements-prod.in mypy-extensions==1.1.0 # via mypy -packaging==25.0 +packaging==26.0 # via # -c backend/requirements.txt # pytest -pathspec==0.12.1 +pathspec==1.0.4 # via mypy pluggy==1.6.0 # via pytest @@ -37,7 +37,7 @@ pytest==9.0.2 # pytest-icdiff # pytest-repeat # pytest-xdist -pytest-datafiles==3.0.0 +pytest-datafiles==3.0.1 # via -r backend/requirements-prod.in pytest-icdiff==0.9 # via -r backend/requirements-prod.in diff --git a/backend/requirements.in b/backend/requirements.in index 410592afb..2cb262e90 100644 --- a/backend/requirements.in +++ b/backend/requirements.in @@ -9,7 +9,7 @@ beautifulsoup4 cachetools celery celery-types -docxcompose +docxcompose3 docxtpl fastapi[all] filelock diff --git a/backend/requirements.txt b/backend/requirements.txt index 600b0ed04..f09ee8191 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,16 +9,18 @@ aiofiles==25.1.0 amqp==5.3.1 # via kombu annotated-doc==0.0.4 - # via fastapi + # via + # fastapi + # typer annotated-types==0.7.0 # via pydantic -anyio==4.12.0 +anyio==4.12.1 # via # httpx # starlette # watchfiles -babel==2.17.0 - # via docxcompose +babel==2.18.0 + # via docxcompose3 beautifulsoup4==4.14.3 # via # -r backend/requirements.in @@ -27,15 +29,15 @@ billiard==4.2.4 # via celery brotli==1.2.0 # via fonttools -cachetools==6.2.3 +cachetools==7.0.1 # via -r backend/requirements.in -celery==5.6.0 +celery==5.6.2 # via # -r backend/requirements.in # flower -celery-types==0.23.0 +celery-types==0.24.0 # via -r backend/requirements.in -certifi==2025.11.12 +certifi==2026.1.4 # via # httpcore # httpx @@ -60,11 +62,11 @@ click-plugins==1.1.1.2 # via celery click-repl==0.3.0 # via celery -cssselect2==0.8.0 +cssselect2==0.9.0 # via weasyprint dnspython==2.8.0 # via email-validator -docxcompose==1.4.0 +docxcompose3==1.4.5 # via -r backend/requirements.in docxtpl==0.20.2 # via -r backend/requirements.in @@ -73,23 +75,21 @@ email-validator==2.3.0 # -r backend/requirements.in # fastapi # pydantic -exceptiongroup==1.3.1 - # via celery -fastapi[all]==0.124.4 +fastapi[all]==0.129.0 # via -r backend/requirements.in -fastapi-cli[standard]==0.0.16 +fastapi-cli[standard]==0.0.21 # via fastapi -fastapi-cloud-cli==0.6.0 +fastapi-cloud-cli==0.11.0 # via fastapi-cli fastar==0.8.0 # via fastapi-cloud-cli -filelock==3.20.0 +filelock==3.21.1 # via -r backend/requirements.in flower==2.0.1 # via -r backend/requirements.in fonttools[woff]==4.61.1 # via weasyprint -gunicorn==23.0.0 +gunicorn==25.0.3 # via -r backend/requirements.in h11==0.16.0 # via @@ -105,7 +105,7 @@ httpx==0.28.1 # via # fastapi # fastapi-cloud-cli -humanize==4.14.0 +humanize==4.15.0 # via flower idna==3.11 # via @@ -120,11 +120,11 @@ jinja2==3.1.6 # -r backend/requirements.in # docxtpl # fastapi -kombu==5.6.1 +kombu==5.6.2 # via celery lxml==6.0.2 # via - # docxcompose + # docxcompose3 # docxtpl # python-docx markdown-it-py==4.0.0 @@ -133,23 +133,23 @@ markupsafe==3.0.3 # via jinja2 mdurl==0.1.2 # via markdown-it-py -mistune==3.1.4 +mistune==3.2.0 # via -r backend/requirements.in -orjson==3.11.5 +orjson==3.11.7 # via # -r backend/requirements.in # fastapi -packaging==25.0 +packaging==26.0 # via # gunicorn # kombu -pillow==12.0.0 +pillow==12.1.1 # via weasyprint -prometheus-client==0.23.1 +prometheus-client==0.24.1 # via flower prompt-toolkit==3.0.52 # via click-repl -pycparser==2.23 +pycparser==3.0 # via cffi pydantic[email]==2.12.5 # via @@ -160,7 +160,7 @@ pydantic[email]==2.12.5 # pydantic-settings pydantic-core==2.41.5 # via pydantic -pydantic-extra-types==2.10.6 +pydantic-extra-types==2.11.0 # via fastapi pydantic-settings==2.12.0 # via fastapi @@ -174,7 +174,7 @@ python-dateutil==2.9.0.post0 # via celery python-docx==1.2.0 # via - # docxcompose + # docxcompose3 # docxtpl # htmldocx python-dotenv==1.2.1 @@ -182,7 +182,7 @@ python-dotenv==1.2.1 # -r backend/requirements.in # pydantic-settings # uvicorn -python-multipart==0.0.20 +python-multipart==0.0.22 # via fastapi pytz==2025.2 # via flower @@ -191,35 +191,35 @@ pyyaml==6.0.3 # -r backend/requirements.in # fastapi # uvicorn -redis==7.1.0 +redis==7.1.1 # via -r backend/requirements.in -regex==2025.11.3 +regex==2026.1.15 # via -r backend/requirements.in requests==2.32.5 # via -r backend/requirements.in -rich==14.2.0 +rich==14.3.2 # via # rich-toolkit # typer -rich-toolkit==0.17.0 +rich-toolkit==0.19.4 # via # fastapi-cli # fastapi-cloud-cli rignore==0.7.6 # via fastapi-cloud-cli -sentry-sdk==2.47.0 +sentry-sdk==2.52.0 # via fastapi-cloud-cli shellingham==1.5.4 # via typer six==1.17.0 # via - # docxcompose + # docxcompose3 # python-dateutil -soupsieve==2.8 +soupsieve==2.8.3 # via beautifulsoup4 -starlette==0.50.0 +starlette==0.52.1 # via fastapi -termcolor==3.2.0 +termcolor==3.3.0 # via -r backend/requirements.in tinycss2==1.5.1 # via @@ -227,9 +227,9 @@ tinycss2==1.5.1 # weasyprint tinyhtml5==2.0.0 # via weasyprint -tornado==6.5.3 +tornado==6.5.4 # via flower -typer==0.20.0 +typer==0.23.0 # via # fastapi-cli # fastapi-cloud-cli @@ -243,9 +243,9 @@ types-orjson==3.6.2 # via -r backend/requirements.in types-pyyaml==6.0.12.20250915 # via -r backend/requirements.in -types-requests==2.32.4.20250913 +types-requests==2.32.4.20260107 # via -r backend/requirements.in -types-setuptools==80.9.0.20250822 +types-setuptools==82.0.0.20260210 # via -r backend/requirements.in types-termcolor==1.1.6.2 # via -r backend/requirements.in @@ -263,10 +263,10 @@ typing-extensions==4.15.0 # pydantic-extra-types # python-docx # rich-toolkit - # typer # typing-inspection typing-inspection==0.4.2 # via + # fastapi # pydantic # pydantic-settings tzdata==2025.3 @@ -275,12 +275,12 @@ tzlocal==5.3.1 # via celery ujson==5.11.0 # via fastapi -urllib3==2.6.2 +urllib3==2.6.3 # via # requests # sentry-sdk # types-requests -uvicorn[standard]==0.38.0 +uvicorn[standard]==0.40.0 # via # -r backend/requirements.in # fastapi @@ -295,18 +295,18 @@ vine==5.1.0 # kombu watchfiles==1.1.1 # via uvicorn -wcwidth==0.2.14 +wcwidth==0.6.0 # via prompt-toolkit -weasyprint==67.0 +weasyprint==68.1 # via -r backend/requirements.in webencodings==0.5.1 # via # cssselect2 # tinycss2 # tinyhtml5 -websockets==15.0.1 +websockets==16.0 # via uvicorn -zopfli==0.4.0 +zopfli==0.4.1 # via fonttools # The following packages are considered to be unsafe in a requirements file: diff --git a/backend/stet/domain/document_generator.py b/backend/stet/domain/document_generator.py index 70bf934e9..a6d3d3933 100644 --- a/backend/stet/domain/document_generator.py +++ b/backend/stet/domain/document_generator.py @@ -41,6 +41,7 @@ add_highlighted_html_to_docx_for_words, add_lined_page_at_end, add_plain_html_to_docx, + add_preformatted_html_to_docx, adjust_table_columns, reduce_spacing_around_tables, ) @@ -243,7 +244,9 @@ def generate_docx_document( else verse_ref_dto.target_reference ) for verse_ref in verse_ref_dto.verse_refs: - if source_selected_usfm_book: + if verse_ref_dto.source_text_with_bolding is not None: + source_verse_text = verse_ref_dto.source_text_with_bolding + elif source_selected_usfm_book: source_verse_text = lookup_verse_text( source_selected_usfm_book, verse_ref_dto.chapter_num, @@ -272,6 +275,9 @@ def generate_docx_document( target_text=target_verse_text, occurrence_index=current, occurrence_total=total, + source_has_preformatted_bolding=( + verse_ref_dto.source_text_with_bolding is not None + ), ) ) word_entries.append(word_entry) @@ -332,7 +338,6 @@ def generate_docx( source_ref_display += ( f" ({verse.occurrence_index}/{verse.occurrence_total})" ) - target_ref_display = verse.target_reference if verse.occurrence_total > 1: target_ref_display += ( @@ -354,7 +359,11 @@ def generate_docx( # Process HTML content in source_text and highlight keyword source_paragraph = row_cells[0].paragraphs[0] source_paragraph.paragraph_format.line_spacing = 2.0 # Adjust line spacing - if len(word_entry.bolded_phrases) > 0: + if verse.source_has_preformatted_bolding: + add_preformatted_html_to_docx( + verse.source_text, source_paragraph + ) + elif len(word_entry.bolded_phrases) > 0: add_highlighted_html_to_docx_for_words( verse.source_text, source_paragraph, word_entry.bolded_phrases ) diff --git a/backend/stet/domain/model.py b/backend/stet/domain/model.py index ef8ee09c0..2e2373bca 100644 --- a/backend/stet/domain/model.py +++ b/backend/stet/domain/model.py @@ -12,6 +12,7 @@ class VerseEntry(NamedTuple): target_text: str occurrence_index: int = 0 # 1, 2, 3, ... occurrence_total: int = 0 # e.g. 3 + source_has_preformatted_bolding: bool = False # True when source came from format @final @@ -34,6 +35,7 @@ class VerseReferenceDto(NamedTuple): source_reference: str target_reference: str verse_refs: list[str] + source_text_with_bolding: Optional[str] = None # When set, from format @final diff --git a/backend/stet/domain/parser.py b/backend/stet/domain/parser.py index 7365cecf8..849aba758 100644 --- a/backend/stet/domain/parser.py +++ b/backend/stet/domain/parser.py @@ -10,6 +10,127 @@ logger = settings.logger(__name__) +_RV_BLOCK_PATTERN = re.compile(r"(.*?)\s*(.*?)", re.DOTALL) +_REF_PATTERN = re.compile(r"^(.*) (\d+):([0-9,\- ]+)\s?(\(.*\))?$") + + +def _parse_ref_to_dto( + reference_: str, + lang0_code: str, + lang1_code: str, + lang0_book_codes_and_names: list[tuple[str, str]], + lang1_book_codes_and_names: list[tuple[str, str]], + book_names: dict[str, str], + lang0_book_codes_and_names__: list[tuple[str, str]], + source_text_with_bolding: str | None = None, +) -> VerseReferenceDto | None: + match = _REF_PATTERN.match(reference_) + if not match: + logger.warning("Couldn't parse %s", reference_) + return None + book_name = match.group(1).replace("\n", "") + book_codes_and_names_ = [ + (bc, bn) for bc, bn in lang0_book_codes_and_names if bn == book_name + ] + if not book_codes_and_names_: + book_codes_and_names_ = [ + (bc, bn) for bc, bn in book_names.items() if bn == book_name + ] + book_code_and_name_ = book_codes_and_names_[0] if book_codes_and_names_ else None + if book_code_and_name_: + lang0_book_codes_and_names__.append(book_code_and_name_) + chapter_num = int(match.group(2)) + verses = match.group(3) + comment = match.group(4) + source_reference = ( + f"{book_name} {chapter_num}:{verses}{comment}" if comment + else f"{book_name} {chapter_num}:{verses}" + ) + lang0_book_code = book_code_and_name_[0] if book_code_and_name_ else "" + lang1_book_code_and_name_ = next( + (x for x in lang1_book_codes_and_names if x[0] == lang0_book_code), + None, + ) + lang1_book_name = lang1_book_code_and_name_[1] if lang1_book_code_and_name_ else "" + target_reference = f"{lang1_book_name} {chapter_num}:{verses}" + verse_refs: list[str] = verses.split(",") + valid_verse_refs: list[str] = [] + for verse_ref in verse_refs: + if is_valid_int(verse_ref): + valid_verse_refs.append(str(verse_ref)) + continue + vm = re.match(r"(\d+)-(\d+)", verse_ref) + if vm: + for verse_num in range(int(vm.group(1)), int(vm.group(2)) + 1): + valid_verse_refs.append(str(verse_num)) + continue + logger.warning("Couldn't parse verse ref: %s", verse_ref) + return VerseReferenceDto( + lang0_code=lang0_code, + lang1_code=lang1_code, + book_code=book_code_and_name_[0] if book_code_and_name_ else "", + book_name=book_name, + chapter_num=chapter_num, + source_reference=source_reference, + target_reference=target_reference, + verse_refs=valid_verse_refs, + source_text_with_bolding=source_text_with_bolding, + ) + + +def _parse_fully_specified_column3( + col3_text: str, + word_entry_dto: WordEntryDto, + lang0_code: str, + lang1_code: str, + lang0_book_codes_and_names: list[tuple[str, str]], + lang1_book_codes_and_names: list[tuple[str, str]], + book_names: dict[str, str], + lang0_book_codes_and_names__: list[tuple[str, str]], +) -> None: + for m in _RV_BLOCK_PATTERN.finditer(col3_text): + ref_part = m.group(1).strip() + verse_part = m.group(2).strip() + dto = _parse_ref_to_dto( + ref_part, + lang0_code, + lang1_code, + lang0_book_codes_and_names, + lang1_book_codes_and_names, + book_names, + lang0_book_codes_and_names__, + source_text_with_bolding=verse_part, + ) + if dto: + word_entry_dto.verse_ref_dtos.append(dto) + + +def _parse_bible_reference_column3( + col3_text: str, + word_entry_dto: WordEntryDto, + lang0_code: str, + lang1_code: str, + lang0_book_codes_and_names: list[tuple[str, str]], + lang1_book_codes_and_names: list[tuple[str, str]], + book_names: dict[str, str], + lang0_book_codes_and_names__: list[tuple[str, str]], +) -> None: + for reference in col3_text.split("\n"): + reference_ = reference.strip() + if not reference_: + continue + dto = _parse_ref_to_dto( + reference_, + lang0_code, + lang1_code, + lang0_book_codes_and_names, + lang1_book_codes_and_names, + book_names, + lang0_book_codes_and_names__, + ) + if dto: + word_entry_dto.verse_ref_dtos.append(dto) + def get_word_entry_dtos( lang0_code: str, @@ -51,92 +172,30 @@ def get_word_entry_dtos( previous_paragraph_style_name = paragraph.style.name word_entry_dto.definition = definition # Get verse references from 3rd column - for reference in row.cells[2].text.split("\n"): - reference_ = reference.strip() - match = re.match(r"^(.*) (\d+):([0-9,\- ]+)\s?(\(.*\))?$", reference_) - if not match: - logger.warning("Couldn't parse %s", reference_) - continue - if match: - # Extract references - book_name = match.group(1) - # Some languages, e.g., bem, have a \n in the book name - book_name = book_name.replace("\n", "") - # We expect this book name to be in localized form according to the - # language of the STET input document (as indicated by the input - # document's filename, stet_[ietf_code].docx). - book_codes_and_names_ = [ - (book_code, book_name_) - for book_code, book_name_ in lang0_book_codes_and_names - if book_name_ - == book_name # Check if DOC and STET input doc agree on book name - ] - # If the names don't lookup in localized form then try to use English - # just in case that was used instead. - if not book_codes_and_names_: - book_codes_and_names_ = [ - (book_code, book_name_) - for book_code, book_name_ in book_names.items() - if book_name_ == book_name - ] - book_code_and_name_ = ( - book_codes_and_names_[0] if book_codes_and_names_ else None - ) - if book_code_and_name_: - lang0_book_codes_and_names__.append(book_code_and_name_) - chapter_num = int(match.group(2)) - verses = match.group(3) - comment = match.group(4) - if comment: - source_reference = ( - f"{book_name} {chapter_num}:{verses}{comment}" - ) - else: - source_reference = f"{book_name} {chapter_num}:{verses}" - lang0_book_code = ( - book_code_and_name_[0] if book_code_and_name_ else "" - ) - lang1_book_code_and_name_ = next( - ( - lang1_book_code_and_name - for lang1_book_code_and_name in lang1_book_codes_and_names - if lang1_book_code_and_name[0] == lang0_book_code - ), - None, - ) - lang1_book_name = ( - lang1_book_code_and_name_[1] - if lang1_book_code_and_name_ - else "" - ) - target_reference = f"{lang1_book_name} {chapter_num}:{verses}" - verse_refs: list[str] = verses.split(",") - valid_verse_refs: list[str] = [] - for verse_ref in verse_refs: - if is_valid_int(verse_ref): - valid_verse_refs.append(str(verse_ref)) - continue - match = re.match(r"(\d+)-(\d+)", verse_ref) - if match: - start_verse = int(match.group(1)) - end_verse = int(match.group(2)) - verse_num = start_verse - while verse_num <= end_verse: - valid_verse_refs.append(str(verse_num)) - verse_num += 1 - continue - logger.warning("Couldn't parse verse ref: %s", verse_ref) - verse_reference_dto = VerseReferenceDto( - lang0_code=lang0_code, - lang1_code=lang1_code, - book_code=book_code_and_name_[0] if book_code_and_name_ else "", - book_name=book_name, - chapter_num=chapter_num, - source_reference=source_reference, - target_reference=target_reference, - verse_refs=valid_verse_refs, - ) - word_entry_dto.verse_ref_dtos.append(verse_reference_dto) + col3_text = row.cells[2].text.strip() + if col3_text.startswith(""): + # Fully specified format: refverse text with bold... + _parse_fully_specified_column3( + col3_text, + word_entry_dto, + lang0_code, + lang1_code, + lang0_book_codes_and_names, + lang1_book_codes_and_names, + book_names, + lang0_book_codes_and_names__, + ) + else: + _parse_bible_reference_column3( + row.cells[2].text, + word_entry_dto, + lang0_code, + lang1_code, + lang0_book_codes_and_names, + lang1_book_codes_and_names, + book_names, + lang0_book_codes_and_names__, + ) # If 4th column exists, get bolded words from it if len(row.cells) > 3 and row.cells[3].text: word_entry_dto.bolded_phrases = [ diff --git a/backend/stet/utils/docx_utils.py b/backend/stet/utils/docx_utils.py index 38f83ad04..c9cfaff6d 100644 --- a/backend/stet/utils/docx_utils.py +++ b/backend/stet/utils/docx_utils.py @@ -169,6 +169,23 @@ def add_plain_html_to_docx(html: str, paragraph: Paragraph) -> None: paragraph.add_run(temp_paragraph.text.strip()) +def add_preformatted_html_to_docx(html: str, paragraph: Paragraph) -> None: + """ + Convert HTML with tags to DOCX, preserving bold formatting. + Used when source text comes from fully specified format. + + :param html: The HTML string to convert (may contain tags). + :param paragraph: The DOCX paragraph where content will be added. + """ + html_to_docx = HtmlToDocx() + temp_doc = Document() + html_to_docx.add_html_to_document(html, temp_doc) + for temp_paragraph in temp_doc.paragraphs: + for run in temp_paragraph.runs: + new_run = paragraph.add_run(run.text) + new_run.bold = run.bold + + def add_lined_page_at_end(doc: DocxDocument) -> DocxDocument: """ Adds a single page filled with ruled lines to the end of the document for note-taking. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b83def388..60be5ab63 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -33,9 +33,11 @@ "prettier-plugin-tailwindcss": "^0.5.9", "svelte": "^4.2.11", "svelte-check": "^3.6.0", + "svelte-language-server": "^0.17.24", + "svelte-preprocess": "^6.0.3", "tailwindcss": "^3.3.6", "tslib": "^2.4.1", - "typescript": "^5.0.0", + "typescript": "^5.9.3", "vite": "^5.0.3", "vitest": "^1.2.0" } @@ -73,6 +75,33 @@ "node": ">=6.0.0" } }, + "node_modules/@emmetio/abbreviation": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz", + "integrity": "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-abbreviation": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@emmetio/css-abbreviation/-/css-abbreviation-2.1.8.tgz", + "integrity": "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/scanner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emmetio/scanner/-/scanner-1.0.4.tgz", + "integrity": "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -1220,7 +1249,8 @@ "version": "2.0.10", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/resolve": { "version": "1.20.2", @@ -1543,6 +1573,42 @@ "@types/estree": "^1.0.0" } }, + "node_modules/@vscode/emmet-helper": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.8.4.tgz", + "integrity": "sha512-lUki5QLS47bz/U8IlG9VQ+1lfxMtxMZENmU5nu4Z71eOD5j9FK0SmYGL5NiVJg9WBWeAU0VxRADMY2Qpq7BfVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "emmet": "^2.3.0", + "jsonc-parser": "^2.3.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.15.1", + "vscode-nls": "^5.0.0", + "vscode-uri": "^2.1.2" + } + }, + "node_modules/@vscode/emmet-helper/node_modules/jsonc-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vscode/emmet-helper/node_modules/vscode-uri": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.2.tgz", + "integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vscode/l10n": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", + "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -1784,12 +1850,13 @@ } }, "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", "dev": true, + "license": "MIT", "engines": { - "node": "*" + "node": ">=8.0.0" } }, "node_modules/cac": { @@ -2079,6 +2146,13 @@ } } }, + "node_modules/dedent-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dedent-js/-/dedent-js-1.0.1.tgz", + "integrity": "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -2119,6 +2193,7 @@ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2187,6 +2262,23 @@ "integrity": "sha512-uHt4FB8SeYdhcOsj2ix/C39S7sPSNFJpzShjxGOm1KdF4MHyGqGi389+T5cErsodsijojXilYaHIKKqJfqh7uQ==", "dev": true }, + "node_modules/emmet": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.11.tgz", + "integrity": "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "./packages/scanner", + "./packages/abbreviation", + "./packages/css-abbreviation", + "./" + ], + "dependencies": { + "@emmetio/abbreviation": "^2.3.3", + "@emmetio/css-abbreviation": "^2.1.8" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -2197,7 +2289,8 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/esbuild": { "version": "0.21.5", @@ -2847,11 +2940,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", @@ -3210,6 +3311,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3302,6 +3410,7 @@ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -3326,6 +3435,7 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3344,6 +3454,7 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.6" }, @@ -3966,9 +4077,9 @@ } }, "node_modules/prettier-plugin-svelte": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.3.tgz", - "integrity": "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.1.tgz", + "integrity": "sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4299,6 +4410,7 @@ "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", "dev": true, + "license": "MIT", "dependencies": { "es6-promise": "^3.1.2", "graceful-fs": "^4.1.3", @@ -4307,10 +4419,11 @@ } }, "node_modules/sander/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4320,7 +4433,9 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -4341,6 +4456,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4352,7 +4468,9 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -4360,6 +4478,13 @@ "rimraf": "bin.js" } }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true, + "license": "MIT" + }, "node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -4443,13 +4568,14 @@ } }, "node_modules/sorcery": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", - "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.1.tgz", + "integrity": "sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.14", - "buffer-crc32": "^0.2.5", + "buffer-crc32": "^1.0.0", "minimist": "^1.2.0", "sander": "^0.5.0" }, @@ -4585,6 +4711,7 @@ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dev": true, + "license": "MIT", "dependencies": { "min-indent": "^1.0.0" }, @@ -4708,6 +4835,69 @@ "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" } }, + "node_modules/svelte-check/node_modules/svelte-preprocess": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz", + "integrity": "sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@types/pug": "^2.0.6", + "detect-indent": "^6.1.0", + "magic-string": "^0.30.5", + "sorcery": "^0.11.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.2", + "coffeescript": "^2.5.1", + "less": "^3.11.3 || ^4.0.0", + "postcss": "^7 || ^8", + "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "pug": "^3.0.0", + "sass": "^1.26.8", + "stylus": "^0.55.0", + "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", + "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "coffeescript": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-load-config": { + "optional": true + }, + "pug": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/svelte-eslint-parser": { "version": "0.34.0-next.8", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.34.0-next.8.tgz", @@ -4746,35 +4936,141 @@ "svelte": "^3.19.0 || ^4.0.0" } }, - "node_modules/svelte-preprocess": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz", - "integrity": "sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==", + "node_modules/svelte-language-server": { + "version": "0.17.24", + "resolved": "https://registry.npmjs.org/svelte-language-server/-/svelte-language-server-0.17.24.tgz", + "integrity": "sha512-Xid+M9mD+VGDttA5oGM73A4nQzrDJgUntnaoMPaH84eE8pavCO8Gk/iJnSviwXtsA8FKXDPqpzV/qb1lZYD8Mw==", "dev": true, - "hasInstallScript": true, + "license": "MIT", "dependencies": { - "@types/pug": "^2.0.6", - "detect-indent": "^6.1.0", - "magic-string": "^0.30.5", - "sorcery": "^0.11.0", - "strip-indent": "^3.0.0" + "@jridgewell/trace-mapping": "^0.3.25", + "@vscode/emmet-helper": "2.8.4", + "chokidar": "^4.0.1", + "estree-walker": "^2.0.1", + "fdir": "^6.2.0", + "globrex": "^0.1.2", + "lodash": "^4.17.21", + "prettier": "~3.3.3", + "prettier-plugin-svelte": "^3.4.0", + "svelte": "^4.2.19", + "svelte2tsx": "~0.7.47", + "typescript": "^5.9.2", + "typescript-auto-import-cache": "^0.3.6", + "vscode-css-languageservice": "~6.3.5", + "vscode-html-languageservice": "~5.4.0", + "vscode-languageserver": "9.0.1", + "vscode-languageserver-protocol": "3.17.5", + "vscode-languageserver-types": "3.17.5", + "vscode-uri": "~3.1.0" + }, + "bin": { + "svelteserver": "bin/server.js" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/svelte-language-server/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 16.0.0", - "pnpm": "^8.0.0" + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/svelte-language-server/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/svelte-language-server/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/svelte-language-server/node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/svelte-language-server/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/svelte-preprocess": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-6.0.3.tgz", + "integrity": "sha512-PLG2k05qHdhmRG7zR/dyo5qKvakhm8IJ+hD2eFRQmMLHp7X3eJnjeupUtvuRpbNiF31RjVw45W+abDwHEmP5OA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" }, "peerDependencies": { "@babel/core": "^7.10.2", "coffeescript": "^2.5.1", "less": "^3.11.3 || ^4.0.0", "postcss": "^7 || ^8", - "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "postcss-load-config": ">=3", "pug": "^3.0.0", "sass": "^1.26.8", - "stylus": "^0.55.0", + "stylus": ">=0.55", "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", - "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", - "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" + "svelte": "^4.0.0 || ^5.0.0-next.100 || ^5.0.0", + "typescript": "^5.0.0" }, "peerDependenciesMeta": { "@babel/core": { @@ -4825,6 +5121,21 @@ "@types/estree": "*" } }, + "node_modules/svelte2tsx": { + "version": "0.7.47", + "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.7.47.tgz", + "integrity": "sha512-1aw/MFKVPM96OBevJdC12do2an9t5Zwr3Va9amLgTLpJje36ibD1iIHpuqCYWUrdR9vw6g6btKGQPmsqE8ZYCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dedent-js": "^1.0.1", + "scule": "^1.3.0" + }, + "peerDependencies": { + "svelte": "^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0", + "typescript": "^4.9.4 || ^5.0.0" + } + }, "node_modules/tailwindcss": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", @@ -5051,9 +5362,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5064,6 +5375,16 @@ "node": ">=14.17" } }, + "node_modules/typescript-auto-import-cache": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.6.tgz", + "integrity": "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.8" + } + }, "node_modules/ufo": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", @@ -5289,6 +5610,94 @@ } } }, + "node_modules/vscode-css-languageservice": { + "version": "6.3.9", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.9.tgz", + "integrity": "sha512-1tLWfp+TDM5ZuVWht3jmaY5y7O6aZmpeXLoHl5bv1QtRsRKt4xYGRMmdJa5Pqx/FTkgRbsna9R+Gn2xE+evVuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-html-languageservice": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.4.0.tgz", + "integrity": "sha512-9/cbc90BSYCghmHI7/VbWettHZdC7WYpz2g5gBK6UDUI1MkZbM773Q12uAYJx9jzAiNHPpyo6KzcwmcnugncAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-nls": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index fde64ea7c..16caa9a61 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,9 +33,11 @@ "prettier-plugin-tailwindcss": "^0.5.9", "svelte": "^4.2.11", "svelte-check": "^3.6.0", + "svelte-language-server": "^0.17.24", + "svelte-preprocess": "^6.0.3", "tailwindcss": "^3.3.6", "tslib": "^2.4.1", - "typescript": "^5.0.0", + "typescript": "^5.9.3", "vite": "^5.0.3", "vitest": "^1.2.0" }, diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 7de7e96b9..111c916db 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,9 +1,15 @@ import type { PlaywrightTestConfig } from '@playwright/test' const config: PlaywrightTestConfig = { - testDir: 'tests', - testMatch: '**/*.ts', - timeout: 640000 // Set global timeout + testDir: 'tests', + testMatch: '**/*.ts', + timeout: 640000 //, // global test timeout + // use: { + // // headless: false, // so you can see the UI + // slowMo: 100, // slow each action by 100ms + // actionTimeout: 30000, // max time per action (click, check, etc.) + // navigationTimeout: 30000 // max time for page.goto and navigation + // } } export default config diff --git a/frontend/src/lib/DesktopBreadcrumb.svelte b/frontend/src/lib/DesktopBreadcrumb.svelte index 938439dd2..76db5df55 100644 --- a/frontend/src/lib/DesktopBreadcrumb.svelte +++ b/frontend/src/lib/DesktopBreadcrumb.svelte @@ -1,6 +1,6 @@ {#if langRegExp.test($page.url.pathname)} @@ -56,40 +55,38 @@ {/if} -{#if $langCodeAndNameStore} - {#if langRegExp.test($page.url.pathname)} -
-
- {getName($langCodeAndNameStore)}({getCode($langCodeAndNameStore)}) -
- -
- {:else} -
({getCode(langCodeAndName)}) +
+ + + {:else} +
-
- {getName($langCodeAndNameStore)}({getCode($langCodeAndNameStore)}) + > +
+ {getName(langCodeAndName)}({getCode(langCodeAndName)}) +
-
- {/if} + {/if} + {/each} {:else}
- Language will appear here selected + Selections will appear here once a language is selected
{/if} diff --git a/frontend/src/lib/passages/MobileBreadcrumb.svelte b/frontend/src/lib/passages/MobileBreadcrumb.svelte index 2f6f4a8b6..8b5dd89c4 100644 --- a/frontend/src/lib/passages/MobileBreadcrumb.svelte +++ b/frontend/src/lib/passages/MobileBreadcrumb.svelte @@ -1,9 +1,8 @@ {#if passagesRegExp.test($page.url.pathname)} @@ -61,8 +61,8 @@ + class:text-[#66768B]={available} + class:text-[#B0B8C3]={!available} + > +
+ {#if passage.endChapter && passage.endChapter > 0 && passage.endChapterVerseRef} + {passage.bookName} + {passage.startChapter}:{passage.startChapterVerseRef}-{passage.endChapter}:{passage.endChapterVerseRef} + ({passage.langCode}) + {:else} + {passage.bookName} + {passage.startChapter}:{passage.startChapterVerseRef} + ({passage.langCode}) + {/if} +
+ {#if available} + + {/if} + + {:else} +
+
+ {#if passage.endChapter && passage.endChapter > 0 && passage.endChapterVerseRef} + ({passage.langCode}) + {passage.bookName} + {passage.startChapter}:{passage.startChapterVerseRef}-{passage.endChapter}:{passage.endChapterVerseRef} + ({passage.langCode}) + {:else} + {passage.bookName} + {passage.startChapter}:{passage.startChapterVerseRef} ({passage.langCode}) + {/if} +
+
{/if} - - {:else} + {/each} + + + {#if hiddenPassages.length > 0}
-
- {#if passage.endChapter && passage.endChapter > 0 && passage.endChapterVerseRef} - {passage.bookName} - {passage.startChapter}:{passage.startChapterVerseRef}-{passage.endChapter}:{passage.endChapterVerseRef} - {:else} - {passage.bookName} - {passage.startChapter}:{passage.startChapterVerseRef} - {/if} + +
+ ({hiddenPassages.length}) items hidden
-
- {/if} - {/each} - - {#if hiddenPassages.length > 0} -
- -
- ({hiddenPassages.length}) items hidden -
-
- {#each hiddenPassages as passage} - {#if passagesRegExp.test($page.url.pathname)} -
+ {#each hiddenPassages as { passage, available }} + {#if passagesRegExp.test($page.url.pathname)} +
-
- {#if passage.endChapter && passage.endChapter > 0 && passage.endChapterVerseRef} - {passage.bookName} - {passage.startChapter}:{passage.startChapterVerseRef}-{passage.endChapter}:{passage.endChapterVerseRef} - {:else} - {passage.bookName} - {passage.startChapter}:{passage.startChapterVerseRef} + class:text-[#66768B]={available} + class:text-[#B0B8C3]={!available} + > +
+ {#if passage.endChapter && passage.endChapter > 0 && passage.endChapterVerseRef} + {passage.bookName} + {passage.startChapter}:{passage.startChapterVerseRef}-{passage.endChapter}:{passage.endChapterVerseRef} + ({passage.langCode}) + {:else} + {passage.bookName} + {passage.startChapter}:{passage.startChapterVerseRef} ({passage.langCode}) + {/if} +
+ {#if available} + {/if}
- {#if isAvailable(passage)} - - {/if} -
- {:else} -
-
- {#if passage.endChapter && passage.endChapter > 0 && passage.endChapterVerseRef} - {passage.bookName} - {passage.startChapter}:{passage.startChapterVerseRef}-{passage.endChapter}:{passage.endChapterVerseRef} - {:else} - {passage.bookName} - {passage.startChapter}:{passage.startChapterVerseRef} - {/if} + > +
+ {#if passage.endChapter && passage.endChapter > 0 && passage.endChapterVerseRef} + ({passage.langCode}) + {passage.bookName} + {passage.startChapter}:{passage.startChapterVerseRef}-{passage.endChapter}:{passage.endChapterVerseRef} + ({passage.langCode}) + {:else} + {passage.bookName} + {passage.startChapter}:{passage.startChapterVerseRef} ({passage.langCode}) + {/if} +
-
- {/if} - {/each} + {/if} + {/each} +
-
- {/if} + {/if} + {:else}
Selections will appear here once a passage is added diff --git a/frontend/src/lib/passages/WizardBreadcrumb.svelte b/frontend/src/lib/passages/WizardBreadcrumb.svelte index 22508148f..a3fca1ab0 100644 --- a/frontend/src/lib/passages/WizardBreadcrumb.svelte +++ b/frontend/src/lib/passages/WizardBreadcrumb.svelte @@ -1,7 +1,6 @@ - diff --git a/frontend/src/routes/passages/language/DesktopLanguageDisplay.svelte b/frontend/src/routes/passages/language/DesktopLanguageDisplay.svelte index cc8d102da..ce1025870 100644 --- a/frontend/src/routes/passages/language/DesktopLanguageDisplay.svelte +++ b/frontend/src/routes/passages/language/DesktopLanguageDisplay.svelte @@ -1,6 +1,11 @@
@@ -21,11 +27,13 @@
handleLangChange(e, langCodeAndName.split(',')[0])} + checked={$languagesClickedOrderStore.includes(langCodeAndName)} + on:change={(e) => handleLangChange(e, langCodeAndName)} class="checkbox-target checkbox-style" + disabled={$langCountStore == maxLanguages && + !$langCodesStore.includes(getCode(langCodeAndName))} /> {getName(langCodeAndName)}
@@ -43,11 +51,13 @@
handleLangChange(e, langCodeAndName.split(',')[0])} + checked={$languagesClickedOrderStore.includes(langCodeAndName)} + on:change={(e) => handleLangChange(e, langCodeAndName)} class="checkbox-target checkbox-style" + disabled={$langCountStore == maxLanguages && + !$langCodesStore.includes(getCode(langCodeAndName))} /> {getName(langCodeAndName)}
diff --git a/frontend/src/routes/passages/language/MobileLanguageDisplay.svelte b/frontend/src/routes/passages/language/MobileLanguageDisplay.svelte index 6fb70c404..1e24d5128 100644 --- a/frontend/src/routes/passages/language/MobileLanguageDisplay.svelte +++ b/frontend/src/routes/passages/language/MobileLanguageDisplay.svelte @@ -1,6 +1,11 @@
@@ -22,11 +28,13 @@
handleLangChange(e, langCodeAndName)} class="checkbox-target checkbox-style" + disabled={$langCountStore == maxLanguages && + !$langCodesStore.includes(getCode(langCodeAndName))} /> {getName(langCodeAndName)}
@@ -46,11 +54,13 @@
handleLangChange(e, langCodeAndName)} class="checkbox-target checkbox-style" + disabled={$langCountStore == maxLanguages && + !$langCodesStore.includes(getCode(langCodeAndName))} /> {getName(langCodeAndName)}
diff --git a/frontend/src/routes/passages/passages/+page.svelte b/frontend/src/routes/passages/passages/+page.svelte index f997ca7a8..8e8db5120 100644 --- a/frontend/src/routes/passages/passages/+page.svelte +++ b/frontend/src/routes/passages/passages/+page.svelte @@ -1,28 +1,30 @@ @@ -60,17 +72,15 @@
-
+

Add Passages

- {#if !bookCodesAndNames || bookCodesAndNames.length === 0} + {#if !bookCodesAndNamesLang0 || bookCodesAndNamesLang0.length === 0}
- +
{:else} - + {#if windowWidth < TAILWIND_SM_MIN_WIDTH}
+
{#if showWizardBasketModal} diff --git a/frontend/src/routes/passages/passages/AddNTComponent.svelte b/frontend/src/routes/passages/passages/AddNTComponent.svelte index 35287ec6a..5e4b901d3 100644 --- a/frontend/src/routes/passages/passages/AddNTComponent.svelte +++ b/frontend/src/routes/passages/passages/AddNTComponent.svelte @@ -1,24 +1,31 @@ {#if showNT} -
+
+{:else} + {/if}