markdownr 0.6.5 → 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: af0a41eadcbe996f7165b92a56174857657a21698308b515a35a67ec81a9a3e0
4
- data.tar.gz: 12f87ff3043049997f895f4dc0bde07d6d7e41c0195286b1abe29c91eada81e2
3
+ metadata.gz: 3c823e9362df716b74c9f6523e2cc0e2a7873ee7b1655b78da6d284921e85a6d
4
+ data.tar.gz: 479d4c5709c1c66560bb9c7116af9c14992f8a8feedb7c2a9eaa58e12af36b6f
5
5
  SHA512:
6
- metadata.gz: cc617683848f464907a1f8d4e17986a80842e631b9e68dcd6d3eb340bb0b658d080a9849c8f1a6ec952f8ceaa23cb95fda4760e44dc02b36a6620a7b30dcbf53
7
- data.tar.gz: 73bbfe99e65f7c50b4c4ffca35b4ab98d9a706f3c8367451b115e61919391ec95d987396cb75f5cbf7890822f4ec4b0456255a47a9872af5745b2dc90737e92b
6
+ metadata.gz: 859817ec7ee12fac0837634bcf3b53f5a18c2b313aec9b56bb2a74242e0df761997714f2abfd71b8d7bad193eee246924232f39d2a78e3e6b843916c1ac5398f
7
+ data.tar.gz: 79092042f9e356da823a922354d8e9de8f919aab6fa6e5fa465685b728fa2a8dfede0d958c0a269c41b534e22a5fab430e6569a898bd4342374f5f123d40756a
@@ -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.5"
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;
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>
@@ -882,6 +882,167 @@
882
882
  document.head.appendChild(style);
883
883
  })();
884
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
+
885
1046
  })();
886
1047
 
887
1048
  // Full-page verse highlighting from URL hash (#vN or #vN-M)
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.5
4
+ version: 0.6.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Dunn