ronn 0.5 → 0.6.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 (48) hide show
  1. data/AUTHORS +7 -0
  2. data/CHANGES +128 -0
  3. data/README.md +64 -79
  4. data/Rakefile +81 -28
  5. data/bin/ronn +124 -65
  6. data/config.ru +15 -0
  7. data/lib/ronn.rb +13 -5
  8. data/lib/ronn/document.rb +87 -13
  9. data/lib/ronn/roff.rb +43 -18
  10. data/lib/ronn/server.rb +70 -0
  11. data/lib/ronn/template.rb +157 -0
  12. data/lib/ronn/template/80c.css +6 -0
  13. data/lib/ronn/template/dark.css +21 -0
  14. data/lib/ronn/template/darktoc.css +17 -0
  15. data/lib/ronn/template/default.html +43 -0
  16. data/lib/ronn/template/man.css +100 -0
  17. data/lib/ronn/template/print.css +5 -0
  18. data/lib/ronn/template/screen.css +105 -0
  19. data/lib/ronn/template/toc.css +27 -0
  20. data/man/ronn.1 +160 -93
  21. data/man/ronn.1.ronn +206 -89
  22. data/man/ronn.5 +94 -96
  23. data/man/ronn.5.ronn +96 -91
  24. data/man/ronn.7 +50 -84
  25. data/man/ronn.7.ronn +64 -79
  26. data/ronn.gemspec +26 -11
  27. data/test/angle_bracket_syntax.html +4 -1
  28. data/test/basic_document.html +4 -1
  29. data/test/contest.rb +68 -0
  30. data/test/custom_title_document.html +4 -1
  31. data/test/definition_list_syntax.html +4 -1
  32. data/test/definition_list_syntax.roff +26 -0
  33. data/test/document_test.rb +51 -4
  34. data/test/entity_encoding_test.html +4 -1
  35. data/test/entity_encoding_test.roff +1 -1
  36. data/test/markdown_syntax.html +955 -0
  37. data/test/markdown_syntax.roff +1467 -0
  38. data/{man/markdown.5.ronn → test/markdown_syntax.ronn} +0 -0
  39. data/test/middle_paragraph.html +5 -2
  40. data/test/middle_paragraph.roff +2 -2
  41. data/test/ronn_test.rb +19 -4
  42. data/test/section_reference_links.html +15 -0
  43. data/test/section_reference_links.roff +10 -0
  44. data/test/section_reference_links.ronn +12 -0
  45. data/test/titleless_document.html +3 -0
  46. metadata +34 -13
  47. data/lib/ronn/layout.html +0 -75
  48. data/man/markdown.5 +0 -1639
data/bin/ronn CHANGED
@@ -1,90 +1,120 @@
1
1
  #!/usr/bin/env ruby
2
- ## Usage: ronn [ OPTIONS ] [ FILE ]
3
- ## ronn --build FILE ...
4
- ## ronn --install FILE ...
5
- ## ronn --man FILE ...
6
- ## Convert ronn FILE to roff man page or HTML and write to standard
7
- ## output. With no FILE, ronn reads from standard input. The build,
8
- ## install, and man forms accept multiple FILE arguments.
9
- ##
10
- ## Modes:
11
- ## --pipe write to standard output (default behavior)
12
- ## -b, --build write to files instead of standard output
13
- ## -i, --install write to file in MAN_HOME or system man path
14
- ## -m, --man open man page like man(1)
15
- ##
16
- ## Formats:
17
- ## -r, --roff generate roff/man text; this is the default behavior
18
- ## -5, --html generate entire HTML page with layout
19
- ## -f, --fragment generate HTML fragment instead of entire HTML page
20
- ##
21
- ## Document attributes:
22
- ## --date=DATE published date in YYYY-MM-DD format;
23
- ## displayed bottom-center in footer
24
- ## --manual=NAME name of the manual this document belongs to;
25
- ## displayed top-center in header
26
- ## --organization=NAME publishing group, organization, or individual;
27
- ## displayed bottom-left in footer
28
- ##
29
- ## --help show this help message
30
- ##
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 file generating behavior:
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 are generated:
15
+ #/ -r, --roff generate roff output
16
+ #/ -5, --html generate entire HTML page with layout
17
+ #/ -f, --fragment generate HTML fragment
18
+ #/
19
+ #/ Document attributes:
20
+ #/ --date=DATE published date in YYYY-MM-DD format (bottom-center)
21
+ #/ --manual=NAME name of the manual (top-center)
22
+ #/ --organization=NAME publishing group or individual (bottom-left)
23
+ #/
24
+ #/ Misc options:
25
+ #/ -w, --warnings show troff warnings on stderr
26
+ #/ -W disable previously enabled troff warnings
27
+ #/ --help show this help message
28
+ #/
29
+ #/ A <file> named example.1.ronn generates example.1.html (HTML manpage)
30
+ #/ and example.1 (roff manpage) by default.
31
31
  require 'date'
