markdownr 0.5.12 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 280f1aab156292d75fc2e561f222bb7b92870cb85a68df13494f4614855f437f
4
- data.tar.gz: dd83fcbf5303eb687017a7bf5783d4557a5e723e367c5c0641c0e072e8ddab23
3
+ metadata.gz: 8825ac583ace67b9cd50c7aef4d09a9de175bd6f3c81d4fcd2d8d5159476b76f
4
+ data.tar.gz: 55301d8c94bea36eef2b30571a4971e4d2661689ed4faab4d85a2632dda61af3
5
5
  SHA512:
6
- metadata.gz: b31d87695427970ae2dd48e4f01b3b39ca5e7e1b7ff1a57a83d231d386881975158a72d4501bb57d838f8247f832892483b70a815fba7b46ae8099eabce3da2d
7
- data.tar.gz: 7e46d20ad61fd47243ad69b02ed95e282817e94b5726d5c3f8d46b1b72a9af4856de17de71b234d0fb2b8a6e506cb21bd4e199c0f1460e38e3c02463cb33a704
6
+ metadata.gz: 4fb68ab533ae36e9509d85c29bddb4c23fc36747a4eadfd1e1b78fe73d1af2d872f14089557db3dbe43ed1fec08df44b60cabd07471664f3792e20ff60857c04
7
+ data.tar.gz: abe3b62dc09675cc99d670915cae25690270ce83a9274cf7954020c8b883b94c22ed9d984e12cf27858fff227346196137cae291b0d944cce3290a7e8672364b
@@ -10,11 +10,14 @@ require "pathname"
10
10
  require "set"
11
11
  require "net/http"
12
12
  require "base64"
13
+ require_relative "bible_citations"
13
14
 
14
15
  module MarkdownServer
15
16
  class App < Sinatra::Base
16
17
  EXCLUDED = %w[.git .obsidian __pycache__ .DS_Store node_modules .claude].freeze
17
18
 
19
+ # Bible citation auto-linking — see lib/markdown_server/bible_citations.rb
20
+
18
21
  set :views, File.expand_path("../../views", __dir__)
19
22
 
20
23
  configure do
@@ -131,6 +134,19 @@ module MarkdownServer
131
134
  "<div class=\"mermaid\">\n#{h($1.rstrip)}\n</div>\n\n"
132
135
  end
133
136
 
137
+ # Auto-link bare Bible verse citations (e.g. "John 3:16" → BibleGateway link).
138
+ # Skips inline code, fenced code blocks, and citations already inside a link.
139
+ text = text.gsub(BIBLE_CITATION_RE) do |m|
140
+ next m if $1 || $2 || $3 # code span / code fence / existing link
141
+ citation = $4
142
+ entry = BIBLE_BOOK_MAP_SORTED.find { |k, _| citation.start_with?(k) }
143
+ next m unless entry
144
+ abbrev, canonical = entry
145
+ rest = citation[abbrev.length..].sub(/\A\.?[ \t]?/, "")
146
+ url = "https://www.biblegateway.com/passage/?search=#{CGI.escape("#{canonical} #{rest}")}&version=CEB"
147
+ "[#{citation}](#{url})"
148
+ end
149
+
134
150
  # Process wiki links BEFORE Kramdown so that | isn't consumed as
135
151
  # a GFM table delimiter.
