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.
- checksums.yaml +7 -0
- data/AUTHORS +8 -0
- data/CHANGES +184 -0
- data/INSTALLING +20 -0
- data/LICENSE.txt +11 -0
- data/README.md +113 -0
- data/Rakefile +163 -0
- data/bin/ronn +223 -0
- data/config.ru +15 -0
- data/lib/ronn.rb +50 -0
- data/lib/ronn/document.rb +495 -0
- data/lib/ronn/index.rb +183 -0
- data/lib/ronn/roff.rb +302 -0
- data/lib/ronn/server.rb +70 -0
- data/lib/ronn/template.rb +171 -0
- data/lib/ronn/template/80c.css +6 -0
- data/lib/ronn/template/dark.css +18 -0
- data/lib/ronn/template/darktoc.css +17 -0
- data/lib/ronn/template/default.html +41 -0
- data/lib/ronn/template/man.css +100 -0
- data/lib/ronn/template/print.css +5 -0
- data/lib/ronn/template/screen.css +105 -0
- data/lib/ronn/template/toc.css +27 -0
- data/lib/ronn/utils.rb +55 -0
- data/man/index.html +78 -0
- data/man/index.txt +15 -0
- data/man/ronn-format.7 +201 -0
- data/man/ronn-format.7.ronn +157 -0
- data/man/ronn.1 +325 -0
- data/man/ronn.1.ronn +306 -0
- data/ronn-ng.gemspec +97 -0
- data/test/angle_bracket_syntax.html +18 -0
- data/test/angle_bracket_syntax.ronn +12 -0
- data/test/basic_document.html +9 -0
- data/test/basic_document.ronn +4 -0
- data/test/contest.rb +68 -0
- data/test/custom_title_document.html +6 -0
- data/test/custom_title_document.ronn +5 -0
- data/test/definition_list_syntax.html +21 -0
- data/test/definition_list_syntax.roff +26 -0
- data/test/definition_list_syntax.ronn +18 -0
- data/test/dots_at_line_start_test.roff +10 -0
- data/test/dots_at_line_start_test.ronn +4 -0
- data/test/entity_encoding_test.html +35 -0
- data/test/entity_encoding_test.roff +61 -0
- data/test/entity_encoding_test.ronn +25 -0
- data/test/index.txt +8 -0
- data/test/markdown_syntax.html +957 -0
- data/test/markdown_syntax.roff +1467 -0
- data/test/markdown_syntax.ronn +881 -0
- data/test/middle_paragraph.html +15 -0
- data/test/middle_paragraph.roff +13 -0
- data/test/middle_paragraph.ronn +10 -0
- data/test/missing_spaces.roff +9 -0
- data/test/missing_spaces.ronn +2 -0
- data/test/pre_block_with_quotes.roff +13 -0
- data/test/pre_block_with_quotes.ronn +6 -0
- data/test/section_reference_links.html +17 -0
- data/test/section_reference_links.roff +10 -0
- data/test/section_reference_links.ronn +12 -0
- data/test/test_ronn.rb +110 -0
- data/test/test_ronn_document.rb +186 -0
- data/test/test_ronn_index.rb +73 -0
- data/test/titleless_document.html +10 -0
- data/test/titleless_document.ronn +3 -0
- data/test/underline_spacing_test.roff +21 -0
- data/test/underline_spacing_test.ronn +11 -0
- 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
|
+
'•' => '\[ci]',
|
244
|
+
'<' => '<',
|
245
|
+
'>' => '>',
|
246
|
+
' ' => '\~',
|
247
|
+
'©' => '\(co',
|
248
|
+
'”' => '\(rs',
|
249
|
+
'—' => '\(em',
|
250
|
+
'®' => '\(rg',
|
251
|
+
'&sec;' => '\(sc',
|
252
|
+
'≥' => '\(>=',
|
253
|
+
'≤' => '\(<=',
|
254
|
+
'≠' => '\(!=',
|
255
|
+
'≡' => '\(=='
|
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!('&', '&') # 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
|