markdownr 0.4.3 → 0.4.5

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: 2e9f4e6b7e443f5f0ae16ff97d7e381935a289e8ef8c385d93dfd961f24437ed
4
- data.tar.gz: 489c935af4f12623485a48eaa88fe4ebcadb94185c711e5b5ded10dffad7cc51
3
+ metadata.gz: 53062f10b38520d041fb4fab0e1155e1b19e15242409996dd82122716bc727d8
4
+ data.tar.gz: 17ee73d28a5ddc64a915d190f31c157c6d8c9319451c1fe754c3c41417bab9a9
5
5
  SHA512:
6
- metadata.gz: bc3fe4f3598f41a12210e61859cc2ee804f9606bb22684e9d6efba05b1cf379b14f77ad33685e2843b4a28785588aa94ecd8b94a3db93d31f65667af608bcdca
7
- data.tar.gz: 509b26791611400e61917a5ff53195ad7077d34c78e055a546e6bf024d0420b9300720b18b74a8f05c12960c7a1aea67f5065e34979616313e060314dceef31f
6
+ metadata.gz: 8c762e19d73d839cdcf1ff5f4e16c009f22b575a5672936eabef2d5131641d639ae26771bce19f25adbb287e6f7c5d71bb0561b76f302ea32db4b3ad41dc2937
7
+ data.tar.gz: e28d99ad58d6ee904aa3814f665fa31d0313605629be193a482b6f0ef30cce74868800a18cec6db0ecb0101f3182dc48d6b8b561a75d9e39f04db8b7daa9e443
data/bin/markdownr CHANGED
@@ -27,6 +27,10 @@ OptionParser.new do |opts|
27
27
  options[:link_tooltips] = false
28
28
  end
29
29
 
30
+ opts.on("--standard-newlines", "Use standard markdown newlines (two newlines = paragraph; default is Obsidian-style where single newlines become line breaks)") do
31
+ options[:hard_wrap] = false
32
+ end
33
+
30
34
  opts.on("-v", "--version", "Show version") do
31
35
  puts "markdownr #{MarkdownServer::VERSION}"
32
36
  exit
@@ -45,6 +49,7 @@ MarkdownServer::App.set :root_dir, dir
45
49
  MarkdownServer::App.set :custom_title, options[:title]
46
50
  MarkdownServer::App.set :allow_robots, options[:allow_robots] || false
47
51
  MarkdownServer::App.set :link_tooltips, options.fetch(:link_tooltips, true)
52
+ MarkdownServer::App.set :hard_wrap, options.fetch(:hard_wrap, true)
48
53
  MarkdownServer::App.set :port, options[:port]
49
54
  MarkdownServer::App.set :bind, options[:bind]
50
55
 
@@ -20,6 +20,7 @@ module MarkdownServer
20
20
  set :custom_title, nil
21
21
  set :allow_robots, false
22
22
  set :link_tooltips, true
23
+ set :hard_wrap, true
23
24
  set :show_exceptions, false
24
25
  set :protection, false
25
26
  set :host_authorization, { permitted_hosts: [] }
@@ -150,7 +151,7 @@ module MarkdownServer
150
151
  input: "GFM",
151
152
  syntax_highlighter: "rouge",
152
153
  syntax_highlighter_opts: { default_lang: "text" },
153
- hard_wrap: false
154
+ hard_wrap: settings.hard_wrap
154
155
  ).to_html
155
156
 
156
157
  # Restore non-numeric footnote labels: Kramdown converts all footnote
@@ -482,8 +483,6 @@ module MarkdownServer
482
483
 
483
484
  get "/preview/*" do
484
485
  content_type :json
485
- halt 404, '{"error":"disabled"}' unless settings.link_tooltips
486
-
487
486
  requested = params["splat"].first.to_s
488
487
  base = File.realpath(root_dir)
489
488
  full = File.join(base, requested)
@@ -1,3 +1,3 @@
1
1
  module MarkdownServer
2
- VERSION = "0.4.3"
2
+ VERSION = "0.4.5"
3
3
  end
