Files
kjvstudy.org/kjvstudy_org/templates/verse.html
T
kennethreitz 1b07aac579 Revert MiniJinja, keep link_names regex optimization
MiniJinja's finalizer workaround (needed for Jinja2-compatible escaping)
added 3x overhead per variable, negating the Rust speed advantage.
Back to Jinja2 with the 17x faster single-regex link_names filter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 14:19:33 -04:00

1618 lines
46 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ book }} {{ chapter }}:{{ verse_num }} - KJV Bible{% endblock %}
{% block description %}{{ verse_text[:155] if verse_text else 'Study the Authorized King James Version Bible' }}{% endblock %}
{% block og_type %}article{% endblock %}
{% block og_title %}{{ book }} {{ chapter }}:{{ verse_num }} - KJV Bible{% endblock %}
{% block og_description %}{{ verse_text[:150] if verse_text else '' }}...{% endblock %}
{% block og_image %}https://kjvstudy.org/og/verse/{{ book | urlencode }}/{{ chapter }}/{{ verse_num }}.png{% endblock %}
{% block twitter_image %}https://kjvstudy.org/og/verse/{{ book | urlencode }}/{{ chapter }}/{{ verse_num }}.png{% endblock %}
{% block structured_data %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": {{ (book ~ " " ~ chapter|string ~ ":" ~ verse_num|string) | tojson }},
"articleBody": {{ verse_text | tojson }},
"author": {
"@type": "Organization",
"name": "KJV Study"
},
"publisher": {
"@type": "Organization",
"name": "KJV Study",
"url": "https://kjvstudy.org"
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://kjvstudy.org/book/{{ book }}/chapter/{{ chapter }}/verse/{{ verse_num }}"
},
"isPartOf": {
"@type": "Book",
"name": {{ book | tojson }},
"@id": "https://kjvstudy.org/book/{{ book }}"
}
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://kjvstudy.org"
},
{
"@type": "ListItem",
"position": 2,
"name": "Books",
"item": "https://kjvstudy.org/books"
},
{
"@type": "ListItem",
"position": 3,
"name": {{ book | tojson }},
"item": "https://kjvstudy.org/book/{{ book | urlencode }}"
},
{
"@type": "ListItem",
"position": 4,
"name": "Chapter {{ chapter }}",
"item": "https://kjvstudy.org/book/{{ book | urlencode }}/chapter/{{ chapter }}"
},
{
"@type": "ListItem",
"position": 5,
"name": "Verse {{ verse_num }}"
}
]
}
</script>
{% endblock %}
{% block head %}
<style>
/* Action buttons */
.chapter-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 1rem 0;
}
.action-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.9rem;
font-size: 0.9rem;
color: var(--text-secondary);
background: var(--code-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.action-btn:hover {
background: var(--bg-color);
border-color: var(--link-color);
color: var(--link-color);
text-decoration: none;
}
.action-btn svg {
width: 16px;
height: 16px;
}
/* Red Letter Edition - Words of Christ */
.words-of-christ {
color: #c41e3a;
}
/* Dark mode for .words-of-christ is in base.html */
/* Links inside red letter text should also be red */
.words-of-christ a {
color: #c41e3a;
text-decoration: underline;
}
[data-theme="dark"] .words-of-christ a {
color: #ff6b6b;
}
/* Prevent sidenotes from inheriting red color */
.words-of-christ .sidenote,
.words-of-christ .marginnote,
.words-of-christ .sidenote-number,
.words-of-christ label.sidenote-number {
color: var(--text-color);
}
[data-theme="dark"] .words-of-christ .sidenote,
[data-theme="dark"] .words-of-christ .marginnote,
[data-theme="dark"] .words-of-christ .sidenote-number,
[data-theme="dark"] .words-of-christ label.sidenote-number {
color: var(--text-color);
}
/* Prevent word definitions from inheriting red color */
.words-of-christ .word-original,
.words-of-christ .word-english,
.words-of-christ .word-definition {
color: #111;
}
.words-of-christ .word-transliteration,
.words-of-christ .word-parsing {
color: #666;
}
.words-of-christ .word-strongs {
color: #0066cc;
}
[data-theme="dark"] .words-of-christ .word-original,
[data-theme="dark"] .words-of-christ .word-english,
[data-theme="dark"] .words-of-christ .word-definition {
color: #ddd;
}
[data-theme="dark"] .words-of-christ .word-transliteration,
[data-theme="dark"] .words-of-christ .word-parsing {
color: #999;
}
[data-theme="dark"] .words-of-christ .word-strongs {
color: #6699ff;
}
.verse-text {
font-size: 1.8rem;
line-height: 2.4rem;
margin: 2rem 0;
font-style: italic;
}
.verse-reference {
color: #666;
font-size: 1.2rem;
margin-bottom: 0.5rem;
}
.share-section {
max-width: 65%;
margin: 1.5rem 0 2rem 0;
}
.share-section .section-heading {
font-size: 1.4rem;
font-weight: 600;
color: var(--text-color);
text-transform: none;
letter-spacing: normal;
}
.share-pills {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
padding: 0.75rem 0;
}
.share-pill {
display: inline-block;
padding: 0.25rem 0.6rem;
font-size: 0.85rem;
font-family: inherit;
color: var(--text-secondary);
background: transparent;
border: 1px solid var(--border-color-darker);
border-radius: 12px;
text-decoration: none;
cursor: pointer;
transition: all 0.15s ease;
}
.share-pill:hover {
border-color: var(--link-color);
color: var(--link-color);
background: rgba(74, 124, 89, 0.05);
}
.share-pill.copied {
border-color: var(--link-color);
color: var(--link-color);
}
[data-theme="dark"] .share-pill {
border-color: #444;
}
[data-theme="dark"] .share-pill:hover {
background: rgba(74, 124, 89, 0.15);
}
[data-font-size="large"] .share-pill {
font-size: 1rem;
padding: 0.3rem 0.75rem;
}
/* Generic collapsible section toggle */
.section-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.5rem 0;
background: none;
border: none;
cursor: pointer;
text-align: left;
font-family: inherit;
transition: opacity 0.2s;
}
.section-toggle:hover {
opacity: 0.7;
}
.section-toggle:focus {
outline: 2px solid var(--link-color);
outline-offset: 2px;
}
.section-toggle .toggle-icon {
font-size: 0.8rem;
color: var(--text-secondary);
transition: transform 0.2s;
}
.section-heading {
font-size: 0.9rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
}
.share-label {
font-size: 0.9rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.75rem;
font-weight: 600;
}
.cross-references-section .section-heading {
font-size: 1.4rem;
text-transform: none;
letter-spacing: normal;
color: var(--text-color);
}
.crossrefs-compact {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
padding: 0.75rem 0;
max-width: 65%;
}
.crossref-pill {
display: inline-block;
padding: 0.25rem 0.6rem;
background: transparent;
border: 1px solid var(--border-color);
border-radius: 15px;
font-size: 0.85rem;
color: var(--text-secondary);
text-decoration: none;
transition: all 0.15s;
white-space: nowrap;
position: relative;
}
a.crossref-pill:hover {
border-color: var(--link-color);
color: var(--link-color);
background: rgba(74, 124, 89, 0.05);
}
.crossref-pill .cross-ref-tooltip {
left: 0;
transform: none;
min-width: 200px;
max-width: 350px;
white-space: normal;
}
[data-theme="dark"] .crossref-pill {
border-color: #444;
}
[data-theme="dark"] a.crossref-pill:hover {
background: rgba(74, 124, 89, 0.15);
}
/* Large font size mode */
[data-font-size="large"] .verse-text {
font-size: 2.4rem;
line-height: 3.2rem;
}
[data-font-size="large"] .word-english {
font-size: 1.1rem;
}
[data-font-size="large"] .word-original {
font-size: 2rem;
}
[data-font-size="large"] .word-original.hebrew {
font-size: 2.2rem;
}
[data-font-size="large"] .word-original.greek {
font-size: 2rem;
}
[data-font-size="large"] .word-strongs {
font-size: 0.85rem;
}
[data-font-size="large"] .crossref-pill {
font-size: 1.1rem;
padding: 0.35rem 0.8rem;
}
[data-font-size="large"] .crossrefs-compact {
gap: 0.5rem;
}
/* Verse navigation pills */
.verse-nav-pills {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin: 1.5rem 0;
}
.nav-pill {
display: inline-block;
padding: 0.3rem 0.7rem;
background: transparent;
border: 1px solid var(--border-color);
border-radius: 20px;
font-size: 0.85rem;
color: var(--text-secondary);
text-decoration: none;
transition: all 0.15s;
white-space: nowrap;
}
.nav-pill:hover {
border-color: var(--link-color);
color: var(--link-color);
background: rgba(74, 124, 89, 0.05);
}
[data-theme="dark"] .nav-pill {
border-color: #444;
}
[data-theme="dark"] .nav-pill:hover {
background: rgba(74, 124, 89, 0.15);
}
[data-font-size="large"] .nav-pill {
font-size: 1.1rem;
padding: 0.4rem 0.9rem;
}
[data-font-size="large"] .verse-nav-pills {
gap: 0.5rem;
}
.share-buttons {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.share-btn {
padding: 0.5rem 1rem;
font-size: 0.9rem;
font-weight: 600;
border: 1px solid var(--border-color-darker);
border-radius: 4px;
background: var(--bg-color);
color: var(--text-color);
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.share-btn:hover {
background: var(--code-bg);
border-color: var(--link-color);
color: var(--link-color);
}
.share-btn.copied {
border-color: var(--link-color);
color: var(--link-color);
}
/* Cross-reference tooltip styles */
.cross-ref-link {
position: relative;
cursor: help;
}
.cross-ref-tooltip {
display: none;
position: absolute;
bottom: 100%;
left: 0;
min-width: 300px;
max-width: 400px;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
background: var(--bg-color);
border: 1px solid var(--border-color-darker);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-color);
}
.cross-ref-link:hover .cross-ref-tooltip {
display: block;
}
.tooltip-verse-text {
font-weight: 600;
font-style: italic;
color: var(--text-secondary);
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--border-color);
}
[data-theme="dark"] .cross-ref-tooltip {
background: var(--bg-color);
border-color: var(--border-color-darker);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
/* Interlinear styles - inline flow design */
.interlinear-container {
max-width: 100%;
margin: 2rem 0;
}
/* Collapsible toggle button */
.interlinear-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.75rem 0;
background: none;
border: none;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
text-align: left;
font-family: inherit;
transition: background 0.2s;
}
.interlinear-toggle:hover {
background: var(--code-bg);
}
.interlinear-toggle:focus {
outline: 2px solid var(--link-color);
outline-offset: 2px;
}
.toggle-icon {
font-size: 0.8rem;
color: var(--text-secondary);
transition: transform 0.2s;
}
.interlinear-toggle.expanded .toggle-icon {
transform: rotate(0deg);
}
.toggle-hint {
font-size: 0.85rem;
color: var(--text-tertiary);
font-weight: normal;
}
.interlinear-heading {
font-size: 1.4rem;
font-weight: 600;
margin: 0;
color: var(--text-color);
text-transform: none;
letter-spacing: normal;
}
.interlinear-flow {
line-height: 1.4;
font-size: 1rem;
margin: 0.75rem 0;
padding: 0.5rem 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem 0.25rem;
align-items: flex-start;
}
.word-unit {
display: inline-flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 0.1rem;
cursor: pointer;
position: relative;
padding: 0.3rem 0.5rem;
border-radius: 4px;
transition: all 0.15s ease;
border: 1px solid transparent;
vertical-align: top;
}
.word-unit:hover {
background: #f8f9fa;
border-color: #e0e0e0;
}
.word-unit.expanded {
background: #f0f7f4;
border-color: #4a7c59;
z-index: 101;
}
.word-original {
font-size: 1.6rem;
font-weight: 400;
color: #222;
line-height: 1.3;
transition: color 0.2s;
}
/* Hebrew text (RTL) */
.word-original.hebrew {
direction: rtl;
font-family: "SBL Hebrew", "Ezra SIL", "Times New Roman", "Noto Serif Hebrew", serif;
font-size: 1.8rem;
}
/* Greek text */
.word-original.greek {
font-family: "SBL Greek", "Gentium Plus", "Times New Roman", "Noto Serif", serif;
font-size: 1.6rem;
}
.word-unit:hover .word-original {
color: #111;
}
.word-english {
font-size: 0.7rem;
color: #4a7c59;
font-weight: 500;
line-height: 1.2;
max-width: 100px;
word-wrap: break-word;
}
.word-strongs {
font-size: 0.6rem;
color: #888;
font-family: monospace;
line-height: 1.2;
}
.word-strongs a {
color: inherit;
text-decoration: none;
}
.word-strongs a:hover {
color: #4a7c59;
}
/* Hover tooltip - full definition preview */
.word-tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(-4px);
background: #333;
color: #fff;
padding: 0.65rem 1rem;
border-radius: 4px;
font-size: 0.95rem;
white-space: normal;
max-width: 380px;
min-width: 200px;
text-align: center;
line-height: 1.5;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s, transform 0.15s;
z-index: 999999999;
}
.word-tooltip::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #333;
}
.word-unit:hover .word-tooltip {
opacity: 1;
transform: translateX(-50%) translateY(-8px);
}
.word-unit.expanded .word-tooltip {
opacity: 0;
}
/* Full detail panel */
.word-detail {
display: none;
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%) translateY(8px);
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem 1.25rem;
min-width: 260px;
max-width: 320px;
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
z-index: 100;
text-align: left;
line-height: 1.5;
opacity: 0;
transition: opacity 0.2s, transform 0.2s;
}
.word-unit.expanded .word-detail {
display: block;
opacity: 1;
transform: translateX(-50%) translateY(12px);
}
.word-detail-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid #eee;
}
.word-detail-original {
font-size: 2.2rem;
font-weight: 400;
color: #222;
line-height: 1.3;
}
.word-detail-original.hebrew {
direction: rtl;
font-family: "SBL Hebrew", "Ezra SIL", "Times New Roman", "Noto Serif Hebrew", serif;
font-size: 2.4rem;
}
.word-detail-original.greek {
font-family: "SBL Greek", "Gentium Plus", "Times New Roman", "Noto Serif", serif;
font-size: 2.2rem;
}
.word-detail-english {
font-size: 1rem;
color: #4a7c59;
font-weight: 600;
}
.word-detail-row {
display: flex;
margin-bottom: 0.35rem;
font-size: 0.85rem;
}
.word-detail-row:last-child {
margin-bottom: 0;
}
.word-detail-label {
color: #888;
min-width: 85px;
flex-shrink: 0;
}
.word-detail-value {
color: #333;
}
.word-detail-definition {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid #eee;
font-size: 0.85rem;
color: #555;
line-height: 1.6;
}
.interlinear-link {
display: inline-block;
margin-top: 0.5rem;
font-size: 0.9rem;
color: #4a7c59;
text-decoration: none;
}
.interlinear-link:hover {
text-decoration: underline;
}
/* Dark mode for interlinear */
[data-theme="dark"] .word-original {
color: #eee;
}
[data-theme="dark"] .word-unit:hover .word-original {
color: #fff;
}
[data-theme="dark"] .word-unit:hover {
background: #2a2a2a;
border-color: #444;
}
[data-theme="dark"] .word-unit.expanded {
background: #1a2e1a;
border-color: #4a7c59;
}
[data-theme="dark"] .word-detail {
background: #222;
border-color: #444;
}
[data-theme="dark"] .word-detail-original {
color: #eee;
}
[data-theme="dark"] .word-detail-value {
color: #ddd;
}
[data-theme="dark"] .word-detail-definition {
color: #bbb;
border-top-color: #444;
}
[data-theme="dark"] .word-detail-header {
border-bottom-color: #444;
}
[data-theme="dark"] .interlinear-heading {
color: #ddd;
border-bottom-color: #444;
}
/* Only apply OS dark mode when explicit theme is not set to light */
@media (prefers-color-scheme: dark) {
html:not([data-theme="light"]) .word-original {
color: #eee;
}
html:not([data-theme="light"]) .word-unit:hover .word-original {
color: #fff;
}
html:not([data-theme="light"]) .word-unit:hover {
background: #2a2a2a;
border-color: #444;
}
html:not([data-theme="light"]) .word-unit.expanded {
background: #1a2e1a;
border-color: #4a7c59;
}
html:not([data-theme="light"]) .word-detail {
background: #222;
border-color: #444;
}
html:not([data-theme="light"]) .word-detail-original {
color: #eee;
}
html:not([data-theme="light"]) .word-detail-value {
color: #ddd;
}
html:not([data-theme="light"]) .word-detail-definition {
color: #bbb;
border-top-color: #444;
}
html:not([data-theme="light"]) .word-detail-header {
border-bottom-color: #444;
}
html:not([data-theme="light"]) .interlinear-heading {
color: #ddd;
border-bottom-color: #444;
}
}
@media (max-width: 768px) {
p.verse-text {
font-size: 2.2rem !important;
line-height: 3rem !important;
margin: 1.5rem 0;
}
.share-container {
max-width: 100%;
padding: 1rem 0;
}
.share-buttons {
gap: 0.5rem;
}
.share-btn {
padding: 0.75rem 1rem;
font-size: 0.9rem;
min-height: 44px;
flex: 1;
justify-content: center;
}
.cross-ref-tooltip {
display: none;
}
.interlinear-flow {
line-height: 1.4;
gap: 0.75rem 0.5rem;
}
.word-unit {
min-width: 60px;
padding: 0.5rem 0.6rem;
}
.word-original {
font-size: 1.8rem;
}
.word-original.hebrew {
font-size: 2rem;
}
.word-original.greek {
font-size: 1.8rem;
}
.word-tooltip {
display: none;
}
.word-detail {
position: fixed;
top: auto;
bottom: 0;
left: 0;
right: 0;
transform: none;
max-width: none;
border-radius: 16px 16px 0 0;
padding: 1.5rem;
box-shadow: 0 -4px 24px rgba(0,0,0,0.15);
}
.word-unit.expanded .word-detail {
transform: none;
}
}
@media print {
.share-container,
.chapter-actions,
nav {
display: none !important;
}
.verse-text {
max-width: 100% !important;
}
section {
page-break-inside: avoid;
}
.sidenote,
.marginnote {
display: block;
float: none;
width: 100%;
margin: 0.5rem 0;
font-size: 0.9rem;
color: var(--text-color);
}
.interlinear-container,
.word-tooltip,
.word-detail {
display: none !important;
}
}
</style>
{% endblock %}
{% block content %}
<h1>{{ book }} {{ chapter }}:{{ verse_num }}</h1>
<p class="subtitle"><a href="/books">Authorized King James Version</a></p>
<div class="chapter-actions">
<button class="action-btn" id="listen-btn" aria-label="Listen to verse and commentary">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
</svg>
Listen
</button>
{% if pdf_available and pdf_url %}
<a href="{{ pdf_url }}" class="action-btn pdf-btn">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
PDF
</a>
{% endif %}
</div>
<section>
<p class="verse-text">{{ verse_text | format_lists | red_letter(book, chapter, verse_num) | safe }}</p>
{% if interlinear_words %}
<section class="interlinear-container" id="interlinear">
<h3 class="interlinear-heading">Original Language Analysis</h3>
<div class="interlinear-flow">
{% for word in interlinear_words %}
<div class="word-unit" onclick="toggleWord(this)">
<span class="word-original {% if is_old_testament %}hebrew{% else %}greek{% endif %}">{{ word.original }}</span>
<span class="word-english">{{ word.english }}</span>
<span class="word-strongs">
{% if word.strongs %}
<a href="/strongs/{{ word.strongs }}" onclick="event.stopPropagation()">{{ word.strongs }}</a>
{% endif %}
</span>
<div class="word-detail">
<div class="word-detail-header">
<span class="word-detail-original {% if is_old_testament %}hebrew{% else %}greek{% endif %}">{{ word.original }}</span>
<span class="word-detail-english">{{ word.english }}</span>
</div>
{% if word.transliteration %}
<div class="word-detail-row">
<span class="word-detail-label">Pronunciation:</span>
<span class="word-detail-value">{{ word.transliteration }}</span>
</div>
{% endif %}
<div class="word-detail-row">
<span class="word-detail-label">Strong's:</span>
<span class="word-detail-value">
{% if word.strongs %}
<a href="/strongs/{{ word.strongs }}" style="color: #4a7c59;">{{ word.strongs }}</a>
{% else %}
{% endif %}
</span>
</div>
{% if word.parsing %}
<div class="word-detail-row">
<span class="word-detail-label">Grammar:</span>
<span class="word-detail-value">{{ word.parsing }}</span>
</div>
{% endif %}
<div class="word-detail-row">
<span class="word-detail-label">Word #:</span>
<span class="word-detail-value">{{ word.position }} of {{ interlinear_words|length }}</span>
</div>
{% if word.definition %}
<div class="word-detail-definition">
{{ word.definition }}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</section>
<script>
function toggleWord(el) {
document.querySelectorAll('.word-unit.expanded').forEach(function(word) {
if (word !== el) word.classList.remove('expanded');
});
el.classList.toggle('expanded');
}
document.addEventListener('click', function(e) {
if (!e.target.closest('.word-unit')) {
document.querySelectorAll('.word-unit.expanded').forEach(function(word) {
word.classList.remove('expanded');
});
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.querySelectorAll('.word-unit.expanded').forEach(function(word) {
word.classList.remove('expanded');
});
}
});
</script>
{% endif %}
<nav class="verse-nav-pills">
<a href="/book/{{ book }}" class="nav-pill">← {{ book }}</a>
<a href="/book/{{ book }}/chapter/{{ chapter }}#verse-{{ verse_num }}" class="nav-pill">Read in Chapter {{ chapter }}</a>
{% if verse_num > 1 %}<a href="/book/{{ book }}/chapter/{{ chapter }}/verse/{{ verse_num - 1 }}" class="nav-pill">← Verse {{ verse_num - 1 }}</a>{% endif %}
{% if verse_num < total_verses %}<a href="/book/{{ book }}/chapter/{{ chapter }}/verse/{{ verse_num + 1 }}" class="nav-pill">Verse {{ verse_num + 1 }} →</a>{% endif %}
</nav>
{% if cross_references %}
<div class="cross-references-section" id="cross-references">
<h3 class="section-heading">Cross References</h3>
<div class="crossrefs-compact">
{% for ref in cross_references %}
{%- set ref_parts = ref.ref.rsplit(' ', 1) -%}
{%- if ref_parts|length == 2 -%}
{%- set book_name = ref_parts[0] -%}
{%- set chapter_verse = ref_parts[1] -%}
{%- if ':' in chapter_verse -%}
{%- set ch = chapter_verse.split(':')[0] -%}
{%- set v = chapter_verse.split(':')[1] -%}
<a href="/book/{{ book_name }}/chapter/{{ ch }}/verse/{{ v }}" class="crossref-pill cross-ref-link">{{ ref.ref }}{% if ref.text %}<span class="cross-ref-tooltip">{{ ref.text }}</span>{% endif %}</a>
{%- else -%}
<span class="crossref-pill">{{ ref.ref }}</span>
{%- endif -%}
{%- else -%}
<span class="crossref-pill">{{ ref.ref }}</span>
{%- endif -%}
{% endfor %}
</div>
</div>
{% endif %}
<script>
function toggleSection(sectionId) {
var contentId = sectionId + '-content';
var content = document.getElementById(contentId);
var toggle = content.previousElementSibling;
while (toggle && !toggle.classList.contains('section-toggle')) {
toggle = toggle.previousElementSibling;
}
if (!toggle) return;
var icon = toggle.querySelector('.toggle-icon');
var isExpanded = toggle.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
content.hidden = true;
toggle.setAttribute('aria-expanded', 'false');
icon.textContent = '▶';
toggle.classList.remove('expanded');
localStorage.setItem(sectionId + '-expanded', 'false');
} else {
content.hidden = false;
toggle.setAttribute('aria-expanded', 'true');
icon.textContent = '▼';
toggle.classList.add('expanded');
localStorage.setItem(sectionId + '-expanded', 'true');
}
}
</script>
</section>
{% if commentary %}
<div id="commentary"></div>
{% if commentary.analysis %}
<div class="commentary-section" id="analysis">
<h2>Analysis &amp; Commentary</h2>
{{ commentary.analysis|split_paragraphs|safe }}
</div>
{% endif %}
{% if commentary.historical %}
<div class="commentary-section" id="historical">
<h2>Historical Context</h2>
{{ commentary.historical|split_paragraphs|safe }}
</div>
{% endif %}
{% if commentary.theological %}
<div class="commentary-section" id="theological">
<h2>Theological Significance</h2>
{{ commentary.theological|split_paragraphs|safe }}
</div>
{% endif %}
{% if commentary.questions %}
<div class="commentary-section" id="questions">
<h2>Questions for Reflection</h2>
<ul>
{% for question in commentary.questions %}
<li>{{ question }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endif %}
<!-- Keyboard Navigation - Paragraph-based with drill-down -->
<script>
(function() {
// Collect all readable elements: paragraphs, list items, cross-ref items
function getElements() {
return Array.from(document.querySelectorAll(
'section > p.verse-text, ' +
'.interlinear-container, ' +
'.commentary-section p, ' +
'.commentary-section li'
)).filter(function(el) {
// Filter out empty or very short elements, and UI elements
if (el.closest('.word-detail') || el.closest('.share-container') || el.closest('nav')) return false;
// Skip elements inside collapsed sections
var hiddenParent = el.closest('[hidden]');
if (hiddenParent) return false;
return el.textContent.trim().length > 10 || el.classList.contains('interlinear-container');
});
}
var elements = getElements();
var selectedIndex = -1;
var inWordMode = false;
var selectedWordIndex = -1;
function getWords() {
var container = document.querySelector('.interlinear-container');
if (!container) return [];
return Array.from(container.querySelectorAll('.word-unit'));
}
function clearWordSelection() {
document.querySelectorAll('.word-unit.keyboard-selected').forEach(function(word) {
word.classList.remove('keyboard-selected');
word.style.outline = '';
word.style.outlineOffset = '';
});
document.querySelectorAll('.word-unit.expanded').forEach(function(word) {
word.classList.remove('expanded');
});
selectedWordIndex = -1;
inWordMode = false;
}
function clearSelection() {
if (selectedIndex >= 0 && selectedIndex < elements.length) {
elements[selectedIndex].style.outline = '';
elements[selectedIndex].style.outlineOffset = '';
elements[selectedIndex].classList.remove('selected');
}
clearWordSelection();
}
function selectElement(index) {
clearSelection();
// Refresh elements list in case sections were toggled
elements = getElements();
if (elements.length === 0) return;
selectedIndex = Math.max(0, Math.min(index, elements.length - 1));
elements[selectedIndex].style.outline = '2px solid #4a7c59';
elements[selectedIndex].style.outlineOffset = '4px';
elements[selectedIndex].classList.add('selected');
elements[selectedIndex].scrollIntoView({ behavior: 'auto', block: 'center' });
}
function selectWord(index) {
var words = getWords();
if (words.length === 0) return;
// Clear previous word selection
document.querySelectorAll('.word-unit.keyboard-selected').forEach(function(word) {
word.classList.remove('keyboard-selected');
word.style.outline = '';
word.style.outlineOffset = '';
});
selectedWordIndex = Math.max(0, Math.min(index, words.length - 1));
var word = words[selectedWordIndex];
word.classList.add('keyboard-selected');
word.style.outline = '2px solid #4a7c59';
word.style.outlineOffset = '2px';
word.scrollIntoView({ behavior: 'auto', block: 'center' });
}
// Grid navigation helpers - find words in same column on different rows
function getWordRows() {
var words = getWords();
if (words.length === 0) return [];
var rows = [];
var currentRow = [];
var lastTop = null;
words.forEach(function(word, i) {
var rect = word.getBoundingClientRect();
var top = Math.round(rect.top);
if (lastTop === null || Math.abs(top - lastTop) < 10) {
currentRow.push({ element: word, index: i, left: rect.left, width: rect.width });
} else {
if (currentRow.length > 0) rows.push(currentRow);
currentRow = [{ element: word, index: i, left: rect.left, width: rect.width }];
}
lastTop = top;
});
if (currentRow.length > 0) rows.push(currentRow);
return rows;
}
function findWordInDirection(currentIndex, direction) {
var words = getWords();
if (words.length === 0 || currentIndex < 0) return currentIndex;
var rows = getWordRows();
if (rows.length === 0) return currentIndex;
// Find which row and position current word is in
var currentRowIndex = -1;
var currentColIndex = -1;
var currentLeft = 0;
var currentWidth = 0;
for (var r = 0; r < rows.length; r++) {
for (var c = 0; c < rows[r].length; c++) {
if (rows[r][c].index === currentIndex) {
currentRowIndex = r;
currentColIndex = c;
currentLeft = rows[r][c].left;
currentWidth = rows[r][c].width;
break;
}
}
if (currentRowIndex >= 0) break;
}
if (currentRowIndex < 0) return currentIndex;
// Find target row
var targetRowIndex = currentRowIndex + direction;
if (targetRowIndex < 0 || targetRowIndex >= rows.length) {
return -1; // Signal to exit word mode
}
// Find closest word in target row by horizontal position
var targetRow = rows[targetRowIndex];
var currentCenter = currentLeft + currentWidth / 2;
var bestMatch = targetRow[0];
var bestDistance = Infinity;
for (var i = 0; i < targetRow.length; i++) {
var wordCenter = targetRow[i].left + targetRow[i].width / 2;
var distance = Math.abs(wordCenter - currentCenter);
if (distance < bestDistance) {
bestDistance = distance;
bestMatch = targetRow[i];
}
}
return bestMatch.index;
}
function isInterlinearSelected() {
return selectedIndex >= 0 && elements[selectedIndex] &&
elements[selectedIndex].classList.contains('interlinear-container');
}
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
return;
}
// Don't handle if sidebar navigation is active
if (KJVNav.sidebarActive) return;
// Escape: Exit word mode or clear selection
if (e.key === 'Escape') {
e.preventDefault();
if (inWordMode) {
clearWordSelection();
// Re-highlight the interlinear container
if (isInterlinearSelected()) {
elements[selectedIndex].style.outline = '2px solid #4a7c59';
elements[selectedIndex].style.outlineOffset = '4px';
}
} else {
clearSelection();
selectedIndex = -1;
}
return;
}
// In word mode - navigate words in grid pattern
if (inWordMode) {
var words = getWords();
if (e.key === 'ArrowRight' || e.key === 'l') {
e.preventDefault();
if (selectedWordIndex < words.length - 1) {
selectWord(selectedWordIndex + 1);
}
} else if (e.key === 'ArrowLeft' || e.key === 'h') {
e.preventDefault();
if (selectedWordIndex > 0) {
selectWord(selectedWordIndex - 1);
}
} else if (e.key === 'ArrowDown' || e.key === 'j') {
e.preventDefault();
// Navigate to word below in grid
var targetIndex = findWordInDirection(selectedWordIndex, 1);
if (targetIndex === -1) {
// No row below, exit word mode and go to next element
clearWordSelection();
selectElement(selectedIndex + 1);
} else {
selectWord(targetIndex);
}
} else if (e.key === 'ArrowUp' || e.key === 'k') {
e.preventDefault();
// Navigate to word above in grid
var targetIndex = findWordInDirection(selectedWordIndex, -1);
if (targetIndex === -1) {
// No row above, exit word mode
clearWordSelection();
if (isInterlinearSelected()) {
elements[selectedIndex].style.outline = '2px solid #4a7c59';
elements[selectedIndex].style.outlineOffset = '4px';
}
} else {
selectWord(targetIndex);
}
} else if (e.key === 'Enter') {
e.preventDefault();
// Toggle word expansion
if (selectedWordIndex >= 0 && words[selectedWordIndex]) {
document.querySelectorAll('.word-unit.expanded').forEach(function(word) {
if (word !== words[selectedWordIndex]) {
word.classList.remove('expanded');
}
});
words[selectedWordIndex].classList.toggle('expanded');
}
} else if (e.key === 'Escape') {
e.preventDefault();
clearWordSelection();
if (isInterlinearSelected()) {
elements[selectedIndex].style.outline = '2px solid #4a7c59';
elements[selectedIndex].style.outlineOffset = '4px';
}
}
return;
}
// Normal navigation mode
if (e.key === 'ArrowDown' || e.key === 'j') {
e.preventDefault();
if (KJVNav.isSelectionOffScreen(elements, selectedIndex)) {
selectElement(KJVNav.findFirstVisibleIndex(elements));
} else {
selectElement(selectedIndex < 0 ? 0 : selectedIndex + 1);
}
} else if (e.key === 'ArrowUp' || e.key === 'k') {
e.preventDefault();
if (KJVNav.isSelectionOffScreen(elements, selectedIndex)) {
selectElement(KJVNav.findFirstVisibleIndex(elements));
} else if (selectedIndex <= 0) {
selectElement(0);
} else {
selectElement(selectedIndex - 1);
}
} else if (e.key === 'Enter') {
e.preventDefault();
// If on interlinear, toggle expand or enter word mode
if (isInterlinearSelected()) {
var toggle = document.querySelector('.interlinear-toggle');
var content = document.getElementById('interlinear-content');
if (content && content.hidden) {
// First expand the section
toggleInterlinear();
} else {
// Already expanded, enter word mode
inWordMode = true;
elements[selectedIndex].style.outline = '';
selectWord(0);
}
} else if (selectedIndex >= 0) {
// If on a cross-reference, navigate to it
var link = elements[selectedIndex].querySelector('a');
if (link) window.location.href = link.href;
}
} else if (e.key === 'ArrowLeft' || e.key === 'h') {
e.preventDefault();
history.back();
} else if (e.key === 'ArrowRight' || e.key === 'l') {
e.preventDefault();
{% if verse_num < total_verses %}
window.location.href = '/book/{{ book }}/chapter/{{ chapter }}/verse/{{ verse_num + 1 }}';
{% endif %}
} else if (e.key === '[') {
// Previous verse
e.preventDefault();
{% if verse_num > 1 %}
window.location.href = '/book/{{ book }}/chapter/{{ chapter }}/verse/{{ verse_num - 1 }}';
{% endif %}
} else if (e.key === ']') {
// Next verse
e.preventDefault();
{% if verse_num < total_verses %}
window.location.href = '/book/{{ book }}/chapter/{{ chapter }}/verse/{{ verse_num + 1 }}';
{% endif %}
}
});
})();
</script>
{% if related_content and (related_content.topics or related_content.people or related_content.resources or related_content.stories) %}
<div style="border-top: 1px solid var(--border-color); padding-top: 2rem; margin-top: 3rem;">
<h2>Related Resources</h2>
<p style="font-size: 0.95rem; color: var(--text-secondary); margin-bottom: 1.5rem;">
Explore related topics, people, and study resources to deepen your understanding of this passage.
</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem;">
{% if related_content.topics %}
<div>
<h3 style="font-size: 1rem; margin-bottom: 0.75rem; color: var(--text-secondary);">Topics</h3>
<ul style="list-style: none; padding: 0; margin: 0;">
{% for topic in related_content.topics %}
<li style="margin-bottom: 0.5rem;">
<a href="{{ topic.url }}" style="font-size: 0.95rem;">{{ topic.name }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if related_content.people %}
<div>
<h3 style="font-size: 1rem; margin-bottom: 0.75rem; color: var(--text-secondary);">People</h3>
<ul style="list-style: none; padding: 0; margin: 0;">
{% for person in related_content.people %}
<li style="margin-bottom: 0.5rem;">
<a href="{{ person.url }}" style="font-size: 0.95rem;">{{ person.name }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if related_content.resources %}
<div>
<h3 style="font-size: 1rem; margin-bottom: 0.75rem; color: var(--text-secondary);">Study Resources</h3>
<ul style="list-style: none; padding: 0; margin: 0;">
{% for resource in related_content.resources %}
<li style="margin-bottom: 0.5rem;">
<a href="{{ resource.url }}" style="font-size: 0.95rem;">{{ resource.name }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if related_content.stories %}
<div>
<h3 style="font-size: 1rem; margin-bottom: 0.75rem; color: var(--text-secondary);">Bible Stories</h3>
<ul style="list-style: none; padding: 0; margin: 0;">
{% for story in related_content.stories %}
<li style="margin-bottom: 0.5rem;">
<a href="{{ story.url }}" style="font-size: 0.95rem;">{{ story.name }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
{% endif %}
<script>
(function() {
// Listen button handler with toggle
var listenBtn = document.getElementById('listen-btn');
var isListening = false;
if (listenBtn) {
listenBtn.addEventListener('click', function() {
if (isListening) {
if (window.KJVSpeech) window.KJVSpeech.stop();
listenBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" /></svg> Listen';
isListening = false;
return;
}
// Collect verse text and commentary
var parts = [];
var verseEl = document.querySelector('.verse-text');
var verseText = verseEl ? verseEl.textContent.trim() : '';
if (verseText) parts.push(verseText);
// Add commentary sections, skipping bold verse text in first paragraph
var commentaryEls = document.querySelectorAll('.commentary-section p, .commentary-section li');
var isFirst = true;
commentaryEls.forEach(function(el) {
var elText;
// For first paragraph, skip the bold/strong part (usually contains verse text)
if (isFirst && el.tagName === 'P') {
var strong = el.querySelector('strong');
if (strong && verseText && strong.textContent.trim().substring(0, 40) === verseText.substring(0, 40)) {
// Clone the element and remove the strong tag to get remaining text
var clone = el.cloneNode(true);
var strongInClone = clone.querySelector('strong');
if (strongInClone) strongInClone.remove();
elText = clone.textContent.trim();
} else {
elText = el.textContent.trim();
}
isFirst = false;
} else {
elText = el.textContent.trim();
}
if (elText) parts.push(elText);
});
var text = parts.join(' ');
if (window.KJVSpeech && text) {
window.KJVSpeech.speak(text);
listenBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" /></svg> Stop';
isListening = true;
}
});
}
})();
</script>
{% endblock %}