markdownr 0.5.10 → 0.5.12

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: afce137b9a88a74b444125a02f30a12dba75210bd3e223052542bea4ea39d4bf
4
- data.tar.gz: b2cebe87af19b5f2ec26270b6f5b79d05472357e3dcb6916b9047ca69a27a3d5
3
+ metadata.gz: 280f1aab156292d75fc2e561f222bb7b92870cb85a68df13494f4614855f437f
4
+ data.tar.gz: dd83fcbf5303eb687017a7bf5783d4557a5e723e367c5c0641c0e072e8ddab23
5
5
  SHA512:
6
- metadata.gz: 94c9857b53bad2ac793ae931607ee74e06f02f2410a029a0cfd5568754daa4befd6d913257ec9fbb673c594b1701ce4a9ef7e2f7681dba7c16f0406e6a8886ea
7
- data.tar.gz: b55430f749eadd254de9b2feffd9d9aef09566166d47f33d42be29c58d826955d42a47f7c2fb704ccc807e0748e3b1bfcb7a245c558a38d014acabef2d3894c9
6
+ metadata.gz: b31d87695427970ae2dd48e4f01b3b39ca5e7e1b7ff1a57a83d231d386881975158a72d4501bb57d838f8247f832892483b70a815fba7b46ae8099eabce3da2d
7
+ data.tar.gz: 7e46d20ad61fd47243ad69b02ed95e282817e94b5726d5c3f8d46b1b72a9af4856de17de71b234d0fb2b8a6e506cb21bd4e199c0f1460e38e3c02463cb33a704
@@ -125,6 +125,12 @@ module MarkdownServer
125
125
  end
126
126
 
127
127
  def render_markdown(text)
128
+ # Convert mermaid code fences to raw HTML divs before Kramdown so Rouge
129
+ # never touches them and the content is preserved exactly for Mermaid.js.
130
+ text = text.gsub(/^```mermaid[ \t]*\r?\n([\s\S]*?)^```[ \t]*(\r?\n|\z)/m) do
131
+ "<div class=\"mermaid\">\n#{h($1.rstrip)}\n</div>\n\n"
132
+ end
133
+
128
134
  # Process wiki links BEFORE Kramdown so that | isn't consumed as
129
135
  # a GFM table delimiter.
130
136
  text = text.gsub(/\[\[([^\]]+)\]\]/) do
@@ -726,6 +732,23 @@ module MarkdownServer
726
732
  info_html + infl_html + usage_html + conc_html
727
733
  end
728
734
 
