ronn 0.6.6 → 0.7.0

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