markdownr 0.5.21 → 0.6.0
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 +21 -0
- data/lib/markdown_server/app.rb +24 -15
- data/lib/markdown_server/plugin.rb +86 -0
- data/lib/markdown_server/plugins/bible_citations/citations.rb +140 -0
- data/lib/markdown_server/plugins/bible_citations/plugin.rb +32 -0
- data/lib/markdown_server/version.rb +1 -1
- data/lib/markdown_server.rb +1 -0
- data/views/layout.erb +22 -3
- data/views/popup_assets.erb +22 -3
- metadata +4 -2
- data/lib/markdown_server/bible_citations.rb +0 -174
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d98ba12bdd31972d827eb7386c4c9c6d61c89d2364ee71165a3f9ff2ee52ce8c
|
|
4
|
+
data.tar.gz: 345dd8d2957592b9005dd3bb859109445787bd0668b4c6f7f128b4ad8c372aa8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 358c3263462fa864bfff96cfbc85fbf0e34363e0a9042ccdad9bbd00d173a57f4f6fd5b3d6301b4ccf1ef51bf41ebc56143dae620d8e79e4f7dfff2557623867
|
|
7
|
+
data.tar.gz: 19efd36dfe7838cca8c5e848c9aa10a846e75c4ff30802ebb9002b1915062f6f2d320deb555daca8a26d5a8bc4631a23810e756844d350c20ccc730deb6d5f60
|
data/bin/markdownr
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
require "optparse"
|
|
3
|
+
require "yaml"
|
|
3
4
|
require_relative "../lib/markdown_server"
|
|
4
5
|
|
|
5
6
|
options = { port: 4567, bind: "127.0.0.1", threads: 5 }
|
|
@@ -47,6 +48,10 @@ OptionParser.new do |opts|
|
|
|
47
48
|
options[:verbose] = true
|
|
48
49
|
end
|
|
49
50
|
|
|
51
|
+
opts.on("--no-bible-citations", "Disable Bible verse citation auto-linking") do
|
|
52
|
+
options[:plugin_overrides] = { "bible_citations" => { "enabled" => false } }
|
|
53
|
+
end
|
|
54
|
+
|
|
50
55
|
opts.on("-v", "--version", "Show version") do
|
|
51
56
|
puts "markdownr #{MarkdownServer::VERSION}"
|
|
52
57
|
exit
|
|
@@ -72,6 +77,22 @@ MarkdownServer::App.set :verbose, options[:verbose] || false
|
|
|
72
77
|
MarkdownServer::App.set :port, options[:port]
|
|
73
78
|
MarkdownServer::App.set :bind, options[:bind]
|
|
74
79
|
MarkdownServer::App.set :server_settings, { max_threads: options[:threads], min_threads: 1 }
|
|
80
|
+
MarkdownServer::App.set :plugin_overrides, options[:plugin_overrides] || {}
|
|
81
|
+
|
|
82
|
+
# Load .markdownr.yml popup settings
|
|
83
|
+
config_path = File.join(dir, ".markdownr.yml")
|
|
84
|
+
if File.exist?(config_path)
|
|
85
|
+
yaml = YAML.safe_load(File.read(config_path)) rescue nil
|
|
86
|
+
if yaml && yaml["popups"]
|
|
87
|
+
popups = yaml["popups"]
|
|
88
|
+
MarkdownServer::App.set :popup_local_md, popups.fetch("local_md", true)
|
|
89
|
+
MarkdownServer::App.set :popup_local_html, popups.fetch("local_html", false)
|
|
90
|
+
MarkdownServer::App.set :popup_external, popups.fetch("external", true)
|
|
91
|
+
MarkdownServer::App.set :popup_external_domains, popups.fetch("external_domains", [])
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
MarkdownServer::App.load_plugins!
|
|
75
96
|
|
|
76
97
|
puts "Serving #{dir} on http://#{options[:bind]}:#{options[:port]}/"
|
|
77
98
|
MarkdownServer::App.run!
|
data/lib/markdown_server/app.rb
CHANGED
|
@@ -10,14 +10,12 @@ require "pathname"
|
|
|
10
10
|
require "set"
|
|
11
11
|
require "net/http"
|
|
12
12
|
require "base64"
|
|
13
|
-
require_relative "
|
|
13
|
+
require_relative "plugin"
|
|
14
14
|
|
|
15
15
|
module MarkdownServer
|
|
16
16
|
class App < Sinatra::Base
|
|
17
17
|
EXCLUDED = %w[.git .obsidian __pycache__ .DS_Store node_modules .claude].freeze
|
|
18
18
|
|
|
19
|
-
# Bible citation auto-linking — see lib/markdown_server/bible_citations.rb
|
|
20
|
-
|
|
21
19
|
set :views, File.expand_path("../../views", __dir__)
|
|
22
20
|
|
|
23
21
|
configure do
|
|
@@ -34,6 +32,17 @@ module MarkdownServer
|
|
|
34
32
|
set :verbose, false
|
|
35
33
|
set :session_secret, ENV.fetch("MARKDOWNR_SESSION_SECRET", SecureRandom.hex(64))
|
|
36
34
|
set :sessions, key: "markdownr_session", same_site: :strict, httponly: true
|
|
35
|
+
set :plugin_overrides, {}
|
|
36
|
+
set :plugins, []
|
|
37
|
+
set :popup_local_md, true
|
|
38
|
+
set :popup_local_html, false
|
|
39
|
+
set :popup_external, true
|
|
40
|
+
set :popup_external_domains, []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.load_plugins!
|
|
44
|
+
Dir[File.join(__dir__, "plugins", "*", "plugin.rb")].sort.each { |f| require f }
|
|
45
|
+
set :plugins, PluginRegistry.load_plugins(settings.root_dir, settings.plugin_overrides)
|
|
37
46
|
end
|
|
38
47
|
|
|
39
48
|
helpers do
|
|
@@ -135,11 +144,8 @@ module MarkdownServer
|
|
|
135
144
|
"<div class=\"mermaid\">\n#{h($1.rstrip)}\n</div>\n\n"
|
|
136
145
|
end
|
|
137
146
|
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
text = MarkdownServer.link_citations(text) do |canonical, verse, citation|
|
|
141
|
-
"[#{citation}](#{MarkdownServer.biblegateway_url(canonical, verse)})"
|
|
142
|
-
end
|
|
147
|
+
# Run plugin markdown transformations (e.g. Bible citation auto-linking)
|
|
148
|
+
settings.plugins.each { |p| text = p.transform_markdown(text) }
|
|
143
149
|
|
|
144
150
|
# Process wiki links BEFORE Kramdown so that | isn't consumed as
|
|
145
151
|
# a GFM table delimiter.
|
|
@@ -792,15 +798,18 @@ module MarkdownServer
|
|
|
792
798
|
end
|
|
793
799
|
|
|
794
800
|
# Injects popup CSS and JS into an HTML document before </body>.
|
|
795
|
-
#
|
|
801
|
+
# Runs plugin HTML transformations (e.g. Bible citation auto-linking).
|
|
796
802
|
# Falls back to appending before </html>, then to end of document.
|
|
797
803
|
def inject_markdownr_assets(html_content)
|
|
798
|
-
html_content =
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
+
settings.plugins.each { |p| html_content = p.transform_html(html_content) }
|
|
805
|
+
|
|
806
|
+
popup_config_script = "<script>var __popupConfig = {" \
|
|
807
|
+
"localMd:#{settings.popup_local_md}," \
|
|
808
|
+
"localHtml:#{settings.popup_local_html}," \
|
|
809
|
+
"external:#{settings.popup_external}," \
|
|
810
|
+
"externalDomains:#{settings.popup_external_domains.to_json}" \
|
|
811
|
+
"};</script>\n"
|
|
812
|
+
assets = popup_config_script + File.read(File.join(settings.views, "popup_assets.erb"))
|
|
804
813
|
inserted = false
|
|
805
814
|
result = html_content.sub(/<\/(body|html)>/i) { inserted = true; "#{assets}</#{$1}>" }
|
|
806
815
|
inserted ? result : html_content + assets
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module MarkdownServer
|
|
6
|
+
module Plugin
|
|
7
|
+
def self.included(klass)
|
|
8
|
+
PluginRegistry.register(klass)
|
|
9
|
+
klass.extend(ClassMethods)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module ClassMethods
|
|
13
|
+
def plugin_name
|
|
14
|
+
name.split("::").last.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def enabled_by_default?
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Called once at startup with resolved config hash
|
|
23
|
+
def setup(config) end
|
|
24
|
+
|
|
25
|
+
# Hook: transform raw markdown text (runs before wiki links + Kramdown)
|
|
26
|
+
def transform_markdown(text) text end
|
|
27
|
+
|
|
28
|
+
# Hook: transform raw HTML (runs on .html files before popup injection)
|
|
29
|
+
def transform_html(html) html end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class PluginRegistry
|
|
33
|
+
class << self
|
|
34
|
+
def registered
|
|
35
|
+
@registered ||= []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def register(klass)
|
|
39
|
+
registered << klass unless registered.include?(klass)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def load_plugins(root_dir, cli_overrides = {})
|
|
43
|
+
config = resolve_config(root_dir, cli_overrides)
|
|
44
|
+
registered.filter_map do |klass|
|
|
45
|
+
plugin_config = config.fetch(klass.plugin_name, {})
|
|
46
|
+
enabled = plugin_config.fetch("enabled", klass.enabled_by_default?)
|
|
47
|
+
next unless enabled
|
|
48
|
+
klass.new.tap { |p| p.setup(plugin_config) }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def resolve_config(root_dir, cli_overrides)
|
|
55
|
+
config = {}
|
|
56
|
+
# Layer 1: .markdownr.yml
|
|
57
|
+
path = File.join(root_dir, ".markdownr.yml")
|
|
58
|
+
if File.exist?(path)
|
|
59
|
+
yaml = YAML.safe_load(File.read(path)) rescue nil
|
|
60
|
+
config.merge!(yaml&.dig("plugins") || {})
|
|
61
|
+
end
|
|
62
|
+
# Layer 2: env vars (MARKDOWNR_PLUGIN_BIBLE_CITATIONS_ENABLED=false)
|
|
63
|
+
registered.each do |klass|
|
|
64
|
+
prefix = "MARKDOWNR_PLUGIN_#{klass.plugin_name.upcase}_"
|
|
65
|
+
ENV.each do |k, v|
|
|
66
|
+
next unless k.start_with?(prefix)
|
|
67
|
+
key = k.delete_prefix(prefix).downcase
|
|
68
|
+
config[klass.plugin_name] ||= {}
|
|
69
|
+
config[klass.plugin_name][key] = parse_env_value(v)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
# Layer 3: CLI overrides (highest priority)
|
|
73
|
+
cli_overrides.each { |k, v| config[k] = (config[k] || {}).merge(v) }
|
|
74
|
+
config
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse_env_value(v)
|
|
78
|
+
case v.downcase
|
|
79
|
+
when "true" then true
|
|
80
|
+
when "false" then false
|
|
81
|
+
else v
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
|
|
5
|
+
module MarkdownServer
|
|
6
|
+
module Plugins
|
|
7
|
+
module Citations
|
|
8
|
+
DEFAULT_VERSION = "CEB"
|
|
9
|
+
|
|
10
|
+
# Maps every recognised abbreviation to its BibleGateway canonical book name.
|
|
11
|
+
# Keys are sorted longest-first when building the regex so longer forms win.
|
|
12
|
+
BIBLE_BOOK_MAP = {
|
|
13
|
+
# -- Old Testament --
|
|
14
|
+
"Genesis"=>"Genesis","Gen."=>"Genesis","Gen"=>"Genesis","Gn"=>"Genesis",
|
|
15
|
+
"Exodus"=>"Exodus","Ex."=>"Exodus","Exo"=>"Exodus","Ex"=>"Exodus",
|
|
16
|
+
"Leviticus"=>"Leviticus","Lev."=>"Leviticus","Lev"=>"Leviticus",
|
|
17
|
+
"Numbers"=>"Numbers","Num."=>"Numbers","Num"=>"Numbers","Nu."=>"Numbers","Nu"=>"Numbers",
|
|
18
|
+
"Deuteronomy"=>"Deuteronomy","Deut."=>"Deuteronomy","Deut"=>"Deuteronomy",
|
|
19
|
+
"Dt."=>"Deuteronomy","Dt"=>"Deuteronomy",
|
|
20
|
+
"Joshua"=>"Joshua","Josh."=>"Joshua","Josh"=>"Joshua",
|
|
21
|
+
"Judges"=>"Judges","Judg."=>"Judges","Judg"=>"Judges",
|
|
22
|
+
"Ruth"=>"Ruth",
|
|
23
|
+
"1 Samuel"=>"1 Samuel","1Samuel"=>"1 Samuel","1 Sam."=>"1 Samuel","1 Sam"=>"1 Samuel","1Sam."=>"1 Samuel",
|
|
24
|
+
"2 Samuel"=>"2 Samuel","2Samuel"=>"2 Samuel","2 Sam."=>"2 Samuel","2 Sam"=>"2 Samuel","2Sam."=>"2 Samuel","2Sa."=>"2 Samuel",
|
|
25
|
+
"1 Kings"=>"1 Kings","1Kings"=>"1 Kings","1 Ki."=>"1 Kings","1Ki."=>"1 Kings","1 Ki"=>"1 Kings","1Ki"=>"1 Kings",
|
|
26
|
+
"2 Kings"=>"2 Kings","2Kings"=>"2 Kings","2 Ki."=>"2 Kings","2Ki."=>"2 Kings","2 Ki"=>"2 Kings","2Ki"=>"2 Kings",
|
|
27
|
+
"1 Chronicles"=>"1 Chronicles","1 Chr."=>"1 Chronicles","1Chr."=>"1 Chronicles","1Ch"=>"1 Chronicles",
|
|
28
|
+
"2 Chronicles"=>"2 Chronicles","2 Chron."=>"2 Chronicles","2Chron."=>"2 Chronicles","2 Chr."=>"2 Chronicles","2Chr."=>"2 Chronicles","2 Ch"=>"2 Chronicles",
|
|
29
|
+
"Ezra"=>"Ezra","Nehemiah"=>"Nehemiah","Neh."=>"Nehemiah","Neh"=>"Nehemiah",
|
|
30
|
+
"Esther"=>"Esther","Esth."=>"Esther","Esth"=>"Esther",
|
|
31
|
+
"Job"=>"Job",
|
|
32
|
+
"Psalms"=>"Psalms","Psalm"=>"Psalms","Psa."=>"Psalms","Psa"=>"Psalms","Ps."=>"Psalms","Ps"=>"Psalms",
|
|
33
|
+
"Proverbs"=>"Proverbs","Prov."=>"Proverbs","Prov"=>"Proverbs","Pr"=>"Proverbs",
|
|
34
|
+
"Ecclesiastes"=>"Ecclesiastes","Eccl."=>"Ecclesiastes","Eccl"=>"Ecclesiastes","Ecc."=>"Ecclesiastes",
|
|
35
|
+
"Song of Songs"=>"Song of Songs","Song"=>"Song of Songs",
|
|
36
|
+
"Isaiah"=>"Isaiah","Isa."=>"Isaiah","Isa"=>"Isaiah","Is"=>"Isaiah",
|
|
37
|
+
"Jeremiah"=>"Jeremiah","Jer."=>"Jeremiah","Jer"=>"Jeremiah","Je"=>"Jeremiah",
|
|
38
|
+
"Lamentations"=>"Lamentations","Lam."=>"Lamentations","Lam"=>"Lamentations","La"=>"Lamentations",
|
|
39
|
+
"Ezekiel"=>"Ezekiel","Ezek."=>"Ezekiel","Ezek"=>"Ezekiel",
|
|
40
|
+
"Daniel"=>"Daniel","Dan."=>"Daniel","Dan"=>"Daniel",
|
|
41
|
+
"Hosea"=>"Hosea","Hos."=>"Hosea","Hos"=>"Hosea",
|
|
42
|
+
"Joel"=>"Joel","Amos"=>"Amos",
|
|
43
|
+
"Obadiah"=>"Obadiah","Jonah"=>"Jonah","Jon."=>"Jonah","Jon"=>"Jonah",
|
|
44
|
+
"Micah"=>"Micah","Mic."=>"Micah","Mic"=>"Micah",
|
|
45
|
+
"Nahum"=>"Nahum","Nah."=>"Nahum","Nah"=>"Nahum",
|
|
46
|
+
"Habakkuk"=>"Habakkuk","Hab."=>"Habakkuk","Hab"=>"Habakkuk",
|
|
47
|
+
"Haggai"=>"Haggai",
|
|
48
|
+
"Zechariah"=>"Zechariah","Zech."=>"Zechariah","Zech"=>"Zechariah","Zec"=>"Zechariah","Zc"=>"Zechariah",
|
|
49
|
+
"Zephaniah"=>"Zephaniah","Zeph."=>"Zephaniah","Zeph"=>"Zephaniah","Zep."=>"Zephaniah","Zep"=>"Zephaniah",
|
|
50
|
+
"Malachi"=>"Malachi","Mal."=>"Malachi","Mal"=>"Malachi",
|
|
51
|
+
# -- New Testament --
|
|
52
|
+
"Matthew"=>"Matthew","Matt."=>"Matthew","Matt"=>"Matthew","Mt."=>"Matthew","Mt"=>"Matthew",
|
|
53
|
+
"Mark"=>"Mark","Mk."=>"Mark","Mk"=>"Mark",
|
|
54
|
+
"Luke"=>"Luke","Lk."=>"Luke","Lk"=>"Luke",
|
|
55
|
+
"John"=>"John","Jn."=>"John","Jn"=>"John","Jo"=>"John",
|
|
56
|
+
"Acts"=>"Acts","Ac."=>"Acts",
|
|
57
|
+
"Romans"=>"Romans","Rom."=>"Romans","Rom"=>"Romans","Ro"=>"Romans",
|
|
58
|
+
"1 Corinthians"=>"1 Corinthians","1Corinthians"=>"1 Corinthians","1 Cor."=>"1 Corinthians","1Cor."=>"1 Corinthians","1 Cor"=>"1 Corinthians","1Cor"=>"1 Corinthians","1Co."=>"1 Corinthians","1Co"=>"1 Corinthians",
|
|
59
|
+
"2 Corinthians"=>"2 Corinthians","2Corinthians"=>"2 Corinthians","2 Cor."=>"2 Corinthians","2Cor."=>"2 Corinthians","2 Cor"=>"2 Corinthians","2Co."=>"2 Corinthians","2Co"=>"2 Corinthians",
|
|
60
|
+
"Galatians"=>"Galatians","Gal."=>"Galatians","Gal"=>"Galatians",
|
|
61
|
+
"Ephesians"=>"Ephesians","Eph."=>"Ephesians","Eph"=>"Ephesians",
|
|
62
|
+
"Philippians"=>"Philippians","Phil."=>"Philippians","Phil"=>"Philippians",
|
|
63
|
+
"Colossians"=>"Colossians","Col."=>"Colossians","Col"=>"Colossians",
|
|
64
|
+
"1 Thessalonians"=>"1 Thessalonians","1 Thess."=>"1 Thessalonians","1Thess."=>"1 Thessalonians","1 Th"=>"1 Thessalonians","1Th"=>"1 Thessalonians",
|
|
65
|
+
"2 Thessalonians"=>"2 Thessalonians","2Thessalonians"=>"2 Thessalonians","2 Thess."=>"2 Thessalonians","2Thess."=>"2 Thessalonians","2 Th"=>"2 Thessalonians","2Th"=>"2 Thessalonians",
|
|
66
|
+
"1 Timothy"=>"1 Timothy","1Timothy"=>"1 Timothy","1 Tim."=>"1 Timothy","1Tim."=>"1 Timothy","1 Tim"=>"1 Timothy","1Tim"=>"1 Timothy","1 Ti."=>"1 Timothy","1Ti."=>"1 Timothy",
|
|
67
|
+
"2 Timothy"=>"2 Timothy","2Timothy"=>"2 Timothy","2 Tim."=>"2 Timothy","2Tim."=>"2 Timothy","2 Tim"=>"2 Timothy","2Tim"=>"2 Timothy","2 Ti."=>"2 Timothy","2Ti."=>"2 Timothy",
|
|
68
|
+
"Titus"=>"Titus","Tit."=>"Titus","Tit"=>"Titus","Ti."=>"Titus",
|
|
69
|
+
"Philemon"=>"Philemon","Philem."=>"Philemon",
|
|
70
|
+
"Hebrews"=>"Hebrews","Hebrew"=>"Hebrews","Heb."=>"Hebrews","Heb"=>"Hebrews","Hb"=>"Hebrews",
|
|
71
|
+
"James"=>"James","Jas."=>"James","Jas"=>"James",
|
|
72
|
+
"1 Peter"=>"1 Peter","1Peter"=>"1 Peter","1 Pet."=>"1 Peter","1Pet."=>"1 Peter","1 Pt."=>"1 Peter","1Pt."=>"1 Peter",
|
|
73
|
+
"2 Peter"=>"2 Peter","2Peter"=>"2 Peter","2 Pet."=>"2 Peter","2Pet."=>"2 Peter","2 Pt."=>"2 Peter","2Pt."=>"2 Peter","2Pe"=>"2 Peter",
|
|
74
|
+
"1 John"=>"1 John","1John"=>"1 John","1 Jn."=>"1 John","1Jn."=>"1 John","1 Jn"=>"1 John","1Jn"=>"1 John",
|
|
75
|
+
"2 John"=>"2 John","2 Jn."=>"2 John","2Jn."=>"2 John",
|
|
76
|
+
"3 John"=>"3 John","3 Jn."=>"3 John","3Jn."=>"3 John",
|
|
77
|
+
"Jude"=>"Jude",
|
|
78
|
+
"Revelation"=>"Revelation","Rev."=>"Revelation","Rev"=>"Revelation",
|
|
79
|
+
# -- Deuterocanonical (present in CEB) --
|
|
80
|
+
"Wisdom of Solomon"=>"Wisdom of Solomon","Wisdom"=>"Wisdom of Solomon","Wis."=>"Wisdom of Solomon",
|
|
81
|
+
"1 Maccabees"=>"1 Maccabees","1Macc."=>"1 Maccabees",
|
|
82
|
+
}.freeze
|
|
83
|
+
|
|
84
|
+
# Sorted longest-first for runtime lookup (same order as the regex alternation).
|
|
85
|
+
BIBLE_BOOK_MAP_SORTED = BIBLE_BOOK_MAP.sort_by { |k, _| -k.length }.freeze
|
|
86
|
+
|
|
87
|
+
def self.biblegateway_url(canonical, verse, version: DEFAULT_VERSION)
|
|
88
|
+
"https://www.biblegateway.com/passage/?search=#{CGI.escape("#{canonical} #{verse}")}&version=#{version}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.link_citations(text)
|
|
92
|
+
text.gsub(BIBLE_CITATION_RE) do |m|
|
|
93
|
+
next m if $1 || $2 || $3
|
|
94
|
+
citation = $4
|
|
95
|
+
entry = BIBLE_BOOK_MAP_SORTED.find { |k, _| citation.start_with?(k) }
|
|
96
|
+
next m unless entry
|
|
97
|
+
abbrev, canonical = entry
|
|
98
|
+
verse = citation[abbrev.length..].sub(/\A\.?[ \t]?/, "")
|
|
99
|
+
yield canonical, verse, citation
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
_book_alts = BIBLE_BOOK_MAP.keys.sort_by(&:length).reverse
|
|
104
|
+
.map { |k| Regexp.escape(k) }.join("|")
|
|
105
|
+
_verse = /\d+[abc]?(?:[–—\-]\d+[abc]?)?(?:,\s*\d+[abc]?(?:[–—\-]\d+[abc]?)?)*(?:ff\.?|f\.)?/
|
|
106
|
+
_citation = "(?:#{_book_alts})\\.?[ \\t]?\\d+(?:[–—\\-]\\d+)?(?::#{_verse.source})?"
|
|
107
|
+
|
|
108
|
+
BIBLE_CITATION_RE = Regexp.new(
|
|
109
|
+
"(`[^`]*?`)" \
|
|
110
|
+
"|(`{3}[\\s\\S]*?`{3})" \
|
|
111
|
+
"|(?<!\\!)(\\[[^\\[\\]]*\\]\\([^)]+\\))" \
|
|
112
|
+
"|(#{_citation})",
|
|
113
|
+
Regexp::MULTILINE
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
HTML_BIBLE_CITATION_RE = Regexp.new(
|
|
117
|
+
"(<script\\b[^>]*>[\\s\\S]*?</script>)" \
|
|
118
|
+
"|(<style\\b[^>]*>[\\s\\S]*?</style>)" \
|
|
119
|
+
"|(<title\\b[^>]*>[\\s\\S]*?</title>)" \
|
|
120
|
+
"|(<select\\b[^>]*>[\\s\\S]*?</select>)" \
|
|
121
|
+
"|(<a\\b[^>]*>[\\s\\S]*?</a>)" \
|
|
122
|
+
"|(<[^>]+>)" \
|
|
123
|
+
"|(#{_citation})",
|
|
124
|
+
Regexp::MULTILINE | Regexp::IGNORECASE
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def self.link_citations_html(html)
|
|
128
|
+
html.gsub(HTML_BIBLE_CITATION_RE) do |m|
|
|
129
|
+
next m if $1 || $2 || $3 || $4 || $5 || $6
|
|
130
|
+
citation = $7
|
|
131
|
+
entry = BIBLE_BOOK_MAP_SORTED.find { |k, _| citation.start_with?(k) }
|
|
132
|
+
next m unless entry
|
|
133
|
+
abbrev, canonical = entry
|
|
134
|
+
verse = citation[abbrev.length..].sub(/\A\.?[ \t]?/, "")
|
|
135
|
+
yield canonical, verse, citation
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
require_relative "citations"
|
|
5
|
+
require_relative "../../plugin"
|
|
6
|
+
|
|
7
|
+
module MarkdownServer
|
|
8
|
+
module Plugins
|
|
9
|
+
class BibleCitations
|
|
10
|
+
include MarkdownServer::Plugin
|
|
11
|
+
|
|
12
|
+
def self.enabled_by_default? = true
|
|
13
|
+
|
|
14
|
+
def setup(config)
|
|
15
|
+
@version = config.fetch("version", Citations::DEFAULT_VERSION)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def transform_markdown(text)
|
|
19
|
+
Citations.link_citations(text) do |canonical, verse, citation|
|
|
20
|
+
"[#{citation}](#{Citations.biblegateway_url(canonical, verse, version: @version)})"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def transform_html(html)
|
|
25
|
+
Citations.link_citations_html(html) do |canonical, verse, citation|
|
|
26
|
+
url = Citations.biblegateway_url(canonical, verse, version: @version)
|
|
27
|
+
%(<a href="#{CGI.escapeHTML(url)}">#{CGI.escapeHTML(citation)}</a>)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/markdown_server.rb
CHANGED
data/views/layout.erb
CHANGED
|
@@ -1647,7 +1647,26 @@
|
|
|
1647
1647
|
if (/^https?:\/\//i.test(href)) return false;
|
|
1648
1648
|
return /\.md([?#]|$)/i.test(href);
|
|
1649
1649
|
}
|
|
1650
|
+
function isLocalHtml(href) {
|
|
1651
|
+
if (/^https?:\/\//i.test(href)) return false;
|
|
1652
|
+
return /\.html?([?#]|$)/i.test(href);
|
|
1653
|
+
}
|
|
1650
1654
|
function isExternal(href) { return /^https?:\/\//i.test(href); }
|
|
1655
|
+
var __popupConfig = <%= { localMd: settings.popup_local_md, localHtml: settings.popup_local_html, external: settings.popup_external, externalDomains: settings.popup_external_domains }.to_json %>;
|
|
1656
|
+
function shouldPopup(href) {
|
|
1657
|
+
if (isAnchorOnly(href)) return false;
|
|
1658
|
+
if (isLocalMd(href)) return __popupConfig.localMd;
|
|
1659
|
+
if (isLocalHtml(href)) return __popupConfig.localHtml;
|
|
1660
|
+
if (isExternal(href)) {
|
|
1661
|
+
if (!__popupConfig.external) return false;
|
|
1662
|
+
if (__popupConfig.externalDomains.length === 0) return true;
|
|
1663
|
+
try {
|
|
1664
|
+
var host = new URL(href).hostname;
|
|
1665
|
+
return __popupConfig.externalDomains.some(function(d) { return host === d || host.endsWith('.' + d); });
|
|
1666
|
+
} catch(e) { return false; }
|
|
1667
|
+
}
|
|
1668
|
+
return false;
|
|
1669
|
+
}
|
|
1651
1670
|
|
|
1652
1671
|
function previewPath(href) {
|
|
1653
1672
|
var resolved = new URL(href, location.href);
|
|
@@ -2104,7 +2123,7 @@
|
|
|
2104
2123
|
var href = anchor.getAttribute('href');
|
|
2105
2124
|
if (!href || isAnchorOnly(href)) { hidePopup(); return; }
|
|
2106
2125
|
if (!anchor.closest('.md-content') && !anchor.closest('.frontmatter')) { hidePopup(); return; }
|
|
2107
|
-
if (!
|
|
2126
|
+
if (!shouldPopup(href)) { hidePopup(); return; }
|
|
2108
2127
|
e.preventDefault();
|
|
2109
2128
|
handleLink(anchor, e.clientX, e.clientY);
|
|
2110
2129
|
});
|
|
@@ -2127,7 +2146,7 @@
|
|
|
2127
2146
|
var href = anchor.getAttribute('href');
|
|
2128
2147
|
if (!href || isAnchorOnly(href)) { hidePopup(); return; }
|
|
2129
2148
|
if (!anchor.closest('.md-content') && !anchor.closest('.frontmatter')) { hidePopup(); return; }
|
|
2130
|
-
if (!
|
|
2149
|
+
if (!shouldPopup(href)) { hidePopup(); return; }
|
|
2131
2150
|
e.preventDefault();
|
|
2132
2151
|
var touch = e.changedTouches[0];
|
|
2133
2152
|
handleLink(anchor, touch.clientX, touch.clientY);
|
|
@@ -2147,7 +2166,7 @@
|
|
|
2147
2166
|
container.querySelectorAll('a').forEach(function(a) {
|
|
2148
2167
|
var href = a.getAttribute('href');
|
|
2149
2168
|
if (!href || isAnchorOnly(href)) return;
|
|
2150
|
-
if (!
|
|
2169
|
+
if (!shouldPopup(href)) return;
|
|
2151
2170
|
a.addEventListener('mouseenter', function(e) {
|
|
2152
2171
|
clearTimeout(hoverTimer);
|
|
2153
2172
|
if (popup) return;
|
data/views/popup_assets.erb
CHANGED
|
@@ -234,7 +234,26 @@
|
|
|
234
234
|
if (/^https?:\/\//i.test(href)) return false;
|
|
235
235
|
return /\.md([?#]|$)/i.test(href);
|
|
236
236
|
}
|
|
237
|
+
function isLocalHtml(href) {
|
|
238
|
+
if (/^https?:\/\//i.test(href)) return false;
|
|
239
|
+
return /\.html?([?#]|$)/i.test(href);
|
|
240
|
+
}
|
|
237
241
|
function isExternal(href) { return /^https?:\/\//i.test(href); }
|
|
242
|
+
function shouldPopup(href) {
|
|
243
|
+
var cfg = window.__popupConfig || { localMd: true, localHtml: false, external: true, externalDomains: [] };
|
|
244
|
+
if (isAnchorOnly(href)) return false;
|
|
245
|
+
if (isLocalMd(href)) return cfg.localMd;
|
|
246
|
+
if (isLocalHtml(href)) return cfg.localHtml;
|
|
247
|
+
if (isExternal(href)) {
|
|
248
|
+
if (!cfg.external) return false;
|
|
249
|
+
if (cfg.externalDomains.length === 0) return true;
|
|
250
|
+
try {
|
|
251
|
+
var host = new URL(href).hostname;
|
|
252
|
+
return cfg.externalDomains.some(function(d) { return host === d || host.endsWith('.' + d); });
|
|
253
|
+
} catch(e) { return false; }
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
238
257
|
|
|
239
258
|
function previewPath(href) {
|
|
240
259
|
var resolved = new URL(href, location.href);
|
|
@@ -671,7 +690,7 @@
|
|
|
671
690
|
if (!anchor) { hidePopup(); return; }
|
|
672
691
|
var href = anchor.getAttribute('href');
|
|
673
692
|
if (!href || isAnchorOnly(href)) { hidePopup(); return; }
|
|
674
|
-
if (!
|
|
693
|
+
if (!shouldPopup(href)) { hidePopup(); return; }
|
|
675
694
|
e.preventDefault();
|
|
676
695
|
handleLink(anchor, e.clientX, e.clientY, false);
|
|
677
696
|
});
|
|
@@ -694,7 +713,7 @@
|
|
|
694
713
|
if (!anchor) { hidePopup(); return; }
|
|
695
714
|
var href = anchor.getAttribute('href');
|
|
696
715
|
if (!href || isAnchorOnly(href)) { hidePopup(); return; }
|
|
697
|
-
if (!
|
|
716
|
+
if (!shouldPopup(href)) { hidePopup(); return; }
|
|
698
717
|
e.preventDefault();
|
|
699
718
|
var touch = e.changedTouches[0];
|
|
700
719
|
handleLink(anchor, touch.clientX, touch.clientY, false);
|
|
@@ -710,7 +729,7 @@
|
|
|
710
729
|
document.querySelectorAll('a').forEach(function(a) {
|
|
711
730
|
var href = a.getAttribute('href');
|
|
712
731
|
if (!href || isAnchorOnly(href)) return;
|
|
713
|
-
if (!
|
|
732
|
+
if (!shouldPopup(href)) return;
|
|
714
733
|
|
|
715
734
|
a.addEventListener('mouseenter', function(e) {
|
|
716
735
|
clearTimeout(hoverTimer);
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: markdownr
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Dunn
|
|
@@ -105,7 +105,9 @@ files:
|
|
|
105
105
|
- bin/markdownr
|
|
106
106
|
- lib/markdown_server.rb
|
|
107
107
|
- lib/markdown_server/app.rb
|
|
108
|
-
- lib/markdown_server/
|
|
108
|
+
- lib/markdown_server/plugin.rb
|
|
109
|
+
- lib/markdown_server/plugins/bible_citations/citations.rb
|
|
110
|
+
- lib/markdown_server/plugins/bible_citations/plugin.rb
|
|
109
111
|
- lib/markdown_server/version.rb
|
|
110
112
|
- views/admin_login.erb
|
|
111
113
|
- views/directory.erb
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "cgi"
|
|
3
|
-
#
|
|
4
|
-
# Bible citation auto-linking for Markdown rendering.
|
|
5
|
-
#
|
|
6
|
-
# Provides two constants used in render_markdown:
|
|
7
|
-
#
|
|
8
|
-
# BIBLE_BOOK_MAP – hash of abbreviation → BibleGateway canonical name
|
|
9
|
-
# BIBLE_CITATION_RE – combined regex that matches inline code, fenced code
|
|
10
|
-
# blocks, existing markdown links (to skip), and bare
|
|
11
|
-
# citations (to replace with a BibleGateway link).
|
|
12
|
-
#
|
|
13
|
-
# Usage in render_markdown:
|
|
14
|
-
#
|
|
15
|
-
# text = text.gsub(BIBLE_CITATION_RE) do |m|
|
|
16
|
-
# next m if $1 || $2 || $3 # code span / code fence / existing link
|
|
17
|
-
# citation = $4
|
|
18
|
-
# canonical = BIBLE_BOOK_MAP.find { |k, _| citation.start_with?(k) }&.last
|
|
19
|
-
# next m unless canonical
|
|
20
|
-
# rest = citation[canonical.length..].sub(/\A\.?[ \t]?/, "")
|
|
21
|
-
# url = "https://www.biblegateway.com/passage/?search=#{CGI.escape("#{canonical} #{rest}")}&version=CEB"
|
|
22
|
-
# "[#{citation}](#{url})"
|
|
23
|
-
# end
|
|
24
|
-
#
|
|
25
|
-
# To copy to another project: copy this file and add the gsub block above to
|
|
26
|
-
# your markdown rendering method.
|
|
27
|
-
|
|
28
|
-
module MarkdownServer
|
|
29
|
-
# Maps every recognised abbreviation to its BibleGateway canonical book name.
|
|
30
|
-
# Keys are sorted longest-first when building the regex so longer forms win.
|
|
31
|
-
BIBLE_BOOK_MAP = {
|
|
32
|
-
# ── Old Testament ────────────────────────────────────────────────────────
|
|
33
|
-
"Genesis"=>"Genesis","Gen."=>"Genesis","Gen"=>"Genesis","Gn"=>"Genesis",
|
|
34
|
-
"Exodus"=>"Exodus","Ex."=>"Exodus","Exo"=>"Exodus","Ex"=>"Exodus",
|
|
35
|
-
"Leviticus"=>"Leviticus","Lev."=>"Leviticus","Lev"=>"Leviticus",
|
|
36
|
-
"Numbers"=>"Numbers","Num."=>"Numbers","Num"=>"Numbers","Nu."=>"Numbers","Nu"=>"Numbers",
|
|
37
|
-
"Deuteronomy"=>"Deuteronomy","Deut."=>"Deuteronomy","Deut"=>"Deuteronomy",
|
|
38
|
-
"Dt."=>"Deuteronomy","Dt"=>"Deuteronomy",
|
|
39
|
-
"Joshua"=>"Joshua","Josh."=>"Joshua","Josh"=>"Joshua",
|
|
40
|
-
"Judges"=>"Judges","Judg."=>"Judges","Judg"=>"Judges",
|
|
41
|
-
"Ruth"=>"Ruth",
|
|
42
|
-
"1 Samuel"=>"1 Samuel","1Samuel"=>"1 Samuel","1 Sam."=>"1 Samuel","1 Sam"=>"1 Samuel","1Sam."=>"1 Samuel",
|
|
43
|
-
"2 Samuel"=>"2 Samuel","2Samuel"=>"2 Samuel","2 Sam."=>"2 Samuel","2 Sam"=>"2 Samuel","2Sam."=>"2 Samuel","2Sa."=>"2 Samuel",
|
|
44
|
-
"1 Kings"=>"1 Kings","1Kings"=>"1 Kings","1 Ki."=>"1 Kings","1Ki."=>"1 Kings","1 Ki"=>"1 Kings","1Ki"=>"1 Kings",
|
|
45
|
-
"2 Kings"=>"2 Kings","2Kings"=>"2 Kings","2 Ki."=>"2 Kings","2Ki."=>"2 Kings","2 Ki"=>"2 Kings","2Ki"=>"2 Kings",
|
|
46
|
-
"1 Chronicles"=>"1 Chronicles","1 Chr."=>"1 Chronicles","1Chr."=>"1 Chronicles","1Ch"=>"1 Chronicles",
|
|
47
|
-
"2 Chronicles"=>"2 Chronicles","2 Chron."=>"2 Chronicles","2Chron."=>"2 Chronicles","2 Chr."=>"2 Chronicles","2Chr."=>"2 Chronicles","2 Ch"=>"2 Chronicles",
|
|
48
|
-
"Ezra"=>"Ezra","Nehemiah"=>"Nehemiah","Neh."=>"Nehemiah","Neh"=>"Nehemiah",
|
|
49
|
-
"Esther"=>"Esther","Esth."=>"Esther","Esth"=>"Esther",
|
|
50
|
-
"Job"=>"Job",
|
|
51
|
-
"Psalms"=>"Psalms","Psalm"=>"Psalms","Psa."=>"Psalms","Psa"=>"Psalms","Ps."=>"Psalms","Ps"=>"Psalms",
|
|
52
|
-
"Proverbs"=>"Proverbs","Prov."=>"Proverbs","Prov"=>"Proverbs","Pr"=>"Proverbs",
|
|
53
|
-
"Ecclesiastes"=>"Ecclesiastes","Eccl."=>"Ecclesiastes","Eccl"=>"Ecclesiastes","Ecc."=>"Ecclesiastes",
|
|
54
|
-
"Song of Songs"=>"Song of Songs","Song"=>"Song of Songs",
|
|
55
|
-
"Isaiah"=>"Isaiah","Isa."=>"Isaiah","Isa"=>"Isaiah","Is"=>"Isaiah",
|
|
56
|
-
"Jeremiah"=>"Jeremiah","Jer."=>"Jeremiah","Jer"=>"Jeremiah","Je"=>"Jeremiah",
|
|
57
|
-
"Lamentations"=>"Lamentations","Lam."=>"Lamentations","Lam"=>"Lamentations","La"=>"Lamentations",
|
|
58
|
-
"Ezekiel"=>"Ezekiel","Ezek."=>"Ezekiel","Ezek"=>"Ezekiel",
|
|
59
|
-
"Daniel"=>"Daniel","Dan."=>"Daniel","Dan"=>"Daniel",
|
|
60
|
-
"Hosea"=>"Hosea","Hos."=>"Hosea","Hos"=>"Hosea",
|
|
61
|
-
"Joel"=>"Joel","Amos"=>"Amos",
|
|
62
|
-
"Obadiah"=>"Obadiah","Jonah"=>"Jonah","Jon."=>"Jonah","Jon"=>"Jonah",
|
|
63
|
-
"Micah"=>"Micah","Mic."=>"Micah","Mic"=>"Micah",
|
|
64
|
-
"Nahum"=>"Nahum","Nah."=>"Nahum","Nah"=>"Nahum",
|
|
65
|
-
"Habakkuk"=>"Habakkuk","Hab."=>"Habakkuk","Hab"=>"Habakkuk",
|
|
66
|
-
"Haggai"=>"Haggai",
|
|
67
|
-
"Zechariah"=>"Zechariah","Zech."=>"Zechariah","Zech"=>"Zechariah","Zec"=>"Zechariah","Zc"=>"Zechariah",
|
|
68
|
-
"Zephaniah"=>"Zephaniah","Zeph."=>"Zephaniah","Zeph"=>"Zephaniah","Zep."=>"Zephaniah","Zep"=>"Zephaniah",
|
|
69
|
-
"Malachi"=>"Malachi","Mal."=>"Malachi","Mal"=>"Malachi",
|
|
70
|
-
# ── New Testament ────────────────────────────────────────────────────────
|
|
71
|
-
"Matthew"=>"Matthew","Matt."=>"Matthew","Matt"=>"Matthew","Mt."=>"Matthew","Mt"=>"Matthew",
|
|
72
|
-
"Mark"=>"Mark","Mk."=>"Mark","Mk"=>"Mark",
|
|
73
|
-
"Luke"=>"Luke","Lk."=>"Luke","Lk"=>"Luke",
|
|
74
|
-
"John"=>"John","Jn."=>"John","Jn"=>"John","Jo"=>"John",
|
|
75
|
-
"Acts"=>"Acts","Ac."=>"Acts",
|
|
76
|
-
"Romans"=>"Romans","Rom."=>"Romans","Rom"=>"Romans","Ro"=>"Romans",
|
|
77
|
-
"1 Corinthians"=>"1 Corinthians","1Corinthians"=>"1 Corinthians","1 Cor."=>"1 Corinthians","1Cor."=>"1 Corinthians","1 Cor"=>"1 Corinthians","1Cor"=>"1 Corinthians","1Co."=>"1 Corinthians","1Co"=>"1 Corinthians",
|
|
78
|
-
"2 Corinthians"=>"2 Corinthians","2Corinthians"=>"2 Corinthians","2 Cor."=>"2 Corinthians","2Cor."=>"2 Corinthians","2 Cor"=>"2 Corinthians","2Co."=>"2 Corinthians","2Co"=>"2 Corinthians",
|
|
79
|
-
"Galatians"=>"Galatians","Gal."=>"Galatians","Gal"=>"Galatians",
|
|
80
|
-
"Ephesians"=>"Ephesians","Eph."=>"Ephesians","Eph"=>"Ephesians",
|
|
81
|
-
"Philippians"=>"Philippians","Phil."=>"Philippians","Phil"=>"Philippians",
|
|
82
|
-
"Colossians"=>"Colossians","Col."=>"Colossians","Col"=>"Colossians",
|
|
83
|
-
"1 Thessalonians"=>"1 Thessalonians","1 Thess."=>"1 Thessalonians","1Thess."=>"1 Thessalonians","1 Th"=>"1 Thessalonians","1Th"=>"1 Thessalonians",
|
|
84
|
-
"2 Thessalonians"=>"2 Thessalonians","2Thessalonians"=>"2 Thessalonians","2 Thess."=>"2 Thessalonians","2Thess."=>"2 Thessalonians","2 Th"=>"2 Thessalonians","2Th"=>"2 Thessalonians",
|
|
85
|
-
"1 Timothy"=>"1 Timothy","1Timothy"=>"1 Timothy","1 Tim."=>"1 Timothy","1Tim."=>"1 Timothy","1 Tim"=>"1 Timothy","1Tim"=>"1 Timothy","1 Ti."=>"1 Timothy","1Ti."=>"1 Timothy",
|
|
86
|
-
"2 Timothy"=>"2 Timothy","2Timothy"=>"2 Timothy","2 Tim."=>"2 Timothy","2Tim."=>"2 Timothy","2 Tim"=>"2 Timothy","2Tim"=>"2 Timothy","2 Ti."=>"2 Timothy","2Ti."=>"2 Timothy",
|
|
87
|
-
"Titus"=>"Titus","Tit."=>"Titus","Tit"=>"Titus","Ti."=>"Titus",
|
|
88
|
-
"Philemon"=>"Philemon","Philem."=>"Philemon",
|
|
89
|
-
"Hebrews"=>"Hebrews","Hebrew"=>"Hebrews","Heb."=>"Hebrews","Heb"=>"Hebrews","Hb"=>"Hebrews",
|
|
90
|
-
"James"=>"James","Jas."=>"James","Jas"=>"James",
|
|
91
|
-
"1 Peter"=>"1 Peter","1Peter"=>"1 Peter","1 Pet."=>"1 Peter","1Pet."=>"1 Peter","1 Pt."=>"1 Peter","1Pt."=>"1 Peter",
|
|
92
|
-
"2 Peter"=>"2 Peter","2Peter"=>"2 Peter","2 Pet."=>"2 Peter","2Pet."=>"2 Peter","2 Pt."=>"2 Peter","2Pt."=>"2 Peter","2Pe"=>"2 Peter",
|
|
93
|
-
"1 John"=>"1 John","1John"=>"1 John","1 Jn."=>"1 John","1Jn."=>"1 John","1 Jn"=>"1 John","1Jn"=>"1 John",
|
|
94
|
-
"2 John"=>"2 John","2 Jn."=>"2 John","2Jn."=>"2 John",
|
|
95
|
-
"3 John"=>"3 John","3 Jn."=>"3 John","3Jn."=>"3 John",
|
|
96
|
-
"Jude"=>"Jude",
|
|
97
|
-
"Revelation"=>"Revelation","Rev."=>"Revelation","Rev"=>"Revelation",
|
|
98
|
-
# ── Deuterocanonical (present in CEB) ───────────────────────────────────
|
|
99
|
-
"Wisdom of Solomon"=>"Wisdom of Solomon","Wisdom"=>"Wisdom of Solomon","Wis."=>"Wisdom of Solomon",
|
|
100
|
-
"1 Maccabees"=>"1 Maccabees","1Macc."=>"1 Maccabees",
|
|
101
|
-
}.freeze
|
|
102
|
-
|
|
103
|
-
# Sorted longest-first for runtime lookup (same order as the regex alternation).
|
|
104
|
-
BIBLE_BOOK_MAP_SORTED = BIBLE_BOOK_MAP.sort_by { |k, _| -k.length }.freeze
|
|
105
|
-
|
|
106
|
-
BIBLEGATEWAY_VERSION = "CEB".freeze
|
|
107
|
-
|
|
108
|
-
def self.biblegateway_url(canonical, verse)
|
|
109
|
-
"https://www.biblegateway.com/passage/?search=#{CGI.escape("#{canonical} #{verse}")}&version=#{BIBLEGATEWAY_VERSION}"
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Scans +text+ for bare Bible verse citations and yields each one as
|
|
113
|
-
# (canonical_book, verse, raw_citation). Returns the transformed string
|
|
114
|
-
# with each citation replaced by whatever the block returns. Code spans,
|
|
115
|
-
# fenced code blocks, and citations already inside a markdown link are
|
|
116
|
-
# passed through unchanged.
|
|
117
|
-
#
|
|
118
|
-
# MarkdownServer.link_citations("See Rom 12:1.") { |c, v, _| "[#{c} #{v}]" }
|
|
119
|
-
# # => "See [Romans 12:1]."
|
|
120
|
-
def self.link_citations(text)
|
|
121
|
-
text.gsub(BIBLE_CITATION_RE) do |m|
|
|
122
|
-
next m if $1 || $2 || $3 # code span / code fence / existing link
|
|
123
|
-
citation = $4
|
|
124
|
-
entry = BIBLE_BOOK_MAP_SORTED.find { |k, _| citation.start_with?(k) }
|
|
125
|
-
next m unless entry
|
|
126
|
-
abbrev, canonical = entry
|
|
127
|
-
verse = citation[abbrev.length..].sub(/\A\.?[ \t]?/, "")
|
|
128
|
-
yield canonical, verse, citation
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
_book_alts = BIBLE_BOOK_MAP.keys.sort_by(&:length).reverse
|
|
133
|
-
.map { |k| Regexp.escape(k) }.join("|")
|
|
134
|
-
_verse = /\d+[abc]?(?:[–—\-]\d+[abc]?)?(?:,\s*\d+[abc]?(?:[–—\-]\d+[abc]?)?)*(?:ff\.?|f\.)?/
|
|
135
|
-
_citation = "(?:#{_book_alts})\\.?[ \\t]?\\d+(?::#{_verse.source})?"
|
|
136
|
-
|
|
137
|
-
# Combined regex. Four alternatives — only the last (group 4) is replaced:
|
|
138
|
-
# 1. inline code span → return verbatim
|
|
139
|
-
# 2. fenced code block → return verbatim
|
|
140
|
-
# 3. existing md link → return verbatim
|
|
141
|
-
# 4. bare citation → replace with BibleGateway markdown link
|
|
142
|
-
BIBLE_CITATION_RE = Regexp.new(
|
|
143
|
-
"(`[^`]*?`)" \
|
|
144
|
-
"|(`{3}[\\s\\S]*?`{3})" \
|
|
145
|
-
"|(?<!\\!)(\\[[^\\[\\]]*\\]\\([^)]+\\))" \
|
|
146
|
-
"|(#{_citation})",
|
|
147
|
-
Regexp::MULTILINE
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
# HTML-aware version of BIBLE_CITATION_RE. Skips <script>, <style>, existing
|
|
151
|
-
# <a> elements, and all other HTML tags. Group 5 captures bare citations.
|
|
152
|
-
HTML_BIBLE_CITATION_RE = Regexp.new(
|
|
153
|
-
"(<script\\b[^>]*>[\\s\\S]*?</script>)" \
|
|
154
|
-
"|(<style\\b[^>]*>[\\s\\S]*?</style>)" \
|
|
155
|
-
"|(<a\\b[^>]*>[\\s\\S]*?</a>)" \
|
|
156
|
-
"|(<[^>]+>)" \
|
|
157
|
-
"|(#{_citation})",
|
|
158
|
-
Regexp::MULTILINE | Regexp::IGNORECASE
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
# Like link_citations but operates on raw HTML. Skips tag content, script/style
|
|
162
|
-
# blocks, and existing anchor elements; yields bare citations in text nodes.
|
|
163
|
-
def self.link_citations_html(html)
|
|
164
|
-
html.gsub(HTML_BIBLE_CITATION_RE) do |m|
|
|
165
|
-
next m if $1 || $2 || $3 || $4 # skip tags / scripts / styles / anchors
|
|
166
|
-
citation = $5
|
|
167
|
-
entry = BIBLE_BOOK_MAP_SORTED.find { |k, _| citation.start_with?(k) }
|
|
168
|
-
next m unless entry
|
|
169
|
-
abbrev, canonical = entry
|
|
170
|
-
verse = citation[abbrev.length..].sub(/\A\.?[ \t]?/, "")
|
|
171
|
-
yield canonical, verse, citation
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
end
|