class Ronn::Document
The Document
class can be used to load and inspect a ronn document and to convert a ronn document into other formats, like roff or HTML.
Ronn
files may optionally follow the naming convention: “<name>.<section>.ronn”. The <name> and <section> are used in generated documentation unless overridden by the information extracted from the document’s name section.
Attributes
The raw input data, read from path or stream and unmodified.
The date the document was published; center displayed in the document footer.
Encoding that the Ronn
document is in
The index used to resolve man and file references.
The manual this document belongs to; center displayed in the header.
The man pages name: usually a single word name of a program or filename; displayed along with the section in the left and right portions of the header as well as the bottom right section of the footer.
The name of the group, organization, or individual responsible for this document; displayed in the left portion of the footer.
Output directory to write files to.
Path to the Ronn
document. This may be ‘-’ or nil when the Ronn::Document
object is created with a stream, in which case stdin will be read.
The man page’s section: a string whose first character is numeric; displayed in parenthesis along with the name.
Array of style modules to apply to the document.
Single sentence description of the thing being described by this man page; displayed in the NAME section.
Public Class Methods
Source
# File lib/ronn/document.rb 71 def initialize(path = nil, attributes = {}, &block) 72 @path = path 73 @basename = path.to_s =~ /^-?$/ ? nil : File.basename(path) 74 @reader = block || 75 lambda do |f| 76 if ['-', nil].include?(f) 77 $stdin.read 78 else 79 File.read(f, encoding: @encoding) 80 end 81 end 82 @data = @reader.call(path) 83 @name, @section, @tagline = sniff 84 85 @styles = %w[man] 86 @manual, @organization, @date = nil 87 @markdown, @input_html, @html = nil 88 @index = Ronn::Index[path || '.'] 89 @index.add_manual(self) if path && name 90 91 attributes.each { |attr_name, value| send("#{attr_name}=", value) } 92 end
Create a Ronn::Document
given a path or with the data returned by calling the block. The document is loaded and preprocessed before the intialize method returns. The attributes hash may contain values for any writeable attributes defined on this class.
Public Instance Methods
Source
# File lib/ronn/document.rb 98 def basename(type = nil) 99 type = nil if ['', 'roff'].include?(type.to_s) 100 [path_name || @name, path_section || @section, type] 101 .compact.join('.') 102 end
Generate a file basename of the form “<name>.<section>.<type>” for the given file extension. Uses the name and section from the source file path but falls back on the name and section defined in the document.
Source
# File lib/ronn/document.rb 245 def convert(format) 246 send "to_#{format}" 247 end
Convert the document to :roff, :html, or :html_fragment and return the result as a string.
Source
# File lib/ronn/document.rb 185 def date 186 return @date if @date 187 188 return Time.at(ENV['SOURCE_DATE_EPOCH'].to_i).gmtime if ENV['SOURCE_DATE_EPOCH'] 189 190 return File.mtime(path) if File.exist?(path) 191 192 Time.now 193 end
The date the man page was published. If not set explicitly, it first checks for “SOURCE_DATE_EPOCH” to support reproducible builds, then the file’s modified time or, if no file is given, the current time. Center displayed in the document footer.
Source
# File lib/ronn/document.rb 239 def html 240 @html ||= process_html! 241 end
A Nokogiri DocumentFragment for the manual content fragment.
Source
# File lib/ronn/document.rb 234 def markdown 235 @markdown ||= process_markdown! 236 end
Preprocessed markdown input text.
Source
# File lib/ronn/document.rb 140 def name 141 @name || path_name 142 end
Returns the manual page name based first on the document’s contents and then on the path name. Usually a single word name of a program or filename; displayed along with the section in the left and right portions of the header as well as the bottom right section of the footer.
Source
# File lib/ronn/document.rb 146 def name? 147 !@name.nil? 148 end
Truthful when the name was extracted from the name section of the document.
Source
# File lib/ronn/document.rb 107 def path_for(type = nil) 108 if @outdir 109 File.join(@outdir, basename(type)) 110 elsif @basename 111 File.join(File.dirname(path), basename(type)) 112 else 113 basename(type) 114 end 115 end
Construct a path for a file near the source file. Uses the Document#basename
method to generate the basename part and appends it to the dirname of the source document.
Source
# File lib/ronn/document.rb 120 def path_name 121 return unless @basename 122 123 parts = @basename.split('.') 124 parts.pop if parts.length > 1 && parts.last =~ /^\w+$/ 125 parts.pop if parts.last =~ /^\d+$/ 126 parts.join('.') 127 end
Returns the <name> part of the path, or nil when no path is available. This is used as the manual page name when the file contents do not include a name section.
Source
# File lib/ronn/document.rb 131 def path_section 132 $1 if @basename.to_s =~ /\.(\d\w*)\./ 133 end
Returns the <section> part of the path, or nil when no path is available.
Source
# File lib/ronn/document.rb 164 def reference_name 165 name + (section && "(#{section})").to_s 166 end
The name used to reference this manual.
Source
# File lib/ronn/document.rb 153 def section 154 @section || path_section 155 end
Returns the manual page section based first on the document’s contents and then on the path name. A string whose first character is numeric; displayed in parenthesis along with the name.
Source
# File lib/ronn/document.rb 159 def section? 160 !@section.nil? 161 end
True when the section number was extracted from the name section of the document.
Source
# File lib/ronn/document.rb 213 def sniff 214 html = Kramdown::Document.new(data[0, 512], auto_ids: false, 215 smart_quotes: ['apos', 'apos', 'quot', 'quot'], 216 typographic_symbols: { hellip: '...', ndash: '--', mdash: '--' }).to_html 217 heading, html = html.split("</h1>\n", 2) 218 return [nil, nil, nil] if html.nil? 219 220 case heading 221 when /([\w_.\[\]~+=@:-]+)\s*\((\d\w*)\)\s*-+\s*(.*)/ 222 # name(section) -- description 223 [$1, $2, $3] 224 when /([\w_.\[\]~+=@:-]+)\s+-+\s+(.*)/ 225 # name -- description 226 [$1, nil, $2] 227 else 228 # description 229 [nil, nil, heading.sub('<h1>', '')] 230 end 231 end
Sniff the document header and extract basic document metadata. Return a tuple of the form: [name, section, description], where missing information is represented by nil and any element may be missing.
Source
# File lib/ronn/document.rb 206 def styles=(styles) 207 @styles = (%w[man] + styles).uniq 208 end
Styles to insert in the generated HTML output. This is a simple Array of string module names or file paths.
Source
# File lib/ronn/document.rb 177 def title 178 @tagline unless name? 179 end
The document’s title when no name section was defined. When a name section exists, this value is nil.
Source
# File lib/ronn/document.rb 171 def title? 172 !name? && tagline 173 end
Truthful when the document started with an h1 but did not follow the “<name>(<sect>) – <tagline>” convention. We assume this is some kind of custom title.
Source
# File lib/ronn/document.rb 290 def to_h 291 %w[name section tagline manual organization date styles toc] 292 .each_with_object({}) { |name, hash| hash[name] = send(name) } 293 end
Source
# File lib/ronn/document.rb 260 def to_html 261 layout = ENV.fetch('RONN_LAYOUT', nil) 262 layout_path = nil 263 if layout 264 layout_path = File.expand_path(layout) 265 unless File.exist?(layout_path) 266 warn "warn: can't find #{layout}, using default layout." 267 layout_path = nil 268 end 269 end 270 271 template = Ronn::Template.new(self) 272 template.context.push html: to_html_fragment(nil) 273 template.render(layout_path || 'default') 274 end
Convert the document to HTML and return the result as a string. The returned string is a complete HTML document.
Source
# File lib/ronn/document.rb 279 def to_html_fragment(wrap_class = 'mp') 280 frag_nodes = html.at('body').children 281 out = frag_nodes.to_s.rstrip 282 out = "<div class='#{wrap_class}'>#{out}\n</div>" unless wrap_class.nil? 283 out 284 end
Convert the document to HTML and return the result as a string. The HTML does not include <html>, <head>, or <style> tags.
Source
# File lib/ronn/document.rb 300 def to_json(*_args) 301 require 'json' 302 to_h.merge('date' => date.iso8601).to_json 303 end
Source
# File lib/ronn/document.rb 250 def to_roff 251 RoffFilter.new( 252 to_html_fragment(nil), 253 name, section, tagline, 254 manual, organization, date 255 ).to_s 256 end
Convert the document to roff and return the result as a string.
Source
# File lib/ronn/document.rb 295 def to_yaml 296 require 'yaml' 297 to_h.to_yaml 298 end
Source
# File lib/ronn/document.rb 198 def toc 199 @toc ||= 200 html.search('h2[@id]').map { |h2| [h2.attributes['id'].content.upcase, h2.inner_text] } 201 end
Retrieve a list of top-level section headings in the document and return as an array of +[id, text]+ tuples, where id
is the element’s generated id and text
is the inner text of the heading element.
Protected Instance Methods
Source
# File lib/ronn/document.rb 515 def html_build_manual_reference_link(name_or_node, section) 516 name = if name_or_node.respond_to?(:inner_text) 517 name_or_node.inner_text 518 else 519 name_or_node 520 end 521 ref = index["#{name}#{section}"] 522 if ref 523 "<a class='man-ref' href='#{ref.url}'>#{name_or_node}<span class='s'>#{section}</span></a>" 524 else 525 # warn "warn: manual reference not defined: '#{name}#{section}'" 526 "<span class='man-ref'>#{name_or_node}<span class='s'>#{section}</span></span>" 527 end 528 end
HTMLize the manual page reference. The result is an <a> if the page appears in the index, otherwise it is a <span>. The first argument may be an HTML element or a string. The second should be a string of the form “(#{section})”.
Source
# File lib/ronn/document.rb 392 def html_filter_angle_quotes 393 # convert all angle quote vars nested in code blocks 394 # back to the original text 395 code_nodes = @html.search('code') 396 code_nodes.search('.//text() | text()').each do |node| 397 next unless node.to_html.include?('var>') 398 399 new = 400 node.to_html 401 .gsub('<var>', '<') 402 .gsub('</var>', '>') 403 node.swap(new) 404 end 405 end
Perform angle quote (<THESE>) post filtering.
Source
# File lib/ronn/document.rb 464 def html_filter_annotate_bare_links 465 @html.search('a[@href]').each do |node| 466 href = node.attributes['href'].content 467 text = node.inner_text 468 469 next unless href == text || href[0] == '#' || 470 CGI.unescapeHTML(href) == "mailto:#{CGI.unescapeHTML(text)}" 471 472 node.set_attribute('data-bare-link', 'true') 473 end 474 end
Add a ‘data-bare-link’ attribute to hyperlinks whose text labels are the same as their href URLs.
Source
# File lib/ronn/document.rb 408 def html_filter_definition_lists 409 # process all unordered lists depth-first 410 @html.search('ul').to_a.reverse_each do |ul| 411 items = ul.search('li') 412 next if items.any? { |item| item.inner_text.strip.split("\n", 2).first !~ /:$/ } 413 414 dl = Nokogiri::XML::Node.new 'dl', html 415 items.each do |item| 416 # This processing is specific to how Markdown generates definition lists 417 term, definition = item.inner_html.strip.split(":\n", 2) 418 term = term.sub(/^<p>/, '') 419 420 dt = Nokogiri::XML::Node.new 'dt', html 421 dt.children = Nokogiri::HTML.fragment(term) 422 dt.attributes['class'] = 'flush' if dt.inner_text.length <= 7 423 424 dd = Nokogiri::XML::Node.new 'dd', html 425 dd_contents = Nokogiri::HTML.fragment(definition) 426 dd.children = dd_contents 427 428 dl.add_child(dt) 429 dl.add_child(dd) 430 end 431 ul.replace(dl) 432 end 433 end
Convert special format unordered lists to definition lists.
Source
# File lib/ronn/document.rb 455 def html_filter_heading_anchors 456 h_nodes = @html.search('//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 and not(@id)]') 457 h_nodes.each do |heading| 458 heading.set_attribute('id', heading.inner_text.gsub(/\W+/, '-')) 459 end 460 end
Add URL anchors to all HTML heading elements.
Source
# File lib/ronn/document.rb 435 def html_filter_inject_name_section 436 markup = 437 if title? 438 "<h1>#{title}</h1>" 439 elsif name 440 "<h2>NAME</h2>\n" \ 441 "<p class='man-name'>\n <code>#{name}</code>" + 442 (tagline ? " - <span class='man-whatis'>#{tagline}</span>\n" : "\n") + 443 "</p>\n" 444 end 445 return unless markup 446 447 if html.at('body').first_element_child 448 html.at('body').first_element_child.before(Nokogiri::HTML.fragment(markup)) 449 else 450 html.at('body').add_child(Nokogiri::HTML.fragment(markup)) 451 end 452 end
Source
# File lib/ronn/document.rb 478 def html_filter_manual_reference_links 479 return if index.nil? 480 481 name_pattern = '[0-9A-Za-z_:.+=@~-]+' 482 483 # Convert "name(section)" by traversing text nodes searching for 484 # text that fits the pattern. This is the original implementation. 485 @html.search('.//text() | text()').each do |node| 486 next unless node.content.include?(')') 487 next if %w[pre code h1 h2 h3].include?(node.parent.name) 488 next if child_of?(node, 'a') 489 node.swap(node.content.gsub(/(#{name_pattern})(\(\d+\w*\))/) do 490 html_build_manual_reference_link($1, $2) 491 end) 492 end 493 494 # Convert "<code>name</code>(section)" by traversing <code> nodes. 495 # For each one that contains exactly an acceptable manual page name, 496 # the next sibling is checked and must be a text node beginning 497 # with a valid section in parentheses. 498 @html.search('code').each do |node| 499 next if %w[pre code h1 h2 h3].include?(node.parent.name) 500 next if child_of?(node, 'a') 501 next unless node.inner_text =~ /^#{name_pattern}$/ 502 sibling = node.next 503 next unless sibling 504 next unless sibling.text? 505 next unless sibling.content =~ /^\((\d+\w*)\)/ 506 node.swap(html_build_manual_reference_link(node, "(#{$1})")) 507 sibling.content = sibling.content.gsub(/^\(\d+\w*\)/, '') 508 end 509 end
Convert text of the form “name(section)” or “name
(section) to a hyperlink. The URL is obtained from the index.
Source
# File lib/ronn/document.rb 317 def input_html 318 @input_html ||= strip_heading(Kramdown::Document.new(markdown, 319 auto_ids: false, 320 input: 'GFM', 321 hard_wrap: 'false', 322 syntax_highlighter_opts: 'line_numbers: false', 323 smart_quotes: ['apos', 'apos', 'quot', 'quot'], 324 typographic_symbols: { hellip: '...', ndash: '--', mdash: '--' }).to_html) 325 end
Source
# File lib/ronn/document.rb 378 def markdown_filter_angle_quotes(markdown) 379 markdown.gsub(/(?<!\\)<([^:.\/]+?)>/) do |match| 380 contents = $1 381 tag, attrs = contents.split(' ', 2) 382 if attrs =~ /\/=/ || html_element?(tag.sub(/^\//, '')) || 383 data.include?("</#{tag}>") || contents =~ /^!/ 384 match.to_s 385 else 386 "<var>#{contents}</var>" 387 end 388 end 389 end
Convert <WORD> to <var>WORD</var> but only if WORD isn’t an HTML tag.
Source
# File lib/ronn/document.rb 365 def markdown_filter_heading_anchors(markdown) 366 first = true 367 markdown.split("\n").grep(/^[#]{2,5} +[\w '-]+[# ]*$/).each do |line| 368 markdown << "\n\n" if first 369 first = false 370 title = line.gsub(/[^\w -]/, '').strip 371 anchor = title.gsub(/\W+/, '-').gsub(/(^-+|-+$)/, '') 372 markdown << "[#{title}]: ##{anchor} \"#{title}\"\n" 373 end 374 markdown 375 end
Add [id]: ANCHOR elements to the markdown source text for all sections. This lets us use the [SECTION-REF][] syntax
Source
# File lib/ronn/document.rb 355 def markdown_filter_link_index(markdown) 356 return markdown if index.nil? || index.empty? 357 358 markdown << "\n\n" 359 index.each { |ref| markdown << "[#{ref.name}]: #{ref.url}\n" } 360 markdown 361 end
Appends all index links to the end of the document as Markdown reference links. This lets us use [foo(3)][] syntax to link to index entries.
Source
# File lib/ronn/document.rb 312 def preprocess! 313 input_html 314 nil 315 end
Parse the document and extract the name, section, and tagline from its contents. This is called while the object is being initialized.
Source
# File lib/ronn/document.rb 338 def process_html! 339 wrapped_html = "<html>\n <body>\n#{input_html}\n </body>\n</html>" 340 @html = Nokogiri::HTML.parse(wrapped_html) 341 html_filter_angle_quotes 342 html_filter_definition_lists 343 html_filter_inject_name_section 344 html_filter_heading_anchors 345 html_filter_annotate_bare_links 346 html_filter_manual_reference_links 347 @html 348 end
Source
# File lib/ronn/document.rb 332 def process_markdown! 333 md = markdown_filter_heading_anchors(data) 334 md = markdown_filter_link_index(md) 335 markdown_filter_angle_quotes(md) 336 end
Source
# File lib/ronn/document.rb 327 def strip_heading(html) 328 heading, html = html.split("</h1>\n", 2) 329 html || heading 330 end