markdownr 0.6.14 → 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 +22 -0
- data/views/popup_assets.erb +167 -0
- 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
|
@@ -2167,6 +2167,25 @@
|
|
|
2167
2167
|
}
|
|
2168
2168
|
}
|
|
2169
2169
|
|
|
2170
|
+
// StudyLight pronunciation: play audio instead of popup
|
|
2171
|
+
function playStudylightPronunc(anchor) {
|
|
2172
|
+
var href = anchor.getAttribute('href') || '';
|
|
2173
|
+
if (!anchor.classList.contains('pronunc-link')) return false;
|
|
2174
|
+
var m = href.match(/studylight\.org\/lexicons\/eng\/(hebrew|greek)\/(\d+)\.html/);
|
|
2175
|
+
if (!m) return false;
|
|
2176
|
+
var audioUrl = 'https://www.studylight.org/multi-media/audio/lexicons/eng/' + m[1] + '.html?n=' + m[2];
|
|
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
|
+
}
|
|
2184
|
+
anchor._slAudio.currentTime = 0;
|
|
2185
|
+
anchor._slAudio.play();
|
|
2186
|
+
return true;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2170
2189
|
// Left-click on desktop
|
|
2171
2190
|
document.addEventListener('click', function(e) {
|
|
2172
2191
|
if (popup && popup.contains(e.target)) return;
|
|
@@ -2174,6 +2193,7 @@
|
|
|
2174
2193
|
if (!anchor) { hidePopup(); return; }
|
|
2175
2194
|
var href = anchor.getAttribute('href');
|
|
2176
2195
|
if (!href || isAnchorOnly(href)) { hidePopup(); return; }
|
|
2196
|
+
if (playStudylightPronunc(anchor)) { e.preventDefault(); return; }
|
|
2177
2197
|
if (!anchor.closest('.md-content') && !anchor.closest('.frontmatter')) { hidePopup(); return; }
|
|
2178
2198
|
if (!shouldPopup(href)) { hidePopup(); return; }
|
|
2179
2199
|
e.preventDefault();
|
|
@@ -2197,6 +2217,7 @@
|
|
|
2197
2217
|
if (!anchor) { hidePopup(); return; }
|
|
2198
2218
|
var href = anchor.getAttribute('href');
|
|
2199
2219
|
if (!href || isAnchorOnly(href)) { hidePopup(); return; }
|
|
2220
|
+
if (playStudylightPronunc(anchor)) { e.preventDefault(); return; }
|
|
2200
2221
|
if (!anchor.closest('.md-content') && !anchor.closest('.frontmatter')) { hidePopup(); return; }
|
|
2201
2222
|
if (!shouldPopup(href)) { hidePopup(); return; }
|
|
2202
2223
|
e.preventDefault();
|
|
@@ -2219,6 +2240,7 @@
|
|
|
2219
2240
|
var href = a.getAttribute('href');
|
|
2220
2241
|
if (!href || isAnchorOnly(href)) return;
|
|
2221
2242
|
if (!shouldPopup(href)) return;
|
|
2243
|
+
if (a.classList.contains('pronunc-link') && /studylight\.org\/lexicons\/eng\//.test(href)) return;
|
|
2222
2244
|
a.addEventListener('mouseenter', function(e) {
|
|
2223
2245
|
clearTimeout(hoverTimer);
|
|
2224
2246
|
if (popup) return;
|
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; }
|
|
@@ -731,6 +769,25 @@
|
|
|
731
769
|
}
|
|
732
770
|
}
|
|
733
771
|
|
|
772
|
+
// StudyLight pronunciation: play audio instead of popup
|
|
773
|
+
function playStudylightPronunc(anchor) {
|
|
774
|
+
var href = anchor.getAttribute('href') || '';
|
|
775
|
+
if (!anchor.classList.contains('pronunc-link')) return false;
|
|
776
|
+
var m = href.match(/studylight\.org\/lexicons\/eng\/(hebrew|greek)\/(\d+)\.html/);
|
|
777
|
+
if (!m) return false;
|
|
778
|
+
var audioUrl = 'https://www.studylight.org/multi-media/audio/lexicons/eng/' + m[1] + '.html?n=' + m[2];
|
|
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
|
+
}
|
|
786
|
+
anchor._slAudio.currentTime = 0;
|
|
787
|
+
anchor._slAudio.play();
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
|
|
734
791
|
// Left-click on desktop
|
|
735
792
|
document.addEventListener('click', function(e) {
|
|
736
793
|
if (popup && popup.contains(e.target)) return;
|
|
@@ -738,6 +795,7 @@
|
|
|
738
795
|
if (!anchor) { hidePopup(); return; }
|
|
739
796
|
var href = anchor.getAttribute('href');
|
|
740
797
|
if (!href || isAnchorOnly(href)) { hidePopup(); return; }
|
|
798
|
+
if (playStudylightPronunc(anchor)) { e.preventDefault(); return; }
|
|
741
799
|
if (!shouldPopup(href)) { hidePopup(); return; }
|
|
742
800
|
e.preventDefault();
|
|
743
801
|
handleLink(anchor, e.clientX, e.clientY, false);
|
|
@@ -761,6 +819,7 @@
|
|
|
761
819
|
if (!anchor) { hidePopup(); return; }
|
|
762
820
|
var href = anchor.getAttribute('href');
|
|
763
821
|
if (!href || isAnchorOnly(href)) { hidePopup(); return; }
|
|
822
|
+
if (playStudylightPronunc(anchor)) { e.preventDefault(); return; }
|
|
764
823
|
if (!shouldPopup(href)) { hidePopup(); return; }
|
|
765
824
|
e.preventDefault();
|
|
766
825
|
var touch = e.changedTouches[0];
|
|
@@ -778,6 +837,7 @@
|
|
|
778
837
|
var href = a.getAttribute('href');
|
|
779
838
|
if (!href || isAnchorOnly(href)) return;
|
|
780
839
|
if (!shouldPopup(href)) return;
|
|
840
|
+
if (a.classList.contains('pronunc-link') && /studylight\.org\/lexicons\/eng\//.test(href)) return;
|
|
781
841
|
|
|
782
842
|
a.addEventListener('mouseenter', function(e) {
|
|
783
843
|
clearTimeout(hoverTimer);
|
|
@@ -989,6 +1049,15 @@
|
|
|
989
1049
|
});
|
|
990
1050
|
})();
|
|
991
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
|
+
|
|
992
1061
|
})();
|
|
993
1062
|
|
|
994
1063
|
// Full-page verse highlighting from URL hash (#vN or #vN-M)
|
|
@@ -1027,4 +1096,102 @@
|
|
|
1027
1096
|
if (toolbar) top -= toolbar.offsetHeight + 4;
|
|
1028
1097
|
window.scrollTo({ top: top, behavior: 'instant' });
|
|
1029
1098
|
})();
|
|
1099
|
+
|
|
1100
|
+
// Inject verse speaker icon for interlinear pages
|
|
1101
|
+
(function() {
|
|
1102
|
+
var verses = document.querySelectorAll('.verse[data-verse]');
|
|
1103
|
+
if (!verses.length) return;
|
|
1104
|
+
|
|
1105
|
+
function audioUrlFromLink(a) {
|
|
1106
|
+
if (!a) return null;
|
|
1107
|
+
var href = a.getAttribute('href') || '';
|
|
1108
|
+
var m = href.match(/studylight\.org\/lexicons\/eng\/(hebrew|greek)\/(\d+)\.html/);
|
|
1109
|
+
if (!m) return null;
|
|
1110
|
+
return 'https://www.studylight.org/multi-media/audio/lexicons/eng/' + m[1] + '.html?n=' + m[2];
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
verses.forEach(function(verse) {
|
|
1114
|
+
var verseNum = verse.querySelector('.verse-num');
|
|
1115
|
+
if (!verseNum) return;
|
|
1116
|
+
var words = verse.querySelectorAll('.word');
|
|
1117
|
+
if (!words.length) return;
|
|
1118
|
+
|
|
1119
|
+
// Collect audio URLs paired with their word elements
|
|
1120
|
+
var items = [];
|
|
1121
|
+
words.forEach(function(w) {
|
|
1122
|
+
var link = w.querySelector('.pronunc-link');
|
|
1123
|
+
var url = audioUrlFromLink(link);
|
|
1124
|
+
if (url) items.push({ word: w, url: url });
|
|
1125
|
+
});
|
|
1126
|
+
if (!items.length) return;
|
|
1127
|
+
|
|
1128
|
+
// Create a column-flex container to replace the bare verse-num,
|
|
1129
|
+
// with the number at top (row 1) and speaker icon at row 5
|
|
1130
|
+
var wrapper = document.createElement('span');
|
|
1131
|
+
wrapper.style.cssText = 'display:flex;flex-direction:column;align-self:stretch;align-items:center;margin-right:4px;';
|
|
1132
|
+
|
|
1133
|
+
// Row 1: verse number
|
|
1134
|
+
verseNum.style.cssText += ';margin:0;';
|
|
1135
|
+
wrapper.appendChild(verseNum);
|
|
1136
|
+
|
|
1137
|
+
// Spacer to push icon down to row 5
|
|
1138
|
+
var spacer = document.createElement('span');
|
|
1139
|
+
spacer.style.cssText = 'flex:1;';
|
|
1140
|
+
wrapper.appendChild(spacer);
|
|
1141
|
+
|
|
1142
|
+
// Row 5: speaker icon
|
|
1143
|
+
var btn = document.createElement('button');
|
|
1144
|
+
btn.textContent = '\u{1F50A}';
|
|
1145
|
+
btn.title = 'Audio from the excellent studylight.org';
|
|
1146
|
+
btn.style.cssText = 'background:none;border:none;cursor:pointer;padding:0;font-size:1em;line-height:1;opacity:0.6;';
|
|
1147
|
+
btn.addEventListener('mouseleave', function() { if (!btn._playing) btn.style.opacity = '0.6'; });
|
|
1148
|
+
|
|
1149
|
+
var preloaded = false;
|
|
1150
|
+
function preloadAll() {
|
|
1151
|
+
if (preloaded) return;
|
|
1152
|
+
preloaded = true;
|
|
1153
|
+
items.forEach(function(item) {
|
|
1154
|
+
var a = new Audio(item.url);
|
|
1155
|
+
a.preload = 'auto';
|
|
1156
|
+
a.load();
|
|
1157
|
+
item.audio = a;
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
// Start preloading on hover so audio is ready before click
|
|
1161
|
+
btn.addEventListener('mouseenter', function() {
|
|
1162
|
+
btn.style.opacity = '1';
|
|
1163
|
+
preloadAll();
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
btn.addEventListener('click', function() {
|
|
1167
|
+
if (btn._playing) return;
|
|
1168
|
+
btn._playing = true;
|
|
1169
|
+
btn.style.opacity = '1';
|
|
1170
|
+
preloadAll();
|
|
1171
|
+
|
|
1172
|
+
var i = 0;
|
|
1173
|
+
function playNext() {
|
|
1174
|
+
if (i >= items.length) {
|
|
1175
|
+
btn._playing = false;
|
|
1176
|
+
btn.style.opacity = '0.6';
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
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);
|
|
1189
|
+
}
|
|
1190
|
+
playNext();
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
wrapper.appendChild(btn);
|
|
1194
|
+
verse.insertBefore(wrapper, verse.firstChild);
|
|
1195
|
+
});
|
|
1196
|
+
})();
|
|
1030
1197
|
</script>
|