markdownr 0.4.9 → 0.5.1
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 +30 -8
- data/lib/markdown_server/version.rb +1 -1
- data/views/layout.erb +193 -266
- 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: cf955ffcbe5dfc1e1f5070b3fa36a6d2d15a092e7be6ca7f8ac5dcd4b863162a
|
|
4
|
+
data.tar.gz: 3ea514cfed258bc71a8a3e82f6ef883e1ed619fc63b46e9217eb4745c455fdd5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 19f44c77aab4f8a5a2978c85f8fd900f6a0cc45d9399897a26acae80d5809ce57669716a1a5e488c276a4b53e0b2e5d5e3bc13d8b6463a5c8d9c76342eb39e4f
|
|
7
|
+
data.tar.gz: b8adb9e49b1def08d6d3f4061de5048d3cf14232d4ce0c3b9fde51f605f52fb9338a9b39ad49f569d9663fdf9e718e1396f37674e55c7c3f025ffa7a271d3548
|
data/lib/markdown_server/app.rb
CHANGED
|
@@ -491,7 +491,7 @@ 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, " ") }
|
|
@@ -508,12 +508,30 @@ module MarkdownServer
|
|
|
508
508
|
w.match(/<body[^>]*>(.*?)<\/body>/im)&.[](1) ||
|
|
509
509
|
w
|
|
510
510
|
|
|
511
|
-
# Rewrite tags: keep allowed (strip attrs), block→newline, rest→empty
|
|
512
|
-
out = content.gsub(/<(\/?)(\w+)[^>]
|
|
513
|
-
slash, tag = $1, $2.downcase
|
|
514
|
-
if ALLOWED_HTML.include?(tag)
|
|
515
|
-
|
|
516
|
-
|
|
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
|
+
""
|
|
517
535
|
end
|
|
518
536
|
end
|
|
519
537
|
|
|
@@ -533,6 +551,9 @@ module MarkdownServer
|
|
|
533
551
|
.gsub(/<(\w+)>\s*<\/\1>/, "") # drop empty tags
|
|
534
552
|
.strip
|
|
535
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
|
+
|
|
536
557
|
out.length > 10_000 ? out[0, 10_000] : out
|
|
537
558
|
end
|
|
538
559
|
|
|
@@ -627,7 +648,8 @@ module MarkdownServer
|
|
|
627
648
|
html = fetch_external_page(url)
|
|
628
649
|
halt 502, '{"error":"fetch failed"}' unless html
|
|
629
650
|
|
|
630
|
-
|
|
651
|
+
title = page_title(html).sub(/ [-–] .*/, "").strip
|
|
652
|
+
JSON.dump({ title: title, html: page_html(html, url) })
|
|
631
653
|
end
|
|
632
654
|
|
|
633
655
|
get "/search/?*" do
|
data/views/layout.erb
CHANGED
|
@@ -678,7 +678,7 @@
|
|
|
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
|
-
/*
|
|
681
|
+
/* Left-click / tap link context popup */
|
|
682
682
|
.link-ctx-popup {
|
|
683
683
|
position: fixed;
|
|
684
684
|
z-index: 500;
|
|
@@ -705,6 +705,13 @@
|
|
|
705
705
|
background: #faf8f4;
|
|
706
706
|
z-index: 1;
|
|
707
707
|
}
|
|
708
|
+
.link-ctx-popup-title-wrap {
|
|
709
|
+
flex: 1;
|
|
710
|
+
min-width: 0;
|
|
711
|
+
display: flex;
|
|
712
|
+
align-items: center;
|
|
713
|
+
gap: 4px;
|
|
714
|
+
}
|
|
708
715
|
.link-ctx-popup-title {
|
|
709
716
|
font-family: Georgia, "Times New Roman", serif;
|
|
710
717
|
font-weight: 700;
|
|
@@ -712,7 +719,9 @@
|
|
|
712
719
|
color: #3a3a3a;
|
|
713
720
|
min-width: 0;
|
|
714
721
|
word-break: break-word;
|
|
722
|
+
text-decoration: none;
|
|
715
723
|
}
|
|
724
|
+
a.link-ctx-popup-title:hover { color: #8b6914; }
|
|
716
725
|
.link-ctx-popup-close {
|
|
717
726
|
background: none;
|
|
718
727
|
border: none;
|
|
@@ -724,12 +733,28 @@
|
|
|
724
733
|
flex-shrink: 0;
|
|
725
734
|
}
|
|
726
735
|
.link-ctx-popup-close:hover { color: #2c2c2c; }
|
|
736
|
+
.link-ctx-popup-newtab {
|
|
737
|
+
color: #666;
|
|
738
|
+
flex-shrink: 0;
|
|
739
|
+
display: flex;
|
|
740
|
+
align-items: center;
|
|
741
|
+
text-decoration: none;
|
|
742
|
+
padding: 0 0.2rem;
|
|
743
|
+
}
|
|
744
|
+
.link-ctx-popup-newtab:hover { color: #8b6914; }
|
|
745
|
+
.link-ctx-popup-back {
|
|
746
|
+
background: none;
|
|
747
|
+
border: none;
|
|
748
|
+
font-size: 1rem;
|
|
749
|
+
line-height: 1;
|
|
750
|
+
color: #888;
|
|
751
|
+
cursor: pointer;
|
|
752
|
+
padding: 0 0.3rem 0 0;
|
|
753
|
+
flex-shrink: 0;
|
|
754
|
+
}
|
|
755
|
+
.link-ctx-popup-back:hover { color: #2c2c2c; }
|
|
727
756
|
.link-ctx-popup-body {
|
|
728
757
|
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
758
|
}
|
|
734
759
|
.link-ctx-popup-url {
|
|
735
760
|
font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
@@ -741,49 +766,8 @@
|
|
|
741
766
|
border-radius: 4px;
|
|
742
767
|
margin-bottom: 0.5rem;
|
|
743
768
|
}
|
|
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
769
|
|
|
786
|
-
/* Link preview popup */
|
|
770
|
+
/* Link preview popup (hover) */
|
|
787
771
|
.link-tooltip-anchor { position: relative; }
|
|
788
772
|
.link-preview-popup {
|
|
789
773
|
position: absolute;
|
|
@@ -799,10 +783,6 @@
|
|
|
799
783
|
border-radius: 6px;
|
|
800
784
|
box-shadow: 0 4px 20px rgba(0,0,0,0.18);
|
|
801
785
|
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
786
|
cursor: auto;
|
|
807
787
|
-webkit-overflow-scrolling: touch;
|
|
808
788
|
}
|
|
@@ -814,26 +794,36 @@
|
|
|
814
794
|
border-bottom: 1px solid #e0d8c8;
|
|
815
795
|
color: #3a3a3a;
|
|
816
796
|
}
|
|
797
|
+
|
|
798
|
+
/* Shared popup content styles */
|
|
799
|
+
.link-ctx-popup-body,
|
|
800
|
+
.link-preview-popup {
|
|
801
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
802
|
+
font-size: 0.85rem;
|
|
803
|
+
line-height: 1.6;
|
|
804
|
+
color: #2c2c2c;
|
|
805
|
+
}
|
|
806
|
+
.link-ctx-popup-body h1, .link-ctx-popup-body h2, .link-ctx-popup-body h3,
|
|
807
|
+
.link-ctx-popup-body h4, .link-ctx-popup-body h5, .link-ctx-popup-body h6,
|
|
817
808
|
.link-preview-popup h1, .link-preview-popup h2, .link-preview-popup h3,
|
|
818
809
|
.link-preview-popup h4, .link-preview-popup h5, .link-preview-popup h6 {
|
|
819
|
-
margin: 0.7rem 0 0.3rem;
|
|
820
|
-
color: #3a3a3a;
|
|
810
|
+
margin: 0.7rem 0 0.3rem; color: #3a3a3a;
|
|
821
811
|
}
|
|
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 {
|
|
812
|
+
.link-ctx-popup-body h1, .link-preview-popup h1 { font-size: 1.2rem; }
|
|
813
|
+
.link-ctx-popup-body h2, .link-preview-popup h2 { font-size: 1.05rem; border-bottom: none; padding-bottom: 0; }
|
|
814
|
+
.link-ctx-popup-body h3, .link-preview-popup h3 { font-size: 0.95rem; }
|
|
815
|
+
.link-ctx-popup-body p, .link-preview-popup p { margin: 0 0 0.5rem; }
|
|
816
|
+
.link-ctx-popup-body p:last-child, .link-preview-popup p:last-child { margin-bottom: 0; }
|
|
817
|
+
.link-ctx-popup-body a, .link-preview-popup a { color: #8b6914; text-decoration: none; border-bottom: 1px solid #d4b96a; }
|
|
818
|
+
.link-ctx-popup-body a.wiki-link, .link-preview-popup a.wiki-link { color: #6a8e3e; border-bottom-color: #6a8e3e; }
|
|
819
|
+
.link-ctx-popup-body code, .link-preview-popup code {
|
|
830
820
|
font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
831
821
|
font-size: 0.82em;
|
|
832
822
|
background: #f0ece3;
|
|
833
823
|
padding: 0.1em 0.3em;
|
|
834
824
|
border-radius: 3px;
|
|
835
825
|
}
|
|
836
|
-
.link-preview-popup pre {
|
|
826
|
+
.link-ctx-popup-body pre, .link-preview-popup pre {
|
|
837
827
|
background: #2d2d2d;
|
|
838
828
|
color: #f0f0f0;
|
|
839
829
|
padding: 0.6rem 0.8rem;
|
|
@@ -843,26 +833,21 @@
|
|
|
843
833
|
overflow-x: auto;
|
|
844
834
|
margin: 0.4rem 0;
|
|
845
835
|
}
|
|
846
|
-
.link-preview-popup pre code { background: none; padding: 0; font-size: 1em; }
|
|
836
|
+
.link-ctx-popup-body pre code, .link-preview-popup pre code { background: none; padding: 0; font-size: 1em; color: inherit; }
|
|
837
|
+
.link-ctx-popup-body ul, .link-ctx-popup-body ol,
|
|
847
838
|
.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 {
|
|
839
|
+
.link-ctx-popup-body li, .link-preview-popup li { margin-bottom: 0.2rem; }
|
|
840
|
+
.link-ctx-popup-body blockquote, .link-preview-popup blockquote {
|
|
850
841
|
border-left: 3px solid #d4b96a;
|
|
851
842
|
margin: 0.5rem 0;
|
|
852
843
|
padding: 0.3rem 0.8rem;
|
|
853
844
|
color: #4a4a4a;
|
|
854
845
|
font-style: italic;
|
|
855
846
|
}
|
|
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; }
|
|
847
|
+
.link-ctx-popup-body table, .link-preview-popup table { border-collapse: collapse; font-size: 0.8rem; margin: 0.5rem 0; }
|
|
848
|
+
.link-ctx-popup-body th, .link-ctx-popup-body td,
|
|
849
|
+
.link-preview-popup th, .link-preview-popup td { border: 1px solid #ddd; padding: 0.3rem 0.5rem; }
|
|
850
|
+
.link-ctx-popup-body th, .link-preview-popup th { background: #f5f0e4; }
|
|
866
851
|
|
|
867
852
|
/* Footnote tooltips */
|
|
868
853
|
.footnote-tooltip {
|
|
@@ -1427,14 +1412,13 @@
|
|
|
1427
1412
|
}, { passive: true });
|
|
1428
1413
|
})();
|
|
1429
1414
|
|
|
1430
|
-
//
|
|
1415
|
+
// Left-click / tap popup for all links
|
|
1431
1416
|
(function() {
|
|
1432
1417
|
var cache = Object.create(null);
|
|
1433
1418
|
var popup = null;
|
|
1434
|
-
var longPressTimer = null;
|
|
1435
1419
|
var touchMoved = false;
|
|
1436
|
-
var
|
|
1437
|
-
var
|
|
1420
|
+
var historyStack = [];
|
|
1421
|
+
var currentPopupPos = { x: 0, y: 0 };
|
|
1438
1422
|
|
|
1439
1423
|
function escHtml(s) {
|
|
1440
1424
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
@@ -1460,19 +1444,91 @@
|
|
|
1460
1444
|
return '/preview/' + resolved.pathname.replace(/^\/browse\//, '');
|
|
1461
1445
|
}
|
|
1462
1446
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1447
|
+
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>';
|
|
1448
|
+
|
|
1449
|
+
function showPopup(x, y, title, bodyHtml, href) {
|
|
1450
|
+
// Remove old popup without clearing historyStack
|
|
1451
|
+
if (popup && popup.parentNode) popup.parentNode.removeChild(popup);
|
|
1452
|
+
popup = null;
|
|
1453
|
+
currentPopupPos = { x: x, y: y };
|
|
1454
|
+
|
|
1465
1455
|
popup = document.createElement('div');
|
|
1466
1456
|
popup.className = 'link-ctx-popup';
|
|
1457
|
+
var backBtn = historyStack.length > 0 ? '<button class="link-ctx-popup-back" aria-label="Back">\u2190</button>' : '';
|
|
1458
|
+
var titleHtml = href
|
|
1459
|
+
? '<div class="link-ctx-popup-title-wrap">' +
|
|
1460
|
+
'<a class="link-ctx-popup-title" href="' + escHtml(href) + '"><span>' + escHtml(title) + '</span></a>' +
|
|
1461
|
+
'<a class="link-ctx-popup-newtab" href="' + escHtml(href) + '" target="_blank" rel="noopener" title="Open in new tab">' + extLinkIcon + '</a>' +
|
|
1462
|
+
'</div>'
|
|
1463
|
+
: '<span class="link-ctx-popup-title-wrap"><span class="link-ctx-popup-title"><span>' + escHtml(title) + '</span></span></span>';
|
|
1467
1464
|
popup.innerHTML =
|
|
1468
1465
|
'<div class="link-ctx-popup-header">' +
|
|
1469
|
-
|
|
1466
|
+
backBtn + titleHtml +
|
|
1470
1467
|
'<button class="link-ctx-popup-close" aria-label="Close">\u00d7</button>' +
|
|
1471
1468
|
'</div>' +
|
|
1472
1469
|
'<div class="link-ctx-popup-body">' + bodyHtml + '</div>';
|
|
1473
1470
|
document.body.appendChild(popup);
|
|
1474
1471
|
|
|
1475
|
-
|
|
1472
|
+
repositionPopup();
|
|
1473
|
+
|
|
1474
|
+
var backBtnEl = popup.querySelector('.link-ctx-popup-back');
|
|
1475
|
+
if (backBtnEl) {
|
|
1476
|
+
backBtnEl.addEventListener('click', function(e) {
|
|
1477
|
+
e.stopPropagation();
|
|
1478
|
+
var prev = historyStack.pop();
|
|
1479
|
+
if (prev) showPopup(prev.x, prev.y, prev.title, prev.bodyHtml, prev.href);
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
popup.querySelector('.link-ctx-popup-close').addEventListener('click', hidePopup);
|
|
1483
|
+
popup.addEventListener('click', function(e) { e.stopPropagation(); });
|
|
1484
|
+
|
|
1485
|
+
// Links in the popup body push current state to history, then open a new popup
|
|
1486
|
+
// at the same screen position as the current popup
|
|
1487
|
+
var body = popup.querySelector('.link-ctx-popup-body');
|
|
1488
|
+
body.addEventListener('click', function(e) {
|
|
1489
|
+
var anchor = findLink(e.target);
|
|
1490
|
+
if (!anchor) return;
|
|
1491
|
+
var linkHref = anchor.getAttribute('href');
|
|
1492
|
+
if (!linkHref || isAnchorOnly(linkHref)) return;
|
|
1493
|
+
e.stopPropagation();
|
|
1494
|
+
e.preventDefault();
|
|
1495
|
+
var savedPos = { x: currentPopupPos.x, y: currentPopupPos.y };
|
|
1496
|
+
var titleEl = popup.querySelector('.link-ctx-popup-title');
|
|
1497
|
+
historyStack.push({
|
|
1498
|
+
x: savedPos.x, y: savedPos.y,
|
|
1499
|
+
title: titleEl ? titleEl.querySelector('span').textContent : '',
|
|
1500
|
+
bodyHtml: body.innerHTML,
|
|
1501
|
+
href: titleEl ? (titleEl.getAttribute('href') || '') : ''
|
|
1502
|
+
});
|
|
1503
|
+
handleLink(anchor, savedPos.x, savedPos.y, true);
|
|
1504
|
+
});
|
|
1505
|
+
body.addEventListener('touchend', function(e) {
|
|
1506
|
+
if (touchMoved) return;
|
|
1507
|
+
var anchor = findLink(e.target);
|
|
1508
|
+
if (!anchor) return;
|
|
1509
|
+
var linkHref = anchor.getAttribute('href');
|
|
1510
|
+
if (!linkHref || isAnchorOnly(linkHref)) return;
|
|
1511
|
+
e.stopPropagation();
|
|
1512
|
+
e.preventDefault();
|
|
1513
|
+
var savedPos = { x: currentPopupPos.x, y: currentPopupPos.y };
|
|
1514
|
+
var titleEl = popup.querySelector('.link-ctx-popup-title');
|
|
1515
|
+
historyStack.push({
|
|
1516
|
+
x: savedPos.x, y: savedPos.y,
|
|
1517
|
+
title: titleEl ? titleEl.querySelector('span').textContent : '',
|
|
1518
|
+
bodyHtml: body.innerHTML,
|
|
1519
|
+
href: titleEl ? (titleEl.getAttribute('href') || '') : ''
|
|
1520
|
+
});
|
|
1521
|
+
handleLink(anchor, savedPos.x, savedPos.y, true);
|
|
1522
|
+
}, { passive: false });
|
|
1523
|
+
|
|
1524
|
+
['touchstart','touchmove','touchend'].forEach(function(ev) {
|
|
1525
|
+
popup.addEventListener(ev, function(e) { e.stopPropagation(); }, { passive: true });
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function repositionPopup() {
|
|
1530
|
+
if (!popup) return;
|
|
1531
|
+
var x = currentPopupPos.x, y = currentPopupPos.y;
|
|
1476
1532
|
var vw = Math.min(window.innerWidth, document.documentElement.clientWidth);
|
|
1477
1533
|
var vh = window.innerHeight;
|
|
1478
1534
|
var left = x + 12;
|
|
@@ -1482,29 +1538,25 @@
|
|
|
1482
1538
|
if (top < 8) top = 8;
|
|
1483
1539
|
popup.style.left = left + 'px';
|
|
1484
1540
|
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
1541
|
}
|
|
1493
1542
|
|
|
1494
1543
|
function updatePopup(bodyHtml, title) {
|
|
1495
1544
|
if (!popup) return;
|
|
1496
1545
|
var body = popup.querySelector('.link-ctx-popup-body');
|
|
1497
|
-
var
|
|
1546
|
+
var titleTextEl = popup.querySelector('.link-ctx-popup-title span');
|
|
1498
1547
|
if (body) body.innerHTML = bodyHtml;
|
|
1499
|
-
if (title &&
|
|
1548
|
+
if (title && titleTextEl) titleTextEl.textContent = title;
|
|
1549
|
+
repositionPopup();
|
|
1500
1550
|
}
|
|
1501
1551
|
|
|
1502
1552
|
function hidePopup() {
|
|
1503
1553
|
if (popup && popup.parentNode) popup.parentNode.removeChild(popup);
|
|
1504
1554
|
popup = null;
|
|
1555
|
+
historyStack = [];
|
|
1505
1556
|
}
|
|
1506
1557
|
|
|
1507
|
-
function handleLink(anchor, x, y) {
|
|
1558
|
+
function handleLink(anchor, x, y, chained) {
|
|
1559
|
+
if (!chained) historyStack = [];
|
|
1508
1560
|
var href = anchor.getAttribute('href');
|
|
1509
1561
|
if (!href || isAnchorOnly(href)) return;
|
|
1510
1562
|
var label = anchor.textContent.trim() || href;
|
|
@@ -1513,13 +1565,13 @@
|
|
|
1513
1565
|
var path = previewPath(href);
|
|
1514
1566
|
var cached = cache[path];
|
|
1515
1567
|
if (cached && typeof cached === 'object') {
|
|
1516
|
-
showPopup(x, y, cached.title || label, cached.html);
|
|
1568
|
+
showPopup(x, y, cached.title || label, cached.html, href);
|
|
1517
1569
|
} else if (cached === false) {
|
|
1518
1570
|
showPopup(x, y, label,
|
|
1519
1571
|
'<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>');
|
|
1572
|
+
'<p style="margin:0;color:#888;font-family:sans-serif;font-size:0.82rem">Preview not available</p>', href);
|
|
1521
1573
|
} else {
|
|
1522
|
-
showPopup(x, y, label, '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>');
|
|
1574
|
+
showPopup(x, y, label, '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>', href);
|
|
1523
1575
|
if (cached === undefined) {
|
|
1524
1576
|
cache[path] = null;
|
|
1525
1577
|
fetch(path)
|
|
@@ -1539,13 +1591,13 @@
|
|
|
1539
1591
|
var extKey = 'ext:' + href;
|
|
1540
1592
|
var extCached = cache[extKey];
|
|
1541
1593
|
if (extCached && typeof extCached === 'object') {
|
|
1542
|
-
showPopup(x, y, extCached.title || label, extCached.html);
|
|
1594
|
+
showPopup(x, y, extCached.title || label, extCached.html, href);
|
|
1543
1595
|
} else if (extCached === false) {
|
|
1544
1596
|
showPopup(x, y, label,
|
|
1545
1597
|
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
1546
|
-
'<p style="margin:0.5rem 0 0;color:#888;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>');
|
|
1598
|
+
'<p style="margin:0.5rem 0 0;color:#888;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>', href);
|
|
1547
1599
|
} else {
|
|
1548
|
-
showPopup(x, y, label, '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>');
|
|
1600
|
+
showPopup(x, y, label, '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>', href);
|
|
1549
1601
|
if (extCached === undefined) {
|
|
1550
1602
|
cache[extKey] = null;
|
|
1551
1603
|
fetch('/fetch?url=' + encodeURIComponent(href))
|
|
@@ -1570,201 +1622,76 @@
|
|
|
1570
1622
|
}
|
|
1571
1623
|
}
|
|
1572
1624
|
} else {
|
|
1573
|
-
showPopup(x, y, label, '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>');
|
|
1625
|
+
showPopup(x, y, label, '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>', href);
|
|
1574
1626
|
}
|
|
1575
1627
|
}
|
|
1576
1628
|
|
|
1577
|
-
//
|
|
1578
|
-
document.addEventListener('
|
|
1579
|
-
if (popup && popup.contains(e.target))
|
|
1629
|
+
// Left-click on desktop
|
|
1630
|
+
document.addEventListener('click', function(e) {
|
|
1631
|
+
if (popup && popup.contains(e.target)) return;
|
|
1580
1632
|
var anchor = findLink(e.target);
|
|
1581
|
-
if (!anchor) return;
|
|
1633
|
+
if (!anchor) { hidePopup(); return; }
|
|
1582
1634
|
var href = anchor.getAttribute('href');
|
|
1583
|
-
if (!href || isAnchorOnly(href)) return;
|
|
1635
|
+
if (!href || isAnchorOnly(href)) { hidePopup(); return; }
|
|
1636
|
+
if (!anchor.closest('.md-content')) { hidePopup(); return; }
|
|
1584
1637
|
e.preventDefault();
|
|
1585
|
-
|
|
1586
|
-
pendingTouch = null;
|
|
1587
|
-
if (!longPressHandled) handleLink(anchor, e.clientX, e.clientY);
|
|
1588
|
-
longPressHandled = false;
|
|
1638
|
+
handleLink(anchor, e.clientX, e.clientY);
|
|
1589
1639
|
});
|
|
1590
1640
|
|
|
1591
|
-
//
|
|
1641
|
+
// Touch tap
|
|
1592
1642
|
document.addEventListener('touchstart', function(e) {
|
|
1593
1643
|
if (e.touches.length !== 1) return;
|
|
1594
|
-
if (popup && popup.contains(e.target)) return;
|
|
1595
|
-
var anchor = findLink(e.target);
|
|
1596
|
-
if (!anchor) return;
|
|
1597
|
-
var href = anchor.getAttribute('href');
|
|
1598
|
-
if (!href || isAnchorOnly(href)) return;
|
|
1599
1644
|
touchMoved = false;
|
|
1600
|
-
longPressHandled = false;
|
|
1601
|
-
var t = e.touches[0];
|
|
1602
|
-
pendingTouch = { anchor: anchor, x: t.clientX, y: t.clientY };
|
|
1603
|
-
clearTimeout(longPressTimer);
|
|
1604
|
-
longPressTimer = setTimeout(function() {
|
|
1605
|
-
if (!touchMoved && pendingTouch) {
|
|
1606
|
-
longPressHandled = true;
|
|
1607
|
-
handleLink(pendingTouch.anchor, pendingTouch.x, pendingTouch.y);
|
|
1608
|
-
pendingTouch = null;
|
|
1609
|
-
setTimeout(function() { longPressHandled = false; }, 400);
|
|
1610
|
-
}
|
|
1611
|
-
}, 600);
|
|
1612
1645
|
}, { passive: true });
|
|
1613
1646
|
|
|
1614
1647
|
document.addEventListener('touchmove', function() {
|
|
1615
1648
|
touchMoved = true;
|
|
1616
|
-
clearTimeout(longPressTimer);
|
|
1617
|
-
pendingTouch = null;
|
|
1618
1649
|
}, { passive: true });
|
|
1619
1650
|
|
|
1620
|
-
document.addEventListener('touchend', function() {
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1651
|
+
document.addEventListener('touchend', function(e) {
|
|
1652
|
+
if (touchMoved) return;
|
|
1653
|
+
if (popup && popup.contains(e.target)) return;
|
|
1654
|
+
var anchor = findLink(e.target);
|
|
1655
|
+
if (!anchor) { hidePopup(); return; }
|
|
1656
|
+
var href = anchor.getAttribute('href');
|
|
1657
|
+
if (!href || isAnchorOnly(href)) { hidePopup(); return; }
|
|
1658
|
+
if (!anchor.closest('.md-content')) { hidePopup(); return; }
|
|
1659
|
+
e.preventDefault();
|
|
1660
|
+
var touch = e.changedTouches[0];
|
|
1661
|
+
handleLink(anchor, touch.clientX, touch.clientY);
|
|
1662
|
+
}, { passive: false });
|
|
1630
1663
|
|
|
1631
|
-
document.addEventListener('click', function(e) {
|
|
1632
|
-
if (popup && !popup.contains(e.target)) hidePopup();
|
|
1633
|
-
});
|
|
1634
1664
|
document.addEventListener('keydown', function(e) {
|
|
1635
1665
|
if (e.key === 'Escape' && popup) hidePopup();
|
|
1636
1666
|
});
|
|
1637
|
-
})();
|
|
1638
1667
|
|
|
1639
1668
|
<% if settings.link_tooltips %>
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
var cache = Object.create(null);
|
|
1646
|
-
var activePopup = null;
|
|
1647
|
-
var activeAnchor = null;
|
|
1648
|
-
var hideTimer = null;
|
|
1649
|
-
|
|
1650
|
-
function escHtml(s) {
|
|
1651
|
-
return String(s)
|
|
1652
|
-
.replace(/&/g, '&').replace(/</g, '<')
|
|
1653
|
-
.replace(/>/g, '>').replace(/"/g, '"');
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
function isLocalMdLink(a) {
|
|
1657
|
-
if (a.classList.contains('broken')) return false;
|
|
1658
|
-
var href = a.getAttribute('href');
|
|
1659
|
-
if (!href) return false;
|
|
1660
|
-
if (/^https?:\/\//i.test(href)) return false;
|
|
1661
|
-
var path = href.split('?')[0].split('#')[0];
|
|
1662
|
-
return /\.md$/i.test(path);
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
function previewUrl(a) {
|
|
1666
|
-
var resolved = new URL(a.getAttribute('href'), location.href);
|
|
1667
|
-
return '/preview/' + resolved.pathname.replace(/^\/browse\//, '');
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
function showPopup(anchor, html) {
|
|
1671
|
-
hidePopup();
|
|
1672
|
-
var pop = document.createElement('div');
|
|
1673
|
-
pop.className = 'link-preview-popup';
|
|
1674
|
-
pop.innerHTML = html;
|
|
1675
|
-
anchor.appendChild(pop);
|
|
1676
|
-
activePopup = pop;
|
|
1677
|
-
activeAnchor = anchor;
|
|
1678
|
-
|
|
1679
|
-
// Clamp right edge to viewport
|
|
1680
|
-
var vw = Math.min(window.innerWidth, document.documentElement.clientWidth);
|
|
1681
|
-
var rect = pop.getBoundingClientRect();
|
|
1682
|
-
if (rect.right > vw - 8) {
|
|
1683
|
-
pop.style.left = Math.max(0, parseFloat(pop.style.left || 0) - (rect.right - (vw - 8))) + 'px';
|
|
1684
|
-
}
|
|
1685
|
-
// If top goes off screen, flip below the link instead
|
|
1686
|
-
rect = pop.getBoundingClientRect();
|
|
1687
|
-
if (rect.top < 8) {
|
|
1688
|
-
pop.style.bottom = 'auto';
|
|
1689
|
-
pop.style.top = '100%';
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
// Keep popup open while hovering over it
|
|
1693
|
-
pop.addEventListener('mouseenter', function() { clearTimeout(hideTimer); });
|
|
1694
|
-
pop.addEventListener('mouseleave', function() { hideTimer = setTimeout(hidePopup, 10); });
|
|
1695
|
-
|
|
1696
|
-
// Stop clicks/touches inside the popup from triggering the outer link
|
|
1697
|
-
pop.addEventListener('click', function(e) {
|
|
1698
|
-
e.stopPropagation();
|
|
1699
|
-
if (!e.target.closest('a')) e.preventDefault();
|
|
1700
|
-
});
|
|
1701
|
-
pop.addEventListener('touchstart', function(e) { e.stopPropagation(); }, { passive: true });
|
|
1702
|
-
pop.addEventListener('touchmove', function(e) { e.stopPropagation(); }, { passive: true });
|
|
1703
|
-
pop.addEventListener('touchend', function(e) {
|
|
1704
|
-
e.stopPropagation();
|
|
1705
|
-
if (!e.target.closest('a')) e.preventDefault();
|
|
1706
|
-
});
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
function hidePopup() {
|
|
1710
|
-
if (activePopup && activePopup.parentNode) activePopup.parentNode.removeChild(activePopup);
|
|
1711
|
-
activePopup = null;
|
|
1712
|
-
activeAnchor = null;
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
function fetchAndShow(anchor) {
|
|
1716
|
-
var url = previewUrl(anchor);
|
|
1717
|
-
var cached = cache[url];
|
|
1718
|
-
if (cached === undefined) {
|
|
1719
|
-
cache[url] = null; // in-flight
|
|
1720
|
-
showPopup(anchor, '<p style="opacity:0.5;margin:0">Loading\u2026</p>');
|
|
1721
|
-
fetch(url)
|
|
1722
|
-
.then(function(r) { return r.ok ? r.json() : null; })
|
|
1723
|
-
.then(function(data) {
|
|
1724
|
-
if (!data) { cache[url] = false; if (activeAnchor === anchor) hidePopup(); return; }
|
|
1725
|
-
var html = '<div class="link-preview-popup-title">' + escHtml(data.title) + '</div>' + data.html;
|
|
1726
|
-
cache[url] = html;
|
|
1727
|
-
if (activeAnchor === anchor) showPopup(anchor, html);
|
|
1728
|
-
})
|
|
1729
|
-
.catch(function() { cache[url] = false; if (activeAnchor === anchor) hidePopup(); });
|
|
1730
|
-
} else if (typeof cached === 'string') {
|
|
1731
|
-
showPopup(anchor, cached);
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
content.querySelectorAll('a').forEach(function(a) {
|
|
1736
|
-
if (!isLocalMdLink(a)) return;
|
|
1737
|
-
a.classList.add('link-tooltip-anchor');
|
|
1738
|
-
|
|
1669
|
+
// Hover — same popup as click, triggered after a short delay when no popup is active
|
|
1670
|
+
(function() {
|
|
1671
|
+
var content = document.querySelector('.md-content');
|
|
1672
|
+
if (!content) return;
|
|
1739
1673
|
var hoverTimer = null;
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
if (activeAnchor === a && activePopup) return; // second tap → navigate
|
|
1755
|
-
e.preventDefault();
|
|
1756
|
-
document.querySelectorAll('.link-preview-popup').forEach(function(p) {
|
|
1757
|
-
if (p.parentNode) p.parentNode.removeChild(p);
|
|
1674
|
+
content.querySelectorAll('a').forEach(function(a) {
|
|
1675
|
+
var href = a.getAttribute('href');
|
|
1676
|
+
if (!href || isAnchorOnly(href)) return;
|
|
1677
|
+
if (!isLocalMd(href) && !isExternal(href)) return;
|
|
1678
|
+
a.addEventListener('mouseenter', function(e) {
|
|
1679
|
+
clearTimeout(hoverTimer);
|
|
1680
|
+
if (popup) return;
|
|
1681
|
+
var x = e.clientX, y = e.clientY;
|
|
1682
|
+
hoverTimer = setTimeout(function() {
|
|
1683
|
+
if (!popup) handleLink(a, x, y, false);
|
|
1684
|
+
}, 300);
|
|
1685
|
+
});
|
|
1686
|
+
a.addEventListener('mouseleave', function() {
|
|
1687
|
+
clearTimeout(hoverTimer);
|
|
1758
1688
|
});
|
|
1759
|
-
fetchAndShow(a);
|
|
1760
1689
|
});
|
|
1761
|
-
});
|
|
1690
|
+
})();
|
|
1691
|
+
<% end %>
|
|
1762
1692
|
|
|
1763
|
-
document.addEventListener('click', function(e) {
|
|
1764
|
-
if (activePopup && activeAnchor && !activeAnchor.contains(e.target)) hidePopup();
|
|
1765
|
-
});
|
|
1766
1693
|
})();
|
|
1767
|
-
|
|
1694
|
+
|
|
1768
1695
|
|
|
1769
1696
|
</script>
|
|
1770
1697
|
</body>
|