136
152
  text = text.gsub(/\[\[([^\]]+)\]\]/) do
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Bible citation auto-linking for Markdown rendering.
4
+ #
5
+ # Provides two constants used in render_markdown:
6
+ #
7
+ # BIBLE_BOOK_MAP – hash of abbreviation → BibleGateway canonical name
8
+ # BIBLE_CITATION_RE – combined regex that matches inline code, fenced code
9
+ # blocks, existing markdown links (to skip), and bare
10
+ # citations (to replace with a BibleGateway link).
11
+ #
12
+ # Usage in render_markdown:
13
+ #
14
+ # text = text.gsub(BIBLE_CITATION_RE) do |m|
15
+ # next m if $1 || $2 || $3 # code span / code fence / existing link
16
+ # citation = $4
17
+ # canonical = BIBLE_BOOK_MAP.find { |k, _| citation.start_with?(k) }&.last
18
+ # next m unless canonical
19
+ # rest = citation[canonical.length..].sub(/\A\.?[ \t]?/, "")
20
+ # url = "https://www.biblegateway.com/passage/?search=#{CGI.escape("#{canonical} #{rest}")}&version=CEB"
21
+ # "[#{citation}](#{url})"
22
+ # end
23
+ #
24
+ # To copy to another project: copy this file and add the gsub block above to
25
+ # your markdown rendering method.
26
+
27
+ module MarkdownServer
28
+ # Maps every recognised abbreviation to its BibleGateway canonical book name.
29
+ # Keys are sorted longest-first when building the regex so longer forms win.
30
+ BIBLE_BOOK_MAP = {
31
+ # ── Old Testament ────────────────────────────────────────────────────────
32
+ "Genesis"=>"Genesis","Gen."=>"Genesis","Gen"=>"Genesis","Gn"=>"Genesis",
33
+ "Exodus"=>"Exodus","Ex."=>"Exodus","Exo"=>"Exodus","Ex"=>"Exodus",
34
+ "Leviticus"=>"Leviticus","Lev."=>"Leviticus","Lev"=>"Leviticus",
35
+ "Numbers"=>"Numbers","Num."=>"Numbers","Nu."=>"Numbers","Nu"=>"Numbers",
36
+ "Deuteronomy"=>"Deuteronomy","Deut."=>"Deuteronomy","Deut"=>"Deuteronomy",
37
+ "Dt."=>"Deuteronomy","Dt"=>"Deuteronomy",
38
+ "Joshua"=>"Joshua","Josh."=>"Joshua",
39
+ "Judges"=>"Judges","Judg."=>"Judges",
40
+ "Ruth"=>"Ruth",
41
+ "1 Samuel"=>"1 Samuel","1Samuel"=>"1 Samuel","1 Sam."=>"1 Samuel","1 Sam"=>"1 Samuel","1Sam."=>"1 Samuel",
42
+ "2 Samuel"=>"2 Samuel","2Samuel"=>"2 Samuel","2 Sam."=>"2 Samuel","2 Sam"=>"2 Samuel","2Sam."=>"2 Samuel","2Sa."=>"2 Samuel",
43
+ "1 Kings"=>"1 Kings","1Kings"=>"1 Kings","1 Ki."=>"1 Kings","1Ki."=>"1 Kings",
44
+ "2 Kings"=>"2 Kings","2Kings"=>"2 Kings","2 Ki."=>"2 Kings","2Ki."=>"2 Kings",
45
+ "1 Chronicles"=>"1 Chronicles","1 Chr."=>"1 Chronicles","1Chr."=>"1 Chronicles","1Ch"=>"1 Chronicles",
46
+ "2 Chronicles"=>"2 Chronicles","2 Chron."=>"2 Chronicles","2Chron."=>"2 Chronicles","2 Chr."=>"2 Chronicles","2Chr."=>"2 Chronicles","2 Ch"=>"2 Chronicles",
47
+ "Ezra"=>"Ezra","Nehemiah"=>"Nehemiah","Neh."=>"Nehemiah",
48
+ "Esther"=>"Esther","Esth."=>"Esther",
49
+ "Job"=>"Job",
50
+ "Psalms"=>"Psalms","Psalm"=>"Psalms","Psa."=>"Psalms","Psa"=>"Psalms","Ps."=>"Psalms","Ps"=>"Psalms",
51
+ "Proverbs"=>"Proverbs","Prov."=>"Proverbs","Prov"=>"Proverbs","Pr"=>"Proverbs",
52
+ "Ecclesiastes"=>"Ecclesiastes","Eccl."=>"Ecclesiastes","Ecc."=>"Ecclesiastes",
53
+ "Song of Songs"=>"Song of Songs","Song"=>"Song of Songs",
54
+ "Isaiah"=>"Isaiah","Isa."=>"Isaiah","Isa"=>"Isaiah","Is"=>"Isaiah",
55
+ "Jeremiah"=>"Jeremiah","Jer."=>"Jeremiah","Je"=>"Jeremiah",
56
+ "Lamentations"=>"Lamentations","Lam."=>"Lamentations","Lam"=>"Lamentations","La"=>"Lamentations",
57
+ "Ezekiel"=>"Ezekiel","Ezek."=>"Ezekiel","Ezek"=>"Ezekiel",
58
+ "Daniel"=>"Daniel","Dan."=>"Daniel","Dan"=>"Daniel",
59
+ "Hosea"=>"Hosea","Hos."=>"Hosea","Hos"=>"Hosea",
60
+ "Joel"=>"Joel","Amos"=>"Amos",
61
+ "Obadiah"=>"Obadiah","Jonah"=>"Jonah","Jon."=>"Jonah",
62
+ "Micah"=>"Micah","Mic."=>"Micah",
63
+ "Nahum"=>"Nahum","Nah."=>"Nahum",
64
+ "Haggai"=>"Haggai",
65
+ "Zechariah"=>"Zechariah","Zech."=>"Zechariah","Zec"=>"Zechariah","Zc"=>"Zechariah",
66
+ "Zephaniah"=>"Zephaniah","Zep."=>"Zephaniah",
67
+ "Malachi"=>"Malachi","Mal."=>"Malachi",
68
+ # ── New Testament ────────────────────────────────────────────────────────
69
+ "Matthew"=>"Matthew","Matt."=>"Matthew","Mt."=>"Matthew","Mt"=>"Matthew",
70
+ "Mark"=>"Mark","Mk."=>"Mark","Mk"=>"Mark",
71
+ "Luke"=>"Luke","Lk."=>"Luke","Lk"=>"Luke",
72
+ "John"=>"John","Jn."=>"John","Jn"=>"John","Jo"=>"John",
73
+ "Acts"=>"Acts","Ac."=>"Acts",
74
+ "Romans"=>"Romans","Rom."=>"Romans","Ro"=>"Romans",
75
+ "1 Corinthians"=>"1 Corinthians","1Corinthians"=>"1 Corinthians","1 Cor."=>"1 Corinthians","1Cor."=>"1 Corinthians","1 Cor"=>"1 Corinthians","1Cor"=>"1 Corinthians","1Co."=>"1 Corinthians","1Co"=>"1 Corinthians",
76
+ "2 Corinthians"=>"2 Corinthians","2Corinthians"=>"2 Corinthians","2 Cor."=>"2 Corinthians","2Cor."=>"2 Corinthians","2 Cor"=>"2 Corinthians","2Co."=>"2 Corinthians","2Co"=>"2 Corinthians",
77
+ "Galatians"=>"Galatians","Gal."=>"Galatians","Gal"=>"Galatians",
78
+ "Ephesians"=>"Ephesians","Eph."=>"Ephesians","Eph"=>"Ephesians",
79
+ "Philippians"=>"Philippians","Phil."=>"Philippians","Phil"=>"Philippians",
80
+ "Colossians"=>"Colossians","Col."=>"Colossians","Col"=>"Colossians",
81
+ "1 Thessalonians"=>"1 Thessalonians","1 Thess."=>"1 Thessalonians","1Thess."=>"1 Thessalonians","1 Th"=>"1 Thessalonians","1Th"=>"1 Thessalonians",
82
+ "2 Thessalonians"=>"2 Thessalonians","2Thessalonians"=>"2 Thessalonians","2 Thess."=>"2 Thessalonians","2Thess."=>"2 Thessalonians","2 Th"=>"2 Thessalonians","2Th"=>"2 Thessalonians",
83
+ "1 Timothy"=>"1 Timothy","1Timothy"=>"1 Timothy","1 Tim."=>"1 Timothy","1Tim."=>"1 Timothy","1 Ti."=>"1 Timothy","1Ti."=>"1 Timothy",
84
+ "2 Timothy"=>"2 Timothy","2Timothy"=>"2 Timothy","2 Tim."=>"2 Timothy","2Tim."=>"2 Timothy","2 Ti."=>"2 Timothy","2Ti."=>"2 Timothy",
85
+ "Titus"=>"Titus","Tit."=>"Titus","Tit"=>"Titus","Ti."=>"Titus",
86
+ "Philemon"=>"Philemon","Philem."=>"Philemon",
87
+ "Hebrews"=>"Hebrews","Hebrew"=>"Hebrews","Heb."=>"Hebrews","Heb"=>"Hebrews","Hb"=>"Hebrews",
88
+ "James"=>"James","Jas."=>"James","Jas"=>"James",
89
+ "1 Peter"=>"1 Peter","1Peter"=>"1 Peter","1 Pet."=>"1 Peter","1Pet."=>"1 Peter","1 Pt."=>"1 Peter","1Pt."=>"1 Peter",
90
+ "2 Peter"=>"2 Peter","2Peter"=>"2 Peter","2 Pet."=>"2 Peter","2Pet."=>"2 Peter","2 Pt."=>"2 Peter","2Pt."=>"2 Peter","2Pe"=>"2 Peter",
91
+ "1 John"=>"1 John","1John"=>"1 John","1 Jn."=>"1 John","1Jn."=>"1 John","1 Jn"=>"1 John","1Jn"=>"1 John",
92
+ "2 John"=>"2 John","2 Jn."=>"2 John","2Jn."=>"2 John",
93
+ "3 John"=>"3 John","3 Jn."=>"3 John","3Jn."=>"3 John",
94
+ "Jude"=>"Jude",
95
+ "Revelation"=>"Revelation","Rev."=>"Revelation","Rev"=>"Revelation",
96
+ # ── Deuterocanonical (present in CEB) ───────────────────────────────────
97
+ "Wisdom of Solomon"=>"Wisdom of Solomon","Wisdom"=>"Wisdom of Solomon","Wis."=>"Wisdom of Solomon",
98
+ "1 Maccabees"=>"1 Maccabees","1Macc."=>"1 Maccabees",
99
+ }.freeze
100
+
101
+ # Sorted longest-first for runtime lookup (same order as the regex alternation).
102
+ BIBLE_BOOK_MAP_SORTED = BIBLE_BOOK_MAP.sort_by { |k, _| -k.length }.freeze
103
+
104
+ _book_alts = BIBLE_BOOK_MAP.keys.sort_by(&:length).reverse
105
+ .map { |k| Regexp.escape(k) }.join("|")
106
+ _verse = /\d+[abc]?(?:[–—\-]\d+[abc]?)?(?:,\s*\d+[abc]?(?:[–—\-]\d+[abc]?)?)*(?:ff\.?|f\.)?/
107
+
108
+ # Combined regex. Four alternatives — only the last (group 4) is replaced:
109
+ # 1. inline code span → return verbatim
110
+ # 2. fenced code block → return verbatim
111
+ # 3. existing md link → return verbatim
112
+ # 4. bare citation → replace with BibleGateway markdown link
113
+ BIBLE_CITATION_RE = Regexp.new(
114
+ "(`[^`]*?`)" \
115
+ "|(`{3}[\\s\\S]*?`{3})" \
116
+ "|(?<!\\!)(\\[[^\\[\\]]*\\]\\([^)]+\\))" \
117
+ "|((?:#{_book_alts})\\.?[ \\t]?\\d+:#{_verse.source})",
118
+ Regexp::MULTILINE
119
+ )
120
+ end
@@ -1,3 +1,3 @@
1
1
  module MarkdownServer