32
32
  require 'optparse'
33
33
 
34
- formats = []
35
- options = {}
36
- build = false
37
- install = false
38
- man = false
39
- groff = "groff -Wall -mtty-char -mandoc -Tascii"
40
- pager = ENV['MANPAGER'] || ENV['PAGER'] || 'more'
41
-
42
- def info(message, *args)
43
- STDERR.puts message % args
44
- end
45
-
46
34
  def usage
47
35
  puts File.readlines(__FILE__).
48
- grep(/^##.*/).
36
+ grep(/^#\/.*/).
49
37
  map { |line| line.chomp[3..-1] }.
50
38
  join("\n")
51
39
  end
52
40
 
53
- # parse command line options
54
- ARGV.options do |option|
41
+ ##
42
+ # Argument defaults
43
+
44
+ build = true
45
+ view = false
46
+ server = false
47
+ formats = nil
48
+ options = {}
49
+ styles = %w[man]
50
+ groff = "groff -Wall -mtty-char -mandoc -Tascii"
51
+ pager = ENV['MANPAGER'] || ENV['PAGER'] || 'more'
52
+
53
+ ##
54
+ # Environment variables
55
+
56
+ %w[manual organization date].each do |attribute|
57
+ value = ENV["RONN_#{attribute.upcase}"]
58
+ next if value.nil? or value.empty?
59
+ options[attribute] = value
60
+ end
61
+
62
+ ##
63
+ # Argument parsing
64
+
65
+ ARGV.options do |argv|
55
66
  # modes
56
- option.on("--pipe") { }
57
- option.on("-b", "--build") { build = true }
58
- option.on("-i", "--install") { install = true }
59
- option.on("-m", "--man") { man = true }
67
+ argv.on("--pipe") { build = server = false }
68
+ argv.on("-b", "--build") { build = true; server = false }
69
+ argv.on("-m", "--man") { build = server = false; view = true }
70
+ argv.on("-S", "--server") { build = view = false; server = true }
71
+ argv.on("-w", "--warnings") { groff += ' -ww' }
72
+ argv.on("-W") { groff += ' -Ww' }
60
73
 
61
74
  # format options
62
- option.on("-r", "--roff") { formats << 'roff' }
63
- option.on("-5", "--html") { formats << 'html' }
64
- option.on("-f", "--fragment") { formats << 'html_fragment' }
75
+ argv.on("-r", "--roff") { (formats ||= []) << 'roff' }
76
+ argv.on("-5", "--html") { (formats ||= []) << 'html' }
77
+ argv.on("-f", "--fragment") { (formats ||= []) << 'html_fragment' }
78
+
79
+ # html output options
80
+ argv.on("-s", "--style=V") { |val| styles += val.split(/[, \n]+/) }
65
81
 
66
82
  # manual attribute options
67
- [:name, :section, :manual, :organization, :date].each do |option_attr|
68
- option.on("--#{option_attr}=VALUE") { |val| options[option_attr] = val }
83
+ %w[name section manual organization date].each do |attribute|
84
+ argv.on("--#{attribute}=VALUE") { |val| options[attribute] = val }
69
85
  end
70
86
 
71
- option.on_tail("--help") { usage ; exit }
72
- option.parse!
87
+ argv.on_tail("--help") { usage ; exit 0 }
88
+ argv.parse!
73
89
  end
74
90
 
75
- if ARGV.empty? && STDIN.tty?
91
+ ##
92
+ # Modes, Formats, Options
93
+
94
+ case
95
+ when ARGV.empty? && $stdin.tty?
76
96
  usage
77
- exit
78
- elsif ARGV.empty?
97
+ exit 2
98
+ when ARGV.empty? && !server
79
99
  ARGV.push '-'
100
+ build = false
101
+ formats ||= %w[roff]
102
+ when view
103
+ formats ||= %w[roff]
104
+ when build
105
+ formats ||= %w[roff html]
80
106
  end
107
+ formats ||= []
108
+ formats.delete('html') if formats.include?('html_fragment')
81
109
 
82
110
  # turn the --date arg into a real date object
83
- options[:date] &&= Date.strptime(options[:date], '%Y-%m-%d')
111
+ options['date'] &&= Date.strptime(options['date'], '%Y-%m-%d')
84
112
 
85
- formats = ['roff'] if formats.empty?
86
- formats.delete('html') if formats.include?('html_fragment')
87
- pid = nil
113
+ # pass the styles option
114
+ options['styles'] = styles
115
+
116
+ ##
117
+ # Libraries and LOAD_PATH shenanigans
88
118
 
89
119
  begin
90
120
  require 'hpricot'
@@ -97,6 +127,8 @@ rescue LoadError
97
127
  end
98
128
  end
99
129
 
130
+ # load ronn libs, setting up the load path if something fails and
131
+ # we're in a development environment.
100
132
  begin
101
133
  require 'ronn'
102
134
  rescue LoadError
@@ -110,12 +142,25 @@ rescue LoadError
110
142
  raise
111
143
  end
112
144
 
145
+ ##
146
+ # Server
147
+
148
+ if server
149
+ require 'ronn/server'
150
+ Ronn::Server.run(ARGV, options)
151
+ exit 0
152
+ end
153
+
154
+ ##
155
+ # Build Pipeline
156
+
157
+ pid = nil
113
158
  wr = STDOUT
114
159
  ARGV.each do |file|
115
160
  doc = Ronn.new(file, options) { file == '-' ? STDIN.read : File.read(file) }
116
161
 
117
162
  # setup the man pipeline if the --man option was specified
118
- if man && !build
163
+ if view && !build
119
164
  rd, wr = IO.pipe
120
165
  if pid = fork
121
166
  rd.close
@@ -130,16 +175,30 @@ ARGV.each do |file|
130
175
  formats.each do |format|
131
176
  if build
132
177
  path = doc.path_for(format)
133
- info "building: #{path}" if build
178
+ case format
179
+ when 'html'
180
+ warn "%5s: %-48s%15s" % [format, path, '+' + doc.styles.join(',')]
181
+ when 'roff', 'html_fragment'
182
+ warn "%5s: %-48s" % [format, path]
183
+ end
184
+
134
185
  output = doc.convert(format)
135
186
  File.open(path, 'wb') { |f| f.puts(output) }
136
- system "man #{path}" if man && format == 'roff'
187
+
188
+ if format == 'roff'
189
+ if view
190
+ system "man #{path}"
191
+ else
192
+ system "#{groff} <#{path} >/dev/null"
193
+ end
194
+ end
137
195
  else
138
196
  output = doc.convert(format)
139
197
  wr.puts(output)
140
198
  end
141
199
  end
142
200
 
201
+ # wait for children to exit
143
202
  if pid
144
203
  wr.close
145
204
  Process.wait
@@ -0,0 +1,15 @@
1
+ #\ -p 1207
2
+ $: << File.expand_path(__FILE__, '../lib')
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)
@@ -3,14 +3,22 @@
3
3
  # install standard UNIX roff(7) formatted manpages or to generate
4
4
  # beautiful HTML manpages.
5
5
  module Ronn
6
- VERSION = '0.5'
7
-
8
- require 'ronn/document'
9
- require 'ronn/roff'
10
-
11
6
  # Create a new Ronn::Document for the given ronn file. See
12
7
  # Ronn::Document.new for usage information.
13
8
  def self.new(filename, attributes={}, &block)
14
9
  Document.new(filename, attributes, &block)
15
10
  end
11
+
12
+ # bring REV up to date with: rake rev
13
+ REV = '0.6.0'
14
+ VERSION = REV[/(?:[\d.]+)(?:-\d+)?/].tr('-', '.')
15
+
16
+ def self.release?
17
+ REV != '' && !REV.include?('-')
18
+ end
19
+
20
+ autoload :Document, 'ronn/document'
21
+ autoload :Roff, 'ronn/roff'
22
+ autoload :Server, 'ronn/server'
23
+ autoload :Template, 'ronn/template'
16
24
  end
@@ -1,7 +1,9 @@
1
1
  require 'set'
2
+ require 'cgi'
2
3
  require 'hpricot'
3
4
  require 'rdiscount'
4
5
  require 'ronn/roff'
6
+ require 'ronn/template'
5
7
 
6
8
  module Ronn
7
9
  # The Document class can be used to load and inspect a ronn document
@@ -41,6 +43,9 @@ module Ronn
41
43
  # the document footer.
42
44
  attr_accessor :date
43
45
 
46
+ # Array of style modules to apply to the document.
47
+ attr_accessor :styles
48
+
44
49
  # Create a Ronn::Document given a path or with the data returned by
45
50
  # calling the block. The document is loaded and preprocessed before
46
51
  # the intialize method returns. The attributes hash may contain values
@@ -53,6 +58,8 @@ module Ronn
53
58
  @name, @section, @tagline = nil
54
59
  @manual, @organization, @date = nil
55
60
  @fragment = preprocess
61
+ @styles = %w[man]
62
+
56
63
  attributes.each { |attr_name,value| send("#{attr_name}=", value) }
57
64
  end
58
65
 
@@ -123,6 +130,21 @@ module Ronn
123
130
  Time.now
124
131
  end
125
132
 
133
+ # Retrieve a list of top-level section headings in the document and return
134
+ # as an array of +[id, text]+ tuples, where +id+ is the element's generated
135
+ # id and +text+ is the inner text of the heading element.
136
+ def section_heads
137
+ parse_html(to_html_fragment).search('h2[@id]').map do |heading|
138
+ [heading.attributes['id'], heading.inner_text]
139
+ end
140
+ end
141
+
142
+ # Styles to insert in the generated HTML output. This is a simple Array of
143
+ # string module names or file paths.
144
+ def styles=(styles)
145
+ @styles = (%w[man] + styles).uniq
146
+ end
147
+
126
148
  # Convert the document to :roff, :html, or :html_fragment and
127
149
  # return the result as a string.
128
150
  def convert(format)
@@ -132,7 +154,7 @@ module Ronn
132
154
  # Convert the document to roff and return the result as a string.
133
155
  def to_roff
134
156
  RoffFilter.new(
135
- to_html_fragment,
157
+ to_html_fragment(wrap_class=nil),
136
158
  name,
137
159
  section,
138
160
  tagline,
@@ -144,42 +166,79 @@ module Ronn
144
166
 
145
167
  # Convert the document to HTML and return the result as a string.
146
168
  def to_html
147
- layout_filter(to_html_fragment)
169
+ if layout = ENV['RONN_LAYOUT']
170
+ if !File.exist?(layout_path = File.expand_path(layout))
171
+ warn "warn: can't find #{layout}, using default layout."
172
+ layout_path = nil
173
+ end
174
+ end
175
+
176
+ template = Ronn::Template.new(self)
177
+ template.render(layout_path || 'default')
148
178
  end
149
179
 
150
180
  # Convert the document to HTML and return the result
151
181
  # as a string. The HTML does not include <html>, <head>,
152
182
  # or <style> tags.
153
- def to_html_fragment
183
+ def to_html_fragment(wrap_class='mp')
184
+ wrap_class = nil if wrap_class.to_s.empty?
154
185
  buf = []
186
+ buf << "<div class='#{wrap_class}'>" if wrap_class
155
187
  if name? && section?
156
188
  buf << "<h2 id='NAME'>NAME</h2>"
157
- buf << "<p><code>#{name}</code> -- #{tagline}</p>"
189
+ buf << "<p><code>#{name}</code> - #{tagline}</p>"
158
190
  elsif tagline
159
- buf << "<h1>#{[name, tagline].compact.join(' -- ')}</h1>"
191
+ buf << "<h1>#{[name, tagline].compact.join(' - ')}</h1>"
160
192
  end
161
193
  buf << @fragment.to_s
194
+ buf << "</div>" if wrap_class
162
195
  buf.join("\n")
163
196
  end
164
197
 
165
198
  protected
199
+ # The preprocessed markdown source text.
200
+ attr_reader :markdown
201
+
166
202
  # Parse the document and extract the name, section, and tagline
167
203
  # from its contents. This is called while the object is being
168
204
  # initialized.
169
205
  def preprocess
170
206
  [
207
+ :heading_anchor_pre_filter,
171
208
  :angle_quote_pre_filter,
172
209
  :markdown_filter,
173
210
  :angle_quote_post_filter,
174
- :definition_list_filter
211
+ :definition_list_filter,
212
+ :heading_anchor_filter,
213
+ :annotate_bare_links_filter
175
214
  ].inject(data) { |res,filter| send(filter, res) }
176
215
  end
177
216
 
178
- # Apply the standard HTML layout template.
179
- def layout_filter(html)
180
- template_file = File.dirname(__FILE__) + "/layout.html"
181
- template = File.read(template_file)
182
- eval("%Q{#{template}}", binding, template_file)
217
+ # Add a 'data-bare-link' attribute to hyperlinks
218
+ # whose text labels are the same as their href URLs.
219
+ def annotate_bare_links_filter(html)
220
+ doc = parse_html(html)
221
+ doc.search('a[@href]').each do |node|
222
+ href = node.attributes['href']
223
+ text = node.inner_text
224
+
225
+ if href == text ||
226
+ href[0] == ?# ||
227
+ CGI.unescapeHTML(href) == "mailto:#{CGI.unescapeHTML(text)}"
228
+ then
229
+ node.set_attribute('data-bare-link', 'true')
230
+ end
231
+ end
232
+ doc
233
+ end
234
+
235
+ # Add URL anchors to all HTML heading elements.
236
+ def heading_anchor_filter(html)
237
+ doc = parse_html(html)
238
+ doc.search('h1|h2|h3|h4|h5|h6').not('[@id]').each do |heading|
239
+ heading.set_attribute('id', heading.inner_text.gsub(/\W+/, '-'))
240
+ end
241
+ doc
183
242
  end
184
243
 
185
244
  # Convert special format unordered lists to definition lists.
@@ -230,6 +289,7 @@ module Ronn
230
289
  # Run markdown on the data and extract name, section, and
231
290
  # tagline.
232
291
  def markdown_filter(data)
292
+ @markdown = data
233
293
  html = Markdown.new(data).to_html
234
294
  @tagline, html = html.split("</h1>\n", 2)
235
295
  if html.nil?
@@ -238,11 +298,11 @@ module Ronn
238
298
  else
239
299
  # grab name and section from title
240
300
  @tagline.sub!('<h1>', '')
241
- if @tagline =~ /([\w_.\[\]~+=@:-]+)\s*\((\d\w*)\)\s*--?\s*(.*)/
301
+ if @tagline =~ /([\w_.\[\]~+=@:-]+)\s*\((\d\w*)\)\s*-+\s*(.*)/
242
302
  @name = $1
243
303
  @section = $2
244
304
  @tagline = $3
245
- elsif @tagline =~ /([\w_.\[\]~+=@:-]+)\s+--\s+(.*)/
305
+ elsif @tagline =~ /([\w_.\[\]~+=@:-]+)\s+-+\s+(.*)/
246
306
  @name = $1
247
307
  @tagline = $2
248
308
  end
@@ -267,6 +327,20 @@ module Ronn
267
327
  end
268
328
  end
269
329
 
330
+ # Add [id]: #ANCHOR elements to the markdown source text for all sections.
331
+ # This lets us use the [SECTION-REF][] syntax
332
+ def heading_anchor_pre_filter(data)
333
+ first = true
334
+ data.split("\n").grep(/^[#]{2,5} +[\w '-]+[# ]*$/).each do |line|
335
+ data << "\n\n" if first
336
+ first = false
337
+ title = line.gsub(/[^\w -]/, '').strip
338
+ anchor = title.gsub(/\W+/, '-').gsub(/(^-+|-+$)/, '')
339
+ data << "[#{title}]: ##{anchor} \"#{title}\"\n"
340
+ end
341
+ data
342
+ end
343
+
270
344
  HTML = %w[
271
345
  a abbr acronym b bdo big br cite code dfn
272
346
  em i img input kbd label q samp select