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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/AUTHORS +8 -0
  3. data/CHANGES +184 -0
  4. data/INSTALLING +20 -0
  5. data/LICENSE.txt +11 -0
  6. data/README.md +113 -0
  7. data/Rakefile +163 -0
  8. data/bin/ronn +223 -0
  9. data/config.ru +15 -0
  10. data/lib/ronn.rb +50 -0
  11. data/lib/ronn/document.rb +495 -0
  12. data/lib/ronn/index.rb +183 -0
  13. data/lib/ronn/roff.rb +302 -0
  14. data/lib/ronn/server.rb +70 -0
  15. data/lib/ronn/template.rb +171 -0
  16. data/lib/ronn/template/80c.css +6 -0
  17. data/lib/ronn/template/dark.css +18 -0
  18. data/lib/ronn/template/darktoc.css +17 -0
  19. data/lib/ronn/template/default.html +41 -0
  20. data/lib/ronn/template/man.css +100 -0
  21. data/lib/ronn/template/print.css +5 -0
  22. data/lib/ronn/template/screen.css +105 -0
  23. data/lib/ronn/template/toc.css +27 -0
  24. data/lib/ronn/utils.rb +55 -0
  25. data/man/index.html +78 -0
  26. data/man/index.txt +15 -0
  27. data/man/ronn-format.7 +201 -0
  28. data/man/ronn-format.7.ronn +157 -0
  29. data/man/ronn.1 +325 -0
  30. data/man/ronn.1.ronn +306 -0
  31. data/ronn-ng.gemspec +97 -0
  32. data/test/angle_bracket_syntax.html +18 -0
  33. data/test/angle_bracket_syntax.ronn +12 -0
  34. data/test/basic_document.html +9 -0
  35. data/test/basic_document.ronn +4 -0
  36. data/test/contest.rb +68 -0
  37. data/test/custom_title_document.html +6 -0
  38. data/test/custom_title_document.ronn +5 -0
  39. data/test/definition_list_syntax.html +21 -0
  40. data/test/definition_list_syntax.roff +26 -0
  41. data/test/definition_list_syntax.ronn +18 -0
  42. data/test/dots_at_line_start_test.roff +10 -0
  43. data/test/dots_at_line_start_test.ronn +4 -0
  44. data/test/entity_encoding_test.html +35 -0
  45. data/test/entity_encoding_test.roff +61 -0
  46. data/test/entity_encoding_test.ronn +25 -0
  47. data/test/index.txt +8 -0
  48. data/test/markdown_syntax.html +957 -0
  49. data/test/markdown_syntax.roff +1467 -0
  50. data/test/markdown_syntax.ronn +881 -0
  51. data/test/middle_paragraph.html +15 -0
  52. data/test/middle_paragraph.roff +13 -0
  53. data/test/middle_paragraph.ronn +10 -0
  54. data/test/missing_spaces.roff +9 -0
  55. data/test/missing_spaces.ronn +2 -0
  56. data/test/pre_block_with_quotes.roff +13 -0
  57. data/test/pre_block_with_quotes.ronn +6 -0
  58. data/test/section_reference_links.html +17 -0
  59. data/test/section_reference_links.roff +10 -0
  60. data/test/section_reference_links.ronn +12 -0
  61. data/test/test_ronn.rb +110 -0
  62. data/test/test_ronn_document.rb +186 -0
  63. data/test/test_ronn_index.rb +73 -0
  64. data/test/titleless_document.html +10 -0
  65. data/test/titleless_document.ronn +3 -0
  66. data/test/underline_spacing_test.roff +21 -0
  67. data/test/underline_spacing_test.ronn +11 -0
  68. metadata +176 -0