735
+ def inline_directory_html(dir_path, relative_dir)
736
+ entries = Dir.entries(dir_path).reject { |e| e.start_with?(".") || EXCLUDED.include?(e) }
737
+ items = entries.map do |name|
738
+ stat = File.stat(File.join(dir_path, name)) rescue next
739
+ is_dir = stat.directory?
740
+ href = "/browse/" + (relative_dir.empty? ? "" : relative_dir + "/") +
741
+ encode_path_component(name) + (is_dir ? "/" : "")
742
+ { name: name, is_dir: is_dir, href: href }
743
+ end.compact.sort_by { |i| [i[:is_dir] ? 0 : 1, i[:name].downcase] }
744
+
745
+ rows = items.map do |i|
746
+ %(<li><a href="#{h(i[:href])}"><span class="icon">#{icon_for(i[:name], i[:is_dir])}</span> ) +
747
+ %(#{h(i[:name])}#{i[:is_dir] ? "/" : ""}</a></li>)
748
+ end.join
749
+ %(<ul class="dir-listing">#{rows}</ul>)
750
+ end
751
+
729
752
  def compile_regexes(query)
730
753
  words = query.split(/\s+/).reject(&:empty?)
731
754
  return nil if words.empty?
@@ -1106,6 +1129,11 @@ module MarkdownServer
1106
1129
  @meta, body = parse_frontmatter(content)
1107
1130
  @current_wiki_dir = File.dirname(real_path)
1108
1131
  @content = render_markdown(body)
1132
+ relative_dir = File.dirname(relative_path)
1133
+ relative_dir = "" if relative_dir == "."
1134
+ listing = -> { inline_directory_html(File.dirname(real_path), relative_dir) }
1135
+ @content.gsub!("<p>{{directory}}</p>") { listing.call }
1136
+ @content.gsub!("<p>{{admin-directory}}</p>") { admin? ? listing.call : "" }
1109
1137
  @toc = extract_toc(@content)
1110
1138
  @has_toc = @toc.length > 1
1111
1139
  erb :markdown
@@ -1,3 +1,3 @@
1
1
  module MarkdownServer
2
- VERSION = "0.5.10"
2
+ VERSION = "0.5.12"
3
3
  end
data/views/layout.erb CHANGED
@@ -815,6 +815,32 @@
815
815
  flex-shrink: 0;
816
816
  }
817
817
  .link-ctx-popup-back:hover { color: #2c2c2c; }
818
+ .link-ctx-popup-pin {
819
+ background: none;
820
+ border: none;
821
+ color: #bbb;
822
+ cursor: pointer;
823
+ padding: 0 0.2rem;
824
+ flex-shrink: 0;
825
+ display: flex;
826
+ align-items: center;
827
+ line-height: 1;
828
+ }
829
+ .link-ctx-popup-pin:hover { color: #2c2c2c; }
830
+ .link-ctx-popup-pin.pinned { color: #8b6914; }
831
+ .link-ctx-popup--pinned {
832
+ resize: both;
833
+ max-height: none;
834
+ min-width: 280px;
835
+ min-height: 120px;
836
+ }
837
+ .link-ctx-popup--pinned .link-ctx-popup-header {
838
+ cursor: grab;
839
+ user-select: none;
840
+ }
841
+ .link-ctx-popup--pinned .link-ctx-popup-header.is-dragging {
842
+ cursor: grabbing;
843
+ }
818
844
  .link-ctx-popup-body {
819
845
  padding: 0.75rem 1rem;
820
846
  }
@@ -829,6 +855,10 @@
829
855
  margin-bottom: 0.5rem;
830
856
  }
831
857
 
858
+ /* Mermaid diagrams */
859
+ .mermaid { text-align: center; margin: 1.2rem 0; overflow-x: auto; }
860
+ .mermaid svg { max-width: 100%; height: auto; }
861
+
832
862
  /* Link preview popup (hover) */
833
863
  .link-tooltip-anchor { position: relative; }
834
864
  .link-preview-popup {
@@ -1219,6 +1249,7 @@
1219
1249
  var touchStartX = 0;
1220
1250
  var touchStartY = 0;
1221
1251
  var touchCurrentX = 0;
1252
+ var touchStartedOnTable = false;
1222
1253
  var isDragging = false;
1223
1254
 
1224
1255
  function openDrawer() {
@@ -1277,6 +1308,7 @@
1277
1308
  touchStartY = e.touches[0].clientY;
1278
1309
  touchCurrentX = touchStartX;
1279
1310
  isDragging = false;
1311
+ touchStartedOnTable = !!e.target.closest('table');
1280
1312
  }, { passive: true });
1281
1313
 
1282
1314
  document.addEventListener('touchmove', function(e) {
@@ -1315,7 +1347,8 @@
1315
1347
  if (dy > 80) return; // Too vertical
1316
1348
 
1317
1349
  if (!isOpen && dx < -threshold) {
1318
- // Swipe left — open drawer
1350
+ // Swipe left — open drawer (not when swipe started on a table)
1351
+ if (touchStartedOnTable) return;
1319
1352
  openDrawer();
1320
1353
  } else if (isOpen && dx > threshold) {
1321
1354
  // Swipe right — close drawer
@@ -1547,12 +1580,34 @@
1547
1580
  }, { passive: true });
1548
1581
  })();
1549
1582
 
1583
+ // Mermaid diagram rendering — lazily loads Mermaid.js on first use
1584
+ var mermaidReady = false;
1585
+ var mermaidQueue = [];
1586
+ function runMermaidIn(container) {
1587
+ var nodes = Array.prototype.slice.call(container.querySelectorAll('.mermaid:not([data-processed="true"])'));
1588
+ if (!nodes.length) return;
1589
+ var doRender = function() { mermaid.run({ nodes: nodes }); };
1590
+ if (mermaidReady) { doRender(); return; }
1591
+ mermaidQueue.push(doRender);
1592
+ if (mermaidQueue.length > 1) return;
1593
+ var s = document.createElement('script');
1594
+ s.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
1595
+ s.onload = function() {
1596
+ mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
1597
+ mermaidReady = true;
1598
+ mermaidQueue.splice(0).forEach(function(fn) { fn(); });
1599
+ };
1600
+ document.head.appendChild(s);
1601
+ }
1602
+ runMermaidIn(document.body);
1603
+
1550
1604
  // Left-click / tap popup for all links
1551
1605
  (function() {
1552
1606
  var cache = Object.create(null);
1553
1607
  var popup = null;
1554
1608
  var touchMoved = false;
1555
1609
  var historyStack = [];
1610
+ var pinnedPopups = [];
1556
1611
  var currentPopupPos = { x: 0, y: 0 };
1557
1612
  var mouseLeaveTimer = null;
1558
1613
 
@@ -1581,6 +1636,7 @@
1581
1636
  }
1582
1637
 
1583
1638
  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>';
1639
+ var pinIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z"/></svg>';
1584
1640
 
1585
1641
  function showPopup(x, y, title, bodyHtml, href, linkRect) {
1586
1642
  // Remove old popup without clearing historyStack
@@ -1590,6 +1646,7 @@
1590
1646
 
1591
1647
  popup = document.createElement('div');
1592
1648
  popup.className = 'link-ctx-popup';
1649
+ var thisPopup = popup;
1593
1650
  var backBtn = historyStack.length > 0 ? '<button class="link-ctx-popup-back" aria-label="Back">\u2190</button>' : '';
1594
1651
  var titleHtml = href
1595
1652
  ? '<div class="link-ctx-popup-title-wrap">' +
@@ -1600,6 +1657,7 @@
1600
1657
  popup.innerHTML =
1601
1658
  '<div class="link-ctx-popup-header">' +
1602
1659
  backBtn + titleHtml +
1660
+ '<button class="link-ctx-popup-pin" aria-label="Pin" title="Pin popup">' + pinIcon + '</button>' +
1603
1661
  '<button class="link-ctx-popup-close" aria-label="Close">\u00d7</button>' +
1604
1662
  '</div>' +
1605
1663
  '<div class="link-ctx-popup-body">' + bodyHtml + '</div>';
@@ -1607,27 +1665,38 @@
1607
1665
 
1608
1666
  clearTimeout(mouseLeaveTimer);
1609
1667
  popup.addEventListener('mouseleave', function() {
1668
+ if (thisPopup.dataset.pinned) return;
1610
1669
  mouseLeaveTimer = setTimeout(hidePopup, 150);
1611
1670
  });
1612
1671
  popup.addEventListener('mouseenter', function() {
1672
+ if (thisPopup.dataset.pinned) return;
1613
1673
  clearTimeout(mouseLeaveTimer);
1614
1674
  });
1615
1675
 
1616
1676
  repositionPopup();
1677
+ runMermaidIn(popup);
1617
1678
 
1618
1679
  var backBtnEl = popup.querySelector('.link-ctx-popup-back');
1619
1680
  if (backBtnEl) {
1620
1681
  backBtnEl.addEventListener('click', function(e) {
1621
1682
  e.stopPropagation();
1622
1683
  var prev = historyStack.pop();
1623
- if (prev) showPopup(prev.x, prev.y, prev.title, prev.bodyHtml, prev.href, prev.linkRect);
1684
+ if (prev) {
1685
+ showPopup(prev.x, prev.y, prev.title, prev.bodyHtml, prev.href, prev.linkRect);
1686
+ if (popup && prev.scrollTop) popup.scrollTop = prev.scrollTop;
1687
+ }
1624
1688
  });
1625
1689
  }
1626
1690
  popup.querySelector('.link-ctx-popup-close').addEventListener('click', hidePopup);
1691
+ popup.querySelector('.link-ctx-popup-pin').addEventListener('click', function(e) {
1692
+ e.stopPropagation();
1693
+ if (!thisPopup.dataset.pinned) pinPopup(thisPopup);
1694
+ });
1627
1695
  popup.addEventListener('click', function(e) { e.stopPropagation(); });
1628
1696
 
1629
1697
  // Links in the popup body push current state to history, then open a new popup
1630
- // at the same screen position as the current popup
1698
+ // at the same screen position as the current popup.
1699
+ // When the popup is pinned, links open a new floating popup at the click position instead.
1631
1700
  var body = popup.querySelector('.link-ctx-popup-body');
1632
1701
  body.addEventListener('click', function(e) {
1633
1702
  var anchor = findLink(e.target);
@@ -1636,13 +1705,15 @@
1636
1705
  if (!linkHref || isAnchorOnly(linkHref)) return;
1637
1706
  e.stopPropagation();
1638
1707
  e.preventDefault();
1708
+ if (thisPopup.dataset.pinned) { handleLink(anchor, e.clientX, e.clientY, false); return; }
1639
1709
  var savedPos = { x: currentPopupPos.x, y: currentPopupPos.y, linkRect: currentPopupPos.linkRect };
1640
- var titleEl = popup.querySelector('.link-ctx-popup-title');
1710
+ var titleEl = thisPopup.querySelector('.link-ctx-popup-title');
1641
1711
  historyStack.push({
1642
1712
  x: savedPos.x, y: savedPos.y, linkRect: savedPos.linkRect,
1643
1713
  title: titleEl ? titleEl.querySelector('span').textContent : '',
1644
1714
  bodyHtml: body.innerHTML,
1645
- href: titleEl ? (titleEl.getAttribute('href') || '') : ''
1715
+ href: titleEl ? (titleEl.getAttribute('href') || '') : '',
1716
+ scrollTop: thisPopup.scrollTop
1646
1717
  });
1647
1718
  handleLink(anchor, savedPos.x, savedPos.y, true);
1648
1719
  });
@@ -1654,13 +1725,15 @@
1654
1725
  if (!linkHref || isAnchorOnly(linkHref)) return;
1655
1726
  e.stopPropagation();
1656
1727
  e.preventDefault();
1728
+ if (thisPopup.dataset.pinned) { handleLink(anchor, e.clientX, e.clientY, false); return; }
1657
1729
  var savedPos = { x: currentPopupPos.x, y: currentPopupPos.y, linkRect: currentPopupPos.linkRect };
1658
- var titleEl = popup.querySelector('.link-ctx-popup-title');
1730
+ var titleEl = thisPopup.querySelector('.link-ctx-popup-title');
1659
1731
  historyStack.push({
1660
1732
  x: savedPos.x, y: savedPos.y, linkRect: savedPos.linkRect,
1661
1733
  title: titleEl ? titleEl.querySelector('span').textContent : '',
1662
1734
  bodyHtml: body.innerHTML,
1663
- href: titleEl ? (titleEl.getAttribute('href') || '') : ''
1735
+ href: titleEl ? (titleEl.getAttribute('href') || '') : '',
1736
+ scrollTop: thisPopup.scrollTop
1664
1737
  });
1665
1738
  handleLink(anchor, savedPos.x, savedPos.y, true);
1666
1739
  }, { passive: false });
@@ -1670,6 +1743,80 @@
1670
1743
  });
1671
1744
  }