data/views/layout.erb CHANGED
@@ -678,6 +678,111 @@
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 */
682
+ .link-ctx-popup {
683
+ position: fixed;
684
+ z-index: 500;
685
+ width: 460px;
686
+ max-width: calc(100vw - 16px);
687
+ max-height: 60vh;
688
+ overflow-y: auto;
689
+ background: #faf8f4;
690
+ border: 1px solid #d4b96a;
691
+ border-radius: 6px;
692
+ box-shadow: 0 4px 20px rgba(0,0,0,0.22);
693
+ -webkit-overflow-scrolling: touch;
694
+ cursor: auto;
695
+ }
696
+ .link-ctx-popup-header {
697
+ display: flex;
698
+ justify-content: space-between;
699
+ align-items: center;
700
+ gap: 0.5rem;
701
+ padding: 0.5rem 0.9rem;
702
+ border-bottom: 1px solid #e0d8c8;
703
+ position: sticky;
704
+ top: 0;
705
+ background: #faf8f4;
706
+ z-index: 1;
707
+ }
708
+ .link-ctx-popup-title {
709
+ font-family: Georgia, "Times New Roman", serif;
710
+ font-weight: 700;
711
+ font-size: 0.9rem;
712
+ color: #3a3a3a;
713
+ min-width: 0;
714
+ word-break: break-word;
715
+ }
716
+ .link-ctx-popup-close {
717
+ background: none;
718
+ border: none;
719
+ font-size: 1.2rem;
720
+ line-height: 1;
721
+ color: #888;
722
+ cursor: pointer;
723
+ padding: 0 0.2rem;
724
+ flex-shrink: 0;
725
+ }
726
+ .link-ctx-popup-close:hover { color: #2c2c2c; }
727
+ .link-ctx-popup-body {
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
+ }
734
+ .link-ctx-popup-url {
735
+ font-family: "SF Mono", Menlo, Consolas, monospace;
736
+ font-size: 0.75rem;
737
+ color: #555;
738
+ word-break: break-all;
739
+ background: #f0ece3;
740
+ padding: 0.35rem 0.6rem;
741
+ border-radius: 4px;
742
+ margin-bottom: 0.5rem;
743
+ }
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
+
681
786
  /* Link preview popup */
682
787
  .link-tooltip-anchor { position: relative; }
683
788
  .link-preview-popup {
@@ -1322,6 +1427,187 @@
1322
1427
  }, { passive: true });
1323
1428
  })();
1324
1429
 
