ronn-ng 0.7.4
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 +7 -0
- data/AUTHORS +8 -0
- data/CHANGES +184 -0
- data/INSTALLING +20 -0
- data/LICENSE.txt +11 -0
- data/README.md +113 -0
- data/Rakefile +163 -0
- data/bin/ronn +223 -0
- data/config.ru +15 -0
- data/lib/ronn.rb +50 -0
- data/lib/ronn/document.rb +495 -0
- data/lib/ronn/index.rb +183 -0
- data/lib/ronn/roff.rb +302 -0
- data/lib/ronn/server.rb +70 -0
- data/lib/ronn/template.rb +171 -0
- data/lib/ronn/template/80c.css +6 -0
- data/lib/ronn/template/dark.css +18 -0
- data/lib/ronn/template/darktoc.css +17 -0
- data/lib/ronn/template/default.html +41 -0
- data/lib/ronn/template/man.css +100 -0
- data/lib/ronn/template/print.css +5 -0
- data/lib/ronn/template/screen.css +105 -0
- data/lib/ronn/template/toc.css +27 -0
- data/lib/ronn/utils.rb +55 -0
- data/man/index.html +78 -0
- data/man/index.txt +15 -0
- data/man/ronn-format.7 +201 -0
- data/man/ronn-format.7.ronn +157 -0
- data/man/ronn.1 +325 -0
- data/man/ronn.1.ronn +306 -0
- data/ronn-ng.gemspec +97 -0
- data/test/angle_bracket_syntax.html +18 -0
- data/test/angle_bracket_syntax.ronn +12 -0
- data/test/basic_document.html +9 -0
- data/test/basic_document.ronn +4 -0
- data/test/contest.rb +68 -0
- data/test/custom_title_document.html +6 -0
- data/test/custom_title_document.ronn +5 -0
- data/test/definition_list_syntax.html +21 -0
- data/test/definition_list_syntax.roff +26 -0
- data/test/definition_list_syntax.ronn +18 -0
- data/test/dots_at_line_start_test.roff +10 -0
- data/test/dots_at_line_start_test.ronn +4 -0
- data/test/entity_encoding_test.html +35 -0
- data/test/entity_encoding_test.roff +61 -0
- data/test/entity_encoding_test.ronn +25 -0
- data/test/index.txt +8 -0
- data/test/markdown_syntax.html +957 -0
- data/test/markdown_syntax.roff +1467 -0
- data/test/markdown_syntax.ronn +881 -0
- data/test/middle_paragraph.html +15 -0
- data/test/middle_paragraph.roff +13 -0
- data/test/middle_paragraph.ronn +10 -0
- data/test/missing_spaces.roff +9 -0
- data/test/missing_spaces.ronn +2 -0
- data/test/pre_block_with_quotes.roff +13 -0
- data/test/pre_block_with_quotes.ronn +6 -0
- data/test/section_reference_links.html +17 -0
- data/test/section_reference_links.roff +10 -0
- data/test/section_reference_links.ronn +12 -0
- data/test/test_ronn.rb +110 -0
- data/test/test_ronn_document.rb +186 -0
- data/test/test_ronn_index.rb +73 -0
- data/test/titleless_document.html +10 -0
- data/test/titleless_document.ronn +3 -0
- data/test/underline_spacing_test.roff +21 -0
- data/test/underline_spacing_test.ronn +11 -0
- metadata +176 -0
    
        data/lib/ronn/index.rb
    ADDED
    
    | @@ -0,0 +1,183 @@ | |
| 1 | 
            +
            require 'ronn'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Ronn
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              # Maintains a list of links / references to manuals and other resources.
         | 
| 6 | 
            +
              class Index
         | 
| 7 | 
            +
                include Enumerable
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                attr_reader :path
         | 
| 10 | 
            +
                attr_reader :references
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                # Retrieve an Index for <path>, where <path> is a directory or normal
         | 
| 13 | 
            +
                # file. The index is loaded from the corresponding index.txt file if
         | 
