mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-05 23:00:16 +00:00
3336863a4d
- Add KJVNav.initGridNav for standardized 2D grid navigation - Migrate books.html, topics.html, resources.html to use initGridNav - Add sidebarActive check to all templates with custom keyboard handlers - Add [ and ] shortcuts for prev/next chapter on chapter pages - Add [ and ] shortcuts for prev/next book on book pages - Update accessibility page with comprehensive keyboard shortcut docs - Add honest note about keyboard navigation complexity - Fix sidebar nav conflicting with main content selection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
292 lines
10 KiB
HTML
292 lines
10 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ guide.title }} - Bible Study Guide - KJV Bible{% endblock %}
|
|
|
|
{% block description %}{{ guide.description }}{% endblock %}
|
|
|
|
{% block og_type %}article{% endblock %}
|
|
{% block og_title %}{{ guide.title }} - Bible Study Guide{% endblock %}
|
|
{% block og_description %}{{ guide.description }}{% endblock %}
|
|
|
|
{% block structured_data %}
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "Article",
|
|
"headline": {{ guide.title | tojson }},
|
|
"description": {{ guide.description | tojson }},
|
|
"author": {
|
|
"@type": "Organization",
|
|
"name": "KJV Study"
|
|
},
|
|
"publisher": {
|
|
"@type": "Organization",
|
|
"name": "KJV Study",
|
|
"url": "https://kjvstudy.org"
|
|
},
|
|
"mainEntityOfPage": {
|
|
"@type": "WebPage",
|
|
"@id": "https://kjvstudy.org{{ request.url.path }}"
|
|
},
|
|
"about": {
|
|
"@type": "Thing",
|
|
"name": "Bible Study"
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|
|
{% block head %}
|
|
<style>
|
|
|
|
.guide-actions {
|
|
margin: 1.25rem 0;
|
|
}
|
|
|
|
.guide-download-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.35rem;
|
|
padding: 0.45rem 0.9rem;
|
|
font-size: 0.9rem;
|
|
color: var(--text-secondary, #666);
|
|
background: var(--code-bg, #f8f8f8);
|
|
border: 1px solid var(--border-color, #ddd);
|
|
border-radius: 4px;
|
|
text-decoration: none;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.guide-download-btn:hover {
|
|
background: var(--bg-color, #fff);
|
|
border-color: var(--link-color);
|
|
color: var(--link-color);
|
|
}
|
|
|
|
.guide-download-btn svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
@media print {
|
|
.guide-actions,
|
|
.guide-download-btn {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Function to link verse references in text
|
|
function linkVerseReferences(element) {
|
|
if (!element) return;
|
|
|
|
// Get all text nodes
|
|
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
|
|
const textNodes = [];
|
|
let node;
|
|
while (node = walker.nextNode()) {
|
|
textNodes.push(node);
|
|
}
|
|
|
|
textNodes.forEach(function(textNode) {
|
|
let text = textNode.textContent;
|
|
let changed = false;
|
|
|
|
// First, handle comma-separated verse lists like "Psalms 7:17, 9:2, 18:13"
|
|
// Pattern: Book chapter:verse, chapter:verse, chapter:verse...
|
|
text = text.replace(/\b(\d?\s?[A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+((?:\d+:\d+(?:-\d+)?(?:\s*,\s*)?)+)/g, function(match, book, verseList) {
|
|
book = book.trim();
|
|
|
|
// Check if this is actually a comma-separated list
|
|
if (!verseList.includes(',')) {
|
|
// Single verse, will be handled by the next regex
|
|
return match;
|
|
}
|
|
|
|
changed = true;
|
|
|
|
// Split by comma and process each verse reference
|
|
var verses = verseList.split(/\s*,\s*/);
|
|
var links = verses.map(function(verseRef) {
|
|
verseRef = verseRef.trim();
|
|
var parts = verseRef.match(/^(\d+):(\d+)(?:-(\d+))?$/);
|
|
if (parts) {
|
|
var chapter = parts[1];
|
|
var verseStart = parts[2];
|
|
var verseEnd = parts[3];
|
|
|
|
if (verseEnd) {
|
|
return '<a href="/book/' + book + '/chapter/' + chapter + '#verse-' + verseStart + '-' + verseEnd + '">' + book + ' ' + chapter + ':' + verseStart + '-' + verseEnd + '</a>';
|
|
} else {
|
|
return '<a href="/book/' + book + '/chapter/' + chapter + '/verse/' + verseStart + '">' + book + ' ' + chapter + ':' + verseStart + '</a>';
|
|
}
|
|
}
|
|
return verseRef;
|
|
});
|
|
|
|
return links.join(', ');
|
|
});
|
|
|
|
// Then handle individual verse references like "John 3:16" or "Romans 8:28-29"
|
|
// This regex handles book names with optional numbers and spaces
|
|
text = text.replace(/\b(\d?\s?[A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+(\d+):(\d+)(?:-(\d+))?\b/g, function(match, book, chapter, verseStart, verseEnd) {
|
|
changed = true;
|
|
book = book.trim();
|
|
if (verseEnd) {
|
|
return '<a href="/book/' + book + '/chapter/' + chapter + '#verse-' + verseStart + '-' + verseEnd + '">' + match + '</a>';
|
|
} else {
|
|
return '<a href="/book/' + book + '/chapter/' + chapter + '/verse/' + verseStart + '">' + match + '</a>';
|
|
}
|
|
});
|
|
|
|
// Handle chapter-only references like "Psalm 2" or "Genesis 1"
|
|
// Must come after verse references to avoid double-matching
|
|
text = text.replace(/\b(Psalm|Genesis|Exodus|Isaiah|Daniel|Revelation|Matthew|Mark|Luke|John|Romans|Hebrews|Proverbs|Ecclesiastes|Job)\s+(\d+)\b(?!:)/g, function(match, book, chapter) {
|
|
changed = true;
|
|
return '<a href="/book/' + book + '/chapter/' + chapter + '">' + match + '</a>';
|
|
});
|
|
|
|
if (changed) {
|
|
const span = document.createElement('span');
|
|
span.innerHTML = text;
|
|
textNode.parentNode.replaceChild(span, textNode);
|
|
// Replace the span's children with its contents
|
|
while (span.firstChild) {
|
|
span.parentNode.insertBefore(span.firstChild, span);
|
|
}
|
|
span.parentNode.removeChild(span);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Link verse references in all section content paragraphs
|
|
document.querySelectorAll('section p').forEach(function(paragraph) {
|
|
// Skip paragraphs that are just margin toggle labels
|
|
if (!paragraph.querySelector('.margin-toggle')) {
|
|
linkVerseReferences(paragraph);
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<h1>{{ guide.title }}</h1>
|
|
<p class="subtitle">{{ guide.description }}</p>
|
|
|
|
<script>
|
|
document.body.dataset.resourceReader = 'true';
|
|
</script>
|
|
|
|
{% if pdf_available and pdf_url %}
|
|
<div class="guide-actions">
|
|
<a href="{{ pdf_url }}" class="guide-download-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>
|
|
Download PDF
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<nav class="toc">
|
|
<h2>Contents</h2>
|
|
<ul>
|
|
{% for section in guide.sections %}
|
|
<li><a href="#section-{{ loop.index }}">{{ section.title }}</a></li>
|
|
{% endfor %}
|
|
</ul>
|
|
</nav>
|
|
|
|
{% for section in guide.sections %}
|
|
{% set section_index = loop.index %}
|
|
<section id="section-{{ section_index }}">
|
|
<h2>{{ section.title }}</h2>
|
|
|
|
{% for verse_data in section.verse_texts %}
|
|
<p>
|
|
<label for="mn-s{{ section_index }}-v{{ loop.index }}" class="margin-toggle">⊕</label>
|
|
<input type="checkbox" id="mn-s{{ section_index }}-v{{ loop.index }}" class="margin-toggle"/>
|
|
<span class="marginnote">
|
|
<strong><a href="{{ verse_data.url }}">{{ verse_data.reference }}</a></strong><br/>
|
|
<em>{{ verse_data.text }}</em>
|
|
</span>
|
|
</p>
|
|
{% endfor %}
|
|
|
|
{% set paragraphs = section.content.split('<br><br>') %}
|
|
{% for paragraph in paragraphs %}
|
|
<p class="guide-paragraph" data-section="{{ section_index }}" data-paragraph="{{ loop.index0 }}">{{ paragraph | format_lists | safe }}</p>
|
|
{% endfor %}
|
|
</section>
|
|
{% endfor %}
|
|
|
|
<nav>
|
|
<p>
|
|
<a href="/study-guides">← Study Guides</a> |
|
|
<a href="/">Home</a>
|
|
</p>
|
|
</nav>
|
|
|
|
<script>
|
|
(function() {
|
|
const paragraphs = Array.from(document.querySelectorAll('.guide-paragraph'));
|
|
if (paragraphs.length === 0) return;
|
|
|
|
let selectedIndex = -1;
|
|
|
|
function selectParagraph(index) {
|
|
if (selectedIndex >= 0 && selectedIndex < paragraphs.length) {
|
|
paragraphs[selectedIndex].style.outline = '';
|
|
paragraphs[selectedIndex].style.outlineOffset = '';
|
|
paragraphs[selectedIndex].classList.remove('selected');
|
|
}
|
|
selectedIndex = Math.max(0, Math.min(index, paragraphs.length - 1));
|
|
paragraphs[selectedIndex].style.outline = '2px solid #4a7c59';
|
|
paragraphs[selectedIndex].style.outlineOffset = '4px';
|
|
paragraphs[selectedIndex].classList.add('selected');
|
|
paragraphs[selectedIndex].scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
if (KJVNav.sidebarActive) return;
|
|
|
|
if (e.key === 'ArrowDown' || e.key === 'j') {
|
|
e.preventDefault();
|
|
if (KJVNav.isSelectionOffScreen(paragraphs, selectedIndex)) {
|
|
selectParagraph(KJVNav.findFirstVisibleIndex(paragraphs));
|
|
} else {
|
|
selectParagraph(selectedIndex < 0 ? 0 : selectedIndex + 1);
|
|
}
|
|
} else if (e.key === 'ArrowUp' || e.key === 'k') {
|
|
e.preventDefault();
|
|
if (KJVNav.isSelectionOffScreen(paragraphs, selectedIndex)) {
|
|
selectParagraph(KJVNav.findFirstVisibleIndex(paragraphs));
|
|
} else if (selectedIndex <= 0) {
|
|
selectParagraph(0);
|
|
} else {
|
|
selectParagraph(selectedIndex - 1);
|
|
}
|
|
} else if (e.key === 'ArrowLeft' || e.key === 'h') {
|
|
e.preventDefault();
|
|
history.back();
|
|
} else if (e.key === 'Enter' && selectedIndex >= 0) {
|
|
e.preventDefault();
|
|
const link = paragraphs[selectedIndex].querySelector('a');
|
|
if (link) window.location.href = link.href;
|
|
} else if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
if (selectedIndex >= 0 && selectedIndex < paragraphs.length) {
|
|
paragraphs[selectedIndex].style.outline = '';
|
|
paragraphs[selectedIndex].style.outlineOffset = '';
|
|
paragraphs[selectedIndex].classList.remove('selected');
|
|
}
|
|
selectedIndex = -1;
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|