ronn-ng 0.7.4

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 (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