markdownr 0.5.1 → 0.5.3
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 +58 -21
- 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: 77751617a0f29c77c283ed5a5861539501bc6e5795af91b83d2f7ca15ff7e19c
|
|
4
|
+
data.tar.gz: fc0003a34b89f185e567954561d5a367a0a671d085f79719d4a292b82d23270f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 65dc66819263dd1367b965e3ec480b90dea99581c9e6039e7bf4e171cfd6cfba6d590c19ef9bbfd4e01a440f8e41478a0640734a6552999b9e69aa72672361f6
|
|
7
|
+
data.tar.gz: 825fe2505eb934401acd0881a8826a2de0b2900ba764b0b3f5e4c5da8a54d06efe567becb87f2d38beb5a2f746c46701272d6c30df95ef3902d8d64596d6738c
|
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
|
@@ -1419,6 +1419,7 @@
|
|
|
1419
1419
|
var touchMoved = false;
|
|
1420
1420
|
var historyStack = [];
|
|
1421
1421
|
var currentPopupPos = { x: 0, y: 0 };
|
|
1422
|
+
var mouseLeaveTimer = null;
|
|
1422
1423
|
|
|
1423
1424
|
function escHtml(s) {
|
|
1424
1425
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
@@ -1446,11 +1447,11 @@
|
|
|
1446
1447
|
|
|
1447
1448
|
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
|
|
|
1449
|
-
function showPopup(x, y, title, bodyHtml, href) {
|
|
1450
|
+
function showPopup(x, y, title, bodyHtml, href, linkRect) {
|
|
1450
1451
|
// Remove old popup without clearing historyStack
|
|
1451
1452
|
if (popup && popup.parentNode) popup.parentNode.removeChild(popup);
|
|
1452
1453
|
popup = null;
|
|
1453
|
-
currentPopupPos = { x: x, y: y };
|
|
1454
|
+
currentPopupPos = { x: x, y: y, linkRect: linkRect || null };
|
|
1454
1455
|
|
|
1455
1456
|
popup = document.createElement('div');
|
|
1456
1457
|
popup.className = 'link-ctx-popup';
|
|
@@ -1469,6 +1470,14 @@
|
|
|
1469
1470
|
'<div class="link-ctx-popup-body">' + bodyHtml + '</div>';
|
|
1470
1471
|
document.body.appendChild(popup);
|
|
1471
1472
|
|
|
1473
|
+
clearTimeout(mouseLeaveTimer);
|
|
1474
|
+
popup.addEventListener('mouseleave', function() {
|
|
1475
|
+
mouseLeaveTimer = setTimeout(hidePopup, 150);
|
|
1476
|
+
});
|
|
1477
|
+
popup.addEventListener('mouseenter', function() {
|
|
1478
|
+
clearTimeout(mouseLeaveTimer);
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1472
1481
|
repositionPopup();
|
|
1473
1482
|
|
|
1474
1483
|
var backBtnEl = popup.querySelector('.link-ctx-popup-back');
|
|
@@ -1476,7 +1485,7 @@
|
|
|
1476
1485
|
backBtnEl.addEventListener('click', function(e) {
|
|
1477
1486
|
e.stopPropagation();
|
|
1478
1487
|
var prev = historyStack.pop();
|
|
1479
|
-
if (prev) showPopup(prev.x, prev.y, prev.title, prev.bodyHtml, prev.href);
|
|
1488
|
+
if (prev) showPopup(prev.x, prev.y, prev.title, prev.bodyHtml, prev.href, prev.linkRect);
|
|
1480
1489
|
});
|
|
1481
1490
|
}
|
|
1482
1491
|
popup.querySelector('.link-ctx-popup-close').addEventListener('click', hidePopup);
|
|
@@ -1492,10 +1501,10 @@
|
|
|
1492
1501
|
if (!linkHref || isAnchorOnly(linkHref)) return;
|
|
1493
1502
|
e.stopPropagation();
|
|
1494
1503
|
e.preventDefault();
|
|
1495
|
-
var savedPos = { x: currentPopupPos.x, y: currentPopupPos.y };
|
|
1504
|
+
var savedPos = { x: currentPopupPos.x, y: currentPopupPos.y, linkRect: currentPopupPos.linkRect };
|
|
1496
1505
|
var titleEl = popup.querySelector('.link-ctx-popup-title');
|
|
1497
1506
|
historyStack.push({
|
|
1498
|
-
x: savedPos.x, y: savedPos.y,
|
|
1507
|
+
x: savedPos.x, y: savedPos.y, linkRect: savedPos.linkRect,
|
|
1499
1508
|
title: titleEl ? titleEl.querySelector('span').textContent : '',
|
|
1500
1509
|
bodyHtml: body.innerHTML,
|
|
1501
1510
|
href: titleEl ? (titleEl.getAttribute('href') || '') : ''
|
|
@@ -1510,10 +1519,10 @@
|
|
|
1510
1519
|
if (!linkHref || isAnchorOnly(linkHref)) return;
|
|
1511
1520
|
e.stopPropagation();
|
|
1512
1521
|
e.preventDefault();
|
|
1513
|
-
var savedPos = { x: currentPopupPos.x, y: currentPopupPos.y };
|
|
1522
|
+
var savedPos = { x: currentPopupPos.x, y: currentPopupPos.y, linkRect: currentPopupPos.linkRect };
|
|
1514
1523
|
var titleEl = popup.querySelector('.link-ctx-popup-title');
|
|
1515
1524
|
historyStack.push({
|
|
1516
|
-
x: savedPos.x, y: savedPos.y,
|
|
1525
|
+
x: savedPos.x, y: savedPos.y, linkRect: savedPos.linkRect,
|
|
1517
1526
|
title: titleEl ? titleEl.querySelector('span').textContent : '',
|
|
1518
1527
|
bodyHtml: body.innerHTML,
|
|
1519
1528
|
href: titleEl ? (titleEl.getAttribute('href') || '') : ''
|
|
@@ -1529,13 +1538,35 @@
|
|
|
1529
1538
|
function repositionPopup() {
|
|
1530
1539
|
if (!popup) return;
|
|
1531
1540
|
var x = currentPopupPos.x, y = currentPopupPos.y;
|
|
1541
|
+
var rect = currentPopupPos.linkRect;
|
|
1532
1542
|
var vw = Math.min(window.innerWidth, document.documentElement.clientWidth);
|
|
1533
1543
|
var vh = window.innerHeight;
|
|
1534
|
-
var
|
|
1535
|
-
var
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1544
|
+
var pw = popup.offsetWidth;
|
|
1545
|
+
var ph = popup.offsetHeight;
|
|
1546
|
+
|
|
1547
|
+
// Horizontal: position so mouse is ~16px inside left edge, clamped to viewport
|
|
1548
|
+
var left = Math.min(x - 16, vw - pw - 8);
|
|
1549
|
+
if (left < 8) left = 8;
|
|
1550
|
+
|
|
1551
|
+
// Vertical: tight to link bounds when available
|
|
1552
|
+
var top;
|
|
1553
|
+
if (rect) {
|
|
1554
|
+
var gap = 3;
|
|
1555
|
+
var belowTop = rect.bottom + gap;
|
|
1556
|
+
var aboveTop = rect.top - ph - gap;
|
|
1557
|
+
if (belowTop + ph <= vh - 8) {
|
|
1558
|
+
top = belowTop; // fits below
|
|
1559
|
+
} else if (aboveTop >= 8) {
|
|
1560
|
+
top = aboveTop; // fits above
|
|
1561
|
+
} else {
|
|
1562
|
+
top = Math.max(8, vh - ph - 8); // clamp: covers link
|
|
1563
|
+
}
|
|
1564
|
+
} else {
|
|
1565
|
+
top = y + 12;
|
|
1566
|
+
if (top + ph > vh - 8) top = Math.max(8, y - ph - 12);
|
|
1567
|
+
if (top < 8) top = 8;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1539
1570
|
popup.style.left = left + 'px';
|
|
1540
1571
|
popup.style.top = top + 'px';
|
|
1541
1572
|
}
|
|
@@ -1550,36 +1581,41 @@
|
|
|
1550
1581
|
}
|
|
1551
1582
|
|
|
1552
1583
|
function hidePopup() {
|
|
1584
|
+
clearTimeout(mouseLeaveTimer);
|
|
1553
1585
|
if (popup && popup.parentNode) popup.parentNode.removeChild(popup);
|
|
1554
1586
|
popup = null;
|
|
1555
1587
|
historyStack = [];
|
|
1556
1588
|
}
|
|
1557
1589
|
|
|
1590
|
+
|
|
1558
1591
|
function handleLink(anchor, x, y, chained) {
|
|
1559
1592
|
if (!chained) historyStack = [];
|
|
1560
1593
|
var href = anchor.getAttribute('href');
|
|
1561
1594
|
if (!href || isAnchorOnly(href)) return;
|
|
1562
1595
|
var label = anchor.textContent.trim() || href;
|
|
1596
|
+
// For chained popup navigation keep the current link rect; for new popups measure the anchor
|
|
1597
|
+
var linkRect = chained ? currentPopupPos.linkRect : anchor.getBoundingClientRect();
|
|
1563
1598
|
|
|
1564
1599
|
if (isLocalMd(href)) {
|
|
1565
1600
|
var path = previewPath(href);
|
|
1566
1601
|
var cached = cache[path];
|
|
1567
1602
|
if (cached && typeof cached === 'object') {
|
|
1568
|
-
showPopup(x, y, cached.title || label, cached.html, href);
|
|
1603
|
+
showPopup(x, y, cached.title || label, cached.html, href, linkRect);
|
|
1569
1604
|
} else if (cached === false) {
|
|
1570
1605
|
showPopup(x, y, label,
|
|
1571
1606
|
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
1572
|
-
'<p style="margin:0;color:#888;font-family:sans-serif;font-size:0.82rem">Preview not available</p>', href);
|
|
1607
|
+
'<p style="margin:0;color:#888;font-family:sans-serif;font-size:0.82rem">Preview not available</p>', href, linkRect);
|
|
1573
1608
|
} else {
|
|
1574
|
-
showPopup(x, y, label, '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>', href);
|
|
1609
|
+
showPopup(x, y, label, '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>', href, linkRect);
|
|
1575
1610
|
if (cached === undefined) {
|
|
1576
1611
|
cache[path] = null;
|
|
1577
1612
|
fetch(path)
|
|
1578
1613
|
.then(function(r) { return r.ok ? r.json() : null; })
|
|
1579
1614
|
.then(function(data) {
|
|
1580
1615
|
if (!data) { cache[path] = false; updatePopup('<p style="margin:0;color:#c44;font-family:sans-serif;font-size:0.85rem">Preview unavailable.</p>'); return; }
|
|
1581
|
-
|
|
1582
|
-
|
|
1616
|
+
var bodyHtml = data.html + (data.frontmatter_html || '');
|
|
1617
|
+
cache[path] = { title: data.title, html: bodyHtml };
|
|
1618
|
+
updatePopup(bodyHtml, data.title || label);
|
|
1583
1619
|
})
|
|
1584
1620
|
.catch(function() {
|
|
1585
1621
|
cache[path] = false;
|
|
@@ -1591,13 +1627,13 @@
|
|
|
1591
1627
|
var extKey = 'ext:' + href;
|
|
1592
1628
|
var extCached = cache[extKey];
|
|
1593
1629
|
if (extCached && typeof extCached === 'object') {
|
|
1594
|
-
showPopup(x, y, extCached.title || label, extCached.html, href);
|
|
1630
|
+
showPopup(x, y, extCached.title || label, extCached.html, href, linkRect);
|
|
1595
1631
|
} else if (extCached === false) {
|
|
1596
1632
|
showPopup(x, y, label,
|
|
1597
1633
|
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
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);
|
|
1634
|
+
'<p style="margin:0.5rem 0 0;color:#888;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>', href, linkRect);
|
|
1599
1635
|
} else {
|
|
1600
|
-
showPopup(x, y, label, '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>', href);
|
|
1636
|
+
showPopup(x, y, label, '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>', href, linkRect);
|
|
1601
1637
|
if (extCached === undefined) {
|
|
1602
1638
|
cache[extKey] = null;
|
|
1603
1639
|
fetch('/fetch?url=' + encodeURIComponent(href))
|
|
@@ -1622,7 +1658,7 @@
|
|
|
1622
1658
|
}
|
|
1623
1659
|
}
|
|
1624
1660
|
} else {
|
|
1625
|
-
showPopup(x, y, label, '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>', href);
|
|
1661
|
+
showPopup(x, y, label, '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>', href, linkRect);
|
|
1626
1662
|
}
|
|
1627
1663
|
}
|
|
1628
1664
|
|
|
@@ -1685,6 +1721,7 @@
|
|
|
1685
1721
|
});
|
|
1686
1722
|
a.addEventListener('mouseleave', function() {
|
|
1687
1723
|
clearTimeout(hoverTimer);
|
|
1724
|
+
if (popup) mouseLeaveTimer = setTimeout(hidePopup, 150);
|
|
1688
1725
|
});
|
|
1689
1726
|
});
|
|
1690
1727
|
})();
|