ronn-ng 0.7.4
Sign up to get free protection for your applications and to get access to all the features.
- 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/bin/ronn
ADDED
@@ -0,0 +1,223 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
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 behavior of generating files:
|
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 / formats are generated:
|
15
|
+
#/ -r, --roff generate roff output
|
16
|
+
#/ -5, --html generate entire HTML page with layout
|
17
|
+
#/ -f, --fragment generate HTML fragment
|
18
|
+
#/ --markdown generate post-processed markdown output
|
19
|
+
#/
|
20
|
+
#/ Document attributes:
|
21
|
+
#/ --date=<date> published date in YYYY-MM-DD format (bottom-center)
|
22
|
+
#/ --manual=<name> name of the manual (top-center)
|
23
|
+
#/ --organization=<name> publishing group or individual (bottom-left)
|
24
|
+
#/
|
25
|
+
#/ Misc options:
|
26
|
+
#/ -w, --warnings show troff warnings on stderr
|
27
|
+
#/ -W disable previously enabled troff warnings
|
28
|
+
#/ --version show ronn version and exit
|
29
|
+
#/ --help show this help message
|
30
|
+
#/
|
31
|
+
#/ A <file> named example.1.ronn generates example.1.html (HTML manpage)
|
32
|
+
#/ and example.1 (roff manpage) by default.
|
33
|
+
require 'date'
|
34
|
+
require 'optparse'
|
35
|
+
|
36
|
+
def usage
|
37
|
+
puts File.readlines(__FILE__).
|
38
|
+
grep(/^#\/.*/).
|
39
|
+
map { |line| line.chomp[3..-1] }.
|
40
|
+
join("\n")
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Libraries and LOAD_PATH shenanigans
|
45
|
+
|
46
|
+
begin
|
47
|
+
require 'rdiscount'
|
48
|
+
require 'hpricot'
|
49
|
+
require 'ronn'
|
50
|
+
rescue LoadError => boom
|
51
|
+
if boom.to_s =~ /ronn/
|
52
|
+
libdir = File.expand_path("../../lib", __FILE__).sub(%r|^#{Dir.pwd}/|, './')
|
53
|
+
if File.directory?(libdir) && !$:.include?(libdir)
|
54
|
+
warn "warn: #{boom}. adding #{libdir} to RUBYLIB ..."
|
55
|
+
$:.unshift libdir
|
56
|
+
retry
|
57
|
+
end
|
58
|
+
elsif !defined?(Gem)
|
59
|
+
warn "warn: #{boom}. loading rubygems ..."
|
60
|
+
require 'rubygems'
|
61
|
+
retry
|
62
|
+
end
|
63
|
+
abort boom.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Argument defaults
|
68
|
+
|
69
|
+
build = true
|
70
|
+
view = false
|
71
|
+
server = false
|
72
|
+
formats = nil
|
73
|
+
options = {}
|
74
|
+
write_index = false
|
75
|
+
styles = %w[man]
|
76
|
+
groff = "groff -Wall -mtty-char -mandoc -Tascii"
|
77
|
+
pager = ENV['MANPAGER'] || ENV['PAGER'] || 'more'
|
78
|
+
|
79
|
+
##
|
80
|
+
# Environment variables
|
81
|
+
|
82
|
+
%w[manual organization date].each do |attribute|
|
83
|
+
value = ENV["RONN_#{attribute.upcase}"]
|
84
|
+
next if value.nil? or value.empty?
|
85
|
+
options[attribute] = value
|
86
|
+
end
|
87
|
+
|
88
|
+
##
|
89
|
+
# Argument parsing
|
90
|
+
|
91
|
+
ARGV.options do |argv|
|
92
|
+
# modes
|
93
|
+
argv.on("--pipe") { build = server = false }
|
94
|
+
argv.on("-b", "--build") { build = true; server = false }
|
95
|
+
argv.on("-m", "--man") { build = server = false; view = true }
|
96
|
+
argv.on("-S", "--server") { build = view = false; server = true }
|
97
|
+
argv.on("-i", "--index") { write_index = true }
|
98
|
+
|
99
|
+
# format options
|
100
|
+
argv.on("-r", "--roff") { (formats ||= []) << 'roff' }
|
101
|
+
argv.on("-5", "--html") { (formats ||= []) << 'html' }
|
102
|
+
argv.on("-f", "--fragment") { (formats ||= []) << 'html_fragment' }
|
103
|
+
argv.on("--markdown") { (formats ||= []) << 'markdown' }
|
104
|
+
|
105
|
+
# html output options
|
106
|
+
argv.on("-s", "--style=V") { |val| styles += val.split(/[, \n]+/) }
|
107
|
+
|
108
|
+
# manual attribute options
|
109
|
+
%w[name section manual organization date].each do |attribute|
|
110
|
+
argv.on("--#{attribute}=VALUE") { |val| options[attribute] = val }
|
111
|
+
end
|
112
|
+
|
113
|
+
# misc
|
114
|
+
argv.on("-w", "--warnings") { groff += ' -ww' }
|
115
|
+
argv.on("-W") { groff += ' -Ww' }
|
116
|
+
argv.on("-v", "--version") do
|
117
|
+
require 'ronn'
|
118
|
+
if Ronn.release?
|
119
|
+
printf "Ronn v%s\n", Ronn::VERSION
|
120
|
+
else
|
121
|
+
printf "Ronn v%s (%s)\n", Ronn::VERSION, Ronn::REV
|
122
|
+
end
|
123
|
+
printf "http://github.com/rtomayko/ronn/tree/%s\n", Ronn.revision
|
124
|
+
exit 0
|
125
|
+
end
|
126
|
+
argv.on_tail("--help") { usage ; exit 0 }
|
127
|
+
argv.parse!
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Modes, Formats, Options
|
132
|
+
|
133
|
+
case
|
134
|
+
when ARGV.empty? && $stdin.tty?
|
135
|
+
usage
|
136
|
+
exit 2
|
137
|
+
when ARGV.empty? && !server
|
138
|
+
ARGV.push '-'
|
139
|
+
build = false
|
140
|
+
formats ||= %w[roff]
|
141
|
+
when view
|
142
|
+
formats ||= %w[roff]
|
143
|
+
when build
|
144
|
+
formats ||= %w[roff html]
|
145
|
+
end
|
146
|
+
formats ||= []
|
147
|
+
formats.delete('html') if formats.include?('html_fragment')
|
148
|
+
|
149
|
+
options['date'] &&= Date.strptime(options['date'], '%Y-%m-%d')
|
150
|
+
options['styles'] = styles
|
151
|
+
|
152
|
+
##
|
153
|
+
# Server
|
154
|
+
|
155
|
+
if server
|
156
|
+
require 'ronn/server'
|
157
|
+
Ronn::Server.run(ARGV, options)
|
158
|
+
exit 0
|
159
|
+
end
|
160
|
+
|
161
|
+
##
|
162
|
+
# Build Pipeline
|
163
|
+
|
164
|
+
pid = nil
|
165
|
+
wr = STDOUT
|
166
|
+
documents = ARGV.map { |file| Ronn::Document.new(file, options) }
|
167
|
+
documents.each do |doc|
|
168
|
+
# setup the man pipeline if the --man option was specified
|
169
|
+
if view && !build
|
170
|
+
rd, wr = IO.pipe
|
171
|
+
if pid = fork
|
172
|
+
rd.close
|
173
|
+
else
|
174
|
+
wr.close
|
175
|
+
STDIN.reopen rd
|
176
|
+
exec "#{groff} | #{pager}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# write output for each format
|
181
|
+
formats.each do |format|
|
182
|
+
if build
|
183
|
+
path = doc.path_for(format)
|
184
|
+
case format
|
185
|
+
when 'html'
|
186
|
+
warn "%9s: %-43s%15s" % [format, path, '+' + doc.styles.join(',')]
|
187
|
+
when 'roff', 'html_fragment', 'markdown'
|
188
|
+
warn "%9s: %-43s" % [format, path]
|
189
|
+
end
|
190
|
+
|
191
|
+
output = doc.convert(format)
|
192
|
+
File.open(path, 'wb') { |f| f.puts(output) }
|
193
|
+
|
194
|
+
if format == 'roff'
|
195
|
+
if view
|
196
|
+
system "man #{path}"
|
197
|
+
else
|
198
|
+
system "#{groff} <#{path} >/dev/null"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
else
|
202
|
+
output = doc.convert(format)
|
203
|
+
wr.puts(output)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# wait for children to exit
|
208
|
+
if pid
|
209
|
+
wr.close
|
210
|
+
Process.wait
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Write index.txt files
|
215
|
+
|
216
|
+
if write_index
|
217
|
+
indexes = documents.map { |doc| doc.index }.uniq
|
218
|
+
indexes.each do |index|
|
219
|
+
File.open(index.path, 'wb') do |fd|
|
220
|
+
fd.puts(index.to_text)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
data/config.ru
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#\ -p 1207
|
2
|
+
$: << File.expand_path('../lib', __FILE__)
|
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)
|
data/lib/ronn.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# Ronn is a humane text format and toolchain for authoring manpages (and
|
2
|
+
# things that appear as manpages from a distance). Use it to build /
|
3
|
+
# install standard Unix roff(7) formatted manpages or to generate
|
4
|
+
# beautiful HTML manpages.
|
5
|
+
module Ronn
|
6
|
+
autoload :Document, 'ronn/document'
|
7
|
+
autoload :Index, 'ronn/index'
|
8
|
+
autoload :Template, 'ronn/template'
|
9
|
+
autoload :Roff, 'ronn/roff'
|
10
|
+
autoload :Server, 'ronn/server'
|
11
|
+
|
12
|
+
# Create a new Ronn::Document for the given ronn file. See
|
13
|
+
# Ronn::Document.new for usage information.
|
14
|
+
def self.new(filename, attributes={}, &block)
|
15
|
+
Document.new(filename, attributes, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
# truthy when this a release (\d.\d.\d) version.
|
19
|
+
def self.release?
|
20
|
+
revision != '' && !revision.include?('-')
|
21
|
+
end
|
22
|
+
|
23
|
+
# version: 0.6.11
|
24
|
+
#
|
25
|
+
# A semantic version number based on the git revision. The third element
|
26
|
+
# of the version is incremented by the commit offset, such that version
|
27
|
+
# 0.6.6-5-gdacd74b => 0.6.11
|
28
|
+
def self.version
|
29
|
+
ver = revision[/^[0-9.-]+/].split(/[.-]/).map { |p| p.to_i }
|
30
|
+
ver[2] += ver.pop while ver.size > 3
|
31
|
+
ver.join('.')
|
32
|
+
end
|
33
|
+
|
34
|
+
# revision: 0.6.6-5-gdacd74b
|
35
|
+
# revision: 0.6.25
|
36
|
+
#
|
37
|
+
# The string revision as reported by: git-describe --tags. This is just the
|
38
|
+
# tag name when a tag references the HEAD commit (0.6.25). When the HEAD
|
39
|
+
# commit is not tagged, this is a "<tag>-<offset>-<sha1>" string:
|
40
|
+
# <tag> - closest tag name
|
41
|
+
# <offset> - number of commits ahead of <tag>
|
42
|
+
# <sha1> - 7c short SHA1 for HEAD
|
43
|
+
def self.revision
|
44
|
+
REV
|
45
|
+
end
|
46
|
+
|
47
|
+
# value generated by: rake rev
|
48
|
+
REV = '0.7.4'
|
49
|
+
VERSION = version
|
50
|
+
end
|
@@ -0,0 +1,495 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'cgi'
|
3
|
+
require 'hpricot'
|
4
|
+
require 'rdiscount'
|
5
|
+
require 'ronn/index'
|
6
|
+
require 'ronn/roff'
|
7
|
+
require 'ronn/template'
|
8
|
+
require 'ronn/utils'
|
9
|
+
|
10
|
+
module Ronn
|
11
|
+
# The Document class can be used to load and inspect a ronn document
|
12
|
+
# and to convert a ronn document into other formats, like roff or
|
13
|
+
# HTML.
|
14
|
+
#
|
15
|
+
# Ronn files may optionally follow the naming convention:
|
16
|
+
# "<name>.<section>.ronn". The <name> and <section> are used in
|
17
|
+
# generated documentation unless overridden by the information
|
18
|
+
# extracted from the document's name section.
|
19
|
+
class Document
|
20
|
+
include Ronn::Utils
|
21
|
+
|
22
|
+
# Path to the Ronn document. This may be '-' or nil when the Ronn::Document
|
23
|
+
# object is created with a stream.
|
24
|
+
attr_reader :path
|
25
|
+
|
26
|
+
# The raw input data, read from path or stream and unmodified.
|
27
|
+
attr_reader :data
|
28
|
+
|
29
|
+
# The index used to resolve man and file references.
|
30
|
+
attr_accessor :index
|
31
|
+
|
32
|
+
# The man pages name: usually a single word name of
|
33
|
+
# a program or filename; displayed along with the section in
|
34
|
+
# the left and right portions of the header as well as the bottom
|
35
|
+
# right section of the footer.
|
36
|
+
attr_accessor :name
|
37
|
+
|
38
|
+
# The man page's section: a string whose first character
|
39
|
+
# is numeric; displayed in parenthesis along with the name.
|
40
|
+
attr_accessor :section
|
41
|
+
|
42
|
+
# Single sentence description of the thing being described
|
43
|
+
# by this man page; displayed in the NAME section.
|
44
|
+
attr_accessor :tagline
|
45
|
+
|
46
|
+
# The manual this document belongs to; center displayed in
|
47
|
+
# the header.
|
48
|
+
attr_accessor :manual
|
49
|
+
|
50
|
+
# The name of the group, organization, or individual responsible
|
51
|
+
# for this document; displayed in the left portion of the footer.
|
52
|
+
attr_accessor :organization
|
53
|
+
|
54
|
+
# The date the document was published; center displayed in
|
55
|
+
# the document footer.
|
56
|
+
attr_accessor :date
|
57
|
+
|
58
|
+
# Array of style modules to apply to the document.
|
59
|
+
attr_accessor :styles
|
60
|
+
|
61
|
+
# Create a Ronn::Document given a path or with the data returned by
|
62
|
+
# calling the block. The document is loaded and preprocessed before
|
63
|
+
# the intialize method returns. The attributes hash may contain values
|
64
|
+
# for any writeable attributes defined on this class.
|
65
|
+
def initialize(path=nil, attributes={}, &block)
|
66
|
+
@path = path
|
67
|
+
@basename = path.to_s =~ /^-?$/ ? nil : File.basename(path)
|
68
|
+
@reader = block ||
|
69
|
+
lambda do |f|
|
70
|
+
if ['-', nil].include?(f)
|
71
|
+
STDIN.read
|
72
|
+
else
|
73
|
+
File.read(f)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
@data = @reader.call(path)
|
77
|
+
@name, @section, @tagline = sniff
|
78
|
+
|
79
|
+
@styles = %w[man]
|
80
|
+
@manual, @organization, @date = nil
|
81
|
+
@markdown, @input_html, @html = nil
|
82
|
+
@index = Ronn::Index[path || '.']
|
83
|
+
@index.add_manual(self) if path && name
|
84
|
+
|
85
|
+
attributes.each { |attr_name,value| send("#{attr_name}=", value) }
|
86
|
+
end
|
87
|
+
|
88
|
+
# Generate a file basename of the form "<name>.<section>.<type>"
|
89
|
+
# for the given file extension. Uses the name and section from
|
90
|
+
# the source file path but falls back on the name and section
|
91
|
+
# defined in the document.
|
92
|
+
def basename(type=nil)
|
93
|
+
type = nil if ['', 'roff'].include?(type.to_s)
|
94
|
+
[path_name || @name, path_section || @section, type].
|
95
|
+
compact.join('.')
|
96
|
+
end
|
97
|
+
|
98
|
+
# Construct a path for a file near the source file. Uses the
|
99
|
+
# Document#basename method to generate the basename part and
|
100
|
+
# appends it to the dirname of the source document.
|
101
|
+
def path_for(type=nil)
|
102
|
+
if @basename
|
103
|
+
File.join(File.dirname(path), basename(type))
|
104
|
+
else
|
105
|
+
basename(type)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns the <name> part of the path, or nil when no path is
|
110
|
+
# available. This is used as the manual page name when the
|
111
|
+
# file contents do not include a name section.
|
112
|
+
def path_name
|
113
|
+
@basename[/^[^.]+/] if @basename
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns the <section> part of the path, or nil when
|
117
|
+
# no path is available.
|
118
|
+
def path_section
|
119
|
+
$1 if @basename.to_s =~ /\.(\d\w*)\./
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns the manual page name based first on the document's
|
123
|
+
# contents and then on the path name.
|
124
|
+
def name
|
125
|
+
@name || path_name
|
126
|
+
end
|
127
|
+
|
128
|
+
# Truthful when the name was extracted from the name section
|
129
|
+
# of the document.
|
130
|
+
def name?
|
131
|
+
!@name.nil?
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns the manual page section based first on the document's
|
135
|
+
# contents and then on the path name.
|
136
|
+
def section
|
137
|
+
@section || path_section
|
138
|
+
end
|
139
|
+
|
140
|
+
# True when the section number was extracted from the name
|
141
|
+
# section of the document.
|
142
|
+
def section?
|
143
|
+
!@section.nil?
|
144
|
+
end
|
145
|
+
|
146
|
+
# The name used to reference this manual.
|
147
|
+
def reference_name
|
148
|
+
name + (section && "(#{section})").to_s
|
149
|
+
end
|
150
|
+
|
151
|
+
# Truthful when the document started with an h1 but did not follow
|
152
|
+
# the "<name>(<sect>) -- <tagline>" convention. We assume this is some kind
|
153
|
+
# of custom title.
|
154
|
+
def title?
|
155
|
+
!name? && tagline
|
156
|
+
end
|
157
|
+
|
158
|
+
# The document's title when no name section was defined. When a name section
|
159
|
+
# exists, this value is nil.
|
160
|
+
def title
|
161
|
+
@tagline if !name?
|
162
|
+
end
|
163
|
+
|
164
|
+
# The date the man page was published. If not set explicitly,
|
165
|
+
# this is the file's modified time or, if no file is given,
|
166
|
+
# the current time.
|
167
|
+
def date
|
168
|
+
return @date if @date
|
169
|
+
return File.mtime(path) if File.exist?(path)
|
170
|
+
Time.now
|
171
|
+
end
|
172
|
+
|
173
|
+
# Retrieve a list of top-level section headings in the document and return
|
174
|
+
# as an array of +[id, text]+ tuples, where +id+ is the element's generated
|
175
|
+
# id and +text+ is the inner text of the heading element.
|
176
|
+
def toc
|
177
|
+
@toc ||=
|
178
|
+
html.search('h2[@id]').map { |h2| [h2.attributes['id'], h2.inner_text] }
|
179
|
+
end
|
180
|
+
alias section_heads toc
|
181
|
+
|
182
|
+
# Styles to insert in the generated HTML output. This is a simple Array of
|
183
|
+
# string module names or file paths.
|
184
|
+
def styles=(styles)
|
185
|
+
@styles = (%w[man] + styles).uniq
|
186
|
+
end
|
187
|
+
|
188
|
+
# Sniff the document header and extract basic document metadata. Return a
|
189
|
+
# tuple of the form: [name, section, description], where missing information
|
190
|
+
# is represented by nil and any element may be missing.
|
191
|
+
def sniff
|
192
|
+
html = Markdown.new(data[0, 512]).to_html
|
193
|
+
heading, html = html.split("</h1>\n", 2)
|
194
|
+
return [nil, nil, nil] if html.nil?
|
195
|
+
|
196
|
+
case heading
|
197
|
+
when /([\w_.\[\]~+=@:-]+)\s*\((\d\w*)\)\s*-+\s*(.*)/
|
198
|
+
# name(section) -- description
|
199
|
+
[$1, $2, $3]
|
200
|
+
when /([\w_.\[\]~+=@:-]+)\s+-+\s+(.*)/
|
201
|
+
# name -- description
|
202
|
+
[$1, nil, $2]
|
203
|
+
else
|
204
|
+
# description
|
205
|
+
[nil, nil, heading.sub('<h1>', '')]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Preprocessed markdown input text.
|
210
|
+
def markdown
|
211
|
+
@markdown ||= process_markdown!
|
212
|
+
end
|
213
|
+
|
214
|
+
# A Hpricot::Document for the manual content fragment.
|
215
|
+
def html
|
216
|
+
@html ||= process_html!
|
217
|
+
end
|
218
|
+
|
219
|
+
# Convert the document to :roff, :html, or :html_fragment and
|
220
|
+
# return the result as a string.
|
221
|
+
def convert(format)
|
222
|
+
send "to_#{format}"
|
223
|
+
end
|
224
|
+
|
225
|
+
# Convert the document to roff and return the result as a string.
|
226
|
+
def to_roff
|
227
|
+
RoffFilter.new(
|
228
|
+
to_html_fragment(wrap_class=nil),
|
229
|
+
name, section, tagline,
|
230
|
+
manual, organization, date
|
231
|
+
).to_s
|
232
|
+
end
|
233
|
+
|
234
|
+
# Convert the document to HTML and return the result as a string.
|
235
|
+
def to_html
|
236
|
+
if layout = ENV['RONN_LAYOUT']
|
237
|
+
if !File.exist?(layout_path = File.expand_path(layout))
|
238
|
+
warn "warn: can't find #{layout}, using default layout."
|
239
|
+
layout_path = nil
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
template = Ronn::Template.new(self)
|
244
|
+
template.context.push :html => to_html_fragment(wrap_class=nil)
|
245
|
+
template.render(layout_path || 'default')
|
246
|
+
end
|
247
|
+
|
248
|
+
# Convert the document to HTML and return the result
|
249
|
+
# as a string. The HTML does not include <html>, <head>,
|
250
|
+
# or <style> tags.
|
251
|
+
def to_html_fragment(wrap_class='mp')
|
252
|
+
return html.to_s if wrap_class.nil?
|
253
|
+
[
|
254
|
+
"<div class='#{wrap_class}'>",
|
255
|
+
html.to_s,
|
256
|
+
"</div>"
|
257
|
+
].join("\n")
|
258
|
+
end
|
259
|
+
|
260
|
+
def to_markdown
|
261
|
+
markdown
|
262
|
+
end
|
263
|
+
|
264
|
+
def to_h
|
265
|
+
%w[name section tagline manual organization date styles toc].
|
266
|
+
inject({}) { |hash, name| hash[name] = send(name); hash }
|
267
|
+
end
|
268
|
+
|
269
|
+
def to_yaml
|
270
|
+
require 'yaml'
|
271
|
+
to_h.to_yaml
|
272
|
+
end
|
273
|
+
|
274
|
+
def to_json
|
275
|
+
require 'json'
|
276
|
+
to_h.merge('date' => date.iso8601).to_json
|
277
|
+
end
|
278
|
+
|
279
|
+
protected
|
280
|
+
##
|
281
|
+
# Document Processing
|
282
|
+
|
283
|
+
# Parse the document and extract the name, section, and tagline from its
|
284
|
+
# contents. This is called while the object is being initialized.
|
285
|
+
def preprocess!
|
286
|
+
input_html
|
287
|
+
nil
|
288
|
+
end
|
289
|
+
|
290
|
+
def input_html
|
291
|
+
@input_html ||= strip_heading(Markdown.new(markdown).to_html)
|
292
|
+
end
|
293
|
+
|
294
|
+
def strip_heading(html)
|
295
|
+
heading, html = html.split("</h1>\n", 2)
|
296
|
+
html || heading
|
297
|
+
end
|
298
|
+
|
299
|
+
def process_markdown!
|
300
|
+
markdown = markdown_filter_heading_anchors(self.data)
|
301
|
+
markdown_filter_link_index(markdown)
|
302
|
+
markdown_filter_angle_quotes(markdown)
|
303
|
+
end
|
304
|
+
|
305
|
+
def process_html!
|
306
|
+
@html = Hpricot(input_html)
|
307
|
+
html_filter_angle_quotes
|
308
|
+
html_filter_definition_lists
|
309
|
+
html_filter_inject_name_section
|
310
|
+
html_filter_heading_anchors
|
311
|
+
html_filter_annotate_bare_links
|
312
|
+
html_filter_manual_reference_links
|
313
|
+
@html
|
314
|
+
end
|
315
|
+
|
316
|
+
##
|
317
|
+
# Filters
|
318
|
+
|
319
|
+
# Appends all index links to the end of the document as Markdown reference
|
320
|
+
# links. This lets us use [foo(3)][] syntax to link to index entries.
|
321
|
+
def markdown_filter_link_index(markdown)
|
322
|
+
return markdown if index.nil? || index.empty?
|
323
|
+
markdown << "\n\n"
|
324
|
+
index.each { |ref| markdown << "[#{ref.name}]: #{ref.url}\n" }
|
325
|
+
end
|
326
|
+
|
327
|
+
# Add [id]: #ANCHOR elements to the markdown source text for all sections.
|
328
|
+
# This lets us use the [SECTION-REF][] syntax
|
329
|
+
def markdown_filter_heading_anchors(markdown)
|
330
|
+
first = true
|
331
|
+
markdown.split("\n").grep(/^[#]{2,5} +[\w '-]+[# ]*$/).each do |line|
|
332
|
+
markdown << "\n\n" if first
|
333
|
+
first = false
|
334
|
+
title = line.gsub(/[^\w -]/, '').strip
|
335
|
+
anchor = title.gsub(/\W+/, '-').gsub(/(^-+|-+$)/, '')
|
336
|
+
markdown << "[#{title}]: ##{anchor} \"#{title}\"\n"
|
337
|
+
end
|
338
|
+
markdown
|
339
|
+
end
|
340
|
+
|
341
|
+
# Convert <WORD> to <var>WORD</var> but only if WORD isn't an HTML tag.
|
342
|
+
def markdown_filter_angle_quotes(markdown)
|
343
|
+
markdown.gsub(/\<([^:.\/]+?)\>/) do |match|
|
344
|
+
contents = $1
|
345
|
+
tag, attrs = contents.split(' ', 2)
|
346
|
+
if attrs =~ /\/=/ || html_element?(tag.sub(/^\//, '')) ||
|
347
|
+
data.include?("</#{tag}>")
|
348
|
+
match.to_s
|
349
|
+
else
|
350
|
+
"<var>#{contents}</var>"
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
# Perform angle quote (<THESE>) post filtering.
|
356
|
+
def html_filter_angle_quotes
|
357
|
+
# convert all angle quote vars nested in code blocks
|
358
|
+
# back to the original text
|
359
|
+
@html.search('code').search('text()').each do |node|
|
360
|
+
next unless node.to_html.include?('var>')
|
361
|
+
new =
|
362
|
+
node.to_html.
|
363
|
+
gsub('<var>', '<').
|
364
|
+
gsub("</var>", '>')
|
365
|
+
node.swap(new)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# Convert special format unordered lists to definition lists.
|
370
|
+
def html_filter_definition_lists
|
371
|
+
# process all unordered lists depth-first
|
372
|
+
@html.search('ul').to_a.reverse.each do |ul|
|
373
|
+
items = ul.search('li')
|
374
|
+
next if items.any? { |item| item.inner_text.split("\n", 2).first !~ /:$/ }
|
375
|
+
|
376
|
+
ul.name = 'dl'
|
377
|
+
items.each do |item|
|
378
|
+
if child = item.at('p')
|
379
|
+
wrap = '<p></p>'
|
380
|
+
container = child
|
381
|
+
else
|
382
|
+
wrap = '<dd></dd>'
|
383
|
+
container = item
|
384
|
+
end
|
385
|
+
term, definition = container.inner_html.split(":\n", 2)
|
386
|
+
|
387
|
+
dt = item.before("<dt>#{term}</dt>").first
|
388
|
+
dt.attributes['class'] = 'flush' if dt.inner_text.length <= 7
|
389
|
+
|
390
|
+
item.name = 'dd'
|
391
|
+
container.swap(wrap.sub(/></, ">#{definition}<"))
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
def html_filter_inject_name_section
|
397
|
+
markup =
|
398
|
+
if title?
|
399
|
+
"<h1>#{title}</h1>"
|
400
|
+
elsif name
|
401
|
+
"<h2>NAME</h2>\n" +
|
402
|
+
"<p class='man-name'>\n <code>#{name}</code>" +
|
403
|
+
(tagline ? " - <span class='man-whatis'>#{tagline}</span>\n" : "\n") +
|
404
|
+
"</p>\n"
|
405
|
+
end
|
406
|
+
if markup
|
407
|
+
if @html.children
|
408
|
+
@html.at("*").before(markup)
|
409
|
+
else
|
410
|
+
@html = Hpricot(markup)
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
# Add URL anchors to all HTML heading elements.
|
416
|
+
def html_filter_heading_anchors
|
417
|
+
@html.search('h2|h3|h4|h5|h6').not('[@id]').each do |heading|
|
418
|
+
heading.set_attribute('id', heading.inner_text.gsub(/\W+/, '-'))
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
# Add a 'data-bare-link' attribute to hyperlinks
|
423
|
+
# whose text labels are the same as their href URLs.
|
424
|
+
def html_filter_annotate_bare_links
|
425
|
+
@html.search('a[@href]').each do |node|
|
426
|
+
href = node.attributes['href']
|
427
|
+
text = node.inner_text
|
428
|
+
|
429
|
+
if href == text ||
|
430
|
+
href[0] == ?# ||
|
431
|
+
CGI.unescapeHTML(href) == "mailto:#{CGI.unescapeHTML(text)}"
|
432
|
+
then
|
433
|
+
node.set_attribute('data-bare-link', 'true')
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
# Convert text of the form "name(section)" or "<code>name</code>(section)
|
439
|
+
# to a hyperlink. The URL is obtained from the index.
|
440
|
+
def html_filter_manual_reference_links
|
441
|
+
return if index.nil?
|
442
|
+
name_pattern = "[0-9A-Za-z_:.+=@~-]+"
|
443
|
+
|
444
|
+
# Convert "name(section)" by traversing text nodes searching for
|
445
|
+
# text that fits the pattern. This is the original implementation.
|
446
|
+
@html.search('text()').each do |node|
|
447
|
+
next if !node.content.include?(')')
|
448
|
+
next if %w[pre code h1 h2 h3].include?(node.parent.name)
|
449
|
+
next if child_of?(node, 'a')
|
450
|
+
node.swap(node.content.gsub(/(#{name_pattern})(\(\d+\w*\))/) {
|
451
|
+
html_build_manual_reference_link($1, $2)
|
452
|
+
})
|
453
|
+
end
|
454
|
+
|
455
|
+
# Convert "<code>name</code>(section)" by traversing <code> nodes.
|
456
|
+
# For each one that contains exactly an acceptable manual page name,
|
457
|
+
# the next sibling is checked and must be a text node beginning
|
458
|
+
# with a valid section in parentheses.
|
459
|
+
@html.search('code').each do |node|
|
460
|
+
next if %w[pre code h1 h2 h3].include?(node.parent.name)
|
461
|
+
next if child_of?(node, 'a')
|
462
|
+
next unless node.inner_text =~ /^#{name_pattern}$/
|
463
|
+
sibling = node.next
|
464
|
+
if sibling
|
465
|
+
next unless sibling.text?
|
466
|
+
next unless sibling.content =~ /^\((\d+\w*)\)/
|
467
|
+
node.swap(html_build_manual_reference_link(node, "(#{$1})"))
|
468
|
+
sibling.content = sibling.content.gsub(/^\(\d+\w*\)/, '')
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
end
|
473
|
+
|
474
|
+
# HTMLize the manual page reference. The result is an <a> if the
|
475
|
+
# page appears in the index, otherwise it is a <span>. The first
|
476
|
+
# argument may be an HTML element or a string. The second should
|
477
|
+
# be a string of the form "(#{section})".
|
478
|
+
def html_build_manual_reference_link(name_or_node, section)
|
479
|
+
name = if name_or_node.respond_to?(:inner_text)
|
480
|
+
name_or_node.inner_text
|
481
|
+
else
|
482
|
+
name_or_node
|
483
|
+
end
|
484
|
+
if ref = index["#{name}#{section}"]
|
485
|
+
"<a class='man-ref' href='#{ref.url}'>#{name_or_node
|
486
|
+
}<span class='s'>#{section}</span></a>"
|
487
|
+
else
|
488
|
+
# warn "warn: manual reference not defined: '#{name}#{section}'"
|
489
|
+
"<span class='man-ref'>#{name_or_node}<span class='s'>#{section
|
490
|
+
}</span></span>"
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
end
|
495
|
+
end
|