ronn 0.5 → 0.6.0

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