markdownr 0.4.4 → 0.4.6
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 +82 -2
- data/lib/markdown_server/version.rb +1 -1
- data/views/layout.erb +319 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 596b75e231efdc839c8729fe1db59a8b2933c08f12059cf753b261445d888813
|
|
4
|
+
data.tar.gz: ddc7fc42eb7f55286e0edcb296ec816567146e1ad412ae4df4243ab31255e4ea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 34a2585d6a3818cd175d40f8b9f728bee1a83b3dbf2ac238c320b8b2be9d49c895dc9fb9709edfeed149317d4a03caa1536a87075da706711ceafa61a3c789d9
|
|
7
|
+
data.tar.gz: 256c941d8ad83a87fca9d205be075390c425c3b883dbd7fc3b2ed4f71e0f61c6cc29517a56897046c9c2a9c4507978b84445c9edeaf456e054527e9be1ed0f00
|
data/lib/markdown_server/app.rb
CHANGED
|
@@ -8,6 +8,7 @@ require "uri"
|
|
|
8
8
|
require "cgi"
|
|
9
9
|
require "pathname"
|
|
10
10
|
require "set"
|
|
11
|
+
require "net/http"
|
|
11
12
|
|
|
12
13
|
module MarkdownServer
|
|
13
14
|
class App < Sinatra::Base
|
|
@@ -431,6 +432,76 @@ module MarkdownServer
|
|
|
431
432
|
html
|
|
432
433
|
end
|
|
433
434
|
|
|
435
|
+
FETCH_MAX_BYTES = 512_000
|
|
436
|
+
FETCH_TIMEOUT = 5
|
|
437
|
+
|
|
438
|
+
def fetch_external_page(url_str)
|
|
439
|
+
uri = URI.parse(url_str)
|
|
440
|
+
return nil unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
441
|
+
fetch_follow_redirects(uri, 5)
|
|
442
|
+
rescue
|
|
443
|
+
nil
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def fetch_follow_redirects(uri, limit)
|
|
447
|
+
return nil if limit <= 0
|
|
448
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
449
|
+
http.use_ssl = (uri.scheme == "https")
|
|
450
|
+
http.open_timeout = FETCH_TIMEOUT
|
|
451
|
+
http.read_timeout = FETCH_TIMEOUT
|
|
452
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
453
|
+
req["User-Agent"] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
|
454
|
+
req["Accept"] = "text/html,application/xhtml+xml;q=0.9,*/*;q=0.8"
|
|
455
|
+
req["Accept-Language"] = "en-US,en;q=0.5"
|
|
456
|
+
resp = http.request(req)
|
|
457
|
+
case resp
|
|
458
|
+
when Net::HTTPSuccess
|
|
459
|
+
ct = resp["content-type"].to_s
|
|
460
|
+
return nil unless ct.match?(/html|text/i)
|
|
461
|
+
body = resp.body.to_s
|
|
462
|
+
body = body.b[0, FETCH_MAX_BYTES].force_encoding("utf-8")
|
|
463
|
+
body.encode("utf-8", invalid: :replace, undef: :replace, replace: "?")
|
|
464
|
+
when Net::HTTPRedirection
|
|
465
|
+
loc = resp["Location"].to_s
|
|
466
|
+
new_uri = (URI.parse(loc) rescue nil)
|
|
467
|
+
return nil unless new_uri
|
|
468
|
+
new_uri = uri + new_uri unless new_uri.absolute?
|
|
469
|
+
return nil unless new_uri.is_a?(URI::HTTP) || new_uri.is_a?(URI::HTTPS)
|
|
470
|
+
fetch_follow_redirects(new_uri, limit - 1)
|
|
471
|
+
end
|
|
472
|
+
rescue
|
|
473
|
+
nil
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def page_title(html)
|
|
477
|
+
html.match(/<title[^>]*>(.*?)<\/title>/im)&.then { |m|
|
|
478
|
+
m[1].gsub(/<[^>]+>/, "").gsub(/&/i, "&").gsub(/</i, "<")
|
|
479
|
+
.gsub(/>/i, ">").gsub(/"/i, '"').gsub(/&#?\w+;/, "").strip
|
|
480
|
+
} || ""
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def page_text(html)
|
|
484
|
+
# Strip inert elements
|
|
485
|
+
w = html
|
|
486
|
+
.gsub(/<script[^>]*>.*?<\/script>/im, " ")
|
|
487
|
+
.gsub(/<style[^>]*>.*?<\/style>/im, " ")
|
|
488
|
+
.gsub(/<nav[^>]*>.*?<\/nav>/im, " ")
|
|
489
|
+
.gsub(/<header[^>]*>.*?<\/header>/im," ")
|
|
490
|
+
.gsub(/<footer[^>]*>.*?<\/footer>/im," ")
|
|
491
|
+
.gsub(/<!--.*?-->/m, " ")
|
|
492
|
+
# Prefer <article> or <main>, fall back to <body>, then whole doc
|
|
493
|
+
content = w.match(/<article[^>]*>(.*?)<\/article>/im)&.[](1) ||
|
|
494
|
+
w.match(/<main[^>]*>(.*?)<\/main>/im)&.[](1) ||
|
|
495
|
+
w.match(/<body[^>]*>(.*?)<\/body>/im)&.[](1) ||
|
|
496
|
+
w
|
|
497
|
+
text = content
|
|
498
|
+
.gsub(/<[^>]+>/, " ")
|
|
499
|
+
.gsub(/ /i, " ").gsub(/&/i, "&").gsub(/</i, "<")
|
|
500
|
+
.gsub(/>/i, ">").gsub(/"/i, '"').gsub(/&#?\w+;/, " ")
|
|
501
|
+
.gsub(/\s+/, " ").strip
|
|
502
|
+
text.length > 3000 ? "#{text[0, 3000]}…" : text
|
|
503
|
+
end
|
|
504
|
+
|
|
434
505
|
def compile_regexes(query)
|
|
435
506
|
words = query.split(/\s+/).reject(&:empty?)
|
|
436
507
|
return nil if words.empty?
|
|
@@ -483,8 +554,6 @@ module MarkdownServer
|
|
|
483
554
|
|
|
484
555
|
get "/preview/*" do
|
|
485
556
|
content_type :json
|
|
486
|
-
halt 404, '{"error":"disabled"}' unless settings.link_tooltips
|
|
487
|
-
|
|
488
557
|
requested = params["splat"].first.to_s
|
|
489
558
|
base = File.realpath(root_dir)
|
|
490
559
|
full = File.join(base, requested)
|
|
@@ -516,6 +585,17 @@ module MarkdownServer
|
|
|
516
585
|
JSON.dump({ title: title.to_s, html: html })
|
|
517
586
|
end
|
|
518
587
|
|
|
588
|
+
get "/fetch" do
|
|
589
|
+
content_type :json
|
|
590
|
+
url = params[:url].to_s.strip
|
|
591
|
+
halt 400, '{"error":"invalid url"}' unless url.match?(/\Ahttps?:\/\//i)
|
|
592
|
+
|
|
593
|
+
html = fetch_external_page(url)
|
|
594
|
+
halt 502, '{"error":"fetch failed"}' unless html
|
|
595
|
+
|
|
596
|
+
JSON.dump({ title: page_title(html), text: page_text(html) })
|
|
597
|
+
end
|
|
598
|
+
|
|
519
599
|
get "/search/?*" do
|
|
520
600
|
requested = params["splat"].first.to_s.chomp("/")
|
|
521
601
|
@query = params[:q].to_s.strip
|
data/views/layout.erb
CHANGED
|
@@ -678,6 +678,111 @@
|
|
|
678
678
|
/* Footnote list — hide numeric markers; labels are inline via <strong> */
|
|
679
679
|
.footnotes ol { list-style: none; padding-left: 1.2em; }
|
|
680
680
|
|
|
681
|
+
/* Right-click / long-press link context popup */
|
|
682
|
+
.link-ctx-popup {
|
|
683
|
+
position: fixed;
|
|
684
|
+
z-index: 500;
|
|
685
|
+
width: 460px;
|
|
686
|
+
max-width: calc(100vw - 16px);
|
|
687
|
+
max-height: 60vh;
|
|
688
|
+
overflow-y: auto;
|
|
689
|
+
background: #faf8f4;
|
|
690
|
+
border: 1px solid #d4b96a;
|
|
691
|
+
border-radius: 6px;
|
|
692
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.22);
|
|
693
|
+
-webkit-overflow-scrolling: touch;
|
|
694
|
+
cursor: auto;
|
|
695
|
+
}
|
|
696
|
+
.link-ctx-popup-header {
|
|
697
|
+
display: flex;
|
|
698
|
+
justify-content: space-between;
|
|
699
|
+
align-items: center;
|
|
700
|
+
gap: 0.5rem;
|
|
701
|
+
padding: 0.5rem 0.9rem;
|
|
702
|
+
border-bottom: 1px solid #e0d8c8;
|
|
703
|
+
position: sticky;
|
|
704
|
+
top: 0;
|
|
705
|
+
background: #faf8f4;
|
|
706
|
+
z-index: 1;
|
|
707
|
+
}
|
|
708
|
+
.link-ctx-popup-title {
|
|
709
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
710
|
+
font-weight: 700;
|
|
711
|
+
font-size: 0.9rem;
|
|
712
|
+
color: #3a3a3a;
|
|
713
|
+
min-width: 0;
|
|
714
|
+
word-break: break-word;
|
|
715
|
+
}
|
|
716
|
+
.link-ctx-popup-close {
|
|
717
|
+
background: none;
|
|
718
|
+
border: none;
|
|
719
|
+
font-size: 1.2rem;
|
|
720
|
+
line-height: 1;
|
|
721
|
+
color: #888;
|
|
722
|
+
cursor: pointer;
|
|
723
|
+
padding: 0 0.2rem;
|
|
724
|
+
flex-shrink: 0;
|
|
725
|
+
}
|
|
726
|
+
.link-ctx-popup-close:hover { color: #2c2c2c; }
|
|
727
|
+
.link-ctx-popup-body {
|
|
728
|
+
padding: 0.75rem 1rem;
|
|
729
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
730
|
+
font-size: 0.85rem;
|
|
731
|
+
line-height: 1.6;
|
|
732
|
+
color: #2c2c2c;
|
|
733
|
+
}
|
|
734
|
+
.link-ctx-popup-url {
|
|
735
|
+
font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
736
|
+
font-size: 0.75rem;
|
|
737
|
+
color: #555;
|
|
738
|
+
word-break: break-all;
|
|
739
|
+
background: #f0ece3;
|
|
740
|
+
padding: 0.35rem 0.6rem;
|
|
741
|
+
border-radius: 4px;
|
|
742
|
+
margin-bottom: 0.5rem;
|
|
743
|
+
}
|
|
744
|
+
.link-ctx-popup-body h1, .link-ctx-popup-body h2, .link-ctx-popup-body h3,
|
|
745
|
+
.link-ctx-popup-body h4, .link-ctx-popup-body h5, .link-ctx-popup-body h6 {
|
|
746
|
+
margin: 0.7rem 0 0.3rem; color: #3a3a3a;
|
|
747
|
+
}
|
|
748
|
+
.link-ctx-popup-body h1 { font-size: 1.2rem; }
|
|
749
|
+
.link-ctx-popup-body h2 { font-size: 1.05rem; border-bottom: none; padding-bottom: 0; }
|
|
750
|
+
.link-ctx-popup-body h3 { font-size: 0.95rem; }
|
|
751
|
+
.link-ctx-popup-body p { margin: 0 0 0.5rem; }
|
|
752
|
+
.link-ctx-popup-body p:last-child { margin-bottom: 0; }
|
|
753
|
+
.link-ctx-popup-body a { color: #8b6914; text-decoration: none; border-bottom: 1px solid #d4b96a; }
|
|
754
|
+
.link-ctx-popup-body a.wiki-link { color: #6a8e3e; border-bottom-color: #6a8e3e; }
|
|
755
|
+
.link-ctx-popup-body code {
|
|
756
|
+
font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
757
|
+
font-size: 0.82em;
|
|
758
|
+
background: #f0ece3;
|
|
759
|
+
padding: 0.1em 0.3em;
|
|
760
|
+
border-radius: 3px;
|
|
761
|
+
}
|
|
762
|
+
.link-ctx-popup-body pre {
|
|
763
|
+
background: #2d2d2d;
|
|
764
|
+
color: #f0f0f0;
|
|
765
|
+
padding: 0.6rem 0.8rem;
|
|
766
|
+
border-radius: 4px;
|
|
767
|
+
font-size: 0.78rem;
|
|
768
|
+
line-height: 1.4;
|
|
769
|
+
overflow-x: auto;
|
|
770
|
+
margin: 0.4rem 0;
|
|
771
|
+
}
|
|
772
|
+
.link-ctx-popup-body pre code { background: none; padding: 0; font-size: 1em; color: inherit; }
|
|
773
|
+
.link-ctx-popup-body ul, .link-ctx-popup-body ol { padding-left: 1.4rem; margin: 0.3rem 0; }
|
|
774
|
+
.link-ctx-popup-body li { margin-bottom: 0.2rem; }
|
|
775
|
+
.link-ctx-popup-body blockquote {
|
|
776
|
+
border-left: 3px solid #d4b96a;
|
|
777
|
+
margin: 0.5rem 0;
|
|
778
|
+
padding: 0.3rem 0.8rem;
|
|
779
|
+
color: #4a4a4a;
|
|
780
|
+
font-style: italic;
|
|
781
|
+
}
|
|
782
|
+
.link-ctx-popup-body table { border-collapse: collapse; font-size: 0.8rem; margin: 0.5rem 0; }
|
|
783
|
+
.link-ctx-popup-body th, .link-ctx-popup-body td { border: 1px solid #ddd; padding: 0.3rem 0.5rem; }
|
|
784
|
+
.link-ctx-popup-body th { background: #f5f0e4; }
|
|
785
|
+
|
|
681
786
|
/* Link preview popup */
|
|
682
787
|
.link-tooltip-anchor { position: relative; }
|
|
683
788
|
.link-preview-popup {
|
|
@@ -1322,6 +1427,220 @@
|
|
|
1322
1427
|
}, { passive: true });
|
|
1323
1428
|
})();
|
|
1324
1429
|
|
|
1430
|
+
// Right-click / long-press popup for all links
|
|
1431
|
+
(function() {
|
|
1432
|
+
var cache = Object.create(null);
|
|
1433
|
+
var popup = null;
|
|
1434
|
+
var longPressTimer = null;
|
|
1435
|
+
var touchMoved = false;
|
|
1436
|
+
var pendingTouch = null;
|
|
1437
|
+
var longPressHandled = false;
|
|
1438
|
+
|
|
1439
|
+
function escHtml(s) {
|
|
1440
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function findLink(el) {
|
|
1444
|
+
while (el && el.tagName !== 'BODY') {
|
|
1445
|
+
if (el.tagName === 'A' && el.getAttribute('href')) return el;
|
|
1446
|
+
el = el.parentElement;
|
|
1447
|
+
}
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function isAnchorOnly(href) { return href.charAt(0) === '#'; }
|
|
1452
|
+
function isLocalMd(href) {
|
|
1453
|
+
if (/^https?:\/\//i.test(href)) return false;
|
|
1454
|
+
return /\.md([?#]|$)/i.test(href);
|
|
1455
|
+
}
|
|
1456
|
+
function isExternal(href) { return /^https?:\/\//i.test(href); }
|
|
1457
|
+
|
|
1458
|
+
function previewPath(href) {
|
|
1459
|
+
var resolved = new URL(href, location.href);
|
|
1460
|
+
return '/preview/' + resolved.pathname.replace(/^\/browse\//, '');
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function showPopup(x, y, title, bodyHtml) {
|
|
1464
|
+
hidePopup();
|
|
1465
|
+
popup = document.createElement('div');
|
|
1466
|
+
popup.className = 'link-ctx-popup';
|
|
1467
|
+
popup.innerHTML =
|
|
1468
|
+
'<div class="link-ctx-popup-header">' +
|
|
1469
|
+
'<span class="link-ctx-popup-title">' + escHtml(title) + '</span>' +
|
|
1470
|
+
'<button class="link-ctx-popup-close" aria-label="Close">\u00d7</button>' +
|
|
1471
|
+
'</div>' +
|
|
1472
|
+
'<div class="link-ctx-popup-body">' + bodyHtml + '</div>';
|
|
1473
|
+
document.body.appendChild(popup);
|
|
1474
|
+
|
|
1475
|
+
// Position near pointer, clamped to viewport
|
|
1476
|
+
var vw = Math.min(window.innerWidth, document.documentElement.clientWidth);
|
|
1477
|
+
var vh = window.innerHeight;
|
|
1478
|
+
var left = x + 12;
|
|
1479
|
+
var top = y + 12;
|
|
1480
|
+
if (left + popup.offsetWidth > vw - 8) left = Math.max(8, vw - popup.offsetWidth - 8);
|
|
1481
|
+
if (top + popup.offsetHeight > vh - 8) top = Math.max(8, y - popup.offsetHeight - 12);
|
|
1482
|
+
if (top < 8) top = 8;
|
|
1483
|
+
popup.style.left = left + 'px';
|
|
1484
|
+
popup.style.top = top + 'px';
|
|
1485
|
+
|
|
1486
|
+
popup.querySelector('.link-ctx-popup-close').addEventListener('click', hidePopup);
|
|
1487
|
+
popup.addEventListener('click', function(e) { e.stopPropagation(); });
|
|
1488
|
+
popup.addEventListener('contextmenu', function(e) { e.preventDefault(); e.stopPropagation(); });
|
|
1489
|
+
['touchstart','touchmove','touchend'].forEach(function(ev) {
|
|
1490
|
+
popup.addEventListener(ev, function(e) { e.stopPropagation(); }, { passive: true });
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
function updatePopup(bodyHtml, title) {
|
|
1495
|
+
if (!popup) return;
|
|
1496
|
+
var body = popup.querySelector('.link-ctx-popup-body');
|
|
1497
|
+
var titleEl = popup.querySelector('.link-ctx-popup-title');
|
|
1498
|
+
if (body) body.innerHTML = bodyHtml;
|
|
1499
|
+
if (title && titleEl) titleEl.textContent = title;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function hidePopup() {
|
|
1503
|
+
if (popup && popup.parentNode) popup.parentNode.removeChild(popup);
|
|
1504
|
+
popup = null;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function handleLink(anchor, x, y) {
|
|
1508
|
+
var href = anchor.getAttribute('href');
|
|
1509
|
+
if (!href || isAnchorOnly(href)) return;
|
|
1510
|
+
var label = anchor.textContent.trim() || href;
|
|
1511
|
+
|
|
1512
|
+
if (isLocalMd(href)) {
|
|
1513
|
+
var path = previewPath(href);
|
|
1514
|
+
var cached = cache[path];
|
|
1515
|
+
if (cached && typeof cached === 'object') {
|
|
1516
|
+
showPopup(x, y, cached.title || label, cached.html);
|
|
1517
|
+
} else if (cached === false) {
|
|
1518
|
+
showPopup(x, y, label,
|
|
1519
|
+
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
1520
|
+
'<p style="margin:0;color:#888;font-family:sans-serif;font-size:0.82rem">Preview not available</p>');
|
|
1521
|
+
} else {
|
|
1522
|
+
showPopup(x, y, label, '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>');
|
|
1523
|
+
if (cached === undefined) {
|
|
1524
|
+
cache[path] = null;
|
|
1525
|
+
fetch(path)
|
|
1526
|
+
.then(function(r) { return r.ok ? r.json() : null; })
|
|
1527
|
+
.then(function(data) {
|
|
1528
|
+
if (!data) { cache[path] = false; updatePopup('<p style="margin:0;color:#c44;font-family:sans-serif;font-size:0.85rem">Preview unavailable.</p>'); return; }
|
|
1529
|
+
cache[path] = { title: data.title, html: data.html };
|
|
1530
|
+
updatePopup(data.html, data.title || label);
|
|
1531
|
+
})
|
|
1532
|
+
.catch(function() {
|
|
1533
|
+
cache[path] = false;
|
|
1534
|
+
updatePopup('<p style="margin:0;color:#c44;font-family:sans-serif;font-size:0.85rem">Preview unavailable.</p>');
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
} else if (isExternal(href)) {
|
|
1539
|
+
var extKey = 'ext:' + href;
|
|
1540
|
+
var extCached = cache[extKey];
|
|
1541
|
+
if (extCached && typeof extCached === 'object') {
|
|
1542
|
+
showPopup(x, y, extCached.title || label,
|
|
1543
|
+
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
1544
|
+
'<div style="font-family:sans-serif;font-size:0.82rem;line-height:1.55;color:#444;margin-top:0.5rem">' + escHtml(extCached.text) + '</div>');
|
|
1545
|
+
} else if (extCached === false) {
|
|
1546
|
+
showPopup(x, y, label,
|
|
1547
|
+
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
1548
|
+
'<p style="margin:0.5rem 0 0;color:#888;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>');
|
|
1549
|
+
} else {
|
|
1550
|
+
showPopup(x, y, label, '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>');
|
|
1551
|
+
if (extCached === undefined) {
|
|
1552
|
+
cache[extKey] = null;
|
|
1553
|
+
fetch('/fetch?url=' + encodeURIComponent(href))
|
|
1554
|
+
.then(function(r) { return r.ok ? r.json() : null; })
|
|
1555
|
+
.then(function(data) {
|
|
1556
|
+
if (!data || data.error) {
|
|
1557
|
+
cache[extKey] = false;
|
|
1558
|
+
updatePopup(
|
|
1559
|
+
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
1560
|
+
'<p style="margin:0.5rem 0 0;color:#888;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>');
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
cache[extKey] = { title: data.title, text: data.text };
|
|
1564
|
+
updatePopup(
|
|
1565
|
+
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
1566
|
+
'<div style="font-family:sans-serif;font-size:0.82rem;line-height:1.55;color:#444;margin-top:0.5rem">' + escHtml(data.text) + '</div>',
|
|
1567
|
+
data.title || label);
|
|
1568
|
+
})
|
|
1569
|
+
.catch(function() {
|
|
1570
|
+
cache[extKey] = false;
|
|
1571
|
+
updatePopup(
|
|
1572
|
+
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
1573
|
+
'<p style="margin:0.5rem 0 0;color:#c44;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>');
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
} else {
|
|
1578
|
+
showPopup(x, y, label, '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>');
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// Right-click (desktop + most mobile browsers fire contextmenu on long-press)
|
|
1583
|
+
document.addEventListener('contextmenu', function(e) {
|
|
1584
|
+
if (popup && popup.contains(e.target)) { e.preventDefault(); return; }
|
|
1585
|
+
var anchor = findLink(e.target);
|
|
1586
|
+
if (!anchor) return;
|
|
1587
|
+
var href = anchor.getAttribute('href');
|
|
1588
|
+
if (!href || isAnchorOnly(href)) return;
|
|
1589
|
+
e.preventDefault();
|
|
1590
|
+
clearTimeout(longPressTimer);
|
|
1591
|
+
pendingTouch = null;
|
|
1592
|
+
if (!longPressHandled) handleLink(anchor, e.clientX, e.clientY);
|
|
1593
|
+
longPressHandled = false;
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
// Long-press fallback for touch devices that don't fire contextmenu
|
|
1597
|
+
document.addEventListener('touchstart', function(e) {
|
|
1598
|
+
if (e.touches.length !== 1) return;
|
|
1599
|
+
if (popup && popup.contains(e.target)) return;
|
|
1600
|
+
var anchor = findLink(e.target);
|
|
1601
|
+
if (!anchor) return;
|
|
1602
|
+
var href = anchor.getAttribute('href');
|
|
1603
|
+
if (!href || isAnchorOnly(href)) return;
|
|
1604
|
+
touchMoved = false;
|
|
1605
|
+
longPressHandled = false;
|
|
1606
|
+
var t = e.touches[0];
|
|
1607
|
+
pendingTouch = { anchor: anchor, x: t.clientX, y: t.clientY };
|
|
1608
|
+
clearTimeout(longPressTimer);
|
|
1609
|
+
longPressTimer = setTimeout(function() {
|
|
1610
|
+
if (!touchMoved && pendingTouch) {
|
|
1611
|
+
longPressHandled = true;
|
|
1612
|
+
handleLink(pendingTouch.anchor, pendingTouch.x, pendingTouch.y);
|
|
1613
|
+
pendingTouch = null;
|
|
1614
|
+
setTimeout(function() { longPressHandled = false; }, 400);
|
|
1615
|
+
}
|
|
1616
|
+
}, 600);
|
|
1617
|
+
}, { passive: true });
|
|
1618
|
+
|
|
1619
|
+
document.addEventListener('touchmove', function() {
|
|
1620
|
+
touchMoved = true;
|
|
1621
|
+
clearTimeout(longPressTimer);
|
|
1622
|
+
pendingTouch = null;
|
|
1623
|
+
}, { passive: true });
|
|
1624
|
+
|
|
1625
|
+
document.addEventListener('touchend', function() {
|
|
1626
|
+
clearTimeout(longPressTimer);
|
|
1627
|
+
pendingTouch = null;
|
|
1628
|
+
}, { passive: true });
|
|
1629
|
+
|
|
1630
|
+
document.addEventListener('touchcancel', function() {
|
|
1631
|
+
clearTimeout(longPressTimer);
|
|
1632
|
+
pendingTouch = null;
|
|
1633
|
+
longPressHandled = false;
|
|
1634
|
+
}, { passive: true });
|
|
1635
|
+
|
|
1636
|
+
document.addEventListener('click', function(e) {
|
|
1637
|
+
if (popup && !popup.contains(e.target)) hidePopup();
|
|
1638
|
+
});
|
|
1639
|
+
document.addEventListener('keydown', function(e) {
|
|
1640
|
+
if (e.key === 'Escape' && popup) hidePopup();
|
|
1641
|
+
});
|
|
1642
|
+
})();
|
|
1643
|
+
|
|
1325
1644
|
<% if settings.link_tooltips %>
|
|
1326
1645
|
// Link preview popup for local markdown links
|
|
1327
1646
|
(function() {
|