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/lib/ronn/index.rb ADDED
@@ -0,0 +1,183 @@
1
+ require 'ronn'
2
+
3
+ module Ronn
4
+
5
+ # Maintains a list of links / references to manuals and other resources.
6
+ class Index
7
+ include Enumerable
8
+
9
+ attr_reader :path
10
+ attr_reader :references
11
+
12
+ # Retrieve an Index for <path>, where <path> is a directory or normal
13
+ # file. The index is loaded from the corresponding index.txt file if
14
+ # one exists.
15
+ def self.[](path)
16
+ (@indexes ||= {})[index_path_for_file(path)] ||=
17
+ Index.new(index_path_for_file(path))
18
+ end
19
+
20
+ def self.index_path_for_file(file)
21
+ File.expand_path(
22
+ if File.directory?(file)
23
+ File.join(file, 'index.txt')
24
+ else
25
+ File.join(File.dirname(file), 'index.txt')
26
+ end
27
+ )
28
+ end
29
+
30
+ def initialize(path, &bk)
31
+ @path = path
32
+ @references = []
33
+ @manuals = {}
34
+
35
+ if block_given?
36
+ read! yield
37
+ elsif exist?
38
+ read! File.read(path)
39
+ end
40
+ end
41
+
42
+ # Determine whether the index file exists.
43
+ def exist?
44
+ File.exist?(path)
45
+ end
46
+
47
+ # Load index data from a string.
48
+ def read!(data)
49
+ data.each_line do |line|
50
+ line = line.strip.gsub(/\s*#.*$/, '')
51
+ if !line.empty?
52
+ name, url = line.split(/\s+/, 2)
53
+ @references << reference(name, url)
54
+ end
55
+ end
56
+ end
57
+
58
+ ##
59
+ # Enumerable and friends
60
+
61
+ def each(&bk)
62
+ references.each(&bk)
63
+ end
64
+
65
+ def size
66
+ references.size
67
+ end
68
+
69
+ def first
70
+ references.first
71
+ end
72
+
73
+ def last
74
+ references.last
75
+ end
76
+
77
+ def empty?
78
+ references.empty?
79
+ end
80
+
81
+ def [](name)
82
+ references.find { |ref| ref.name == name }
83
+ end
84
+
85
+ def reference(name, path)
86
+ Reference.new(self, name, path)
87
+ end
88
+
89
+ def <<(path)
90
+ raise ArgumentError, "local paths only" if path =~ /(https?|mailto):/
91
+ return self if any? { |ref| ref.path == File.expand_path(path) }
92
+ relative_path = relative_to_index(path)
93
+ @references << \
94
+ if path =~ /\.ronn?$/
95
+ reference manual(path).reference_name, relative_path
96
+ else
97
+ reference File.basename(path), relative_path
98
+ end
99
+ self
100
+ end
101
+
102
+ def add_manual(manual)
103
+ @manuals[File.expand_path(manual.path)] = manual
104
+ self << manual.path
105
+ end
106
+
107
+ def manual(path)
108
+ @manuals[File.expand_path(path)] ||= Document.new(path)
109
+ end
110
+
111
+ def manuals
112
+ select { |ref| ref.relative? && ref.ronn? }.
113
+ map { |ref| manual(ref.path) }
114
+ end
115
+
116
+ ##
117
+ # Converting
118
+
119
+ def to_text
120
+ map { |ref| [ref.name, ref.location].join(' ') }.join("\n")
121
+ end
122
+
123
+ def to_a
124
+ references
125
+ end
126
+
127
+ def to_h
128
+ to_a.map { |doc| doc.to_hash }
129
+ end
130
+
131
+ def relative_to_index(path)
132
+ path = File.expand_path(path)
133
+ index_dir = File.dirname(File.expand_path(self.path))
134
+ path.sub(/^#{index_dir}\//, '')
135
+ end
136
+ end
137
+
138
+ # An individual index reference. A reference can point to one of a few types
139
+ # of locations:
140
+ #
141
+ # - URLs: "http://man.cx/crontab(5)"
142
+ # - Relative paths to ronn manuals: "crontab.5.ronn"
143
+ #
144
+ # The #url method should be used to obtain the href value for HTML.
145
+ class Reference
146
+ attr_reader :name
147
+ attr_reader :location
148
+
149
+ def initialize(index, name, location)
150
+ @index = index
151
+ @name = name
152
+ @location = location
153
+ end
154
+
155
+ def manual?
156
+ name =~ /\([0-9]\w*\)$/
157
+ end
158
+
159
+ def ronn?
160
+ location =~ /\.ronn?$/
161
+ end
162
+
163
+ def remote?
164
+ location =~ /^(?:https?|mailto):/
165
+ end
166
+
167
+ def relative?
168
+ !remote?
169
+ end
170
+
171
+ def url
172
+ if remote?
173
+ location
174
+ else
175
+ location.chomp('.ronn') + '.html'
176
+ end
177
+ end
178
+
179
+ def path
180
+ File.expand_path(location, File.dirname(@index.path)) if relative?
181
+ end
182
+ end
183
+ end
data/lib/ronn/roff.rb ADDED
@@ -0,0 +1,302 @@
1
+ require 'hpricot'
2
+ require 'ronn/utils'
3
+
4
+ module Ronn
5
+ class RoffFilter
6
+ include Ronn::Utils
7
+
8
+ # Convert Ronn HTML to roff.
9
+ def initialize(html, name, section, tagline, manual=nil, version=nil, date=nil)
10
+ @buf = []
11
+ title_heading name, section, tagline, manual, version, date
12
+ doc = Hpricot(html)
13
+ remove_extraneous_elements! doc
14
+ normalize_whitespace! doc
15
+ block_filter doc
16
+ write "\n"
17
+ end
18
+
19
+ def to_s
20
+ @buf.join.gsub(/[ \t]+$/, '')
21
+ end
22
+
23
+ protected
24
+ def previous(node)
25
+ if node.respond_to?(:previous)
26
+ prev = node.previous
27
+ prev = prev.previous until prev.nil? || prev.elem?
28
+ prev
29
+ end
30
+ end
31
+
32
+ def title_heading(name, section, tagline, manual, version, date)
33
+ comment "generated with Ronn-NG/v#{Ronn.version}"
34
+ comment "http://github.com/apjanke/ronn-ng/tree/#{Ronn.revision}"
35
+ return if name.nil?
36
+ macro "TH", %["#{escape(name.upcase)}" "#{section}" "#{date.strftime('%B %Y')}" "#{version}" "#{manual}"]
37
+ end
38
+
39
+ def remove_extraneous_elements!(doc)
40
+ doc.traverse_all_element do |node|
41
+ if node.comment? || node.procins? || node.doctype? || node.xmldecl?
42
+ node.parent.children.delete(node)
43
+ end
44
+ end
45
+ end
46
+
47
+ def normalize_whitespace!(node)
48
+ case
49
+ when node.kind_of?(Array) || node.kind_of?(Hpricot::Elements)
50
+ node.to_a.dup.each { |ch| normalize_whitespace! ch }
51
+ when node.text?
52
+ preceding, following = node.previous, node.next
53
+ content = node.content.gsub(/[\n ]+/m, ' ')
54
+ if preceding.nil? || block_element?(preceding.name) ||
55
+ preceding.name == 'br'
56
+ content.lstrip!
57
+ end
58
+ if following.nil? || block_element?(following.name) ||
59
+ following.name == 'br'
60
+ content.rstrip!
61
+ end
62
+ if content.empty?
63
+ node.parent.children.delete(node)
64
+ else
65
+ node.content = content
66
+ end
67
+ when node.elem? && node.name == 'pre'
68
+ # stop traversing
69
+ when node.elem? && node.children
70
+ normalize_whitespace! node.children
71
+ when node.elem?
72
+ # element has no children
73
+ when node.doc?
74
+ normalize_whitespace! node.children
75
+ else
76
+ warn "unexpected node during whitespace normalization: %p", node
77
+ end
78
+ end
79
+
80
+ def block_filter(node)
81
+ if node.kind_of?(Array) || node.kind_of?(Hpricot::Elements)
82
+ node.each { |ch| block_filter(ch) }
83
+
84
+ elsif node.doc?
85
+ block_filter(node.children)
86
+
87
+ elsif node.text?
88
+ warn "unexpected text: %p", node
89
+
90
+ elsif node.elem?
91
+ case node.name
92
+ when 'div'
93
+ block_filter(node.children)
94
+ when 'h1'
95
+ # discard
96
+ when 'h2'
97
+ macro "SH", quote(escape(node.html))
98
+ when 'h3'
99
+ macro "SS", quote(escape(node.html))
100
+
101
+ when 'p'
102
+ prev = previous(node)
103
+ if prev && %w[dd li blockquote].include?(node.parent.name)
104
+ macro "IP"
105
+ elsif prev && !%w[h1 h2 h3].include?(prev.name)
106
+ macro "P"
107
+ end
108
+ inline_filter(node.children)
109
+
110
+ when 'blockquote'
111
+ prev = previous(node)
112
+ indent = prev.nil? || !%w[h1 h2 h3].include?(prev.name)
113
+ macro "IP", %w["" 4] if indent
114
+ block_filter(node.children)
115
+ macro "IP", %w["" 0] if indent
116
+
117
+ when 'pre'
118
+ prev = previous(node)
119
+ indent = prev.nil? || !%w[h1 h2 h3].include?(prev.name)
120
+ macro "IP", %w["" 4] if indent
121
+ macro "nf"
122
+ write "\n"
123
+ inline_filter(node.children)
124
+ macro "fi"
125
+ macro "IP", %w["" 0] if indent
126
+
127
+ when 'dl'
128
+ macro "TP"
129
+ block_filter(node.children)
130
+ when 'dt'
131
+ prev = previous(node)
132
+ macro "TP" unless prev.nil?
133
+ inline_filter(node.children)
134
+ write "\n"
135
+ when 'dd'
136
+ if node.at('p')
137
+ block_filter(node.children)
138
+ else
139
+ inline_filter(node.children)
140
+ end
141
+ write "\n"
142
+
143
+ when 'ol', 'ul'
144
+ block_filter(node.children)
145
+ macro "IP", %w["" 0]
146
+ when 'li'
147
+ case node.parent.name
148
+ when 'ol'
149
+ macro "IP", %W["#{node.position + 1}." 4]
150
+ when 'ul'
151
+ macro "IP", %w["\\\[ci\]" 4]
152
+ end
153
+ if node.at('p|ol|ul|dl|div')
154
+ block_filter(node.children)
155
+ else
156
+ inline_filter(node.children)
157
+ end
158
+ write "\n"
159
+
160
+ when 'span', 'code', 'b', 'strong', 'kbd', 'samp', 'var', 'em', 'i',
161
+ 'u', 'br', 'a'
162
+ inline_filter(node)
163
+ else
164
+ warn "unrecognized block tag: %p", node.name
165
+ end
166
+
167
+ else
168
+ fail "unexpected node: #{node.inspect}"
169
+ end
170
+ end
171
+
172
+ def inline_filter(node)
173
+ return unless node # is an empty node
174
+
175
+ if node.kind_of?(Array) || node.kind_of?(Hpricot::Elements)
176
+ node.each { |ch| inline_filter(ch) }
177
+
178
+ elsif node.text?
179
+ text = node.to_html.dup
180
+ write escape(text)
181
+
182
+ elsif node.elem?
183
+ case node.name
184
+ when 'span'
185
+ inline_filter(node.children)
186
+ when 'code'
187
+ if child_of?(node, 'pre')
188
+ inline_filter(node.children)
189
+ else
190
+ write '\fB'
191
+ inline_filter(node.children)
192
+ write '\fR'
193
+ end
194
+
195
+ when 'b', 'strong', 'kbd', 'samp'
196
+ write '\fB'
197
+ inline_filter(node.children)
198
+ write '\fR'
199
+
200
+ when 'var', 'em', 'i', 'u'
201
+ write '\fI'
202
+ inline_filter(node.children)
203
+ write '\fR'
204
+
205
+ when 'br'
206
+ macro 'br'
207
+
208
+ when 'a'
209
+ if node.classes.include?('man-ref')
210
+ inline_filter(node.children)
211
+ elsif node.has_attribute?('data-bare-link')
212
+ write '\fI'
213
+ inline_filter(node.children)
214
+ write '\fR'
215
+ else
216
+ inline_filter(node.children)
217
+ write ' '
218
+ write '\fI'
219
+ write escape(node.attributes['href'])
220
+ write '\fR'
221
+ end
222
+
223
+ when 'sup'
224
+ # This superscript equivalent is a big ugly hack.
225
+ write '^('
226
+ inline_filter(node.children)
227
+ write ')'
228
+
229
+ else
230
+ warn "unrecognized inline tag: %p", node.name
231
+ end
232
+
233
+ else
234
+ fail "unexpected node: #{node.inspect}"
235
+ end
236
+ end
237
+
238
+ def macro(name, value=nil)
239
+ writeln ".\n.#{[name, value].compact.join(' ')}"
240
+ end
241
+
242
+ HTML_ROFF_ENTITIES = {
243
+ '&bull;' => '\[ci]',
244
+ '&lt;' => '<',
245
+ '&gt;' => '>',
246
+ '&nbsp;' => '\~',
247
+ '&copy;' => '\(co',
248
+ '&rdquo;' => '\(rs',
249
+ '&mdash;' => '\(em',
250
+ '&reg;' => '\(rg',
251
+ '&sec;' => '\(sc',
252
+ '&ge;' => '\(>=',
253
+ '&le;' => '\(<=',
254
+ '&ne;' => '\(!=',
255
+ '&equiv;' => '\(=='
256
+ }
257
+
258
+ def escape(text)
259
+ return text.to_s if text.nil? || text.empty?
260
+ ent = HTML_ROFF_ENTITIES
261
+ text = text.dup
262
+ text.gsub!(/&#x([0-9A-Fa-f]+);/) { $1.to_i(16).chr } # hex entities
263
+ text.gsub!(/&#(\d+);/) { $1.to_i.chr } # dec entities
264
+ text.gsub!('\\', '\e') # backslash
265
+ text.gsub!('...', '\|.\|.\|.') # ellipses
266
+ text.gsub!(/['.-]/) { |m| "\\#{m}" } # control chars
267
+ text.gsub!(/(&[A-Za-z]+;)/) { ent[$1] || $1 } # named entities
268
+ text.gsub!('&amp;', '&') # amps
269
+ text
270
+ end
271
+
272
+ def quote(text)
273
+ "\"#{text.gsub(/"/, '\\"')}\""
274
+ end
275
+
276
+ # write text to output buffer
277
+ def write(text)
278
+ return if text.nil? || text.empty?
279
+ # lines cannot start with a '.'. insert zero-width character before.
280
+ if text[0,2] == '\.' &&
281
+ (@buf.last && @buf.last[-1] == ?\n)
282
+ @buf << '\&'
283
+ end
284
+ @buf << text
285
+ end
286
+
287
+ # write text to output buffer on a new line.
288
+ def writeln(text)
289
+ write "\n" if @buf.last && @buf.last[-1] != ?\n
290
+ write text
291
+ write "\n"
292
+ end
293
+
294
+ def comment(text)
295
+ writeln %[.\\" #{text}]
296
+ end
297
+
298
+ def warn(text, *args)
299
+ $stderr.puts "warn: #{text}" % args
300
+ end
301
+ end
302
+ end