markdownr 0.5.11 → 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: 49c6809e18e2fe51a4e65446e185529edd529e61bb77e6eea79696b734ebdb8b
4
- data.tar.gz: 9d533023489082e69413b4e718c27efa05076f1da863f25d9d68de26bd3ec090
3
+ metadata.gz: 280f1aab156292d75fc2e561f222bb7b92870cb85a68df13494f4614855f437f
4
+ data.tar.gz: dd83fcbf5303eb687017a7bf5783d4557a5e723e367c5c0641c0e072e8ddab23
5
5
  SHA512:
6
- metadata.gz: a2602173ee41f8d6034e410a8deba4fb11f5f3149e39c410dbc6366349dc93ba0bec519c6860e4970a7f14ed20dd5f8eeadf03d6452e821993e0d0450bebf19e
7
- data.tar.gz: a5dc9a0bb156371fddcbdcdab626ee36b82079a175c56d967f7d4ed1f746d5f191bf709efbb7c0d4cb4f8419d072f51b1d92c8e39e90c8e643c220ebe57d4cbf
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
@@ -1,3 +1,3 @@
1
1
  module MarkdownServer
2
- VERSION = "0.5.11"
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 {
@@ -1550,12 +1580,34 @@
1550
1580
  }, { passive: true });
1551
1581
  })();
1552
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
+
1553
1604
  // Left-click / tap popup for all links
1554
1605
  (function() {
1555
1606
  var cache = Object.create(null);
1556
1607
  var popup = null;
1557
1608
  var touchMoved = false;
1558
1609
  var historyStack = [];
1610
+ var pinnedPopups = [];
1559
1611
  var currentPopupPos = { x: 0, y: 0 };
1560
1612
  var mouseLeaveTimer = null;
1561
1613
 
@@ -1584,6 +1636,7 @@
1584
1636
  }
1585
1637
 
1586
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>';
1587
1640
 
1588
1641
  function showPopup(x, y, title, bodyHtml, href, linkRect) {
1589
1642
  // Remove old popup without clearing historyStack
@@ -1593,6 +1646,7 @@
1593
1646
 
1594
1647
  popup = document.createElement('div');
1595
1648
  popup.className = 'link-ctx-popup';
1649
+ var thisPopup = popup;
1596
1650
  var backBtn = historyStack.length > 0 ? '<button class="link-ctx-popup-back" aria-label="Back">\u2190</button>' : '';
1597
1651
  var titleHtml = href
1598
1652
  ? '<div class="link-ctx-popup-title-wrap">' +
@@ -1603,6 +1657,7 @@
1603
1657
  popup.innerHTML =
1604
1658
  '<div class="link-ctx-popup-header">' +
1605
1659
  backBtn + titleHtml +
1660
+ '<button class="link-ctx-popup-pin" aria-label="Pin" title="Pin popup">' + pinIcon + '</button>' +
1606
1661
  '<button class="link-ctx-popup-close" aria-label="Close">\u00d7</button>' +
1607
1662
  '</div>' +
1608
1663
  '<div class="link-ctx-popup-body">' + bodyHtml + '</div>';
@@ -1610,27 +1665,38 @@
1610
1665
 
1611
1666
  clearTimeout(mouseLeaveTimer);
1612
1667
  popup.addEventListener('mouseleave', function() {
1668
+ if (thisPopup.dataset.pinned) return;
1613
1669
  mouseLeaveTimer = setTimeout(hidePopup, 150);
1614
1670
  });
1615
1671
  popup.addEventListener('mouseenter', function() {
1672
+ if (thisPopup.dataset.pinned) return;
1616
1673
  clearTimeout(mouseLeaveTimer);
1617
1674
  });
1618
1675
 
1619
1676
  repositionPopup();
1677
+ runMermaidIn(popup);
1620
1678
 
1621
1679
  var backBtnEl = popup.querySelector('.link-ctx-popup-back');
1622
1680
  if (backBtnEl) {
1623
1681
  backBtnEl.addEventListener('click', function(e) {
1624
1682
  e.stopPropagation();
1625
1683
  var prev = historyStack.pop();
1626
- 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
+ }
1627
1688
  });
1628
1689
  }
1629
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
+ });
1630
1695
  popup.addEventListener('click', function(e) { e.stopPropagation(); });
