markdownr 0.4.8 → 0.5.0
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 +36 -8
- data/lib/markdown_server/version.rb +1 -1
- data/views/layout.erb +113 -117
- 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: 03b8c1c4665231a3fb1ead89abea54e0630131743216938a1c293945c942e710
|
|
4
|
+
data.tar.gz: 004d43321fcda1b894c6481eed4b755f29a9fae14c6e0f6bbb5c1f9d82b6afd9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d44c7b65617670e54d450d5d958294bd78914407cf925087f1547b551b5b91dd3b556820009aa197491bdd14bff9fc6e205a47bf6662fb5b5a495d8e5d8c71d
|
|
7
|
+
data.tar.gz: 07f03bc67cc2b668c90c1c9467eb7eafe60f2c58c4030553adbec8af4309bdbf3943c9ffdd454cdde0283619f702c6a7ef75992ed679e84983557c5ed5fc3675
|
data/lib/markdown_server/app.rb
CHANGED
|
@@ -491,13 +491,16 @@ module MarkdownServer
|
|
|
491
491
|
} || ""
|
|
492
492
|
end
|
|
493
493
|
|
|
494
|
-
def page_html(raw)
|
|
494
|
+
def page_html(raw, base_url = nil)
|
|
495
495
|
w = raw.dup
|
|
496
496
|
# Remove inert elements and their entire contents
|
|
497
497
|
STRIP_FULL.each { |t| w.gsub!(/<#{t}[^>]*>.*?<\/#{t}>/im, " ") }
|
|
498
498
|
w.gsub!(/<!--.*?-->/m, " ")
|
|
499
499
|
# Remove known ad/recommendation blocks
|
|
500
500
|
w.gsub!(/Bible\s+Gateway\s+Recommends[\s\S]*?View\s+more\s+titles/i, " ")
|
|
501
|
+
# Remove BibleGateway header/toolbar noise (promo text + login/toolbar block)
|
|
502
|
+
w.gsub!(/trusted\s+resources\s+beside\s+every\s+verse[\s\S]*?Your\s+Content/i, " ")
|
|
503
|
+
w.gsub!(/Log\s+In\s*\/\s*Sign\s+Up[\s\S]*?Your\s+Content/i, " ")
|
|
501
504
|
|
|
502
505
|
# Prefer a focused content block
|
|
503
506
|
content = w.match(/<article[^>]*>(.*?)<\/article>/im)&.[](1) ||
|
|
@@ -505,15 +508,36 @@ module MarkdownServer
|
|
|
505
508
|
w.match(/<body[^>]*>(.*?)<\/body>/im)&.[](1) ||
|
|
506
509
|
w
|
|
507
510
|
|
|
508
|
-
# Rewrite tags: keep allowed (strip attrs), block→newline, rest→empty
|
|
509
|
-
out = content.gsub(/<(\/?)(\w+)[^>]
|
|
510
|
-
slash, tag = $1, $2.downcase
|
|
511
|
-
if ALLOWED_HTML.include?(tag)
|
|
512
|
-
|
|
513
|
-
|
|
511
|
+
# Rewrite tags: keep allowed (strip attrs), preserve <a href>, block→newline, rest→empty
|
|
512
|
+
out = content.gsub(/<(\/?)(\w+)([^>]*)>/) do
|
|
513
|
+
slash, tag, attrs = $1, $2.downcase, $3
|
|
514
|
+
if ALLOWED_HTML.include?(tag)
|
|
515
|
+
"<#{slash}#{tag}>"
|
|
516
|
+
elsif tag == "a"
|
|
517
|
+
if slash.empty?
|
|
518
|
+
m = attrs.match(/href\s*=\s*["']([^"']*)["']/i)
|
|
519
|
+
if m && !m[1].match?(/\Ajavascript:/i)
|
|
520
|
+
href = m[1]
|
|
521
|
+
if base_url && href !~ /\Ahttps?:\/\//i && !href.start_with?("#")
|
|
522
|
+
href = (URI.join(base_url, href).to_s rescue href)
|
|
523
|
+
end
|
|
524
|
+
%(<a href="#{h(href)}" target="_blank" rel="noopener">)
|
|
525
|
+
else
|
|
526
|
+
""
|
|
527
|
+
end
|
|
528
|
+
else
|
|
529
|
+
"</a>"
|
|
530
|
+
end
|
|
531
|
+
elsif BLOCK_HTML.include?(tag)
|
|
532
|
+
"\n"
|
|
533
|
+
else
|
|
534
|
+
""
|
|
514
535
|
end
|
|
515
536
|
end
|
|
516
537
|
|
|
538
|
+
# Strip any preamble before the first heading (site chrome, toolbars, etc.)
|
|
539
|
+
out.sub!(/\A[\s\S]*?(?=<h[1-6]>)/i, "")
|
|
540
|
+
|
|
517
541
|
# Decode HTML entities
|
|
518
542
|
out = out
|
|
519
543
|
.gsub(/ /i, " ").gsub(/&/i, "&").gsub(/</i, "<").gsub(/>/i, ">")
|
|
@@ -527,6 +551,9 @@ module MarkdownServer
|
|
|
527
551
|
.gsub(/<(\w+)>\s*<\/\1>/, "") # drop empty tags
|
|
528
552
|
.strip
|
|
529
553
|
|
|
554
|
+
# Strip all footer navigation after "Read full chapter" up to (but not including) copyright
|
|
555
|
+
out.gsub!(/(<a[^>]*>Read\s+full\s+chapter<\/a>)[\s\S]*?(?=©|Copyright\b)/i, "\\1\n")
|
|
556
|
+
|
|
530
557
|
out.length > 10_000 ? out[0, 10_000] : out
|
|
531
558
|
end
|
|
532
559
|
|
|
@@ -621,7 +648,8 @@ module MarkdownServer
|
|
|
621
648
|
html = fetch_external_page(url)
|
|
622
649
|
halt 502, '{"error":"fetch failed"}' unless html
|
|
623
650
|
|
|
624
|
-
|
|
651
|
+
title = page_title(html).sub(/ [-–] .*/, "").strip
|
|
652
|
+
JSON.dump({ title: title, html: page_html(html, url) })
|
|
625
653
|
end
|
|
626
654
|
|
|
627
655
|
get "/search/?*" do
|
data/views/layout.erb
CHANGED
|
@@ -726,10 +726,6 @@
|
|
|
726
726
|
.link-ctx-popup-close:hover { color: #2c2c2c; }
|
|
727
727
|
.link-ctx-popup-body {
|
|
728
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
729
|
}
|
|
734
730
|
.link-ctx-popup-url {
|
|
735
731
|
font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
@@ -741,49 +737,8 @@
|
|
|
741
737
|
border-radius: 4px;
|
|
742
738
|
margin-bottom: 0.5rem;
|
|
743
739
|
}
|
|
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
740
|
|
|
786
|
-
/* Link preview popup */
|
|
741
|
+
/* Link preview popup (hover) */
|
|
787
742
|
.link-tooltip-anchor { position: relative; }
|
|
788
743
|
.link-preview-popup {
|
|
789
744
|
position: absolute;
|
|
@@ -799,10 +754,6 @@
|
|
|
799
754
|
border-radius: 6px;
|
|
800
755
|
box-shadow: 0 4px 20px rgba(0,0,0,0.18);
|
|
801
756
|
padding: 0.8rem 1rem;
|
|
802
|
-
font-family: Georgia, "Times New Roman", serif;
|
|
803
|
-
font-size: 0.85rem;
|
|
804
|
-
line-height: 1.6;
|
|
805
|
-
color: #2c2c2c;
|
|
806
757
|
cursor: auto;
|
|
807
758
|
-webkit-overflow-scrolling: touch;
|
|
808
759
|
}
|
|
@@ -814,26 +765,36 @@
|
|
|
814
765
|
border-bottom: 1px solid #e0d8c8;
|
|
815
766
|
color: #3a3a3a;
|
|
816
767
|
}
|
|
768
|
+
|
|
769
|
+
/* Shared popup content styles */
|
|
770
|
+
.link-ctx-popup-body,
|
|
771
|
+
.link-preview-popup {
|
|
772
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
773
|
+
font-size: 0.85rem;
|
|
774
|
+
line-height: 1.6;
|
|
775
|
+
color: #2c2c2c;
|
|
776
|
+
}
|
|
777
|
+
.link-ctx-popup-body h1, .link-ctx-popup-body h2, .link-ctx-popup-body h3,
|
|
778
|
+
.link-ctx-popup-body h4, .link-ctx-popup-body h5, .link-ctx-popup-body h6,
|
|
817
779
|
.link-preview-popup h1, .link-preview-popup h2, .link-preview-popup h3,
|
|
818
780
|
.link-preview-popup h4, .link-preview-popup h5, .link-preview-popup h6 {
|
|
819
|
-
margin: 0.7rem 0 0.3rem;
|
|
820
|
-
color: #3a3a3a;
|
|
781
|
+
margin: 0.7rem 0 0.3rem; color: #3a3a3a;
|
|
821
782
|
}
|
|
822
|
-
.link-preview-popup h1 { font-size: 1.2rem; }
|
|
823
|
-
.link-preview-popup h2 { font-size: 1.05rem; border-bottom: none; padding-bottom: 0; }
|
|
824
|
-
.link-preview-popup h3 { font-size: 0.95rem; }
|
|
825
|
-
.link-preview-popup p { margin: 0 0 0.5rem; }
|
|
826
|
-
.link-preview-popup p:last-child { margin-bottom: 0; }
|
|
827
|
-
.link-preview-popup a { color: #8b6914; text-decoration: none; border-bottom: 1px solid #d4b96a; }
|
|
828
|
-
.link-preview-popup a.wiki-link { color: #6a8e3e; border-bottom-color: #6a8e3e; }
|
|
829
|
-
.link-preview-popup code {
|
|
783
|
+
.link-ctx-popup-body h1, .link-preview-popup h1 { font-size: 1.2rem; }
|
|
784
|
+
.link-ctx-popup-body h2, .link-preview-popup h2 { font-size: 1.05rem; border-bottom: none; padding-bottom: 0; }
|
|
785
|
+
.link-ctx-popup-body h3, .link-preview-popup h3 { font-size: 0.95rem; }
|
|
786
|
+
.link-ctx-popup-body p, .link-preview-popup p { margin: 0 0 0.5rem; }
|
|
787
|
+
.link-ctx-popup-body p:last-child, .link-preview-popup p:last-child { margin-bottom: 0; }
|
|
788
|
+
.link-ctx-popup-body a, .link-preview-popup a { color: #8b6914; text-decoration: none; border-bottom: 1px solid #d4b96a; }
|
|
789
|
+
.link-ctx-popup-body a.wiki-link, .link-preview-popup a.wiki-link { color: #6a8e3e; border-bottom-color: #6a8e3e; }
|
|
790
|
+
.link-ctx-popup-body code, .link-preview-popup code {
|
|
830
791
|
font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
831
792
|
font-size: 0.82em;
|
|
832
793
|
background: #f0ece3;
|
|
833
794
|
padding: 0.1em 0.3em;
|
|
834
795
|
border-radius: 3px;
|
|
835
796
|
}
|
|
836
|
-
.link-preview-popup pre {
|
|
797
|
+
.link-ctx-popup-body pre, .link-preview-popup pre {
|
|
837
798
|
background: #2d2d2d;
|
|
838
799
|
color: #f0f0f0;
|
|
839
800
|
padding: 0.6rem 0.8rem;
|
|
@@ -843,26 +804,21 @@
|
|
|
843
804
|
overflow-x: auto;
|
|
844
805
|
margin: 0.4rem 0;
|
|
845
806
|
}
|
|
846
|
-
.link-preview-popup pre code { background: none; padding: 0; font-size: 1em; }
|
|
807
|
+
.link-ctx-popup-body pre code, .link-preview-popup pre code { background: none; padding: 0; font-size: 1em; color: inherit; }
|
|
808
|
+
.link-ctx-popup-body ul, .link-ctx-popup-body ol,
|
|
847
809
|
.link-preview-popup ul, .link-preview-popup ol { padding-left: 1.4rem; margin: 0.3rem 0; }
|
|
848
|
-
.link-preview-popup li { margin-bottom: 0.2rem; }
|
|
849
|
-
.link-preview-popup blockquote {
|
|
810
|
+
.link-ctx-popup-body li, .link-preview-popup li { margin-bottom: 0.2rem; }
|
|
811
|
+
.link-ctx-popup-body blockquote, .link-preview-popup blockquote {
|
|
850
812
|
border-left: 3px solid #d4b96a;
|
|
851
813
|
margin: 0.5rem 0;
|
|
852
814
|
padding: 0.3rem 0.8rem;
|
|
853
815
|
color: #4a4a4a;
|
|
854
816
|
font-style: italic;
|
|
855
817
|
}
|
|
856
|
-
.link-preview-popup table {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
}
|
|
861
|
-
.link-preview-popup th, .link-preview-popup td {
|
|
862
|
-
border: 1px solid #ddd;
|
|
863
|
-
padding: 0.3rem 0.5rem;
|
|
864
|
-
}
|
|
865
|
-
.link-preview-popup th { background: #f5f0e4; }
|
|
818
|
+
.link-ctx-popup-body table, .link-preview-popup table { border-collapse: collapse; font-size: 0.8rem; margin: 0.5rem 0; }
|
|
819
|
+
.link-ctx-popup-body th, .link-ctx-popup-body td,
|
|
820
|
+
.link-preview-popup th, .link-preview-popup td { border: 1px solid #ddd; padding: 0.3rem 0.5rem; }
|
|
821
|
+
.link-ctx-popup-body th, .link-preview-popup th { background: #f5f0e4; }
|
|
866
822
|
|
|
867
823
|
/* Footnote tooltips */
|
|
868
824
|
.footnote-tooltip {
|
|
@@ -1539,9 +1495,7 @@
|
|
|
1539
1495
|
var extKey = 'ext:' + href;
|
|
1540
1496
|
var extCached = cache[extKey];
|
|
1541
1497
|
if (extCached && typeof extCached === 'object') {
|
|
1542
|
-
showPopup(x, y, extCached.title || label,
|
|
1543
|
-
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
1544
|
-
extCached.html);
|
|
1498
|
+
showPopup(x, y, extCached.title || label, extCached.html);
|
|
1545
1499
|
} else if (extCached === false) {
|
|
1546
1500
|
showPopup(x, y, label,
|
|
1547
1501
|
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
@@ -1561,10 +1515,7 @@
|
|
|
1561
1515
|
return;
|
|
1562
1516
|
}
|
|
1563
1517
|
cache[extKey] = { title: data.title, html: data.html };
|
|
1564
|
-
updatePopup(
|
|
1565
|
-
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
1566
|
-
data.html,
|
|
1567
|
-
data.title || label);
|
|
1518
|
+
updatePopup(data.html, data.title || label);
|
|
1568
1519
|
})
|
|
1569
1520
|
.catch(function() {
|
|
1570
1521
|
cache[extKey] = false;
|
|
@@ -1639,10 +1590,11 @@
|
|
|
1639
1590
|
document.addEventListener('keydown', function(e) {
|
|
1640
1591
|
if (e.key === 'Escape' && popup) hidePopup();
|
|
1641
1592
|
});
|
|
1593
|
+
|
|
1642
1594
|
})();
|
|
1643
1595
|
|
|
1644
1596
|
<% if settings.link_tooltips %>
|
|
1645
|
-
// Link preview popup for local markdown links
|
|
1597
|
+
// Link preview popup for local markdown links and external links
|
|
1646
1598
|
(function() {
|
|
1647
1599
|
var content = document.querySelector('.md-content');
|
|
1648
1600
|
if (!content) return;
|
|
@@ -1667,48 +1619,79 @@
|
|
|
1667
1619
|
return /\.md$/i.test(path);
|
|
1668
1620
|
}
|
|
1669
1621
|
|
|
1622
|
+
function isExternalLink(a) {
|
|
1623
|
+
var href = a.getAttribute('href');
|
|
1624
|
+
return href && /^https?:\/\//i.test(href);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1670
1627
|
function previewUrl(a) {
|
|
1671
1628
|
var resolved = new URL(a.getAttribute('href'), location.href);
|
|
1672
1629
|
return '/preview/' + resolved.pathname.replace(/^\/browse\//, '');
|
|
1673
1630
|
}
|
|
1674
1631
|
|
|
1632
|
+
// Returns true if el has an ancestor that clips overflow (prevents absolute children escaping)
|
|
1633
|
+
function hasOverflowClip(el) {
|
|
1634
|
+
var parent = el.parentElement;
|
|
1635
|
+
while (parent && parent !== document.body) {
|
|
1636
|
+
var s = window.getComputedStyle(parent);
|
|
1637
|
+
if (s.overflow !== 'visible' || s.overflowX !== 'visible' || s.overflowY !== 'visible') return true;
|
|
1638
|
+
parent = parent.parentElement;
|
|
1639
|
+
}
|
|
1640
|
+
return false;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1675
1643
|
function showPopup(anchor, html) {
|
|
1676
1644
|
hidePopup();
|
|
1677
1645
|
var pop = document.createElement('div');
|
|
1678
1646
|
pop.className = 'link-preview-popup';
|
|
1679
1647
|
pop.innerHTML = html;
|
|
1680
|
-
anchor.appendChild(pop);
|
|
1681
1648
|
activePopup = pop;
|
|
1682
1649
|
activeAnchor = anchor;
|
|
1683
1650
|
|
|
1684
|
-
// Clamp right edge to viewport
|
|
1685
1651
|
var vw = Math.min(window.innerWidth, document.documentElement.clientWidth);
|
|
1686
|
-
var
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1652
|
+
var vh = window.innerHeight;
|
|
1653
|
+
|
|
1654
|
+
if (hasOverflowClip(anchor)) {
|
|
1655
|
+
// Anchor is inside an overflow container — use fixed positioning to escape clipping
|
|
1656
|
+
pop.style.position = 'fixed';
|
|
1657
|
+
pop.style.bottom = 'auto'; // override the CSS class's bottom: 100%
|
|
1658
|
+
document.body.appendChild(pop);
|
|
1659
|
+
var aRect = anchor.getBoundingClientRect();
|
|
1660
|
+
var popH = pop.offsetHeight;
|
|
1661
|
+
var popW = pop.offsetWidth;
|
|
1662
|
+
// Prefer above the link, fall back to below
|
|
1663
|
+
var top = aRect.top - popH - 4;
|
|
1664
|
+
if (top < 8) top = aRect.bottom + 4;
|
|
1665
|
+
if (top + popH > vh - 8) top = Math.max(8, vh - popH - 8);
|
|
1666
|
+
var left = aRect.left;
|
|
1667
|
+
if (left + popW > vw - 8) left = Math.max(8, vw - popW - 8);
|
|
1668
|
+
pop.style.top = top + 'px';
|
|
1669
|
+
pop.style.left = left + 'px';
|
|
1670
|
+
} else {
|
|
1671
|
+
// Normal anchor-attached positioning
|
|
1672
|
+
anchor.appendChild(pop);
|
|
1673
|
+
var rect = pop.getBoundingClientRect();
|
|
1674
|
+
var shift = 0;
|
|
1675
|
+
if (rect.right > vw - 8) shift = (vw - 8) - rect.right;
|
|
1676
|
+
if (rect.left + shift < 8) shift = 8 - rect.left;
|
|
1677
|
+
if (shift !== 0) pop.style.left = (parseFloat(pop.style.left || 0) + shift) + 'px';
|
|
1678
|
+
// If top goes off screen, flip below the link instead
|
|
1679
|
+
rect = pop.getBoundingClientRect();
|
|
1680
|
+
if (rect.top < 8) {
|
|
1681
|
+
pop.style.bottom = 'auto';
|
|
1682
|
+
pop.style.top = '100%';
|
|
1683
|
+
}
|
|
1695
1684
|
}
|
|
1696
1685
|
|
|
1697
1686
|
// Keep popup open while hovering over it
|
|
1698
1687
|
pop.addEventListener('mouseenter', function() { clearTimeout(hideTimer); });
|
|
1699
1688
|
pop.addEventListener('mouseleave', function() { hideTimer = setTimeout(hidePopup, 10); });
|
|
1700
1689
|
|
|
1701
|
-
// Stop clicks
|
|
1690
|
+
// Stop clicks inside the popup from triggering the outer link
|
|
1702
1691
|
pop.addEventListener('click', function(e) {
|
|
1703
1692
|
e.stopPropagation();
|
|
1704
1693
|
if (!e.target.closest('a')) e.preventDefault();
|
|
1705
1694
|
});
|
|
1706
|
-
pop.addEventListener('touchstart', function(e) { e.stopPropagation(); }, { passive: true });
|
|
1707
|
-
pop.addEventListener('touchmove', function(e) { e.stopPropagation(); }, { passive: true });
|
|
1708
|
-
pop.addEventListener('touchend', function(e) {
|
|
1709
|
-
e.stopPropagation();
|
|
1710
|
-
if (!e.target.closest('a')) e.preventDefault();
|
|
1711
|
-
});
|
|
1712
1695
|
}
|
|
1713
1696
|
|
|
1714
1697
|
function hidePopup() {
|
|
@@ -1737,36 +1720,49 @@
|
|
|
1737
1720
|
}
|
|
1738
1721
|
}
|
|
1739
1722
|
|
|
1723
|
+
function fetchExternalAndShow(anchor) {
|
|
1724
|
+
var href = anchor.getAttribute('href');
|
|
1725
|
+
var key = 'ext:' + href;
|
|
1726
|
+
var cached = cache[key];
|
|
1727
|
+
if (cached === undefined) {
|
|
1728
|
+
cache[key] = null; // in-flight
|
|
1729
|
+
showPopup(anchor, '<p style="opacity:0.5;margin:0">Loading\u2026</p>');
|
|
1730
|
+
fetch('/fetch?url=' + encodeURIComponent(href))
|
|
1731
|
+
.then(function(r) { return r.ok ? r.json() : null; })
|
|
1732
|
+
.then(function(data) {
|
|
1733
|
+
if (!data || data.error) { cache[key] = false; if (activeAnchor === anchor) hidePopup(); return; }
|
|
1734
|
+
var html = '<div class="link-preview-popup-title">' + escHtml(data.title) + '</div>' + data.html;
|
|
1735
|
+
cache[key] = html;
|
|
1736
|
+
if (activeAnchor === anchor) showPopup(anchor, html);
|
|
1737
|
+
})
|
|
1738
|
+
.catch(function() { cache[key] = false; if (activeAnchor === anchor) hidePopup(); });
|
|
1739
|
+
} else if (typeof cached === 'string') {
|
|
1740
|
+
showPopup(anchor, cached);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1740
1744
|
content.querySelectorAll('a').forEach(function(a) {
|
|
1741
|
-
|
|
1745
|
+
var localMd = isLocalMdLink(a);
|
|
1746
|
+
var external = isExternalLink(a);
|
|
1747
|
+
if (!localMd && !external) return;
|
|
1742
1748
|
a.classList.add('link-tooltip-anchor');
|
|
1743
1749
|
|
|
1744
1750
|
var hoverTimer = null;
|
|
1745
1751
|
a.addEventListener('click', hidePopup);
|
|
1746
1752
|
a.addEventListener('mouseenter', function() {
|
|
1747
|
-
hoverTimer = setTimeout(function() {
|
|
1753
|
+
hoverTimer = setTimeout(function() {
|
|
1754
|
+
if (localMd) fetchAndShow(a);
|
|
1755
|
+
else fetchExternalAndShow(a);
|
|
1756
|
+
}, 300);
|
|
1748
1757
|
});
|
|
1749
1758
|
a.addEventListener('mouseleave', function() {
|
|
1750
1759
|
clearTimeout(hoverTimer);
|
|
1751
|
-
hideTimer = setTimeout(hidePopup,
|
|
1752
|
-
});
|
|
1753
|
-
|
|
1754
|
-
var touchMoved = false;
|
|
1755
|
-
a.addEventListener('touchstart', function() { touchMoved = false; }, { passive: true });
|
|
1756
|
-
a.addEventListener('touchmove', function() { touchMoved = true; }, { passive: true });
|
|
1757
|
-
a.addEventListener('touchend', function(e) {
|
|
1758
|
-
if (touchMoved) return;
|
|
1759
|
-
if (activeAnchor === a && activePopup) return; // second tap → navigate
|
|
1760
|
-
e.preventDefault();
|
|
1761
|
-
document.querySelectorAll('.link-preview-popup').forEach(function(p) {
|
|
1762
|
-
if (p.parentNode) p.parentNode.removeChild(p);
|
|
1763
|
-
});
|
|
1764
|
-
fetchAndShow(a);
|
|
1760
|
+
hideTimer = setTimeout(hidePopup, 150);
|
|
1765
1761
|
});
|
|
1766
1762
|
});
|
|
1767
1763
|
|
|
1768
1764
|
document.addEventListener('click', function(e) {
|
|
1769
|
-
if (activePopup && activeAnchor && !activeAnchor.contains(e.target)) hidePopup();
|
|
1765
|
+
if (activePopup && !activePopup.contains(e.target) && activeAnchor && !activeAnchor.contains(e.target)) hidePopup();
|
|
1770
1766
|
});
|
|
1771
1767
|
})();
|
|
1772
1768
|
<% end %>
|