markdownr 0.5.0 → 0.5.2
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 +167 -240
- 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: e91138e164e026f52b6957f44693eb86d75b79eda7e4404d094f1ea0d4e35d26
|
|
4
|
+
data.tar.gz: b549ff9d07b45f43cc1639c2b4a47171f3a38bb76e2e98c1bb0ffe03ddc53fbe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cfc1004e65a89e09263f3a35f2da2b3b354afe8351efa87e49194b0aec3bef5aaeedd7d4aba70f00c01ebca6468150a296be2bdad0d8ca238f87b5bc73ebca0d
|
|
7
|
+
data.tar.gz: c0cc3d73580b42c40c3f9244ed5acc69a74363237c54af0d21877bf4f1a64d2b642150fc4c384d13a6efb74a1bf86f12819d10b913f935ff60298e1e055e8460
|
data/lib/markdown_server/app.rb
CHANGED
|
@@ -180,15 +180,33 @@ module MarkdownServer
|
|
|
180
180
|
def resolve_wiki_link(name)
|
|
181
181
|
filename = "#{name}.md"
|
|
182
182
|
base = File.realpath(root_dir)
|
|
183
|
+
|
|
184
|
+
# Check the current file's directory first (exact case, then case-insensitive)
|
|
185
|
+
if @current_wiki_dir
|
|
186
|
+
local_exact = nil
|
|
187
|
+
local_ci = nil
|
|
188
|
+
Dir.glob(File.join(@current_wiki_dir, filename), File::FNM_CASEFOLD).each do |path|
|
|
189
|
+
real = File.realpath(path) rescue next
|
|
190
|
+
next unless real.start_with?(base)
|
|
191
|
+
relative = real.sub("#{base}/", "")
|
|
192
|
+
first_segment = relative.split("/").first
|
|
193
|
+
next if EXCLUDED.include?(first_segment) || first_segment&.start_with?(".")
|
|
194
|
+
if File.basename(real) == filename
|
|
195
|
+
local_exact = relative
|
|
196
|
+
break
|
|
197
|
+
else
|
|
198
|
+
local_ci ||= relative
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
return local_exact if local_exact
|
|
202
|
+
return local_ci if local_ci
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Fall back to global recursive search
|
|
183
206
|
exact_match = nil
|
|
184
207
|
ci_match = nil
|
|
185
|
-
|
|
186
208
|
Dir.glob(File.join(base, "**", filename), File::FNM_CASEFOLD).each do |path|
|
|
187
|
-
|
|
188
|
-
real = File.realpath(path)
|
|
189
|
-
rescue Errno::ENOENT
|
|
190
|
-
next
|
|
191
|
-
end
|
|
209
|
+
real = File.realpath(path) rescue next
|
|
192
210
|
next unless real.start_with?(base)
|
|
193
211
|
relative = real.sub("#{base}/", "")
|
|
194
212
|
first_segment = relative.split("/").first
|
|
@@ -517,7 +535,7 @@ module MarkdownServer
|
|
|
517
535
|
if slash.empty?
|
|
518
536
|
m = attrs.match(/href\s*=\s*["']([^"']*)["']/i)
|
|
519
537
|
if m && !m[1].match?(/\Ajavascript:/i)
|
|
520
|
-
href = m[1]
|
|
538
|
+
href = m[1].gsub(/&/i, "&")
|
|
521
539
|
if base_url && href !~ /\Ahttps?:\/\//i && !href.start_with?("#")
|
|
522
540
|
href = (URI.join(base_url, href).to_s rescue href)
|
|
523
541
|
end
|
|
@@ -635,9 +653,18 @@ module MarkdownServer
|
|
|
635
653
|
|
|
636
654
|
meta, body = parse_frontmatter(content)
|
|
637
655
|
title = (meta.is_a?(Hash) && meta["title"]) || File.basename(real, ".md")
|
|
656
|
+
@current_wiki_dir = File.dirname(real)
|
|
638
657
|
html = render_markdown(body)
|
|
639
658
|
|
|
640
|
-
|
|
659
|
+
frontmatter_html = ""
|
|
660
|
+
if meta && !meta.empty?
|
|
661
|
+
rows = meta.map { |key, value|
|
|
662
|
+
"<tr><th>#{h(key)}</th><td>#{render_frontmatter_value(value)}</td></tr>"
|
|
663
|
+
}.join
|
|
664
|
+
frontmatter_html = %(<div class="frontmatter"><div class="frontmatter-heading">Frontmatter</div><table class="meta-table">#{rows}</table></div>)
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
JSON.dump({ title: title.to_s, html: html, frontmatter_html: frontmatter_html })
|
|
641
668
|
end
|
|
642
669
|
|
|
643
670
|
get "/fetch" do
|
|
@@ -748,6 +775,7 @@ module MarkdownServer
|
|
|
748
775
|
when ".md"
|
|
749
776
|
content = File.read(real_path, encoding: "utf-8")
|
|
750
777
|
@meta, body = parse_frontmatter(content)
|
|
778
|
+
@current_wiki_dir = File.dirname(real_path)
|
|
751
779
|
@content = render_markdown(body)
|
|
752
780
|
@toc = extract_toc(@content)
|
|
753
781
|
@has_toc = @toc.length > 1
|
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,6 +733,26 @@
|
|
|
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
758
|
}
|
|
@@ -1383,14 +1412,13 @@
|
|
|
1383
1412
|
}, { passive: true });
|
|
1384
1413
|
})();
|
|
1385
1414
|
|
|
1386
|
-
//
|
|
1415
|
+
// Left-click / tap popup for all links
|
|
1387
1416
|
(function() {
|
|
1388
1417
|
var cache = Object.create(null);
|
|
1389
1418
|
var popup = null;
|
|
1390
|
-
var longPressTimer = null;
|
|
1391
1419
|
var touchMoved = false;
|
|
1392
|
-
var
|
|
1393
|
-
var
|
|
1420
|
+
var historyStack = [];
|
|
1421
|
+
var currentPopupPos = { x: 0, y: 0 };
|
|
1394
1422
|
|
|
1395
1423
|
function escHtml(s) {
|
|
1396
1424
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
@@ -1416,19 +1444,91 @@
|
|
|
1416
1444
|
return '/preview/' + resolved.pathname.replace(/^\/browse\//, '');
|
|
1417
1445
|
}
|
|
1418
1446
|
|
|
1419
|
-
|
|
1420
|
-
|
|
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
|
+
|
|
1421
1455
|
popup = document.createElement('div');
|
|
1422
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>';
|
|
1423
1464
|
popup.innerHTML =
|
|
1424
1465
|
'<div class="link-ctx-popup-header">' +
|
|
1425
|
-
|
|
1466
|
+
backBtn + titleHtml +
|
|
1426
1467
|
'<button class="link-ctx-popup-close" aria-label="Close">\u00d7</button>' +
|
|
1427
1468
|
'</div>' +
|
|
1428
1469
|
'<div class="link-ctx-popup-body">' + bodyHtml + '</div>';
|
|
1429
1470
|
document.body.appendChild(popup);
|
|
1430
1471
|
|
|
1431
|
-
|
|
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;
|
|
1432
1532
|
var vw = Math.min(window.innerWidth, document.documentElement.clientWidth);
|
|
1433
1533
|
var vh = window.innerHeight;
|
|
1434
1534
|
var left = x + 12;
|
|
@@ -1438,29 +1538,25 @@
|
|
|
1438
1538
|
if (top < 8) top = 8;
|
|
1439
1539
|
popup.style.left = left + 'px';
|
|
1440
1540
|
popup.style.top = top + 'px';
|
|
1441
|
-
|
|
1442
|
-
popup.querySelector('.link-ctx-popup-close').addEventListener('click', hidePopup);
|
|
1443
|
-
popup.addEventListener('click', function(e) { e.stopPropagation(); });
|
|
1444
|
-
popup.addEventListener('contextmenu', function(e) { e.preventDefault(); e.stopPropagation(); });
|
|
1445
|
-
['touchstart','touchmove','touchend'].forEach(function(ev) {
|
|
1446
|
-
popup.addEventListener(ev, function(e) { e.stopPropagation(); }, { passive: true });
|
|
1447
|
-
});
|
|
1448
1541
|
}
|
|
1449
1542
|
|
|
1450
1543
|
function updatePopup(bodyHtml, title) {
|
|
1451
1544
|
if (!popup) return;
|
|
1452
1545
|
var body = popup.querySelector('.link-ctx-popup-body');
|
|
1453
|
-
var
|
|
1546
|
+
var titleTextEl = popup.querySelector('.link-ctx-popup-title span');
|
|
1454
1547
|
if (body) body.innerHTML = bodyHtml;
|
|
1455
|
-
if (title &&
|
|
1548
|
+
if (title && titleTextEl) titleTextEl.textContent = title;
|
|
1549
|
+
repositionPopup();
|
|
1456
1550
|
}
|
|
1457
1551
|
|
|
1458
1552
|
function hidePopup() {
|
|
1459
1553
|
if (popup && popup.parentNode) popup.parentNode.removeChild(popup);
|
|
1460
1554
|
popup = null;
|
|
1555
|
+
historyStack = [];
|
|
1461
1556
|
}
|
|
1462
1557
|
|
|
1463
|
-
function handleLink(anchor, x, y) {
|
|
1558
|
+
function handleLink(anchor, x, y, chained) {
|
|
1559
|
+
if (!chained) historyStack = [];
|
|
1464
1560
|
var href = anchor.getAttribute('href');
|
|
1465
1561
|
if (!href || isAnchorOnly(href)) return;
|
|
1466
1562
|
var label = anchor.textContent.trim() || href;
|
|
@@ -1469,21 +1565,22 @@
|
|
|
1469
1565
|
var path = previewPath(href);
|
|
1470
1566
|
var cached = cache[path];
|
|
1471
1567
|
if (cached && typeof cached === 'object') {
|
|
1472
|
-
showPopup(x, y, cached.title || label, cached.html);
|
|
1568
|
+
showPopup(x, y, cached.title || label, cached.html, href);
|
|
1473
1569
|
} else if (cached === false) {
|
|
1474
1570
|
showPopup(x, y, label,
|
|
1475
1571
|
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
1476
|
-
'<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);
|
|
1477
1573
|
} else {
|
|
1478
|
-
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);
|
|
1479
1575
|
if (cached === undefined) {
|
|
1480
1576
|
cache[path] = null;
|
|
1481
1577
|
fetch(path)
|
|
1482
1578
|
.then(function(r) { return r.ok ? r.json() : null; })
|
|
1483
1579
|
.then(function(data) {
|
|
1484
1580
|
if (!data) { cache[path] = false; updatePopup('<p style="margin:0;color:#c44;font-family:sans-serif;font-size:0.85rem">Preview unavailable.</p>'); return; }
|
|
1485
|
-
|
|
1486
|
-
|
|
1581
|
+
var bodyHtml = data.html + (data.frontmatter_html || '');
|
|
1582
|
+
cache[path] = { title: data.title, html: bodyHtml };
|
|
1583
|
+
updatePopup(bodyHtml, data.title || label);
|
|
1487
1584
|
})
|
|
1488
1585
|
.catch(function() {
|
|
1489
1586
|
cache[path] = false;
|
|
@@ -1495,13 +1592,13 @@
|
|
|
1495
1592
|
var extKey = 'ext:' + href;
|
|
1496
1593
|
var extCached = cache[extKey];
|
|
1497
1594
|
if (extCached && typeof extCached === 'object') {
|
|
1498
|
-
showPopup(x, y, extCached.title || label, extCached.html);
|
|
1595
|
+
showPopup(x, y, extCached.title || label, extCached.html, href);
|
|
1499
1596
|
} else if (extCached === false) {
|
|
1500
1597
|
showPopup(x, y, label,
|
|
1501
1598
|
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
1502
|
-
'<p style="margin:0.5rem 0 0;color:#888;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>');
|
|
1599
|
+
'<p style="margin:0.5rem 0 0;color:#888;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>', href);
|
|
1503
1600
|
} else {
|
|
1504
|
-
showPopup(x, y, label, '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>');
|
|
1601
|
+
showPopup(x, y, label, '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>', href);
|
|
1505
1602
|
if (extCached === undefined) {
|
|
1506
1603
|
cache[extKey] = null;
|
|
1507
1604
|
fetch('/fetch?url=' + encodeURIComponent(href))
|
|
@@ -1526,246 +1623,76 @@
|
|
|
1526
1623
|
}
|
|
1527
1624
|
}
|
|
1528
1625
|
} else {
|
|
1529
|
-
showPopup(x, y, label, '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>');
|
|
1626
|
+
showPopup(x, y, label, '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>', href);
|
|
1530
1627
|
}
|
|
1531
1628
|
}
|
|
1532
1629
|
|
|
1533
|
-
//
|
|
1534
|
-
document.addEventListener('
|
|
1535
|
-
if (popup && popup.contains(e.target))
|
|
1630
|
+
// Left-click on desktop
|
|
1631
|
+
document.addEventListener('click', function(e) {
|
|
1632
|
+
if (popup && popup.contains(e.target)) return;
|
|
1536
1633
|
var anchor = findLink(e.target);
|
|
1537
|
-
if (!anchor) return;
|
|
1634
|
+
if (!anchor) { hidePopup(); return; }
|
|
1538
1635
|
var href = anchor.getAttribute('href');
|
|
1539
|
-
if (!href || isAnchorOnly(href)) return;
|
|
1636
|
+
if (!href || isAnchorOnly(href)) { hidePopup(); return; }
|
|
1637
|
+
if (!anchor.closest('.md-content')) { hidePopup(); return; }
|
|
1540
1638
|
e.preventDefault();
|
|
1541
|
-
|
|
1542
|
-
pendingTouch = null;
|
|
1543
|
-
if (!longPressHandled) handleLink(anchor, e.clientX, e.clientY);
|
|
1544
|
-
longPressHandled = false;
|
|
1639
|
+
handleLink(anchor, e.clientX, e.clientY);
|
|
1545
1640
|
});
|
|
1546
1641
|
|
|
1547
|
-
//
|
|
1642
|
+
// Touch tap
|
|
1548
1643
|
document.addEventListener('touchstart', function(e) {
|
|
1549
1644
|
if (e.touches.length !== 1) return;
|
|
1550
|
-
if (popup && popup.contains(e.target)) return;
|
|
1551
|
-
var anchor = findLink(e.target);
|
|
1552
|
-
if (!anchor) return;
|
|
1553
|
-
var href = anchor.getAttribute('href');
|
|
1554
|
-
if (!href || isAnchorOnly(href)) return;
|
|
1555
1645
|
touchMoved = false;
|
|
1556
|
-
longPressHandled = false;
|
|
1557
|
-
var t = e.touches[0];
|
|
1558
|
-
pendingTouch = { anchor: anchor, x: t.clientX, y: t.clientY };
|
|
1559
|
-
clearTimeout(longPressTimer);
|
|
1560
|
-
longPressTimer = setTimeout(function() {
|
|
1561
|
-
if (!touchMoved && pendingTouch) {
|
|
1562
|
-
longPressHandled = true;
|
|
1563
|
-
handleLink(pendingTouch.anchor, pendingTouch.x, pendingTouch.y);
|
|
1564
|
-
pendingTouch = null;
|
|
1565
|
-
setTimeout(function() { longPressHandled = false; }, 400);
|
|
1566
|
-
}
|
|
1567
|
-
}, 600);
|
|
1568
1646
|
}, { passive: true });
|
|
1569
1647
|
|
|
1570
1648
|
document.addEventListener('touchmove', function() {
|
|
1571
1649
|
touchMoved = true;
|
|
1572
|
-
clearTimeout(longPressTimer);
|
|
1573
|
-
pendingTouch = null;
|
|
1574
1650
|
}, { passive: true });
|
|
1575
1651
|
|
|
1576
|
-
document.addEventListener('touchend', function() {
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1652
|
+
document.addEventListener('touchend', function(e) {
|
|
1653
|
+
if (touchMoved) return;
|
|
1654
|
+
if (popup && popup.contains(e.target)) return;
|
|
1655
|
+
var anchor = findLink(e.target);
|
|
1656
|
+
if (!anchor) { hidePopup(); return; }
|
|
1657
|
+
var href = anchor.getAttribute('href');
|
|
1658
|
+
if (!href || isAnchorOnly(href)) { hidePopup(); return; }
|
|
1659
|
+
if (!anchor.closest('.md-content')) { hidePopup(); return; }
|
|
1660
|
+
e.preventDefault();
|
|
1661
|
+
var touch = e.changedTouches[0];
|
|
1662
|
+
handleLink(anchor, touch.clientX, touch.clientY);
|
|
1663
|
+
}, { passive: false });
|
|
1586
1664
|
|
|
1587
|
-
document.addEventListener('click', function(e) {
|
|
1588
|
-
if (popup && !popup.contains(e.target)) hidePopup();
|
|
1589
|
-
});
|
|
1590
1665
|
document.addEventListener('keydown', function(e) {
|
|
1591
1666
|
if (e.key === 'Escape' && popup) hidePopup();
|
|
1592
1667
|
});
|
|
1593
1668
|
|
|
1594
|
-
})();
|
|
1595
|
-
|
|
1596
1669
|
<% if settings.link_tooltips %>
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
var cache = Object.create(null);
|
|
1603
|
-
var activePopup = null;
|
|
1604
|
-
var activeAnchor = null;
|
|
1605
|
-
var hideTimer = null;
|
|
1606
|
-
|
|
1607
|
-
function escHtml(s) {
|
|
1608
|
-
return String(s)
|
|
1609
|
-
.replace(/&/g, '&').replace(/</g, '<')
|
|
1610
|
-
.replace(/>/g, '>').replace(/"/g, '"');
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
function isLocalMdLink(a) {
|
|
1614
|
-
if (a.classList.contains('broken')) return false;
|
|
1615
|
-
var href = a.getAttribute('href');
|
|
1616
|
-
if (!href) return false;
|
|
1617
|
-
if (/^https?:\/\//i.test(href)) return false;
|
|
1618
|
-
var path = href.split('?')[0].split('#')[0];
|
|
1619
|
-
return /\.md$/i.test(path);
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
function isExternalLink(a) {
|
|
1623
|
-
var href = a.getAttribute('href');
|
|
1624
|
-
return href && /^https?:\/\//i.test(href);
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
function previewUrl(a) {
|
|
1628
|
-
var resolved = new URL(a.getAttribute('href'), location.href);
|
|
1629
|
-
return '/preview/' + resolved.pathname.replace(/^\/browse\//, '');
|
|
1630
|
-
}
|
|
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
|
-
|
|
1643
|
-
function showPopup(anchor, html) {
|
|
1644
|
-
hidePopup();
|
|
1645
|
-
var pop = document.createElement('div');
|
|
1646
|
-
pop.className = 'link-preview-popup';
|
|
1647
|
-
pop.innerHTML = html;
|
|
1648
|
-
activePopup = pop;
|
|
1649
|
-
activeAnchor = anchor;
|
|
1650
|
-
|
|
1651
|
-
var vw = Math.min(window.innerWidth, document.documentElement.clientWidth);
|
|
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
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
// Keep popup open while hovering over it
|
|
1687
|
-
pop.addEventListener('mouseenter', function() { clearTimeout(hideTimer); });
|
|
1688
|
-
pop.addEventListener('mouseleave', function() { hideTimer = setTimeout(hidePopup, 10); });
|
|
1689
|
-
|
|
1690
|
-
// Stop clicks inside the popup from triggering the outer link
|
|
1691
|
-
pop.addEventListener('click', function(e) {
|
|
1692
|
-
e.stopPropagation();
|
|
1693
|
-
if (!e.target.closest('a')) e.preventDefault();
|
|
1694
|
-
});
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
function hidePopup() {
|
|
1698
|
-
if (activePopup && activePopup.parentNode) activePopup.parentNode.removeChild(activePopup);
|
|
1699
|
-
activePopup = null;
|
|
1700
|
-
activeAnchor = null;
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
function fetchAndShow(anchor) {
|
|
1704
|
-
var url = previewUrl(anchor);
|
|
1705
|
-
var cached = cache[url];
|
|
1706
|
-
if (cached === undefined) {
|
|
1707
|
-
cache[url] = null; // in-flight
|
|
1708
|
-
showPopup(anchor, '<p style="opacity:0.5;margin:0">Loading\u2026</p>');
|
|
1709
|
-
fetch(url)
|
|
1710
|
-
.then(function(r) { return r.ok ? r.json() : null; })
|
|
1711
|
-
.then(function(data) {
|
|
1712
|
-
if (!data) { cache[url] = false; if (activeAnchor === anchor) hidePopup(); return; }
|
|
1713
|
-
var html = '<div class="link-preview-popup-title">' + escHtml(data.title) + '</div>' + data.html;
|
|
1714
|
-
cache[url] = html;
|
|
1715
|
-
if (activeAnchor === anchor) showPopup(anchor, html);
|
|
1716
|
-
})
|
|
1717
|
-
.catch(function() { cache[url] = false; if (activeAnchor === anchor) hidePopup(); });
|
|
1718
|
-
} else if (typeof cached === 'string') {
|
|
1719
|
-
showPopup(anchor, cached);
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
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
|
-
|
|
1744
|
-
content.querySelectorAll('a').forEach(function(a) {
|
|
1745
|
-
var localMd = isLocalMdLink(a);
|
|
1746
|
-
var external = isExternalLink(a);
|
|
1747
|
-
if (!localMd && !external) return;
|
|
1748
|
-
a.classList.add('link-tooltip-anchor');
|
|
1749
|
-
|
|
1670
|
+
// Hover — same popup as click, triggered after a short delay when no popup is active
|
|
1671
|
+
(function() {
|
|
1672
|
+
var content = document.querySelector('.md-content');
|
|
1673
|
+
if (!content) return;
|
|
1750
1674
|
var hoverTimer = null;
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1675
|
+
content.querySelectorAll('a').forEach(function(a) {
|
|
1676
|
+
var href = a.getAttribute('href');
|
|
1677
|
+
if (!href || isAnchorOnly(href)) return;
|
|
1678
|
+
if (!isLocalMd(href) && !isExternal(href)) return;
|
|
1679
|
+
a.addEventListener('mouseenter', function(e) {
|
|
1680
|
+
clearTimeout(hoverTimer);
|
|
1681
|
+
if (popup) return;
|
|
1682
|
+
var x = e.clientX, y = e.clientY;
|
|
1683
|
+
hoverTimer = setTimeout(function() {
|
|
1684
|
+
if (!popup) handleLink(a, x, y, false);
|
|
1685
|
+
}, 300);
|
|
1686
|
+
});
|
|
1687
|
+
a.addEventListener('mouseleave', function() {
|
|
1688
|
+
clearTimeout(hoverTimer);
|
|
1689
|
+
});
|
|
1761
1690
|
});
|
|
1762
|
-
});
|
|
1691
|
+
})();
|
|
1692
|
+
<% end %>
|
|
1763
1693
|
|
|
1764
|
-
document.addEventListener('click', function(e) {
|
|
1765
|
-
if (activePopup && !activePopup.contains(e.target) && activeAnchor && !activeAnchor.contains(e.target)) hidePopup();
|
|
1766
|
-
});
|
|
1767
1694
|
})();
|
|
1768
|
-
|
|
1695
|
+
|
|
1769
1696
|
|
|
1770
1697
|
</script>
|
|
1771
1698
|
</body>
|