markdownr 0.6.16 → 0.6.17

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: ae47c745347215a97c8448c31431bde10bf60935740970585d3b01626aab4d49
4
- data.tar.gz: 75c279c8578e8ef37e82748431b605dfa38fa786a1a3f162331dbbe5086b91a1
3
+ metadata.gz: 1a2282422f5341fe474b7b7e34af4387a7d6784b1945501f969ea3098e768377
4
+ data.tar.gz: 45b589b50541dc8e88999cba5df250047a0a298d00bbc2b54bb5d7dd5868009d
5
5
  SHA512:
6
- metadata.gz: f4b4af8f7f2b1d175c156b3a93b4f66f6300e715a0cef021d7fd97b222af522fa0b7be26e8a09224db705aec240762766f7b005bdfb1825803396b2e452982bc
7
- data.tar.gz: 6e220707c92e1f80fe676b9353514d426c03419b6514fc2d0ed49f9f1d431ed595065c22588aea028369a1edf07c53591bfa851eac4009b8211cf0d4ffa6868a
6
+ metadata.gz: 56e7530f96dd2622ad883c0fdf9ee83d6731f8ce372476907a2ea7f22979e739e13f630f3ee0b121f39d3d3192f702ca27434de441ae59cd11f3515054f62c8d
7
+ data.tar.gz: d0831ec05ccd0e1e57dfe07f57dd3ba6ccca5d7d958c7ddf72e578995dbd4ea9ce911eb7af8e6fac96553b32c76221369338e521fab88c67ac5839d8c2d932cb
@@ -47,6 +47,10 @@ module MarkdownServer
47
47
  @@strongs_fetched_at = nil
48
48
  DICTIONARY_TTL = 3600 # 1 hour
49
49
 
50
+ # CSS cache for external stylesheets (keyed by absolute URL)
51
+ @@css_cache = {}
52
+ CSS_TTL = 3600 # 1 hour
53
+
50
54
  def self.load_plugins!
51
55
  Dir[File.join(__dir__, "plugins", "*", "plugin.rb")].sort.each { |f| require f }
52
56
  set :plugins, PluginRegistry.load_plugins(settings.root_dir, settings.plugin_overrides)
@@ -568,7 +572,7 @@ module MarkdownServer
568
572
 
569
573
  def inject_strongs_urls(html)
570
574
  return html unless settings.dictionary_url
571
- html.gsub(/<span\s+class="subst"([^>]*data-strongs="([^"]+)"[^>]*)>/) do
575
+ html = html.gsub(/<span\s+class="subst"([^>]*data-strongs="([^"]+)"[^>]*)>/) do
572
576
  attrs, strongs = $1, $2
573
577
  popup_url = strongs_popup_url(strongs)
574
578
  if popup_url && !attrs.include?("data-popup-url")
@@ -577,6 +581,16 @@ module MarkdownServer
577
581
  $&
578
582
  end
579
583
  end
