markdownr 0.6.4 → 0.6.6
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 +27 -29
- data/lib/markdown_server/version.rb +1 -1
- data/views/layout.erb +20 -4
- data/views/markdown.erb +5 -9
- data/views/popup_assets.erb +192 -10
- 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: 3c823e9362df716b74c9f6523e2cc0e2a7873ee7b1655b78da6d284921e85a6d
|
|
4
|
+
data.tar.gz: 479d4c5709c1c66560bb9c7116af9c14992f8a8feedb7c2a9eaa58e12af36b6f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 859817ec7ee12fac0837634bcf3b53f5a18c2b313aec9b56bb2a74242e0df761997714f2abfd71b8d7bad193eee246924232f39d2a78e3e6b843916c1ac5398f
|
|
7
|
+
data.tar.gz: 79092042f9e356da823a922354d8e9de8f919aab6fa6e5fa465685b728fa2a8dfede0d958c0a269c41b534e22a5fab430e6569a898bd4342374f5f123d40756a
|
data/lib/markdown_server/app.rb
CHANGED
|
@@ -750,7 +750,7 @@ module MarkdownServer
|
|
|
750
750
|
|
|
751
751
|
def scrip_html(html)
|
|
752
752
|
# Extract the passage content block
|
|
753
|
-
content = html[/<div\s+class="passage-text[^"]*"[^>]*>([\s\S]*?)<\/div>\s*(?:<nav
|
|
753
|
+
content = html[/<div\s+class="passage-text[^"]*"[^>]*>([\s\S]*?)<\/div>\s*(?:<nav|\z)/im, 1] || ""
|
|
754
754
|
return page_html(html) if content.empty?
|
|
755
755
|
|
|
756
756
|
# Add data-verse attributes to verse spans for popup scrolling.
|
|
@@ -762,7 +762,7 @@ module MarkdownServer
|
|
|
762
762
|
# Inline the essential substitution CSS (show replacement mode by default)
|
|
763
763
|
css = <<~CSS
|
|
764
764
|
<style>
|
|
765
|
-
.subst { cursor:
|
|
765
|
+
.subst { cursor: pointer; border-radius: 2px; background: #f0ebff; position: relative; }
|
|
766
766
|
.subst:hover { background: #e0d4ff; outline: 1px solid #a78bfa; }
|
|
767
767
|
.subst-t, .subst-o, .subst-x, .subst-s { display: none; }
|
|
768
768
|
.subst-r { display: inline; color: #7c3aed; font-style: italic; }
|
|
@@ -777,36 +777,11 @@ module MarkdownServer
|
|
|
777
777
|
.poetry-line { margin: 0; padding: 0; line-height: 1.5; }
|
|
778
778
|
.poetry-line.indent-1 { padding-left: 2em; }
|
|
779
779
|
.poetry-line.indent-2 { padding-left: 4em; }
|
|
780
|
-
.subst-tip { display: none;
|
|
781
|
-
.subst-tip::after { content: ""; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); border: 5px solid transparent; border-top-color: #1e293b; }
|
|
782
|
-
.subst:hover .subst-tip { display: block; }
|
|
780
|
+
.subst-tip { display: none; }
|
|
783
781
|
</style>
|
|
784
782
|
CSS
|
|
785
783
|
|
|
786
|
-
|
|
787
|
-
js = <<~JS
|
|
788
|
-
<script>
|
|
789
|
-
document.querySelectorAll('.subst').forEach(function(el) {
|
|
790
|
-
var t = el.querySelector('.subst-t'), r = el.querySelector('.subst-r'),
|
|
791
|
-
o = el.querySelector('.subst-o'), x = el.querySelector('.subst-x'),
|
|
792
|
-
s = el.querySelector('.subst-s');
|
|
793
|
-
var lines = [];
|
|
794
|
-
if (t) lines.push(t.textContent);
|
|
795
|
-
if (r) lines.push(r.textContent);
|
|
796
|
-
if (o) lines.push(o.textContent);
|
|
797
|
-
if (x) lines.push(x.textContent);
|
|
798
|
-
if (s) lines.push(s.textContent);
|
|
799
|
-
if (lines.length > 1) {
|
|
800
|
-
var tip = document.createElement('span');
|
|
801
|
-
tip.className = 'subst-tip';
|
|
802
|
-
tip.innerHTML = lines.join('<br>');
|
|
803
|
-
el.appendChild(tip);
|
|
804
|
-
}
|
|
805
|
-
});
|
|
806
|
-
</script>
|
|
807
|
-
JS
|
|
808
|
-
|
|
809
|
-
css + '<div style="font-family:Georgia,serif;line-height:1.7">' + content + "</div>" + js
|
|
784
|
+
css + '<div style="font-family:Georgia,serif;line-height:1.7">' + content + "</div>"
|
|
810
785
|
end
|
|
811
786
|
|
|
812
787
|
def inline_directory_html(dir_path, relative_dir)
|
|
@@ -1122,6 +1097,29 @@ module MarkdownServer
|
|
|
1122
1097
|
JSON.dump({ title: title.to_s, html: html, frontmatter_html: frontmatter_html })
|
|
1123
1098
|
end
|
|
1124
1099
|
|
|
1100
|
+
get "/fetch-json" do
|
|
1101
|
+
content_type :json
|
|
1102
|
+
url = params[:url].to_s.strip
|
|
1103
|
+
halt 400, '{"error":"invalid url"}' unless url.match?(/\Ahttps?:\/\//i)
|
|
1104
|
+
|
|
1105
|
+
uri = URI.parse(url)
|
|
1106
|
+
halt 400, '{"error":"invalid url"}' unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
1107
|
+
|
|
1108
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
1109
|
+
http.use_ssl = (uri.scheme == "https")
|
|
1110
|
+
http.open_timeout = FETCH_TIMEOUT
|
|
1111
|
+
http.read_timeout = FETCH_TIMEOUT
|
|
1112
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
1113
|
+
req["Accept"] = "application/json"
|
|
1114
|
+
resp = http.request(req)
|
|
1115
|
+
halt 502, '{"error":"fetch failed"}' unless resp.is_a?(Net::HTTPSuccess)
|
|
1116
|
+
|
|
1117
|
+
headers "Cache-Control" => "public, max-age=3600"
|
|
1118
|
+
resp.body
|
|
1119
|
+
rescue StandardError
|
|
1120
|
+
halt 502, '{"error":"fetch failed"}'
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1125
1123
|
get "/fetch" do
|
|
1126
1124
|
content_type :json
|
|
1127
1125
|
url = params[:url].to_s.strip
|
data/views/layout.erb
CHANGED
|
@@ -99,6 +99,12 @@
|
|
|
99
99
|
border-bottom: 2px solid #d4b96a;
|
|
100
100
|
padding-bottom: 0.5rem;
|
|
101
101
|
}
|
|
102
|
+
.page-title-download {
|
|
103
|
+
color: inherit;
|
|
104
|
+
text-decoration: none;
|
|
105
|
+
border-bottom: none;
|
|
106
|
+
}
|
|
107
|
+
.page-title-download:hover { color: inherit; }
|
|
102
108
|
|
|
103
109
|
/* Directory listing */
|
|
104
110
|
.dir-listing {
|
|
@@ -199,6 +205,7 @@
|
|
|
199
205
|
.md-content {
|
|
200
206
|
line-height: 1.8;
|
|
201
207
|
}
|
|
208
|
+
.md-content > :first-child { margin-top: 0; }
|
|
202
209
|
.md-content h1 { font-size: 1.5rem; margin: 1.5rem 0 0.8rem; color: #3a3a3a; }
|
|
203
210
|
.md-content h2 {
|
|
204
211
|
font-size: 1.25rem;
|
|
@@ -2024,10 +2031,19 @@
|
|
|
2024
2031
|
}
|
|
2025
2032
|
|
|
2026
2033
|
function findVerseSpans(root, verseNum) {
|
|
2027
|
-
var
|
|
2028
|
-
|
|
2029
|
-
var
|
|
2030
|
-
|
|
2034
|
+
var num = String(verseNum);
|
|
2035
|
+
// Prefer .verse spans (full verse text) with exact verse number match
|
|
2036
|
+
var all = root.querySelectorAll('.verse[data-verse]');
|
|
2037
|
+
var matches = [];
|
|
2038
|
+
for (var i = 0; i < all.length; i++) {
|
|
2039
|
+
var dv = all[i].getAttribute('data-verse');
|
|
2040
|
+
var after = dv.split(':').pop();
|
|
2041
|
+
if (after === num) matches.push(all[i]);
|
|
2042
|
+
}
|
|
2043
|
+
if (matches.length) return matches;
|
|
2044
|
+
// Fallback: bare data-verse="N" (e.g. sup elements from scrip_html)
|
|
2045
|
+
var span = root.querySelector('[data-verse="' + num + '"]');
|
|
2046
|
+
return span ? [span] : [];
|
|
2031
2047
|
}
|
|
2032
2048
|
|
|
2033
2049
|
function applyPopupAnchor(hash) {
|
data/views/markdown.erb
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
<% _file_path = @crumbs.map { |c| c[:name] }.drop(1).join("/") %>
|
|
2
2
|
<div class="title-bar">
|
|
3
|
-
<h1 class="page-title"><%= h(@title) %></h1>
|
|
3
|
+
<h1 class="page-title"><a href="<%= @download_href %>" class="page-title-download"><%= h(@title) %></a></h1>
|
|
4
4
|
<form class="search-form" action="<%= search_form_path(_file_path) %>" method="get">
|
|
5
5
|
<input type="text" name="q" placeholder="Search this document..." value="">
|
|
6
6
|
</form>
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
|
-
<div class="toolbar">
|
|
10
|
-
<a href="<%= @download_href %>">Download</a>
|
|
11
|
-
</div>
|
|
12
|
-
|
|
13
9
|
<% if @has_toc %>
|
|
14
10
|
<button class="toc-fab" id="toc-fab" aria-label="Table of Contents">☰</button>
|
|
15
11
|
<div class="toc-overlay" id="toc-overlay"></div>
|
|
@@ -43,6 +39,10 @@
|
|
|
43
39
|
<% end %>
|
|
44
40
|
|
|
45
41
|
<div class="page-main">
|
|
42
|
+
<div class="md-content" data-file="<%= h(@title) %>">
|
|
43
|
+
<%= @content %>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
46
|
<% if @meta && !@meta.empty? %>
|
|
47
47
|
<div class="frontmatter">
|
|
48
48
|
<div class="frontmatter-heading">Frontmatter</div>
|
|
@@ -56,9 +56,5 @@
|
|
|
56
56
|
</table>
|
|
57
57
|
</div>
|
|
58
58
|
<% end %>
|
|
59
|
-
|
|
60
|
-
<div class="md-content" data-file="<%= h(@title) %>">
|
|
61
|
-
<%= @content %>
|
|
62
|
-
</div>
|
|
63
59
|
</div>
|
|
64
60
|
</div>
|
data/views/popup_assets.erb
CHANGED
|
@@ -596,10 +596,19 @@
|
|
|
596
596
|
}
|
|
597
597
|
|
|
598
598
|
function findVerseSpans(root, verseNum) {
|
|
599
|
-
var
|
|
600
|
-
|
|
601
|
-
var
|
|
602
|
-
|
|
599
|
+
var num = String(verseNum);
|
|
600
|
+
// Prefer .verse spans (full verse text) with exact verse number match
|
|
601
|
+
var all = root.querySelectorAll('.verse[data-verse]');
|
|
602
|
+
var matches = [];
|
|
603
|
+
for (var i = 0; i < all.length; i++) {
|
|
604
|
+
var dv = all[i].getAttribute('data-verse');
|
|
605
|
+
var after = dv.split(':').pop();
|
|
606
|
+
if (after === num) matches.push(all[i]);
|
|
607
|
+
}
|
|
608
|
+
if (matches.length) return matches;
|
|
609
|
+
// Fallback: bare data-verse="N" (e.g. sup elements from scrip_html)
|
|
610
|
+
var span = root.querySelector('[data-verse="' + num + '"]');
|
|
611
|
+
return span ? [span] : [];
|
|
603
612
|
}
|
|
604
613
|
|
|
605
614
|
function applyPopupAnchor(hash) {
|
|
@@ -873,6 +882,167 @@
|
|
|
873
882
|
document.head.appendChild(style);
|
|
874
883
|
})();
|
|
875
884
|
|
|
885
|
+
// Substitution span popups — look up Strong's numbers in dictionary.json
|
|
886
|
+
(function() {
|
|
887
|
+
var substEls = document.querySelectorAll('.subst[data-strongs]');
|
|
888
|
+
if (!substEls.length) return;
|
|
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
|
+
function blbFallbackUrl(strongs) {
|
|
933
|
+
var s = strongs.toLowerCase();
|
|
934
|
+
var prefix = s.charAt(0) === 'h' ? 'wlc' : 'tr';
|
|
935
|
+
return 'https://www.blueletterbible.org/lexicon/' + s + '/nasb20/' + prefix + '/0-1/';
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function handleSubst(el, x, y) {
|
|
939
|
+
var strongs = (el.dataset.strongs || '').toUpperCase();
|
|
940
|
+
if (!strongs) return;
|
|
941
|
+
|
|
942
|
+
var rect = el.getBoundingClientRect();
|
|
943
|
+
var replacement = el.dataset.replacement || '';
|
|
944
|
+
var original = el.dataset.original || '';
|
|
945
|
+
var translit = el.dataset.translit || '';
|
|
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
|
+
}
|
|
970
|
+
|
|
971
|
+
// Show popup with loading state and correct title/href
|
|
972
|
+
showPopup(x, y, title,
|
|
973
|
+
'<p style="opacity:0.5;margin:0;font-family:sans-serif">Loading\u2026</p>', href, rect);
|
|
974
|
+
|
|
975
|
+
if (cached === undefined) {
|
|
976
|
+
cache[cacheKey] = null;
|
|
977
|
+
fetch('/fetch?url=' + encodeURIComponent(href))
|
|
978
|
+
.then(function(r) { return r.ok ? r.json() : null; })
|
|
979
|
+
.then(function(data) {
|
|
980
|
+
if (!data || data.error) {
|
|
981
|
+
cache[cacheKey] = false;
|
|
982
|
+
updatePopup(
|
|
983
|
+
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
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() {
|
|
991
|
+
cache[cacheKey] = false;
|
|
992
|
+
updatePopup(
|
|
993
|
+
'<div class="link-ctx-popup-url">' + escHtml(href) + '</div>' +
|
|
994
|
+
'<p style="margin:0.5rem 0 0;color:#c44;font-family:sans-serif;font-size:0.82rem">Could not fetch page content.</p>');
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function findSubst(el) {
|
|
1001
|
+
while (el && el.tagName !== 'BODY') {
|
|
1002
|
+
if (el.classList && el.classList.contains('subst') && el.dataset.strongs) return el;
|
|
1003
|
+
el = el.parentElement;
|
|
1004
|
+
}
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Click
|
|
1009
|
+
document.addEventListener('click', function(e) {
|
|
1010
|
+
var el = findSubst(e.target);
|
|
1011
|
+
if (!el) return;
|
|
1012
|
+
e.preventDefault();
|
|
1013
|
+
e.stopPropagation();
|
|
1014
|
+
handleSubst(el, e.clientX, e.clientY);
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
// Touch
|
|
1018
|
+
document.addEventListener('touchend', function(e) {
|
|
1019
|
+
if (touchMoved) return;
|
|
1020
|
+
var el = findSubst(e.target);
|
|
1021
|
+
if (!el) return;
|
|
1022
|
+
e.preventDefault();
|
|
1023
|
+
e.stopPropagation();
|
|
1024
|
+
var touch = e.changedTouches[0];
|
|
1025
|
+
handleSubst(el, touch.clientX, touch.clientY);
|
|
1026
|
+
}, { passive: false });
|
|
1027
|
+
|
|
1028
|
+
// Hover
|
|
1029
|
+
var hoverTimer = null;
|
|
1030
|
+
substEls.forEach(function(el) {
|
|
1031
|
+
el.addEventListener('mouseenter', function(e) {
|
|
1032
|
+
clearTimeout(hoverTimer);
|
|
1033
|
+
if (popup) return;
|
|
1034
|
+
var x = e.clientX, y = e.clientY;
|
|
1035
|
+
hoverTimer = setTimeout(function() {
|
|
1036
|
+
if (!popup) handleSubst(el, x, y);
|
|
1037
|
+
}, 300);
|
|
1038
|
+
});
|
|
1039
|
+
el.addEventListener('mouseleave', function() {
|
|
1040
|
+
clearTimeout(hoverTimer);
|
|
1041
|
+
if (popup) mouseLeaveTimer = setTimeout(hidePopup, 150);
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
})();
|
|
1045
|
+
|
|
876
1046
|
})();
|
|
877
1047
|
|
|
878
1048
|
// Full-page verse highlighting from URL hash (#vN or #vN-M)
|
|
@@ -881,14 +1051,26 @@
|
|
|
881
1051
|
if (!m) return;
|
|
882
1052
|
var start = parseInt(m[1], 10);
|
|
883
1053
|
var end = m[2] ? parseInt(m[2], 10) : start;
|
|
1054
|
+
var bookMeta = document.querySelector('meta[name="book"]');
|
|
1055
|
+
var chMeta = document.querySelector('meta[name="chapter"]');
|
|
1056
|
+
var book = bookMeta && bookMeta.content;
|
|
1057
|
+
var ch = chMeta && chMeta.content;
|
|
884
1058
|
var first = null;
|
|
885
1059
|
for (var v = start; v <= end; v++) {
|
|
886
|
-
var
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1060
|
+
var spans;
|
|
1061
|
+
if (book && ch) {
|
|
1062
|
+
spans = document.querySelectorAll('.verse[data-verse="' + book + ' ' + ch + ':' + v + '"]');
|
|
1063
|
+
} else {
|
|
1064
|
+
// No meta tags — filter all .verse spans by exact verse number
|
|
1065
|
+
var all = document.querySelectorAll('.verse[data-verse]');
|
|
1066
|
+
spans = [];
|
|
1067
|
+
for (var i = 0; i < all.length; i++) {
|
|
1068
|
+
if (all[i].getAttribute('data-verse').split(':').pop() === String(v)) spans.push(all[i]);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
for (var j = 0; j < spans.length; j++) {
|
|
1072
|
+
spans[j].classList.add('verse-selected');
|
|
1073
|
+
if (!first) first = spans[j];
|
|
892
1074
|
}
|
|
893
1075
|
}
|
|
894
1076
|
if (!first) return;
|