| 14 | 
            +
                # one exists.
         | 
| 15 | 
            +
                def self.[](path)
         | 
| 16 | 
            +
                  (@indexes ||= {})[index_path_for_file(path)] ||=
         | 
| 17 | 
            +
                    Index.new(index_path_for_file(path))
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def self.index_path_for_file(file)
         | 
| 21 | 
            +
                  File.expand_path(
         | 
| 22 | 
            +
                    if File.directory?(file)
         | 
| 23 | 
            +
                      File.join(file, 'index.txt')
         | 
| 24 | 
            +
                    else
         | 
| 25 | 
            +
                      File.join(File.dirname(file), 'index.txt')
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                  )
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def initialize(path, &bk)
         | 
| 31 | 
            +
                  @path = path
         | 
| 32 | 
            +
                  @references = []
         | 
| 33 | 
            +
                  @manuals    = {}
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  if block_given?
         | 
| 36 | 
            +
                    read! yield
         | 
| 37 | 
            +
                  elsif exist?
         | 
| 38 | 
            +
                    read! File.read(path)
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                # Determine whether the index file exists.
         | 
| 43 | 
            +
                def exist?
         | 
| 44 | 
            +
                  File.exist?(path)
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                # Load index data from a string.
         | 
| 48 | 
            +
                def read!(data)
         | 
| 49 | 
            +
                  data.each_line do |line|
         | 
