markdownr 0.5.17 → 0.5.19
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/bin/markdownr +5 -0
- data/lib/markdown_server/app.rb +45 -2
- data/lib/markdown_server/bible_citations.rb +34 -1
- data/lib/markdown_server/version.rb +1 -1
- data/views/layout.erb +8 -15
- data/views/popup_assets.erb +732 -0
- 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: a3cd57887599183f3350f1dd63f44f636ab03b9d3c3a462a1aaec04793cbbfeb
|
|
4
|
+
data.tar.gz: 8ecb0febc8ba672c28ad2cc9d2cdea0cbe7a1e1c79508bb0ec1b4fcb40c133fa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 83b163d0b783c29163dfc8d7803e0536dd78a295d7b4d72518dd55015b7e302fae23d83bec507fb1f363eee3af2f5e031f5ca8fd9a42de10c42ff72bce996446
|
|
7
|
+
data.tar.gz: b6a498d1d81abdd6ad981c4c930ba934776895c2606bc0ae9ae42bcfb71890d79be8bcdeec55e190c36199c34ed6b74ce2926b1705161ba862f68752b2d9a838
|
data/bin/markdownr
CHANGED
|
@@ -43,6 +43,10 @@ OptionParser.new do |opts|
|
|
|
43
43
|
options[:hard_wrap] = false
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
opts.on("--verbose", "Print each request (time, IP, method, path) to stdout") do
|
|
47
|
+
options[:verbose] = true
|
|
48
|
+
end
|
|
49
|
+
|
|
46
50
|
opts.on("-v", "--version", "Show version") do
|
|
47
51
|
puts "markdownr #{MarkdownServer::VERSION}"
|
|
48
52
|
exit
|
|
@@ -64,6 +68,7 @@ MarkdownServer::App.set :allow_robots, options[:allow_robots] || false
|
|
|
64
68
|
MarkdownServer::App.set :index_file, options[:index_file]
|
|
65
69
|
MarkdownServer::App.set :link_tooltips, options.fetch(:link_tooltips, true)
|
|
66
70
|
MarkdownServer::App.set :hard_wrap, options.fetch(:hard_wrap, true)
|
|
71
|
+
MarkdownServer::App.set :verbose, options[:verbose] || false
|
|
67
72
|
MarkdownServer::App.set :port, options[:port]
|
|
68
73
|
MarkdownServer::App.set :bind, options[:bind]
|
|
69
74
|
MarkdownServer::App.set :server_settings, { max_threads: options[:threads], min_threads: 1 }
|
data/lib/markdown_server/app.rb
CHANGED
|
@@ -31,8 +31,10 @@ module MarkdownServer
|
|
|
31
31
|
set :protection, false
|
|
32
32
|
set :host_authorization, { permitted_hosts: [] }
|
|
33
33
|
set :behind_proxy, false
|
|
34
|
+
set :verbose, false
|
|
34
35
|
set :session_secret, ENV.fetch("MARKDOWNR_SESSION_SECRET", SecureRandom.hex(64))
|
|
35
36
|
set :sessions, key: "markdownr_session", same_site: :strict, httponly: true
|
|
37
|
+
set :popup_assets_content, File.read(File.join(settings.views, "popup_assets.erb"))
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
helpers do
|
|
@@ -137,8 +139,7 @@ module MarkdownServer
|
|
|
137
139
|
# Auto-link bare Bible verse citations (e.g. "John 3:16" → BibleGateway link).
|
|
138
140
|
# Skips inline code, fenced code blocks, and citations already inside a link.
|
|
139
141
|
text = MarkdownServer.link_citations(text) do |canonical, verse, citation|
|
|
140
|
-
|
|
141
|
-
"[#{citation}](#{url})"
|
|
142
|
+
"[#{citation}](#{MarkdownServer.biblegateway_url(canonical, verse)})"
|
|
142
143
|
end
|
|
143
144
|
|
|
144
145
|
# Process wiki links BEFORE Kramdown so that | isn't consumed as
|
|
@@ -785,6 +786,27 @@ module MarkdownServer
|
|
|
785
786
|
end
|
|
786
787
|
end
|
|
787
788
|
|
|
789
|
+
# Returns true when serving an HTML file that should have popup assets injected.
|
|
790
|
+
# Add additional path prefixes here as needed.
|
|
791
|
+
def inject_assets_for_html_path?(relative_path)
|
|
792
|
+
relative_path.start_with?("scripture/resources/books/bible")
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# Injects popup CSS and JS into an HTML document before </body>.
|
|
796
|
+
# Also auto-links bare Bible verse citations in HTML text nodes.
|
|
797
|
+
# Falls back to appending before </html>, then to end of document.
|
|
798
|
+
def inject_markdownr_assets(html_content)
|
|
799
|
+
html_content = MarkdownServer.link_citations_html(html_content) do |canonical, verse, citation|
|
|
800
|
+
url = MarkdownServer.biblegateway_url(canonical, verse)
|
|
801
|
+
%(<a href="#{h(url)}">#{h(citation)}</a>)
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
assets = settings.popup_assets_content
|
|
805
|
+
inserted = false
|
|
806
|
+
result = html_content.sub(/<\/(body|html)>/i) { inserted = true; "#{assets}</#{$1}>" }
|
|
807
|
+
inserted ? result : html_content + assets
|
|
808
|
+
end
|
|
809
|
+
|
|
788
810
|
def admin?
|
|
789
811
|
return true if session[:admin]
|
|
790
812
|
|
|
@@ -817,6 +839,13 @@ module MarkdownServer
|
|
|
817
839
|
end
|
|
818
840
|
end
|
|
819
841
|
|
|
842
|
+
before do
|
|
843
|
+
if settings.verbose
|
|
844
|
+
$stdout.puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} #{client_ip} #{request.request_method} #{request.fullpath}"
|
|
845
|
+
$stdout.flush
|
|
846
|
+
end
|
|
847
|
+
end
|
|
848
|
+
|
|
820
849
|
get "/" do
|
|
821
850
|
redirect "/browse/"
|
|
822
851
|
end
|
|
@@ -1167,6 +1196,20 @@ module MarkdownServer
|
|
|
1167
1196
|
when ".epub"
|
|
1168
1197
|
redirect @download_href
|
|
1169
1198
|
|
|
1199
|
+
when ".html"
|
|
1200
|
+
if inject_assets_for_html_path?(relative_path)
|
|
1201
|
+
content = File.read(real_path, encoding: "utf-8")
|
|
1202
|
+
halt 200, { "Content-Type" => "text/html; charset=utf-8" }, inject_markdownr_assets(content)
|
|
1203
|
+
else
|
|
1204
|
+
send_file real_path, type: "text/html"
|
|
1205
|
+
end
|
|
1206
|
+
|
|
1207
|
+
when ".css"
|
|
1208
|
+
send_file real_path, type: "text/css"
|
|
1209
|
+
|
|
1210
|
+
when ".js"
|
|
1211
|
+
send_file real_path, type: "application/javascript"
|
|
1212
|
+
|
|
1170
1213
|
else
|
|
1171
1214
|
content = File.read(real_path, encoding: "utf-8") rescue nil
|
|
1172
1215
|
if content.nil? || content.encoding == Encoding::BINARY || !content.valid_encoding?
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require "cgi"
|
|
2
3
|
#
|
|
3
4
|
# Bible citation auto-linking for Markdown rendering.
|
|
4
5
|
#
|
|
@@ -102,6 +103,12 @@ module MarkdownServer
|
|
|
102
103
|
# Sorted longest-first for runtime lookup (same order as the regex alternation).
|
|
103
104
|
BIBLE_BOOK_MAP_SORTED = BIBLE_BOOK_MAP.sort_by { |k, _| -k.length }.freeze
|
|
104
105
|
|
|
106
|
+
BIBLEGATEWAY_VERSION = "CEB".freeze
|
|
107
|
+
|
|
108
|
+
def self.biblegateway_url(canonical, verse)
|
|
109
|
+
"https://www.biblegateway.com/passage/?search=#{CGI.escape("#{canonical} #{verse}")}&version=#{BIBLEGATEWAY_VERSION}"
|
|
110
|
+
end
|
|
111
|
+
|
|
105
112
|
# Scans +text+ for bare Bible verse citations and yields each one as
|
|
106
113
|
# (canonical_book, verse, raw_citation). Returns the transformed string
|
|
107
114
|
# with each citation replaced by whatever the block returns. Code spans,
|
|
@@ -125,6 +132,7 @@ module MarkdownServer
|
|
|
125
132
|
_book_alts = BIBLE_BOOK_MAP.keys.sort_by(&:length).reverse
|
|
126
133
|
.map { |k| Regexp.escape(k) }.join("|")
|
|
127
134
|
_verse = /\d+[abc]?(?:[–—\-]\d+[abc]?)?(?:,\s*\d+[abc]?(?:[–—\-]\d+[abc]?)?)*(?:ff\.?|f\.)?/
|
|
135
|
+
_citation = "(?:#{_book_alts})\\.?[ \\t]?\\d+(?::#{_verse.source})?"
|
|
128
136
|
|
|
129
137
|
# Combined regex. Four alternatives — only the last (group 4) is replaced:
|
|
130
138
|
# 1. inline code span → return verbatim
|
|
@@ -135,7 +143,32 @@ module MarkdownServer
|
|
|
135
143
|
"(`[^`]*?`)" \
|
|
136
144
|
"|(`{3}[\\s\\S]*?`{3})" \
|
|
137
145
|
"|(?<!\\!)(\\[[^\\[\\]]*\\]\\([^)]+\\))" \
|
|
138
|
-
"|(
|
|
146
|
+
"|(#{_citation})",
|
|
139
147
|
Regexp::MULTILINE
|
|
140
148
|
)
|
|
149
|
+
|
|
150
|
+
# HTML-aware version of BIBLE_CITATION_RE. Skips <script>, <style>, existing
|
|
151
|
+
# <a> elements, and all other HTML tags. Group 5 captures bare citations.
|
|
152
|
+
HTML_BIBLE_CITATION_RE = Regexp.new(
|
|
153
|
+
"(<script\\b[^>]*>[\\s\\S]*?</script>)" \
|
|
154
|
+
"|(<style\\b[^>]*>[\\s\\S]*?</style>)" \
|
|
155
|
+
"|(<a\\b[^>]*>[\\s\\S]*?</a>)" \
|
|
156
|
+
"|(<[^>]+>)" \
|
|
157
|
+
"|(#{_citation})",
|
|
158
|
+
Regexp::MULTILINE | Regexp::IGNORECASE
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Like link_citations but operates on raw HTML. Skips tag content, script/style
|
|
162
|
+
# blocks, and existing anchor elements; yields bare citations in text nodes.
|
|
163
|
+
def self.link_citations_html(html)
|
|
164
|
+
html.gsub(HTML_BIBLE_CITATION_RE) do |m|
|
|
165
|
+
next m if $1 || $2 || $3 || $4 # skip tags / scripts / styles / anchors
|
|
166
|
+
citation = $5
|
|
167
|
+
entry = BIBLE_BOOK_MAP_SORTED.find { |k, _| citation.start_with?(k) }
|
|
168
|
+
next m unless entry
|
|
169
|
+
abbrev, canonical = entry
|
|
170
|
+
verse = citation[abbrev.length..].sub(/\A\.?[ \t]?/, "")
|
|
171
|
+
yield canonical, verse, citation
|
|
172
|
+
end
|
|
173
|
+
end
|
|
141
174
|
end
|
data/views/layout.erb
CHANGED
|
@@ -777,6 +777,7 @@
|
|
|
777
777
|
background: #faf8f4;
|
|
778
778
|
flex-shrink: 0;
|
|
779
779
|
cursor: grab;
|
|
780
|
+
touch-action: none;
|
|
780
781
|
}
|
|
781
782
|
.link-ctx-popup-header.is-dragging {
|
|
782
783
|
cursor: grabbing;
|
|
@@ -861,8 +862,8 @@
|
|
|
861
862
|
position: absolute;
|
|
862
863
|
bottom: 0;
|
|
863
864
|
right: 0;
|
|
864
|
-
width:
|
|
865
|
-
height:
|
|
865
|
+
width: 36px;
|
|
866
|
+
height: 36px;
|
|
866
867
|
cursor: nwse-resize;
|
|
867
868
|
touch-action: none;
|
|
868
869
|
border-bottom-right-radius: 5px;
|
|
@@ -982,7 +983,9 @@
|
|
|
982
983
|
.blb-usage li { margin-bottom: 0.15rem; }
|
|
983
984
|
.blb-usage p { margin: 0; }
|
|
984
985
|
|
|
986
|
+
|
|
985
987
|
/* Footnote tooltips */
|
|
988
|
+
sup[id^="fnref:"] { position: relative; }
|
|
986
989
|
.footnote-tooltip {
|
|
987
990
|
position: absolute;
|
|
988
991
|
bottom: 100%;
|
|
@@ -999,8 +1002,8 @@
|
|
|
999
1002
|
z-index: 150;
|
|
1000
1003
|
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
|
|
1001
1004
|
margin-bottom: 6px;
|
|
1005
|
+
cursor: pointer;
|
|
1002
1006
|
}
|
|
1003
|
-
.footnote-tooltip { cursor: pointer; }
|
|
1004
1007
|
.footnote-tooltip::after {
|
|
1005
1008
|
content: '';
|
|
1006
1009
|
position: absolute;
|
|
@@ -1015,9 +1018,6 @@
|
|
|
1015
1018
|
.footnote-tooltip a { color: #d4b96a; }
|
|
1016
1019
|
.footnote-tooltip p { margin: 0; }
|
|
1017
1020
|
.footnote-tooltip p + p { margin-top: 0.3rem; }
|
|
1018
|
-
sup[id^="fnref:"] {
|
|
1019
|
-
position: relative;
|
|
1020
|
-
}
|
|
1021
1021
|
|
|
1022
1022
|
/* Responsive */
|
|
1023
1023
|
@media (max-width: 768px) {
|
|
@@ -1426,10 +1426,6 @@
|
|
|
1426
1426
|
tooltip.style.left = '50%';
|
|
1427
1427
|
tooltip.style.transform = 'translateX(-50%)';
|
|
1428
1428
|
tooltip.style.removeProperty('--arrow-offset');
|
|
1429
|
-
// Clamp tooltip to viewport.
|
|
1430
|
-
// document.documentElement.clientWidth is more reliable than window.innerWidth
|
|
1431
|
-
// on Android browsers (e.g. EinkBro) which may return a pre-scaled layout
|
|
1432
|
-
// viewport width for window.innerWidth rather than the CSS viewport width.
|
|
1433
1429
|
var viewportWidth = Math.min(window.innerWidth, document.documentElement.clientWidth);
|
|
1434
1430
|
var rect = tooltip.getBoundingClientRect();
|
|
1435
1431
|
var shift = 0;
|
|
@@ -1453,19 +1449,16 @@
|
|
|
1453
1449
|
sup.addEventListener('mouseleave', hideTooltip);
|
|
1454
1450
|
|
|
1455
1451
|
// Touch: first tap shows tooltip, second tap navigates
|
|
1456
|
-
// Using touchend instead of click so no hover media query check is needed —
|
|
1457
|
-
// touchend only fires on real touch, regardless of what (hover:none) reports.
|
|
1458
1452
|
var linkTouchMoved = false;
|
|
1459
1453
|
link.addEventListener('touchstart', function() { linkTouchMoved = false; }, { passive: true });
|
|
1460
1454
|
link.addEventListener('touchmove', function() { linkTouchMoved = true; }, { passive: true });
|
|
1461
1455
|
link.addEventListener('touchend', function(e) {
|
|
1462
|
-
if (linkTouchMoved) return;
|
|
1456
|
+
if (linkTouchMoved) return;
|
|
1463
1457
|
if (tooltip.parentNode === sup) {
|
|
1464
1458
|
// Tooltip already showing — let the link navigate
|
|
1465
1459
|
return;
|
|
1466
1460
|
}
|
|
1467
1461
|
e.preventDefault();
|
|
1468
|
-
// Dismiss any other open tooltip
|
|
1469
1462
|
document.querySelectorAll('.footnote-tooltip').forEach(function(t) {
|
|
1470
1463
|
if (t.parentNode) t.parentNode.removeChild(t);
|
|
1471
1464
|
});
|
|
@@ -1482,7 +1475,7 @@
|
|
|
1482
1475
|
window.location.hash = fnHref;
|
|
1483
1476
|
});
|
|
1484
1477
|
|
|
1485
|
-
// Dismiss tooltip when
|
|
1478
|
+
// Dismiss tooltip when clicking elsewhere
|
|
1486
1479
|
document.addEventListener('click', function(e) {
|
|
1487
1480
|
if (tooltip.parentNode === sup && !sup.contains(e.target)) {
|
|
1488
1481
|
hideTooltip();
|
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
/* Bible Gateway citation links */
|
|
3
|
+
a[href*="biblegateway.com"] {
|
|
4
|
+
color: #7b3fa0;
|
|
5
|
+
font-weight: bold;
|
|
6
|
+
text-decoration: none;
|
|
7
|
+
border-bottom: 1px solid #b07fd4;
|
|
8
|
+
}
|
|
9
|
+
a[href*="biblegateway.com"]:hover { color: #5a2c78; border-bottom-color: #5a2c78; }
|
|
10
|
+
|
|
11
|
+
/* Left-click / tap link context popup */
|
|
12
|
+
.link-ctx-popup {
|
|
13
|
+
position: fixed;
|
|
14
|
+
z-index: 500;
|
|
15
|
+
width: 460px;
|
|
16
|
+
max-width: calc(100vw - 16px);
|
|
17
|
+
max-height: 60vh;
|
|
18
|
+
background: #faf8f4;
|
|
19
|
+
border: 1px solid #d4b96a;
|
|
20
|
+
border-radius: 6px;
|
|
21
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.22);
|
|
22
|
+
cursor: auto;
|
|
23
|
+
min-width: 280px;
|
|
24
|
+
min-height: 80px;
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
}
|
|
29
|
+
.link-ctx-popup-header {
|
|
30
|
+
display: flex;
|
|
31
|
+
justify-content: space-between;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: 0.5rem;
|
|
34
|
+
padding: 0.5rem 0.9rem;
|
|
35
|
+
border-bottom: 1px solid #e0d8c8;
|
|
36
|
+
background: #faf8f4;
|
|
37
|
+
flex-shrink: 0;
|
|
38
|
+
cursor: grab;
|
|
39
|
+
touch-action: none;
|
|
40
|
+
}
|
|
41
|
+
.link-ctx-popup-header.is-dragging {
|
|
42
|
+
cursor: grabbing;
|
|
43
|
+
user-select: none;
|
|
44
|
+
}
|
|
45
|
+
.link-ctx-popup-title-wrap {
|
|
46
|
+
flex: 1;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: 4px;
|
|
51
|
+
}
|
|
52
|
+
.link-ctx-popup-title {
|
|
53
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
54
|
+
font-weight: 700;
|
|
55
|
+
font-size: 0.9rem;
|
|
56
|
+
color: #3a3a3a;
|
|
57
|
+
min-width: 0;
|
|
58
|
+
word-break: break-word;
|
|
59
|
+
text-decoration: none;
|
|
60
|
+
}
|
|
61
|
+
a.link-ctx-popup-title:hover { color: #8b6914; }
|
|
62
|
+
.link-ctx-popup-close {
|
|
63
|
+
background: none;
|
|
64
|
+
border: none;
|
|
65
|
+
font-size: 1.2rem;
|
|
66
|
+
line-height: 1;
|
|
67
|
+
color: #888;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
padding: 0 0.2rem;
|
|
70
|
+
flex-shrink: 0;
|
|
71
|
+
}
|
|
72
|
+
.link-ctx-popup-close:hover { color: #2c2c2c; }
|
|
73
|
+
.link-ctx-popup-newtab {
|
|
74
|
+
color: #666;
|
|
75
|
+
flex-shrink: 0;
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
text-decoration: none;
|
|
79
|
+
padding: 0 0.2rem;
|
|
80
|
+
}
|
|
81
|
+
.link-ctx-popup-newtab:hover { color: #8b6914; }
|
|
82
|
+
.link-ctx-popup-back {
|
|
83
|
+
background: none;
|
|
84
|
+
border: none;
|
|
85
|
+
font-size: 1rem;
|
|
86
|
+
line-height: 1;
|
|
87
|
+
color: #888;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
padding: 0 0.3rem 0 0;
|
|
90
|
+
flex-shrink: 0;
|
|
91
|
+
}
|
|
92
|
+
.link-ctx-popup-back:hover { color: #2c2c2c; }
|
|
93
|
+
.link-ctx-popup-pin {
|
|
94
|
+
background: none;
|
|
95
|
+
border: none;
|
|
96
|
+
color: #bbb;
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
padding: 0 0.2rem;
|
|
99
|
+
flex-shrink: 0;
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
line-height: 1;
|
|
103
|
+
}
|
|
104
|
+
.link-ctx-popup-pin:hover { color: #2c2c2c; }
|
|
105
|
+
.link-ctx-popup-pin.pinned { color: #8b6914; }
|
|
106
|
+
.link-ctx-popup--pinned {
|
|
107
|
+
max-height: none;
|
|
108
|
+
min-height: 120px;
|
|
109
|
+
}
|
|
110
|
+
.link-ctx-popup--pinned .link-ctx-popup-header {
|
|
111
|
+
user-select: none;
|
|
112
|
+
}
|
|
113
|
+
.link-ctx-popup-body {
|
|
114
|
+
padding: 0.75rem 1rem;
|
|
115
|
+
flex: 1;
|
|
116
|
+
min-height: 0;
|
|
117
|
+
overflow-y: auto;
|
|
118
|
+
-webkit-overflow-scrolling: touch;
|
|
119
|
+
}
|
|
120
|
+
.link-ctx-popup-resize {
|
|
121
|
+
position: absolute;
|
|
122
|
+
bottom: 0;
|
|
123
|
+
right: 0;
|
|
124
|
+
width: 36px;
|
|
125
|
+
height: 36px;
|
|
126
|
+
cursor: nwse-resize;
|
|
127
|
+
touch-action: none;
|
|
128
|
+
border-bottom-right-radius: 5px;
|
|
129
|
+
background: repeating-linear-gradient(-45deg, transparent, transparent 2px, rgba(0,0,0,0.18) 2px, rgba(0,0,0,0.18) 3px);
|
|
130
|
+
z-index: 2;
|
|
131
|
+
}
|
|
132
|
+
.link-ctx-popup-url {
|
|
133
|
+
font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
134
|
+
font-size: 0.75rem;
|
|
135
|
+
color: #555;
|
|
136
|
+
word-break: break-all;
|
|
137
|
+
background: #f0ece3;
|
|
138
|
+
padding: 0.35rem 0.6rem;
|
|
139
|
+
border-radius: 4px;
|
|
140
|
+
margin-bottom: 0.5rem;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Shared popup content styles */
|
|
144
|
+
.link-ctx-popup-body {
|
|
145
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
146
|
+
font-size: 0.85rem;
|
|
147
|
+
line-height: 1.6;
|
|
148
|
+
color: #2c2c2c;
|
|
149
|
+
}
|
|
150
|
+
.link-ctx-popup-body h1, .link-ctx-popup-body h2, .link-ctx-popup-body h3,
|
|
151
|
+
.link-ctx-popup-body h4, .link-ctx-popup-body h5, .link-ctx-popup-body h6 {
|
|
152
|
+
margin: 0.7rem 0 0.3rem; color: #3a3a3a;
|
|
153
|
+
}
|
|
154
|
+
.link-ctx-popup-body h1 { font-size: 1.2rem; }
|
|
155
|
+
.link-ctx-popup-body h2 { font-size: 1.05rem; border-bottom: none; padding-bottom: 0; }
|
|
156
|
+
.link-ctx-popup-body h3 { font-size: 0.95rem; }
|
|
157
|
+
.link-ctx-popup-body p { margin: 0 0 0.5rem; }
|
|
158
|
+
.link-ctx-popup-body p:last-child { margin-bottom: 0; }
|
|
159
|
+
.link-ctx-popup-body a { color: #8b6914; text-decoration: none; border-bottom: 1px solid #d4b96a; }
|
|
160
|
+
.link-ctx-popup-body a.wiki-link { color: #6a8e3e; border-bottom-color: #6a8e3e; }
|
|
161
|
+
.link-ctx-popup-body code {
|
|
162
|
+
font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
163
|
+
font-size: 0.82em;
|
|
164
|
+
background: #f0ece3;
|
|
165
|
+
padding: 0.1em 0.3em;
|
|
166
|
+
border-radius: 3px;
|
|
167
|
+
}
|
|
168
|
+
.link-ctx-popup-body pre {
|
|
169
|
+
background: #2d2d2d;
|
|
170
|
+
color: #f0f0f0;
|
|
171
|
+
padding: 0.6rem 0.8rem;
|
|
172
|
+
border-radius: 4px;
|
|
173
|
+
font-size: 0.78rem;
|
|
174
|
+
line-height: 1.4;
|
|
175
|
+
overflow-x: auto;
|
|
176
|
+
margin: 0.4rem 0;
|
|
177
|
+
}
|
|
178
|
+
.link-ctx-popup-body pre code { background: none; padding: 0; font-size: 1em; color: inherit; }
|
|
179
|
+
.link-ctx-popup-body ul, .link-ctx-popup-body ol { padding-left: 1.4rem; margin: 0.3rem 0; }
|
|
180
|
+
.link-ctx-popup-body li { margin-bottom: 0.2rem; }
|
|
181
|
+
.link-ctx-popup-body blockquote {
|
|
182
|
+
border-left: 3px solid #d4b96a;
|
|
183
|
+
margin: 0.5rem 0;
|
|
184
|
+
padding: 0.3rem 0.8rem;
|
|
185
|
+
color: #4a4a4a;
|
|
186
|
+
font-style: italic;
|
|
187
|
+
}
|
|
188
|
+
.link-ctx-popup-body table { border-collapse: collapse; font-size: 0.8rem; margin: 0.5rem 0; }
|
|
189
|
+
.link-ctx-popup-body th, .link-ctx-popup-body td { border: 1px solid #ddd; padding: 0.3rem 0.5rem; }
|
|
190
|
+
.link-ctx-popup-body th { background: #f5f0e4; }
|
|
191
|
+
|
|
192
|
+
/* Blue Letter Bible popup tables */
|
|
193
|
+
.blb-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; margin-bottom: 0.6rem; }
|
|
194
|
+
.blb-table th, .blb-table td { padding: 3px 7px; border: 1px solid #ddd; }
|
|
195
|
+
.blb-th { text-align: left; font-weight: normal; background: #f5f0e4; color: #555; width: 38%; }
|
|
196
|
+
.blb-right { text-align: right; }
|
|
197
|
+
.blb-nowrap { white-space: nowrap; vertical-align: top; }
|
|
198
|
+
.blb-match { color: #b33 !important; font-weight: 700 !important; font-style: italic !important; }
|
|
199
|
+
.blb-heading { font-size: 0.82rem; font-weight: 600; margin: 0.7rem 0 0.25rem; color: #555; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
200
|
+
.blb-usage { font-size: 0.85rem; }
|
|
201
|
+
.blb-usage ol { margin: 0.1rem 0 0.1rem 1.3rem; padding: 0; list-style-type: decimal; }
|
|
202
|
+
.blb-usage ol ol { list-style-type: lower-alpha; }
|
|
203
|
+
.blb-usage ol ol ol { list-style-type: lower-roman; }
|
|
204
|
+
.blb-usage ol ol ol ol { list-style-type: lower-alpha; }
|
|
205
|
+
.blb-usage li { margin-bottom: 0.15rem; }
|
|
206
|
+
.blb-usage p { margin: 0; }
|
|
207
|
+
</style>
|
|
208
|
+
<script>
|
|
209
|
+
// Left-click / tap popup for all links (standalone — no .md-content scope restriction)
|
|
210
|
+
(function() {
|
|
211
|
+
var cache = Object.create(null);
|
|
212
|
+
var popup = null;
|
|
213
|
+
var touchMoved = false;
|
|
214
|
+
var historyStack = [];
|
|
215
|
+
var pinnedPopups = [];
|
|
216
|
+
var popupDragging = false;
|
|
217
|
+
var currentPopupPos = { x: 0, y: 0 };
|
|
218
|
+
var mouseLeaveTimer = null;
|
|
219
|
+
|
|
220
|
+
function escHtml(s) {
|
|
221
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function findLink(el) {
|
|
225
|
+
while (el && el.tagName !== 'BODY') {
|
|
226
|
+
if (el.tagName === 'A' && el.getAttribute('href')) return el;
|
|
227
|
+
el = el.parentElement;
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isAnchorOnly(href) { return href.charAt(0) === '#'; }
|
|
233
|
+
function isLocalMd(href) {
|
|
234
|
+
if (/^https?:\/\//i.test(href)) return false;
|
|
235
|
+
return /\.md([?#]|$)/i.test(href);
|
|
236
|
+
}
|
|
237
|
+
function isExternal(href) { return /^https?:\/\//i.test(href); }
|
|
238
|
+
|
|
239
|
+
function previewPath(href) {
|
|
240
|
+
var resolved = new URL(href, location.href);
|
|
241
|
+
return '/preview/' + resolved.pathname.replace(/^\/browse\//, '');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
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>';
|
|
245
|
+
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>';
|
|
246
|
+
|
|
247
|
+
function showPopup(x, y, title, bodyHtml, href, linkRect) {
|
|
248
|
+
if (popup && popup.parentNode) popup.parentNode.removeChild(popup);
|
|
249
|
+
popup = null;
|
|
250
|
+
currentPopupPos = { x: x, y: y, linkRect: linkRect || null };
|
|
251
|
+
|
|
252
|
+
popup = document.createElement('div');
|
|
253
|
+
popup.className = 'link-ctx-popup';
|
|
254
|
+
var thisPopup = popup;
|
|
255
|
+
var backBtn = historyStack.length > 0 ? '<button class="link-ctx-popup-back" aria-label="Back">\u2190</button>' : '';
|
|
256
|
+
var titleHtml = href
|
|
257
|
+
? '<div class="link-ctx-popup-title-wrap">' +
|
|
258
|
+
'<a class="link-ctx-popup-title" href="' + escHtml(href) + '"><span>' + escHtml(title) + '</span></a>' +
|
|
259
|
+
'<a class="link-ctx-popup-newtab" href="' + escHtml(href) + '" target="_blank" rel="noopener" title="Open in new tab">' + extLinkIcon + '</a>' +
|
|
260
|
+
'</div>'
|
|
261
|
+
: '<span class="link-ctx-popup-title-wrap"><span class="link-ctx-popup-title"><span>' + escHtml(title) + '</span></span></span>';
|
|
262
|
+
popup.innerHTML =
|
|
263
|
+
'<div class="link-ctx-popup-header">' +
|
|
264
|
+
backBtn + titleHtml +
|
|
265
|
+
'<button class="link-ctx-popup-pin" aria-label="Pin" title="Pin popup">' + pinIcon + '</button>' +
|
|
266
|
+
'<button class="link-ctx-popup-close" aria-label="Close">\u00d7</button>' +
|
|
267
|
+
'</div>' +
|
|
268
|
+
'<div class="link-ctx-popup-body">' + bodyHtml + '</div>' +
|
|
269
|
+
'<div class="link-ctx-popup-resize" aria-hidden="true"></div>';
|
|
270
|
+
document.body.appendChild(popup);
|
|
271
|
+
|
|
272
|
+
clearTimeout(mouseLeaveTimer);
|
|
273
|
+
popup.addEventListener('mouseleave', function() {
|
|
274
|
+
if (thisPopup.dataset.pinned || popupDragging) return;
|
|
275
|
+
mouseLeaveTimer = setTimeout(hidePopup, 150);
|
|
276
|
+
});
|
|
277
|
+
popup.addEventListener('mouseenter', function() {
|
|
278
|
+
if (thisPopup.dataset.pinned) return;
|
|
279
|
+
clearTimeout(mouseLeaveTimer);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
repositionPopup();
|
|
283
|
+
makeDraggable(popup);
|
|
284
|
+
makeResizable(popup);
|
|
285
|
+
|
|
286
|
+
var backBtnEl = popup.querySelector('.link-ctx-popup-back');
|
|
287
|
+
if (backBtnEl) {
|
|
288
|
+
backBtnEl.addEventListener('click', function(e) {
|
|
289
|
+
e.stopPropagation();
|
|
290
|
+
var prev = historyStack.pop();
|
|
291
|
+
if (prev) {
|
|
292
|
+
showPopup(prev.x, prev.y, prev.title, prev.bodyHtml, prev.href, prev.linkRect);
|
|
293
|
+
if (popup && prev.scrollTop) popup.scrollTop = prev.scrollTop;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
popup.querySelector('.link-ctx-popup-close').addEventListener('click', hidePopup);
|
|
298
|
+
popup.querySelector('.link-ctx-popup-pin').addEventListener('click', function(e) {
|
|
299
|
+
e.stopPropagation();
|
|
300
|
+
if (!thisPopup.dataset.pinned) pinPopup(thisPopup);
|
|
301
|
+
else unpinPopup(thisPopup);
|
|
302
|
+
});
|
|
303
|
+
popup.addEventListener('click', function(e) { e.stopPropagation(); });
|
|
304
|
+
|
|
305
|
+
var body = popup.querySelector('.link-ctx-popup-body');
|
|
306
|
+
body.addEventListener('click', function(e) {
|
|
307
|
+
var anchor = findLink(e.target);
|
|
308
|
+
if (!anchor) return;
|
|
309
|
+
var linkHref = anchor.getAttribute('href');
|
|
310
|
+
if (!linkHref || isAnchorOnly(linkHref)) return;
|
|
311
|
+
e.stopPropagation();
|
|
312
|
+
e.preventDefault();
|
|
313
|
+
if (thisPopup.dataset.pinned) { handleLink(anchor, e.clientX, e.clientY, false); return; }
|
|
314
|
+
var savedPos = { x: currentPopupPos.x, y: currentPopupPos.y, linkRect: currentPopupPos.linkRect };
|
|
315
|
+
var titleEl = thisPopup.querySelector('.link-ctx-popup-title');
|
|
316
|
+
historyStack.push({
|
|
317
|
+
x: savedPos.x, y: savedPos.y, linkRect: savedPos.linkRect,
|
|
318
|
+
title: titleEl ? titleEl.querySelector('span').textContent : '',
|
|
319
|
+
bodyHtml: body.innerHTML,
|
|
320
|
+
href: titleEl ? (titleEl.getAttribute('href') || '') : '',
|
|
321
|
+
scrollTop: thisPopup.scrollTop
|
|
322
|
+
});
|
|
323
|
+
handleLink(anchor, savedPos.x, savedPos.y, true);
|
|
324
|
+
});
|
|
325
|
+
body.addEventListener('touchend', function(e) {
|
|
326
|
+
if (touchMoved) return;
|
|
327
|
+
var anchor = findLink(e.target);
|
|
328
|
+
if (!anchor) return;
|
|
329
|
+
var linkHref = anchor.getAttribute('href');
|
|
330
|
+
if (!linkHref || isAnchorOnly(linkHref)) return;
|
|
331
|
+
e.stopPropagation();
|
|
332
|
+
e.preventDefault();
|
|
333
|
+
if (thisPopup.dataset.pinned) { handleLink(anchor, e.clientX, e.clientY, false); return; }
|
|
334
|
+
var savedPos = { x: currentPopupPos.x, y: currentPopupPos.y, linkRect: currentPopupPos.linkRect };
|
|
335
|
+
var titleEl = thisPopup.querySelector('.link-ctx-popup-title');
|
|
336
|
+
historyStack.push({
|
|
337
|
+
x: savedPos.x, y: savedPos.y, linkRect: savedPos.linkRect,
|
|
338
|
+
title: titleEl ? titleEl.querySelector('span').textContent : '',
|
|
339
|
+
bodyHtml: body.innerHTML,
|
|
340
|
+
href: titleEl ? (titleEl.getAttribute('href') || '') : '',
|
|
341
|
+
scrollTop: thisPopup.scrollTop
|
|
342
|
+
});
|
|
343
|
+
handleLink(anchor, savedPos.x, savedPos.y, true);
|
|
344
|
+
}, { passive: false });
|
|
345
|
+
|
|
346
|
+
['touchstart','touchmove','touchend'].forEach(function(ev) {
|
|
347
|
+
popup.addEventListener(ev, function(e) { e.stopPropagation(); }, { passive: true });
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function pinPopup(el) {
|
|
352
|
+
clearTimeout(mouseLeaveTimer);
|
|
353
|
+
var ownHistory = historyStack.slice();
|
|
354
|
+
popup = null;
|
|
355
|
+
historyStack = [];
|
|
356
|
+
el.style.width = el.offsetWidth + 'px';
|
|
357
|
+
el.style.height = el.offsetHeight + 'px';
|
|
358
|
+
el.style.maxHeight = 'none';
|
|
359
|
+
el.dataset.pinned = '1';
|
|
360
|
+
el.classList.add('link-ctx-popup--pinned');
|
|
361
|
+
var pinBtn = el.querySelector('.link-ctx-popup-pin');
|
|
362
|
+
if (pinBtn) pinBtn.classList.add('pinned');
|
|
363
|
+
var backBtn = el.querySelector('.link-ctx-popup-back');
|
|
364
|
+
if (backBtn) {
|
|
365
|
+
var newBack = backBtn.cloneNode(true);
|
|
366
|
+
backBtn.parentNode.replaceChild(newBack, backBtn);
|
|
367
|
+
newBack.addEventListener('click', function(e) {
|
|
368
|
+
e.stopPropagation();
|
|
369
|
+
goBackInPinned(el, ownHistory);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
var closeBtn = el.querySelector('.link-ctx-popup-close');
|
|
373
|
+
var newClose = closeBtn.cloneNode(true);
|
|
374
|
+
closeBtn.parentNode.replaceChild(newClose, closeBtn);
|
|
375
|
+
newClose.addEventListener('click', function(e) {
|
|
376
|
+
e.stopPropagation();
|
|
377
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
|
378
|
+
pinnedPopups = pinnedPopups.filter(function(p) { return p !== el; });
|
|
379
|
+
});
|
|
380
|
+
el._popupHistory = ownHistory;
|
|
381
|
+
pinnedPopups.push(el);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function unpinPopup(el) {
|
|
385
|
+
if (popup && popup !== el && popup.parentNode) popup.parentNode.removeChild(popup);
|
|
386
|
+
pinnedPopups = pinnedPopups.filter(function(p) { return p !== el; });
|
|
387
|
+
popup = el;
|
|
388
|
+
historyStack = el._popupHistory || [];
|
|
389
|
+
delete el._popupHistory;
|
|
390
|
+
delete el.dataset.pinned;
|
|
391
|
+
el.classList.remove('link-ctx-popup--pinned');
|
|
392
|
+
var pinBtn = el.querySelector('.link-ctx-popup-pin');
|
|
393
|
+
if (pinBtn) pinBtn.classList.remove('pinned');
|
|
394
|
+
var backBtn = el.querySelector('.link-ctx-popup-back');
|
|
395
|
+
if (backBtn) {
|
|
396
|
+
var newBack = backBtn.cloneNode(true);
|
|
397
|
+
backBtn.parentNode.replaceChild(newBack, backBtn);
|
|
398
|
+
newBack.addEventListener('click', function(e) {
|
|
399
|
+
e.stopPropagation();
|
|
400
|
+
var prev = historyStack.pop();
|
|
401
|
+
if (prev) {
|
|
402
|
+
showPopup(prev.x, prev.y, prev.title, prev.bodyHtml, prev.href, prev.linkRect);
|
|
403
|
+
if (popup && prev.scrollTop) popup.scrollTop = prev.scrollTop;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
var closeBtn = el.querySelector('.link-ctx-popup-close');
|
|
408
|
+
var newClose = closeBtn.cloneNode(true);
|
|
409
|
+
closeBtn.parentNode.replaceChild(newClose, closeBtn);
|
|
410
|
+
newClose.addEventListener('click', hidePopup);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function goBackInPinned(el, ownHistory) {
|
|
414
|
+
var prev = ownHistory.pop();
|
|
415
|
+
if (!prev) return;
|
|
416
|
+
var body = el.querySelector('.link-ctx-popup-body');
|
|
417
|
+
if (body) body.innerHTML = prev.bodyHtml;
|
|
418
|
+
var titleSpan = el.querySelector('.link-ctx-popup-title span');
|
|
419
|
+
if (titleSpan) titleSpan.textContent = prev.title;
|
|
420
|
+
var titleLink = el.querySelector('a.link-ctx-popup-title');
|
|
421
|
+
if (titleLink && prev.href) titleLink.href = prev.href;
|
|
422
|
+
var backBtnEl = el.querySelector('.link-ctx-popup-back');
|
|
423
|
+
if (backBtnEl) backBtnEl.style.display = ownHistory.length > 0 ? '' : 'none';
|
|
424
|
+
if (prev.scrollTop) el.scrollTop = prev.scrollTop;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function makeDraggable(el) {
|
|
428
|
+
var header = el.querySelector('.link-ctx-popup-header');
|
|
429
|
+
if (!header) return;
|
|
430
|
+
var startX, startY, startLeft, startTop;
|
|
431
|
+
|
|
432
|
+
function onDragStart(clientX, clientY) {
|
|
433
|
+
startX = clientX; startY = clientY;
|
|
434
|
+
startLeft = el.offsetLeft; startTop = el.offsetTop;
|
|
435
|
+
el.style.width = el.offsetWidth + 'px';
|
|
436
|
+
el.style.height = el.offsetHeight + 'px';
|
|
437
|
+
el.style.maxHeight = 'none';
|
|
438
|
+
popupDragging = true;
|
|
439
|
+
header.classList.add('is-dragging');
|
|
440
|
+
}
|
|
441
|
+
function onDragMove(clientX, clientY) {
|
|
442
|
+
el.style.left = (startLeft + clientX - startX) + 'px';
|
|
443
|
+
el.style.top = (startTop + clientY - startY) + 'px';
|
|
444
|
+
}
|
|
445
|
+
function onDragEnd() {
|
|
446
|
+
popupDragging = false;
|
|
447
|
+
header.classList.remove('is-dragging');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
header.addEventListener('mousedown', function(e) {
|
|
451
|
+
if (e.target.closest('button') || e.target.closest('a')) return;
|
|
452
|
+
e.preventDefault();
|
|
453
|
+
onDragStart(e.clientX, e.clientY);
|
|
454
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
455
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
456
|
+
});
|
|
457
|
+
function onMouseMove(e) { onDragMove(e.clientX, e.clientY); }
|
|
458
|
+
function onMouseUp() {
|
|
459
|
+
onDragEnd();
|
|
460
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
461
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
header.addEventListener('touchstart', function(e) {
|
|
465
|
+
if (e.target.closest('button') || e.target.closest('a')) return;
|
|
466
|
+
e.preventDefault();
|
|
467
|
+
var t = e.touches[0];
|
|
468
|
+
onDragStart(t.clientX, t.clientY);
|
|
469
|
+
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
470
|
+
document.addEventListener('touchend', onTouchEnd);
|
|
471
|
+
}, { passive: false });
|
|
472
|
+
function onTouchMove(e) { e.preventDefault(); var t = e.touches[0]; onDragMove(t.clientX, t.clientY); }
|
|
473
|
+
function onTouchEnd() {
|
|
474
|
+
onDragEnd();
|
|
475
|
+
document.removeEventListener('touchmove', onTouchMove);
|
|
476
|
+
document.removeEventListener('touchend', onTouchEnd);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function makeResizable(el) {
|
|
481
|
+
var handle = el.querySelector('.link-ctx-popup-resize');
|
|
482
|
+
if (!handle) return;
|
|
483
|
+
var startX, startY, startW, startH;
|
|
484
|
+
|
|
485
|
+
function onResizeStart(clientX, clientY) {
|
|
486
|
+
startX = clientX; startY = clientY;
|
|
487
|
+
startW = el.offsetWidth; startH = el.offsetHeight;
|
|
488
|
+
el.style.width = startW + 'px';
|
|
489
|
+
el.style.height = startH + 'px';
|
|
490
|
+
el.style.maxHeight = 'none';
|
|
491
|
+
}
|
|
492
|
+
function onResizeMove(clientX, clientY) {
|
|
493
|
+
el.style.width = Math.max(280, startW + clientX - startX) + 'px';
|
|
494
|
+
el.style.height = Math.max(80, startH + clientY - startY) + 'px';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
handle.addEventListener('mousedown', function(e) {
|
|
498
|
+
e.preventDefault(); e.stopPropagation();
|
|
499
|
+
onResizeStart(e.clientX, e.clientY);
|
|
500
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
501
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
502
|
+
});
|
|
503
|
+
function onMouseMove(e) { onResizeMove(e.clientX, e.clientY); }
|
|
504
|
+
function onMouseUp() {
|
|
505
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
506
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
handle.addEventListener('touchstart', function(e) {
|
|
510
|
+
e.preventDefault(); e.stopPropagation();
|
|
511
|
+
var t = e.touches[0];
|
|
512
|
+
onResizeStart(t.clientX, t.clientY);
|
|
513
|
+
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
514
|
+
document.addEventListener('touchend', onTouchEnd);
|
|
515
|
+
}, { passive: false });
|
|
516
|
+
function onTouchMove(e) { e.preventDefault(); onResizeMove(e.touches[0].clientX, e.touches[0].clientY); }
|
|
517
|
+
function onTouchEnd() {
|
|
518
|
+
document.removeEventListener('touchmove', onTouchMove);
|
|
519
|
+
document.removeEventListener('touchend', onTouchEnd);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function repositionPopup() {
|
|
524
|
+
if (!popup) return;
|
|
525
|
+
var x = currentPopupPos.x, y = currentPopupPos.y;
|
|
526
|
+
var rect = currentPopupPos.linkRect;
|
|
527
|
+
var vw = Math.min(window.innerWidth, document.documentElement.clientWidth);
|
|
528
|
+
var vh = window.innerHeight;
|
|
529
|
+
var pw = popup.offsetWidth;
|
|
530
|
+
var ph = popup.offsetHeight;
|
|
531
|
+
|
|
532
|
+
var left = Math.min(x - 16, vw - pw - 8);
|
|
533
|
+
if (left < 8) left = 8;
|
|
534
|
+
|
|
535
|
+
var top;
|
|
536
|
+
if (rect) {
|
|
537
|
+
var gap = 3;
|
|
538
|
+
var belowTop = rect.bottom + gap;
|
|
539
|
+
var aboveTop = rect.top - ph - gap;
|
|
540
|
+
if (belowTop + ph <= vh - 8) {
|
|
541
|
+
top = belowTop;
|
|
542
|
+
} else if (aboveTop >= 8) {
|
|
543
|
+
top = aboveTop;
|
|
544
|
+
} else {
|
|
545
|
+
top = Math.max(8, vh - ph - 8);
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
top = y + 12;
|
|
549
|
+
if (top + ph > vh - 8) top = Math.max(8, y - ph - 12);
|
|
550
|
+
if (top < 8) top = 8;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
popup.style.left = left + 'px';
|
|
554
|
+
popup.style.top = top + 'px';
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function updatePopup(bodyHtml, title) {
|
|
558
|
+
if (!popup) return;
|
|
559
|
+
var body = popup.querySelector('.link-ctx-popup-body');
|
|
560
|
+
var titleTextEl = popup.querySelector('.link-ctx-popup-title span');
|
|
561
|
+
if (body) body.innerHTML = bodyHtml;
|
|
562
|
+
if (title && titleTextEl) titleTextEl.textContent = title;
|
|
563
|
+
repositionPopup();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function hidePopup() {
|
|
567
|
+
clearTimeout(mouseLeaveTimer);
|
|
568
|
+
if (popup && popup.parentNode) popup.parentNode.removeChild(popup);
|
|
569
|
+
popup = null;
|
|
570
|
+
historyStack = [];
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function applyPopupAnchor(hash) {
|
|
574
|
+
if (!hash || !popup) return;
|
|
575
|
+
var id = hash.replace(/^#/, '');
|
|
576
|
+
try {
|
|
577
|
+
var target = popup.querySelector('[id="' + id.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"]');
|
|
578
|
+
if (!target) return;
|
|
579
|
+
var header = popup.querySelector('.link-ctx-popup-header');
|
|
580
|
+
var headerHeight = header ? header.offsetHeight : 0;
|
|
581
|
+
var targetTop = target.getBoundingClientRect().top;
|
|
582
|
+
var popupTop = popup.getBoundingClientRect().top;
|
|
583
|
+
popup.scrollTop += targetTop - popupTop - headerHeight - 8;
|
|
584
|
+
if (/^H[1-6]$/.test(target.tagName)) {
|
|
585
|
+
var titleEl = popup.querySelector('.link-ctx-popup-title span');
|
|
586
|
+
if (titleEl) titleEl.textContent = titleEl.textContent + ' \u203a ' + target.textContent;
|
|
587
|
+
}
|
|
588
|
+
} catch(e) {}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function handleLink(anchor, x, y, chained) {
|
|
592
|
+
if (!chained) historyStack = [];
|
|
593
|
+
var href = anchor.getAttribute('href');
|
|
594
|
+
if (!href || isAnchorOnly(href)) return;
|
|
595
|
+
var label = anchor.textContent.trim() || href;
|
|
596
|
+
var linkRect = chained ? currentPopupPos.linkRect : anchor.getBoundingClientRect();
|
|
597
|
+
var linkHash = (function() { try { return new URL(href, location.href).hash; } catch(e) { return ''; } })();
|
|
598
|
+
|
|
599
|
+
if (isLocalMd(href)) {
|
|
600
|
+
var path = previewPath(href);
|
|
601
|
+
var cached = cache[path];
|
|
602
|
+
if (cached && typeof cached === 'object') {
|
|
603
|
+
showPopup(x, y, cached.title || label, cached.html, href, linkRect);
|
|
604
|
+
applyPopupAnchor(linkHash);
|
|
605
|
+
} else if (cached === false) {
|
|
606
|
+
showPopup(x, y, label,
|
|
607
|
+
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
608
|
+
'<p style="margin:0;color:#888;font-family:sans-serif;font-size:0.82rem">Preview not available</p>', href, linkRect);
|
|
609
|
+
} else {
|
|
610
|
+
showPopup(x, y, label, '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>', href, linkRect);
|
|
611
|
+
if (cached === undefined) {
|
|
612
|
+
cache[path] = null;
|
|
613
|
+
fetch(path)
|
|
614
|
+
.then(function(r) { return r.ok ? r.json() : null; })
|
|
615
|
+
.then(function(data) {
|
|
616
|
+
if (!data) { cache[path] = false; updatePopup('<p style="margin:0;color:#c44;font-family:sans-serif;font-size:0.85rem">Preview unavailable.</p>'); return; }
|
|
617
|
+
var bodyHtml = data.html + (data.frontmatter_html || '');
|
|
618
|
+
cache[path] = { title: data.title, html: bodyHtml };
|
|
619
|
+
updatePopup(bodyHtml, data.title || label);
|
|
620
|
+
applyPopupAnchor(linkHash);
|
|
621
|
+
})
|
|
622
|
+
.catch(function() {
|
|
623
|
+
cache[path] = false;
|
|
624
|
+
updatePopup('<p style="margin:0;color:#c44;font-family:sans-serif;font-size:0.85rem">Preview unavailable.</p>');
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
} else if (isExternal(href)) {
|
|
629
|
+
var extKey = 'ext:' + href;
|
|
630
|
+
var extCached = cache[extKey];
|
|
631
|
+
if (extCached && typeof extCached === 'object') {
|
|
632
|
+
showPopup(x, y, extCached.title || label, extCached.html, href, linkRect);
|
|
633
|
+
} else if (extCached === false) {
|
|
634
|
+
showPopup(x, y, label,
|
|
635
|
+
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
636
|
+
'<p style="margin:0.5rem 0 0;color:#888;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>', href, linkRect);
|
|
637
|
+
} else {
|
|
638
|
+
showPopup(x, y, label, '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>', href, linkRect);
|
|
639
|
+
if (extCached === undefined) {
|
|
640
|
+
cache[extKey] = null;
|
|
641
|
+
fetch('/fetch?url=' + encodeURIComponent(href))
|
|
642
|
+
.then(function(r) { return r.ok ? r.json() : null; })
|
|
643
|
+
.then(function(data) {
|
|
644
|
+
if (!data || data.error) {
|
|
645
|
+
cache[extKey] = false;
|
|
646
|
+
updatePopup(
|
|
647
|
+
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
648
|
+
'<p style="margin:0.5rem 0 0;color:#888;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>');
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
cache[extKey] = { title: data.title, html: data.html };
|
|
652
|
+
updatePopup(data.html, data.title || label);
|
|
653
|
+
})
|
|
654
|
+
.catch(function() {
|
|
655
|
+
cache[extKey] = false;
|
|
656
|
+
updatePopup(
|
|
657
|
+
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
658
|
+
'<p style="margin:0.5rem 0 0;color:#c44;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>');
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
} else {
|
|
663
|
+
showPopup(x, y, label, '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>', href, linkRect);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Left-click on desktop
|
|
668
|
+
document.addEventListener('click', function(e) {
|
|
669
|
+
if (popup && popup.contains(e.target)) return;
|
|
670
|
+
var anchor = findLink(e.target);
|
|
671
|
+
if (!anchor) { hidePopup(); return; }
|
|
672
|
+
var href = anchor.getAttribute('href');
|
|
673
|
+
if (!href || isAnchorOnly(href)) { hidePopup(); return; }
|
|
674
|
+
if (!isLocalMd(href) && !isExternal(href)) { hidePopup(); return; }
|
|
675
|
+
e.preventDefault();
|
|
676
|
+
handleLink(anchor, e.clientX, e.clientY, false);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// Touch tracking
|
|
680
|
+
document.addEventListener('touchstart', function(e) {
|
|
681
|
+
if (e.touches.length !== 1) return;
|
|
682
|
+
touchMoved = false;
|
|
683
|
+
}, { passive: true });
|
|
684
|
+
|
|
685
|
+
document.addEventListener('touchmove', function() {
|
|
686
|
+
touchMoved = true;
|
|
687
|
+
}, { passive: true });
|
|
688
|
+
|
|
689
|
+
// Touch tap
|
|
690
|
+
document.addEventListener('touchend', function(e) {
|
|
691
|
+
if (touchMoved) return;
|
|
692
|
+
if (popup && popup.contains(e.target)) return;
|
|
693
|
+
var anchor = findLink(e.target);
|
|
694
|
+
if (!anchor) { hidePopup(); return; }
|
|
695
|
+
var href = anchor.getAttribute('href');
|
|
696
|
+
if (!href || isAnchorOnly(href)) { hidePopup(); return; }
|
|
697
|
+
if (!isLocalMd(href) && !isExternal(href)) { hidePopup(); return; }
|
|
698
|
+
e.preventDefault();
|
|
699
|
+
var touch = e.changedTouches[0];
|
|
700
|
+
handleLink(anchor, touch.clientX, touch.clientY, false);
|
|
701
|
+
}, { passive: false });
|
|
702
|
+
|
|
703
|
+
document.addEventListener('keydown', function(e) {
|
|
704
|
+
if (e.key === 'Escape' && popup) hidePopup();
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// Hover — per-link, triggered after a short delay when no popup is active
|
|
708
|
+
(function() {
|
|
709
|
+
var hoverTimer = null;
|
|
710
|
+
document.querySelectorAll('a').forEach(function(a) {
|
|
711
|
+
var href = a.getAttribute('href');
|
|
712
|
+
if (!href || isAnchorOnly(href)) return;
|
|
713
|
+
if (!isLocalMd(href) && !isExternal(href)) return;
|
|
714
|
+
|
|
715
|
+
a.addEventListener('mouseenter', function(e) {
|
|
716
|
+
clearTimeout(hoverTimer);
|
|
717
|
+
if (popup) return;
|
|
718
|
+
var x = e.clientX, y = e.clientY;
|
|
719
|
+
hoverTimer = setTimeout(function() {
|
|
720
|
+
if (!popup) handleLink(a, x, y, false);
|
|
721
|
+
}, 300);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
a.addEventListener('mouseleave', function() {
|
|
725
|
+
clearTimeout(hoverTimer);
|
|
726
|
+
if (popup) mouseLeaveTimer = setTimeout(hidePopup, 150);
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
})();
|
|
730
|
+
|
|
731
|
+
})();
|
|
732
|
+
</script>
|
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.19
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Dunn
|
|
@@ -111,6 +111,7 @@ files:
|
|
|
111
111
|
- views/directory.erb
|
|
112
112
|
- views/layout.erb
|
|
113
113
|
- views/markdown.erb
|
|
114
|
+
- views/popup_assets.erb
|
|
114
115
|
- views/raw.erb
|
|
115
116
|
- views/search.erb
|
|
116
117
|
- views/setup_info.erb
|