markdownr 0.5.18 → 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/lib/markdown_server/app.rb +29 -3
- data/lib/markdown_server/bible_citations.rb +34 -1
- data/lib/markdown_server/version.rb +1 -1
- 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/lib/markdown_server/app.rb
CHANGED
|
@@ -34,6 +34,7 @@ module MarkdownServer
|
|
|
34
34
|
set :verbose, false
|
|
35
35
|
set :session_secret, ENV.fetch("MARKDOWNR_SESSION_SECRET", SecureRandom.hex(64))
|
|
36
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"))
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
helpers do
|
|
@@ -138,8 +139,7 @@ module MarkdownServer
|
|
|
138
139
|
# Auto-link bare Bible verse citations (e.g. "John 3:16" → BibleGateway link).
|
|
139
140
|
# Skips inline code, fenced code blocks, and citations already inside a link.
|
|
140
141
|
text = MarkdownServer.link_citations(text) do |canonical, verse, citation|
|
|
141
|
-
|
|
142
|
-
"[#{citation}](#{url})"
|
|
142
|
+
"[#{citation}](#{MarkdownServer.biblegateway_url(canonical, verse)})"
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
# Process wiki links BEFORE Kramdown so that | isn't consumed as
|
|
@@ -786,6 +786,27 @@ module MarkdownServer
|
|
|
786
786
|
end
|
|
787
787
|
end
|
|
788
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
|
+
|
|
789
810
|
def admin?
|
|
790
811
|
return true if session[:admin]
|
|
791
812
|
|
|
@@ -1176,7 +1197,12 @@ module MarkdownServer
|
|
|
1176
1197
|
redirect @download_href
|
|
1177
1198
|
|
|
1178
1199
|
when ".html"
|
|
1179
|
-
|
|
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
|
|
1180
1206
|
|
|
1181
1207
|
when ".css"
|
|
1182
1208
|
send_file real_path, type: "text/css"
|
|
@@ -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
|
|
@@ -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
|