Class: Docbook::XIncludeResolver
- Inherits:
-
Object
- Object
- Docbook::XIncludeResolver
- Defined in:
- lib/docbook/xinclude_resolver.rb
Constant Summary collapse
- XINCLUDE_NS =
"http://www.w3.org/2001/XInclude"
Class Method Summary collapse
-
.apply_char_fragid(content, char_spec) ⇒ Object
Apply char= fragid scheme (RFC 5147) Syntax: char=START,END.
-
.apply_delimited_search(lines, spec, delim) ⇒ Object
Parse delimited patterns and apply search Delimiter is # (literal) or / (regex).
-
.apply_fragid(content, fragid) ⇒ Object
Apply a fragid filter to text content.
-
.apply_line_fragid(content, line_spec) ⇒ Object
Apply line= fragid scheme (RFC 5147) Syntax: line=START,END[;length=N].
-
.apply_search_fragid(content, search_spec) ⇒ Object
Apply search= fragid scheme (DocBook xslTNG extension) Syntax: search=#PATTERN#[;after|;before],#STOP# or: search=/REGEX/[,/STOP/].
- .base_dir_for(document) ⇒ Object
-
.resolve(doc) ⇒ Object
Resolve XInclude elements in a Nokogiri document.
- .resolve_path(base_dir, href) ⇒ Object
-
.resolve_string(xml_string, base_path: nil) ⇒ Object
Pre-process an XML string to resolve XIncludes before model parsing.
-
.resolve_text_include(doc, inc, file_path, fragid) ⇒ Object
── Include Resolution ──────────────────────────────────────────.
- .resolve_xml_include(doc, inc, file_path) ⇒ Object
Class Method Details
.apply_char_fragid(content, char_spec) ⇒ Object
Apply char= fragid scheme (RFC 5147) Syntax: char=START,END
215 216 217 218 219 220 |
# File 'lib/docbook/xinclude_resolver.rb', line 215 def self.apply_char_fragid(content, char_spec) char_start, char_end = char_spec.split(",").map(&:to_i) return nil unless char_start && char_end content[char_start...char_end] end |
.apply_delimited_search(lines, spec, delim) ⇒ Object
Parse delimited patterns and apply search Delimiter is # (literal) or / (regex)
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/docbook/xinclude_resolver.rb', line 124 def self.apply_delimited_search(lines, spec, delim) # Parse: DELIM pattern DELIM [;modifier] [, DELIM stop DELIM] remainder = spec[1..] # skip opening delimiter close_idx = remainder.index(delim) return nil unless close_idx pattern_str = remainder[0...close_idx] remainder = remainder[(close_idx + 1)..] # Check for modifier (;after or ;before) modifier = nil if remainder&.start_with?(";") mod_match = remainder.match(/\A;(after|before)/) if mod_match modifier = mod_match[1].to_sym remainder = remainder[mod_match[0].length..] end end # Skip comma separator remainder = remainder[1..] if remainder&.start_with?(",") # Parse stop pattern if present stop_pattern = nil if remainder && !remainder.empty? && remainder.start_with?(delim) stop_remainder = remainder[1..] stop_close = stop_remainder.index(delim) stop_pattern = stop_remainder[0...stop_close] if stop_close end # Apply search match_fn = if delim == "/" ->(line, pat) { line.match?(Regexp.new(pat)) } else ->(line, pat) { line.include?(pat) } end start_idx = lines.index { |l| match_fn.call(l, pattern_str) } return nil unless start_idx # Determine range case modifier when :after range_start = start_idx + 1 when :before range_start = 0 range_end = start_idx else range_start = start_idx end # Find stop (exclusive) if stop_pattern stop_slice = lines[range_start..] stop_idx = stop_slice&.index { |l| match_fn.call(l, stop_pattern) } range_end = stop_idx ? range_start + stop_idx : lines.length elsif modifier != :before range_end = start_idx + 1 end range_end ||= lines.length selected = lines[range_start...range_end] return nil unless selected && !selected.empty? selected.join end |
.apply_fragid(content, fragid) ⇒ Object
Apply a fragid filter to text content
100 101 102 103 104 105 106 107 108 |
# File 'lib/docbook/xinclude_resolver.rb', line 100 def self.apply_fragid(content, fragid) if fragid.start_with?("search=") apply_search_fragid(content, fragid[7..]) elsif fragid.start_with?("line=") apply_line_fragid(content, fragid[5..]) elsif fragid.start_with?("char=") apply_char_fragid(content, fragid[5..]) end end |
.apply_line_fragid(content, line_spec) ⇒ Object
Apply line= fragid scheme (RFC 5147) Syntax: line=START,END[;length=N]
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/docbook/xinclude_resolver.rb', line 193 def self.apply_line_fragid(content, line_spec) parts = line_spec.split(";") range_part = parts[0] length_part = parts.find { |p| p.start_with?("length=") } line_start, line_end = range_part.split(",").map(&:to_i) return nil unless line_start && line_end lines = content.lines selected = lines[(line_start - 1)...(line_end)] return nil unless selected result = selected.join if length_part max_len = length_part.split("=")[1].to_i result = result[0...max_len] if max_len.positive? end result end |
.apply_search_fragid(content, search_spec) ⇒ Object
Apply search= fragid scheme (DocBook xslTNG extension) Syntax: search=#PATTERN#[;after|;before],#STOP# or: search=/REGEX/[,/STOP/]
113 114 115 116 117 118 119 120 |
# File 'lib/docbook/xinclude_resolver.rb', line 113 def self.apply_search_fragid(content, search_spec) lines = content.lines if search_spec.start_with?("/") apply_delimited_search(lines, search_spec, "/") elsif search_spec.start_with?("#") apply_delimited_search(lines, search_spec, "#") end end |
.base_dir_for(document) ⇒ Object
84 85 86 87 88 89 |
# File 'lib/docbook/xinclude_resolver.rb', line 84 def self.base_dir_for(document) url = document.url return nil unless url File.dirname(url.sub(%r{^file://}, "")) end |
.resolve(doc) ⇒ Object
Resolve XInclude elements in a Nokogiri document. Handles standard XML includes, text includes, and text+fragid extensions. Processes iteratively to handle nested includes (e.g., file A includes file B which contains text+fragid self-references).
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/docbook/xinclude_resolver.rb', line 13 def self.resolve(doc) loop do includes = doc.xpath("//xi:include", "xi" => XINCLUDE_NS) break if includes.empty? resolved_any = false includes.each do |inc| href = inc["href"] next unless href parse_mode = inc["parse"] || "xml" fragid = inc["fragid"] base_dir = base_dir_for(inc.document) file_path = resolve_path(base_dir, href) next unless file_path && File.exist?(file_path) if parse_mode == "text" resolve_text_include(doc, inc, file_path, fragid) else resolve_xml_include(doc, inc, file_path) end resolved_any = true break # re-scan after each resolve (tree changes) end break unless resolved_any end doc rescue StandardError doc end |
.resolve_path(base_dir, href) ⇒ Object
91 92 93 94 95 |
# File 'lib/docbook/xinclude_resolver.rb', line 91 def self.resolve_path(base_dir, href) return nil unless base_dir File.join(base_dir, href) end |
.resolve_string(xml_string, base_path: nil) ⇒ Object
Pre-process an XML string to resolve XIncludes before model parsing.
45 46 47 48 49 50 51 52 53 |
# File 'lib/docbook/xinclude_resolver.rb', line 45 def self.resolve_string(xml_string, base_path: nil) doc = if base_path file_uri = "file://#{File.(base_path)}" Nokogiri::XML(xml_string, file_uri) else Nokogiri::XML(xml_string) end resolve(doc) end |
.resolve_text_include(doc, inc, file_path, fragid) ⇒ Object
── Include Resolution ──────────────────────────────────────────
57 58 59 60 61 62 63 64 65 |
# File 'lib/docbook/xinclude_resolver.rb', line 57 def self.resolve_text_include(doc, inc, file_path, fragid) content = File.read(file_path) if fragid && !fragid.empty? filtered = apply_fragid(content, fragid) content = filtered if filtered end text_node = doc.create_text_node(content) inc.replace(text_node) end |
.resolve_xml_include(doc, inc, file_path) ⇒ Object
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/docbook/xinclude_resolver.rb', line 67 def self.resolve_xml_include(doc, inc, file_path) included_xml = File.read(file_path) included_doc = Nokogiri::XML(included_xml, "file://#{File.(file_path)}") root = included_doc.root # Ensure namespace is declared in target document if root.namespace ns = root.namespace existing = doc.root.namespace_definitions.find { |n| n.href == ns.href } doc.root.add_namespace_definition(ns.prefix, ns.href) unless existing end # Replace the xi:include with the root element itself (not its children) inc.replace(root.dup) end |