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 +4 -4
- data/bin/markdownr +8 -0
- data/lib/markdown_server/app.rb +86 -0
- 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: 352e2d418a6bf94404763caeaa5b8e43c5f07fa2bff53c208a60972b16420a7e
|
|
4
|
+
data.tar.gz: e2b8d84227484fca2134dc9a1cffa67bebf3022134377945a9081dc19c6c3fe0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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!
|
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)
|
|
@@ -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)">🔊</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}," \
|
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) {
|