markdownr 0.8.0 → 0.8.1
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 +64 -0
- data/bin/start-claude +2 -0
- data/lib/markdown_server/app.rb +253 -40
- data/lib/markdown_server/assets/editor-loader.js +362 -0
- data/lib/markdown_server/helpers/admin_helpers.rb +10 -0
- data/lib/markdown_server/helpers/formatting_helpers.rb +3 -1
- data/lib/markdown_server/helpers/markdown_helpers.rb +132 -5
- data/lib/markdown_server/helpers/path_helpers.rb +56 -7
- data/lib/markdown_server/helpers/search_helpers.rb +31 -3
- data/lib/markdown_server/permitted_bases.rb +13 -0
- data/lib/markdown_server/plugins/bible_citations/citations.rb +4 -4
- data/lib/markdown_server/unhide.rb +114 -0
- data/lib/markdown_server/version.rb +1 -1
- data/views/browser.erb +1436 -50
- data/views/layout.erb +122 -5
- data/views/popup_assets.erb +52 -26
- metadata +6 -2
|
@@ -60,13 +60,41 @@ module MarkdownServer
|
|
|
60
60
|
results
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
def walk_directory(dir_path, &block)
|
|
63
|
+
def walk_directory(dir_path, parent_segs = nil, parent_mode = nil, &block)
|
|
64
|
+
rules = Array(settings.unhide_rules)
|
|
65
|
+
bases = permitted_bases
|
|
66
|
+
|
|
67
|
+
if parent_segs.nil?
|
|
68
|
+
base = File.realpath(root_dir)
|
|
69
|
+
real_dir = File.realpath(dir_path) rescue dir_path
|
|
70
|
+
parent_segs = (real_dir == base || !real_dir.start_with?("#{base}/")) ? [] : real_dir.sub("#{base}/", "").split("/")
|
|
71
|
+
parent_mode = :open
|
|
72
|
+
parent_segs.each_with_index do |seg, i|
|
|
73
|
+
ok, parent_mode = MarkdownServer::Unhide.step(parent_mode, parent_segs, i, seg, rules)
|
|
74
|
+
return unless ok
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
64
78
|
Dir.entries(dir_path).sort.each do |entry|
|
|
65
|
-
next if entry
|
|
79
|
+
next if entry == "." || entry == ".."
|
|
80
|
+
|
|
81
|
+
visible, child_mode = MarkdownServer::Unhide.entry_step(parent_mode, parent_segs, entry, rules)
|
|
82
|
+
next unless visible
|
|
83
|
+
|
|
66
84
|
full = File.join(dir_path, entry)
|
|
85
|
+
real = File.realpath(full) rescue next
|
|
86
|
+
next unless MarkdownServer::PermittedBases.base_for(real, bases)
|
|
87
|
+
|
|
88
|
+
# Restricted entries that are symlinks: only descend / index when
|
|
89
|
+
# the symlink's realpath is explicitly in --follow-link. Mirrors
|
|
90
|
+
# entry_admitted? in path_helpers; prevents unhide from
|
|
91
|
+
# double-walking internal-aliased dotfile symlinks.
|
|
92
|
+
if MarkdownServer::Unhide.restricted?(entry) && File.symlink?(full)
|
|
93
|
+
next unless Array(settings.followed_links).include?(real)
|
|
94
|
+
end
|
|
67
95
|
|
|
68
96
|
if File.directory?(full)
|
|
69
|
-
walk_directory(full, &block)
|
|
97
|
+
walk_directory(full, parent_segs + [entry], child_mode, &block)
|
|
70
98
|
elsif File.file?(full)
|
|
71
99
|
ext = File.extname(entry).downcase
|
|
72
100
|
next if BINARY_EXTENSIONS.include?(ext)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module MarkdownServer
|
|
2
|
+
module PermittedBases
|
|
3
|
+
# Returns the permitted base (one of `bases`) that contains `real`,
|
|
4
|
+
# or nil if no base contains it. Each base is the realpath of the
|
|
5
|
+
# served root or a --follow-link target.
|
|
6
|
+
def self.base_for(real, bases)
|
|
7
|
+
Array(bases).each do |b|
|
|
8
|
+
return b if real == b || real.start_with?("#{b}/")
|
|
9
|
+
end
|
|
10
|
+
nil
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -90,18 +90,18 @@ module MarkdownServer
|
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
def self.scrip_url(canonical, verse, version: DEFAULT_VERSION)
|
|
93
|
-
# Parse chapter and optional
|
|
93
|
+
# Parse chapter and optional verse range from the verse string
|
|
94
94
|
# verse is e.g. "1:1", "1:1-5", "1", "1-2", "1:1,3,5"
|
|
95
|
-
if verse =~ /\A(\d+)(?::(\d+))?/
|
|
95
|
+
if verse =~ /\A(\d+)(?::(\d+(?:[–—\-]\d+)?))?/
|
|
96
96
|
chapter = format("%03d", $1.to_i)
|
|
97
|
-
|
|
97
|
+
verse_part = $2&.gsub(/[–—]/, "-")
|
|
98
98
|
else
|
|
99
99
|
return biblegateway_url(canonical, verse, version: version)
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
encode = ->(s) { URI.encode_www_form_component(s).gsub("+", "%20") }
|
|
103
103
|
url = "https://scrip.risensavior.com/browse/#{encode[version]}/#{encode[canonical]}/#{chapter}.html"
|
|
104
|
-
url += "#v#{
|
|
104
|
+
url += "#v#{verse_part}" if verse_part
|
|
105
105
|
url
|
|
106
106
|
end
|
|
107
107
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
module MarkdownServer
|
|
2
|
+
module Unhide
|
|
3
|
+
Rule = Struct.new(:kind, :segments)
|
|
4
|
+
|
|
5
|
+
class CompileError < StandardError; end
|
|
6
|
+
|
|
7
|
+
def self.compile(raw_entries)
|
|
8
|
+
Array(raw_entries).compact.map { |e| compile_one(e) }.uniq
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.compile_one(raw)
|
|
12
|
+
entry = raw.to_s.strip
|
|
13
|
+
raise CompileError, "unhide entry is empty" if entry.empty?
|
|
14
|
+
|
|
15
|
+
if entry.start_with?("@/")
|
|
16
|
+
path = entry.sub(%r{^@/+}, "")
|
|
17
|
+
raise CompileError, "unhide entry '#{raw}' has no path after '@/'" if path.empty?
|
|
18
|
+
segments = normalize_segments(path)
|
|
19
|
+
raise CompileError, "unhide entry '#{raw}' has no usable segments" if segments.empty?
|
|
20
|
+
Rule.new(:anchored, segments)
|
|
21
|
+
else
|
|
22
|
+
if entry.start_with?("/")
|
|
23
|
+
raise CompileError,
|
|
24
|
+
"unhide entry '#{raw}' has a leading '/'. " \
|
|
25
|
+
"Use '@/...' for project-root-anchored, or omit the slash for any-depth match."
|
|
26
|
+
end
|
|
27
|
+
segments = normalize_segments(entry)
|
|
28
|
+
raise CompileError, "unhide entry '#{raw}' has no usable segments" if segments.empty?
|
|
29
|
+
Rule.new(segments.length == 1 ? :basename : :suffix, segments)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.normalize_segments(path)
|
|
34
|
+
path.split("/").reject(&:empty?)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.restricted?(name)
|
|
38
|
+
return false if name.nil? || name.empty?
|
|
39
|
+
name.start_with?(".") || MarkdownServer::EXCLUDED.include?(name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns :leaf if any rule matches segment i as its leaf,
|
|
43
|
+
# :prefix if any rule matches segment i as a navigational prefix,
|
|
44
|
+
# :none otherwise.
|
|
45
|
+
def self.match_at(path_segs, i, rules)
|
|
46
|
+
best = :none
|
|
47
|
+
rules.each do |r|
|
|
48
|
+
case r.kind
|
|
49
|
+
when :basename
|
|
50
|
+
return :leaf if r.segments[0] == path_segs[i]
|
|
51
|
+
when :suffix
|
|
52
|
+
r.segments.length.times do |k|
|
|
53
|
+
next unless i >= k
|
|
54
|
+
next unless path_segs[(i - k)..i] == r.segments[0..k]
|
|
55
|
+
if k + 1 == r.segments.length
|
|
56
|
+
return :leaf
|
|
57
|
+
else
|
|
58
|
+
best = :prefix
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
when :anchored
|
|
62
|
+
if i < r.segments.length && path_segs[0..i] == r.segments[0..i]
|
|
63
|
+
if i + 1 == r.segments.length
|
|
64
|
+
return :leaf
|
|
65
|
+
else
|
|
66
|
+
best = :prefix
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
best
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Cold check: walks segment by segment from root, tracking mode.
|
|
75
|
+
# Returns true iff every segment of path_segments is admitted.
|
|
76
|
+
def self.visible?(path_segments, rules)
|
|
77
|
+
mode = :open
|
|
78
|
+
path_segments.each_with_index do |seg, i|
|
|
79
|
+
visible, mode = step(mode, path_segments, i, seg, rules)
|
|
80
|
+
return false unless visible
|
|
81
|
+
end
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Amortized step for an enumeration. parent_mode and parent_segs are known
|
|
86
|
+
# from the enclosing recursion; this checks one entry. Returns [visible, child_mode].
|
|
87
|
+
def self.entry_step(parent_mode, parent_segs, entry_name, rules)
|
|
88
|
+
path_segs = parent_segs + [entry_name]
|
|
89
|
+
step(parent_mode, path_segs, path_segs.length - 1, entry_name, rules)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.step(mode, path_segs, i, seg, rules)
|
|
93
|
+
restricted = restricted?(seg)
|
|
94
|
+
|
|
95
|
+
if restricted
|
|
96
|
+
case match_at(path_segs, i, rules)
|
|
97
|
+
when :leaf then [true, :open]
|
|
98
|
+
when :prefix then [true, :narrow]
|
|
99
|
+
else [false, nil]
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
if mode == :open
|
|
103
|
+
[true, :open]
|
|
104
|
+
else
|
|
105
|
+
case match_at(path_segs, i, rules)
|
|
106
|
+
when :leaf then [true, :open]
|
|
107
|
+
when :prefix then [true, :narrow]
|
|
108
|
+
else [false, nil]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|