ronn-ng 0.7.4

Sign up to get free protection for your applications and to get access to all the features.
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