Implement Phase 2 performance optimizations

1. Replace regex markdown with mistune library
   - Add mistune>=3.0.2 dependency
   - Replace custom regex patterns with proper markdown parser
   - Better performance and more robust parsing
   - Supports full markdown syntax (bold, italic, strikethrough, etc.)

2. Cache story counts
   - Cache get_story_count() and get_category_count()
   - Expected: 10-20x faster story index page loads
   - Added cache invalidation to refresh_stories()

3. Fix O(n) pattern in helpers.py
   - Replace manual chapter filtering with bible.get_chapters_for_book()
   - Uses existing @lru_cache for instant lookups

Combined expected improvement: 10-20% on story pages, faster markdown

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-27 12:06:13 -05:00
parent 6c4c23a83e
commit 7522b27e7c
5 changed files with 51 additions and 20 deletions
+20 -14
View File
@@ -189,27 +189,33 @@ templates = Jinja2Templates(directory=str(templates_dir))
# Register custom Jinja2 filters
templates.env.filters['slugify'] = create_slug
# Initialize mistune for markdown rendering
import mistune
# Create mistune instance for full markdown (with paragraphs)
_markdown = mistune.create_markdown(escape=False, hard_wrap=False)
# Create inline renderer for markdown without paragraph wrapping
_inline_markdown = mistune.create_markdown(
renderer=mistune.HTMLRenderer(escape=False),
plugins=['strikethrough']
)
def markdown_inline(text):
"""Convert inline markdown to HTML (bold and italic only, no paragraph wrapping)."""
"""Convert inline markdown to HTML (bold, italic, etc. - no paragraph wrapping)."""
if not text:
return text
# Convert **bold** to <strong>bold</strong> (must be done before italic)
text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
# Convert *italic* to <em>italic</em>
text = re.sub(r'\*(.+?)\*', r'<em>\1</em>', text)
return text
# Render and strip any outer <p> tags that mistune might add
html = _inline_markdown(text).strip()
if html.startswith('<p>') and html.endswith('</p>'):
html = html[3:-4]
return html
def markdown_to_html(text):
"""Convert simple markdown to HTML (bold, italic, and paragraphs)."""
"""Convert markdown to HTML (bold, italic, paragraphs, etc.)."""
if not text:
return text
# First apply inline markdown
text = markdown_inline(text)
# Convert paragraphs (split on double newlines)
paragraphs = text.split('\n\n')
# Always wrap in <p> tags
text = ''.join(f'<p>{p.strip()}</p>' for p in paragraphs if p.strip())
return text
return _markdown(text).strip()
templates.env.filters['md'] = markdown_to_html
templates.env.filters['mdi'] = markdown_inline
+18 -5
View File
@@ -86,14 +86,25 @@ def get_category_by_slug(category_slug: str) -> Optional[dict]:
return None
# Cache story counts
_CACHED_STORY_COUNT = None
_CACHED_CATEGORY_COUNT = None
def get_story_count() -> int:
"""Get total number of stories."""
return len(get_all_stories_flat())
"""Get total number of stories (cached)."""
global _CACHED_STORY_COUNT
if _CACHED_STORY_COUNT is None:
_CACHED_STORY_COUNT = len(get_all_stories_flat())
return _CACHED_STORY_COUNT
def get_category_count() -> int:
"""Get total number of categories."""
return len(load_all_stories())
"""Get total number of categories (cached)."""
global _CACHED_CATEGORY_COUNT
if _CACHED_CATEGORY_COUNT is None:
_CACHED_CATEGORY_COUNT = len(load_all_stories())
return _CACHED_CATEGORY_COUNT
# Pre-load categories on module import for faster access
@@ -110,5 +121,7 @@ def get_categories() -> list[dict]:
def refresh_stories():
"""Refresh the cached stories (useful after adding new files)."""
global STORY_CATEGORIES
global STORY_CATEGORIES, _CACHED_STORY_COUNT, _CACHED_CATEGORY_COUNT
STORY_CATEGORIES = load_all_stories()
_CACHED_STORY_COUNT = None
_CACHED_CATEGORY_COUNT = None
+1 -1
View File
@@ -208,7 +208,7 @@ def get_chapter_popularity_score(book: str, chapter: int) -> int:
if book in HIGH_READERSHIP_BOOKS:
default_score += 1
total_chapters = len([ch for bk, ch in bible.iter_chapters() if bk == book])
total_chapters = len(bible.get_chapters_for_book(book))
if total_chapters <= 5:
default_score += 1
+1
View File
@@ -8,6 +8,7 @@ dependencies = [
"biblepy>=0.1.3",
"fastapi[standard]>=0.115.12",
"ged4py>=0.5.2",
"mistune>=3.0.2",
"parse>=1.20.2",
"python-gedcom>=1.0.0",
"requests>=2.32.3",
Generated
+11
View File
@@ -483,6 +483,7 @@ dependencies = [
{ name = "biblepy" },
{ name = "fastapi", extra = ["standard"] },
{ name = "ged4py" },
{ name = "mistune" },
{ name = "parse" },
{ name = "python-gedcom" },
{ name = "requests" },
@@ -506,6 +507,7 @@ requires-dist = [
{ name = "biblepy", specifier = ">=0.1.3" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
{ name = "ged4py", specifier = ">=0.5.2" },
{ name = "mistune", specifier = ">=3.0.2" },
{ name = "parse", specifier = ">=1.20.2" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.5" },
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.0.0" },
@@ -568,6 +570,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "mistune"
version = "3.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/02/a7fb8b21d4d55ac93cdcde9d3638da5dd0ebdd3a4fed76c7725e10b81cbe/mistune-3.1.4.tar.gz", hash = "sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164", size = 94588, upload-time = "2025-08-29T07:20:43.594Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/f0/8282d9641415e9e33df173516226b404d367a0fc55e1a60424a152913abc/mistune-3.1.4-py3-none-any.whl", hash = "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d", size = 53481, upload-time = "2025-08-29T07:20:42.218Z" },
]
[[package]]
name = "packaging"
version = "25.0"