ronn 0.6.6 → 0.7.0

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 (46) hide show
  1. data/CHANGES +34 -0
  2. data/INSTALLING +18 -0
  3. data/README.md +43 -69
  4. data/Rakefile +9 -10
  5. data/bin/ronn +66 -49
  6. data/config.ru +1 -1
  7. data/lib/ronn.rb +35 -9
  8. data/lib/ronn/document.rb +239 -135
  9. data/lib/ronn/index.rb +183 -0
  10. data/lib/ronn/roff.rb +48 -28
  11. data/lib/ronn/template.rb +22 -8
  12. data/lib/ronn/template/dark.css +1 -4
  13. data/lib/ronn/template/default.html +0 -2
  14. data/lib/ronn/template/man.css +12 -12
  15. data/lib/ronn/utils.rb +8 -0
  16. data/man/index.html +78 -0
  17. data/man/index.txt +15 -0
  18. data/man/{ronn.5 → ronn-format.7} +26 -30
  19. data/man/{ronn.5.ronn → ronn-format.7.ronn} +39 -39
  20. data/man/ronn.1 +47 -15
  21. data/man/ronn.1.ronn +53 -23
  22. data/ronn.gemspec +14 -8
  23. data/test/angle_bracket_syntax.html +4 -2
  24. data/test/basic_document.html +4 -2
  25. data/test/custom_title_document.html +1 -2
  26. data/test/definition_list_syntax.html +4 -2
  27. data/test/dots_at_line_start_test.roff +10 -0
  28. data/test/dots_at_line_start_test.ronn +4 -0
  29. data/test/entity_encoding_test.html +24 -2
  30. data/test/entity_encoding_test.roff +41 -1
  31. data/test/entity_encoding_test.ronn +17 -0
  32. data/test/index.txt +8 -0
  33. data/test/markdown_syntax.html +5 -3
  34. data/test/markdown_syntax.roff +4 -4
  35. data/test/middle_paragraph.html +4 -2
  36. data/test/missing_spaces.roff +3 -0
  37. data/test/section_reference_links.html +4 -2
  38. data/test/{ronn_test.rb → test_ronn.rb} +18 -5
  39. data/test/{document_test.rb → test_ronn_document.rb} +59 -8
  40. data/test/test_ronn_index.rb +73 -0
  41. data/test/titleless_document.html +7 -2
  42. data/test/titleless_document.ronn +3 -2
  43. data/test/underline_spacing_test.roff +5 -0
  44. metadata +30 -14
  45. data/man/ronn.7 +0 -168
  46. data/man/ronn.7.ronn +0 -120
data/config.ru CHANGED
@@ -1,5 +1,5 @@
1
1
  #\ -p 1207
2
- $: << File.expand_path(__FILE__, '../lib')
2
+ $: << File.expand_path('../lib', __FILE__)
3
3
 
4
4
  require 'ronn'
5
5
  require 'ronn/server'
@@ -3,22 +3,48 @@
3
3
  # install standard UNIX roff(7) formatted manpages or to generate
4
4
  # beautiful HTML manpages.
5
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
+
6
12
  # Create a new Ronn::Document for the given ronn file. See
7
13
  # Ronn::Document.new for usage information.
8
14
  def self.new(filename, attributes={}, &block)
9
15
  Document.new(filename, attributes, &block)
10
16
  end
11
17
 
12
- # bring REV up to date with: rake rev
13
- REV = '0.6-6-gd7645f2'
14
- VERSION = REV[/(?:[\d.]+)(?:-\d+)?/].tr('-', '.')
15
-
18
+ # truthy when this a release (\d.\d.\d) version.
16
19
  def self.release?
17
- REV != '' && !REV.include?('-')
20
+ revision != '' && !revision.include?('-')
18
21
  end
19
22
 
20
- autoload :Document, 'ronn/document'
21
- autoload :Roff, 'ronn/roff'
22
- autoload :Server, 'ronn/server'
23
- autoload :Template, 'ronn/template'
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.0'
49
+ VERSION = version
24
50
  end
@@ -1,4 +1,4 @@
1
- require 'set'
1
+ require 'time'
2
2
  require 'cgi'
3
3
  require 'hpricot'
4
4
  require 'rdiscount'
@@ -18,7 +18,15 @@ module Ronn
18
18
  class Document
19
19
  include Ronn::Utils
20
20
 
