markdownr 0.6.16 → 0.6.17
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/lib/markdown_server/app.rb +60 -35
- data/lib/markdown_server/version.rb +1 -1
- data/views/layout.erb +6 -0
- data/views/popup_assets.erb +71 -14
- 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: 1a2282422f5341fe474b7b7e34af4387a7d6784b1945501f969ea3098e768377
|
|
4
|
+
data.tar.gz: 45b589b50541dc8e88999cba5df250047a0a298d00bbc2b54bb5d7dd5868009d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 56e7530f96dd2622ad883c0fdf9ee83d6731f8ce372476907a2ea7f22979e739e13f630f3ee0b121f39d3d3192f702ca27434de441ae59cd11f3515054f62c8d
|
|
7
|
+
data.tar.gz: d0831ec05ccd0e1e57dfe07f57dd3ba6ccca5d7d958c7ddf72e578995dbd4ea9ce911eb7af8e6fac96553b32c76221369338e521fab88c67ac5839d8c2d932cb
|
data/lib/markdown_server/app.rb
CHANGED
|
@@ -47,6 +47,10 @@ module MarkdownServer
|
|
|
47
47
|
@@strongs_fetched_at = nil
|
|
48
48
|
DICTIONARY_TTL = 3600 # 1 hour
|
|
49
49
|
|
|
50
|
+
# CSS cache for external stylesheets (keyed by absolute URL)
|
|
51
|
+
@@css_cache = {}
|
|
52
|
+
CSS_TTL = 3600 # 1 hour
|
|
53
|
+
|
|
50
54
|
def self.load_plugins!
|
|
51
55
|
Dir[File.join(__dir__, "plugins", "*", "plugin.rb")].sort.each { |f| require f }
|
|
52
56
|
set :plugins, PluginRegistry.load_plugins(settings.root_dir, settings.plugin_overrides)
|
|
@@ -568,7 +572,7 @@ module MarkdownServer
|
|
|
568
572
|
|
|
569
573
|
def inject_strongs_urls(html)
|
|
570
574
|
return html unless settings.dictionary_url
|
|
571
|
-
html.gsub(/<span\s+class="subst"([^>]*data-strongs="([^"]+)"[^>]*)>/) do
|
|
575
|
+
html = html.gsub(/<span\s+class="subst"([^>]*data-strongs="([^"]+)"[^>]*)>/) do
|
|
572
576
|
attrs, strongs = $1, $2
|
|
573
577
|
popup_url = strongs_popup_url(strongs)
|
|
574
578
|
if popup_url && !attrs.include?("data-popup-url")
|
|
@@ -577,6 +581,16 @@ module MarkdownServer
|
|
|
577
581
|
$&
|
|
578
582
|
end
|
|
579
583
|
end
|
|
584
|
+
# Interlinear .word spans: inject data-popup-url when Strong's is in dictionary
|
|
585
|
+
html.gsub(/<span\s+class="word"([^>]*data-strongs="([^"]+)"[^>]*)>/) do
|
|
586
|
+
attrs, strongs = $1, $2
|
|
587
|
+
dict_url = strongs_map[strongs.strip.upcase]
|
|
588
|
+
if dict_url && !attrs.include?("data-popup-url")
|
|
589
|
+
%(<span class="word" data-popup-url="#{h(dict_url)}"#{attrs}>)
|
|
590
|
+
else
|
|
591
|
+
$&
|
|
592
|
+
end
|
|
593
|
+
end
|
|
580
594
|
end
|
|
581
595
|
|
|
582
596
|
# Tags kept as-is (attributes stripped)
|
|
@@ -590,6 +604,26 @@ module MarkdownServer
|
|
|
590
604
|
STRIP_FULL = %w[script style nav header footer form input
|
|
591
605
|
button select textarea svg iframe noscript].to_set
|
|
592
606
|
|
|
607
|
+
def fetch_css(url_str)
|
|
608
|
+
cached = @@css_cache[url_str]
|
|
609
|
+
return cached[:body] if cached && (Time.now - cached[:at]) < CSS_TTL
|
|
610
|
+
|
|
611
|
+
uri = URI.parse(url_str)
|
|
612
|
+
return nil unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
613
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
614
|
+
http.use_ssl = (uri.scheme == "https")
|
|
615
|
+
http.open_timeout = FETCH_TIMEOUT
|
|
616
|
+
http.read_timeout = FETCH_TIMEOUT
|
|
617
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
618
|
+
req["Accept"] = "text/css"
|
|
619
|
+
resp = http.request(req)
|
|
620
|
+
body = resp.is_a?(Net::HTTPSuccess) ? resp.body.to_s.encode("utf-8", invalid: :replace, undef: :replace) : nil
|
|
621
|
+
@@css_cache[url_str] = { body: body, at: Time.now } if body
|
|
622
|
+
body
|
|
623
|
+
rescue
|
|
624
|
+
@@css_cache.dig(url_str, :body)
|
|
625
|
+
end
|
|
626
|
+
|
|
593
627
|
def fetch_external_page(url_str)
|
|
594
628
|
uri = URI.parse(url_str)
|
|
595
629
|
return nil unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
@@ -865,10 +899,10 @@ module MarkdownServer
|
|
|
865
899
|
info_html + infl_html + usage_html + conc_html
|
|
866
900
|
end
|
|
867
901
|
|
|
868
|
-
def scrip_html(html)
|
|
902
|
+
def scrip_html(html, source_url = nil)
|
|
869
903
|
# Extract the passage content block
|
|
870
904
|
content = html[/<div\s+class="passage-text[^"]*"[^>]*>([\s\S]*)<\/div>\s*(?:<nav|<script|<style|\z)/im, 1] || ""
|
|
871
|
-
return page_html(html) if content.empty?
|
|
905
|
+
return page_html(html, source_url) if content.empty?
|
|
872
906
|
|
|
873
907
|
# Add data-verse attributes to verse spans for popup scrolling.
|
|
874
908
|
# Verse numbers live in <sup class="verse-number">N </sup> inside verse spans.
|
|
@@ -876,39 +910,30 @@ module MarkdownServer
|
|
|
876
910
|
%(<sup class="verse-number" data-verse="#{$1}">#{$1} </sup>)
|
|
877
911
|
end
|
|
878
912
|
|
|
879
|
-
#
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
.
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
.poetry-stanza { margin: 0 0 0 3rem; }
|
|
901
|
-
.poetry-stanza.stanza-break { margin-top: 0.75rem; }
|
|
902
|
-
.poetry-line { margin: 0; padding: 0; line-height: 1.5; }
|
|
903
|
-
.poetry-line.indent-1 { padding-left: 2em; }
|
|
904
|
-
.poetry-line.indent-2 { padding-left: 4em; }
|
|
905
|
-
.verse-selected { background: #e5eefb; border-radius: 2px; }
|
|
906
|
-
.subst-tip { display: none; }
|
|
907
|
-
</style>
|
|
908
|
-
CSS
|
|
913
|
+
# Fetch referenced stylesheets from the source page and inline them
|
|
914
|
+
css_blocks = ""
|
|
915
|
+
if source_url
|
|
916
|
+
base_uri = URI.parse(source_url) rescue nil
|
|
917
|
+
html.scan(/<link[^>]+rel=["']stylesheet["'][^>]*>/i).each do |tag|
|
|
918
|
+
href = tag[/href=["']([^"']+)["']/i, 1]
|
|
919
|
+
next unless href
|
|
920
|
+
abs = (base_uri ? (URI.join(base_uri, href).to_s rescue nil) : nil)
|
|
921
|
+
next unless abs
|
|
922
|
+
css_body = fetch_css(abs)
|
|
923
|
+
# Resolve @import within the CSS (one level deep)
|
|
924
|
+
if css_body
|
|
925
|
+
css_dir = abs.sub(%r{/[^/]*\z}, "/")
|
|
926
|
+
css_body = css_body.gsub(/@import\s+url\(["']?([^"')]+)["']?\)\s*;/) do
|
|
927
|
+
import_url = (URI.join(css_dir, $1).to_s rescue nil)
|
|
928
|
+
import_url ? (fetch_css(import_url) || "") : ""
|
|
929
|
+
end
|
|
930
|
+
css_blocks += "<style>#{css_body}</style>\n"
|
|
931
|
+
end
|
|
932
|
+
end
|
|
933
|
+
end
|
|
909
934
|
|
|
910
935
|
content = inject_strongs_urls(content)
|
|
911
|
-
|
|
936
|
+
css_blocks + '<div style="font-family:Georgia,serif;line-height:1.7">' + content + "</div>"
|
|
912
937
|
end
|
|
913
938
|
|
|
914
939
|
def inline_directory_html(dir_path, relative_dir)
|
|
@@ -1315,7 +1340,7 @@ btn._loading=false;btn.style.opacity='1';});})(this)">🔊</button>)
|
|
|
1315
1340
|
JSON.dump({ title: title, html: markdownr_html(html) })
|
|
1316
1341
|
elsif url.match?(/scrip\.risensavior\.com/i)
|
|
1317
1342
|
title = page_title(html).sub(/ [-–] .*/, "").strip
|
|
1318
|
-
JSON.dump({ title: title, html: scrip_html(html) })
|
|
1343
|
+
JSON.dump({ title: title, html: scrip_html(html, url) })
|
|
1319
1344
|
else
|
|
1320
1345
|
title = page_title(html).sub(/ [-–] .*/, "").strip
|
|
1321
1346
|
JSON.dump({ title: title, html: page_html(html, url) })
|
data/views/layout.erb
CHANGED
|
@@ -2175,6 +2175,12 @@
|
|
|
2175
2175
|
if (!m) return false;
|
|
2176
2176
|
var audioUrl = 'https://www.studylight.org/multi-media/audio/lexicons/eng/' + m[1] + '.html?n=' + m[2];
|
|
2177
2177
|
if (!anchor._slAudio) anchor._slAudio = new Audio(audioUrl);
|
|
2178
|
+
var wordEl = anchor.closest('.word');
|
|
2179
|
+
if (wordEl) {
|
|
2180
|
+
wordEl.classList.add('audio-playing');
|
|
2181
|
+
anchor._slAudio.onended = function() { wordEl.classList.remove('audio-playing'); };
|
|
2182
|
+
anchor._slAudio.onerror = function() { wordEl.classList.remove('audio-playing'); };
|
|
2183
|
+
}
|
|
2178
2184
|
anchor._slAudio.currentTime = 0;
|
|
2179
2185
|
anchor._slAudio.play();
|
|
2180
2186
|
return true;
|
data/views/popup_assets.erb
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
<style>
|
|
2
|
+
/* Word highlight during verse audio playback */
|
|
3
|
+
.word.audio-playing {
|
|
4
|
+
background: #fef08a;
|
|
5
|
+
border-radius: 3px;
|
|
6
|
+
transition: background 0.15s ease;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* Interlinear translit-link with dictionary definition available */
|
|
10
|
+
.word[data-popup-url] .translit-link {
|
|
11
|
+
color: #7b3fa0;
|
|
12
|
+
text-decoration: underline dotted #b07fd4 1px;
|
|
13
|
+
text-underline-offset: 2px;
|
|
14
|
+
}
|
|
15
|
+
.word[data-popup-url] .translit-link:hover {
|
|
16
|
+
color: #5a2c78;
|
|
17
|
+
text-decoration: underline solid #5a2c78 1px;
|
|
18
|
+
}
|
|
19
|
+
|
|
2
20
|
/* Bible Gateway citation links */
|
|
3
21
|
a[href*="biblegateway.com"],
|
|
4
22
|
a[href*="scrip.risensavior.com"] {
|
|
@@ -197,6 +215,26 @@
|
|
|
197
215
|
.link-ctx-popup-body th, .link-ctx-popup-body td { border: 1px solid #ddd; padding: 0.3rem 0.5rem; }
|
|
198
216
|
.link-ctx-popup-body th { background: #f5f0e4; }
|
|
199
217
|
|
|
218
|
+
/*
|
|
219
|
+
* Reset interlinear .verse flex layout back to inline for passage popups.
|
|
220
|
+
* Interlinear pages define .verse { display: flex; border-bottom: ... } which
|
|
221
|
+
* would break inline passage content rendered inside a popup.
|
|
222
|
+
*/
|
|
223
|
+
.link-ctx-popup-body .verse { display: inline; flex-wrap: initial; align-items: initial; gap: initial; margin: 0; padding: 0; border-bottom: none; }
|
|
224
|
+
|
|
225
|
+
/*
|
|
226
|
+
* Substitution mode for popups: force translation-only.
|
|
227
|
+
* The source CSS uses body.show-t / body.show-l / etc. to toggle modes, but
|
|
228
|
+
* those body classes leak into popup content. Override with !important to
|
|
229
|
+
* ensure only the translation span is visible inside popups.
|
|
230
|
+
*/
|
|
231
|
+
.link-ctx-popup-body .subst-x { display: inline !important; color: #1a5276; font-style: italic; }
|
|
232
|
+
.link-ctx-popup-body .subst-t,
|
|
233
|
+
.link-ctx-popup-body .subst-l,
|
|
234
|
+
.link-ctx-popup-body .subst-o,
|
|
235
|
+
.link-ctx-popup-body .subst-p,
|
|
236
|
+
.link-ctx-popup-body .subst-s { display: none !important; }
|
|
237
|
+
|
|
200
238
|
/* Blue Letter Bible popup tables */
|
|
201
239
|
.blb-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; margin-bottom: 0.6rem; }
|
|
202
240
|
.blb-table th, .blb-table td { padding: 3px 7px; border: 1px solid #ddd; }
|
|
@@ -739,6 +777,12 @@
|
|
|
739
777
|
if (!m) return false;
|
|
740
778
|
var audioUrl = 'https://www.studylight.org/multi-media/audio/lexicons/eng/' + m[1] + '.html?n=' + m[2];
|
|
741
779
|
if (!anchor._slAudio) anchor._slAudio = new Audio(audioUrl);
|
|
780
|
+
var wordEl = anchor.closest('.word');
|
|
781
|
+
if (wordEl) {
|
|
782
|
+
wordEl.classList.add('audio-playing');
|
|
783
|
+
anchor._slAudio.onended = function() { wordEl.classList.remove('audio-playing'); };
|
|
784
|
+
anchor._slAudio.onerror = function() { wordEl.classList.remove('audio-playing'); };
|
|
785
|
+
}
|
|
742
786
|
anchor._slAudio.currentTime = 0;
|
|
743
787
|
anchor._slAudio.play();
|
|
744
788
|
return true;
|
|
@@ -1005,6 +1049,15 @@
|
|
|
1005
1049
|
});
|
|
1006
1050
|
})();
|
|
1007
1051
|
|
|
1052
|
+
// Rewrite translit-link hrefs for words with dictionary definitions
|
|
1053
|
+
// so the existing popup system uses the dictionary URL instead of BLB
|
|
1054
|
+
(function() {
|
|
1055
|
+
document.querySelectorAll('.word[data-popup-url]').forEach(function(word) {
|
|
1056
|
+
var link = word.querySelector('.translit-link');
|
|
1057
|
+
if (link) link.setAttribute('href', word.dataset.popupUrl);
|
|
1058
|
+
});
|
|
1059
|
+
})();
|
|
1060
|
+
|
|
1008
1061
|
})();
|
|
1009
1062
|
|
|
1010
1063
|
// Full-page verse highlighting from URL hash (#vN or #vN-M)
|
|
@@ -1063,14 +1116,14 @@
|
|
|
1063
1116
|
var words = verse.querySelectorAll('.word');
|
|
1064
1117
|
if (!words.length) return;
|
|
1065
1118
|
|
|
1066
|
-
// Collect audio URLs
|
|
1067
|
-
var
|
|
1119
|
+
// Collect audio URLs paired with their word elements
|
|
1120
|
+
var items = [];
|
|
1068
1121
|
words.forEach(function(w) {
|
|
1069
1122
|
var link = w.querySelector('.pronunc-link');
|
|
1070
1123
|
var url = audioUrlFromLink(link);
|
|
1071
|
-
if (url)
|
|
1124
|
+
if (url) items.push({ word: w, url: url });
|
|
1072
1125
|
});
|
|
1073
|
-
if (!
|
|
1126
|
+
if (!items.length) return;
|
|
1074
1127
|
|
|
1075
1128
|
// Create a column-flex container to replace the bare verse-num,
|
|
1076
1129
|
// with the number at top (row 1) and speaker icon at row 5
|
|
@@ -1093,16 +1146,15 @@
|
|
|
1093
1146
|
btn.style.cssText = 'background:none;border:none;cursor:pointer;padding:0;font-size:1em;line-height:1;opacity:0.6;';
|
|
1094
1147
|
btn.addEventListener('mouseleave', function() { if (!btn._playing) btn.style.opacity = '0.6'; });
|
|
1095
1148
|
|
|
1096
|
-
var audioCache = [];
|
|
1097
1149
|
var preloaded = false;
|
|
1098
1150
|
function preloadAll() {
|
|
1099
1151
|
if (preloaded) return;
|
|
1100
1152
|
preloaded = true;
|
|
1101
|
-
|
|
1102
|
-
var a = new Audio(
|
|
1153
|
+
items.forEach(function(item) {
|
|
1154
|
+
var a = new Audio(item.url);
|
|
1103
1155
|
a.preload = 'auto';
|
|
1104
1156
|
a.load();
|
|
1105
|
-
|
|
1157
|
+
item.audio = a;
|
|
1106
1158
|
});
|
|
1107
1159
|
}
|
|
1108
1160
|
// Start preloading on hover so audio is ready before click
|
|
@@ -1119,16 +1171,21 @@
|
|
|
1119
1171
|
|
|
1120
1172
|
var i = 0;
|
|
1121
1173
|
function playNext() {
|
|
1122
|
-
if (i >=
|
|
1174
|
+
if (i >= items.length) {
|
|
1123
1175
|
btn._playing = false;
|
|
1124
1176
|
btn.style.opacity = '0.6';
|
|
1125
1177
|
return;
|
|
1126
1178
|
}
|
|
1127
|
-
var
|
|
1128
|
-
audio
|
|
1129
|
-
audio.
|
|
1130
|
-
|
|
1131
|
-
|
|
1179
|
+
var item = items[i];
|
|
1180
|
+
item.word.classList.add('audio-playing');
|
|
1181
|
+
item.audio.currentTime = 0;
|
|
1182
|
+
var advance = function() {
|
|
1183
|
+
item.word.classList.remove('audio-playing');
|
|
1184
|
+
i++; playNext();
|
|
1185
|
+
};
|
|
1186
|
+
item.audio.onended = advance;
|
|
1187
|
+
item.audio.onerror = advance;
|
|
1188
|
+
item.audio.play().catch(advance);
|
|
1132
1189
|
}
|
|
1133
1190
|
playNext();
|
|
1134
1191
|
});
|