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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf955ffcbe5dfc1e1f5070b3fa36a6d2d15a092e7be6ca7f8ac5dcd4b863162a
4
- data.tar.gz: 3ea514cfed258bc71a8a3e82f6ef883e1ed619fc63b46e9217eb4745c455fdd5
3
+ metadata.gz: 77751617a0f29c77c283ed5a5861539501bc6e5795af91b83d2f7ca15ff7e19c
4
+ data.tar.gz: fc0003a34b89f185e567954561d5a367a0a671d085f79719d4a292b82d23270f
5
5
  SHA512:
6
- metadata.gz: 19f44c77aab4f8a5a2978c85f8fd900f6a0cc45d9399897a26acae80d5809ce57669716a1a5e488c276a4b53e0b2e5d5e3bc13d8b6463a5c8d9c76342eb39e4f
7
- data.tar.gz: b8adb9e49b1def08d6d3f4061de5048d3cf14232d4ce0c3b9fde51f605f52fb9338a9b39ad49f569d9663fdf9e718e1396f37674e55c7c3f025ffa7a271d3548
6
+ metadata.gz: 65dc66819263dd1367b965e3ec480b90dea99581c9e6039e7bf4e171cfd6cfba6d590c19ef9bbfd4e01a440f8e41478a0640734a6552999b9e69aa72672361f6
7
+ data.tar.gz: 825fe2505eb934401acd0881a8826a2de0b2900ba764b0b3f5e4c5da8a54d06efe567becb87f2d38beb5a2f746c46701272d6c30df95ef3902d8d64596d6738c
@@ -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
- begin
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
- JSON.dump({ title: title.to_s, html: html })
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
@@ -1,3 +1,3 @@
1
1
  module MarkdownServer
2
- VERSION = "0.5.1"
2
+ VERSION = "0.5.3"
3
3
  end
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
@@ -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 left = x + 12;
1535
- var top = y + 12;
1536
- if (left + popup.offsetWidth > vw - 8) left = Math.max(8, vw - popup.offsetWidth - 8);
1537
- if (top + popup.offsetHeight > vh - 8) top = Math.max(8, y - popup.offsetHeight - 12);
1538
- if (top < 8) top = 8;
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
- cache[path] = { title: data.title, html: data.html };
1582
- updatePopup(data.html, data.title || label);
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
  })();
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: markdownr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Dunn