markdownr 0.4.9 → 0.5.0

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: 498dc869909593a83b885a1cc30de710e07b6e686434da94a49c2d2e1f473d41
4
- data.tar.gz: 83c31394846d27ec2b474465dafa548494b42683e800348e752b080caeb3df42
3
+ metadata.gz: 03b8c1c4665231a3fb1ead89abea54e0630131743216938a1c293945c942e710
4
+ data.tar.gz: 004d43321fcda1b894c6481eed4b755f29a9fae14c6e0f6bbb5c1f9d82b6afd9
5
5
  SHA512:
6
- metadata.gz: bf3f02fae7411669eb42765b7a65280a55d6e4dca36a77601017fd52c42e2a75adee172d173cf470033d897d75c6a18bb84277094348a3f9b1f578b3d962586e
7
- data.tar.gz: 8388865e12e977a8f6ec1be0047dd4dd14981d2671bbcb3182fc77e87ccb20ee5617f6b2e8f944b2355187bdffb4a2107ac2266f171aacb848a9d010d4fca137
6
+ metadata.gz: 0d44c7b65617670e54d450d5d958294bd78914407cf925087f1547b551b5b91dd3b556820009aa197491bdd14bff9fc6e205a47bf6662fb5b5a495d8e5d8c71d
7
+ data.tar.gz: 07f03bc67cc2b668c90c1c9467eb7eafe60f2c58c4030553adbec8af4309bdbf3943c9ffdd454cdde0283619f702c6a7ef75992ed679e84983557c5ed5fc3675
@@ -491,7 +491,7 @@ module MarkdownServer
491
491
  } || ""
492
492
  end
493
493
 
494
- def page_html(raw)
494
+ def page_html(raw, base_url = nil)
495
495
  w = raw.dup
496
496
  # Remove inert elements and their entire contents
