markdownr 0.6.9 → 0.6.11

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: 072ccb5cc0d25d1b06e7fc40cc51a6d4d71a03627fbf6569a822782a4151c8a4
4
+ data.tar.gz: '039ec59649a81611b158230190dee9c02343dc7fbbdf97008b8b05a116eecc66'
5
5
  SHA512:
6
- metadata.gz: dc4bc27debd2a2093965e2bc46ce94364e18db6970659091ebe94febab5e37cefb1ddb0fcc64810006eeb1db1b72faef32543e3b186595c85619a85303b9b4bf
7
- data.tar.gz: 54b2fbd599cdae2d6e582cdf73d8adb6363244ff9ecda175c9627bf9b1619f5ae510884f053f8d5d585551b539708db684de8a00ea51adce62e48c0946b93d8f
6
+ metadata.gz: eb6dab68fec96f02ff6c3c1243196d64166e489e78ea2ffc77423bb808d8737e52850fc51dadebe415a0258f8f53f2ed6bc42232efadca6b328620efe46853e5
7
+ data.tar.gz: 48f192f288f773f9fddc677fc68e0bc856f63780f4fa1e983bfbb2376d4b2be2ef2c4c520c95ba5988af2693cd2cced5ed599cea090948f28ae73d345a6db0a0
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)
@@ -209,23 +216,30 @@ module MarkdownServer
209
216
  def resolve_wiki_link(name)
210
217
  filename = "#{name}.md"
211
218
  base = File.realpath(root_dir)
219
+ # On case-sensitive filesystems (Linux/Docker), FNM_CASEFOLD doesn't help
220
+ # with directory listings. Try exact filename first, then lowercased variant.
221
+ candidates = [filename]
222
+ candidates << filename.downcase if filename != filename.downcase
212
223
 
213
224
  # Check the current file's directory first (exact case, then case-insensitive)
214
225
  if @current_wiki_dir
215
226
  local_exact = nil
216
227
  local_ci = nil
217
- Dir.glob(File.join(@current_wiki_dir, filename), File::FNM_CASEFOLD).each do |path|
218
- real = File.realpath(path) rescue next
219
- next unless real.start_with?(base)
220
- relative = real.sub("#{base}/", "")
221
- first_segment = relative.split("/").first
222
- next if EXCLUDED.include?(first_segment) || first_segment&.start_with?(".")
223
- if File.basename(real) == filename
224
- local_exact = relative
225
- break
226
- else
227
- local_ci ||= relative
228
+ candidates.each do |fn|
229
+ Dir.glob(File.join(@current_wiki_dir, fn)).each do |path|
230
+ real = File.realpath(path) rescue next
231
+ next unless real.start_with?(base)
232
+ relative = real.sub("#{base}/", "")
233
+ first_segment = relative.split("/").first
234
+ next if EXCLUDED.include?(first_segment) || first_segment&.start_with?(".")
235
+ if File.basename(real) == filename
236
+ local_exact = relative
237
+ break
238
+ else
239
+ local_ci ||= relative
240
+ end
228
241
  end
242
+ break if local_exact
229
243
  end
230
244
  return local_exact if local_exact
231
245
  return local_ci if local_ci
@@ -234,17 +248,20 @@ module MarkdownServer
234
248
  # Fall back to global recursive search
235
249
  exact_match = nil
236
250
  ci_match = nil
237
- Dir.glob(File.join(base, "**", filename), File::FNM_CASEFOLD).each do |path|
238
- real = File.realpath(path) rescue next
239
- next unless real.start_with?(base)
240
- relative = real.sub("#{base}/", "")
241
- first_segment = relative.split("/").first
242
- next if EXCLUDED.include?(first_segment) || first_segment&.start_with?(".")
243
- if File.basename(real) == filename
244
- exact_match ||= relative
245
- else
246
- ci_match ||= relative
251
+ candidates.each do |fn|
252
+ Dir.glob(File.join(base, "**", fn)).each do |path|
253
+ real = File.realpath(path) rescue next
254
+ next unless real.start_with?(base)
255
+ relative = real.sub("#{base}/", "")
256
+ first_segment = relative.split("/").first
257
+ next if EXCLUDED.include?(first_segment) || first_segment&.start_with?(".")
258
+ if File.basename(real) == filename
259
+ exact_match ||= relative
260
+ else
261
+ ci_match ||= relative
262
+ end
247
263
  end
264
+ break if exact_match
248
265
  end
249
266
 
250
267
  exact_match || ci_match
@@ -485,6 +502,83 @@ module MarkdownServer
485
502
  FETCH_MAX_BYTES = 512_000
486
503
  FETCH_TIMEOUT = 5
