markdownr 0.5.11 → 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 +4 -4
- data/lib/markdown_server/app.rb +22 -0
- data/lib/markdown_server/bible_citations.rb +120 -0
- data/lib/markdown_server/version.rb +1 -1
- data/views/layout.erb +209 -6
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8825ac583ace67b9cd50c7aef4d09a9de175bd6f3c81d4fcd2d8d5159476b76f
|
|
4
|
+
data.tar.gz: 55301d8c94bea36eef2b30571a4971e4d2661689ed4faab4d85a2632dda61af3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4fb68ab533ae36e9509d85c29bddb4c23fc36747a4eadfd1e1b78fe73d1af2d872f14089557db3dbe43ed1fec08df44b60cabd07471664f3792e20ff60857c04
|
|
7
|
+
data.tar.gz: abe3b62dc09675cc99d670915cae25690270ce83a9274cf7954020c8b883b94c22ed9d984e12cf27858fff227346196137cae291b0d944cce3290a7e8672364b
|
data/lib/markdown_server/app.rb
CHANGED
|
@@ -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
|
|
@@ -125,6 +128,25 @@ module MarkdownServer
|
|
|
125
128
|
end
|
|
126
129
|
|
|
127
130
|
def render_markdown(text)
|
|
131
|
+
# Convert mermaid code fences to raw HTML divs before Kramdown so Rouge
|
|
132
|
+
# never touches them and the content is preserved exactly for Mermaid.js.
|
|
133
|
+
text = text.gsub(/^```mermaid[ \t]*\r?\n([\s\S]*?)^```[ \t]*(\r?\n|\z)/m) do
|
|
134
|
+
"<div class=\"mermaid\">\n#{h($1.rstrip)}\n</div>\n\n"
|
|
135
|
+
end
|
|
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
|
+
|
|
128
150
|
# Process wiki links BEFORE Kramdown so that | isn't consumed as
|
|
129
151
|
# a GFM table delimiter.
|
|
130
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
|
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;
|
|
@@ -815,6 +832,26 @@
|
|
|
815
832
|
flex-shrink: 0;
|
|
816
833
|
}
|
|
817
834
|
.link-ctx-popup-back:hover { color: #2c2c2c; }
|
|
835
|
+
.link-ctx-popup-pin {
|
|
836
|
+
background: none;
|
|
837
|
+
border: none;
|
|
838
|
+
color: #bbb;
|
|
839
|
+
cursor: pointer;
|
|
840
|
+
padding: 0 0.2rem;
|
|
841
|
+
flex-shrink: 0;
|
|
842
|
+
display: flex;
|
|
843
|
+
align-items: center;
|
|
844
|
+
line-height: 1;
|
|
845
|
+
}
|
|
846
|
+
.link-ctx-popup-pin:hover { color: #2c2c2c; }
|
|
847
|
+
.link-ctx-popup-pin.pinned { color: #8b6914; }
|
|
848
|
+
.link-ctx-popup--pinned {
|
|
849
|
+
max-height: none;
|
|
850
|
+
min-height: 120px;
|
|
851
|
+
}
|
|
852
|
+
.link-ctx-popup--pinned .link-ctx-popup-header {
|
|
853
|
+
user-select: none;
|
|
854
|
+
}
|
|
818
855
|
.link-ctx-popup-body {
|
|
819
856
|
padding: 0.75rem 1rem;
|
|
820
857
|
}
|
|
@@ -829,6 +866,10 @@
|
|
|
829
866
|
margin-bottom: 0.5rem;
|
|
830
867
|
}
|
|
831
868
|
|
|
869
|
+
/* Mermaid diagrams */
|
|
870
|
+
.mermaid { text-align: center; margin: 1.2rem 0; overflow-x: auto; }
|
|
871
|
+
.mermaid svg { max-width: 100%; height: auto; }
|
|
872
|
+
|
|
832
873
|
/* Link preview popup (hover) */
|
|
833
874
|
.link-tooltip-anchor { position: relative; }
|
|
834
875
|
.link-preview-popup {
|
|
@@ -1550,12 +1591,35 @@
|
|
|
1550
1591
|
}, { passive: true });
|
|
1551
1592
|
})();
|
|
1552
1593
|
|
|
1594
|
+
// Mermaid diagram rendering — lazily loads Mermaid.js on first use
|
|
1595
|
+
var mermaidReady = false;
|
|
1596
|
+
var mermaidQueue = [];
|
|
1597
|
+
function runMermaidIn(container) {
|
|
1598
|
+
var nodes = Array.prototype.slice.call(container.querySelectorAll('.mermaid:not([data-processed="true"])'));
|
|
1599
|
+
if (!nodes.length) return;
|
|
1600
|
+
var doRender = function() { mermaid.run({ nodes: nodes }); };
|
|
1601
|
+
if (mermaidReady) { doRender(); return; }
|
|
1602
|
+
mermaidQueue.push(doRender);
|
|
1603
|
+
if (mermaidQueue.length > 1) return;
|
|
1604
|
+
var s = document.createElement('script');
|
|
1605
|
+
s.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
|
|
1606
|
+
s.onload = function() {
|
|
1607
|
+
mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
|
|
1608
|
+
mermaidReady = true;
|
|
1609
|
+
mermaidQueue.splice(0).forEach(function(fn) { fn(); });
|
|
1610
|
+
};
|
|
1611
|
+
document.head.appendChild(s);
|
|
1612
|
+
}
|
|
1613
|
+
runMermaidIn(document.body);
|
|
1614
|
+
|
|
1553
1615
|
// Left-click / tap popup for all links
|
|
1554
1616
|
(function() {
|
|
1555
1617
|
var cache = Object.create(null);
|
|
1556
1618
|
var popup = null;
|
|
1557
1619
|
var touchMoved = false;
|
|
1558
1620
|
var historyStack = [];
|
|
1621
|
+
var pinnedPopups = [];
|
|
1622
|
+
var popupDragging = false;
|
|
1559
1623
|
var currentPopupPos = { x: 0, y: 0 };
|
|
1560
1624
|
var mouseLeaveTimer = null;
|
|
1561
1625
|
|
|
@@ -1584,6 +1648,7 @@
|
|
|
1584
1648
|
}
|
|
1585
1649
|
|
|
1586
1650
|
var extLinkIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;flex-shrink:0"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>';
|
|
1651
|
+
var pinIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z"/></svg>';
|
|
1587
1652
|
|
|
1588
1653
|
function showPopup(x, y, title, bodyHtml, href, linkRect) {
|
|
1589
1654
|
// Remove old popup without clearing historyStack
|
|
@@ -1593,6 +1658,7 @@
|
|
|
1593
1658
|
|
|
1594
1659
|
popup = document.createElement('div');
|
|
1595
1660
|
popup.className = 'link-ctx-popup';
|
|
1661
|
+
var thisPopup = popup;
|
|
1596
1662
|
var backBtn = historyStack.length > 0 ? '<button class="link-ctx-popup-back" aria-label="Back">\u2190</button>' : '';
|
|
1597
1663
|
var titleHtml = href
|
|
1598
1664
|
? '<div class="link-ctx-popup-title-wrap">' +
|
|
@@ -1603,6 +1669,7 @@
|
|
|
1603
1669
|
popup.innerHTML =
|
|
1604
1670
|
'<div class="link-ctx-popup-header">' +
|
|
1605
1671
|
backBtn + titleHtml +
|
|
1672
|
+
'<button class="link-ctx-popup-pin" aria-label="Pin" title="Pin popup">' + pinIcon + '</button>' +
|
|
1606
1673
|
'<button class="link-ctx-popup-close" aria-label="Close">\u00d7</button>' +
|
|
1607
1674
|
'</div>' +
|
|
1608
1675
|
'<div class="link-ctx-popup-body">' + bodyHtml + '</div>';
|
|
@@ -1610,27 +1677,47 @@
|
|
|
1610
1677
|
|
|
1611
1678
|
clearTimeout(mouseLeaveTimer);
|
|
1612
1679
|
popup.addEventListener('mouseleave', function() {
|
|
1680
|
+
if (thisPopup.dataset.pinned || popupDragging) return;
|
|
1613
1681
|
mouseLeaveTimer = setTimeout(hidePopup, 150);
|
|
1614
1682
|
});
|
|
1615
1683
|
popup.addEventListener('mouseenter', function() {
|
|
1684
|
+
if (thisPopup.dataset.pinned) return;
|
|
1616
1685
|
clearTimeout(mouseLeaveTimer);
|
|
1617
1686
|
});
|
|
1618
1687
|
|
|
1619
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);
|
|
1697
|
+
runMermaidIn(popup);
|
|
1620
1698
|
|
|
1621
1699
|
var backBtnEl = popup.querySelector('.link-ctx-popup-back');
|
|
1622
1700
|
if (backBtnEl) {
|
|
1623
1701
|
backBtnEl.addEventListener('click', function(e) {
|
|
1624
1702
|
e.stopPropagation();
|
|
1625
1703
|
var prev = historyStack.pop();
|
|
1626
|
-
if (prev)
|
|
1704
|
+
if (prev) {
|
|
1705
|
+
showPopup(prev.x, prev.y, prev.title, prev.bodyHtml, prev.href, prev.linkRect);
|
|
1706
|
+
if (popup && prev.scrollTop) popup.scrollTop = prev.scrollTop;
|
|
1707
|
+
}
|
|
1627
1708
|
});
|
|
1628
1709
|
}
|
|
1629
1710
|
popup.querySelector('.link-ctx-popup-close').addEventListener('click', hidePopup);
|
|
1711
|
+
popup.querySelector('.link-ctx-popup-pin').addEventListener('click', function(e) {
|
|
1712
|
+
e.stopPropagation();
|
|
1713
|
+
if (!thisPopup.dataset.pinned) pinPopup(thisPopup);
|
|
1714
|
+
else unpinPopup(thisPopup);
|
|
1715
|
+
});
|
|
1630
1716
|
popup.addEventListener('click', function(e) { e.stopPropagation(); });
|
|
1631
1717
|
|
|
1632
1718
|
// Links in the popup body push current state to history, then open a new popup
|
|
1633
|
-
// at the same screen position as the current popup
|
|
1719
|
+
// at the same screen position as the current popup.
|
|
1720
|
+
// When the popup is pinned, links open a new floating popup at the click position instead.
|
|
1634
1721
|
var body = popup.querySelector('.link-ctx-popup-body');
|
|
1635
1722
|
body.addEventListener('click', function(e) {
|
|
1636
1723
|
var anchor = findLink(e.target);
|
|
@@ -1639,13 +1726,15 @@
|
|
|
1639
1726
|
if (!linkHref || isAnchorOnly(linkHref)) return;
|
|
1640
1727
|
e.stopPropagation();
|
|
1641
1728
|
e.preventDefault();
|
|
1729
|
+
if (thisPopup.dataset.pinned) { handleLink(anchor, e.clientX, e.clientY, false); return; }
|
|
1642
1730
|
var savedPos = { x: currentPopupPos.x, y: currentPopupPos.y, linkRect: currentPopupPos.linkRect };
|
|
1643
|
-
var titleEl =
|
|
1731
|
+
var titleEl = thisPopup.querySelector('.link-ctx-popup-title');
|
|
1644
1732
|
historyStack.push({
|
|
1645
1733
|
x: savedPos.x, y: savedPos.y, linkRect: savedPos.linkRect,
|
|
1646
1734
|
title: titleEl ? titleEl.querySelector('span').textContent : '',
|
|
1647
1735
|
bodyHtml: body.innerHTML,
|
|
1648
|
-
href: titleEl ? (titleEl.getAttribute('href') || '') : ''
|
|
1736
|
+
href: titleEl ? (titleEl.getAttribute('href') || '') : '',
|
|
1737
|
+
scrollTop: thisPopup.scrollTop
|
|
1649
1738
|
});
|
|
1650
1739
|
handleLink(anchor, savedPos.x, savedPos.y, true);
|
|
1651
1740
|
});
|
|
@@ -1657,13 +1746,15 @@
|
|
|
1657
1746
|
if (!linkHref || isAnchorOnly(linkHref)) return;
|
|
1658
1747
|
e.stopPropagation();
|
|
1659
1748
|
e.preventDefault();
|
|
1749
|
+
if (thisPopup.dataset.pinned) { handleLink(anchor, e.clientX, e.clientY, false); return; }
|
|
1660
1750
|
var savedPos = { x: currentPopupPos.x, y: currentPopupPos.y, linkRect: currentPopupPos.linkRect };
|
|
1661
|
-
var titleEl =
|
|
1751
|
+
var titleEl = thisPopup.querySelector('.link-ctx-popup-title');
|
|
1662
1752
|
historyStack.push({
|
|
1663
1753
|
x: savedPos.x, y: savedPos.y, linkRect: savedPos.linkRect,
|
|
1664
1754
|
title: titleEl ? titleEl.querySelector('span').textContent : '',
|
|
1665
1755
|
bodyHtml: body.innerHTML,
|
|
1666
|
-
href: titleEl ? (titleEl.getAttribute('href') || '') : ''
|
|
1756
|
+
href: titleEl ? (titleEl.getAttribute('href') || '') : '',
|
|
1757
|
+
scrollTop: thisPopup.scrollTop
|
|
1667
1758
|
});
|
|
1668
1759
|
handleLink(anchor, savedPos.x, savedPos.y, true);
|
|
1669
1760
|
}, { passive: false });
|
|
@@ -1673,6 +1764,117 @@
|
|
|
1673
1764
|
});
|
|
1674
1765
|
}
|
|
1675
1766
|
|
|
1767
|
+
function pinPopup(el) {
|
|
1768
|
+
clearTimeout(mouseLeaveTimer);
|
|
1769
|
+
var ownHistory = historyStack.slice();
|
|
1770
|
+
popup = null;
|
|
1771
|
+
historyStack = [];
|
|
1772
|
+
el.style.width = el.offsetWidth + 'px';
|
|
1773
|
+
el.style.height = el.offsetHeight + 'px';
|
|
1774
|
+
el.style.maxHeight = 'none';
|
|
1775
|
+
el.dataset.pinned = '1';
|
|
1776
|
+
el.classList.add('link-ctx-popup--pinned');
|
|
1777
|
+
var pinBtn = el.querySelector('.link-ctx-popup-pin');
|
|
1778
|
+
if (pinBtn) pinBtn.classList.add('pinned');
|
|
1779
|
+
// Re-wire back button to operate on this element's own history
|
|
1780
|
+
var backBtn = el.querySelector('.link-ctx-popup-back');
|
|
1781
|
+
if (backBtn) {
|
|
1782
|
+
var newBack = backBtn.cloneNode(true);
|
|
1783
|
+
backBtn.parentNode.replaceChild(newBack, backBtn);
|
|
1784
|
+
newBack.addEventListener('click', function(e) {
|
|
1785
|
+
e.stopPropagation();
|
|
1786
|
+
goBackInPinned(el, ownHistory);
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
// Re-wire close button to remove this element
|
|
1790
|
+
var closeBtn = el.querySelector('.link-ctx-popup-close');
|
|
1791
|
+
var newClose = closeBtn.cloneNode(true);
|
|
1792
|
+
closeBtn.parentNode.replaceChild(newClose, closeBtn);
|
|
1793
|
+
newClose.addEventListener('click', function(e) {
|
|
1794
|
+
e.stopPropagation();
|
|
1795
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
|
1796
|
+
pinnedPopups = pinnedPopups.filter(function(p) { return p !== el; });
|
|
1797
|
+
});
|
|
1798
|
+
el._popupHistory = ownHistory;
|
|
1799
|
+
pinnedPopups.push(el);
|
|
1800
|
+
}
|
|
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
|
+
|
|
1836
|
+
function goBackInPinned(el, ownHistory) {
|
|
1837
|
+
var prev = ownHistory.pop();
|
|
1838
|
+
if (!prev) return;
|
|
1839
|
+
var body = el.querySelector('.link-ctx-popup-body');
|
|
1840
|
+
if (body) body.innerHTML = prev.bodyHtml;
|
|
1841
|
+
var titleSpan = el.querySelector('.link-ctx-popup-title span');
|
|
1842
|
+
if (titleSpan) titleSpan.textContent = prev.title;
|
|
1843
|
+
var titleLink = el.querySelector('a.link-ctx-popup-title');
|
|
1844
|
+
if (titleLink && prev.href) titleLink.href = prev.href;
|
|
1845
|
+
var backBtnEl = el.querySelector('.link-ctx-popup-back');
|
|
1846
|
+
if (backBtnEl) backBtnEl.style.display = ownHistory.length > 0 ? '' : 'none';
|
|
1847
|
+
if (prev.scrollTop) el.scrollTop = prev.scrollTop;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
function makeDraggable(el) {
|
|
1851
|
+
var header = el.querySelector('.link-ctx-popup-header');
|
|
1852
|
+
if (!header) return;
|
|
1853
|
+
var startX, startY, startLeft, startTop;
|
|
1854
|
+
header.addEventListener('mousedown', function(e) {
|
|
1855
|
+
if (e.target.closest('button') || e.target.closest('a')) return;
|
|
1856
|
+
e.preventDefault();
|
|
1857
|
+
startX = e.clientX;
|
|
1858
|
+
startY = e.clientY;
|
|
1859
|
+
startLeft = el.offsetLeft;
|
|
1860
|
+
startTop = el.offsetTop;
|
|
1861
|
+
popupDragging = true;
|
|
1862
|
+
header.classList.add('is-dragging');
|
|
1863
|
+
document.addEventListener('mousemove', onMove);
|
|
1864
|
+
document.addEventListener('mouseup', onUp);
|
|
1865
|
+
});
|
|
1866
|
+
function onMove(e) {
|
|
1867
|
+
el.style.left = (startLeft + e.clientX - startX) + 'px';
|
|
1868
|
+
el.style.top = (startTop + e.clientY - startY) + 'px';
|
|
1869
|
+
}
|
|
1870
|
+
function onUp() {
|
|
1871
|
+
popupDragging = false;
|
|
1872
|
+
header.classList.remove('is-dragging');
|
|
1873
|
+
document.removeEventListener('mousemove', onMove);
|
|
1874
|
+
document.removeEventListener('mouseup', onUp);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1676
1878
|
function repositionPopup() {
|
|
1677
1879
|
if (!popup) return;
|
|
1678
1880
|
var x = currentPopupPos.x, y = currentPopupPos.y;
|
|
@@ -1716,6 +1918,7 @@
|
|
|
1716
1918
|
if (body) body.innerHTML = bodyHtml;
|
|
1717
1919
|
if (title && titleTextEl) titleTextEl.textContent = title;
|
|
1718
1920
|
repositionPopup();
|
|
1921
|
+
runMermaidIn(popup);
|
|
1719
1922
|
}
|
|
1720
1923
|
|
|
1721
1924
|
function hidePopup() {
|
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.
|
|
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
|