markdownr 0.6.8 → 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 +102 -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
|
|
@@ -607,6 +691,19 @@ module MarkdownServer
|
|
|
607
691
|
out.length > 10_000 ? out[0, 10_000] : out
|
|
608
692
|
end
|
|
609
693
|
|
|
694
|
+
def markdownr_html(html)
|
|
695
|
+
# Extract .md-content from a trusted markdownr page, preserving HTML as-is
|
|
696
|
+
content = html[/<div[^>]+class="md-content"[^>]*>([\s\S]*?)<\/div>\s*(?:<div\s+class="frontmatter"|<\/div>\s*<\/div>|\z)/im, 1]
|
|
697
|
+
return page_html(html) unless content && content.length > 10
|
|
698
|
+
|
|
699
|
+
# Also extract frontmatter if present
|
|
700
|
+
fm = html[/<div\s+class="frontmatter">([\s\S]*?)<\/div>\s*<\/div>/im, 0] || ""
|
|
701
|
+
|
|
702
|
+
result = content.strip
|
|
703
|
+
result += "\n#{fm}" unless fm.empty?
|
|
704
|
+
result.length > 15_000 ? result[0, 15_000] : result
|
|
705
|
+
end
|
|
706
|
+
|
|
610
707
|
def blueletterbible_html(html, url)
|
|
611
708
|
base = "https://www.blueletterbible.org"
|
|
612
709
|
|
|
@@ -781,6 +878,7 @@ module MarkdownServer
|
|
|
781
878
|
</style>
|
|
782
879
|
CSS
|
|
783
880
|
|
|
881
|
+
content = inject_strongs_urls(content)
|
|
784
882
|
css + '<div style="font-family:Georgia,serif;line-height:1.7">' + content + "</div>"
|
|
785
883
|
end
|
|
786
884
|
|
|
@@ -856,6 +954,7 @@ btn._loading=false;btn.style.opacity='1';});})(this)">🔊</button>)
|
|
|
856
954
|
# Falls back to appending before </html>, then to end of document.
|
|
857
955
|
def inject_markdownr_assets(html_content)
|
|
858
956
|
settings.plugins.each { |p| html_content = p.transform_html(html_content) }
|
|
957
|
+
html_content = inject_strongs_urls(html_content)
|
|
859
958
|
|
|
860
959
|
popup_config_script = "<script>var __popupConfig = {" \
|
|
861
960
|
"localMd:#{settings.popup_local_md}," \
|
|
@@ -1172,6 +1271,9 @@ btn._loading=false;btn.style.opacity='1';});})(this)">🔊</button>)
|
|
|
1172
1271
|
title = raw.match(/^([GH]\d+ - \w+)/i)&.[](1)&.sub(" - ", " – ") ||
|
|
1173
1272
|
raw.sub(/ [-–] .*/, "").strip
|
|
1174
1273
|
JSON.dump({ title: title, html: blueletterbible_html(html, url) })
|
|
1274
|
+
elsif url.match?(%r{/definitions/[^/]+\.md(\?|#|\z)}i)
|
|
1275
|
+
title = page_title(html).sub(/ [-–] .*/, "").strip
|
|
1276
|
+
JSON.dump({ title: title, html: markdownr_html(html) })
|
|
1175
1277
|
elsif url.match?(/scrip\.risensavior\.com/i)
|
|
1176
1278
|
title = page_title(html).sub(/ [-–] .*/, "").strip
|
|
1177
1279
|
JSON.dump({ title: title, html: scrip_html(html) })
|
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) {
|