21
- attr_reader :path, :data
21
+ # Path to the Ronn document. This may be '-' or nil when the Ronn::Document
22
+ # object is created with a stream.
23
+ attr_reader :path
24
+
25
+ # The raw input data, read from path or stream and unmodified.
26
+ attr_reader :data
27
+
28
+ # The index used to resolve man and file references.
29
+ attr_accessor :index
22
30
 
23
31
  # The man pages name: usually a single word name of
24
32
  # a program or filename; displayed along with the section in
@@ -56,12 +64,22 @@ module Ronn
56
64
  def initialize(path=nil, attributes={}, &block)
57
65
  @path = path
58
66
  @basename = path.to_s =~ /^-?$/ ? nil : File.basename(path)
59
- @reader = block || Proc.new { |f| File.read(f) }
67
+ @reader = block ||
68
+ lambda do |f|
69
+ if ['-', nil].include?(f)
70
+ STDIN.read
71
+ else
72
+ File.read(f)
73
+ end
74
+ end
60
75
  @data = @reader.call(path)
61
- @name, @section, @tagline = nil
62
- @manual, @organization, @date = nil
63
- @fragment = preprocess
76
+ @name, @section, @tagline = sniff
77
+
64
78
  @styles = %w[man]
79
+ @manual, @organization, @date = nil
80
+ @markdown, @input_html, @html = nil
81
+ @index = Ronn::Index[path || '.']
82
+ @index.add_manual(self) if path && name
65
83
 
66
84
  attributes.each { |attr_name,value| send("#{attr_name}=", value) }
67
85
  end
@@ -109,7 +127,7 @@ module Ronn
109
127
  # Truthful when the name was extracted from the name section
110
128
  # of the document.
111
129
  def name?
112
- !name.nil?
130
+ !@name.nil?
113
131
  end
114
132
 
115
133
  # Returns the manual page section based first on the document's
@@ -121,7 +139,25 @@ module Ronn
121
139
  # True when the section number was extracted from the name
122
140
  # section of the document.
123
141
  def section?
124
- !section.nil?
142
+ !@section.nil?
143
+ end
144
+
145
+ # The name used to reference this manual.
146
+ def reference_name
147
+ name + (section && "(#{section})").to_s
148
+ end
149
+
150
+ # Truthful when the document started with an h1 but did not follow
151
+ # the "<name>(<sect>) -- <tagline>" convention. We assume this is some kind
152
+ # of custom title.
153
+ def title?
154
+ !name? && tagline
155
+ end
156
+
157
+ # The document's title when no name section was defined. When a name section
158
+ # exists, this value is nil.
159
+ def title
160
+ @tagline if !name?
125
161
  end
126
162
 
127
163
  # The date the man page was published. If not set explicitly,
@@ -136,11 +172,11 @@ module Ronn
136
172
  # Retrieve a list of top-level section headings in the document and return
137
173
  # as an array of +[id, text]+ tuples, where +id+ is the element's generated
138
174
  # id and +text+ is the inner text of the heading element.
139
- def section_heads
140
- parse_html(to_html_fragment).search('h2[@id]').map do |heading|
141
- [heading.attributes['id'], heading.inner_text]
142
- end
175
+ def toc
176
+ @toc ||=
177
+ html.search('h2[@id]').map { |h2| [h2.attributes['id'], h2.inner_text] }
143
178
  end
179
+ alias section_heads toc
144
180
 
145
181
  # Styles to insert in the generated HTML output. This is a simple Array of
146
182
  # string module names or file paths.
@@ -148,6 +184,37 @@ module Ronn
148
184
  @styles = (%w[man] + styles).uniq
149
185
  end
150
186
 
187
+ # Sniff the document header and extract basic document metadata. Return a
188
+ # tuple of the form: [name, section, description], where missing information
189
+ # is represented by nil and any element may be missing.
190
+ def sniff
191
+ html = Markdown.new(data[0, 512]).to_html
192
+ heading, html = html.split("</h1>\n", 2)
193
+ return [nil, nil, nil] if html.nil?
194
+
195
+ case heading
196
+ when /([\w_.\[\]~+=@:-]+)\s*\((\d\w*)\)\s*-+\s*(.*)/
197
+ # name(section) -- description
198
+ [$1, $2, $3]
199
+ when /([\w_.\[\]~+=@:-]+)\s+-+\s+(.*)/
200
+ # name -- description
201
+ [$1, nil, $2]
202
+ else
203
+ # description
204
+ [nil, nil, heading.sub('<h1>', '')]
205
+ end
206
+ end
207
+
208
+ # Preprocessed markdown input text.
209
+ def markdown
210
+ @markdown ||= process_markdown!
211
+ end
212
+
213
+ # A Hpricot::Document for the manual content fragment.
214
+ def html
215
+ @html ||= process_html!
216
+ end
217
+
151
218
  # Convert the document to :roff, :html, or :html_fragment and
