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.
@@ -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.start_with?(".") || EXCLUDED.include?(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 starting verse from the verse string
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
- start_verse = $2
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#{start_verse}" if start_verse
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
@@ -1,3 +1,3 @@
1
1
  module MarkdownServer
2
- VERSION = "0.8.0"
2
+ VERSION = "0.8.1"
3
3
  end