data/bin/ronn ADDED
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env ruby
2
+ #/ Usage: ronn <options> <file>...
3
+ #/ ronn -m|--man <file>
4
+ #/ ronn -S|--server <file> ...
5
+ #/ ronn --pipe [<file>...]
6
+ #/ Convert ronn source <file>s to roff or HTML manpage. In the first synopsis form,
7
+ #/ build HTML and roff output files based on the input file names.
8
+ #/
9
+ #/ Mode options alter the default behavior of generating files:
10
+ #/ --pipe write to standard output instead of generating files
11
+ #/ -m, --man show manual like with man(1)
12
+ #/ -S, --server serve <file>s at http://localhost:1207/
13
+ #/
14
+ #/ Format options control which files / formats are generated:
15
+ #/ -r, --roff generate roff output
16
+ #/ -5, --html generate entire HTML page with layout
17
+ #/ -f, --fragment generate HTML fragment
18
+ #/ --markdown generate post-processed markdown output
19
+ #/
20
+ #/ Document attributes:
21
+ #/ --date=<date> published date in YYYY-MM-DD format (bottom-center)
22
+ #/ --manual=<name> name of the manual (top-center)
23
+ #/ --organization=<name> publishing group or individual (bottom-left)
24
+ #/
25
+ #/ Misc options:
26
+ #/ -w, --warnings show troff warnings on stderr
27
+ #/ -W disable previously enabled troff warnings
28
+ #/ --version show ronn version and exit
29
+ #/ --help show this help message
30
+ #/
31
+ #/ A <file> named example.1.ronn generates example.1.html (HTML manpage)
32
+ #/ and example.1 (roff manpage) by default.
33
+ require 'date'
34
+ require 'optparse'
35
+
36
+ def usage
37
+ puts File.readlines(__FILE__).
38
+ grep(/^#\/.*/).
39
+ map { |line| line.chomp[3..-1] }.
40
+ join("\n")
41
+ end
42
+
43
+ ##
44
+ # Libraries and LOAD_PATH shenanigans
45
+
46
+ begin
47
+ require 'rdiscount'
48
+ require 'hpricot'
49
+ require 'ronn'
50
+ rescue LoadError => boom
51
+ if boom.to_s =~ /ronn/
52
+ libdir = File.expand_path("../../lib", __FILE__).sub(%r|^#{Dir.pwd}/|, './')
53
+ if File.directory?(libdir) && !$:.include?(libdir)
54
+ warn "warn: #{boom}. adding #{libdir} to RUBYLIB ..."
55
+ $:.unshift libdir
56
+ retry
57
+ end
58
+ elsif !defined?(Gem)
59
+ warn "warn: #{boom}. loading rubygems ..."
60
+ require 'rubygems'
61
+ retry
62
+ end
63
+ abort boom.to_s
64
+ end
65
+
66
+ ##
67
+ # Argument defaults
68
+
69
+ build = true
70
+ view = false
71
+ server = false
72
+ formats = nil
73
+ options = {}
74
+ write_index = false
75
+ styles = %w[man]
76
+ groff = "groff -Wall -mtty-char -mandoc -Tascii"
77
+ pager = ENV['MANPAGER'] || ENV['PAGER'] || 'more'
78
+
79
+ ##
80
+ # Environment variables
81
+
82
+ %w[manual organization date].each do |attribute|
83
+ value = ENV["RONN_#{attribute.upcase}"]
84
+ next if value.nil? or value.empty?
85
+ options[attribute] = value
86
+ end
87
+
88
+ ##
89
+ # Argument parsing
90
+
91
+ ARGV.options do |argv|
92
+ # modes
93
+ argv.on("--pipe") { build = server = false }
94
+ argv.on("-b", "--build") { build = true; server = false }
95
+ argv.on("-m", "--man") { build = server = false; view = true }
96
+ argv.on("-S", "--server") { build = view = false; server = true }
97
+ argv.on("-i", "--index") { write_index = true }
98
+
99
+ # format options
100
+ argv.on("-r", "--roff") { (formats ||= []) << 'roff' }
101
+ argv.on("-5", "--html") { (formats ||= []) << 'html' }
102
+ argv.on("-f", "--fragment") { (formats ||= []) << 'html_fragment' }
103
+ argv.on("--markdown") { (formats ||= []) << 'markdown' }
104
+
105
+ # html output options
106
+ argv.on("-s", "--style=V") { |val| styles += val.split(/[, \n]+/) }
107
+
108
+ # manual attribute options
109
+ %w[name section manual organization date].each do |attribute|
110
+ argv.on("--#{attribute}=VALUE") { |val| options[attribute] = val }
111
+ end
112
+
113
+ # misc
114
+ argv.on("-w", "--warnings") { groff += ' -ww' }
115
+ argv.on("-W") { groff += ' -Ww' }
116
+ argv.on("-v", "--version") do
117
+ require 'ronn'
118
+ if Ronn.release?
119
+ printf "Ronn v%s\n", Ronn::VERSION
120
+ else
121
+ printf "Ronn v%s (%s)\n", Ronn::VERSION, Ronn::REV
122
+ end
123
+ printf "http://github.com/rtomayko/ronn/tree/%s\n", Ronn.revision
124
+ exit 0
125
+ end
126
+ argv.on_tail("--help") { usage ; exit 0 }
127
+ argv.parse!
128
+ end
129
+
130
+ ##
131
+ # Modes, Formats, Options
132
+
133
+ case
134
+ when ARGV.empty? && $stdin.tty?
135
+ usage
136
+ exit 2
137
+ when ARGV.empty? && !server
138
+ ARGV.push '-'
139
+ build = false
140
+ formats ||= %w[roff]
141
+ when view
142
+ formats ||= %w[roff]
143
+ when build
144
+ formats ||= %w[roff html]
145
+ end
146
+ formats ||= []
147
+ formats.delete('html') if formats.include?('html_fragment')
148
+
149
+ options['date'] &&= Date.strptime(options['date'], '%Y-%m-%d')
150
+ options['styles'] = styles
151
+
152
+ ##
153
+ # Server
154
+
155
+ if server
156
+ require 'ronn/server'
157
+ Ronn::Server.run(ARGV, options)
158
+ exit 0
159
+ end
160
+
161
+ ##
162
+ # Build Pipeline
163
+
164
+ pid = nil
165
+ wr = STDOUT
166
+ documents = ARGV.map { |file| Ronn::Document.new(file, options) }
167
+ documents.each do |doc|
168
+ # setup the man pipeline if the --man option was specified
169
+ if view && !build
170
+ rd, wr = IO.pipe
171
+ if pid = fork
172
+ rd.close
173
+ else
174
+ wr.close
175
+ STDIN.reopen rd
176
+ exec "#{groff} | #{pager}"
177
+ end
178
+ end
179
+
180
+ # write output for each format
181
+ formats.each do |format|
182
+ if build
183
+ path = doc.path_for(format)
184
+ case format
185
+ when 'html'
186
+ warn "%9s: %-43s%15s" % [format, path, '+' + doc.styles.join(',')]
187
+ when 'roff', 'html_fragment', 'markdown'
188
+ warn "%9s: %-43s" % [format, path]
189
+ end
190
+
191
+ output = doc.convert(format)
192
+ File.open(path, 'wb') { |f| f.puts(output) }
193
+
194
+ if format == 'roff'
195
+ if view
196
+ system "man #{path}"
197
+ else
198
+ system "#{groff} <#{path} >/dev/null"
199
+ end
200
+ end
201
+ else
202
+ output = doc.convert(format)
203
+ wr.puts(output)
204
+ end
205
+ end
206
+
207
+ # wait for children to exit
208
+ if pid
209
+ wr.close
210
+ Process.wait
211
+ end
212
+ end
213
+
214
+ # Write index.txt files
215
+
216
+ if write_index
217
+ indexes = documents.map { |doc| doc.index }.uniq
218
+ indexes.each do |index|
219
+ File.open(index.path, 'wb') do |fd|
220
+ fd.puts(index.to_text)
221
+ end
222
+ end
223
+ end
data/config.ru ADDED
@@ -0,0 +1,15 @@
1
+ #\ -p 1207
2
+ $: << File.expand_path('../lib', __FILE__)
3
+
4
+ require 'ronn'
5
+ require 'ronn/server'
6
+
7
+ # use Rack::Lint
8
+
9
+ options = {
10
+ :styles => %w[man toc],
11
+ :organization => "Ronn v#{Ronn::VERSION}"
12
+ }
13
+ files = Dir['man/*.ronn'] + Dir['test/*.ronn']
14
+
15
+ run Ronn::Server.new(files, options)
data/lib/ronn.rb ADDED
@@ -0,0 +1,50 @@
1
+ # Ronn is a humane text format and toolchain for authoring manpages (and
2
+ # things that appear as manpages from a distance). Use it to build /
3
+ # install standard Unix roff(7) formatted manpages or to generate
4
+ # beautiful HTML manpages.
5
+ module Ronn
6
+ autoload :Document, 'ronn/document'
7
+ autoload :Index, 'ronn/index'
8
+ autoload :Template, 'ronn/template'
9
+ autoload :Roff, 'ronn/roff'
10
+ autoload :Server, 'ronn/server'
11
+
12
+ # Create a new Ronn::Document for the given ronn file. See
13
+ # Ronn::Document.new for usage information.
14
+ def self.new(filename, attributes={}, &block)
15
+ Document.new(filename, attributes, &block)
16
+ end
17
+
18
+ # truthy when this a release (\d.\d.\d) version.
19
+ def self.release?
20
+ revision != '' && !revision.include?('-')
21
+ end
22
+
23
+ # version: 0.6.11
24
+ #
25
+ # A semantic version number based on the git revision. The third element
26
+ # of the version is incremented by the commit offset, such that version
27
+ # 0.6.6-5-gdacd74b => 0.6.11
28
+ def self.version
29
+ ver = revision[/^[0-9.-]+/].split(/[.-]/).map { |p| p.to_i }
30
+ ver[2] += ver.pop while ver.size > 3
31
+ ver.join('.')
32
+ end
33
+
34
+ # revision: 0.6.6-5-gdacd74b
35
+ # revision: 0.6.25
36
+ #
37
+ # The string revision as reported by: git-describe --tags. This is just the
38
+ # tag name when a tag references the HEAD commit (0.6.25). When the HEAD
39
+ # commit is not tagged, this is a "<tag>-<offset>-<sha1>" string:
40
+ # <tag> - closest tag name
41
+ # <offset> - number of commits ahead of <tag>
42
+ # <sha1> - 7c short SHA1 for HEAD
43
+ def self.revision
44
+ REV
45
+ end
46
+
47
+ # value generated by: rake rev
48
+ REV = '0.7.4'
49
+ VERSION = version
50
+ end
@@ -0,0 +1,495 @@
1
+ require 'time'
2
+ require 'cgi'
3
+ require 'hpricot'
4
+ require 'rdiscount'
5
+ require 'ronn/index'
6
+ require 'ronn/roff'
7
+ require 'ronn/template'
8
+ require 'ronn/utils'
9
+
10
+ module Ronn
11
+ # The Document class can be used to load and inspect a ronn document
12
+ # and to convert a ronn document into other formats, like roff or
13
+ # HTML.
14
+ #
15
+ # Ronn files may optionally follow the naming convention:
16
+ # "<name>.<section>.ronn". The <name> and <section> are used in
17
+ # generated documentation unless overridden by the information
18
+ # extracted from the document's name section.
19
+ class Document
20
+ include Ronn::Utils
21
+
22
+ # Path to the Ronn document. This may be '-' or nil when the Ronn::Document
23
+ # object is created with a stream.
24
+ attr_reader :path
25
+
26
+ # The raw input data, read from path or stream and unmodified.
27
+ attr_reader :data
28
+
29
+ # The index used to resolve man and file references.
30
+ attr_accessor :index
31
+
32
+ # The man pages name: usually a single word name of
33
+ # a program or filename; displayed along with the section in
34
+ # the left and right portions of the header as well as the bottom
35
+ # right section of the footer.
36
+ attr_accessor :name
37
+
38
+ # The man page's section: a string whose first character
39
+ # is numeric; displayed in parenthesis along with the name.
40
+ attr_accessor :section
41
+
42
+ # Single sentence description of the thing being described
43
+ # by this man page; displayed in the NAME section.
44
+ attr_accessor :tagline
45
+
46
+ # The manual this document belongs to; center displayed in
47
+ # the header.
48
+ attr_accessor :manual
49
+
50
+ # The name of the group, organization, or individual responsible
51
+ # for this document; displayed in the left portion of the footer.
52
+ attr_accessor :organization
53
+
54
+ # The date the document was published; center displayed in
55
+ # the document footer.
56
+ attr_accessor :date
57
+
58
+ # Array of style modules to apply to the document.
59
+ attr_accessor :styles
60
+
61
+ # Create a Ronn::Document given a path or with the data returned by
62
+ # calling the block. The document is loaded and preprocessed before
63
+ # the intialize method returns. The attributes hash may contain values
64
+ # for any writeable attributes defined on this class.
65
+ def initialize(path=nil, attributes={}, &block)
66
+ @path = path
67
+ @basename = path.to_s =~ /^-?$/ ? nil : File.basename(path)
68
+ @reader = block ||
69
+ lambda do |f|
70
+ if ['-', nil].include?(f)
71
+ STDIN.read
72
+ else
73
+ File.read(f)
74
+ end
75
+ end
76
+ @data = @reader.call(path)
77
+ @name, @section, @tagline = sniff
78
+
79
+ @styles = %w[man]
80
+ @manual, @organization, @date = nil
81
+ @markdown, @input_html, @html = nil
82
+ @index = Ronn::Index[path || '.']
83
+ @index.add_manual(self) if path && name
84
+
85
+ attributes.each { |attr_name,value| send("#{attr_name}=", value) }
86
+ end
87
+
88
+ # Generate a file basename of the form "<name>.<section>.<type>"
89
+ # for the given file extension. Uses the name and section from
90
+ # the source file path but falls back on the name and section
91
+ # defined in the document.
92
+ def basename(type=nil)
93
+ type = nil if ['', 'roff'].include?(type.to_s)
94
+ [path_name || @name, path_section || @section, type].
95
+ compact.join('.')
96
+ end
97
+
98
+ # Construct a path for a file near the source file. Uses the
99
+ # Document#basename method to generate the basename part and
100
+ # appends it to the dirname of the source document.
101
+ def path_for(type=nil)
102
+ if @basename
103
+ File.join(File.dirname(path), basename(type))
104
+ else
105
+ basename(type)
106
+ end
107
+ end
108
+
109
+ # Returns the <name> part of the path, or nil when no path is
110
+ # available. This is used as the manual page name when the
111
+ # file contents do not include a name section.
112
+ def path_name
113
+ @basename[/^[^.]+/] if @basename
114
+ end
115
+
116
+ # Returns the <section> part of the path, or nil when
117
+ # no path is available.
118
+ def path_section
119
+ $1 if @basename.to_s =~ /\.(\d\w*)\./
120
+ end
121
+
122
+ # Returns the manual page name based first on the document's
123
+ # contents and then on the path name.
124
+ def name
125
+ @name || path_name
126
+ end
127
+
128
+ # Truthful when the name was extracted from the name section
129
+ # of the document.
130
+ def name?
131
+ !@name.nil?
132
+ end
133
+
134
+ # Returns the manual page section based first on the document's
135
+ # contents and then on the path name.
136
+ def section
137
+ @section || path_section
138
+ end
139
+
140
+ # True when the section number was extracted from the name
141
+ # section of the document.
142
+ def section?
143
+ !@section.nil?
144
+ end
145
+
146
+ # The name used to reference this manual.
147
+ def reference_name
148
+ name + (section && "(#{section})").to_s
149
+ end
150
+
151
+ # Truthful when the document started with an h1 but did not follow
152
+ # the "<name>(<sect>) -- <tagline>" convention. We assume this is some kind
153
+ # of custom title.
154
+ def title?
155
+ !name? && tagline
156
+ end
157
+
158
+ # The document's title when no name section was defined. When a name section
159
+ # exists, this value is nil.
160
+ def title
161
+ @tagline if !name?
162
+ end
163
+
164
+ # The date the man page was published. If not set explicitly,
165
+ # this is the file's modified time or, if no file is given,
166
+ # the current time.
167
+ def date
168
+ return @date if @date
169
+ return File.mtime(path) if File.exist?(path)
170
+ Time.now
171
+ end
172
+
173
+ # Retrieve a list of top-level section headings in the document and return
174
+ # as an array of +[id, text]+ tuples, where +id+ is the element's generated
175
+ # id and +text+ is the inner text of the heading element.
176
+ def toc
177
+ @toc ||=
178
+ html.search('h2[@id]').map { |h2| [h2.attributes['id'], h2.inner_text] }
179
+ end
180
+ alias section_heads toc
181
+
182
+ # Styles to insert in the generated HTML output. This is a simple Array of
183
+ # string module names or file paths.
184
+ def styles=(styles)
185
+ @styles = (%w[man] + styles).uniq
186
+ end
187
+
188
+ # Sniff the document header and extract basic document metadata. Return a
189
+ # tuple of the form: [name, section, description], where missing information
190
+ # is represented by nil and any element may be missing.
191
+ def sniff
192
+ html = Markdown.new(data[0, 512]).to_html
193
+ heading, html = html.split("</h1>\n", 2)
194
+ return [nil, nil, nil] if html.nil?
195
+
196
+ case heading
197
+ when /([\w_.\[\]~+=@:-]+)\s*\((\d\w*)\)\s*-+\s*(.*)/
198
+ # name(section) -- description
199
+ [$1, $2, $3]
200
+ when /([\w_.\[\]~+=@:-]+)\s+-+\s+(.*)/
201
+ # name -- description
202
+ [$1, nil, $2]
203
+ else
204
+ # description
205
+ [nil, nil, heading.sub('<h1>', '')]
206
+ end
207
+ end
208
+
209
+ # Preprocessed markdown input text.
210
+ def markdown
211
+ @markdown ||= process_markdown!
212
+ end
213
+
214
+ # A Hpricot::Document for the manual content fragment.
215
+ def html
216
+ @html ||= process_html!
217
+ end
218
+
219
+ # Convert the document to :roff, :html, or :html_fragment and
220
+ # return the result as a string.
221
+ def convert(format)
222
+ send "to_#{format}"
223
+ end
224
+
225
+ # Convert the document to roff and return the result as a string.
226
+ def to_roff
227
+ RoffFilter.new(
228
+ to_html_fragment(wrap_class=nil),
229
+ name, section, tagline,
230
+ manual, organization, date
231
+ ).to_s
232
+ end
233
+
234
+ # Convert the document to HTML and return the result as a string.
235
+ def to_html
236
+ if layout = ENV['RONN_LAYOUT']
237
+ if !File.exist?(layout_path = File.expand_path(layout))
238
+ warn "warn: can't find #{layout}, using default layout."
239
+ layout_path = nil
240
+ end
241
+ end
242
+
243
+ template = Ronn::Template.new(self)
244
+ template.context.push :html => to_html_fragment(wrap_class=nil)
245
+ template.render(layout_path || 'default')
246
+ end
247
+
248
+ # Convert the document to HTML and return the result
249
+ # as a string. The HTML does not include <html>, <head>,
250
+ # or <style> tags.
251
+ def to_html_fragment(wrap_class='mp')
252
+ return html.to_s if wrap_class.nil?
253
+ [
254
+ "<div class='#{wrap_class}'>",
255
+ html.to_s,
256
+ "</div>"
257
+ ].join("\n")
258
+ end
259
+
260
+ def to_markdown
261
+ markdown
262
+ end
263
+
264
+ def to_h
265
+ %w[name section tagline manual organization date styles toc].
266
+ inject({}) { |hash, name| hash[name] = send(name); hash }
267
+ end
268
+
269
+ def to_yaml
270
+ require 'yaml'
271
+ to_h.to_yaml
272
+ end
273
+
274
+ def to_json
275
+ require 'json'
276
+ to_h.merge('date' => date.iso8601).to_json
277
+ end
278
+
279
+ protected
280
+ ##
281
+ # Document Processing
282
+
283
+ # Parse the document and extract the name, section, and tagline from its
284
+ # contents. This is called while the object is being initialized.
285
+ def preprocess!
286
+ input_html
287
+ nil
288
+ end
289
+
290
+ def input_html
291
+ @input_html ||= strip_heading(Markdown.new(markdown).to_html)
292
+ end
293
+
294
+ def strip_heading(html)
295
+ heading, html = html.split("</h1>\n", 2)
296
+ html || heading
297
+ end
298
+
299
+ def process_markdown!
300
+ markdown = markdown_filter_heading_anchors(self.data)
301
+ markdown_filter_link_index(markdown)
302
+ markdown_filter_angle_quotes(markdown)
303
+ end
304
+
305
+ def process_html!
306
+ @html = Hpricot(input_html)
307
+ html_filter_angle_quotes
308
+ html_filter_definition_lists
309
+ html_filter_inject_name_section
310
+ html_filter_heading_anchors
311
+ html_filter_annotate_bare_links
312
+ html_filter_manual_reference_links
313
+ @html
314
+ end
315
+
316
+ ##
317
+ # Filters
318
+
319
+ # Appends all index links to the end of the document as Markdown reference
320
+ # links. This lets us use [foo(3)][] syntax to link to index entries.
321
+ def markdown_filter_link_index(markdown)
322
+ return markdown if index.nil? || index.empty?
323
+ markdown << "\n\n"
324
+ index.each { |ref| markdown << "[#{ref.name}]: #{ref.url}\n" }
325
+ end
326
+
327
+ # Add [id]: #ANCHOR elements to the markdown source text for all sections.
328
+ # This lets us use the [SECTION-REF][] syntax
329
+ def markdown_filter_heading_anchors(markdown)
330
+ first = true
331
+ markdown.split("\n").grep(/^[#]{2,5} +[\w '-]+[# ]*$/).each do |line|
332
+ markdown << "\n\n" if first
333
+ first = false
334
+ title = line.gsub(/[^\w -]/, '').strip
335
+ anchor = title.gsub(/\W+/, '-').gsub(/(^-+|-+$)/, '')
336
+ markdown << "[#{title}]: ##{anchor} \"#{title}\"\n"
337
+ end
338
+ markdown
339
+ end
340
+
341
+ # Convert <WORD> to <var>WORD</var> but only if WORD isn't an HTML tag.
342
+ def markdown_filter_angle_quotes(markdown)
343
+ markdown.gsub(/\<([^:.\/]+?)\>/) do |match|
344
+ contents = $1
345
+ tag, attrs = contents.split(' ', 2)
346
+ if attrs =~ /\/=/ || html_element?(tag.sub(/^\//, '')) ||
347
+ data.include?("</#{tag}>")
348
+ match.to_s
349
+ else
350
+ "<var>#{contents}</var>"
351
+ end
352
+ end
353
+ end
354
+
355
+ # Perform angle quote (<THESE>) post filtering.
356
+ def html_filter_angle_quotes
357
+ # convert all angle quote vars nested in code blocks
358
+ # back to the original text
359
+ @html.search('code').search('text()').each do |node|
360
+ next unless node.to_html.include?('var&gt;')
361
+ new =
362
+ node.to_html.
363
+ gsub('&lt;var&gt;', '&lt;').
364
+ gsub("&lt;/var&gt;", '>')
365
+ node.swap(new)
366
+ end
367
+ end
368
+
369
+ # Convert special format unordered lists to definition lists.
370
+ def html_filter_definition_lists
371
+ # process all unordered lists depth-first
372
+ @html.search('ul').to_a.reverse.each do |ul|
373
+ items = ul.search('li')
374
+ next if items.any? { |item| item.inner_text.split("\n", 2).first !~ /:$/ }
375
+
376
+ ul.name = 'dl'
377
+ items.each do |item|
378
+ if child = item.at('p')
379
+ wrap = '<p></p>'
380
+ container = child
381
+ else
382
+ wrap = '<dd></dd>'
383
+ container = item
384
+ end
385
+ term, definition = container.inner_html.split(":\n", 2)
386
+
387
+ dt = item.before("<dt>#{term}</dt>").first
388
+ dt.attributes['class'] = 'flush' if dt.inner_text.length <= 7
389
+
390
+ item.name = 'dd'
391
+ container.swap(wrap.sub(/></, ">#{definition}<"))
392
+ end
393
+ end
394
+ end
395
+
396
+ def html_filter_inject_name_section
397
+ markup =
398
+ if title?
399
+ "<h1>#{title}</h1>"
400
+ elsif name
401
+ "<h2>NAME</h2>\n" +
402
+ "<p class='man-name'>\n <code>#{name}</code>" +
403
+ (tagline ? " - <span class='man-whatis'>#{tagline}</span>\n" : "\n") +
404
+ "</p>\n"
405
+ end
406
+ if markup
407
+ if @html.children
408
+ @html.at("*").before(markup)
409
+ else
410
+ @html = Hpricot(markup)
411
+ end
412
+ end
413
+ end
414
+
415
+ # Add URL anchors to all HTML heading elements.
416
+ def html_filter_heading_anchors
417
+ @html.search('h2|h3|h4|h5|h6').not('[@id]').each do |heading|
418
+ heading.set_attribute('id', heading.inner_text.gsub(/\W+/, '-'))
419
+ end
420
+ end
421
+
422
+ # Add a 'data-bare-link' attribute to hyperlinks
423
+ # whose text labels are the same as their href URLs.
424
+ def html_filter_annotate_bare_links
425
+ @html.search('a[@href]').each do |node|
426
+ href = node.attributes['href']
427
+ text = node.inner_text
428
+
429
+ if href == text ||
430
+ href[0] == ?# ||
431
+ CGI.unescapeHTML(href) == "mailto:#{CGI.unescapeHTML(text)}"
432
+ then
433
+ node.set_attribute('data-bare-link', 'true')
434
+ end
435
+ end
436
+ end
437
+
438
+ # Convert text of the form "name(section)" or "<code>name</code>(section)
439
+ # to a hyperlink. The URL is obtained from the index.
440
+ def html_filter_manual_reference_links
441
+ return if index.nil?
442
+ name_pattern = "[0-9A-Za-z_:.+=@~-]+"
443
+
444
+ # Convert "name(section)" by traversing text nodes searching for
445
+ # text that fits the pattern. This is the original implementation.
446
+ @html.search('text()').each do |node|
447
+ next if !node.content.include?(')')
448
+ next if %w[pre code h1 h2 h3].include?(node.parent.name)
449
+ next if child_of?(node, 'a')
450
+ node.swap(node.content.gsub(/(#{name_pattern})(\(\d+\w*\))/) {
451
+ html_build_manual_reference_link($1, $2)
452
+ })
453
+ end
454
+
455
+ # Convert "<code>name</code>(section)" by traversing <code> nodes.
456
+ # For each one that contains exactly an acceptable manual page name,
457
+ # the next sibling is checked and must be a text node beginning
458
+ # with a valid section in parentheses.
459
+ @html.search('code').each do |node|
460
+ next if %w[pre code h1 h2 h3].include?(node.parent.name)
461
+ next if child_of?(node, 'a')
462
+ next unless node.inner_text =~ /^#{name_pattern}$/
463
+ sibling = node.next
464
+ if sibling
465
+ next unless sibling.text?
466
+ next unless sibling.content =~ /^\((\d+\w*)\)/
467
+ node.swap(html_build_manual_reference_link(node, "(#{$1})"))
468
+ sibling.content = sibling.content.gsub(/^\(\d+\w*\)/, '')
469
+ end
470
+ end
471
+
472
+ end
473
+
474
+ # HTMLize the manual page reference. The result is an <a> if the
475
+ # page appears in the index, otherwise it is a <span>. The first
476
+ # argument may be an HTML element or a string. The second should
477
+ # be a string of the form "(#{section})".
478
+ def html_build_manual_reference_link(name_or_node, section)
479
+ name = if name_or_node.respond_to?(:inner_text)
480
+ name_or_node.inner_text
481
+ else
482
+ name_or_node
483
+ end
484
+ if ref = index["#{name}#{section}"]
485
+ "<a class='man-ref' href='#{ref.url}'>#{name_or_node
486
+ }<span class='s'>#{section}</span></a>"
487
+ else
488
+ # warn "warn: manual reference not defined: '#{name}#{section}'"
489
+ "<span class='man-ref'>#{name_or_node}<span class='s'>#{section
490
+ }</span></span>"
491
+ end
492
+ end
493
+
494
+ end
495
+ end