1631
1696
 
1632
1697
  // Links in the popup body push current state to history, then open a new popup
1633
- // 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.
1634
1700
  var body = popup.querySelector('.link-ctx-popup-body');
1635
1701
  body.addEventListener('click', function(e) {
1636
1702
  var anchor = findLink(e.target);
@@ -1639,13 +1705,15 @@
1639
1705
  if (!linkHref || isAnchorOnly(linkHref)) return;
1640
1706
  e.stopPropagation();
1641
1707
  e.preventDefault();
1708
+ if (thisPopup.dataset.pinned) { handleLink(anchor, e.clientX, e.clientY, false); return; }
1642
1709
  var savedPos = { x: currentPopupPos.x, y: currentPopupPos.y, linkRect: currentPopupPos.linkRect };
1643
- var titleEl = popup.querySelector('.link-ctx-popup-title');
1710
+ var titleEl = thisPopup.querySelector('.link-ctx-popup-title');
1644
1711
  historyStack.push({
1645
1712
  x: savedPos.x, y: savedPos.y, linkRect: savedPos.linkRect,
1646
1713
  title: titleEl ? titleEl.querySelector('span').textContent : '',
1647
1714
  bodyHtml: body.innerHTML,
1648
- href: titleEl ? (titleEl.getAttribute('href') || '') : ''
1715
+ href: titleEl ? (titleEl.getAttribute('href') || '') : '',
1716
+ scrollTop: thisPopup.scrollTop
1649
1717
  });
1650
1718
  handleLink(anchor, savedPos.x, savedPos.y, true);
1651
1719
  });
@@ -1657,13 +1725,15 @@
1657
1725
  if (!linkHref || isAnchorOnly(linkHref)) return;
1658
1726
  e.stopPropagation();
1659
1727
  e.preventDefault();
1728
+ if (thisPopup.dataset.pinned) { handleLink(anchor, e.clientX, e.clientY, false); return; }
1660
1729
  var savedPos = { x: currentPopupPos.x, y: currentPopupPos.y, linkRect: currentPopupPos.linkRect };
1661
- var titleEl = popup.querySelector('.link-ctx-popup-title');
1730
+ var titleEl = thisPopup.querySelector('.link-ctx-popup-title');
1662
1731
  historyStack.push({
1663
1732
  x: savedPos.x, y: savedPos.y, linkRect: savedPos.linkRect,
1664
1733
  title: titleEl ? titleEl.querySelector('span').textContent : '',
1665
1734
  bodyHtml: body.innerHTML,
1666
- href: titleEl ? (titleEl.getAttribute('href') || '') : ''
1735
+ href: titleEl ? (titleEl.getAttribute('href') || '') : '',
1736
+ scrollTop: thisPopup.scrollTop
1667
1737
  });
1668
1738
  handleLink(anchor, savedPos.x, savedPos.y, true);
1669
1739
  }, { passive: false });
@@ -1673,6 +1743,80 @@
1673
1743
  });
1674
1744
  }
1675
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
+
1676
1820
  function repositionPopup() {
1677
1821
  if (!popup) return;
1678
1822
  var x = currentPopupPos.x, y = currentPopupPos.y;
@@ -1716,6 +1860,7 @@
1716
1860
  if (body) body.innerHTML = bodyHtml;
1717
1861
  if (title && titleTextEl) titleTextEl.textContent = title;
1718
1862
  repositionPopup();
1863
+ runMermaidIn(popup);
1719
1864
  }
1720
1865
 
1721
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.11
4
+ version: 0.5.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Dunn