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

data[R]

The raw input data, read from path or stream and unmodified.

date[W]

The date the document was published; center displayed in the document footer.

encoding[RW]

Encoding that the Ronn document is in

index[RW]

The index used to resolve man and file references.

manual[RW]

The manual this document belongs to; center displayed in the header.

name[W]

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.

organization[RW]

The name of the group, organization, or individual responsible for this document; displayed in the left portion of the footer.

outdir[RW]

Output directory to write files to.

path[R]

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.

section[W]

The man page’s section: a string whose first character is numeric; displayed in parenthesis along with the name.

styles[R]

Array of style modules to apply to the document.

tagline[RW]

Single sentence description of the thing being described by this man page; displayed in the NAME section.

Public Class Methods

new(path = nil, attributes = {}, &block) click to toggle source

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.

   # 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

Public Instance Methods

basename(type = nil) click to toggle source

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.

    # 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
convert(format) click to toggle source

Convert the document to :roff, :html, or :html_fragment and return the result as a string.

    # File lib/ronn/document.rb
245 def convert(format)
246   send "to_#{format}"
247 end
date() click to toggle source

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.

    # 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
html() click to toggle source

A Nokogiri DocumentFragment for the manual content fragment.

    # File lib/ronn/document.rb
239 def html
240   @html ||= process_html!
241 end
markdown() click to toggle source

Preprocessed markdown input text.

    # File lib/ronn/document.rb
234 def markdown
235   @markdown ||= process_markdown!
236 end
name() click to toggle source

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.

    # File lib/ronn/document.rb
140 def name
141   @name || path_name
142 end
name?() click to toggle source

Truthful when the name was extracted from the name section of the document.

    # File lib/ronn/document.rb
146 def name?
147   !@name.nil?
148 end
path_for(type = nil) click to toggle source

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.

    # 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
path_name() click to toggle source

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.

    # 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
path_section() click to toggle source

Returns the <section> part of the path, or nil when no path is available.

    # File lib/ronn/document.rb
131 def path_section
132   $1 if @basename.to_s =~ /\.(\d\w*)\./
133 end
reference_name() click to toggle source

The name used to reference this manual.

    # File lib/ronn/document.rb
164 def reference_name
165   name + (section && "(#{section})").to_s
166 end
section() click to toggle source

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.

    # File lib/ronn/document.rb
153 def section
154   @section || path_section
155 end
section?() click to toggle source

True when the section number was extracted from the name section of the document.

    # File lib/ronn/document.rb
159 def section?
160   !@section.nil?
161 end
section_heads()
Alias for: toc
sniff() click to toggle source

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.

    # 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
styles=(styles) click to toggle source

Styles to insert in the generated HTML output. This is a simple Array of string module names or file paths.

    # File lib/ronn/document.rb
206 def styles=(styles)
207   @styles = (%w[man] + styles).uniq
208 end
title() click to toggle source

The document’s title when no name section was defined. When a name section exists, this value is nil.

    # File lib/ronn/document.rb
177 def title
178   @tagline unless name?
179 end
title?() click to toggle source

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.

    # File lib/ronn/document.rb
171 def title?
172   !name? && tagline
173 end
to_h() click to toggle 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
to_html() click to toggle source

Convert the document to HTML and return the result as a string. The returned string is a complete HTML document.

    # 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
to_html_fragment(wrap_class = 'mp') click to toggle source

Convert the document to HTML and return the result as a string. The HTML does not include <html>, <head>, or <style> tags.

    # 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
to_json(*_args) click to toggle 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
to_markdown() click to toggle source
    # File lib/ronn/document.rb
286 def to_markdown
287   markdown
288 end
to_roff() click to toggle source

Convert the document to roff and return the result as a string.

    # 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
to_yaml() click to toggle source
    # File lib/ronn/document.rb
295 def to_yaml
296   require 'yaml'
297   to_h.to_yaml
298 end
toc() click to toggle source

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.

    # 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
Also aliased as: section_heads

Protected Instance Methods

html_filter_angle_quotes() click to toggle source

Perform angle quote (<THESE>) post filtering.

    # 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&gt;')
398 
399     new =
400       node.to_html
401           .gsub('&lt;var&gt;', '&lt;')
402           .gsub('&lt;/var&gt;', '>')
403     node.swap(new)
404   end
405 end
html_filter_definition_lists() click to toggle source

Convert special format unordered lists to definition lists.

    # 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
html_filter_heading_anchors() click to toggle source

Add URL anchors to all HTML heading elements.

    # 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
html_filter_inject_name_section() click to toggle 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
input_html() click to toggle 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
markdown_filter_angle_quotes(markdown) click to toggle source

Convert <WORD> to <var>WORD</var> but only if WORD isn’t an HTML tag.

    # 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
markdown_filter_heading_anchors(markdown) click to toggle source

Add [id]: ANCHOR elements to the markdown source text for all sections. This lets us use the [SECTION-REF][] syntax

    # 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
preprocess!() click to toggle source

Parse the document and extract the name, section, and tagline from its contents. This is called while the object is being initialized.

    # File lib/ronn/document.rb
312 def preprocess!
313   input_html
314   nil
315 end
process_html!() click to toggle 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
process_markdown!() click to toggle 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
strip_heading(html) click to toggle source
    # File lib/ronn/document.rb
327 def strip_heading(html)
328   heading, html = html.split("</h1>\n", 2)
329   html || heading
330 end