584
+ # Interlinear .word spans: inject data-popup-url when Strong's is in dictionary
585
+ html.gsub(/<span\s+class="word"([^>]*data-strongs="([^"]+)"[^>]*)>/) do
586
+ attrs, strongs = $1, $2
587
+ dict_url = strongs_map[strongs.strip.upcase]
588
+ if dict_url && !attrs.include?("data-popup-url")
589
+ %(<span class="word" data-popup-url="#{h(dict_url)}"#{attrs}>)
590
+ else
591
+ $&
592
+ end
593
+ end
580
594
  end
581
595
 
582
596
  # Tags kept as-is (attributes stripped)
@@ -590,6 +604,26 @@ module MarkdownServer
590
604
  STRIP_FULL = %w[script style nav header footer form input
591
605
  button select textarea svg iframe noscript].to_set
592
606
 
607
+ def fetch_css(url_str)
608
+ cached = @@css_cache[url_str]
609
+ return cached[:body] if cached && (Time.now - cached[:at]) < CSS_TTL
610
+
611
+ uri = URI.parse(url_str)
612
+ return nil unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
613
+ http = Net::HTTP.new(uri.host, uri.port)
614
+ http.use_ssl = (uri.scheme == "https")
615
+ http.open_timeout = FETCH_TIMEOUT
616
+ http.read_timeout = FETCH_TIMEOUT
617
+ req = Net::HTTP::Get.new(uri.request_uri)
618
+ req["Accept"] = "text/css"
619
+ resp = http.request(req)
620
+ body = resp.is_a?(Net::HTTPSuccess) ? resp.body.to_s.encode("utf-8", invalid: :replace, undef: :replace) : nil
621
+ @@css_cache[url_str] = { body: body, at: Time.now } if body
622
+ body
623
+ rescue
624
+ @@css_cache.dig(url_str, :body)
625
+ end
626
+
593
627
  def fetch_external_page(url_str)
594
628
  uri = URI.parse(url_str)
595
629
  return nil unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
@@ -865,10 +899,10 @@ module MarkdownServer
865
899
  info_html + infl_html + usage_html + conc_html
866
900
  end
867
901
 
868
- def scrip_html(html)
902
+ def scrip_html(html, source_url = nil)
869
903
  # Extract the passage content block
870
904
  content = html[/<div\s+class="passage-text[^"]*"[^>]*>([\s\S]*)<\/div>\s*(?:<nav|<script|<style|\z)/im, 1] || ""
871
- return page_html(html) if content.empty?
905
+ return page_html(html, source_url) if content.empty?
872
906
 
873
907
  # Add data-verse attributes to verse spans for popup scrolling.
874
908
  # Verse numbers live in <sup class="verse-number">N </sup> inside verse spans.
@@ -876,39 +910,30 @@ module MarkdownServer
876
910
  %(<sup class="verse-number" data-verse="#{$1}">#{$1} </sup>)
877
911
  end
878
912
 
879
- # Inline the essential substitution CSS (show replacement mode by default)
880
- css = <<~CSS
881
- <style>
882
- .subst { cursor: pointer; border-radius: 2px; background: #f0ebff; position: relative; }
883
- .subst:hover { background: #e0d4ff; outline: 1px solid #a78bfa; }
884
- .subst-t, .subst-o, .subst-x, .subst-s { display: none; }
885
- .subst-r { display: inline; color: #7c3aed; font-style: italic; }
886
- .section-heading { font-size: 1rem; font-weight: bold; margin: 1.5rem 0 0.25rem; color: #444; }
887
- .verse-block { margin: 0.5rem 0; text-indent: 1.5em; }
888
- .verse-block.chapter-opening { margin-top: 0; text-indent: 0; }
889
- .verse-block.no-first-indent { text-indent: 0; }
890
- .verse-number { font-size: 0.65em; font-weight: bold; color: #888; vertical-align: super; margin-right: 0.1em; line-height: 0; }
891
- .chapter-number { font-size: 1.5em; font-weight: bold; vertical-align: 0.1em; margin-right: 0.15em; }
892
- .divine-name { font-variant: small-caps; }
893
- .footnotes, .crossrefs { border-top: 1px solid #ddd; margin-top: 2rem; padding-top: 0.75rem; font-size: 0.85rem; color: #555; }
894
- .footnotes ol { padding-left: 1.5rem; margin: 0.5rem 0 0; list-style-type: lower-alpha; }
895
- .crossrefs ol { padding-left: 2.5rem; margin: 0.5rem 0 0; }
896
- .footnote-marker, .crossref-marker { cursor: pointer; font-size: 0.75em; line-height: 0; vertical-align: super; text-decoration: none; }
897
- .footnote-marker { color: #2563eb; }
898
- .crossref-marker { color: #16a34a; }
899
- .words-of-jesus { color: #8b0000; }
900
- .poetry-stanza { margin: 0 0 0 3rem; }
901
- .poetry-stanza.stanza-break { margin-top: 0.75rem; }
902
- .poetry-line { margin: 0; padding: 0; line-height: 1.5; }
903
- .poetry-line.indent-1 { padding-left: 2em; }
904
- .poetry-line.indent-2 { padding-left: 4em; }
905
- .verse-selected { background: #e5eefb; border-radius: 2px; }
906
- .subst-tip { display: none; }
907
- </style>
908
- CSS
913
+ # Fetch referenced stylesheets from the source page and inline them
914
+ css_blocks = ""
915
+ if source_url
916
+ base_uri = URI.parse(source_url) rescue nil
917
+ html.scan(/<link[^>]+rel=["']stylesheet["'][^>]*>/i).each do |tag|
918
+ href = tag[/href=["']([^"']+)["']/i, 1]
919
+ next unless href
920
+ abs = (base_uri ? (URI.join(base_uri, href).to_s rescue nil) : nil)
921
+ next unless abs
922
+ css_body = fetch_css(abs)
923
+ # Resolve @import within the CSS (one level deep)
924
+ if css_body
925
+ css_dir = abs.sub(%r{/[^/]*\z}, "/")
926
+ css_body = css_body.gsub(/@import\s+url\(["']?([^"')]+)["']?\)\s*;/) do
927
+ import_url = (URI.join(css_dir, $1).to_s rescue nil)
928
+ import_url ? (fetch_css(import_url) || "") : ""
929
+ end
930
+ css_blocks += "<style>#{css_body}</style>\n"
931
+ end
932
+ end
933
+ end
909
934
 
910
935
  content = inject_strongs_urls(content)
911
- css + '<div style="font-family:Georgia,serif;line-height:1.7">' + content + "</div>"
936
+ css_blocks + '<div style="font-family:Georgia,serif;line-height:1.7">' + content + "</div>"
912
937
  end
913
938
 
914
939
  def inline_directory_html(dir_path, relative_dir)
@@ -1315,7 +1340,7 @@ btn._loading=false;btn.style.opacity='1';});})(this)">&#128266;</button>)
1315
1340
  JSON.dump({ title: title, html: markdownr_html(html) })
1316
1341
  elsif url.match?(/scrip\.risensavior\.com/i)
1317
1342
  title = page_title(html).sub(/ [-–] .*/, "").strip
1318
- JSON.dump({ title: title, html: scrip_html(html) })
1343
+ JSON.dump({ title: title, html: scrip_html(html, url) })
1319
1344
  else
1320
1345
  title = page_title(html).sub(/ [-–] .*/, "").strip
1321
1346
  JSON.dump({ title: title, html: page_html(html, url) })
@@ -1,3 +1,3 @@
1
1
  module MarkdownServer
2
- VERSION = "0.6.16"
2
+ VERSION = "0.6.17"
3
3
  end
data/views/layout.erb CHANGED
@@ -2175,6 +2175,12 @@
2175
2175
  if (!m) return false;
2176
2176
  var audioUrl = 'https://www.studylight.org/multi-media/audio/lexicons/eng/' + m[1] + '.html?n=' + m[2];
2177
2177
  if (!anchor._slAudio) anchor._slAudio = new Audio(audioUrl);
2178
+ var wordEl = anchor.closest('.word');
2179
+ if (wordEl) {
2180
+ wordEl.classList.add('audio-playing');
2181
+ anchor._slAudio.onended = function() { wordEl.classList.remove('audio-playing'); };
2182
+ anchor._slAudio.onerror = function() { wordEl.classList.remove('audio-playing'); };
2183
+ }
2178
2184
  anchor._slAudio.currentTime = 0;
2179
2185
  anchor._slAudio.play();
2180
2186
  return true;
@@ -1,4 +1,22 @@
1
1
  <style>
2
+ /* Word highlight during verse audio playback */
3
+ .word.audio-playing {
4
+ background: #fef08a;
5
+ border-radius: 3px;
6
+ transition: background 0.15s ease;
7
+ }
8
+
9
+ /* Interlinear translit-link with dictionary definition available */
10
+ .word[data-popup-url] .translit-link {
11
+ color: #7b3fa0;
12
+ text-decoration: underline dotted #b07fd4 1px;
13
+ text-underline-offset: 2px;
14
+ }
15
+ .word[data-popup-url] .translit-link:hover {
16
+ color: #5a2c78;
17
+ text-decoration: underline solid #5a2c78 1px;
18
+ }
19
+
2
20
  /* Bible Gateway citation links */
3
21
  a[href*="biblegateway.com"],
4
22
  a[href*="scrip.risensavior.com"] {
@@ -197,6 +215,26 @@
197
215
  .link-ctx-popup-body th, .link-ctx-popup-body td { border: 1px solid #ddd; padding: 0.3rem 0.5rem; }
198
216
  .link-ctx-popup-body th { background: #f5f0e4; }
199
217
 
218
+ /*
219
+ * Reset interlinear .verse flex layout back to inline for passage popups.
220
+ * Interlinear pages define .verse { display: flex; border-bottom: ... } which
221
+ * would break inline passage content rendered inside a popup.
222
+ */
223
+ .link-ctx-popup-body .verse { display: inline; flex-wrap: initial; align-items: initial; gap: initial; margin: 0; padding: 0; border-bottom: none; }
224
+
225
+ /*
226
+ * Substitution mode for popups: force translation-only.
227
+ * The source CSS uses body.show-t / body.show-l / etc. to toggle modes, but
228
+ * those body classes leak into popup content. Override with !important to
229
+ * ensure only the translation span is visible inside popups.
230
+ */
231
+ .link-ctx-popup-body .subst-x { display: inline !important; color: #1a5276; font-style: italic; }
232
+ .link-ctx-popup-body .subst-t,
233
+ .link-ctx-popup-body .subst-l,
234
+ .link-ctx-popup-body .subst-o,
235
+ .link-ctx-popup-body .subst-p,
236
+ .link-ctx-popup-body .subst-s { display: none !important; }
237
+
200
238
  /* Blue Letter Bible popup tables */
201
239
  .blb-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; margin-bottom: 0.6rem; }
202
240
  .blb-table th, .blb-table td { padding: 3px 7px; border: 1px solid #ddd; }
@@ -739,6 +777,12 @@
739
777
  if (!m) return false;
740
778
  var audioUrl = 'https://www.studylight.org/multi-media/audio/lexicons/eng/' + m[1] + '.html?n=' + m[2];
741
779
  if (!anchor._slAudio) anchor._slAudio = new Audio(audioUrl);
780
+ var wordEl = anchor.closest('.word');
781
+ if (wordEl) {
782
+ wordEl.classList.add('audio-playing');
783
+ anchor._slAudio.onended = function() { wordEl.classList.remove('audio-playing'); };
784
+ anchor._slAudio.onerror = function() { wordEl.classList.remove('audio-playing'); };
785
+ }
742
786
  anchor._slAudio.currentTime = 0;
743
787
  anchor._slAudio.play();
744
788
  return true;
@@ -1005,6 +1049,15 @@
1005
1049
  });
1006
1050
  })();
1007
1051
 
1052
+ // Rewrite translit-link hrefs for words with dictionary definitions
1053
+ // so the existing popup system uses the dictionary URL instead of BLB
1054
+ (function() {
1055
+ document.querySelectorAll('.word[data-popup-url]').forEach(function(word) {
1056
+ var link = word.querySelector('.translit-link');
1057
+ if (link) link.setAttribute('href', word.dataset.popupUrl);
1058
+ });
1059
+ })();
1060
+
1008
1061
  })();
1009
1062
 
1010
1063
  // Full-page verse highlighting from URL hash (#vN or #vN-M)
@@ -1063,14 +1116,14 @@
1063
1116
  var words = verse.querySelectorAll('.word');
1064
1117
  if (!words.length) return;
1065
1118
 
1066
- // Collect audio URLs for all words in this verse
1067
- var urls = [];
1119
+ // Collect audio URLs paired with their word elements
1120
+ var items = [];
1068
1121
  words.forEach(function(w) {
1069
1122
  var link = w.querySelector('.pronunc-link');
1070
1123
  var url = audioUrlFromLink(link);
1071
- if (url) urls.push(url);
1124
+ if (url) items.push({ word: w, url: url });
1072
1125
  });
1073
- if (!urls.length) return;
1126
+ if (!items.length) return;
1074
1127
 
1075
1128
  // Create a column-flex container to replace the bare verse-num,
1076
1129
  // with the number at top (row 1) and speaker icon at row 5
@@ -1093,16 +1146,15 @@
1093
1146
  btn.style.cssText = 'background:none;border:none;cursor:pointer;padding:0;font-size:1em;line-height:1;opacity:0.6;';
1094
1147
  btn.addEventListener('mouseleave', function() { if (!btn._playing) btn.style.opacity = '0.6'; });
1095
1148
 
1096
- var audioCache = [];
1097
1149
  var preloaded = false;
1098
1150
  function preloadAll() {
1099
1151
  if (preloaded) return;
1100
1152
  preloaded = true;
1101
- urls.forEach(function(u) {
1102
- var a = new Audio(u);
1153
+ items.forEach(function(item) {
1154
+ var a = new Audio(item.url);
1103
1155
  a.preload = 'auto';
1104
1156
  a.load();
1105
- audioCache.push(a);
1157
+ item.audio = a;
1106
1158
  });
1107
1159
  }
1108
1160
  // Start preloading on hover so audio is ready before click
@@ -1119,16 +1171,21 @@
1119
1171
 
1120
1172
  var i = 0;
1121
1173
  function playNext() {
1122
- if (i >= audioCache.length) {
1174
+ if (i >= items.length) {
1123
1175
  btn._playing = false;
1124
1176
  btn.style.opacity = '0.6';
1125
1177
  return;
1126
1178
  }
1127
- var audio = audioCache[i];
1128
- audio.currentTime = 0;
1129
- audio.onended = function() { i++; playNext(); };
1130
- audio.onerror = function() { i++; playNext(); };
1131
- audio.play().catch(function() { i++; playNext(); });
1179
+ var item = items[i];
1180
+ item.word.classList.add('audio-playing');
1181
+ item.audio.currentTime = 0;
1182
+ var advance = function() {
1183
+ item.word.classList.remove('audio-playing');
1184
+ i++; playNext();
1185
+ };
1186
+ item.audio.onended = advance;
1187
+ item.audio.onerror = advance;
1188
+ item.audio.play().catch(advance);
1132
1189
  }
1133
1190
  playNext();
1134
1191
  });
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.16
4
+ version: 0.6.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Dunn