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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1186498f524f4e5d50aa1d9ddb93f4d48e3dcfb327b6d4e10bb530be747179a3
4
- data.tar.gz: 2d6131e668d24603309ae2b25893b3bdde2d1764f971e18acec87b1fbb26250a
3
+ metadata.gz: a3cd57887599183f3350f1dd63f44f636ab03b9d3c3a462a1aaec04793cbbfeb
4
+ data.tar.gz: 8ecb0febc8ba672c28ad2cc9d2cdea0cbe7a1e1c79508bb0ec1b4fcb40c133fa
5
5
  SHA512:
6
- metadata.gz: 92d7e42dffe104f2c5e8a3c6289962af0ea643f27ecac103e714298b11827dd1089bd71503116bacd044cb779fca5acd192cba47d59314d991ff4d3c5514d39e
7
- data.tar.gz: 8ca0350721e961e55d556bf67a9fa8a8b940e21578ac1faba965d1e9fe34968f06c4f5e5ec3ea729d7305581396be595a1fa8cd8a6664f2f97502e99cbeaf93c
6
+ metadata.gz: 83b163d0b783c29163dfc8d7803e0536dd78a295d7b4d72518dd55015b7e302fae23d83bec507fb1f363eee3af2f5e031f5ca8fd9a42de10c42ff72bce996446
7
+ data.tar.gz: b6a498d1d81abdd6ad981c4c930ba934776895c2606bc0ae9ae42bcfb71890d79be8bcdeec55e190c36199c34ed6b74ce2926b1705161ba862f68752b2d9a838
@@ -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
- url = "https://www.biblegateway.com/passage/?search=#{CGI.escape("#{canonical} #{verse}")}&version=CEB"
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
- send_file real_path, type: "text/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
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
- "|((?:#{_book_alts})\\.?[ \\t]?\\d+(?::#{_verse.source})?)",
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
@@ -1,3 +1,3 @@
1
1
  module MarkdownServer
2
- VERSION = "0.5.18"
2
+ VERSION = "0.5.19"
3
3
  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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
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.18
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