1430
+ // Right-click / long-press popup for all links
1431
+ (function() {
1432
+ var cache = Object.create(null);
1433
+ var popup = null;
1434
+ var longPressTimer = null;
1435
+ var touchMoved = false;
1436
+ var pendingTouch = null;
1437
+ var longPressHandled = false;
1438
+
1439
+ function escHtml(s) {
1440
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1441
+ }
1442
+
1443
+ function findLink(el) {
1444
+ while (el && el.tagName !== 'BODY') {
1445
+ if (el.tagName === 'A' && el.getAttribute('href')) return el;
1446
+ el = el.parentElement;
1447
+ }
1448
+ return null;
1449
+ }
1450
+
1451
+ function isAnchorOnly(href) { return href.charAt(0) === '#'; }
1452
+ function isLocalMd(href) {
1453
+ if (/^https?:\/\//i.test(href)) return false;
1454
+ return /\.md([?#]|$)/i.test(href);
1455
+ }
1456
+ function isExternal(href) { return /^https?:\/\//i.test(href); }
1457
+
1458
+ function previewPath(href) {
1459
+ var resolved = new URL(href, location.href);
1460
+ return '/preview/' + resolved.pathname.replace(/^\/browse\//, '');
1461
+ }
1462
+
1463
+ function showPopup(x, y, title, bodyHtml) {
1464
+ hidePopup();
1465
+ popup = document.createElement('div');
1466
+ popup.className = 'link-ctx-popup';
1467
+ popup.innerHTML =
1468
+ '<div class="link-ctx-popup-header">' +
1469
+ '<span class="link-ctx-popup-title">' + escHtml(title) + '</span>' +
1470
+ '<button class="link-ctx-popup-close" aria-label="Close">\u00d7</button>' +
1471
+ '</div>' +
1472
+ '<div class="link-ctx-popup-body">' + bodyHtml + '</div>';
1473
+ document.body.appendChild(popup);
1474
+
1475
+ // Position near pointer, clamped to viewport
1476
+ var vw = Math.min(window.innerWidth, document.documentElement.clientWidth);
1477
+ var vh = window.innerHeight;
1478
+ var left = x + 12;
1479
+ var top = y + 12;
1480
+ if (left + popup.offsetWidth > vw - 8) left = Math.max(8, vw - popup.offsetWidth - 8);
1481
+ if (top + popup.offsetHeight > vh - 8) top = Math.max(8, y - popup.offsetHeight - 12);
1482
+ if (top < 8) top = 8;
1483
+ popup.style.left = left + 'px';
1484
+ popup.style.top = top + 'px';
1485
+
1486
+ popup.querySelector('.link-ctx-popup-close').addEventListener('click', hidePopup);
1487
+ popup.addEventListener('click', function(e) { e.stopPropagation(); });
1488
+ popup.addEventListener('contextmenu', function(e) { e.preventDefault(); e.stopPropagation(); });
1489
+ ['touchstart','touchmove','touchend'].forEach(function(ev) {
1490
+ popup.addEventListener(ev, function(e) { e.stopPropagation(); }, { passive: true });
1491
+ });
1492
+ }
1493
+
1494
+ function updatePopup(bodyHtml, title) {
1495
+ if (!popup) return;
1496
+ var body = popup.querySelector('.link-ctx-popup-body');
1497
+ var titleEl = popup.querySelector('.link-ctx-popup-title');
1498
+ if (body) body.innerHTML = bodyHtml;
1499
+ if (title && titleEl) titleEl.textContent = title;
1500
+ }
1501
+
1502
+ function hidePopup() {
1503
+ if (popup && popup.parentNode) popup.parentNode.removeChild(popup);
1504
+ popup = null;
1505
+ }
1506
+
1507
+ function handleLink(anchor, x, y) {
1508
+ var href = anchor.getAttribute('href');
1509
+ if (!href || isAnchorOnly(href)) return;
1510
+ var label = anchor.textContent.trim() || href;
1511
+
1512
+ if (isLocalMd(href)) {
1513
+ var path = previewPath(href);
1514
+ var cached = cache[path];
1515
+ if (cached && typeof cached === 'object') {
1516
+ showPopup(x, y, cached.title || label, cached.html);
1517
+ } else if (cached === false) {
1518
+ showPopup(x, y, label,
1519
+ '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
1520
+ '<p style="margin:0;color:#888;font-family:sans-serif;font-size:0.82rem">Preview not available</p>');
1521
+ } else {
1522
+ showPopup(x, y, label, '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>');
1523
+ if (cached === undefined) {
1524
+ cache[path] = null;
1525
+ fetch(path)
1526
+ .then(function(r) { return r.ok ? r.json() : null; })
1527
+ .then(function(data) {
1528
+ if (!data) { cache[path] = false; updatePopup('<p style="margin:0;color:#c44;font-family:sans-serif;font-size:0.85rem">Preview unavailable.</p>'); return; }
1529
+ cache[path] = { title: data.title, html: data.html };
1530
+ updatePopup(data.html, data.title || label);
1531
+ })
1532
+ .catch(function() {
1533
+ cache[path] = false;
1534
+ updatePopup('<p style="margin:0;color:#c44;font-family:sans-serif;font-size:0.85rem">Preview unavailable.</p>');
1535
+ });
1536
+ }
1537
+ }
1538
+ } else {
1539
+ var urlBody = '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>';
1540
+ if (isExternal(href)) {
1541
+ var host = '';
1542
+ try { host = new URL(href).hostname; } catch(e) {}
1543
+ if (host) urlBody += '<p style="margin:0;color:#888;font-family:sans-serif;font-size:0.82rem">External: <strong style="color:#555">' + escHtml(host) + '</strong></p>';
1544
+ }
1545
+ showPopup(x, y, label, urlBody);
1546
+ }
1547
+ }
1548
+
1549
+ // Right-click (desktop + most mobile browsers fire contextmenu on long-press)
1550
+ document.addEventListener('contextmenu', function(e) {
1551
+ if (popup && popup.contains(e.target)) { e.preventDefault(); return; }
1552
+ var anchor = findLink(e.target);
1553
+ if (!anchor) return;
1554
+ var href = anchor.getAttribute('href');
1555
+ if (!href || isAnchorOnly(href)) return;
1556
+ e.preventDefault();
1557
+ clearTimeout(longPressTimer);
1558
+ pendingTouch = null;
1559
+ if (!longPressHandled) handleLink(anchor, e.clientX, e.clientY);
1560
+ longPressHandled = false;
1561
+ });
1562
+
1563
+ // Long-press fallback for touch devices that don't fire contextmenu
1564
+ document.addEventListener('touchstart', function(e) {
1565
+ if (e.touches.length !== 1) return;
1566
+ if (popup && popup.contains(e.target)) return;
1567
+ var anchor = findLink(e.target);
1568
+ if (!anchor) return;
1569
+ var href = anchor.getAttribute('href');
1570
+ if (!href || isAnchorOnly(href)) return;
1571
+ touchMoved = false;
1572
+ longPressHandled = false;
1573
+ var t = e.touches[0];
1574
+ pendingTouch = { anchor: anchor, x: t.clientX, y: t.clientY };
1575
+ clearTimeout(longPressTimer);
1576
+ longPressTimer = setTimeout(function() {
1577
+ if (!touchMoved && pendingTouch) {
1578
+ longPressHandled = true;
1579
+ handleLink(pendingTouch.anchor, pendingTouch.x, pendingTouch.y);
1580
+ pendingTouch = null;
1581
+ setTimeout(function() { longPressHandled = false; }, 400);
1582
+ }
1583
+ }, 600);
1584
+ }, { passive: true });
1585
+
1586
+ document.addEventListener('touchmove', function() {
1587
+ touchMoved = true;
1588
+ clearTimeout(longPressTimer);
1589
+ pendingTouch = null;
1590
+ }, { passive: true });
1591
+
1592
+ document.addEventListener('touchend', function() {
1593
+ clearTimeout(longPressTimer);
1594
+ pendingTouch = null;
1595
+ }, { passive: true });
1596
+
1597
+ document.addEventListener('touchcancel', function() {
1598
+ clearTimeout(longPressTimer);
1599
+ pendingTouch = null;
1600
+ longPressHandled = false;
1601
+ }, { passive: true });
1602
+
1603
+ document.addEventListener('click', function(e) {
1604
+ if (popup && !popup.contains(e.target)) hidePopup();
1605
+ });
1606
+ document.addEventListener('keydown', function(e) {
1607
+ if (e.key === 'Escape' && popup) hidePopup();
1608
+ });
1609
+ })();
1610
+
1325
1611
  <% if settings.link_tooltips %>
1326
1612
  // Link preview popup for local markdown links
1327
1613
  (function() {
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.3
4
+ version: 0.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Dunn