2
- VERSION = "0.5.12"
2
+ VERSION = "0.5.13"
3
3
  end
data/views/layout.erb CHANGED
@@ -315,6 +315,15 @@
315
315
  }
316
316
  .md-content tr:nth-child(even) { background: #fdfcf9; }
317
317
 
318
+ /* Bible Gateway citation links */
319
+ a[href*="biblegateway.com"] {
320
+ color: #7b3fa0;
321
+ font-weight: bold;
322
+ text-decoration: none;
323
+ border-bottom: 1px solid #b07fd4;
324
+ }
325
+ a[href*="biblegateway.com"]:hover { color: #5a2c78; border-bottom-color: #5a2c78; }
326
+
318
327
  /* Wiki links */
319
328
  a.wiki-link {
320
329
  color: #6a8e3e;
@@ -754,6 +763,9 @@
754
763
  box-shadow: 0 4px 20px rgba(0,0,0,0.22);
755
764
  -webkit-overflow-scrolling: touch;
756
765
  cursor: auto;
766
+ resize: both;
767
+ min-width: 280px;
768
+ min-height: 80px;
757
769
  }
758
770
  .link-ctx-popup-header {
759
771
  display: flex;
@@ -766,6 +778,11 @@
766
778
  top: 0;
767
779
  background: #faf8f4;
768
780
  z-index: 1;
781
+ cursor: grab;
782
+ }
783
+ .link-ctx-popup-header.is-dragging {
784
+ cursor: grabbing;
785
+ user-select: none;
769
786
  }
770
787
  .link-ctx-popup-title-wrap {
771
788
  flex: 1;
@@ -829,18 +846,12 @@
829
846
  .link-ctx-popup-pin:hover { color: #2c2c2c; }
830
847
  .link-ctx-popup-pin.pinned { color: #8b6914; }
831
848
  .link-ctx-popup--pinned {
832
- resize: both;
833
849
  max-height: none;
834
- min-width: 280px;
835
850
  min-height: 120px;
836
851
  }
837
852
  .link-ctx-popup--pinned .link-ctx-popup-header {
838
- cursor: grab;
839
853
  user-select: none;
840
854
  }
841
- .link-ctx-popup--pinned .link-ctx-popup-header.is-dragging {
842
- cursor: grabbing;
843
- }
844
855
  .link-ctx-popup-body {
845
856
  padding: 0.75rem 1rem;
846
857
  }
@@ -1608,6 +1619,7 @@
1608
1619
  var touchMoved = false;
1609
1620
  var historyStack = [];
1610
1621
  var pinnedPopups = [];
1622
+ var popupDragging = false;
1611
1623
  var currentPopupPos = { x: 0, y: 0 };
1612
1624
  var mouseLeaveTimer = null;
1613
1625
 
@@ -1665,7 +1677,7 @@
1665
1677
 
1666
1678
  clearTimeout(mouseLeaveTimer);
1667
1679
  popup.addEventListener('mouseleave', function() {
1668
- if (thisPopup.dataset.pinned) return;
1680
+ if (thisPopup.dataset.pinned || popupDragging) return;
1669
1681
  mouseLeaveTimer = setTimeout(hidePopup, 150);
1670
1682
  });
1671
1683
  popup.addEventListener('mouseenter', function() {
@@ -1674,6 +1686,14 @@
1674
1686
  });
1675
1687
 
1676
1688
  repositionPopup();
1689
+ makeDraggable(popup);
1690
+ // Lock explicit dimensions on first interaction so resize can grow past max-height
1691
+ thisPopup.addEventListener('mousedown', function lockSize() {
1692
+ thisPopup.style.width = thisPopup.offsetWidth + 'px';
1693
+ thisPopup.style.height = thisPopup.offsetHeight + 'px';
1694
+ thisPopup.style.maxHeight = 'none';
1695
+ thisPopup.removeEventListener('mousedown', lockSize);
1696
+ }, true);
1677
1697
  runMermaidIn(popup);
1678
1698
 
1679
1699
  var backBtnEl = popup.querySelector('.link-ctx-popup-back');
@@ -1691,6 +1711,7 @@
1691
1711
  popup.querySelector('.link-ctx-popup-pin').addEventListener('click', function(e) {
1692
1712
  e.stopPropagation();
1693
1713
  if (!thisPopup.dataset.pinned) pinPopup(thisPopup);
1714
+ else unpinPopup(thisPopup);
1694
1715
  });
1695
1716
  popup.addEventListener('click', function(e) { e.stopPropagation(); });
1696
1717
 
@@ -1750,6 +1771,7 @@
1750
1771
  historyStack = [];
1751
1772
  el.style.width = el.offsetWidth + 'px';
1752
1773
  el.style.height = el.offsetHeight + 'px';
1774
+ el.style.maxHeight = 'none';
1753
1775
  el.dataset.pinned = '1';
1754
1776
  el.classList.add('link-ctx-popup--pinned');
1755
1777
  var pinBtn = el.querySelector('.link-ctx-popup-pin');
@@ -1773,10 +1795,44 @@
1773
1795
  if (el.parentNode) el.parentNode.removeChild(el);
1774
1796
  pinnedPopups = pinnedPopups.filter(function(p) { return p !== el; });
1775
1797
  });
1776
- makeDraggable(el);
1798
+ el._popupHistory = ownHistory;
1777
1799
  pinnedPopups.push(el);
1778
1800
  }
1779
1801
 
1802
+ function unpinPopup(el) {
1803
+ // Close any existing floating popup
1804
+ if (popup && popup !== el && popup.parentNode) popup.parentNode.removeChild(popup);
1805
+ pinnedPopups = pinnedPopups.filter(function(p) { return p !== el; });
1806
+ // Restore as active floating popup
1807
+ popup = el;
1808
+ historyStack = el._popupHistory || [];
1809
+ delete el._popupHistory;
1810
+ delete el.dataset.pinned;
1811
+ el.classList.remove('link-ctx-popup--pinned');
1812
+ // Update pin button
1813
+ var pinBtn = el.querySelector('.link-ctx-popup-pin');
1814
+ if (pinBtn) pinBtn.classList.remove('pinned');
1815
+ // Re-wire back button to use restored global historyStack
1816
+ var backBtn = el.querySelector('.link-ctx-popup-back');
1817
+ if (backBtn) {
1818
+ var newBack = backBtn.cloneNode(true);
1819
+ backBtn.parentNode.replaceChild(newBack, backBtn);
1820
+ newBack.addEventListener('click', function(e) {
1821
+ e.stopPropagation();
1822
+ var prev = historyStack.pop();
1823
+ if (prev) {
1824
+ showPopup(prev.x, prev.y, prev.title, prev.bodyHtml, prev.href, prev.linkRect);
1825
+ if (popup && prev.scrollTop) popup.scrollTop = prev.scrollTop;
1826
+ }
1827
+ });
1828
+ }
1829
+ // Re-wire close button
1830
+ var closeBtn = el.querySelector('.link-ctx-popup-close');
1831
+ var newClose = closeBtn.cloneNode(true);
1832
+ closeBtn.parentNode.replaceChild(newClose, closeBtn);
1833
+ newClose.addEventListener('click', hidePopup);
1834
+ }
1835
+
1780
1836
  function goBackInPinned(el, ownHistory) {
1781
1837
  var prev = ownHistory.pop();
1782
1838
  if (!prev) return;
@@ -1802,6 +1858,7 @@
1802
1858
  startY = e.clientY;
1803
1859
  startLeft = el.offsetLeft;
1804
1860
  startTop = el.offsetTop;
1861
+ popupDragging = true;
1805
1862
  header.classList.add('is-dragging');
1806
1863
  document.addEventListener('mousemove', onMove);
1807
1864
  document.addEventListener('mouseup', onUp);
@@ -1811,6 +1868,7 @@
1811
1868
  el.style.top = (startTop + e.clientY - startY) + 'px';
1812
1869
  }
1813
1870
  function onUp() {
1871
+ popupDragging = false;
1814
1872
  header.classList.remove('is-dragging');
1815
1873
  document.removeEventListener('mousemove', onMove);
1816
1874
  document.removeEventListener('mouseup', onUp);
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: markdownr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.12
4
+ version: 0.5.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Dunn
@@ -105,6 +105,7 @@ files:
105
105
  - bin/markdownr
106
106
  - lib/markdown_server.rb
107
107
  - lib/markdown_server/app.rb
108
+ - lib/markdown_server/bible_citations.rb
108
109
  - lib/markdown_server/version.rb
109
110
  - views/admin_login.erb
110
111
  - views/directory.erb