mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-05 06:46:13 +00:00
Add 6 new reading plans with full Scripture text in web/PDF
New reading plans: - Books of Moses in 40 Days (pentateuch_40) - Major Prophets in 60 Days (prophets_60) - Minor Prophets in 14 Days (minor_prophets_14) - Wisdom Literature in 30 Days (wisdom_30) - Historical Books in 45 Days (historical_45) - General Epistles in 14 Days (general_epistles_14) Features: - Reading plan pages now show full Scripture text for plans 90 days or less - Day navigation bar for quick jumping between days - PDF exports include full Bible text for shorter plans - Refactored Jinja filters into separate jinja_filters.py module - Added parse_reading_reference() and get_reading_text() helpers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"general_epistles_14": [
|
||||
{"day": 1, "readings": ["Hebrews 1-4"], "theme": "Christ Superior to Angels and Moses"},
|
||||
{"day": 2, "readings": ["Hebrews 5-7"], "theme": "Melchizedek Priesthood"},
|
||||
{"day": 3, "readings": ["Hebrews 8-10"], "theme": "New Covenant, Better Sacrifice"},
|
||||
{"day": 4, "readings": ["Hebrews 11-13"], "theme": "Hall of Faith, Endurance"},
|
||||
{"day": 5, "readings": ["James 1-3"], "theme": "Faith and Works, Taming the Tongue"},
|
||||
{"day": 6, "readings": ["James 4-5"], "theme": "Humility, Patience, Prayer"},
|
||||
{"day": 7, "readings": ["1 Peter 1-3"], "theme": "Living Hope, Submission"},
|
||||
{"day": 8, "readings": ["1 Peter 4-5"], "theme": "Suffering for Christ, Shepherding"},
|
||||
{"day": 9, "readings": ["2 Peter 1-3"], "theme": "Knowledge, False Teachers, Day of the Lord"},
|
||||
{"day": 10, "readings": ["1 John 1-3"], "theme": "Fellowship, Walking in Light, Love"},
|
||||
{"day": 11, "readings": ["1 John 4-5"], "theme": "Testing Spirits, God is Love, Assurance"},
|
||||
{"day": 12, "readings": ["2 John", "3 John"], "theme": "Truth and Love, Hospitality"},
|
||||
{"day": 13, "readings": ["Jude"], "theme": "Contending for the Faith"},
|
||||
{"day": 14, "readings": ["Revelation 1-3"], "theme": "Vision of Christ, Letters to Churches"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"historical_45": [
|
||||
{"day": 1, "readings": ["Joshua 1-5"], "theme": "Entering the Promised Land"},
|
||||
{"day": 2, "readings": ["Joshua 6-10"], "theme": "Jericho Falls, Conquest Begins"},
|
||||
{"day": 3, "readings": ["Joshua 11-15"], "theme": "Northern Conquest, Land Division"},
|
||||
{"day": 4, "readings": ["Joshua 16-21"], "theme": "Tribal Allotments, Cities of Refuge"},
|
||||
{"day": 5, "readings": ["Joshua 22-24"], "theme": "Joshua's Farewell, Covenant Renewed"},
|
||||
{"day": 6, "readings": ["Judges 1-5"], "theme": "Incomplete Conquest, Deborah"},
|
||||
{"day": 7, "readings": ["Judges 6-9"], "theme": "Gideon's Victory"},
|
||||
{"day": 8, "readings": ["Judges 10-14"], "theme": "Jephthah, Samson's Birth"},
|
||||
{"day": 9, "readings": ["Judges 15-18"], "theme": "Samson's Exploits, Micah's Idols"},
|
||||
{"day": 10, "readings": ["Judges 19-21"], "theme": "Benjamite War"},
|
||||
{"day": 11, "readings": ["Ruth 1-4"], "theme": "Loyalty, Redemption, David's Ancestry"},
|
||||
{"day": 12, "readings": ["1 Samuel 1-5"], "theme": "Samuel's Birth, Ark Captured"},
|
||||
{"day": 13, "readings": ["1 Samuel 6-10"], "theme": "Ark Returned, Saul Anointed King"},
|
||||
{"day": 14, "readings": ["1 Samuel 11-15"], "theme": "Saul's Reign, Rejection"},
|
||||
{"day": 15, "readings": ["1 Samuel 16-20"], "theme": "David Anointed, Goliath, Jonathan"},
|
||||
{"day": 16, "readings": ["1 Samuel 21-25"], "theme": "David Flees, Nabal and Abigail"},
|
||||
{"day": 17, "readings": ["1 Samuel 26-31"], "theme": "David Spares Saul, Saul's Death"},
|
||||
{"day": 18, "readings": ["2 Samuel 1-5"], "theme": "David Becomes King"},
|
||||
{"day": 19, "readings": ["2 Samuel 6-10"], "theme": "Ark to Jerusalem, Davidic Covenant"},
|
||||
{"day": 20, "readings": ["2 Samuel 11-14"], "theme": "David and Bathsheba, Absalom"},
|
||||
{"day": 21, "readings": ["2 Samuel 15-19"], "theme": "Absalom's Rebellion"},
|
||||
{"day": 22, "readings": ["2 Samuel 20-24"], "theme": "Sheba's Revolt, David's Last Words"},
|
||||
{"day": 23, "readings": ["1 Kings 1-4"], "theme": "Solomon Becomes King, Wisdom"},
|
||||
{"day": 24, "readings": ["1 Kings 5-8"], "theme": "Temple Built and Dedicated"},
|
||||
{"day": 25, "readings": ["1 Kings 9-11"], "theme": "Solomon's Glory and Fall"},
|
||||
{"day": 26, "readings": ["1 Kings 12-16"], "theme": "Kingdom Divided, Evil Kings"},
|
||||
{"day": 27, "readings": ["1 Kings 17-19"], "theme": "Elijah, Drought, Mount Carmel"},
|
||||
{"day": 28, "readings": ["1 Kings 20-22"], "theme": "Ahab's Wars, Naboth's Vineyard"},
|
||||
{"day": 29, "readings": ["2 Kings 1-5"], "theme": "Elijah Taken Up, Elisha's Miracles"},
|
||||
{"day": 30, "readings": ["2 Kings 6-10"], "theme": "Siege, Jehu's Revolution"},
|
||||
{"day": 31, "readings": ["2 Kings 11-15"], "theme": "Joash, Decline of Israel"},
|
||||
{"day": 32, "readings": ["2 Kings 16-19"], "theme": "Ahaz, Hezekiah, Assyrian Threat"},
|
||||
{"day": 33, "readings": ["2 Kings 20-23"], "theme": "Hezekiah's Illness, Josiah's Reforms"},
|
||||
{"day": 34, "readings": ["2 Kings 24-25"], "theme": "Jerusalem Falls, Exile"},
|
||||
{"day": 35, "readings": ["1 Chronicles 1-9"], "theme": "Genealogies from Adam to Saul"},
|
||||
{"day": 36, "readings": ["1 Chronicles 10-16"], "theme": "David's Reign, Ark to Jerusalem"},
|
||||
{"day": 37, "readings": ["1 Chronicles 17-22"], "theme": "Davidic Covenant, Temple Preparations"},
|
||||
{"day": 38, "readings": ["1 Chronicles 23-29"], "theme": "Levites, Solomon Anointed"},
|
||||
{"day": 39, "readings": ["2 Chronicles 1-7"], "theme": "Solomon's Temple"},
|
||||
{"day": 40, "readings": ["2 Chronicles 8-16"], "theme": "Solomon to Asa"},
|
||||
{"day": 41, "readings": ["2 Chronicles 17-24"], "theme": "Jehoshaphat to Joash"},
|
||||
{"day": 42, "readings": ["2 Chronicles 25-32"], "theme": "Amaziah to Hezekiah"},
|
||||
{"day": 43, "readings": ["2 Chronicles 33-36"], "theme": "Manasseh to Exile"},
|
||||
{"day": 44, "readings": ["Ezra 1-6", "Nehemiah 1-2"], "theme": "Return from Exile, Temple Rebuilt"},
|
||||
{"day": 45, "readings": ["Nehemiah 3-13", "Esther 1-10"], "theme": "Walls Rebuilt, Esther Saves Her People"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"minor_prophets_14": [
|
||||
{"day": 1, "readings": ["Hosea 1-7"], "theme": "God's Faithful Love for Unfaithful Israel"},
|
||||
{"day": 2, "readings": ["Hosea 8-14"], "theme": "Judgment and Restoration"},
|
||||
{"day": 3, "readings": ["Joel 1-3"], "theme": "Day of the Lord and Spirit Poured Out"},
|
||||
{"day": 4, "readings": ["Amos 1-5"], "theme": "Judgment on Nations and Israel"},
|
||||
{"day": 5, "readings": ["Amos 6-9"], "theme": "Woe and Future Restoration"},
|
||||
{"day": 6, "readings": ["Obadiah", "Jonah 1-4"], "theme": "Edom's Doom, Jonah and God's Mercy"},
|
||||
{"day": 7, "readings": ["Micah 1-7"], "theme": "Judgment, Bethlehem Prophecy, God's Requirements"},
|
||||
{"day": 8, "readings": ["Nahum 1-3"], "theme": "Nineveh's Destruction"},
|
||||
{"day": 9, "readings": ["Habakkuk 1-3"], "theme": "The Just Shall Live by Faith"},
|
||||
{"day": 10, "readings": ["Zephaniah 1-3"], "theme": "Day of the Lord, Remnant Restored"},
|
||||
{"day": 11, "readings": ["Haggai 1-2"], "theme": "Rebuild the Temple"},
|
||||
{"day": 12, "readings": ["Zechariah 1-7"], "theme": "Visions of Restoration"},
|
||||
{"day": 13, "readings": ["Zechariah 8-14"], "theme": "Messianic King, Pierced One"},
|
||||
{"day": 14, "readings": ["Malachi 1-4"], "theme": "Messenger of the Covenant, Elijah Returns"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"pentateuch_40": [
|
||||
{"day": 1, "readings": ["Genesis 1-4"], "theme": "Creation, Fall, Cain and Abel"},
|
||||
{"day": 2, "readings": ["Genesis 5-9"], "theme": "Genealogy, Noah, the Flood, Covenant"},
|
||||
{"day": 3, "readings": ["Genesis 10-14"], "theme": "Nations, Babel, Call of Abram, Lot"},
|
||||
{"day": 4, "readings": ["Genesis 15-19"], "theme": "Covenant, Ishmael, Sodom and Gomorrah"},
|
||||
{"day": 5, "readings": ["Genesis 20-24"], "theme": "Isaac Born, Sacrifice, Rebekah"},
|
||||
{"day": 6, "readings": ["Genesis 25-28"], "theme": "Abraham Dies, Jacob and Esau, Bethel"},
|
||||
{"day": 7, "readings": ["Genesis 29-32"], "theme": "Jacob, Laban, Wrestles with God"},
|
||||
{"day": 8, "readings": ["Genesis 33-36"], "theme": "Reconciliation, Dinah, Israel"},
|
||||
{"day": 9, "readings": ["Genesis 37-41"], "theme": "Joseph's Dreams, Judah and Tamar, Egypt"},
|
||||
{"day": 10, "readings": ["Genesis 42-46"], "theme": "Brothers in Egypt, Reconciliation, Jacob Goes"},
|
||||
{"day": 11, "readings": ["Genesis 47-50"], "theme": "Jacob Blesses, Deaths"},
|
||||
{"day": 12, "readings": ["Exodus 1-4"], "theme": "Bondage, Birth of Moses, Burning Bush"},
|
||||
{"day": 13, "readings": ["Exodus 5-8"], "theme": "Pharaoh's Heart, First Plagues"},
|
||||
{"day": 14, "readings": ["Exodus 9-12"], "theme": "More Plagues, Passover Instituted"},
|
||||
{"day": 15, "readings": ["Exodus 13-16"], "theme": "Red Sea Crossing, Manna"},
|
||||
{"day": 16, "readings": ["Exodus 17-20"], "theme": "Water from Rock, Jethro, Ten Commandments"},
|
||||
{"day": 17, "readings": ["Exodus 21-24"], "theme": "Laws, Covenant Confirmed"},
|
||||
{"day": 18, "readings": ["Exodus 25-28"], "theme": "Tabernacle Instructions, Priestly Garments"},
|
||||
{"day": 19, "readings": ["Exodus 29-32"], "theme": "Priestly Consecration, Golden Calf"},
|
||||
{"day": 20, "readings": ["Exodus 33-36"], "theme": "God's Presence, Tablets Renewed"},
|
||||
{"day": 21, "readings": ["Exodus 37-40"], "theme": "Tabernacle Completed, Glory Fills"},
|
||||
{"day": 22, "readings": ["Leviticus 1-5"], "theme": "Burnt, Grain, Peace, Sin, Guilt Offerings"},
|
||||
{"day": 23, "readings": ["Leviticus 6-10"], "theme": "Offering Laws, Priests Ordained, Nadab and Abihu"},
|
||||
{"day": 24, "readings": ["Leviticus 11-15"], "theme": "Clean and Unclean, Purification"},
|
||||
{"day": 25, "readings": ["Leviticus 16-20"], "theme": "Day of Atonement, Holiness Code"},
|
||||
{"day": 26, "readings": ["Leviticus 21-24"], "theme": "Priests, Feasts, Blasphemer"},
|
||||
{"day": 27, "readings": ["Leviticus 25-27"], "theme": "Sabbath Year, Jubilee, Vows"},
|
||||
{"day": 28, "readings": ["Numbers 1-4"], "theme": "Census, Camp Arrangement, Levites"},
|
||||
{"day": 29, "readings": ["Numbers 5-8"], "theme": "Purity Laws, Nazarites, Levite Service"},
|
||||
{"day": 30, "readings": ["Numbers 9-12"], "theme": "Passover, Cloud and Fire, Complaints"},
|
||||
{"day": 31, "readings": ["Numbers 13-16"], "theme": "Spies, Rebellion, Korah"},
|
||||
{"day": 32, "readings": ["Numbers 17-21"], "theme": "Aaron's Rod, Red Heifer, Bronze Serpent"},
|
||||
{"day": 33, "readings": ["Numbers 22-26"], "theme": "Balaam, Baal of Peor, Second Census"},
|
||||
{"day": 34, "readings": ["Numbers 27-32"], "theme": "Zelophehad's Daughters, Joshua, Vows, Midianites"},
|
||||
{"day": 35, "readings": ["Numbers 33-36"], "theme": "Journey Stages, Land Boundaries, Levite Cities"},
|
||||
{"day": 36, "readings": ["Deuteronomy 1-4"], "theme": "Moses Reviews History, Obedience"},
|
||||
{"day": 37, "readings": ["Deuteronomy 5-11"], "theme": "Ten Commandments, Shema, Warnings"},
|
||||
{"day": 38, "readings": ["Deuteronomy 12-20"], "theme": "Worship, Clean Foods, Laws, Cities of Refuge"},
|
||||
{"day": 39, "readings": ["Deuteronomy 21-28"], "theme": "Various Laws, Blessings and Curses"},
|
||||
{"day": 40, "readings": ["Deuteronomy 29-34"], "theme": "Covenant Renewed, Song of Moses, Moses Dies"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"prophets_60": [
|
||||
{"day": 1, "readings": ["Isaiah 1-3"], "theme": "Judah's Rebellion and Call to Return"},
|
||||
{"day": 2, "readings": ["Isaiah 4-6"], "theme": "Isaiah's Vision and Call"},
|
||||
{"day": 3, "readings": ["Isaiah 7-9"], "theme": "Immanuel and Prince of Peace"},
|
||||
{"day": 4, "readings": ["Isaiah 10-12"], "theme": "Remnant and Branch of Jesse"},
|
||||
{"day": 5, "readings": ["Isaiah 13-16"], "theme": "Oracles Against Babylon and Moab"},
|
||||
{"day": 6, "readings": ["Isaiah 17-20"], "theme": "Damascus, Cush, and Egypt"},
|
||||
{"day": 7, "readings": ["Isaiah 21-23"], "theme": "Desert, Dumah, and Tyre"},
|
||||
{"day": 8, "readings": ["Isaiah 24-27"], "theme": "God's Universal Judgment and Victory"},
|
||||
{"day": 9, "readings": ["Isaiah 28-30"], "theme": "Woes to Ephraim and Jerusalem"},
|
||||
{"day": 10, "readings": ["Isaiah 31-33"], "theme": "Trust in God, Not Egypt"},
|
||||
{"day": 11, "readings": ["Isaiah 34-36"], "theme": "Judgment and Hezekiah's Crisis"},
|
||||
{"day": 12, "readings": ["Isaiah 37-39"], "theme": "Deliverance and Babylonian Embassy"},
|
||||
{"day": 13, "readings": ["Isaiah 40-42"], "theme": "Comfort and The Servant"},
|
||||
{"day": 14, "readings": ["Isaiah 43-45"], "theme": "Redemption and Cyrus"},
|
||||
{"day": 15, "readings": ["Isaiah 46-48"], "theme": "Babylon's Gods Fall"},
|
||||
{"day": 16, "readings": ["Isaiah 49-51"], "theme": "Servant's Mission to Nations"},
|
||||
{"day": 17, "readings": ["Isaiah 52-54"], "theme": "The Suffering Servant"},
|
||||
{"day": 18, "readings": ["Isaiah 55-57"], "theme": "Invitation to Grace"},
|
||||
{"day": 19, "readings": ["Isaiah 58-60"], "theme": "True Fasting and Zion's Glory"},
|
||||
{"day": 20, "readings": ["Isaiah 61-63"], "theme": "Year of Favor and Divine Warrior"},
|
||||
{"day": 21, "readings": ["Isaiah 64-66"], "theme": "Prayer and New Creation"},
|
||||
{"day": 22, "readings": ["Jeremiah 1-3"], "theme": "Jeremiah's Call and Israel's Adultery"},
|
||||
{"day": 23, "readings": ["Jeremiah 4-6"], "theme": "Invasion from the North"},
|
||||
{"day": 24, "readings": ["Jeremiah 7-9"], "theme": "Temple Sermon and Lament"},
|
||||
{"day": 25, "readings": ["Jeremiah 10-12"], "theme": "Idolatry and Jeremiah's Complaint"},
|
||||
{"day": 26, "readings": ["Jeremiah 13-15"], "theme": "Signs of Judgment"},
|
||||
{"day": 27, "readings": ["Jeremiah 16-18"], "theme": "No Marriage, Potter's House"},
|
||||
{"day": 28, "readings": ["Jeremiah 19-21"], "theme": "Broken Jar, Persecution"},
|
||||
{"day": 29, "readings": ["Jeremiah 22-24"], "theme": "Wicked Kings and Good Figs"},
|
||||
{"day": 30, "readings": ["Jeremiah 25-26"], "theme": "Seventy Years and Temple Trial"},
|
||||
{"day": 31, "readings": ["Jeremiah 27-29"], "theme": "Yokes and Letter to Exiles"},
|
||||
{"day": 32, "readings": ["Jeremiah 30-31"], "theme": "Book of Consolation"},
|
||||
{"day": 33, "readings": ["Jeremiah 32-33"], "theme": "Field at Anathoth, New Covenant"},
|
||||
{"day": 34, "readings": ["Jeremiah 34-36"], "theme": "Broken Covenant, Scroll Burned"},
|
||||
{"day": 35, "readings": ["Jeremiah 37-39"], "theme": "Jeremiah Imprisoned, Jerusalem Falls"},
|
||||
{"day": 36, "readings": ["Jeremiah 40-42"], "theme": "Gedaliah and Flight to Egypt"},
|
||||
{"day": 37, "readings": ["Jeremiah 43-45"], "theme": "In Egypt, Baruch's Promise"},
|
||||
{"day": 38, "readings": ["Jeremiah 46-48"], "theme": "Egypt, Philistia, Moab"},
|
||||
{"day": 39, "readings": ["Jeremiah 49-50"], "theme": "Ammon, Edom, Babylon"},
|
||||
{"day": 40, "readings": ["Jeremiah 51-52"], "theme": "Babylon's Doom and Jerusalem's Fall"},
|
||||
{"day": 41, "readings": ["Ezekiel 1-3"], "theme": "Ezekiel's Call and Commission"},
|
||||
{"day": 42, "readings": ["Ezekiel 4-7"], "theme": "Signs of Siege and Judgment"},
|
||||
{"day": 43, "readings": ["Ezekiel 8-11"], "theme": "Temple Abominations, Glory Departs"},
|
||||
{"day": 44, "readings": ["Ezekiel 12-14"], "theme": "Signs and False Prophets"},
|
||||
{"day": 45, "readings": ["Ezekiel 15-17"], "theme": "Useless Vine, Unfaithful Wife, Eagles"},
|
||||
{"day": 46, "readings": ["Ezekiel 18-20"], "theme": "Individual Responsibility, Israel's History"},
|
||||
{"day": 47, "readings": ["Ezekiel 21-23"], "theme": "Sword of the Lord, Two Sisters"},
|
||||
{"day": 48, "readings": ["Ezekiel 24-26"], "theme": "Cooking Pot, Wife's Death, Tyre"},
|
||||
{"day": 49, "readings": ["Ezekiel 27-29"], "theme": "Tyre's Wealth, Egypt's Fall"},
|
||||
{"day": 50, "readings": ["Ezekiel 30-32"], "theme": "Egypt's Judgment"},
|
||||
{"day": 51, "readings": ["Ezekiel 33-35"], "theme": "Watchman, Shepherds, Edom"},
|
||||
{"day": 52, "readings": ["Ezekiel 36-37"], "theme": "New Heart, Valley of Dry Bones"},
|
||||
{"day": 53, "readings": ["Ezekiel 38-39"], "theme": "Gog and Magog"},
|
||||
{"day": 54, "readings": ["Ezekiel 40-42"], "theme": "New Temple Vision"},
|
||||
{"day": 55, "readings": ["Ezekiel 43-45"], "theme": "Glory Returns, Prince's Duties"},
|
||||
{"day": 56, "readings": ["Ezekiel 46-48"], "theme": "Worship, River, Land Division"},
|
||||
{"day": 57, "readings": ["Daniel 1-3"], "theme": "Babylon, Dream, Fiery Furnace"},
|
||||
{"day": 58, "readings": ["Daniel 4-6"], "theme": "Nebuchadnezzar's Pride, Lion's Den"},
|
||||
{"day": 59, "readings": ["Daniel 7-9"], "theme": "Four Beasts, Seventy Weeks"},
|
||||
{"day": 60, "readings": ["Daniel 10-12"], "theme": "Kings of North and South, End Times"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"wisdom_30": [
|
||||
{"day": 1, "readings": ["Job 1-3"], "theme": "Job's Suffering Begins"},
|
||||
{"day": 2, "readings": ["Job 4-7"], "theme": "Eliphaz Speaks, Job Responds"},
|
||||
{"day": 3, "readings": ["Job 8-11"], "theme": "Bildad and Zophar Speak"},
|
||||
{"day": 4, "readings": ["Job 12-15"], "theme": "Job's Wisdom, Eliphaz Again"},
|
||||
{"day": 5, "readings": ["Job 16-19"], "theme": "Job's Redeemer Lives"},
|
||||
{"day": 6, "readings": ["Job 20-24"], "theme": "Wicked and Righteous"},
|
||||
{"day": 7, "readings": ["Job 25-31"], "theme": "Job's Final Defense"},
|
||||
{"day": 8, "readings": ["Job 32-37"], "theme": "Elihu's Speeches"},
|
||||
{"day": 9, "readings": ["Job 38-42"], "theme": "God Speaks, Job Restored"},
|
||||
{"day": 10, "readings": ["Psalms 1-18"], "theme": "Blessed Man, Messianic Psalms"},
|
||||
{"day": 11, "readings": ["Psalms 19-33"], "theme": "Heavens Declare, Good Shepherd"},
|
||||
{"day": 12, "readings": ["Psalms 34-45"], "theme": "Taste and See, Royal Wedding"},
|
||||
{"day": 13, "readings": ["Psalms 46-59"], "theme": "God Our Refuge, Mercy Psalms"},
|
||||
{"day": 14, "readings": ["Psalms 60-72"], "theme": "Rock Higher Than I, Solomon's Prayer"},
|
||||
{"day": 15, "readings": ["Psalms 73-85"], "theme": "Asaph's Struggles, Revival"},
|
||||
{"day": 16, "readings": ["Psalms 86-98"], "theme": "Teach Me Thy Way, New Song"},
|
||||
{"day": 17, "readings": ["Psalms 99-110"], "theme": "Enthronement, Melchizedek"},
|
||||
{"day": 18, "readings": ["Psalms 111-118"], "theme": "Hallel, Stone Rejected"},
|
||||
{"day": 19, "readings": ["Psalms 119"], "theme": "The Word of God"},
|
||||
{"day": 20, "readings": ["Psalms 120-134"], "theme": "Songs of Ascent"},
|
||||
{"day": 21, "readings": ["Psalms 135-150"], "theme": "Praise Collection, Hallelujah"},
|
||||
{"day": 22, "readings": ["Proverbs 1-5"], "theme": "Wisdom's Call, Avoid Folly"},
|
||||
{"day": 23, "readings": ["Proverbs 6-10"], "theme": "Warnings, Contrast of Wise and Foolish"},
|
||||
{"day": 24, "readings": ["Proverbs 11-15"], "theme": "Righteous vs. Wicked"},
|
||||
{"day": 25, "readings": ["Proverbs 16-20"], "theme": "The Lord Weighs Hearts"},
|
||||
{"day": 26, "readings": ["Proverbs 21-25"], "theme": "Solomon's Proverbs Continued"},
|
||||
{"day": 27, "readings": ["Proverbs 26-31"], "theme": "Fools, Agur, The Virtuous Woman"},
|
||||
{"day": 28, "readings": ["Ecclesiastes 1-6"], "theme": "Vanity of Vanities, Time for Everything"},
|
||||
{"day": 29, "readings": ["Ecclesiastes 7-12"], "theme": "Wisdom's Value, Remember Your Creator"},
|
||||
{"day": 30, "readings": ["Song of Solomon 1-8"], "theme": "The Beloved and the Lover"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
"""
|
||||
Custom Jinja2 template filters for KJV Study.
|
||||
|
||||
All filters are registered in register_filters() which should be called with
|
||||
the Jinja2 environment after templates are initialized.
|
||||
|
||||
Available Filters:
|
||||
slugify - Create URL-safe slugs from text
|
||||
md - Convert Markdown to HTML (full block elements)
|
||||
mdi - Convert inline Markdown (no paragraph wrapping)
|
||||
link_names - Link person names to family tree pages
|
||||
link_verses - Link verse references like "John 3:16" to verse pages
|
||||
inject_word_markers - Add sidenote markers for word studies
|
||||
red_letter - Wrap words of Christ in red letter spans
|
||||
format_lists - Convert (1), (2) patterns to HTML ordered lists
|
||||
split_paragraphs - Split text on double newlines into separate <p> tags
|
||||
number_format - Format numbers with commas (31102 -> 31,102)
|
||||
linkify_strongs - Convert Strong's refs (G1234, H5678) to links
|
||||
"""
|
||||
|
||||
import re
|
||||
import mistune
|
||||
|
||||
from .utils.helpers import create_slug
|
||||
|
||||
|
||||
# Initialize mistune for markdown rendering
|
||||
_markdown = mistune.create_markdown(escape=False, hard_wrap=False)
|
||||
_inline_markdown = mistune.create_markdown(
|
||||
renderer=mistune.HTMLRenderer(escape=False),
|
||||
plugins=['strikethrough']
|
||||
)
|
||||
|
||||
|
||||
def markdown_to_html(text):
|
||||
"""Convert markdown to HTML (bold, italic, paragraphs, etc.)."""
|
||||
if not text:
|
||||
return text
|
||||
return _markdown(text).strip()
|
||||
|
||||
|
||||
def markdown_inline(text):
|
||||
"""Convert inline markdown to HTML (bold, italic, etc. - no paragraph wrapping)."""
|
||||
if not text:
|
||||
return text
|
||||
html = _inline_markdown(text).strip()
|
||||
if html.startswith('<p>') and html.endswith('</p>'):
|
||||
html = html[3:-4]
|
||||
return html
|
||||
|
||||
|
||||
def link_person_names_in_text(text):
|
||||
"""
|
||||
Automatically link verse references and person names in text.
|
||||
|
||||
Links verse references like "Genesis 1:1" and person names like "Abraham"
|
||||
to their respective pages.
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
# First, link verse references
|
||||
verse_pattern = r'\b((?:1|2|3)\s+)?([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+(\d+):(\d+)(?:-(\d+))?\b'
|
||||
|
||||
def verse_replace_callback(match):
|
||||
number_prefix = match.group(1) or ''
|
||||
book_name = match.group(2)
|
||||
chapter = match.group(3)
|
||||
verse_start = match.group(4)
|
||||
matched_text = match.group(0)
|
||||
full_book = (number_prefix + book_name).strip()
|
||||
return f'<a href="/book/{full_book}/chapter/{chapter}/verse/{verse_start}">{matched_text}</a>'
|
||||
|
||||
text = re.sub(verse_pattern, verse_replace_callback, text)
|
||||
|
||||
# Then, link person names to family tree (lazy import to avoid circular dependency)
|
||||
from .server import get_person_name_mapping
|
||||
name_to_id = get_person_name_mapping()
|
||||
if not name_to_id:
|
||||
return text
|
||||
|
||||
sorted_names = sorted(name_to_id.keys(), key=len, reverse=True)
|
||||
|
||||
for name_lower in sorted_names:
|
||||
person_id = name_to_id[name_lower]
|
||||
name_pattern = re.escape(name_lower)
|
||||
|
||||
def replace_callback(match, text=text, person_id=person_id):
|
||||
matched_text = match.group(0)
|
||||
start_pos = match.start()
|
||||
text_before = text[:start_pos]
|
||||
last_lt = text_before.rfind('<')
|
||||
last_gt = text_before.rfind('>')
|
||||
|
||||
if last_lt > last_gt:
|
||||
return matched_text
|
||||
|
||||
if last_lt != -1:
|
||||
tag_content = text[last_lt:start_pos]
|
||||
if 'href=' in tag_content or 'src=' in tag_content:
|
||||
return matched_text
|
||||
|
||||
return f'<a href="/family-tree/person/{person_id}">{matched_text}</a>'
|
||||
|
||||
pattern = r'\b' + name_pattern + r'\b'
|
||||
text = re.sub(pattern, replace_callback, text, flags=re.IGNORECASE)
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def link_verse_references_in_text(text):
|
||||
"""Automatically link verse references in text (e.g., 'Genesis 1:1', 'Hebrews 9:22')"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
pattern = r'\b((?:1|2|3)\s+)?([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+(\d+):(\d+)(?:-(\d+))?\b'
|
||||
|
||||
def replace_reference(match):
|
||||
number_prefix = match.group(1) or ''
|
||||
book_name = match.group(2)
|
||||
chapter = match.group(3)
|
||||
verse_start = match.group(4)
|
||||
verse_end = match.group(5)
|
||||
full_book = (number_prefix + book_name).strip()
|
||||
full_reference = match.group(0)
|
||||
|
||||
start_pos = match.start()
|
||||
text_before = text[:start_pos]
|
||||
last_lt = text_before.rfind('<')
|
||||
last_gt = text_before.rfind('>')
|
||||
|
||||
if last_lt > last_gt:
|
||||
return full_reference
|
||||
|
||||
if last_lt != -1:
|
||||
tag_content = text[last_lt:start_pos]
|
||||
if 'href=' in tag_content or 'src=' in tag_content:
|
||||
return full_reference
|
||||
|
||||
if verse_end:
|
||||
url = f'/book/{full_book}/chapter/{chapter}#verse-{verse_start}-{verse_end}'
|
||||
else:
|
||||
url = f'/book/{full_book}/chapter/{chapter}#verse-{verse_start}'
|
||||
return f'<a href="{url}">{full_reference}</a>'
|
||||
|
||||
return re.sub(pattern, replace_reference, text)
|
||||
|
||||
|
||||
def inject_word_markers(text, word_studies, verse_num):
|
||||
"""Inject sidenote markers into verse text next to annotated words"""
|
||||
if not word_studies:
|
||||
return text
|
||||
|
||||
for idx, study in enumerate(word_studies, 1):
|
||||
word = study['word']
|
||||
marker = f'<label for="sn-{verse_num}-word-{idx}" class="margin-toggle sidenote-number"></label><input type="checkbox" id="sn-{verse_num}-word-{idx}" class="margin-toggle"/><span class="sidenote"><strong>{word}:</strong> {study["term"]} (<em>{study["translit"]}</em>). {study["note"]}</span>'
|
||||
pattern = re.compile(r'\b(' + re.escape(word) + r')(?!\'[sS])\b', re.IGNORECASE)
|
||||
text = pattern.sub(r'\1' + marker, text, count=1)
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def red_letter(text, book, chapter, verse_num):
|
||||
"""Wrap the words of Christ in red letter span tags"""
|
||||
from .red_letter import wrap_red_letter_text
|
||||
return wrap_red_letter_text(text, book, chapter, verse_num)
|
||||
|
||||
|
||||
def format_numbered_lists(text):
|
||||
"""Convert (1), (2), etc. patterns into HTML ordered lists"""
|
||||
def _convert(pattern: str, payload: str) -> str:
|
||||
markers = list(re.finditer(pattern, payload))
|
||||
if len(markers) < 2:
|
||||
return payload
|
||||
|
||||
numbers = [int(m.group(1)) for m in markers]
|
||||
if numbers[0] != 1:
|
||||
return payload
|
||||
|
||||
seq_length = 1
|
||||
for i in range(1, len(numbers)):
|
||||
if numbers[i] == seq_length + 1:
|
||||
seq_length += 1
|
||||
else:
|
||||
break
|
||||
|
||||
if seq_length < 2:
|
||||
return payload
|
||||
|
||||
markers = markers[:seq_length]
|
||||
list_items = []
|
||||
for i, marker in enumerate(markers):
|
||||
start = marker.end()
|
||||
|
||||
if i + 1 < len(markers):
|
||||
end = markers[i + 1].start()
|
||||
else:
|
||||
remaining = payload[start:]
|
||||
end_match = re.search(r'[.;]\s+(?=[A-Z])|$', remaining)
|
||||
end = start + end_match.start() + 1 if end_match else len(payload)
|
||||
|
||||
item_text = payload[start:end].strip().rstrip(';,')
|
||||
if item_text.endswith(' and'):
|
||||
item_text = item_text[:-4]
|
||||
list_items.append(f'<li>{item_text}</li>')
|
||||
|
||||
html_list = '<ol>' + ''.join(list_items) + '</ol>'
|
||||
list_start = markers[0].start()
|
||||
last_marker = markers[-1]
|
||||
last_item_start = last_marker.end()
|
||||
remaining_after_last = payload[last_item_start:]
|
||||
end_match = re.search(r'[.;]\s+(?=[A-Z])|$', remaining_after_last)
|
||||
list_end = last_item_start + end_match.start() + 1 if end_match else len(payload)
|
||||
|
||||
after_list = payload[list_end:].strip()
|
||||
if after_list:
|
||||
return payload[:list_start] + html_list + '</p><p>' + after_list
|
||||
return payload[:list_start] + html_list
|
||||
|
||||
primary_pattern = r'\((\d+)\)\s*'
|
||||
converted = _convert(primary_pattern, text)
|
||||
if converted != text:
|
||||
return converted
|
||||
|
||||
fallback_pattern = r'(?<=\s)(\d+)\)\s*'
|
||||
return _convert(fallback_pattern, text)
|
||||
|
||||
|
||||
def split_paragraphs(text):
|
||||
"""Convert double newlines or <br><br> into separate <p> tags for better navigation"""
|
||||
if not text:
|
||||
return text
|
||||
parts = re.split(r'\n\n|<br\s*/?>\s*<br\s*/?>', text)
|
||||
paragraphs = [f'<p>{p.strip()}</p>' for p in parts if p.strip()]
|
||||
return '\n'.join(paragraphs)
|
||||
|
||||
|
||||
def number_format(value):
|
||||
"""Format a number with commas (e.g., 31102 -> 31,102)"""
|
||||
return f"{value:,}"
|
||||
|
||||
|
||||
def linkify_strongs(text):
|
||||
"""Convert Strong's references like G1234 or H5678 to links."""
|
||||
if not text:
|
||||
return text
|
||||
pattern = r'\b([GH])(\d+)\b'
|
||||
def replace(match):
|
||||
prefix = match.group(1)
|
||||
num = match.group(2)
|
||||
return f'<a href="/strongs/{prefix}{num}" class="strongs-ref">{prefix}{num}</a>'
|
||||
return re.sub(pattern, replace, text)
|
||||
|
||||
|
||||
def register_filters(env):
|
||||
"""Register all custom filters with a Jinja2 environment."""
|
||||
env.filters['slugify'] = create_slug
|
||||
env.filters['md'] = markdown_to_html
|
||||
env.filters['mdi'] = markdown_inline
|
||||
env.filters['link_names'] = link_person_names_in_text
|
||||
env.filters['link_verses'] = link_verse_references_in_text
|
||||
env.filters['inject_word_markers'] = inject_word_markers
|
||||
env.filters['red_letter'] = red_letter
|
||||
env.filters['format_lists'] = format_numbered_lists
|
||||
env.filters['split_paragraphs'] = split_paragraphs
|
||||
env.filters['number_format'] = number_format
|
||||
env.filters['linkify_strongs'] = linkify_strongs
|
||||
@@ -37,6 +37,12 @@ NT_90_DAYS = _data.get("nt_90_days", {})
|
||||
PSALMS_PROVERBS = _data.get("psalms_proverbs", {})
|
||||
GOSPELS_ACTS_30 = _data.get("gospels_acts_30", {})
|
||||
PAUL_EPISTLES_30 = _data.get("paul_epistles_30", {})
|
||||
PENTATEUCH_40 = _data.get("pentateuch_40", {})
|
||||
PROPHETS_60 = _data.get("prophets_60", {})
|
||||
MINOR_PROPHETS_14 = _data.get("minor_prophets_14", {})
|
||||
WISDOM_30 = _data.get("wisdom_30", {})
|
||||
HISTORICAL_45 = _data.get("historical_45", {})
|
||||
GENERAL_EPISTLES_14 = _data.get("general_epistles_14", {})
|
||||
|
||||
# Reading plans database
|
||||
READING_PLANS = {
|
||||
@@ -125,6 +131,7 @@ READING_PLANS = {
|
||||
"description": "Study the Law and foundational narratives",
|
||||
"duration_days": 40,
|
||||
"overview": "Read Genesis through Deuteronomy in 40 days, exploring creation, the patriarchs, the Exodus, and the giving of the Law.",
|
||||
"days": PENTATEUCH_40,
|
||||
"sample_days": [
|
||||
{"day": 1, "readings": ["Genesis 1-3"], "theme": "Creation and Fall"},
|
||||
{"day": 10, "readings": ["Genesis 37-40"], "theme": "Joseph in Egypt"},
|
||||
@@ -138,6 +145,7 @@ READING_PLANS = {
|
||||
"description": "Read Isaiah, Jeremiah, Ezekiel, and Daniel",
|
||||
"duration_days": 60,
|
||||
"overview": "Study the major prophets, their messages of judgment and hope, and their Messianic prophecies fulfilled in Christ.",
|
||||
"days": PROPHETS_60,
|
||||
"sample_days": [
|
||||
{"day": 1, "readings": ["Isaiah 1-3"], "theme": "Isaiah's call"},
|
||||
{"day": 15, "readings": ["Isaiah 40-42"], "theme": "Comfort and the Servant"},
|
||||
@@ -161,6 +169,61 @@ READING_PLANS = {
|
||||
{"day": 25, "readings": ["1 Timothy 3-5"], "theme": "Church order"},
|
||||
{"day": 30, "readings": ["Philemon"], "theme": "Forgiveness and brotherhood"},
|
||||
]
|
||||
},
|
||||
"minor-prophets": {
|
||||
"name": "Minor Prophets in 14 Days",
|
||||
"description": "Read Hosea through Malachi",
|
||||
"duration_days": 14,
|
||||
"overview": "Study the twelve Minor Prophets in two weeks, exploring themes of judgment, repentance, and the coming Messiah. These shorter books pack powerful messages of God's justice and mercy.",
|
||||
"days": MINOR_PROPHETS_14,
|
||||
"sample_days": [
|
||||
{"day": 1, "readings": ["Hosea 1-7"], "theme": "God's Faithful Love for Unfaithful Israel"},
|
||||
{"day": 6, "readings": ["Obadiah", "Jonah 1-4"], "theme": "Edom's Doom, Jonah and God's Mercy"},
|
||||
{"day": 9, "readings": ["Habakkuk 1-3"], "theme": "The Just Shall Live by Faith"},
|
||||
{"day": 14, "readings": ["Malachi 1-4"], "theme": "Messenger of the Covenant, Elijah Returns"},
|
||||
]
|
||||
},
|
||||
"wisdom": {
|
||||
"name": "Wisdom Literature in 30 Days",
|
||||
"description": "Study Job, Psalms, Proverbs, Ecclesiastes, and Song of Solomon",
|
||||
"duration_days": 30,
|
||||
"overview": "Immerse yourself in the wisdom books of Scripture, from Job's suffering to the praises of the Psalms, the practical wisdom of Proverbs, the philosophical reflections of Ecclesiastes, and the poetry of the Song of Solomon.",
|
||||
"days": WISDOM_30,
|
||||
"sample_days": [
|
||||
{"day": 1, "readings": ["Job 1-3"], "theme": "Job's Suffering Begins"},
|
||||
{"day": 9, "readings": ["Job 38-42"], "theme": "God Speaks, Job Restored"},
|
||||
{"day": 19, "readings": ["Psalms 119"], "theme": "The Word of God"},
|
||||
{"day": 27, "readings": ["Proverbs 26-31"], "theme": "Fools, Agur, The Virtuous Woman"},
|
||||
{"day": 30, "readings": ["Song of Solomon 1-8"], "theme": "The Beloved and the Lover"},
|
||||
]
|
||||
},
|
||||
"historical": {
|
||||
"name": "Historical Books in 45 Days",
|
||||
"description": "Read Joshua through Esther",
|
||||
"duration_days": 45,
|
||||
"overview": "Journey through Israel's history from conquering the Promised Land to the exile and return. Experience the judges, kings, prophets, and God's faithfulness through triumph and tragedy.",
|
||||
"days": HISTORICAL_45,
|
||||
"sample_days": [
|
||||
{"day": 1, "readings": ["Joshua 1-5"], "theme": "Entering the Promised Land"},
|
||||
{"day": 11, "readings": ["Ruth 1-4"], "theme": "Loyalty, Redemption, David's Ancestry"},
|
||||
{"day": 24, "readings": ["1 Kings 5-8"], "theme": "Temple Built and Dedicated"},
|
||||
{"day": 34, "readings": ["2 Kings 24-25"], "theme": "Jerusalem Falls, Exile"},
|
||||
{"day": 45, "readings": ["Nehemiah 3-13", "Esther 1-10"], "theme": "Walls Rebuilt, Esther Saves Her People"},
|
||||
]
|
||||
},
|
||||
"general-epistles": {
|
||||
"name": "General Epistles in 14 Days",
|
||||
"description": "Read Hebrews through Jude plus Revelation's letters",
|
||||
"duration_days": 14,
|
||||
"overview": "Study the non-Pauline letters of the New Testament, exploring themes of faith, perseverance, practical Christian living, and warnings against false teaching.",
|
||||
"days": GENERAL_EPISTLES_14,
|
||||
"sample_days": [
|
||||
{"day": 1, "readings": ["Hebrews 1-4"], "theme": "Christ Superior to Angels and Moses"},
|
||||
{"day": 5, "readings": ["James 1-3"], "theme": "Faith and Works, Taming the Tongue"},
|
||||
{"day": 10, "readings": ["1 John 1-3"], "theme": "Fellowship, Walking in Light, Love"},
|
||||
{"day": 13, "readings": ["Jude"], "theme": "Contending for the Faith"},
|
||||
{"day": 14, "readings": ["Revelation 1-3"], "theme": "Vision of Christ, Letters to Churches"},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+91
-302
@@ -223,42 +223,12 @@ app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
||||
templates = Jinja2Templates(directory=str(templates_dir))
|
||||
|
||||
# Register custom Jinja2 filters
|
||||
templates.env.filters['slugify'] = create_slug
|
||||
from .jinja_filters import register_filters
|
||||
register_filters(templates.env)
|
||||
|
||||
# Add global template variables
|
||||
templates.env.globals['disable_analytics'] = os.getenv("DISABLE_ANALYTICS", "false").lower() == "true"
|
||||
|
||||
# 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, italic, etc. - no paragraph wrapping)."""
|
||||
if not 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 markdown to HTML (bold, italic, paragraphs, etc.)."""
|
||||
if not text:
|
||||
return text
|
||||
return _markdown(text).strip()
|
||||
|
||||
templates.env.filters['md'] = markdown_to_html
|
||||
templates.env.filters['mdi'] = markdown_inline
|
||||
|
||||
# Initialize templates for route modules
|
||||
init_api_templates(templates)
|
||||
init_resources_templates(templates)
|
||||
@@ -823,274 +793,6 @@ def search_family_tree(query: str, limit: Optional[int] = None) -> List[Dict]:
|
||||
return results
|
||||
|
||||
|
||||
def link_person_names_in_text(text: str) -> str:
|
||||
"""
|
||||
Find person names and verse references in text and link them.
|
||||
Links person names to family tree pages and verse references to verse pages.
|
||||
Avoids linking content that's already inside HTML tags.
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
# First, link verse references (e.g., "Genesis 3:15", "1 Samuel 2:1")
|
||||
# Pattern matches: Book name + chapter:verse
|
||||
verse_pattern = r'\b((?:1|2|3)\s)?([A-Z][a-z]+(?:\s+of\s+[A-Z][a-z]+)?)\s+(\d+):(\d+)(?:-(\d+))?\b'
|
||||
|
||||
def verse_replace_callback(match):
|
||||
matched_text = match.group(0)
|
||||
start_pos = match.start()
|
||||
|
||||
# Check if we're inside an HTML tag
|
||||
text_before = text[:start_pos]
|
||||
last_lt = text_before.rfind('<')
|
||||
last_gt = text_before.rfind('>')
|
||||
|
||||
if last_lt > last_gt:
|
||||
return matched_text
|
||||
|
||||
if last_lt != -1:
|
||||
tag_content = text[last_lt:start_pos]
|
||||
if 'href=' in tag_content or 'src=' in tag_content:
|
||||
return matched_text
|
||||
|
||||
# Extract parts
|
||||
number_prefix = match.group(1) or '' # "1 ", "2 ", etc.
|
||||
book_name = match.group(2) # Main book name
|
||||
chapter = match.group(3)
|
||||
verse_start = match.group(4)
|
||||
verse_end = match.group(5) # May be None
|
||||
|
||||
# Construct full book name
|
||||
full_book = (number_prefix + book_name).strip()
|
||||
|
||||
# Link to the first verse in the range
|
||||
return f'<a href="/book/{full_book}/chapter/{chapter}/verse/{verse_start}">{matched_text}</a>'
|
||||
|
||||
text = re.sub(verse_pattern, verse_replace_callback, text)
|
||||
|
||||
# Then, link person names to family tree
|
||||
name_to_id = get_person_name_mapping()
|
||||
|
||||
if not name_to_id:
|
||||
return text
|
||||
|
||||
# Sort names by length (longest first) to handle multi-word names correctly
|
||||
sorted_names = sorted(name_to_id.keys(), key=len, reverse=True)
|
||||
|
||||
# Process each name
|
||||
for name_lower in sorted_names:
|
||||
person_id = name_to_id[name_lower]
|
||||
|
||||
# Create a case-insensitive regex pattern with word boundaries
|
||||
# This will match the name but not if it's inside an HTML tag
|
||||
name_pattern = re.escape(name_lower)
|
||||
|
||||
# Use a callback function to avoid replacing text inside HTML tags
|
||||
def replace_callback(match):
|
||||
matched_text = match.group(0)
|
||||
# Check if this match is inside an HTML tag
|
||||
start_pos = match.start()
|
||||
|
||||
# Look backwards to see if we're inside a tag
|
||||
text_before = text[:start_pos]
|
||||
last_lt = text_before.rfind('<')
|
||||
last_gt = text_before.rfind('>')
|
||||
|
||||
# If the last '<' is more recent than the last '>', we're inside a tag
|
||||
if last_lt > last_gt:
|
||||
return matched_text
|
||||
|
||||
# Also check if we're inside an href or other attribute
|
||||
if last_lt != -1:
|
||||
tag_content = text[last_lt:start_pos]
|
||||
if 'href=' in tag_content or 'src=' in tag_content:
|
||||
return matched_text
|
||||
|
||||
# Safe to link
|
||||
return f'<a href="/family-tree/person/{person_id}">{matched_text}</a>'
|
||||
|
||||
# Use word boundaries and case-insensitive matching
|
||||
pattern = r'\b' + name_pattern + r'\b'
|
||||
text = re.sub(pattern, replace_callback, text, flags=re.IGNORECASE)
|
||||
|
||||
return text
|
||||
|
||||
|
||||
# Register the custom Jinja2 filter for linking person names in templates
|
||||
templates.env.filters['link_names'] = link_person_names_in_text
|
||||
|
||||
|
||||
def link_verse_references_in_text(text):
|
||||
"""Automatically link verse references in text (e.g., 'Genesis 1:1', 'Hebrews 9:22')"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
# Pattern to match verse references like "Genesis 1:1", "1 Corinthians 5:7", "Romans 4:3"
|
||||
# Matches: BookName Chapter:Verse or BookName Chapter:Verse-Verse
|
||||
pattern = r'\b((?:1|2|3)\s+)?([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+(\d+):(\d+)(?:-(\d+))?\b'
|
||||
|
||||
def replace_reference(match):
|
||||
number_prefix = match.group(1) or '' # "1 ", "2 ", "3 " or empty
|
||||
book_name = match.group(2) # "Corinthians", "Kings", "Genesis"
|
||||
chapter = match.group(3)
|
||||
verse_start = match.group(4)
|
||||
verse_end = match.group(5) # Could be None if no range
|
||||
|
||||
# Construct full book name
|
||||
full_book = (number_prefix + book_name).strip()
|
||||
full_reference = match.group(0)
|
||||
|
||||
# Check if this match is inside an HTML tag
|
||||
start_pos = match.start()
|
||||
text_before = text[:start_pos]
|
||||
last_lt = text_before.rfind('<')
|
||||
last_gt = text_before.rfind('>')
|
||||
|
||||
# If inside a tag, don't replace
|
||||
if last_lt > last_gt:
|
||||
return full_reference
|
||||
|
||||
# Also check if we're inside an href or other attribute
|
||||
if last_lt != -1:
|
||||
tag_content = text[last_lt:start_pos]
|
||||
if 'href=' in tag_content or 'src=' in tag_content:
|
||||
return full_reference
|
||||
|
||||
# Create link to chapter view with anchor
|
||||
if verse_end:
|
||||
url = f'/book/{full_book}/chapter/{chapter}#verse-{verse_start}-{verse_end}'
|
||||
else:
|
||||
url = f'/book/{full_book}/chapter/{chapter}#verse-{verse_start}'
|
||||
return f'<a href="{url}">{full_reference}</a>'
|
||||
|
||||
return re.sub(pattern, replace_reference, text)
|
||||
|
||||
|
||||
# Register the verse reference linking filter
|
||||
templates.env.filters['link_verses'] = link_verse_references_in_text
|
||||
|
||||
|
||||
def inject_word_markers(text, word_studies, verse_num):
|
||||
"""Inject sidenote markers into verse text next to annotated words"""
|
||||
if not word_studies:
|
||||
return text
|
||||
|
||||
# Process each word study
|
||||
for idx, study in enumerate(word_studies, 1):
|
||||
word = study['word']
|
||||
# Create the sidenote marker HTML
|
||||
marker = f'<label for="sn-{verse_num}-word-{idx}" class="margin-toggle sidenote-number"></label><input type="checkbox" id="sn-{verse_num}-word-{idx}" class="margin-toggle"/><span class="sidenote"><strong>{word}:</strong> {study["term"]} (<em>{study["translit"]}</em>). {study["note"]}</span>'
|
||||
|
||||
# Find and replace the word with word + marker
|
||||
# Use a more precise replacement to avoid replacing partial matches
|
||||
import re
|
||||
# Match the word with word boundaries, but NOT if followed by possessive 's
|
||||
# This prevents breaking up "LORD'S" into "LORD" + "'S"
|
||||
pattern = re.compile(r'\b(' + re.escape(word) + r')(?!\'[sS])\b', re.IGNORECASE)
|
||||
text = pattern.sub(r'\1' + marker, text, count=1)
|
||||
|
||||
return text
|
||||
|
||||
templates.env.filters['inject_word_markers'] = inject_word_markers
|
||||
|
||||
|
||||
def red_letter(text, book, chapter, verse_num):
|
||||
"""Wrap the words of Christ in red letter span tags"""
|
||||
from .red_letter import wrap_red_letter_text
|
||||
|
||||
return wrap_red_letter_text(text, book, chapter, verse_num)
|
||||
|
||||
templates.env.filters['red_letter'] = red_letter
|
||||
|
||||
|
||||
def format_numbered_lists(text):
|
||||
"""Convert (1), (2), etc. patterns into HTML ordered lists"""
|
||||
import re
|
||||
|
||||
def _convert(pattern: str, payload: str) -> str:
|
||||
markers = list(re.finditer(pattern, payload))
|
||||
if len(markers) < 2:
|
||||
return payload
|
||||
|
||||
numbers = [int(m.group(1)) for m in markers]
|
||||
if numbers[0] != 1:
|
||||
return payload
|
||||
|
||||
seq_length = 1
|
||||
for i in range(1, len(numbers)):
|
||||
if numbers[i] == seq_length + 1:
|
||||
seq_length += 1
|
||||
else:
|
||||
break
|
||||
|
||||
if seq_length < 2:
|
||||
return payload
|
||||
|
||||
markers = markers[:seq_length]
|
||||
list_items = []
|
||||
for i, marker in enumerate(markers):
|
||||
start = marker.end()
|
||||
|
||||
if i + 1 < len(markers):
|
||||
end = markers[i + 1].start()
|
||||
else:
|
||||
remaining = payload[start:]
|
||||
end_match = re.search(r'[.;]\s+(?=[A-Z])|$', remaining)
|
||||
end = start + end_match.start() + 1 if end_match else len(payload)
|
||||
|
||||
item_text = payload[start:end].strip().rstrip(';,')
|
||||
if item_text.endswith(' and'):
|
||||
item_text = item_text[:-4]
|
||||
list_items.append(f'<li>{item_text}</li>')
|
||||
|
||||
html_list = '<ol>' + ''.join(list_items) + '</ol>'
|
||||
list_start = markers[0].start()
|
||||
last_marker = markers[-1]
|
||||
last_item_start = last_marker.end()
|
||||
remaining_after_last = payload[last_item_start:]
|
||||
end_match = re.search(r'[.;]\s+(?=[A-Z])|$', remaining_after_last)
|
||||
list_end = last_item_start + end_match.start() + 1 if end_match else len(payload)
|
||||
|
||||
after_list = payload[list_end:].strip()
|
||||
if after_list:
|
||||
return payload[:list_start] + html_list + '</p><p>' + after_list
|
||||
return payload[:list_start] + html_list
|
||||
|
||||
# Try classic (1) pattern first to avoid false positives in verse refs
|
||||
primary_pattern = r'\((\d+)\)\s*'
|
||||
converted = _convert(primary_pattern, text)
|
||||
if converted != text:
|
||||
return converted
|
||||
|
||||
# Fallback: bare "1)" patterns preceded by whitespace (not verse refs)
|
||||
fallback_pattern = r'(?<=\s)(\d+)\)\s*'
|
||||
return _convert(fallback_pattern, text)
|
||||
|
||||
templates.env.filters['format_lists'] = format_numbered_lists
|
||||
|
||||
def number_format(value):
|
||||
"""Format a number with commas (e.g., 31102 -> 31,102)"""
|
||||
return f"{value:,}"
|
||||
|
||||
templates.env.filters['number_format'] = number_format
|
||||
|
||||
|
||||
def linkify_strongs(text):
|
||||
"""Convert Strong's references like G1234 or H5678 to links."""
|
||||
import re
|
||||
if not text:
|
||||
return text
|
||||
# Match G or H followed by digits, optionally in parentheses with Greek/Hebrew text
|
||||
pattern = r'\b([GH])(\d+)\b'
|
||||
def replace(match):
|
||||
prefix = match.group(1)
|
||||
num = match.group(2)
|
||||
return f'<a href="/strongs/{prefix}{num}" class="strongs-ref">{prefix}{num}</a>'
|
||||
return re.sub(pattern, replace, text)
|
||||
|
||||
templates.env.filters['linkify_strongs'] = linkify_strongs
|
||||
|
||||
|
||||
def get_biblical_timeline_context():
|
||||
"""
|
||||
Load comprehensive biblical timeline data from JSON file.
|
||||
@@ -1769,6 +1471,22 @@ async def reading_plan_detail(request: Request, plan_id: str):
|
||||
if not plan:
|
||||
raise HTTPException(status_code=404, detail="Reading plan not found")
|
||||
|
||||
# For plans 90 days or less, include full Bible text
|
||||
include_text = plan.get('duration_days', 365) <= 90
|
||||
days_with_text = None
|
||||
|
||||
if include_text:
|
||||
all_days = plan.get('days') or plan.get('sample_days', [])
|
||||
days_with_text = []
|
||||
for day in all_days:
|
||||
day_data = {
|
||||
'day': day['day'],
|
||||
'theme': day.get('theme', ''),
|
||||
'readings': day['readings'],
|
||||
'text': get_reading_text(day['readings'])
|
||||
}
|
||||
days_with_text.append(day_data)
|
||||
|
||||
breadcrumbs = [
|
||||
{"text": "Home", "url": "/"},
|
||||
{"text": "Reading Plans", "url": "/reading-plans"},
|
||||
@@ -1784,11 +1502,62 @@ async def reading_plan_detail(request: Request, plan_id: str):
|
||||
"books": books,
|
||||
"breadcrumbs": breadcrumbs,
|
||||
"pdf_available": WEASYPRINT_AVAILABLE,
|
||||
"pdf_url": f"/reading-plans/{plan_id}/pdf" if WEASYPRINT_AVAILABLE else None
|
||||
"pdf_url": f"/reading-plans/{plan_id}/pdf" if WEASYPRINT_AVAILABLE else None,
|
||||
"include_text": include_text,
|
||||
"days_with_text": days_with_text
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def parse_reading_reference(ref: str) -> list:
|
||||
"""Parse a reading reference like 'Genesis 1-3' or 'Matthew 1' into chapter list.
|
||||
|
||||
Returns list of tuples: [(book, chapter), ...]
|
||||
"""
|
||||
# Handle patterns like "Genesis 1-3", "Matthew 1", "1 John 2-3"
|
||||
# Pattern: optional number prefix + book name + chapter range
|
||||
pattern = r'^((?:\d\s+)?[A-Za-z]+(?:\s+[A-Za-z]+)?)\s+(\d+)(?:-(\d+))?$'
|
||||
match = re.match(pattern, ref.strip())
|
||||
if not match:
|
||||
return []
|
||||
|
||||
book = match.group(1)
|
||||
start_ch = int(match.group(2))
|
||||
end_ch = int(match.group(3)) if match.group(3) else start_ch
|
||||
|
||||
# Normalize book name - if it's already canonical, use it as-is
|
||||
normalized = normalize_book_name(book)
|
||||
if not normalized:
|
||||
# Check if it's already a valid canonical name
|
||||
all_books = OT_BOOKS + NT_BOOKS
|
||||
if book in all_books:
|
||||
normalized = book
|
||||
else:
|
||||
return []
|
||||
|
||||
return [(normalized, ch) for ch in range(start_ch, end_ch + 1)]
|
||||
|
||||
|
||||
def get_reading_text(readings: list) -> list:
|
||||
"""Get the Bible text for a list of reading references.
|
||||
|
||||
Returns list of dicts with book, chapter, and verses.
|
||||
"""
|
||||
result = []
|
||||
for ref in readings:
|
||||
chapters = parse_reading_reference(ref)
|
||||
for book, chapter in chapters:
|
||||
verses = bible.get_verses_by_book_chapter(book, chapter)
|
||||
if verses:
|
||||
result.append({
|
||||
'book': book,
|
||||
'chapter': chapter,
|
||||
'verses': verses,
|
||||
'reference': f"{book} {chapter}"
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
@app.get("/reading-plans/{plan_id}/pdf")
|
||||
async def reading_plan_pdf(plan_id: str):
|
||||
"""Generate a PDF export for a reading plan."""
|
||||
@@ -1802,7 +1571,27 @@ async def reading_plan_pdf(plan_id: str):
|
||||
if not plan:
|
||||
raise HTTPException(status_code=404, detail="Reading plan not found")
|
||||
|
||||
html_content = templates.get_template("reading_plan_pdf.html").render(plan=plan)
|
||||
# For plans 90 days or less, include full Bible text (excludes 365-day plans)
|
||||
include_text = plan.get('duration_days', 365) <= 90
|
||||
|
||||
days_with_text = None
|
||||
if include_text:
|
||||
all_days = plan.get('days') or plan.get('sample_days', [])
|
||||
days_with_text = []
|
||||
for day in all_days:
|
||||
day_data = {
|
||||
'day': day['day'],
|
||||
'theme': day.get('theme', ''),
|
||||
'readings': day['readings'],
|
||||
'text': get_reading_text(day['readings'])
|
||||
}
|
||||
days_with_text.append(day_data)
|
||||
|
||||
html_content = templates.get_template("reading_plan_pdf.html").render(
|
||||
plan=plan,
|
||||
include_text=include_text,
|
||||
days_with_text=days_with_text
|
||||
)
|
||||
pdf_buffer = await render_html_to_pdf_async(html_content)
|
||||
|
||||
filename = f"reading-plan-{plan_id}.pdf"
|
||||
|
||||
@@ -15,43 +15,6 @@
|
||||
background: var(--code-bg);
|
||||
}
|
||||
|
||||
.sample-days {
|
||||
max-width: 70%;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.day-entry {
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
font-weight: 600;
|
||||
color: var(--link-color);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.day-readings {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.reading-ref {
|
||||
display: inline-block;
|
||||
margin: 0.25rem 0.5rem 0.25rem 0;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: var(--code-bg);
|
||||
border-radius: 3px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.day-theme {
|
||||
font-style: italic;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.plan-stats {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
@@ -116,12 +79,157 @@
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Day navigation */
|
||||
.day-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin: 1.5rem 0;
|
||||
padding: 1rem;
|
||||
background: var(--code-bg);
|
||||
border-radius: 4px;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.day-nav a {
|
||||
display: inline-block;
|
||||
padding: 0.3rem 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
text-decoration: none;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.day-nav a:hover {
|
||||
border-color: var(--link-color);
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
/* Day entries with scripture */
|
||||
.reading-day {
|
||||
margin: 2.5rem 0;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.day-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
.day-theme {
|
||||
font-style: italic;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.day-readings-summary {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.day-readings-summary span {
|
||||
display: inline-block;
|
||||
margin-right: 0.5rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
background: var(--code-bg);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Scripture text display */
|
||||
.scripture-content {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.chapter-section {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.chapter-heading {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.chapter-heading a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.chapter-heading a:hover {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
.verse-text {
|
||||
line-height: 1.85;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.verse-num {
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
vertical-align: super;
|
||||
margin-right: 0.1rem;
|
||||
}
|
||||
|
||||
/* Reference-only view (for 365-day plans) */
|
||||
.sample-days {
|
||||
max-width: 70%;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.day-entry {
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.reading-ref {
|
||||
display: inline-block;
|
||||
margin: 0.25rem 0.5rem 0.25rem 0;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: var(--code-bg);
|
||||
border-radius: 3px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.plan-actions,
|
||||
.print-btn {
|
||||
.print-btn,
|
||||
.day-nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.plan-overview,
|
||||
.plan-stats,
|
||||
.intro-text,
|
||||
.sample-days {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.day-nav {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -135,7 +243,7 @@
|
||||
<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 Plan (PDF)
|
||||
Download PDF{% if include_text %} (with Scripture text){% endif %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -157,6 +265,53 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if include_text and days_with_text %}
|
||||
{# Full text version with scripture #}
|
||||
<section>
|
||||
<h2>Reading Schedule</h2>
|
||||
<p class="intro-text">Jump to any day:</p>
|
||||
|
||||
<div class="day-nav">
|
||||
{% for day in days_with_text %}
|
||||
<a href="#day-{{ day.day }}">{{ day.day }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% for day in days_with_text %}
|
||||
<section class="reading-day" id="day-{{ day.day }}">
|
||||
<div class="day-header">
|
||||
<span class="day-number">Day {{ day.day }}</span>
|
||||
{% if day.theme %}<span class="day-theme">{{ day.theme }}</span>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="day-readings-summary">
|
||||
{% for reading in day.readings %}
|
||||
<span>{{ reading }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if day.text %}
|
||||
<div class="scripture-content">
|
||||
{% for section in day.text %}
|
||||
<div class="chapter-section">
|
||||
<div class="chapter-heading">
|
||||
<a href="/book/{{ section.book }}/chapter/{{ section.chapter }}">{{ section.reference }}</a>
|
||||
</div>
|
||||
<div class="verse-text">
|
||||
{% for verse in section.verses %}
|
||||
<sup class="verse-num">{{ verse.verse }}</sup>{{ verse.text }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
|
||||
{% else %}
|
||||
{# Reference-only version for longer plans #}
|
||||
<section>
|
||||
<h2>Complete Reading Schedule</h2>
|
||||
<p class="intro-text">All {{ plan.duration_days }} days of readings for this plan.</p>
|
||||
@@ -173,7 +328,6 @@
|
||||
{% if ref_parts|length >= 2 %}
|
||||
{% set chapter_verse = ref_parts[-1] %}
|
||||
{% if ':' in chapter_verse %}
|
||||
{# Format: "Book Chapter:Verse" or "Book Chapter:Verse-Verse" #}
|
||||
{% set chapter = chapter_verse.split(':')[0] %}
|
||||
{% set verse_part = chapter_verse.split(':')[1] %}
|
||||
{% if '-' in verse_part %}
|
||||
@@ -184,18 +338,15 @@
|
||||
{% set book = ' '.join(ref_parts[:-1]) %}
|
||||
<a href="/book/{{ book }}/chapter/{{ chapter }}/verse/{{ verse_num }}">{{ reading }}</a>
|
||||
{% elif '-' in chapter_verse %}
|
||||
{# Format: "Book Chapter-Chapter" (e.g., "Psalm 1-3") #}
|
||||
{% set first_chapter = chapter_verse.split('-')[0] %}
|
||||
{% set book = ' '.join(ref_parts[:-1]) %}
|
||||
<a href="/book/{{ book }}/chapter/{{ first_chapter }}">{{ reading }}</a>
|
||||
{% else %}
|
||||
{# Format: "Book Chapter" (single chapter) #}
|
||||
{% set chapter = ref_parts[-1] %}
|
||||
{% set book = ' '.join(ref_parts[:-1]) %}
|
||||
<a href="/book/{{ book }}/chapter/{{ chapter }}">{{ reading }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{# Fallback: just display as text #}
|
||||
{{ reading }}
|
||||
{% endif %}
|
||||
</span>
|
||||
@@ -206,6 +357,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section>
|
||||
<h2>How to Use This Plan</h2>
|
||||
@@ -222,8 +374,7 @@
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// Include overview, intro text, day entries, and PDF button
|
||||
const elements = Array.from(document.querySelectorAll('.plan-overview, .intro-text, .day-entry, .print-btn'));
|
||||
const elements = Array.from(document.querySelectorAll('.plan-overview, .intro-text, .reading-day, .day-entry, .print-btn'));
|
||||
if (elements.length === 0) return;
|
||||
|
||||
let selectedIndex = -1;
|
||||
|
||||
@@ -41,14 +41,29 @@
|
||||
}
|
||||
|
||||
.day-entry {
|
||||
margin-bottom: 0.2in;
|
||||
padding-bottom: 0.15in;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 0.3in;
|
||||
padding-bottom: 0.2in;
|
||||
border-bottom: 1px solid #ddd;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.day-header {
|
||||
background: #f5f5f5;
|
||||
padding: 0.1in 0.15in;
|
||||
margin-bottom: 0.15in;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
font-weight: 600;
|
||||
color: #444;
|
||||
color: #333;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
.day-theme {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
margin-left: 0.2in;
|
||||
}
|
||||
|
||||
.day-readings {
|
||||
@@ -64,9 +79,35 @@
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.day-theme {
|
||||
font-style: italic;
|
||||
/* Scripture text styling */
|
||||
.scripture-section {
|
||||
margin-top: 0.15in;
|
||||
}
|
||||
|
||||
.chapter-heading {
|
||||
font-size: 13pt;
|
||||
font-weight: 600;
|
||||
color: #444;
|
||||
margin: 0.2in 0 0.1in 0;
|
||||
padding-bottom: 0.05in;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.verse {
|
||||
margin: 0.03in 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.verse-num {
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
font-size: 9pt;
|
||||
vertical-align: super;
|
||||
margin-right: 0.03in;
|
||||
}
|
||||
|
||||
.verse-text {
|
||||
/* inline with verse number */
|
||||
}
|
||||
|
||||
.footer {
|
||||
@@ -76,6 +117,15 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Page breaks */
|
||||
.day-entry-with-text {
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.day-entry-with-text:last-child {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
/* Style sidenotes as inline notes for PDF */
|
||||
.margin-toggle,
|
||||
.sidenote-number,
|
||||
@@ -110,6 +160,31 @@
|
||||
|
||||
<div class="overview">{{ plan.overview | safe }}</div>
|
||||
|
||||
{% if include_text and days_with_text %}
|
||||
{# Full text version for shorter plans #}
|
||||
{% for day in days_with_text %}
|
||||
<div class="day-entry day-entry-with-text">
|
||||
<div class="day-header">
|
||||
<span class="day-number">Day {{ day.day }}</span>
|
||||
{% if day.theme %}<span class="day-theme">{{ day.theme }}</span>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="scripture-section">
|
||||
{% for section in day.text %}
|
||||
<div class="chapter-heading">{{ section.reference }}</div>
|
||||
{% for verse in section.verses %}
|
||||
<span class="verse">
|
||||
<span class="verse-num">{{ verse.verse }}</span>
|
||||
<span class="verse-text">{{ verse.text }}</span>
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% else %}
|
||||
{# Reference-only version for longer plans #}
|
||||
{% set all_days = plan.days if plan.days else plan.sample_days %}
|
||||
{% for day in all_days %}
|
||||
<div class="day-entry">
|
||||
@@ -122,6 +197,7 @@
|
||||
<div class="day-theme">Theme: {{ day.theme }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="footer">From KJV Study • kjvstudy.org</div>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user