markdownr 0.4.4 → 0.4.5
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 +0 -2
- data/lib/markdown_server/version.rb +1 -1
- data/views/layout.erb +286 -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: 53062f10b38520d041fb4fab0e1155e1b19e15242409996dd82122716bc727d8
|
|
4
|
+
data.tar.gz: 17ee73d28a5ddc64a915d190f31c157c6d8c9319451c1fe754c3c41417bab9a9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8c762e19d73d839cdcf1ff5f4e16c009f22b575a5672936eabef2d5131641d639ae26771bce19f25adbb287e6f7c5d71bb0561b76f302ea32db4b3ad41dc2937
|
|
7
|
+
data.tar.gz: e28d99ad58d6ee904aa3814f665fa31d0313605629be193a482b6f0ef30cce74868800a18cec6db0ecb0101f3182dc48d6b8b561a75d9e39f04db8b7daa9e443
|
data/lib/markdown_server/app.rb
CHANGED
|
@@ -483,8 +483,6 @@ module MarkdownServer
|
|
|
483
483
|
|
|
484
484
|
get "/preview/*" do
|
|
485
485
|
content_type :json
|
|
486
|
-
halt 404, '{"error":"disabled"}' unless settings.link_tooltips
|
|
487
|
-
|
|
488
486
|
requested = params["splat"].first.to_s
|
|
489
487
|
base = File.realpath(root_dir)
|
|
490
488
|
full = File.join(base, requested)
|
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,187 @@
|
|
|
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 {
|
|
1539
|
+
var urlBody = '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>';
|
|
1540
|
+
if (isExternal(href)) {
|
|
1541
|
+
var host = '';
|
|
1542
|
+
try { host = new URL(href).hostname; } catch(e) {}
|
|
1543
|
+
if (host) urlBody += '<p style="margin:0;color:#888;font-family:sans-serif;font-size:0.82rem">External: <strong style="color:#555">' + escHtml(host) + '</strong></p>';
|
|
1544
|
+
}
|
|
1545
|
+
showPopup(x, y, label, urlBody);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Right-click (desktop + most mobile browsers fire contextmenu on long-press)
|
|
1550
|
+
document.addEventListener('contextmenu', function(e) {
|
|
1551
|
+
if (popup && popup.contains(e.target)) { e.preventDefault(); return; }
|
|
1552
|
+
var anchor = findLink(e.target);
|
|
1553
|
+
if (!anchor) return;
|
|
1554
|
+
var href = anchor.getAttribute('href');
|
|
1555
|
+
if (!href || isAnchorOnly(href)) return;
|
|
1556
|
+
e.preventDefault();
|
|
1557
|
+
clearTimeout(longPressTimer);
|
|
1558
|
+
pendingTouch = null;
|
|
1559
|
+
if (!longPressHandled) handleLink(anchor, e.clientX, e.clientY);
|
|
1560
|
+
longPressHandled = false;
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
// Long-press fallback for touch devices that don't fire contextmenu
|
|
1564
|
+
document.addEventListener('touchstart', function(e) {
|
|
1565
|
+
if (e.touches.length !== 1) return;
|
|
1566
|
+
if (popup && popup.contains(e.target)) return;
|
|
1567
|
+
var anchor = findLink(e.target);
|
|
1568
|
+
if (!anchor) return;
|
|
1569
|
+
var href = anchor.getAttribute('href');
|
|
1570
|
+
if (!href || isAnchorOnly(href)) return;
|
|
1571
|
+
touchMoved = false;
|
|
1572
|
+
longPressHandled = false;
|
|
1573
|
+
var t = e.touches[0];
|
|
1574
|
+
pendingTouch = { anchor: anchor, x: t.clientX, y: t.clientY };
|
|
1575
|
+
clearTimeout(longPressTimer);
|
|
1576
|
+
longPressTimer = setTimeout(function() {
|
|
1577
|
+
if (!touchMoved && pendingTouch) {
|
|
1578
|
+
longPressHandled = true;
|
|
1579
|
+
handleLink(pendingTouch.anchor, pendingTouch.x, pendingTouch.y);
|
|
1580
|
+
pendingTouch = null;
|
|
1581
|
+
setTimeout(function() { longPressHandled = false; }, 400);
|
|
1582
|
+
}
|
|
1583
|
+
}, 600);
|
|
1584
|
+
}, { passive: true });
|
|
1585
|
+
|
|
1586
|
+
document.addEventListener('touchmove', function() {
|
|
1587
|
+
touchMoved = true;
|
|
1588
|
+
clearTimeout(longPressTimer);
|
|
1589
|
+
pendingTouch = null;
|
|
1590
|
+
}, { passive: true });
|
|
1591
|
+
|
|
1592
|
+
document.addEventListener('touchend', function() {
|
|
1593
|
+
clearTimeout(longPressTimer);
|
|
1594
|
+
pendingTouch = null;
|
|
1595
|
+
}, { passive: true });
|
|
1596
|
+
|
|
1597
|
+
document.addEventListener('touchcancel', function() {
|
|
1598
|
+
clearTimeout(longPressTimer);
|
|
1599
|
+
pendingTouch = null;
|
|
1600
|
+
longPressHandled = false;
|
|
1601
|
+
}, { passive: true });
|
|
1602
|
+
|
|
1603
|
+
document.addEventListener('click', function(e) {
|
|
1604
|
+
if (popup && !popup.contains(e.target)) hidePopup();
|
|
1605
|
+
});
|
|
1606
|
+
document.addEventListener('keydown', function(e) {
|
|
1607
|
+
if (e.key === 'Escape' && popup) hidePopup();
|
|
1608
|
+
});
|
|
1609
|
+
})();
|
|
1610
|
+
|
|
1325
1611
|
<% if settings.link_tooltips %>
|
|
1326
1612
|
// Link preview popup for local markdown links
|
|
1327
1613
|
(function() {
|