| 50 | 
            +
                    line = line.strip.gsub(/\s*#.*$/, '')
         | 
| 51 | 
            +
                    if !line.empty?
         | 
| 52 | 
            +
                      name, url = line.split(/\s+/, 2)
         | 
| 53 | 
            +
                      @references << reference(name, url)
         | 
| 54 | 
            +
                    end
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                ##
         | 
| 59 | 
            +
                # Enumerable and friends
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def each(&bk)
         | 
| 62 | 
            +
                  references.each(&bk)
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                def size
         | 
| 66 | 
            +
                  references.size
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def first
         | 
| 70 | 
            +
                  references.first
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                def last
         | 
| 74 | 
            +
                  references.last
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                def empty?
         | 
| 78 | 
            +
                  references.empty?
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                def [](name)
         | 
| 82 | 
            +
                  references.find { |ref| ref.name == name }
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def reference(name, path)
         | 
| 86 | 
            +
                  Reference.new(self, name, path)
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                def <<(path)
         | 
| 90 | 
            +
                  raise ArgumentError, "local paths only" if path =~ /(https?|mailto):/
         | 
| 91 | 
            +
                  return self if any? { |ref| ref.path == File.expand_path(path) }
         | 
| 92 | 
            +
                  relative_path = relative_to_index(path)
         | 
| 93 | 
            +
                  @references << \
         | 
| 94 | 
            +
                    if path =~ /\.ronn?$/
         | 
| 95 | 
            +
                      reference manual(path).reference_name, relative_path
         | 
| 96 | 
            +
                    else
         | 
| 97 | 
            +
                      reference File.basename(path), relative_path
         | 
| 98 | 
            +
                    end
         | 
| 99 | 
            +
                  self
         | 
| 100 | 
            +
                end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                def add_manual(manual)
         | 
| 103 | 
            +
                  @manuals[File.expand_path(manual.path)] = manual
         | 
| 104 | 
            +
                  self << manual.path
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                def manual(path)
         | 
| 108 | 
            +
                  @manuals[File.expand_path(path)] ||= Document.new(path)
         | 
| 109 | 
            +
                end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                def manuals
         | 
| 112 | 
            +
                  select { |ref| ref.relative? && ref.ronn? }.
         | 
| 113 | 
            +
                  map    { |ref| manual(ref.path) }
         | 
| 114 | 
            +
                end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                ##
         | 
| 117 | 
            +
                # Converting
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                def to_text
         | 
| 120 | 
            +
                  map { |ref| [ref.name, ref.location].join(' ') }.join("\n")
         | 
| 121 | 
            +
                end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                def to_a
         | 
| 124 | 
            +
                  references
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                def to_h
         | 
| 128 | 
            +
                  to_a.map { |doc| doc.to_hash }
         | 
| 129 | 
            +
                end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                def relative_to_index(path)
         | 
| 132 | 
            +
                  path = File.expand_path(path)
         | 
| 133 | 
            +
                  index_dir = File.dirname(File.expand_path(self.path))
         | 
| 134 | 
            +
                  path.sub(/^#{index_dir}\//, '')
         | 
| 135 | 
            +
                end
         | 
| 136 | 
            +
              end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
              # An individual index reference. A reference can point to one of a few types
         | 
| 139 | 
            +
              # of locations:
         | 
| 140 | 
            +
              #
         | 
| 141 | 
            +
              #   - URLs: "http://man.cx/crontab(5)"
         | 
| 142 | 
            +
              #   - Relative paths to ronn manuals: "crontab.5.ronn"
         | 
| 143 | 
            +
              #
         | 
| 144 | 
            +
              # The #url method should be used to obtain the href value for HTML.
         | 
| 145 | 
            +
              class Reference
         | 
| 146 | 
            +
                attr_reader :name
         | 
| 147 | 
            +
                attr_reader :location
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                def initialize(index, name, location)
         | 
| 150 | 
            +
                  @index = index
         | 
| 151 | 
            +
                  @name = name
         | 
| 152 | 
            +
                  @location = location
         | 
| 153 | 
            +
                end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                def manual?
         | 
| 156 | 
            +
                  name =~ /\([0-9]\w*\)$/
         | 
| 157 | 
            +
                end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                def ronn?
         | 
| 160 | 
            +
                  location =~ /\.ronn?$/
         | 
| 161 | 
            +
                end
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                def remote?
         | 
| 164 | 
            +
                  location =~ /^(?:https?|mailto):/
         | 
| 165 | 
            +
                end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                def relative?
         | 
| 168 | 
            +
                  !remote?
         | 
| 169 | 
            +
                end
         | 
| 170 | 
            +
             | 
| 171 | 
            +
                def url
         | 
| 172 | 
            +
                  if remote?
         | 
| 173 | 
            +
                    location
         | 
| 174 | 
            +
                  else
         | 
| 175 | 
            +
                    location.chomp('.ronn') + '.html'
         | 
| 176 | 
            +
                  end
         | 
| 177 | 
            +
                end
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                def path
         | 
| 180 | 
            +
                  File.expand_path(location, File.dirname(@index.path)) if relative?
         | 
| 181 | 
            +
                end
         | 
| 182 | 
            +
              end
         | 
| 183 | 
            +
            end
         | 
    
        data/lib/ronn/roff.rb
    ADDED
    
    | @@ -0,0 +1,302 @@ | |
| 1 | 
            +
            require 'hpricot'
         | 
| 2 | 
            +
            require 'ronn/utils'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Ronn
         | 
| 5 | 
            +
              class RoffFilter
         | 
| 6 | 
            +
                include Ronn::Utils
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                # Convert Ronn HTML to roff.
         | 
| 9 | 
            +
                def initialize(html, name, section, tagline, manual=nil, version=nil, date=nil)
         | 
| 10 | 
            +
                  @buf = []
         | 
| 11 | 
            +
                  title_heading name, section, tagline, manual, version, date
         | 
| 12 | 
            +
                  doc = Hpricot(html)
         | 
| 13 | 
            +
                  remove_extraneous_elements! doc
         | 
| 14 | 
            +
                  normalize_whitespace! doc
         | 
| 15 | 
            +
                  block_filter doc
         | 
| 16 | 
            +
                  write "\n"
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def to_s
         | 
| 20 | 
            +
                  @buf.join.gsub(/[ \t]+$/, '')
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              protected
         | 
| 24 | 
            +
                def previous(node)
         | 
| 25 | 
            +
                  if node.respond_to?(:previous)
         | 
| 26 | 
            +
                    prev = node.previous
         | 
| 27 | 
            +
                    prev = prev.previous until prev.nil? || prev.elem?
         | 
| 28 | 
            +
                    prev
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def title_heading(name, section, tagline, manual, version, date)
         | 
| 33 | 
            +
                  comment "generated with Ronn-NG/v#{Ronn.version}"
         | 
| 34 | 
            +
                  comment "http://github.com/apjanke/ronn-ng/tree/#{Ronn.revision}"
         | 
| 35 | 
            +
                  return if name.nil?
         | 
| 36 | 
            +
                  macro "TH", %["#{escape(name.upcase)}" "#{section}" "#{date.strftime('%B %Y')}" "#{version}" "#{manual}"]
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def remove_extraneous_elements!(doc)
         | 
| 40 | 
            +
                  doc.traverse_all_element do |node|
         | 
| 41 | 
            +
                    if node.comment? || node.procins? || node.doctype? || node.xmldecl?
         | 
| 42 | 
            +
                      node.parent.children.delete(node)
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def normalize_whitespace!(node)
         | 
| 48 | 
            +
                  case
         | 
| 49 | 
            +
                  when node.kind_of?(Array) || node.kind_of?(Hpricot::Elements)
         | 
| 50 | 
            +
                    node.to_a.dup.each { |ch| normalize_whitespace! ch }
         | 
| 51 | 
            +
                  when node.text?
         | 
| 52 | 
            +
                    preceding, following = node.previous, node.next
         | 
| 53 | 
            +
                    content = node.content.gsub(/[\n ]+/m, ' ')
         | 
| 54 | 
            +
                    if preceding.nil? || block_element?(preceding.name) ||
         | 
| 55 | 
            +
                       preceding.name == 'br'
         | 
| 56 | 
            +
                      content.lstrip!
         | 
| 57 | 
            +
                    end
         | 
| 58 | 
            +
                    if following.nil? || block_element?(following.name) ||
         | 
| 59 | 
            +
                       following.name == 'br'
         | 
| 60 | 
            +
                      content.rstrip!
         | 
| 61 | 
            +
                    end
         | 
| 62 | 
            +
                    if content.empty?
         | 
| 63 | 
            +
                      node.parent.children.delete(node)
         | 
| 64 | 
            +
                    else
         | 
| 65 | 
            +
                      node.content = content
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
                  when node.elem? && node.name == 'pre'
         | 
| 68 | 
            +
                    # stop traversing
         | 
| 69 | 
            +
                  when node.elem? && node.children
         | 
| 70 | 
            +
                    normalize_whitespace! node.children
         | 
| 71 | 
            +
                  when node.elem?
         | 
| 72 | 
            +
                    # element has no children
         | 
| 73 | 
            +
                  when node.doc?
         | 
| 74 | 
            +
                    normalize_whitespace! node.children
         | 
| 75 | 
            +
                  else
         | 
| 76 | 
            +
                    warn "unexpected node during whitespace normalization: %p", node
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                def block_filter(node)
         | 
| 81 | 
            +
                  if node.kind_of?(Array) || node.kind_of?(Hpricot::Elements)
         | 
| 82 | 
            +
                    node.each { |ch| block_filter(ch) }
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  elsif node.doc?
         | 
| 85 | 
            +
                    block_filter(node.children)
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  elsif node.text?
         | 
| 88 | 
            +
                    warn "unexpected text: %p",  node
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  elsif node.elem?
         | 
| 91 | 
            +
                    case node.name
         | 
| 92 | 
            +
                    when 'div'
         | 
| 93 | 
            +
                      block_filter(node.children)
         | 
| 94 | 
            +
                    when 'h1'
         | 
| 95 | 
            +
                      # discard
         | 
| 96 | 
            +
                    when 'h2'
         | 
| 97 | 
            +
                      macro "SH", quote(escape(node.html))
         | 
| 98 | 
            +
                    when 'h3'
         | 
| 99 | 
            +
                      macro "SS", quote(escape(node.html))
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                    when 'p'
         | 
| 102 | 
            +
                      prev = previous(node)
         | 
| 103 | 
            +
                      if prev && %w[dd li blockquote].include?(node.parent.name)
         | 
| 104 | 
            +
                        macro "IP"
         | 
| 105 | 
            +
                      elsif prev && !%w[h1 h2 h3].include?(prev.name)
         | 
| 106 | 
            +
                        macro "P"
         | 
| 107 | 
            +
                      end
         | 
| 108 | 
            +
                      inline_filter(node.children)
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                    when 'blockquote'
         | 
| 111 | 
            +
                      prev = previous(node)
         | 
| 112 | 
            +
                      indent = prev.nil? || !%w[h1 h2 h3].include?(prev.name)
         | 
| 113 | 
            +
                      macro "IP", %w["" 4] if indent
         | 
| 114 | 
            +
                      block_filter(node.children)
         | 
| 115 | 
            +
                      macro "IP", %w["" 0] if indent
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                    when 'pre'
         | 
| 118 | 
            +
                      prev = previous(node)
         | 
| 119 | 
            +
                      indent = prev.nil? || !%w[h1 h2 h3].include?(prev.name)
         | 
| 120 | 
            +
                      macro "IP", %w["" 4] if indent
         | 
| 121 | 
            +
                      macro "nf"
         | 
| 122 | 
            +
                      write "\n"
         | 
| 123 | 
            +
                      inline_filter(node.children)
         | 
| 124 | 
            +
                      macro "fi"
         | 
| 125 | 
            +
                      macro "IP", %w["" 0] if indent
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                    when 'dl'
         | 
| 128 | 
            +
                      macro "TP"
         | 
| 129 | 
            +
                      block_filter(node.children)
         | 
| 130 | 
            +
                    when 'dt'
         | 
| 131 | 
            +
                      prev = previous(node)
         | 
| 132 | 
            +
                      macro "TP" unless prev.nil?
         | 
| 133 | 
            +
                      inline_filter(node.children)
         | 
| 134 | 
            +
                      write "\n"
         | 
| 135 | 
            +
                    when 'dd'
         | 
| 136 | 
            +
                      if node.at('p')
         | 
| 137 | 
            +
                        block_filter(node.children)
         | 
| 138 | 
            +
                      else
         | 
| 139 | 
            +
                        inline_filter(node.children)
         | 
| 140 | 
            +
                      end
         | 
| 141 | 
            +
                      write "\n"
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                    when 'ol', 'ul'
         | 
| 144 | 
            +
                      block_filter(node.children)
         | 
| 145 | 
            +
                      macro "IP", %w["" 0]
         | 
| 146 | 
            +
                    when 'li'
         | 
| 147 | 
            +
                      case node.parent.name
         | 
| 148 | 
            +
                      when 'ol'
         | 
| 149 | 
            +
                        macro "IP", %W["#{node.position + 1}." 4]
         | 
| 150 | 
            +
                      when 'ul'
         | 
| 151 | 
            +
                        macro "IP", %w["\\\[ci\]" 4]
         | 
| 152 | 
            +
                      end
         | 
| 153 | 
            +
                      if node.at('p|ol|ul|dl|div')
         | 
| 154 | 
            +
                        block_filter(node.children)
         | 
| 155 | 
            +
                      else
         | 
| 156 | 
            +
                        inline_filter(node.children)
         | 
| 157 | 
            +
                      end
         | 
| 158 | 
            +
                      write "\n"
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                    when 'span', 'code', 'b', 'strong', 'kbd', 'samp', 'var', 'em', 'i',
         | 
| 161 | 
            +
                         'u', 'br', 'a'
         | 
| 162 | 
            +
                      inline_filter(node)
         | 
| 163 | 
            +
                    else
         | 
| 164 | 
            +
                      warn "unrecognized block tag: %p", node.name
         | 
| 165 | 
            +
                    end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                  else
         | 
| 168 | 
            +
                    fail "unexpected node: #{node.inspect}"
         | 
| 169 | 
            +
                  end
         | 
| 170 | 
            +
                end
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                def inline_filter(node)
         | 
| 173 | 
            +
                  return unless node # is an empty node
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                  if node.kind_of?(Array) || node.kind_of?(Hpricot::Elements)
         | 
| 176 | 
            +
                    node.each { |ch| inline_filter(ch) }
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                  elsif node.text?
         | 
| 179 | 
            +
                    text = node.to_html.dup
         | 
| 180 | 
            +
                    write escape(text)
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                  elsif node.elem?
         | 
| 183 | 
            +
                    case node.name
         | 
| 184 | 
            +
                    when 'span'
         | 
| 185 | 
            +
                      inline_filter(node.children)
         | 
| 186 | 
            +
                    when 'code'
         | 
| 187 | 
            +
                      if child_of?(node, 'pre')
         | 
| 188 | 
            +
                        inline_filter(node.children)
         | 
| 189 | 
            +
                      else
         | 
| 190 | 
            +
                        write '\fB'
         | 
| 191 | 
            +
                        inline_filter(node.children)
         | 
| 192 | 
            +
                        write '\fR'
         | 
| 193 | 
            +
                      end
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                    when 'b', 'strong', 'kbd', 'samp'
         | 
| 196 | 
            +
                      write '\fB'
         | 
| 197 | 
            +
                      inline_filter(node.children)
         | 
| 198 | 
            +
                      write '\fR'
         | 
| 199 | 
            +
             | 
| 200 | 
            +
                    when 'var', 'em', 'i', 'u'
         | 
| 201 | 
            +
                      write '\fI'
         | 
| 202 | 
            +
                      inline_filter(node.children)
         | 
| 203 | 
            +
                      write '\fR'
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                    when 'br'
         | 
| 206 | 
            +
                      macro 'br'
         | 
| 207 | 
            +
             | 
| 208 | 
            +
                    when 'a'
         | 
| 209 | 
            +
                      if node.classes.include?('man-ref')
         | 
| 210 | 
            +
                        inline_filter(node.children)
         | 
| 211 | 
            +
                      elsif node.has_attribute?('data-bare-link')
         | 
| 212 | 
            +
                        write '\fI'
         | 
| 213 | 
            +
                        inline_filter(node.children)
         | 
| 214 | 
            +
                        write '\fR'
         | 
| 215 | 
            +
                      else
         | 
| 216 | 
            +
                        inline_filter(node.children)
         | 
| 217 | 
            +
                        write ' '
         | 
| 218 | 
            +
                        write '\fI'
         | 
| 219 | 
            +
                        write escape(node.attributes['href'])
         | 
| 220 | 
            +
                        write '\fR'
         | 
| 221 | 
            +
                      end
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                    when 'sup'
         | 
| 224 | 
            +
                      # This superscript equivalent is a big ugly hack.
         | 
| 225 | 
            +
                      write '^('
         | 
| 226 | 
            +
                      inline_filter(node.children)
         | 
| 227 | 
            +
                      write ')'
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                    else
         | 
| 230 | 
            +
                      warn "unrecognized inline tag: %p", node.name
         | 
| 231 | 
            +
                    end
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                  else
         | 
| 234 | 
            +
                    fail "unexpected node: #{node.inspect}"
         | 
| 235 | 
            +
                  end
         | 
| 236 | 
            +
                end
         | 
| 237 | 
            +
             | 
| 238 | 
            +
                def macro(name, value=nil)
         | 
| 239 | 
            +
                  writeln ".\n.#{[name, value].compact.join(' ')}"
         | 
| 240 | 
            +
                end
         | 
| 241 | 
            +
             | 
| 242 | 
            +
                HTML_ROFF_ENTITIES = {
         | 
| 243 | 
            +
                  '•'  => '\[ci]',
         | 
| 244 | 
            +
                  '<'    => '<',
         | 
| 245 | 
            +
                  '>'    => '>',
         | 
| 246 | 
            +
                  ' '  => '\~',
         | 
| 247 | 
            +
                  '©'  => '\(co',
         | 
| 248 | 
            +
                  '”' => '\(rs',
         | 
| 249 | 
            +
                  '—' => '\(em',
         | 
| 250 | 
            +
                  '®'   => '\(rg',
         | 
| 251 | 
            +
                  '&sec;'   => '\(sc',
         | 
| 252 | 
            +
                  '≥'    => '\(>=',
         | 
| 253 | 
            +
                  '≤'    => '\(<=',
         | 
| 254 | 
            +
                  '≠'    => '\(!=',
         | 
| 255 | 
            +
                  '≡' => '\(=='
         | 
| 256 | 
            +
                }
         | 
| 257 | 
            +
             | 
| 258 | 
            +
                def escape(text)
         | 
| 259 | 
            +
                  return text.to_s if text.nil? || text.empty?
         | 
| 260 | 
            +
                  ent = HTML_ROFF_ENTITIES
         | 
| 261 | 
            +
                  text = text.dup
         | 
| 262 | 
            +
                  text.gsub!(/&#x([0-9A-Fa-f]+);/) { $1.to_i(16).chr }  # hex entities
         | 
| 263 | 
            +
                  text.gsub!(/&#(\d+);/) { $1.to_i.chr }                # dec entities
         | 
| 264 | 
            +
                  text.gsub!('\\', '\e')                                # backslash
         | 
| 265 | 
            +
                  text.gsub!('...', '\|.\|.\|.')                        # ellipses
         | 
| 266 | 
            +
                  text.gsub!(/['.-]/) { |m| "\\#{m}" }                  # control chars
         | 
| 267 | 
            +
                  text.gsub!(/(&[A-Za-z]+;)/) { ent[$1] || $1 }         # named entities
         | 
| 268 | 
            +
                  text.gsub!('&',  '&')                             # amps
         | 
| 269 | 
            +
                  text
         | 
| 270 | 
            +
                end
         | 
| 271 | 
            +
             | 
| 272 | 
            +
                def quote(text)
         | 
| 273 | 
            +
                  "\"#{text.gsub(/"/, '\\"')}\""
         | 
| 274 | 
            +
                end
         | 
| 275 | 
            +
             | 
| 276 | 
            +
                # write text to output buffer
         | 
| 277 | 
            +
                def write(text)
         | 
| 278 | 
            +
                  return if text.nil? || text.empty?
         | 
| 279 | 
            +
                  # lines cannot start with a '.'. insert zero-width character before.
         | 
| 280 | 
            +
                  if text[0,2] == '\.' &&
         | 
| 281 | 
            +
                    (@buf.last && @buf.last[-1] == ?\n)
         | 
| 282 | 
            +
                    @buf << '\&'
         | 
| 283 | 
            +
                  end
         | 
| 284 | 
            +
                  @buf << text
         | 
| 285 | 
            +
                end
         | 
| 286 | 
            +
             | 
| 287 | 
            +
                # write text to output buffer on a new line.
         | 
| 288 | 
            +
                def writeln(text)
         | 
| 289 | 
            +
                  write "\n" if @buf.last && @buf.last[-1] != ?\n
         | 
| 290 | 
            +
                  write text
         | 
| 291 | 
            +
                  write "\n"
         | 
| 292 | 
            +
                end
         | 
| 293 | 
            +
             | 
| 294 | 
            +
                def comment(text)
         | 
| 295 | 
            +
                  writeln %[.\\" #{text}]
         | 
| 296 | 
            +
                end
         | 
| 297 | 
            +
             | 
| 298 | 
            +
                def warn(text, *args)
         | 
| 299 | 
            +
                  $stderr.puts "warn: #{text}" % args
         | 
| 300 | 
            +
                end
         | 
| 301 | 
            +
              end
         | 
| 302 | 
            +
            end
         |