markdownr 0.4.3 → 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/bin/markdownr +5 -0
- data/lib/markdown_server/app.rb +2 -3
- 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/bin/markdownr
CHANGED
|
@@ -27,6 +27,10 @@ OptionParser.new do |opts|
|
|
|
27
27
|
options[:link_tooltips] = false
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
opts.on("--standard-newlines", "Use standard markdown newlines (two newlines = paragraph; default is Obsidian-style where single newlines become line breaks)") do
|
|
31
|
+
options[:hard_wrap] = false
|
|
32
|
+
end
|
|
33
|
+
|
|
30
34
|
opts.on("-v", "--version", "Show version") do
|
|
31
35
|
puts "markdownr #{MarkdownServer::VERSION}"
|
|
32
36
|
exit
|
|
@@ -45,6 +49,7 @@ MarkdownServer::App.set :root_dir, dir
|
|
|
45
49
|
MarkdownServer::App.set :custom_title, options[:title]
|
|
46
50
|
MarkdownServer::App.set :allow_robots, options[:allow_robots] || false
|
|
47
51
|
MarkdownServer::App.set :link_tooltips, options.fetch(:link_tooltips, true)
|
|
52
|
+
MarkdownServer::App.set :hard_wrap, options.fetch(:hard_wrap, true)
|
|
48
53
|
MarkdownServer::App.set :port, options[:port]
|
|
49
54
|
MarkdownServer::App.set :bind, options[:bind]
|
|
50
55
|
|
data/lib/markdown_server/app.rb
CHANGED
|
@@ -20,6 +20,7 @@ module MarkdownServer
|
|
|
20
20
|
set :custom_title, nil
|
|
21
21
|
set :allow_robots, false
|
|
22
22
|
set :link_tooltips, true
|
|
23
|
+
set :hard_wrap, true
|
|
23
24
|
set :show_exceptions, false
|
|
24
25
|
set :protection, false
|
|
25
26
|
set :host_authorization, { permitted_hosts: [] }
|
|
@@ -150,7 +151,7 @@ module MarkdownServer
|
|
|
150
151
|
input: "GFM",
|
|
151
152
|
syntax_highlighter: "rouge",
|
|
152
153
|
syntax_highlighter_opts: { default_lang: "text" },
|
|
153
|
-
hard_wrap:
|
|
154
|
+
hard_wrap: settings.hard_wrap
|
|
154
155
|
).to_html
|
|
155
156
|
|
|
156
157
|
# Restore non-numeric footnote labels: Kramdown converts all footnote
|
|
@@ -482,8 +483,6 @@ module MarkdownServer
|
|
|
482
483
|
|
|
483
484
|
get "/preview/*" do
|
|
484
485
|
content_type :json
|
|
485
|
-
halt 404, '{"error":"disabled"}' unless settings.link_tooltips
|
|
486
|
-
|
|
487
486
|
requested = params["splat"].first.to_s
|
|
488
487
|
base = File.realpath(root_dir)
|
|
489
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() {
|