markdownr 0.6.9 → 0.6.10

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: c1bd3ffb960e7f2e3750d7f70fe188c551d1fcf764a6517f65a4c45fee4634e4
4
- data.tar.gz: 49a2c9f9a777fc280bc9bd98a38fb135873e9e2392edbe4d2ada07cd2d20e8b2
3
+ metadata.gz: 352e2d418a6bf94404763caeaa5b8e43c5f07fa2bff53c208a60972b16420a7e
4
+ data.tar.gz: e2b8d84227484fca2134dc9a1cffa67bebf3022134377945a9081dc19c6c3fe0
5
5
  SHA512:
6
- metadata.gz: dc4bc27debd2a2093965e2bc46ce94364e18db6970659091ebe94febab5e37cefb1ddb0fcc64810006eeb1db1b72faef32543e3b186595c85619a85303b9b4bf
7
- data.tar.gz: 54b2fbd599cdae2d6e582cdf73d8adb6363244ff9ecda175c9627bf9b1619f5ae510884f053f8d5d585551b539708db684de8a00ea51adce62e48c0946b93d8f
6
+ metadata.gz: 15653576feb66ee4cb2dd259ed2ce21e11ce6dd6872e5533b8d7c28eb5eafa2aa471b61d8997afcacfb86aa21ff7f442366ec16157014321feba7ed3627059d4
7
+ data.tar.gz: ee7ea43025abdd7cc04a1238660985480664c7220fd98327331be5e93682d896e470525c4c9dc1186d235c3dc5885cde770012e29111e62e42d4bf99d1397494
data/bin/markdownr CHANGED
@@ -60,6 +60,10 @@ OptionParser.new do |opts|
60
60
  )
61
61
  end
62
62
 
63
+ opts.on("--dictionary-url URL", "URL to dictionary.json for Strong's number popup resolution") do |u|
64
+ options[:dictionary_url] = u
65
+ end
66
+
63
67
  opts.on("-v", "--version", "Show version") do
64
68
  puts "markdownr #{MarkdownServer::VERSION}"
65
69
  exit
@@ -86,6 +90,7 @@ MarkdownServer::App.set :port, options[:port]
86
90
  MarkdownServer::App.set :bind, options[:bind]
87
91
  MarkdownServer::App.set :server_settings, { max_threads: options[:threads], min_threads: 1 }
88
92
  MarkdownServer::App.set :plugin_overrides, options[:plugin_overrides] || {}
93
+ MarkdownServer::App.set :dictionary_url, options[:dictionary_url] if options[:dictionary_url]
89
94
 
90
95
  # Load .markdownr.yml popup settings
91
96
  config_path = File.join(dir, ".markdownr.yml")
@@ -98,6 +103,9 @@ if File.exist?(config_path)
98
103
  MarkdownServer::App.set :popup_external, popups.fetch("external", true)
99
104
  MarkdownServer::App.set :popup_external_domains, popups.fetch("external_domains", [])
100
105
  end
106
+ if yaml && yaml["dictionary_url"]
107
+ MarkdownServer::App.set :dictionary_url, yaml["dictionary_url"]
108
+ end
101
109
  end
102
110
 
103
111
  MarkdownServer::App.load_plugins!
@@ -38,8 +38,15 @@ module MarkdownServer
38
38
  set :popup_local_html, false
39
39
  set :popup_external, true
40
40
  set :popup_external_domains, []
41
+ set :dictionary_url, nil
41
42
  end
42
43
 
44
+ # Server-side Strong's dictionary cache
45
+ @@strongs_cache = nil
46
+ @@strongs_cache_url = nil
47
+ @@strongs_fetched_at = nil
48
+ DICTIONARY_TTL = 3600 # 1 hour
49
+
43
50
  def self.load_plugins!
44
51
  Dir[File.join(__dir__, "plugins", "*", "plugin.rb")].sort.each { |f| require f }
45
52
  set :plugins, PluginRegistry.load_plugins(settings.root_dir, settings.plugin_overrides)
@@ -485,6 +492,83 @@ module MarkdownServer
485
492
  FETCH_MAX_BYTES = 512_000
486
493
  FETCH_TIMEOUT = 5
487
494
 