1672
1745
 
1746
+ function pinPopup(el) {
1747
+ clearTimeout(mouseLeaveTimer);
1748
+ var ownHistory = historyStack.slice();
1749
+ popup = null;
1750
+ historyStack = [];
1751
+ el.style.width = el.offsetWidth + 'px';
1752
+ el.style.height = el.offsetHeight + 'px';
1753
+ el.dataset.pinned = '1';
1754
+ el.classList.add('link-ctx-popup--pinned');
1755
+ var pinBtn = el.querySelector('.link-ctx-popup-pin');
1756
+ if (pinBtn) pinBtn.classList.add('pinned');
1757
+ // Re-wire back button to operate on this element's own history
1758
+ var backBtn = el.querySelector('.link-ctx-popup-back');
1759
+ if (backBtn) {
1760
+ var newBack = backBtn.cloneNode(true);
1761
+ backBtn.parentNode.replaceChild(newBack, backBtn);
1762
+ newBack.addEventListener('click', function(e) {
1763
+ e.stopPropagation();
1764
+ goBackInPinned(el, ownHistory);
1765
+ });
1766
+ }
1767
+ // Re-wire close button to remove this element
1768
+ var closeBtn = el.querySelector('.link-ctx-popup-close');
1769
+ var newClose = closeBtn.cloneNode(true);
1770
+ closeBtn.parentNode.replaceChild(newClose, closeBtn);
1771
+ newClose.addEventListener('click', function(e) {
1772
+ e.stopPropagation();
1773
+ if (el.parentNode) el.parentNode.removeChild(el);
1774
+ pinnedPopups = pinnedPopups.filter(function(p) { return p !== el; });
1775
+ });
1776
+ makeDraggable(el);
1777
+ pinnedPopups.push(el);
1778
+ }
1779
+
1780
+ function goBackInPinned(el, ownHistory) {
1781
+ var prev = ownHistory.pop();
1782
+ if (!prev) return;
1783
+ var body = el.querySelector('.link-ctx-popup-body');
1784
+ if (body) body.innerHTML = prev.bodyHtml;
1785
+ var titleSpan = el.querySelector('.link-ctx-popup-title span');
1786
+ if (titleSpan) titleSpan.textContent = prev.title;
1787
+ var titleLink = el.querySelector('a.link-ctx-popup-title');
1788
+ if (titleLink && prev.href) titleLink.href = prev.href;
1789
+ var backBtnEl = el.querySelector('.link-ctx-popup-back');
1790
+ if (backBtnEl) backBtnEl.style.display = ownHistory.length > 0 ? '' : 'none';
1791
+ if (prev.scrollTop) el.scrollTop = prev.scrollTop;
1792
+ }
1793
+
1794
+ function makeDraggable(el) {
1795
+ var header = el.querySelector('.link-ctx-popup-header');
1796
+ if (!header) return;
1797
+ var startX, startY, startLeft, startTop;
1798
+ header.addEventListener('mousedown', function(e) {
1799
+ if (e.target.closest('button') || e.target.closest('a')) return;
1800
+ e.preventDefault();
1801
+ startX = e.clientX;
1802
+ startY = e.clientY;
1803
+ startLeft = el.offsetLeft;
1804
+ startTop = el.offsetTop;
1805
+ header.classList.add('is-dragging');
1806
+ document.addEventListener('mousemove', onMove);
1807
+ document.addEventListener('mouseup', onUp);
1808
+ });
1809
+ function onMove(e) {
1810
+ el.style.left = (startLeft + e.clientX - startX) + 'px';
1811
+ el.style.top = (startTop + e.clientY - startY) + 'px';
1812
+ }
1813
+ function onUp() {
1814
+ header.classList.remove('is-dragging');
1815
+ document.removeEventListener('mousemove', onMove);
1816
+ document.removeEventListener('mouseup', onUp);
1817
+ }
1818
+ }
1819
+
1673
1820
  function repositionPopup() {
1674
1821
  if (!popup) return;
1675
1822
  var x = currentPopupPos.x, y = currentPopupPos.y;
@@ -1713,6 +1860,7 @@
1713
1860
  if (body) body.innerHTML = bodyHtml;
1714
1861
  if (title && titleTextEl) titleTextEl.textContent = title;
1715
1862
  repositionPopup();
1863
+ runMermaidIn(popup);
1716
1864
  }
1717
1865
 
1718
1866
  function hidePopup() {
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.10
4
+ version: 0.5.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Dunn
@@ -101,8 +101,8 @@ extensions: []
101
101
  extra_rdoc_files: []
102
102
  files:
103
103
  - bin/Dockerfile.markdownr
104
+ - bin/build-and-push-to-docker
104
105
  - bin/markdownr
105
- - bin/push-to-docker
106
106
  - lib/markdown_server.rb
107
107
  - lib/markdown_server/app.rb
108
108
  - lib/markdown_server/version.rb
File without changes