152
219
  # return the result as a string.
153
220
  def convert(format)
@@ -158,12 +225,8 @@ module Ronn
158
225
  def to_roff
159
226
  RoffFilter.new(
160
227
  to_html_fragment(wrap_class=nil),
161
- name,
162
- section,
163
- tagline,
164
- manual,
165
- organization,
166
- date
228
+ name, section, tagline,
229
+ manual, organization, date
167
230
  ).to_s
168
231
  end
169
232
 
@@ -177,6 +240,7 @@ module Ronn
177
240
  end
178
241
 
179
242
  template = Ronn::Template.new(self)
243
+ template.context.push :html => to_html_fragment(wrap_class=nil)
180
244
  template.render(layout_path || 'default')
181
245
  end
182
246
 
@@ -184,71 +248,127 @@ module Ronn
184
248
  # as a string. The HTML does not include <html>, <head>,
185
249
  # or <style> tags.
186
250
  def to_html_fragment(wrap_class='mp')
187
- wrap_class = nil if wrap_class.to_s.empty?
188
- buf = []
189
- buf << "<div class='#{wrap_class}'>" if wrap_class
190
- if name? && section?
191
- buf << "<h2 id='NAME'>NAME</h2>"
192
- buf << "<p><code>#{name}</code> - #{tagline}</p>"
193
- elsif tagline
194
- buf << "<h1>#{[name, tagline].compact.join(' - ')}</h1>"
195
- end
196
- buf << @fragment.to_s
197
- buf << "</div>" if wrap_class
198
- buf.join("\n")
251
+ return html.to_s if wrap_class.nil?
252
+ [
253
+ "<div class='#{wrap_class}'>",
254
+ html.to_s,
255
+ "</div>"
256
+ ].join("\n")
257
+ end
258
+
259
+ def to_markdown
260
+ markdown
261
+ end
262
+
263
+ def to_h
264
+ %w[name section tagline manual organization date styles toc].
265
+ inject({}) { |hash, name| hash[name] = send(name); hash }
266
+ end
267
+
268
+ def to_yaml
269
+ require 'yaml'
270
+ to_h.to_yaml
271
+ end
272
+
273
+ def to_json
274
+ require 'json'
275
+ to_h.merge('date' => date.iso8601).to_json
199
276
  end
200
277
 
201
278
  protected
202
- # The preprocessed markdown source text.
203
- attr_reader :markdown
279
+ ##
280
+ # Document Processing
281
+
282
+ # Parse the document and extract the name, section, and tagline from its
283
+ # contents. This is called while the object is being initialized.
284
+ def preprocess!
285
+ input_html
286
+ nil
287
+ end
204
288
 
205
- # Parse the document and extract the name, section, and tagline
206
- # from its contents. This is called while the object is being
207
- # initialized.
208
- def preprocess
209
- [
210
- :heading_anchor_pre_filter,
211
- :angle_quote_pre_filter,
212
- :markdown_filter,
213
- :angle_quote_post_filter,
214
- :definition_list_filter,
215
- :heading_anchor_filter,
216
- :annotate_bare_links_filter
217
- ].inject(data) { |res,filter| send(filter, res) }
289
+ def input_html
290
+ @input_html ||= strip_heading(Markdown.new(markdown).to_html)
218
291
  end
219
292
 
220
- # Add a 'data-bare-link' attribute to hyperlinks
221
- # whose text labels are the same as their href URLs.
222
- def annotate_bare_links_filter(html)
223
- doc = parse_html(html)
224
- doc.search('a[@href]').each do |node|
225
- href = node.attributes['href']
226
- text = node.inner_text
293
+ def strip_heading(html)
294
+ heading, html = html.split("</h1>\n", 2)
295
+ html || heading
296
+ end
227
297
 
