markdownr 0.6.5 → 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 +26 -28
- data/lib/markdown_server/version.rb +1 -1
- data/views/layout.erb +7 -0
- data/views/markdown.erb +5 -9
- data/views/popup_assets.erb +161 -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: 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
|
@@ -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;
|
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
|
@@ -882,6 +882,167 @@
|
|
|
882
882
|
document.head.appendChild(style);
|
|
883
883
|
})();
|
|
884
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
|
+
|
|
885
1046
|
})();
|
|
886
1047
|
|
|
887
1048
|
// Full-page verse highlighting from URL hash (#vN or #vN-M)
|