495
+ def strongs_map
496
+ url = settings.dictionary_url
497
+ return {} unless url
498
+
499
+ now = Time.now
500
+ if @@strongs_cache && @@strongs_cache_url == url && @@strongs_fetched_at
501
+ if url.match?(/\Ahttps?:\/\//i)
502
+ return @@strongs_cache if (now - @@strongs_fetched_at) < DICTIONARY_TTL
503
+ else
504
+ path = File.expand_path(url, root_dir)
505
+ mtime = File.mtime(path) rescue nil
506
+ return @@strongs_cache if mtime && mtime <= @@strongs_fetched_at
507
+ end
508
+ end
509
+
510
+ begin
511
+ raw = if url.match?(/\Ahttps?:\/\//i)
512
+ uri = URI.parse(url)
513
+ http = Net::HTTP.new(uri.host, uri.port)
514
+ http.use_ssl = (uri.scheme == "https")
515
+ http.open_timeout = FETCH_TIMEOUT
516
+ http.read_timeout = 10
517
+ req = Net::HTTP::Get.new(uri.request_uri)
518
+ req["Accept"] = "application/json"
519
+ resp = http.request(req)
520
+ return @@strongs_cache || {} unless resp.is_a?(Net::HTTPSuccess)
521
+ resp.body
522
+ else
523
+ path = File.expand_path(url, root_dir)
524
+ return @@strongs_cache || {} unless File.exist?(path)
525
+ File.read(path, encoding: "utf-8")
526
+ end
527
+
528
+ data = JSON.parse(raw)
529
+ url_tpl = data["url"] || ""
530
+ map = {}
531
+ %w[greek hebrew].each do |lang|
532
+ stems = data.dig("stems", lang)
533
+ next unless stems.is_a?(Hash)
534
+ stems.each_value do |entry|
535
+ sn = entry["strongs"].to_s.strip.upcase
536
+ next if sn.empty?
537
+ map[sn] = url_tpl.gsub("{filename}", entry["filename"].to_s)
538
+ end
539
+ end
540
+ @@strongs_cache = map
541
+ @@strongs_cache_url = url
542
+ @@strongs_fetched_at = now
543
+ map
544
+ rescue StandardError
545
+ @@strongs_cache || {}
546
+ end
547
+ end
548
+
549
+ def strongs_popup_url(strongs)
550
+ sn = strongs.to_s.strip.upcase
551
+ return nil unless sn.match?(/\A[GH]\d+\z/)
552
+ url = strongs_map[sn]
553
+ return url if url
554
+ # Fallback: Blue Letter Bible
555
+ prefix = sn.start_with?("H") ? "wlc" : "tr"
556
+ "https://www.blueletterbible.org/lexicon/#{sn.downcase}/nasb20/#{prefix}/0-1/"
557
+ end
558
+
559
+ def inject_strongs_urls(html)
560
+ return html unless settings.dictionary_url
561
+ html.gsub(/<span\s+class="subst"([^>]*data-strongs="([^"]+)"[^>]*)>/) do
562
+ attrs, strongs = $1, $2
563
+ popup_url = strongs_popup_url(strongs)
564
+ if popup_url && !attrs.include?("data-popup-url")
565
+ %(<span class="subst" data-popup-url="#{h(popup_url)}"#{attrs}>)
566
+ else
567
+ $&
568
+ end
569
+ end
570
+ end
571
+
488
572
  # Tags kept as-is (attributes stripped)
489
573
  ALLOWED_HTML = %w[p h1 h2 h3 h4 h5 h6 blockquote ul ol li
490
574
  pre br hr strong b em i sup sub code
@@ -794,6 +878,7 @@ module MarkdownServer
794
878
  </style>
795
879
  CSS
796
880
 
881
+ content = inject_strongs_urls(content)
797
882
  css + '<div style="font-family:Georgia,serif;line-height:1.7">' + content + "</div>"
798
883
  end
799
884
 
@@ -869,6 +954,7 @@ btn._loading=false;btn.style.opacity='1';});})(this)">&#128266;</button>)
869
954
  # Falls back to appending before </html>, then to end of document.
870
955
  def inject_markdownr_assets(html_content)
871
956
  settings.plugins.each { |p| html_content = p.transform_html(html_content) }
957
+ html_content = inject_strongs_urls(html_content)
872
958
 
873
959
  popup_config_script = "<script>var __popupConfig = {" \
874
960
  "localMd:#{settings.popup_local_md}," \
@@ -1,3 +1,3 @@
1
1
  module MarkdownServer
2
- VERSION = "0.6.9"
2
+ VERSION = "0.6.10"
3
3
  end
@@ -882,53 +882,11 @@
882
882
  document.head.appendChild(style);
883
883
  })();
884
884
 
885
- // Substitution span popups — look up Strong's numbers in dictionary.json
885
+ // Substitution span popups — use server-injected data-popup-url
886
886
  (function() {
887
887
  var substEls = document.querySelectorAll('.subst[data-strongs]');
888
888
  if (!substEls.length) return;
889
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
890
  function blbFallbackUrl(strongs) {
933
891
  var s = strongs.toLowerCase();
934
892
  var prefix = s.charAt(0) === 'h' ? 'wlc' : 'tr';
@@ -941,60 +899,48 @@
941
899
 
942
900
  var rect = el.getBoundingClientRect();
943
901
  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
- }
902
+ var href = el.dataset.popupUrl || blbFallbackUrl(strongs);
903
+ var title = replacement || strongs;
970
904
 
971
- // Show popup with loading state and correct title/href
905
+ // Fetch the target page through the existing proxy
906
+ var cacheKey = 'ext:' + href;
907
+ var cached = cache[cacheKey];
908
+ if (cached && typeof cached === 'object') {
909
+ showPopup(x, y, cached.title || title, cached.html, href, rect);
910
+ return;
911
+ }
912
+ if (cached === false) {
972
913
  showPopup(x, y, title,
973
- '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>', href, rect);
914
+ '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
915
+ '<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);
916
+ return;
917
+ }
974
918
 
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() {
919
+ showPopup(x, y, title,
920
+ '<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>', href, rect);
921
+
922
+ if (cached === undefined) {
923
+ cache[cacheKey] = null;
924
+ fetch('/fetch?url=' + encodeURIComponent(href))
925
+ .then(function(r) { return r.ok ? r.json() : null; })
926
+ .then(function(data) {
927
+ if (!data || data.error) {
991
928
  cache[cacheKey] = false;
992
929
  updatePopup(
993
930
  '<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
- });
931
+ '<p style="margin:0.5rem 0 0;color:#888;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>');
932
+ return;
933
+ }
934
+ cache[cacheKey] = { title: data.title, html: data.html };
935
+ updatePopup(data.html, data.title || title);
936
+ })
937
+ .catch(function() {
938
+ cache[cacheKey] = false;
939
+ updatePopup(
940
+ '<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
941
+ '<p style="margin:0.5rem 0 0;color:#c44;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>');
942
+ });
943
+ }
998
944
  }
999
945
 
1000
946
  function findSubst(el) {
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.9
4
+ version: 0.6.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Dunn