497
497
  STRIP_FULL.each { |t| w.gsub!(/<#{t}[^>]*>.*?<\/#{t}>/im, " ") }
@@ -508,12 +508,30 @@ module MarkdownServer
508
508
  w.match(/<body[^>]*>(.*?)<\/body>/im)&.[](1) ||
509
509
  w
510
510
 
511
- # Rewrite tags: keep allowed (strip attrs), block→newline, rest→empty
512
- out = content.gsub(/<(\/?)(\w+)[^>]*>/) do
513
- slash, tag = $1, $2.downcase
514
- if ALLOWED_HTML.include?(tag) then "<#{slash}#{tag}>"
515
- elsif BLOCK_HTML.include?(tag) then "\n"
516
- else ""
511
+ # Rewrite tags: keep allowed (strip attrs), preserve <a href>, block→newline, rest→empty
512
+ out = content.gsub(/<(\/?)(\w+)([^>]*)>/) do
513
+ slash, tag, attrs = $1, $2.downcase, $3
514
+ if ALLOWED_HTML.include?(tag)
515
+ "<#{slash}#{tag}>"
516
+ elsif tag == "a"
517
+ if slash.empty?
518
+ m = attrs.match(/href\s*=\s*["']([^"']*)["']/i)
519
+ if m && !m[1].match?(/\Ajavascript:/i)
520
+ href = m[1]
521
+ if base_url && href !~ /\Ahttps?:\/\//i && !href.start_with?("#")
522
+ href = (URI.join(base_url, href).to_s rescue href)
523
+ end
524
+ %(<a href="#{h(href)}" target="_blank" rel="noopener">)
525
+ else
526
+ ""
527
+ end
528
+ else
529
+ "</a>"
530
+ end
531
+ elsif BLOCK_HTML.include?(tag)
532
+ "\n"
533
+ else
534
+ ""
517
535
  end
518
536
  end
519
537
 
@@ -533,6 +551,9 @@ module MarkdownServer
533
551
  .gsub(/<(\w+)>\s*<\/\1>/, "") # drop empty tags
534
552
  .strip
535
553
 
554
+ # Strip all footer navigation after "Read full chapter" up to (but not including) copyright
555
+ out.gsub!(/(<a[^>]*>Read\s+full\s+chapter<\/a>)[\s\S]*?(?=©|Copyright\b)/i, "\\1\n")
556
+
536
557
  out.length > 10_000 ? out[0, 10_000] : out
537
558
  end
538
559
 
@@ -627,7 +648,8 @@ module MarkdownServer
627
648
  html = fetch_external_page(url)
628
649
  halt 502, '{"error":"fetch failed"}' unless html
629
650
 
630
- JSON.dump({ title: page_title(html), html: page_html(html) })
651
+ title = page_title(html).sub(/ [-–] .*/, "").strip
652
+ JSON.dump({ title: title, html: page_html(html, url) })
631
653
  end
632
654
 
633
655
  get "/search/?*" do
@@ -1,3 +1,3 @@
1
1
  module MarkdownServer
2
- VERSION = "0.4.9"
2
+ VERSION = "0.5.0"
3
3
  end
data/views/layout.erb CHANGED
@@ -726,10 +726,6 @@
726
726
  .link-ctx-popup-close:hover { color: #2c2c2c; }
727
727
  .link-ctx-popup-body {
728
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
729
  }
734
730
  .link-ctx-popup-url {
735
731
  font-family: "SF Mono", Menlo, Consolas, monospace;
@@ -741,49 +737,8 @@
741
737
  border-radius: 4px;
742
738
  margin-bottom: 0.5rem;
743
739
  }
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
740
 
786
- /* Link preview popup */
741
+ /* Link preview popup (hover) */
787
742
  .link-tooltip-anchor { position: relative; }
788
743
  .link-preview-popup {
789
744
  position: absolute;
@@ -799,10 +754,6 @@
799
754
  border-radius: 6px;
800
755
  box-shadow: 0 4px 20px rgba(0,0,0,0.18);
801
756
  padding: 0.8rem 1rem;
802
- font-family: Georgia, "Times New Roman", serif;
803
- font-size: 0.85rem;
804
- line-height: 1.6;
805
- color: #2c2c2c;
806
757
  cursor: auto;
807
758
  -webkit-overflow-scrolling: touch;
808
759
  }
@@ -814,26 +765,36 @@
814
765
  border-bottom: 1px solid #e0d8c8;
815
766
  color: #3a3a3a;
816
767
  }
768
+
769
+ /* Shared popup content styles */
770
+ .link-ctx-popup-body,
771
+ .link-preview-popup {
772
+ font-family: Georgia, "Times New Roman", serif;
773
+ font-size: 0.85rem;
774
+ line-height: 1.6;
775
+ color: #2c2c2c;
776
+ }
777
+ .link-ctx-popup-body h1, .link-ctx-popup-body h2, .link-ctx-popup-body h3,
778
+ .link-ctx-popup-body h4, .link-ctx-popup-body h5, .link-ctx-popup-body h6,
817
779
  .link-preview-popup h1, .link-preview-popup h2, .link-preview-popup h3,
818
780
  .link-preview-popup h4, .link-preview-popup h5, .link-preview-popup h6 {
819
- margin: 0.7rem 0 0.3rem;
820
- color: #3a3a3a;
781
+ margin: 0.7rem 0 0.3rem; color: #3a3a3a;
821
782
  }
822
- .link-preview-popup h1 { font-size: 1.2rem; }
823
- .link-preview-popup h2 { font-size: 1.05rem; border-bottom: none; padding-bottom: 0; }
824
- .link-preview-popup h3 { font-size: 0.95rem; }
825
- .link-preview-popup p { margin: 0 0 0.5rem; }
826
- .link-preview-popup p:last-child { margin-bottom: 0; }
827
- .link-preview-popup a { color: #8b6914; text-decoration: none; border-bottom: 1px solid #d4b96a; }
828
- .link-preview-popup a.wiki-link { color: #6a8e3e; border-bottom-color: #6a8e3e; }
829
- .link-preview-popup code {
783
+ .link-ctx-popup-body h1, .link-preview-popup h1 { font-size: 1.2rem; }
784
+ .link-ctx-popup-body h2, .link-preview-popup h2 { font-size: 1.05rem; border-bottom: none; padding-bottom: 0; }
785
+ .link-ctx-popup-body h3, .link-preview-popup h3 { font-size: 0.95rem; }
786
+ .link-ctx-popup-body p, .link-preview-popup p { margin: 0 0 0.5rem; }
787
+ .link-ctx-popup-body p:last-child, .link-preview-popup p:last-child { margin-bottom: 0; }
788
+ .link-ctx-popup-body a, .link-preview-popup a { color: #8b6914; text-decoration: none; border-bottom: 1px solid #d4b96a; }
789
+ .link-ctx-popup-body a.wiki-link, .link-preview-popup a.wiki-link { color: #6a8e3e; border-bottom-color: #6a8e3e; }
790
+ .link-ctx-popup-body code, .link-preview-popup code {
830
791
  font-family: "SF Mono", Menlo, Consolas, monospace;
831
792
  font-size: 0.82em;
832
793
  background: #f0ece3;
833
794
  padding: 0.1em 0.3em;
834
795
  border-radius: 3px;
835
796
  }
836
- .link-preview-popup pre {
797
+ .link-ctx-popup-body pre, .link-preview-popup pre {
837
798
  background: #2d2d2d;
838
799
  color: #f0f0f0;
839
800
  padding: 0.6rem 0.8rem;
@@ -843,26 +804,21 @@
843
804
  overflow-x: auto;
844
805
  margin: 0.4rem 0;
845
806
  }
846
- .link-preview-popup pre code { background: none; padding: 0; font-size: 1em; }
807
+ .link-ctx-popup-body pre code, .link-preview-popup pre code { background: none; padding: 0; font-size: 1em; color: inherit; }
808
+ .link-ctx-popup-body ul, .link-ctx-popup-body ol,
847
809
  .link-preview-popup ul, .link-preview-popup ol { padding-left: 1.4rem; margin: 0.3rem 0; }
848
- .link-preview-popup li { margin-bottom: 0.2rem; }
849
- .link-preview-popup blockquote {
810
+ .link-ctx-popup-body li, .link-preview-popup li { margin-bottom: 0.2rem; }
811
+ .link-ctx-popup-body blockquote, .link-preview-popup blockquote {
850
812
  border-left: 3px solid #d4b96a;
851
813
  margin: 0.5rem 0;
852
814
  padding: 0.3rem 0.8rem;
853
815
  color: #4a4a4a;
854
816
  font-style: italic;
855
817
  }
856
- .link-preview-popup table {
857
- border-collapse: collapse;
858
- font-size: 0.8rem;
859
- margin: 0.5rem 0;
860
- }
861
- .link-preview-popup th, .link-preview-popup td {
862
- border: 1px solid #ddd;
863
- padding: 0.3rem 0.5rem;
864
- }
865
- .link-preview-popup th { background: #f5f0e4; }
818
+ .link-ctx-popup-body table, .link-preview-popup table { border-collapse: collapse; font-size: 0.8rem; margin: 0.5rem 0; }
819
+ .link-ctx-popup-body th, .link-ctx-popup-body td,
820
+ .link-preview-popup th, .link-preview-popup td { border: 1px solid #ddd; padding: 0.3rem 0.5rem; }
821
+ .link-ctx-popup-body th, .link-preview-popup th { background: #f5f0e4; }
866
822
 
867
823
  /* Footnote tooltips */
868
824
  .footnote-tooltip {
@@ -1634,10 +1590,11 @@
1634
1590
  document.addEventListener('keydown', function(e) {
1635
1591
  if (e.key === 'Escape' && popup) hidePopup();
1636
1592
  });
1593
+
1637
1594
  })();
1638
1595
 
1639
1596
  <% if settings.link_tooltips %>
1640
- // Link preview popup for local markdown links
1597
+ // Link preview popup for local markdown links and external links
1641
1598
  (function() {
1642
1599
  var content = document.querySelector('.md-content');
1643
1600
  if (!content) return;
@@ -1662,48 +1619,79 @@
1662
1619
  return /\.md$/i.test(path);
1663
1620
  }
1664
1621
 
1622
+ function isExternalLink(a) {
1623
+ var href = a.getAttribute('href');
1624
+ return href && /^https?:\/\//i.test(href);
1625
+ }
1626
+
1665
1627
  function previewUrl(a) {
1666
1628
  var resolved = new URL(a.getAttribute('href'), location.href);
1667
1629
  return '/preview/' + resolved.pathname.replace(/^\/browse\//, '');
1668
1630
  }
1669
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
+
1670
1643
  function showPopup(anchor, html) {
1671
1644
  hidePopup();
1672
1645
  var pop = document.createElement('div');
1673
1646
  pop.className = 'link-preview-popup';
1674
1647
  pop.innerHTML = html;
1675
- anchor.appendChild(pop);
1676
1648
  activePopup = pop;
1677
1649
  activeAnchor = anchor;
1678
1650
 
1679
- // Clamp right edge to viewport
1680
1651
  var vw = Math.min(window.innerWidth, document.documentElement.clientWidth);
1681
- var rect = pop.getBoundingClientRect();
1682
- if (rect.right > vw - 8) {
1683
- pop.style.left = Math.max(0, parseFloat(pop.style.left || 0) - (rect.right - (vw - 8))) + 'px';
1684
- }
1685
- // If top goes off screen, flip below the link instead
1686
- rect = pop.getBoundingClientRect();
1687
- if (rect.top < 8) {
1688
- pop.style.bottom = 'auto';
1689
- pop.style.top = '100%';
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
+ }
1690
1684
  }
1691
1685
 
1692
1686
  // Keep popup open while hovering over it
1693
1687
  pop.addEventListener('mouseenter', function() { clearTimeout(hideTimer); });
1694
1688
  pop.addEventListener('mouseleave', function() { hideTimer = setTimeout(hidePopup, 10); });
1695
1689
 
1696
- // Stop clicks/touches inside the popup from triggering the outer link
1690
+ // Stop clicks inside the popup from triggering the outer link
1697
1691
  pop.addEventListener('click', function(e) {
1698
1692
  e.stopPropagation();
1699
1693
  if (!e.target.closest('a')) e.preventDefault();
1700
1694
  });
1701
- pop.addEventListener('touchstart', function(e) { e.stopPropagation(); }, { passive: true });
1702
- pop.addEventListener('touchmove', function(e) { e.stopPropagation(); }, { passive: true });
1703
- pop.addEventListener('touchend', function(e) {
1704
- e.stopPropagation();
1705
- if (!e.target.closest('a')) e.preventDefault();
1706
- });
1707
1695
  }
1708
1696
 
1709
1697
  function hidePopup() {
@@ -1732,36 +1720,49 @@
1732
1720
  }
1733
1721
  }
1734
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
+
1735
1744
  content.querySelectorAll('a').forEach(function(a) {
1736
- if (!isLocalMdLink(a)) return;
1745
+ var localMd = isLocalMdLink(a);
1746
+ var external = isExternalLink(a);
1747
+ if (!localMd && !external) return;
1737
1748
  a.classList.add('link-tooltip-anchor');
1738
1749
 
1739
1750
  var hoverTimer = null;
1740
1751
  a.addEventListener('click', hidePopup);
1741
1752
  a.addEventListener('mouseenter', function() {
1742
- hoverTimer = setTimeout(function() { fetchAndShow(a); }, 300);
1753
+ hoverTimer = setTimeout(function() {
1754
+ if (localMd) fetchAndShow(a);
1755
+ else fetchExternalAndShow(a);
1756
+ }, 300);
1743
1757
  });
1744
1758
  a.addEventListener('mouseleave', function() {
1745
1759
  clearTimeout(hoverTimer);
1746
- hideTimer = setTimeout(hidePopup, 10);
1747
- });
1748
-
1749
- var touchMoved = false;
1750
- a.addEventListener('touchstart', function() { touchMoved = false; }, { passive: true });
1751
- a.addEventListener('touchmove', function() { touchMoved = true; }, { passive: true });
1752
- a.addEventListener('touchend', function(e) {
1753
- if (touchMoved) return;
1754
- if (activeAnchor === a && activePopup) return; // second tap → navigate
1755
- e.preventDefault();
1756
- document.querySelectorAll('.link-preview-popup').forEach(function(p) {
1757
- if (p.parentNode) p.parentNode.removeChild(p);
1758
- });
1759
- fetchAndShow(a);
1760
+ hideTimer = setTimeout(hidePopup, 150);
1760
1761
  });
1761
1762
  });
1762
1763
 
1763
1764
  document.addEventListener('click', function(e) {
1764
- if (activePopup && activeAnchor && !activeAnchor.contains(e.target)) hidePopup();
1765
+ if (activePopup && !activePopup.contains(e.target) && activeAnchor && !activeAnchor.contains(e.target)) hidePopup();
1765
1766
  });
1766
1767
  })();
1767
1768
  <% end %>
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.4.9
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Dunn