228
- if href == text ||
229
- href[0] == ?# ||
230
- CGI.unescapeHTML(href) == "mailto:#{CGI.unescapeHTML(text)}"
231
- then
232
- node.set_attribute('data-bare-link', 'true')
298
+ def process_markdown!
299
+ markdown = markdown_filter_heading_anchors(self.data)
300
+ markdown_filter_link_index(markdown)
301
+ markdown_filter_angle_quotes(markdown)
302
+ end
303
+
304
+ def process_html!
305
+ @html = Hpricot(input_html)
306
+ html_filter_angle_quotes
307
+ html_filter_definition_lists
308
+ html_filter_inject_name_section
309
+ html_filter_heading_anchors
310
+ html_filter_annotate_bare_links
311
+ html_filter_manual_reference_links
312
+ @html
313
+ end
314
+
315
+ ##
316
+ # Filters
317
+
318
+ # Appends all index links to the end of the document as Markdown reference
319
+ # links. This lets us use [foo(3)][] syntax to link to index entries.
320
+ def markdown_filter_link_index(markdown)
321
+ return markdown if index.nil? || index.empty?
322
+ markdown << "\n\n"
323
+ index.each { |ref| markdown << "[#{ref.name}]: #{ref.url}\n" }
324
+ end
325
+
326
+ # Add [id]: #ANCHOR elements to the markdown source text for all sections.
327
+ # This lets us use the [SECTION-REF][] syntax
328
+ def markdown_filter_heading_anchors(markdown)
329
+ first = true
330
+ markdown.split("\n").grep(/^[#]{2,5} +[\w '-]+[# ]*$/).each do |line|
331
+ markdown << "\n\n" if first
332
+ first = false
333
+ title = line.gsub(/[^\w -]/, '').strip
334
+ anchor = title.gsub(/\W+/, '-').gsub(/(^-+|-+$)/, '')
335
+ markdown << "[#{title}]: ##{anchor} \"#{title}\"\n"
336
+ end
337
+ markdown
338
+ end
339
+
340
+ # Convert <WORD> to <var>WORD</var> but only if WORD isn't an HTML tag.
341
+ def markdown_filter_angle_quotes(markdown)
342
+ markdown.gsub(/\<([^:.\/]+?)\>/) do |match|
343
+ contents = $1
344
+ tag, attrs = contents.split(' ', 2)
345
+ if attrs =~ /\/=/ || html_element?(tag.sub(/^\//, '')) ||
346
+ data.include?("</#{tag}>")
347
+ match.to_s
348
+ else
349
+ "<var>#{contents}</var>"
233
350
  end
234
351
  end
235
- doc
236
352
  end
237
353
 
238
- # Add URL anchors to all HTML heading elements.
239
- def heading_anchor_filter(html)
240
- doc = parse_html(html)
241
- doc.search('h1|h2|h3|h4|h5|h6').not('[@id]').each do |heading|
242
- heading.set_attribute('id', heading.inner_text.gsub(/\W+/, '-'))
354
+ # Perform angle quote (<THESE>) post filtering.
355
+ def html_filter_angle_quotes
356
+ # convert all angle quote vars nested in code blocks
357
+ # back to the original text
358
+ @html.search('code').search('text()').each do |node|
359
+ next unless node.to_html.include?('var&gt;')
360
+ new =
361
+ node.to_html.
362
+ gsub('&lt;var&gt;', '&lt;').
363
+ gsub("&lt;/var&gt;", '>')
364
+ node.swap(new)
243
365
  end
244
- doc
245
366
  end
246
367
 
247
368
  # Convert special format unordered lists to definition lists.
248
- def definition_list_filter(html)
249
- doc = parse_html(html)
369
+ def html_filter_definition_lists
250
370
  # process all unordered lists depth-first
251
- doc.search('ul').to_a.reverse.each do |ul|
371
+ @html.search('ul').to_a.reverse.each do |ul|
252
372
  items = ul.search('li')
253
373
  next if items.any? { |item| item.inner_text.split("\n", 2).first !~ /:$/ }
254
374
 
@@ -270,85 +390,69 @@ module Ronn
270
390
  container.swap(wrap.sub(/></, ">#{definition}<"))
271
391
  end
272
392
  end
273
- doc
274
393
  end
275
394
 
276
- # Perform angle quote (<THESE>) post filtering.
277
- def angle_quote_post_filter(html)
278
- doc = parse_html(html)
279
- # convert all angle quote vars nested in code blocks
280
- # back to the original text
281
- doc.search('code').search('text()').each do |node|
282
- next unless node.to_html.include?('var&gt;')
283
- new =
284
- node.to_html.
285
- gsub('&lt;var&gt;', '&lt;').
286
- gsub("&lt;/var&gt;", '>')
287
- node.swap(new)
288
- end
289
- doc
290
- end
291
-
292
- # Run markdown on the data and extract name, section, and
293
- # tagline.
294
- def markdown_filter(data)
295
- @markdown = data
296
- html = Markdown.new(data).to_html
297
- @tagline, html = html.split("</h1>\n", 2)
298
- if html.nil?
299
- html = @tagline
300
- @tagline = nil
301
- else
302
- # grab name and section from title
303
- @tagline.sub!('<h1>', '')
304
- if @tagline =~ /([\w_.\[\]~+=@:-]+)\s*\((\d\w*)\)\s*-+\s*(.*)/
305
- @name = $1
306
- @section = $2
307
- @tagline = $3
308
- elsif @tagline =~ /([\w_.\[\]~+=@:-]+)\s+-+\s+(.*)/
309
- @name = $1
310
- @tagline = $2
395
+ def html_filter_inject_name_section
396
+ markup =
397
+ if title?
398
+ "<h1>#{title}</h1>"
399
+ elsif name
400
+ "<h2>NAME</h2>\n" +
401
+ "<p class='man-name'>\n <code>#{name}</code>" +
402
+ (tagline ? " - <span class='man-whatis'>#{tagline}</span>\n" : "\n") +
403
+ "</p>\n"
404
+ end
405
+ if markup
406
+ if @html.children
407
+ @html.at("*").before(markup)
408
+ else
409
+ @html = Hpricot(markup)
311
410
  end
312
411
  end
313
-
314
- html.to_s
315
412
  end
316
413
 
317
- # Convert all <WORD> to <var>WORD</var> but only if WORD
318
- # isn't an HTML tag.
319
- def angle_quote_pre_filter(data)
320
- data.gsub(/\<([^:.\/]+?)\>/) do |match|
321
- contents = $1
322
- tag, attrs = contents.split(' ', 2)
323
- if attrs =~ /\/=/ ||
324
- html_element?(tag.sub(/^\//, '')) ||
325
- data.include?("</#{tag}>")
326
- match.to_s
327
- else
328
- "<var>#{contents}</var>"
329
- end
414
+ # Add URL anchors to all HTML heading elements.
415
+ def html_filter_heading_anchors
416
+ @html.search('h2|h3|h4|h5|h6').not('[@id]').each do |heading|
417
+ heading.set_attribute('id', heading.inner_text.gsub(/\W+/, '-'))
330
418
  end
331
419
  end
332
420
 
333
- # Add [id]: #ANCHOR elements to the markdown source text for all sections.
334
- # This lets us use the [SECTION-REF][] syntax
335
- def heading_anchor_pre_filter(data)
336
- first = true
337
- data.split("\n").grep(/^[#]{2,5} +[\w '-]+[# ]*$/).each do |line|
338
- data << "\n\n" if first
339
- first = false
340
- title = line.gsub(/[^\w -]/, '').strip
341
- anchor = title.gsub(/\W+/, '-').gsub(/(^-+|-+$)/, '')
342
- data << "[#{title}]: ##{anchor} \"#{title}\"\n"
421
+ # Add a 'data-bare-link' attribute to hyperlinks
422
+ # whose text labels are the same as their href URLs.
423
+ def html_filter_annotate_bare_links
424
+ @html.search('a[@href]').each do |node|
425
+ href = node.attributes['href']
426
+ text = node.inner_text
427
+
428
+ if href == text ||
429
+ href[0] == ?# ||
430
+ CGI.unescapeHTML(href) == "mailto:#{CGI.unescapeHTML(text)}"
431
+ then
432
+ node.set_attribute('data-bare-link', 'true')
433
+ end
343
434
  end
344
- data
345
435
  end
346
- private
347
- def parse_html(html)
348
- if html.respond_to?(:doc?) && html.doc?
349
- html
350
- else
351
- Hpricot(html.to_s)
436
+
437
+ # Convert text of the form "name(section)" to a hyperlink. The URL is
438
+ # obtaiend from the index.
439
+ def html_filter_manual_reference_links
440
+ return if index.nil?
441
+ @html.search('text()').each do |node|
442
+ next if !node.content.include?(')')
443
+ next if %w[pre code h1 h2 h3].include?(node.parent.name)
444
+ next if child_of?(node, 'a')
445
+ node.swap(
446
+ node.content.gsub(/([0-9A-Za-z_:.+=@~-]+)(\(\d+\w*\))/) {
447
+ name, sect = $1, $2
448
+ if ref = index["#{name}#{sect}"]
449
+ "<a class='man-ref' href='#{ref.url}'>#{name}<span class='s'>#{sect}</span></a>"
450
+ else
451
+ # warn "warn: manual reference not defined: '#{name}#{sect}'"
452
+ "<span class='man-ref'>#{name}<span class='s'>#{sect}</span></span>"
453
+ end
454
+ }
455
+ )
352
456
  end
353
457
  end
354
458
  end