markdownr 0.6.4 → 0.6.6

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: 845f5eba9de7543ef7e3c386e54f127d8ecfaf48e368d5fb11de54ca5d96cc6c
4
- data.tar.gz: 9b00fb58e4c3a4e9e3b5c8236f30b96577f7e8bd4d18a17d8576b6382d568380
3
+ metadata.gz: 3c823e9362df716b74c9f6523e2cc0e2a7873ee7b1655b78da6d284921e85a6d
4
+ data.tar.gz: 479d4c5709c1c66560bb9c7116af9c14992f8a8feedb7c2a9eaa58e12af36b6f
5
5
  SHA512:
6
- metadata.gz: 619d4b659c762122321ee3215e06b996c111d88f36bcc83a57b7613f2c9d241a9dea383bb2fbd86c9afa3a579085c7ae7b42519f597e795cb327f23426a5de7d
7
- data.tar.gz: cf8bc3acf215f5fae8fbbafa137a231e8b37a1db8e73691f95cdc4f1f4acdd9631b19200cf4483b3dcad8df4c95d1a5ee6487cc34e7fb9de16c5c7d0d516b7d7
6
+ metadata.gz: 859817ec7ee12fac0837634bcf3b53f5a18c2b313aec9b56bb2a74242e0df761997714f2abfd71b8d7bad193eee246924232f39d2a78e3e6b843916c1ac5398f
7
+ data.tar.gz: 79092042f9e356da823a922354d8e9de8f919aab6fa6e5fa465685b728fa2a8dfede0d958c0a269c41b534e22a5fab430e6569a898bd4342374f5f123d40756a
@@ -750,7 +750,7 @@ module MarkdownServer
750
750
 
751
751
  def scrip_html(html)
752
752
  # Extract the passage content block
753
- content = html[/<div\s+class="passage-text[^"]*"[^>]*>([\s\S]*?)<\/div>\s*(?:<nav|$)/im, 1] || ""
753
+ content = html[/<div\s+class="passage-text[^"]*"[^>]*>([\s\S]*?)<\/div>\s*(?:<nav|\z)/im, 1] || ""
754
754
  return page_html(html) if content.empty?
755
755
 
756
756
  # Add data-verse attributes to verse spans for popup scrolling.
@@ -762,7 +762,7 @@ module MarkdownServer
762
762
  # Inline the essential substitution CSS (show replacement mode by default)
763
763
  css = <<~CSS
764
764
  <style>