487
504
 
505
+ def strongs_map
506
+ url = settings.dictionary_url
507
+ return {} unless url
508
+
509
+ now = Time.now
510
+ if @@strongs_cache && @@strongs_cache_url == url && @@strongs_fetched_at
511
+ if url.match?(/\Ahttps?:\/\//i)
512
+ return @@strongs_cache if (now - @@strongs_fetched_at) < DICTIONARY_TTL
513
+ else
514
+ path = File.expand_path(url, root_dir)
515
+ mtime = File.mtime(path) rescue nil
516
+ return @@strongs_cache if mtime && mtime <= @@strongs_fetched_at
517
+ end
518
+ end
519
+
520
+ begin
521
+ raw = if url.match?(/\Ahttps?:\/\//i)
522
+ uri = URI.parse(url)
523
+ http = Net::HTTP.new(uri.host, uri.port)
524
+ http.use_ssl = (uri.scheme == "https")
525
+ http.open_timeout = FETCH_TIMEOUT
526
+ http.read_timeout = 10
527
+ req = Net::HTTP::Get.new(uri.request_uri)
528
+ req["Accept"] = "application/json"
529
+ resp = http.request(req)
530
+ return @@strongs_cache || {} unless resp.is_a?(Net::HTTPSuccess)
531
+ resp.body
532
+ else
533
+ path = File.expand_path(url, root_dir)
534
+ return @@strongs_cache || {} unless File.exist?(path)
535
+ File.read(path, encoding: "utf-8")
536
+ end
537
+
538
+ data = JSON.parse(raw)
539
+ url_tpl = data["url"] || ""
540
+ map = {}
541
+ %w[greek hebrew].each do |lang|
542
+ stems = data.dig("stems", lang)
543
+ next unless stems.is_a?(Hash)
544
+ stems.each_value do |entry|
545
+ sn = entry["strongs"].to_s.strip.upcase
546
+ next if sn.empty?
547
+ map[sn] = url_tpl.gsub("{filename}", entry["filename"].to_s)
548
+ end
549
+ end
550
+ @@strongs_cache = map
551
+ @@strongs_cache_url = url
552
+ @@strongs_fetched_at = now
553
+ map
554
+ rescue StandardError
555
+ @@strongs_cache || {}
556
+ end
557
+ end
558
+
559
+ def strongs_popup_url(strongs)
560
+ sn = strongs.to_s.strip.upcase
561
+ return nil unless sn.match?(/\A[GH]\d+\z/)
562
+ url = strongs_map[sn]
563
+ return url if url
564
+ # Fallback: Blue Letter Bible
565
+ prefix = sn.start_with?("H") ? "wlc" : "tr"
566
+ "https://www.blueletterbible.org/lexicon/#{sn.downcase}/nasb20/#{prefix}/0-1/"
567
+ end
568
+
569
+ def inject_strongs_urls(html)
570
+ return html unless settings.dictionary_url
571
+ html.gsub(/<span\s+class="subst"([^>]*data-strongs="([^"]+)"[^>]*)>/) do
572
+ attrs, strongs = $1, $2
573
+ popup_url = strongs_popup_url(strongs)
574
+ if popup_url && !attrs.include?("data-popup-url")
575
+ %(<span class="subst" data-popup-url="#{h(popup_url)}"#{attrs}>)
576
+ else
577
+ $&
578
+ end
579
+ end
580
+ end
581
+
488
582
  # Tags kept as-is (attributes stripped)
489
583
  ALLOWED_HTML = %w[p h1 h2 h3 h4 h5 h6 blockquote ul ol li
490
584
  pre br hr strong b em i sup sub code
@@ -794,6 +888,7 @@ module MarkdownServer
794
888
  </style>
795
889
  CSS
796
890
 
891
+ content = inject_strongs_urls(content)
797
892
  css + '<div style="font-family:Georgia,serif;line-height:1.7">' + content + "</div>"
798
893
  end
799
894
 
@@ -869,6 +964,7 @@ btn._loading=false;btn.style.opacity='1';});})(this)">&#128266;</button>)
869
964
  # Falls back to appending before </html>, then to end of document.
870
965
  def inject_markdownr_assets(html_content)
871
966
  settings.plugins.each { |p| html_content = p.transform_html(html_content) }
967
+ html_content = inject_strongs_urls(html_content)
872
968
 
873
969
  popup_config_script = "<script>var __popupConfig = {" \
874
970
  "localMd:#{settings.popup_local_md}," \
@@ -1,3 +1,3 @@
1
1
  module MarkdownServer
2
- VERSION = "0.6.9"
2
+ VERSION = "0.6.11"
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.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Dunn