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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03b8c1c4665231a3fb1ead89abea54e0630131743216938a1c293945c942e710
4
- data.tar.gz: 004d43321fcda1b894c6481eed4b755f29a9fae14c6e0f6bbb5c1f9d82b6afd9
3
+ metadata.gz: e91138e164e026f52b6957f44693eb86d75b79eda7e4404d094f1ea0d4e35d26
4
+ data.tar.gz: b549ff9d07b45f43cc1639c2b4a47171f3a38bb76e2e98c1bb0ffe03ddc53fbe
5
5
  SHA512:
6
- metadata.gz: 0d44c7b65617670e54d450d5d958294bd78914407cf925087f1547b551b5b91dd3b556820009aa197491bdd14bff9fc6e205a47bf6662fb5b5a495d8e5d8c71d
7
- data.tar.gz: 07f03bc67cc2b668c90c1c9467eb7eafe60f2c58c4030553adbec8af4309bdbf3943c9ffdd454cdde0283619f702c6a7ef75992ed679e84983557c5ed5fc3675
6
+ metadata.gz: cfc1004e65a89e09263f3a35f2da2b3b354afe8351efa87e49194b0aec3bef5aaeedd7d4aba70f00c01ebca6468150a296be2bdad0d8ca238f87b5bc73ebca0d
7
+ data.tar.gz: c0cc3d73580b42c40c3f9244ed5acc69a74363237c54af0d21877bf4f1a64d2b642150fc4c384d13a6efb74a1bf86f12819d10b913f935ff60298e1e055e8460
@@ -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.0"
2
+ VERSION = "0.5.2"
3
3
  end
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
- /* Right-click / long-press link context popup */
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
- // Right-click / long-press popup for all links
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 pendingTouch = null;
1393
- var longPressHandled = false;
1420
+ var historyStack = [];
1421
+ var currentPopupPos = { x: 0, y: 0 };
1394
1422
 
1395
1423
  function escHtml(s) {
1396
1424
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
@@ -1416,19 +1444,91 @@
1416
1444
  return '/preview/' + resolved.pathname.replace(/^\/browse\//, '');
1417
1445
  }
1418
1446
 
1419
- function showPopup(x, y, title, bodyHtml) {
1420
- hidePopup();
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
- '<span class="link-ctx-popup-title">' + escHtml(title) + '</span>' +
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
- // Position near pointer, clamped to viewport
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 titleEl = popup.querySelector('.link-ctx-popup-title');
1546
+ var titleTextEl = popup.querySelector('.link-ctx-popup-title span');
1454
1547
  if (body) body.innerHTML = bodyHtml;
1455
- if (title && titleEl) titleEl.textContent = 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
- cache[path] = { title: data.title, html: data.html };
1486
- updatePopup(data.html, data.title || label);
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
- // Right-click (desktop + most mobile browsers fire contextmenu on long-press)
1534
- document.addEventListener('contextmenu', function(e) {
1535
- if (popup && popup.contains(e.target)) { e.preventDefault(); return; }
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
- clearTimeout(longPressTimer);
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
- // Long-press fallback for touch devices that don't fire contextmenu
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
- clearTimeout(longPressTimer);
1578
- pendingTouch = null;
1579
- }, { passive: true });
1580
-
1581
- document.addEventListener('touchcancel', function() {
1582
- clearTimeout(longPressTimer);
1583
- pendingTouch = null;
1584
- longPressHandled = false;
1585
- }, { passive: true });
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
- // Link preview popup for local markdown links and external links
1598
- (function() {
1599
- var content = document.querySelector('.md-content');
1600
- if (!content) return;
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, '&amp;').replace(/</g, '&lt;')
1610
- .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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
- a.addEventListener('click', hidePopup);
1752
- a.addEventListener('mouseenter', function() {
1753
- hoverTimer = setTimeout(function() {
1754
- if (localMd) fetchAndShow(a);
1755
- else fetchExternalAndShow(a);
1756
- }, 300);
1757
- });
1758
- a.addEventListener('mouseleave', function() {
1759
- clearTimeout(hoverTimer);
1760
- hideTimer = setTimeout(hidePopup, 150);
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
- <% end %>
1695
+
1769
1696
 
1770
1697
  </script>
1771
1698
  </body>
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.0
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Dunn