765
- .subst { cursor: help; border-radius: 2px; background: #f0ebff; position: relative; }
765
+ .subst { cursor: pointer; border-radius: 2px; background: #f0ebff; position: relative; }
766
766
  .subst:hover { background: #e0d4ff; outline: 1px solid #a78bfa; }
767
767
  .subst-t, .subst-o, .subst-x, .subst-s { display: none; }
768
768
  .subst-r { display: inline; color: #7c3aed; font-style: italic; }
@@ -777,36 +777,11 @@ module MarkdownServer
777
777
  .poetry-line { margin: 0; padding: 0; line-height: 1.5; }
778
778
  .poetry-line.indent-1 { padding-left: 2em; }
779
779
  .poetry-line.indent-2 { padding-left: 4em; }
780
- .subst-tip { display: none; position: absolute; bottom: calc(100% + 4px); left: 50%; transform: translateX(-50%); background: #1e293b; color: #f8fafc; font-size: 0.78rem; line-height: 1.5; padding: 0.4rem 0.6rem; border-radius: 5px; white-space: nowrap; z-index: 100; pointer-events: none; font-style: normal; }
781
- .subst-tip::after { content: ""; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); border: 5px solid transparent; border-top-color: #1e293b; }
782
- .subst:hover .subst-tip { display: block; }
780
+ .subst-tip { display: none; }
783
781
  </style>
784
782
  CSS
785
783
 
786
- # Build tooltips for substitution spans via JS
787
- js = <<~JS
788
- <script>
789
- document.querySelectorAll('.subst').forEach(function(el) {
790
- var t = el.querySelector('.subst-t'), r = el.querySelector('.subst-r'),
791
- o = el.querySelector('.subst-o'), x = el.querySelector('.subst-x'),
792
- s = el.querySelector('.subst-s');
793
- var lines = [];
794
- if (t) lines.push(t.textContent);
795
- if (r) lines.push(r.textContent);
796
- if (o) lines.push(o.textContent);
797
- if (x) lines.push(x.textContent);
798
- if (s) lines.push(s.textContent);
799
- if (lines.length > 1) {
800
- var tip = document.createElement('span');
801
- tip.className = 'subst-tip';
802
- tip.innerHTML = lines.join('<br>');
803
- el.appendChild(tip);
804
- }
805
- });
806
- </script>
807
- JS
808
-
809
- css + '<div style="font-family:Georgia,serif;line-height:1.7">' + content + "</div>" + js
784
+ css + '<div style="font-family:Georgia,serif;line-height:1.7">' + content + "</div>"
810
785
  end
811
786
 
812
787
  def inline_directory_html(dir_path, relative_dir)
@@ -1122,6 +1097,29 @@ module MarkdownServer
1122
1097
  JSON.dump({ title: title.to_s, html: html, frontmatter_html: frontmatter_html })
1123
1098
  end
1124
1099
 
1100
+ get "/fetch-json" do
1101
+ content_type :json
1102
+ url = params[:url].to_s.strip
1103
+ halt 400, '{"error":"invalid url"}' unless url.match?(/\Ahttps?:\/\//i)
1104
+
1105
+ uri = URI.parse(url)
1106
+ halt 400, '{"error":"invalid url"}' unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
1107
+
1108
+ http = Net::HTTP.new(uri.host, uri.port)
1109
+ http.use_ssl = (uri.scheme == "https")
1110
+ http.open_timeout = FETCH_TIMEOUT
1111
+ http.read_timeout = FETCH_TIMEOUT
1112
+ req = Net::HTTP::Get.new(uri.request_uri)
1113
+ req["Accept"] = "application/json"
1114
+ resp = http.request(req)
1115
+ halt 502, '{"error":"fetch failed"}' unless resp.is_a?(Net::HTTPSuccess)
1116
+
1117
+ headers "Cache-Control" => "public, max-age=3600"
1118
+ resp.body
1119
+ rescue StandardError
1120
+ halt 502, '{"error":"fetch failed"}'
1121
+ end
1122
+
1125
1123
  get "/fetch" do
1126
1124
  content_type :json
1127
1125
  url = params[:url].to_s.strip
@@ -1,3 +1,3 @@
1
1
  module MarkdownServer
2
- VERSION = "0.6.4"
2
+ VERSION = "0.6.6"
3
3
  end
data/views/layout.erb CHANGED
@@ -99,6 +99,12 @@
99
99
  border-bottom: 2px solid #d4b96a;
100
100
  padding-bottom: 0.5rem;
101
101
  }
102
+ .page-title-download {
103
+ color: inherit;
104
+ text-decoration: none;
105
+ border-bottom: none;
106
+ }
107
+ .page-title-download:hover { color: inherit; }
102
108
 
103
109
  /* Directory listing */
104
110
  .dir-listing {
@@ -199,6 +205,7 @@
199
205
  .md-content {
200
206
  line-height: 1.8;
201
207
  }
208
+ .md-content > :first-child { margin-top: 0; }
202
209
  .md-content h1 { font-size: 1.5rem; margin: 1.5rem 0 0.8rem; color: #3a3a3a; }
203
210
  .md-content h2 {
204
211
  font-size: 1.25rem;
@@ -2024,10 +2031,19 @@
2024
2031
  }
2025
2032
 
2026
2033
  function findVerseSpans(root, verseNum) {
2027
- var span = root.querySelector('[data-verse="' + verseNum + '"]');
2028
- if (span) return [span];
2029
- var spans = root.querySelectorAll('[data-verse$=":' + verseNum + '"]');
2030
- return spans.length ? Array.prototype.slice.call(spans) : [];
2034
+ var num = String(verseNum);
2035
+ // Prefer .verse spans (full verse text) with exact verse number match
2036
+ var all = root.querySelectorAll('.verse[data-verse]');
2037
+ var matches = [];
2038
+ for (var i = 0; i < all.length; i++) {
2039
+ var dv = all[i].getAttribute('data-verse');
2040
+ var after = dv.split(':').pop();
2041
+ if (after === num) matches.push(all[i]);
2042
+ }
2043
+ if (matches.length) return matches;
2044
+ // Fallback: bare data-verse="N" (e.g. sup elements from scrip_html)
2045
+ var span = root.querySelector('[data-verse="' + num + '"]');
2046
+ return span ? [span] : [];
2031
2047
  }
2032
2048
 
2033
2049
  function applyPopupAnchor(hash) {
data/views/markdown.erb CHANGED
@@ -1,15 +1,11 @@
1
1
  <% _file_path = @crumbs.map { |c| c[:name] }.drop(1).join("/") %>
2
2
  <div class="title-bar">
3
- <h1 class="page-title"><%= h(@title) %></h1>
3
+ <h1 class="page-title"><a href="<%= @download_href %>" class="page-title-download"><%= h(@title) %></a></h1>
4
4
  <form class="search-form" action="<%= search_form_path(_file_path) %>" method="get">
5
5
  <input type="text" name="q" placeholder="Search this document..." value="">
6
6
  </form>
7
7
  </div>
8
8
 
9
- <div class="toolbar">
10
- <a href="<%= @download_href %>">Download</a>
11
- </div>
12
-
13
9
  <% if @has_toc %>
14
10
  <button class="toc-fab" id="toc-fab" aria-label="Table of Contents">&#9776;</button>
15
11
  <div class="toc-overlay" id="toc-overlay"></div>
@@ -43,6 +39,10 @@
43
39
  <% end %>
44
40
 
45
41
  <div class="page-main">
42
+ <div class="md-content" data-file="<%= h(@title) %>">
43
+ <%= @content %>
44
+ </div>
45
+
46
46
  <% if @meta && !@meta.empty? %>
47
47
  <div class="frontmatter">
48
48
  <div class="frontmatter-heading">Frontmatter</div>
@@ -56,9 +56,5 @@
56
56
  </table>
57
57
  </div>
58
58
  <% end %>
59
-
60
- <div class="md-content" data-file="<%= h(@title) %>">
61
- <%= @content %>
62
- </div>
63
59
  </div>
64
60
  </div>
@@ -596,10 +596,19 @@
596
596
  }
597
597
 
598
598
  function findVerseSpans(root, verseNum) {
599
- var span = root.querySelector('[data-verse="' + verseNum + '"]');
600
- if (span) return [span];
601
- var spans = root.querySelectorAll('[data-verse$=":' + verseNum + '"]');
602
- return spans.length ? Array.prototype.slice.call(spans) : [];
599
+ var num = String(verseNum);
600
+ // Prefer .verse spans (full verse text) with exact verse number match
601
+ var all = root.querySelectorAll('.verse[data-verse]');
602
+ var matches = [];
603
+ for (var i = 0; i < all.length; i++) {
604
+ var dv = all[i].getAttribute('data-verse');
605
+ var after = dv.split(':').pop();
606
+ if (after === num) matches.push(all[i]);
607
+ }
608
+ if (matches.length) return matches;
609
+ // Fallback: bare data-verse="N" (e.g. sup elements from scrip_html)
610
+ var span = root.querySelector('[data-verse="' + num + '"]');
611
+ return span ? [span] : [];
603
612
  }
604
613
 
605
614
  function applyPopupAnchor(hash) {
@@ -873,6 +882,167 @@
873
882
  document.head.appendChild(style);
874
883
  })();
875
884
 
885
+ // Substitution span popups — look up Strong's numbers in dictionary.json
886
+ (function() {
887
+ var substEls = document.querySelectorAll('.subst[data-strongs]');
888
+ if (!substEls.length) return;
889
+
890
+ var dictUrl = null;
891
+ var strongsMap = null; // strongs -> { url, blb, label }
892
+ var dictLoading = false;
893
+ var dictCallbacks = [];
894
+
895
+ function loadDict(cb) {
896
+ if (strongsMap) { cb(); return; }
897
+ dictCallbacks.push(cb);
898
+ if (dictLoading) return;
899
+ dictLoading = true;
900
+
901
+ // Discover dictionary.json URL from the first .subst's data-strongs
902
+ // Convention: definitions/dictionary.json on the bible server
903
+ var jsonUrl = 'https://bible2.risensavior.com/download/definitions/dictionary.json';
904
+ fetch('/fetch-json?url=' + encodeURIComponent(jsonUrl))
905
+ .then(function(r) { return r.ok ? r.json() : null; })
906
+ .then(function(data) {
907
+ if (!data || !data.stems) { strongsMap = {}; return; }
908
+ var urlTpl = data.url || '';
909
+ strongsMap = {};
910
+ ['greek', 'hebrew'].forEach(function(lang) {
911
+ var stems = data.stems[lang];
912
+ if (!stems) return;
913
+ Object.keys(stems).forEach(function(key) {
914
+ var entry = stems[key];
915
+ if (!entry.strongs) return;
916
+ var sn = entry.strongs.toUpperCase();
917
+ strongsMap[sn] = {
918
+ url: urlTpl.replace('{filename}', entry.filename || ''),
919
+ blb: entry.blueletterbible || null,
920
+ label: entry.original || entry.transliteration || key
921
+ };
922
+ });
923
+ });
924
+ })
925
+ .catch(function() { strongsMap = {}; })
926
+ .then(function() {
927
+ dictCallbacks.forEach(function(fn) { fn(); });
928
+ dictCallbacks = [];
929
+ });
930
+ }
931
+
932
+ function blbFallbackUrl(strongs) {
933
+ var s = strongs.toLowerCase();
934
+ var prefix = s.charAt(0) === 'h' ? 'wlc' : 'tr';
935
+ return 'https://www.blueletterbible.org/lexicon/' + s + '/nasb20/' + prefix + '/0-1/';
936
+ }
937
+
938
+ function handleSubst(el, x, y) {
939
+ var strongs = (el.dataset.strongs || '').toUpperCase();
940
+ if (!strongs) return;
941
+
942
+ var rect = el.getBoundingClientRect();
943
+ var replacement = el.dataset.replacement || '';
944
+ var original = el.dataset.original || '';
945
+ var translit = el.dataset.translit || '';
946
+
947
+ // Show loading popup immediately
948
+ var loadingTitle = replacement || original || strongs;
949
+ showPopup(x, y, loadingTitle,
950
+ '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>', null, rect);
951
+
952
+ loadDict(function() {
953
+ var entry = strongsMap[strongs];
954
+ var href = entry ? entry.url : blbFallbackUrl(strongs);
955
+ var title = replacement || (entry && entry.label) || strongs;
956
+
957
+ // Fetch the target page through the existing proxy
958
+ var cacheKey = 'ext:' + href;
959
+ var cached = cache[cacheKey];
960
+ if (cached && typeof cached === 'object') {
961
+ showPopup(x, y, cached.title || title, cached.html, href, rect);
962
+ return;
963
+ }
964
+ if (cached === false) {
965
+ showPopup(x, y, title,
966
+ '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
967
+ '<p style="margin:0.5rem 0 0;color:#888;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>', href, rect);
968
+ return;
969
+ }
970
+
971
+ // Show popup with loading state and correct title/href
972
+ showPopup(x, y, title,
973
+ '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>', href, rect);
974
+
975
+ if (cached === undefined) {
976
+ cache[cacheKey] = null;
977
+ fetch('/fetch?url=' + encodeURIComponent(href))
978
+ .then(function(r) { return r.ok ? r.json() : null; })
979
+ .then(function(data) {
980
+ if (!data || data.error) {
981
+ cache[cacheKey] = false;
982
+ updatePopup(
983
+ '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
984
+ '<p style="margin:0.5rem 0 0;color:#888;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>');
985
+ return;
986
+ }
987
+ cache[cacheKey] = { title: data.title, html: data.html };
988
+ updatePopup(data.html, data.title || title);
989
+ })
990
+ .catch(function() {
991
+ cache[cacheKey] = false;
992
+ updatePopup(
993
+ '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
994
+ '<p style="margin:0.5rem 0 0;color:#c44;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>');
995
+ });
996
+ }
997
+ });
998
+ }
999
+
1000
+ function findSubst(el) {
1001
+ while (el && el.tagName !== 'BODY') {
1002
+ if (el.classList && el.classList.contains('subst') && el.dataset.strongs) return el;
1003
+ el = el.parentElement;
1004
+ }
1005
+ return null;
1006
+ }
1007
+
1008
+ // Click
1009
+ document.addEventListener('click', function(e) {
1010
+ var el = findSubst(e.target);
1011
+ if (!el) return;
1012
+ e.preventDefault();
1013
+ e.stopPropagation();
1014
+ handleSubst(el, e.clientX, e.clientY);
1015
+ });
1016
+
1017
+ // Touch
1018
+ document.addEventListener('touchend', function(e) {
1019
+ if (touchMoved) return;
1020
+ var el = findSubst(e.target);
1021
+ if (!el) return;
1022
+ e.preventDefault();
1023
+ e.stopPropagation();
1024
+ var touch = e.changedTouches[0];
1025
+ handleSubst(el, touch.clientX, touch.clientY);
1026
+ }, { passive: false });
1027
+
1028
+ // Hover
1029
+ var hoverTimer = null;
1030
+ substEls.forEach(function(el) {
1031
+ el.addEventListener('mouseenter', function(e) {
1032
+ clearTimeout(hoverTimer);
1033
+ if (popup) return;
1034
+ var x = e.clientX, y = e.clientY;
1035
+ hoverTimer = setTimeout(function() {
1036
+ if (!popup) handleSubst(el, x, y);
1037
+ }, 300);
1038
+ });
1039
+ el.addEventListener('mouseleave', function() {
1040
+ clearTimeout(hoverTimer);
1041
+ if (popup) mouseLeaveTimer = setTimeout(hidePopup, 150);
1042
+ });
1043
+ });
1044
+ })();
1045
+
876
1046
  })();
877
1047
 
878
1048
  // Full-page verse highlighting from URL hash (#vN or #vN-M)
@@ -881,14 +1051,26 @@
881
1051
  if (!m) return;
882
1052
  var start = parseInt(m[1], 10);
883
1053
  var end = m[2] ? parseInt(m[2], 10) : start;
1054
+ var bookMeta = document.querySelector('meta[name="book"]');
1055
+ var chMeta = document.querySelector('meta[name="chapter"]');
1056
+ var book = bookMeta && bookMeta.content;
1057
+ var ch = chMeta && chMeta.content;
884
1058
  var first = null;
885
1059
  for (var v = start; v <= end; v++) {
886
- var sel = '[data-verse$=":' + v + '"]';
887
- var spans = document.querySelectorAll(sel);
888
- if (!spans.length) spans = document.querySelectorAll('[data-verse="' + v + '"]');
889
- for (var i = 0; i < spans.length; i++) {
890
- spans[i].classList.add('verse-selected');
891
- if (!first) first = spans[i];
1060
+ var spans;
1061
+ if (book && ch) {
1062
+ spans = document.querySelectorAll('.verse[data-verse="' + book + ' ' + ch + ':' + v + '"]');
1063
+ } else {
1064
+ // No meta tags — filter all .verse spans by exact verse number
1065
+ var all = document.querySelectorAll('.verse[data-verse]');
1066
+ spans = [];
1067
+ for (var i = 0; i < all.length; i++) {
1068
+ if (all[i].getAttribute('data-verse').split(':').pop() === String(v)) spans.push(all[i]);
1069
+ }
1070
+ }
1071
+ for (var j = 0; j < spans.length; j++) {
1072
+ spans[j].classList.add('verse-selected');
1073
+ if (!first) first = spans[j];
892
1074
  }
893
1075
  }
894
1076
  if (!first) return;
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.6.4
4
+ version: 0.6.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Dunn