Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
.PHONY: startdocker
startdocker:
@if docker info >/dev/null 2>&1; then \
echo "Docker is already running."; \
else \
echo "Starting Docker..."; \
if [ "$$(uname)" = "Darwin" ]; then \
open -a Docker; \
else \
sudo systemctl start docker; \
fi; \
echo "Waiting for Docker to fully initialize..."; \
until docker info >/dev/null 2>&1; do sleep 1; done; \
echo "Docker is ready!"; \
fi
Comment thread
PurpleGuitar marked this conversation as resolved.

.PHONY: checkvenv
checkvenv:
checkvenv: startdocker
# raises error if environment is not active
ifeq ("$(VIRTUAL_ENV)","")
@echo "Venv is not activated!"
Expand Down Expand Up @@ -133,7 +149,7 @@ e2e-docx-tests: clean-local-docker-output-dir


.PHONY: frontend-tests
frontend-tests:
frontend-tests: startdocker
# NOTE If we are experiencing some issues with the docker
# compose running of frontend tests, we can still use the
# non-Dockerized approach successfully. Doing so requires that
Expand Down Expand Up @@ -184,7 +200,7 @@ clean-mypyc-artifacts:
find . ! -path .venv -type f -name "*.c" -exec rm -- {} +

.PHONY: prune-docker-images-volumes
prune-docker-images-volumes:
prune-docker-images-volumes: checkvenv
docker system prune --volumes

# https://radon.readthedocs.io/en/latest/commandline.html
Expand Down Expand Up @@ -301,7 +317,8 @@ local-run-celery:

.PHONY: local-run-flower
local-run-flower:
celery --broker=redis:// --result-backend=redis:// flower
celery --broker=redis:// --result-backend=redis:// flower

# This is one to run after running local-e2e-tests or any tests which
# has yielded HTML and PDFs that need to be checked for linking
# correctness.
Expand Down
2 changes: 2 additions & 0 deletions backend/doc/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class Settings(BaseSettings):

DATA_API_URL: HttpUrl

LANGUAGES_WHERE_NON_ULB_PREFERRED: Sequence[str] = ["fr"]

# This is only used to see if a lang_code is in the collection
# otherwise it is a heart language. Eventually the graphql data api may
# provide gateway/heart boolean value.
Expand Down
10 changes: 6 additions & 4 deletions backend/doc/domain/assembly_strategies/assemble_by_book.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
TWBook,
USFMBook,
)
from doc.domain.parsing import handle_split_chapter_into_verses
from doc.domain.parsing import split_chapter_into_verses_with_formatting
from doc.reviewers_guide.model import RGBook

