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 +4 -4
- data/bin/markdownr +8 -0
- data/lib/markdown_server/app.rb +117 -21
- data/lib/markdown_server/version.rb +1 -1
- data/views/popup_assets.erb +37 -91
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 072ccb5cc0d25d1b06e7fc40cc51a6d4d71a03627fbf6569a822782a4151c8a4
|
|
4
|
+
data.tar.gz: '039ec59649a81611b158230190dee9c02343dc7fbbdf97008b8b05a116eecc66'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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!
|
data/lib/markdown_server/app.rb
CHANGED
|
@@ -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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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)">🔊</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}," \
|
data/views/popup_assets.erb
CHANGED
|
@@ -882,53 +882,11 @@
|
|
|
882
882
|
document.head.appendChild(style);
|
|
883
883
|
})();
|
|
884
884
|
|
|
885
|
-
// Substitution span popups —
|
|
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
|
|
945
|
-
var
|
|
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
|
-
|
|
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
|
-
'<
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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:#
|
|
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) {
|