logger = settings.logger(__name__)
Expand Down Expand Up @@ -475,7 +475,7 @@ def assemble_usfm_by_verse_book_at_a_time(
chapter_num,
chapter,
) in usfm_book.chapters.items():
chapter.verses = handle_split_chapter_into_verses(usfm_book, chapter)
chapter.verses = split_chapter_into_verses_with_formatting(chapter)
chapter_intros = get_chapter_intros(
tn_book,
tnc_book,
Expand Down Expand Up @@ -531,8 +531,10 @@ def assemble_usfm_by_verse_book_at_a_time(
# ulb, f10, show the second USFM content here
if usfm_book2:
usfm_book2_chapter = usfm_book2.chapters[chapter_num]
usfm_book2_chapter.verses = handle_split_chapter_into_verses(
usfm_book2, usfm_book2_chapter
usfm_book2_chapter.verses = (
split_chapter_into_verses_with_formatting(
usfm_book2_chapter
)
)
if (
usfm_book2_chapter.verses
Expand Down
11 changes: 5 additions & 6 deletions backend/doc/domain/assembly_strategies/assemble_by_chapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,9 @@
TWBook,
USFMBook,
)
from doc.domain.parsing import handle_split_chapter_into_verses
from doc.domain.parsing import split_chapter_into_verses_with_formatting
from doc.reviewers_guide.model import RGBook


logger = settings.logger(__name__)


Expand Down Expand Up @@ -625,13 +624,13 @@ def assemble_usfm_by_verse_chapter_at_a_time(
primary_verses = None
secondary_verses = None
if usfm_book and usfm_chapter:
usfm_chapter.verses = handle_split_chapter_into_verses(
usfm_book, usfm_chapter
usfm_chapter.verses = split_chapter_into_verses_with_formatting(
usfm_chapter
)
primary_verses = usfm_chapter.verses
if usfm_book2 and usfm_book2_chapter:
usfm_book2_chapter.verses = handle_split_chapter_into_verses(
usfm_book2, usfm_book2_chapter
usfm_book2_chapter.verses = split_chapter_into_verses_with_formatting(
usfm_book2_chapter
)
secondary_verses = usfm_book2_chapter.verses
if usfm_book and primary_verses:
Expand Down
10 changes: 10 additions & 0 deletions backend/doc/domain/document_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from doc.utils.docx_util import (
add_internal_docx_links,
generate_docx_toc,
override_and_clean_hyperlinks,
style_superscripts,
)
from doc.utils.file_utils import (
Expand Down Expand Up @@ -778,6 +779,15 @@ def compose_docx_document(
if part.add_page_break:
add_page_break(doc)
style_superscripts(doc, lift_half_points=2, color=None)
# html4doc defeats normal use of hyperlink inline styling in Word
# via a customized (otherwise standard) Hyperlink style, but
# this handles it by doing a pass over the document after the fact
# and forcing hyperlinks to render using a specific style we
# created to match the PO's desired look.
# PlainHyperlinkChar is a character style we created in
# template.docx, but its actual style ID is PlainHyperlinkChar0
# under the hood.
override_and_clean_hyperlinks(doc, "PlainHyperlinkChar0")
t1 = time.time()
logger.info("Time for converting HTML to Docx: %.2f seconds", t1 - t0)
return doc
Expand Down
159 changes: 32 additions & 127 deletions backend/doc/domain/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
This module provides an API for parsing content.
"""

from bs4 import BeautifulSoup, NavigableString
from re import (
compile,
escape,
Expand Down Expand Up @@ -276,14 +277,19 @@ def split_usfm_by_chapters(
chapters = re_split(chapter_regex, usfm_text)
frontmatter = chapters.pop(0).strip()

defective_lang_codes = {resource[0] for resource in resources_with_usfm_defects}

def needs_fixing() -> bool:
"""
Determine if a chapter needs fixing based on configuration.
Determine whether this resource should be checked for known USFM defects.

If CHECK_ALL_BOOKS_FOR_LANGUAGE is enabled, then the presence of any
known defective resource for a language causes all books for that
language to be checked for similar defects. Otherwise, only explicitly
listed resource tuples are checked.
"""
if check_all_books_for_language:
return lang_code in [
resource[0] for resource in resources_with_usfm_defects
]
return lang_code in defective_lang_codes
return (
lang_code,
resource_type,
Expand Down Expand Up @@ -527,9 +533,7 @@ def usfm_book_content(
cleaned_chapter_html_content_ = remove_null_bytes_and_control_characters(
chapter_html_content
)
cleaned_chapter_html_content = remove_unwanted_elements(
cleaned_chapter_html_content_
)
cleaned_chapter_html_content = clean_content_html(cleaned_chapter_html_content_)
usfm_chapters[chapter_num] = USFMChapter(
content=(
cleaned_chapter_html_content if cleaned_chapter_html_content else ""
Expand All @@ -544,7 +548,7 @@ def usfm_book_content(
national_book_name=(
localized_book_name
if localized_book_name
else BOOK_NAMES[resource_lookup_dto.book_code]
else book_names[resource_lookup_dto.book_code]
),
resource_type_name=resource_lookup_dto.resource_type_name,
chapters=usfm_chapters if usfm_chapters else {},
Expand Down Expand Up @@ -1408,72 +1412,13 @@ def lookup_verse_text(usfm_book: USFMBook, chapter_num: int, verse_ref: str) ->
return verse


# Used by STET and PASSAGES apps
def split_chapter_into_verses(chapter: USFMChapter) -> dict[str, str]:
# Sample HTML content with multiple verse elements
# html_content = '''
# <span class="verse">
# <sup class="versemarker">19</sup>
# For through the law I died to the law, so that I might live for God. I have been crucified with Christ.
# <sup id="footnote-caller-1" class="caller"><a href="#footnote-target-1">1</a></sup>
# <div class="sectionhead-5"></div>
# </span>
# <span class="verse">
# <sup class="versemarker">20</sup>
# I have been crucified with Christ and I no longer live, but Christ lives in me. The life I now live in the body, I live by faith in the Son of God, who loved me and gave himself for me.
# <sup id="footnote-caller-2" class="caller"><a href="#footnote-target-2">2</a></sup>
# <div class="sectionhead-5"></div>
# </span>
# '''
verse_dict = {}
# Find all verse spans
verse_spans = findall(r'<span class="verse">(.*?)</span>', chapter.content, DOTALL)
for verse_span in verse_spans:
# Extract the verse number from the versemarker
verse_number = search(r'<sup class="versemarker">(\d+)</sup>', verse_span)
if verse_number:
verse_number_ = verse_number.group(1)
# Remove versemarker
verse_text = sub(r'<sup class="versemarker">.*?</sup>', "", verse_span)
# Remove footnotes numbers
verse_text = sub(r'<sup id=".*?" class="caller">.*?</sup>', "", verse_text)
Comment thread
PurpleGuitar marked this conversation as resolved.
# Fix spacing issue when div class="poetry-*" type divs
# are used, e.g., yielding 'heartsas' for Hebrews 3:8
verse_text = sub(
r'<div class="poetry-\d">(.*?)</div>',
r" \1",
verse_text,
)
Comment thread
PurpleGuitar marked this conversation as resolved.
# Add to the dictionary with verse number as the key and verse text as the value
verse_dict[verse_number_] = verse_text
return verse_dict


def handle_split_chapter_into_verses(
usfm_book: USFMBook,
usfm_chapter: USFMChapter,
resource_type_codes_and_names: Mapping[
str, str
] = settings.RESOURCE_TYPE_CODES_AND_NAMES,
) -> dict[VerseRef, str]:
if (
usfm_book.lang_code == "fr"
and usfm_book.resource_type_name == resource_type_codes_and_names["f10"]
):
return split_chapter_into_verses_with_formatting_for_f10(usfm_chapter)
else:
return split_chapter_into_verses_with_formatting(usfm_chapter)


def split_chapter_into_verses_with_formatting(
chapter: USFMChapter,
empty_paragraph: str = "<p></p>",
sectionhead5_element: str = '<div class="sectionhead-5"></div>',
) -> dict[VerseRef, str]:
"""
Given a USFMChapter instance, return the same instance with its
verses attribute set to a dictionary where the key is the verse
number and the value is the verse HTML.
Parse chapter.content as HTML, extract each <span class="verse">,
unwrap <span class="word-entry"> elements (preserving their text),
and return a dict mapping verse number -> cleaned HTML fragment for that verse.

Sample HTML content with multiple verse elements:

Expand All @@ -1500,74 +1445,34 @@ def split_chapter_into_verses_with_formatting(
<sup id="footnote-caller-1" class="caller"><a href="#footnote-target-1">1</a></sup>
<BLANKLINE>
"""
# TODO What to do about footnote targets? Perhaps have the value be a
# tuple with first element of the verse HTML (which includes the
# footnote callers) and the second element the target footnotes HTML?
verse_dict = {}
# Find all verse spans
verse_spans = findall(r'<span class="verse">(.*?)</span>', chapter.content, DOTALL)
for verse_span in verse_spans:
# Extract the verse number from the versemarker
verse_number = search(r'<sup class="versemarker">(\d+)</sup>', verse_span)
if verse_number:
verse_number_ = verse_number.group(1)
# Add to the dictionary with verse number as the key and verse text as the value
verse_dict[verse_number_] = (
verse_span.strip()
.replace(empty_paragraph, "")
.replace(sectionhead5_element, "")
)
return verse_dict


def split_chapter_into_verses_with_formatting_for_f10(
chapter: USFMChapter,
empty_paragraph: str = "<p></p>",
sectionhead5_element: str = '<div class="sectionhead-5"></div>',
) -> dict[str, str]:
"""
Parse chapter.content as HTML, extract each <span class="verse">,
unwrap <span class="word-entry"> elements (preserving their text),
and return a dict mapping verse number -> cleaned HTML fragment for that verse.
"""
soup = BeautifulSoup(chapter.content, "html.parser")
verse_dict: dict[str, str] = {}
# find all verse spans (parser handles nesting correctly)
verse_dict: dict[VerseRef, str] = {}
for verse_span in soup.find_all("span", class_="verse"):
# find the verse number from <sup class="versemarker">NN</sup>
sup = verse_span.find("sup", class_="versemarker")
if not sup or not sup.string:
continue
verse_number = sup.string.strip()
# unwrap all word-entry spans: replace <span class="word-entry">X</span>
# with X (preserving whitespace/punctuation)
# Remove the versemarker number sup
sup.decompose()
# fr f10 uses word-entry tags
for we in verse_span.find_all("span", class_="word-entry"):
we.unwrap()
# Option: normalize whitespace (optional)
# If you want to preserve original spacing/punctuation exactly, skip this.
# cleaned_html = "".join(str(c) for c in verse_span.contents)
cleaned_html = str(verse_span)
# Fix spacing issues introduced by inner spans
cleaned_html = sub(
r"\s+([,;:.!?])", r"\1", cleaned_html
) # remove space before punctuation
cleaned_html = sub(r"\s+'", "'", cleaned_html) # remove space before apostrophe
cleaned_html = sub(r"'\s+", "'", cleaned_html) # remove space after apostrophe
cleaned_html = sub(
r"\s*-\s*", "-", cleaned_html
) # normalize spaces around hyphens
cleaned_html = sub(r"\s{2,}", " ", cleaned_html) # collapse double spaces
cleaned_html = cleaned_html.strip()
# if you want plain text instead, use: cleaned_text = verse_span.get_text(" ", strip=True)
# store cleaned HTML fragment (still contains <sup> etc.)
verse_dict[verse_number] = (
cleaned_html.strip()
.replace(empty_paragraph, "")
.replace(sectionhead5_element, "")
)
cleaned_html = clean_content_html(str(verse_span))
verse_dict[verse_number] = cleaned_html
return verse_dict


def clean_content_html(raw_content: str) -> str:
Comment thread
PurpleGuitar marked this conversation as resolved.
soup = BeautifulSoup(raw_content, "html.parser")
cleaned_html = str(soup)
cleaned_html = sub(r"\s+([,;:.!?])", r"\1", cleaned_html)
cleaned_html = sub(r"\s+'", "'", cleaned_html)
cleaned_html = sub(r"'\s+", "'", cleaned_html)
cleaned_html = sub(r"\s*-\s*", "-", cleaned_html)
cleaned_html = sub(r"\s{2,}", " ", cleaned_html).strip()
return cleaned_html


if __name__ == "__main__":

# To run the doctests in this module, in the root of the project do:
Expand Down
Loading
Loading