nronn 0.10.1.pre2
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 +230 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +72 -0
- data/INSTALLING.md +92 -0
- data/LICENSE.txt +12 -0
- data/README.md +131 -0
- data/Rakefile +153 -0
- data/bin/ronn +253 -0
- data/completion/bash/ronn +32 -0
- data/completion/zsh/_ronn +24 -0
- data/config.ru +15 -0
- data/lib/ronn/document.rb +530 -0
- data/lib/ronn/index.rb +180 -0
- data/lib/ronn/roff.rb +393 -0
- data/lib/ronn/server.rb +67 -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/template.rb +173 -0
- data/lib/ronn/utils.rb +57 -0
- data/lib/ronn.rb +47 -0
- data/man/index.html +78 -0
- data/man/index.txt +15 -0
- data/man/ronn-format.7 +145 -0
- data/man/ronn-format.7.ronn +157 -0
- data/man/ronn.1 +227 -0
- data/man/ronn.1.ronn +316 -0
- data/nronn.gemspec +136 -0
- data/test/angle_bracket_syntax.html +27 -0
- data/test/angle_bracket_syntax.roff +24 -0
- data/test/angle_bracket_syntax.ronn +22 -0
- data/test/backticks.html +14 -0
- data/test/backticks.ronn +10 -0
- data/test/basic_document.html +8 -0
- data/test/basic_document.ronn +4 -0
- data/test/circumflexes.ronn +1 -0
- data/test/code_blocks.html +38 -0
- data/test/code_blocks.roff +38 -0
- data/test/code_blocks.ronn +41 -0
- data/test/code_blocks_regression +19 -0
- data/test/code_blocks_regression.html +38 -0
- data/test/code_blocks_regression.ronn +40 -0
- data/test/contest.rb +70 -0
- data/test/custom_title_document.html +6 -0
- data/test/custom_title_document.ronn +5 -0
- data/test/definition_list_syntax.html +25 -0
- data/test/definition_list_syntax.roff +19 -0
- data/test/definition_list_syntax.ronn +18 -0
- data/test/dots_at_line_start_test.roff +19 -0
- data/test/dots_at_line_start_test.ronn +12 -0
- data/test/ellipses.roff +7 -0
- data/test/ellipses.ronn +7 -0
- data/test/entity_encoding_test.html +42 -0
- data/test/entity_encoding_test.roff +51 -0
- data/test/entity_encoding_test.ronn +34 -0
- data/test/index.txt +8 -0
- data/test/markdown_syntax.html +954 -0
- data/test/markdown_syntax.roff +907 -0
- data/test/markdown_syntax.ronn +881 -0
- data/test/middle_paragraph.html +14 -0
- data/test/middle_paragraph.roff +9 -0
- data/test/middle_paragraph.ronn +10 -0
- data/test/missing_spaces.roff +7 -0
- data/test/missing_spaces.ronn +2 -0
- data/test/nested_list.ronn +19 -0
- data/test/nested_list_with_code.html +14 -0
- data/test/nested_list_with_code.roff +11 -0
- data/test/nested_list_with_code.ronn +6 -0
- data/test/ordered_list.html +28 -0
- data/test/ordered_list.roff +25 -0
- data/test/ordered_list.ronn +21 -0
- data/test/page.with.periods.in.name.5.ronn +4 -0
- data/test/pre_block_with_quotes.roff +8 -0
- data/test/pre_block_with_quotes.ronn +6 -0
- data/test/section_reference_links.html +16 -0
- data/test/section_reference_links.roff +7 -0
- data/test/section_reference_links.ronn +12 -0
- data/test/single_quotes.html +11 -0
- data/test/single_quotes.roff +5 -0
- data/test/single_quotes.ronn +9 -0
- data/test/tables.ronn +24 -0
- data/test/test_ronn.rb +124 -0
- data/test/test_ronn_document.rb +186 -0
- data/test/test_ronn_index.rb +73 -0
- data/test/titleless_document.html +9 -0
- data/test/titleless_document.ronn +3 -0
- data/test/underline_spacing_test.roff +13 -0
- data/test/underline_spacing_test.ronn +11 -0
- metadata +309 -0
@@ -0,0 +1,530 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'cgi'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'kramdown'
|
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, in which case stdin will be read.
|
24
|
+
attr_reader :path
|
25
|
+
|
26
|
+
# Encoding that the Ronn document is in
|
27
|
+
attr_accessor :encoding
|
28
|
+
|
29
|
+
# The raw input data, read from path or stream and unmodified.
|
30
|
+
attr_reader :data
|
31
|
+
|
32
|
+
# The index used to resolve man and file references.
|
33
|
+
attr_accessor :index
|
34
|
+
|
35
|
+
# The man pages name: usually a single word name of
|
36
|
+
# a program or filename; displayed along with the section in
|
37
|
+
# the left and right portions of the header as well as the bottom
|
38
|
+
# right section of the footer.
|
39
|
+
attr_writer :name
|
40
|
+
|
41
|
+
# The man page's section: a string whose first character
|
42
|
+
# is numeric; displayed in parenthesis along with the name.
|
43
|
+
attr_writer :section
|
44
|
+
|
45
|
+
# Single sentence description of the thing being described
|
46
|
+
# by this man page; displayed in the NAME section.
|
47
|
+
attr_accessor :tagline
|
48
|
+
|
49
|
+
# The manual this document belongs to; center displayed in
|
50
|
+
# the header.
|
51
|
+
attr_accessor :manual
|
52
|
+
|
53
|
+
# The name of the group, organization, or individual responsible
|
54
|
+
# for this document; displayed in the left portion of the footer.
|
55
|
+
attr_accessor :organization
|
56
|
+
|
57
|
+
# The date the document was published; center displayed in
|
58
|
+
# the document footer.
|
59
|
+
attr_writer :date
|
60
|
+
|
61
|
+
# Array of style modules to apply to the document.
|
62
|
+
attr_reader :styles
|
63
|
+
|
64
|
+
# Output directory to write files to.
|
65
|
+
attr_accessor :outdir
|
66
|
+
|
67
|
+
# Create a Ronn::Document given a path or with the data returned by
|
68
|
+
# calling the block. The document is loaded and preprocessed before
|
69
|
+
# the intialize method returns. The attributes hash may contain values
|
70
|
+
# for any writeable attributes defined on this class.
|
71
|
+
def initialize(path = nil, attributes = {}, &block)
|
72
|
+
@path = path
|
73
|
+
@basename = path.to_s =~ /^-?$/ ? nil : File.basename(path)
|
74
|
+
@reader = block ||
|
75
|
+
lambda do |f|
|
76
|
+
if ['-', nil].include?(f)
|
77
|
+
$stdin.read
|
78
|
+
else
|
79
|
+
File.read(f, encoding: @encoding)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
@data = @reader.call(path)
|
83
|
+
@name, @section, @tagline = sniff
|
84
|
+
|
85
|
+
@styles = %w[man]
|
86
|
+
@manual, @organization, @date = nil
|
87
|
+
@markdown, @input_html, @html = nil
|
88
|
+
@index = Ronn::Index[path || '.']
|
89
|
+
@index.add_manual(self) if path && name
|
90
|
+
|
91
|
+
attributes.each { |attr_name, value| send("#{attr_name}=", value) }
|
92
|
+
end
|
93
|
+
|
94
|
+
# Generate a file basename of the form "<name>.<section>.<type>"
|
95
|
+
# for the given file extension. Uses the name and section from
|
96
|
+
# the source file path but falls back on the name and section
|
97
|
+
# defined in the document.
|
98
|
+
def basename(type = nil)
|
99
|
+
type = nil if ['', 'roff'].include?(type.to_s)
|
100
|
+
[path_name || @name, path_section || @section, type]
|
101
|
+
.compact.join('.')
|
102
|
+
end
|
103
|
+
|
104
|
+
# Construct a path for a file near the source file. Uses the
|
105
|
+
# Document#basename method to generate the basename part and
|
106
|
+
# appends it to the dirname of the source document.
|
107
|
+
def path_for(type = nil)
|
108
|
+
if @outdir
|
109
|
+
File.join(@outdir, basename(type))
|
110
|
+
elsif @basename
|
111
|
+
File.join(File.dirname(path), basename(type))
|
112
|
+
else
|
113
|
+
basename(type)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns the <name> part of the path, or nil when no path is
|
118
|
+
# available. This is used as the manual page name when the
|
119
|
+
# file contents do not include a name section.
|
120
|
+
def path_name
|
121
|
+
return unless @basename
|
122
|
+
|
123
|
+
parts = @basename.split('.')
|
124
|
+
parts.pop if parts.length > 1 && parts.last =~ /^\w+$/
|
125
|
+
parts.pop if parts.last =~ /^\d+$/
|
126
|
+
parts.join('.')
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns the <section> part of the path, or nil when
|
130
|
+
# no path is available.
|
131
|
+
def path_section
|
132
|
+
$1 if @basename.to_s =~ /\.(\d\w*)\./
|
133
|
+
end
|
134
|
+
|
135
|
+
# Returns the manual page name based first on the document's
|
136
|
+
# contents and then on the path name. Usually a single word name of
|
137
|
+
# a program or filename; displayed along with the section in
|
138
|
+
# the left and right portions of the header as well as the bottom
|
139
|
+
# right section of the footer.
|
140
|
+
def name
|
141
|
+
@name || path_name
|
142
|
+
end
|
143
|
+
|
144
|
+
# Truthful when the name was extracted from the name section
|
145
|
+
# of the document.
|
146
|
+
def name?
|
147
|
+
!@name.nil?
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns the manual page section based first on the document's
|
151
|
+
# contents and then on the path name. A string whose first character
|
152
|
+
# is numeric; displayed in parenthesis along with the name.
|
153
|
+
def section
|
154
|
+
@section || path_section
|
155
|
+
end
|
156
|
+
|
157
|
+
# True when the section number was extracted from the name
|
158
|
+
# section of the document.
|
159
|
+
def section?
|
160
|
+
!@section.nil?
|
161
|
+
end
|
162
|
+
|
163
|
+
# The name used to reference this manual.
|
164
|
+
def reference_name
|
165
|
+
name + (section && "(#{section})").to_s
|
166
|
+
end
|
167
|
+
|
168
|
+
# Truthful when the document started with an h1 but did not follow
|
169
|
+
# the "<name>(<sect>) -- <tagline>" convention. We assume this is some kind
|
170
|
+
# of custom title.
|
171
|
+
def title?
|
172
|
+
!name? && tagline
|
173
|
+
end
|
174
|
+
|
175
|
+
# The document's title when no name section was defined. When a name section
|
176
|
+
# exists, this value is nil.
|
177
|
+
def title
|
178
|
+
@tagline unless name?
|
179
|
+
end
|
180
|
+
|
181
|
+
# The date the man page was published. If not set explicitly,
|
182
|
+
# it first checks for "SOURCE_DATE_EPOCH" to support reproducible
|
183
|
+
# builds, then the file's modified time or, if no file is given,
|
184
|
+
# the current time. Center displayed in the document footer.
|
185
|
+
def date
|
186
|
+
return @date if @date
|
187
|
+
|
188
|
+
return Time.at(ENV['SOURCE_DATE_EPOCH'].to_i).gmtime if ENV['SOURCE_DATE_EPOCH']
|
189
|
+
|
190
|
+
return File.mtime(path) if File.exist?(path)
|
191
|
+
|
192
|
+
Time.now
|
193
|
+
end
|
194
|
+
|
195
|
+
# Retrieve a list of top-level section headings in the document and return
|
196
|
+
# as an array of +[id, text]+ tuples, where +id+ is the element's generated
|
197
|
+
# id and +text+ is the inner text of the heading element.
|
198
|
+
def toc
|
199
|
+
@toc ||=
|
200
|
+
html.search('h2[@id]').map { |h2| [h2.attributes['id'].content.upcase, h2.inner_text] }
|
201
|
+
end
|
202
|
+
alias section_heads toc
|
203
|
+
|
204
|
+
# Styles to insert in the generated HTML output. This is a simple Array of
|
205
|
+
# string module names or file paths.
|
206
|
+
def styles=(styles)
|
207
|
+
@styles = (%w[man] + styles).uniq
|
208
|
+
end
|
209
|
+
|
210
|
+
# Sniff the document header and extract basic document metadata. Return a
|
211
|
+
# tuple of the form: [name, section, description], where missing information
|
212
|
+
# is represented by nil and any element may be missing.
|
213
|
+
def sniff
|
214
|
+
html = Kramdown::Document.new(data[0, 512], auto_ids: false,
|
215
|
+
smart_quotes: ['apos', 'apos', 'quot', 'quot'],
|
216
|
+
typographic_symbols: { hellip: '...', ndash: '--', mdash: '--' }).to_html
|
217
|
+
heading, html = html.split("</h1>\n", 2)
|
218
|
+
return [nil, nil, nil] if html.nil?
|
219
|
+
|
220
|
+
case heading
|
221
|
+
when /([\w_.\[\]~+=@:-]+)\s*\((\d\w*)\)\s*-+\s*(.*)/
|
222
|
+
# name(section) -- description
|
223
|
+
[$1, $2, $3]
|
224
|
+
when /([\w_.\[\]~+=@:-]+)\s+-+\s+(.*)/
|
225
|
+
# name -- description
|
226
|
+
[$1, nil, $2]
|
227
|
+
else
|
228
|
+
# description
|
229
|
+
[nil, nil, heading.sub('<h1>', '')]
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Preprocessed markdown input text.
|
234
|
+
def markdown
|
235
|
+
@markdown ||= process_markdown!
|
236
|
+
end
|
237
|
+
|
238
|
+
# A Nokogiri DocumentFragment for the manual content fragment.
|
239
|
+
def html
|
240
|
+
@html ||= process_html!
|
241
|
+
end
|
242
|
+
|
243
|
+
# Convert the document to :roff, :html, or :html_fragment and
|
244
|
+
# return the result as a string.
|
245
|
+
def convert(format)
|
246
|
+
send "to_#{format}"
|
247
|
+
end
|
248
|
+
|
249
|
+
# Convert the document to roff and return the result as a string.
|
250
|
+
def to_roff
|
251
|
+
RoffFilter.new(
|
252
|
+
to_html_fragment(nil),
|
253
|
+
name, section, tagline,
|
254
|
+
manual, organization, date
|
255
|
+
).to_s
|
256
|
+
end
|
257
|
+
|
258
|
+
# Convert the document to HTML and return the result as a string.
|
259
|
+
# The returned string is a complete HTML document.
|
260
|
+
def to_html
|
261
|
+
layout = ENV['RONN_LAYOUT']
|
262
|
+
layout_path = nil
|
263
|
+
if layout
|
264
|
+
layout_path = File.expand_path(layout)
|
265
|
+
unless File.exist?(layout_path)
|
266
|
+
warn "warn: can't find #{layout}, using default layout."
|
267
|
+
layout_path = nil
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
template = Ronn::Template.new(self)
|
272
|
+
template.context.push html: to_html_fragment(nil)
|
273
|
+
template.render(layout_path || 'default')
|
274
|
+
end
|
275
|
+
|
276
|
+
# Convert the document to HTML and return the result
|
277
|
+
# as a string. The HTML does not include <html>, <head>,
|
278
|
+
# or <style> tags.
|
279
|
+
def to_html_fragment(wrap_class = 'mp')
|
280
|
+
frag_nodes = html.at('body').children
|
281
|
+
out = frag_nodes.to_s.rstrip
|
282
|
+
out = "<div class='#{wrap_class}'>#{out}\n</div>" unless wrap_class.nil?
|
283
|
+
out
|
284
|
+
end
|
285
|
+
|
286
|
+
def to_markdown
|
287
|
+
markdown
|
288
|
+
end
|
289
|
+
|
290
|
+
def to_h
|
291
|
+
%w[name section tagline manual organization date styles toc]
|
292
|
+
.each_with_object({}) { |name, hash| hash[name] = send(name) }
|
293
|
+
end
|
294
|
+
|
295
|
+
def to_yaml
|
296
|
+
require 'yaml'
|
297
|
+
to_h.to_yaml
|
298
|
+
end
|
299
|
+
|
300
|
+
def to_json(*_args)
|
301
|
+
require 'json'
|
302
|
+
to_h.merge('date' => date.iso8601).to_json
|
303
|
+
end
|
304
|
+
|
305
|
+
protected
|
306
|
+
|
307
|
+
##
|
308
|
+
# Document Processing
|
309
|
+
|
310
|
+
# Parse the document and extract the name, section, and tagline from its
|
311
|
+
# contents. This is called while the object is being initialized.
|
312
|
+
def preprocess!
|
313
|
+
input_html
|
314
|
+
nil
|
315
|
+
end
|
316
|
+
|
317
|
+
def input_html
|
318
|
+
@input_html ||= strip_heading(Kramdown::Document.new(markdown,
|
319
|
+
auto_ids: false,
|
320
|
+
input: 'GFM',
|
321
|
+
hard_wrap: 'false',
|
322
|
+
syntax_highlighter_opts: 'line_numbers: false',
|
323
|
+
smart_quotes: ['apos', 'apos', 'quot', 'quot'],
|
324
|
+
typographic_symbols: { hellip: '...', ndash: '--', mdash: '--' }).to_html)
|
325
|
+
end
|
326
|
+
|
327
|
+
def strip_heading(html)
|
328
|
+
heading, html = html.split("</h1>\n", 2)
|
329
|
+
html || heading
|
330
|
+
end
|
331
|
+
|
332
|
+
def process_markdown!
|
333
|
+
md = markdown_filter_heading_anchors(data)
|
334
|
+
md = markdown_filter_link_index(md)
|
335
|
+
markdown_filter_angle_quotes(md)
|
336
|
+
end
|
337
|
+
|
338
|
+
def process_html!
|
339
|
+
wrapped_html = "<html>\n <body>\n#{input_html}\n </body>\n</html>"
|
340
|
+
@html = Nokogiri::HTML.parse(wrapped_html)
|
341
|
+
html_filter_angle_quotes
|
342
|
+
html_filter_definition_lists
|
343
|
+
html_filter_inject_name_section
|
344
|
+
html_filter_heading_anchors
|
345
|
+
html_filter_annotate_bare_links
|
346
|
+
html_filter_manual_reference_links
|
347
|
+
@html
|
348
|
+
end
|
349
|
+
|
350
|
+
##
|
351
|
+
# Filters
|
352
|
+
|
353
|
+
# Appends all index links to the end of the document as Markdown reference
|
354
|
+
# links. This lets us use [foo(3)][] syntax to link to index entries.
|
355
|
+
def markdown_filter_link_index(markdown)
|
356
|
+
return markdown if index.nil? || index.empty?
|
357
|
+
|
358
|
+
markdown << "\n\n"
|
359
|
+
index.each { |ref| markdown << "[#{ref.name}]: #{ref.url}\n" }
|
360
|
+
markdown
|
361
|
+
end
|
362
|
+
|
363
|
+
# Add [id]: #ANCHOR elements to the markdown source text for all sections.
|
364
|
+
# This lets us use the [SECTION-REF][] syntax
|
365
|
+
def markdown_filter_heading_anchors(markdown)
|
366
|
+
first = true
|
367
|
+
markdown.split("\n").grep(/^[#]{2,5} +[\w '-]+[# ]*$/).each do |line|
|
368
|
+
markdown << "\n\n" if first
|
369
|
+
first = false
|
370
|
+
title = line.gsub(/[^\w -]/, '').strip
|
371
|
+
anchor = title.gsub(/\W+/, '-').gsub(/(^-+|-+$)/, '')
|
372
|
+
markdown << "[#{title}]: ##{anchor} \"#{title}\"\n"
|
373
|
+
end
|
374
|
+
markdown
|
375
|
+
end
|
376
|
+
|
377
|
+
# Convert <WORD> to <var>WORD</var> but only if WORD isn't an HTML tag.
|
378
|
+
def markdown_filter_angle_quotes(markdown)
|
379
|
+
markdown.gsub(/(?<!\\)<([^:.\/]+?)>/) do |match|
|
380
|
+
contents = $1
|
381
|
+
tag, attrs = contents.split(' ', 2)
|
382
|
+
if attrs =~ /\/=/ || html_element?(tag.sub(/^\//, '')) ||
|
383
|
+
data.include?("</#{tag}>") || contents =~ /^!/
|
384
|
+
match.to_s
|
385
|
+
else
|
386
|
+
"<var>#{contents}</var>"
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
# Perform angle quote (<THESE>) post filtering.
|
392
|
+
def html_filter_angle_quotes
|
393
|
+
# convert all angle quote vars nested in code blocks
|
394
|
+
# back to the original text
|
395
|
+
code_nodes = @html.search('code')
|
396
|
+
code_nodes.search('.//text() | text()').each do |node|
|
397
|
+
next unless node.to_html.include?('var>')
|
398
|
+
|
399
|
+
new =
|
400
|
+
node.to_html
|
401
|
+
.gsub('<var>', '<')
|
402
|
+
.gsub('</var>', '>')
|
403
|
+
node.swap(new)
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
# Convert special format unordered lists to definition lists.
|
408
|
+
def html_filter_definition_lists
|
409
|
+
# process all unordered lists depth-first
|
410
|
+
@html.search('ul').to_a.reverse_each do |ul|
|
411
|
+
items = ul.search('li')
|
412
|
+
next if items.any? { |item| item.inner_text.strip.split("\n", 2).first !~ /:$/ }
|
413
|
+
|
414
|
+
dl = Nokogiri::XML::Node.new 'dl', html
|
415
|
+
items.each do |item|
|
416
|
+
# This processing is specific to how Markdown generates definition lists
|
417
|
+
term, definition = item.inner_html.strip.split(":\n", 2)
|
418
|
+
term = term.sub(/^<p>/, '')
|
419
|
+
|
420
|
+
dt = Nokogiri::XML::Node.new 'dt', html
|
421
|
+
dt.children = Nokogiri::HTML.fragment(term)
|
422
|
+
dt.attributes['class'] = 'flush' if dt.inner_text.length <= 7
|
423
|
+
|
424
|
+
dd = Nokogiri::XML::Node.new 'dd', html
|
425
|
+
dd_contents = Nokogiri::HTML.fragment(definition)
|
426
|
+
dd.children = dd_contents
|
427
|
+
|
428
|
+
dl.add_child(dt)
|
429
|
+
dl.add_child(dd)
|
430
|
+
end
|
431
|
+
ul.replace(dl)
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
def html_filter_inject_name_section
|
436
|
+
markup =
|
437
|
+
if title?
|
438
|
+
"<h1>#{title}</h1>"
|
439
|
+
elsif name
|
440
|
+
"<h2>NAME</h2>\n" \
|
441
|
+
"<p class='man-name'>\n <code>#{name}</code>" +
|
442
|
+
(tagline ? " - <span class='man-whatis'>#{tagline}</span>\n" : "\n") +
|
443
|
+
"</p>\n"
|
444
|
+
end
|
445
|
+
return unless markup
|
446
|
+
|
447
|
+
if html.at('body').first_element_child
|
448
|
+
html.at('body').first_element_child.before(Nokogiri::HTML.fragment(markup))
|
449
|
+
else
|
450
|
+
html.at('body').add_child(Nokogiri::HTML.fragment(markup))
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
# Add URL anchors to all HTML heading elements.
|
455
|
+
def html_filter_heading_anchors
|
456
|
+
h_nodes = @html.search('//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 and not(@id)]')
|
457
|
+
h_nodes.each do |heading|
|
458
|
+
heading.set_attribute('id', heading.inner_text.gsub(/\W+/, '-'))
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
# Add a 'data-bare-link' attribute to hyperlinks
|
463
|
+
# whose text labels are the same as their href URLs.
|
464
|
+
def html_filter_annotate_bare_links
|
465
|
+
@html.search('a[@href]').each do |node|
|
466
|
+
href = node.attributes['href'].content
|
467
|
+
text = node.inner_text
|
468
|
+
|
469
|
+
next unless href == text || href[0] == '#' ||
|
470
|
+
CGI.unescapeHTML(href) == "mailto:#{CGI.unescapeHTML(text)}"
|
471
|
+
|
472
|
+
node.set_attribute('data-bare-link', 'true')
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
# Convert text of the form "name(section)" or "<code>name</code>(section)
|
477
|
+
# to a hyperlink. The URL is obtained from the index.
|
478
|
+
def html_filter_manual_reference_links
|
479
|
+
return if index.nil?
|
480
|
+
|
481
|
+
name_pattern = '[0-9A-Za-z_:.+=@~-]+'
|
482
|
+
|
483
|
+
# Convert "name(section)" by traversing text nodes searching for
|
484
|
+
# text that fits the pattern. This is the original implementation.
|
485
|
+
@html.search('.//text() | text()').each do |node|
|
486
|
+
next unless node.content.include?(')')
|
487
|
+
next if %w[pre code h1 h2 h3].include?(node.parent.name)
|
488
|
+
next if child_of?(node, 'a')
|
489
|
+
node.swap(node.content.gsub(/(#{name_pattern})(\(\d+\w*\))/) do
|
490
|
+
html_build_manual_reference_link($1, $2)
|
491
|
+
end)
|
492
|
+
end
|
493
|
+
|
494
|
+
# Convert "<code>name</code>(section)" by traversing <code> nodes.
|
495
|
+
# For each one that contains exactly an acceptable manual page name,
|
496
|
+
# the next sibling is checked and must be a text node beginning
|
497
|
+
# with a valid section in parentheses.
|
498
|
+
@html.search('code').each do |node|
|
499
|
+
next if %w[pre code h1 h2 h3].include?(node.parent.name)
|
500
|
+
next if child_of?(node, 'a')
|
501
|
+
next unless node.inner_text =~ /^#{name_pattern}$/
|
502
|
+
sibling = node.next
|
503
|
+
next unless sibling
|
504
|
+
next unless sibling.text?
|
505
|
+
next unless sibling.content =~ /^\((\d+\w*)\)/
|
506
|
+
node.swap(html_build_manual_reference_link(node, "(#{$1})"))
|
507
|
+
sibling.content = sibling.content.gsub(/^\(\d+\w*\)/, '')
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
# HTMLize the manual page reference. The result is an <a> if the
|
512
|
+
# page appears in the index, otherwise it is a <span>. The first
|
513
|
+
# argument may be an HTML element or a string. The second should
|
514
|
+
# be a string of the form "(#{section})".
|
515
|
+
def html_build_manual_reference_link(name_or_node, section)
|
516
|
+
name = if name_or_node.respond_to?(:inner_text)
|
517
|
+
name_or_node.inner_text
|
518
|
+
else
|
519
|
+
name_or_node
|
520
|
+
end
|
521
|
+
ref = index["#{name}#{section}"]
|
522
|
+
if ref
|
523
|
+
"<a class='man-ref' href='#{ref.url}'>#{name_or_node}<span class='s'>#{section}</span></a>"
|
524
|
+
else
|
525
|
+
# warn "warn: manual reference not defined: '#{name}#{section}'"
|
526
|
+
"<span class='man-ref'>#{name_or_node}<span class='s'>#{section}</span></span>"
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
end
|
data/lib/ronn/index.rb
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'ronn'
|
2
|
+
|
3
|
+
module Ronn
|
4
|
+
# Maintains a list of links / references to manuals and other resources.
|
5
|
+
class Index
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
attr_reader :path, :references
|
9
|
+
|
10
|
+
# Retrieve an Index for <path>, where <path> is a directory or normal
|
11
|
+
# file. The index is loaded from the corresponding index.txt file if
|
12
|
+
# one exists.
|
13
|
+
def self.[](path)
|
14
|
+
(@indexes ||= {})[index_path_for_file(path)] ||=
|
15
|
+
Index.new(index_path_for_file(path))
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.index_path_for_file(file)
|
19
|
+
File.expand_path(
|
20
|
+
if File.directory?(file)
|
21
|
+
File.join(file, 'index.txt')
|
22
|
+
else
|
23
|
+
File.join(File.dirname(file), 'index.txt')
|
24
|
+
end
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(path)
|
29
|
+
@path = path
|
30
|
+
@references = []
|
31
|
+
@manuals = {}
|
32
|
+
|
33
|
+
if block_given?
|
34
|
+
read! yield
|
35
|
+
elsif exist?
|
36
|
+
read! File.read(path)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Determine whether the index file exists.
|
41
|
+
def exist?
|
42
|
+
File.exist?(path)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Load index data from a string.
|
46
|
+
def read!(data)
|
47
|
+
data.each_line do |line|
|
48
|
+
line = line.strip.gsub(/\s*#.*$/, '')
|
49
|
+
unless line.empty?
|
50
|
+
name, url = line.split(/\s+/, 2)
|
51
|
+
@references << reference(name, url)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Enumerable and friends
|
58
|
+
|
59
|
+
def each(&block)
|
60
|
+
references.each(&block)
|
61
|
+
end
|
62
|
+
|
63
|
+
def size
|
64
|
+
references.size
|
65
|
+
end
|
66
|
+
|
67
|
+
def first
|
68
|
+
references.first
|
69
|
+
end
|
70
|
+
|
71
|
+
def last
|
72
|
+
references.last
|
73
|
+
end
|
74
|
+
|
75
|
+
def empty?
|
76
|
+
references.empty?
|
77
|
+
end
|
78
|
+
|
79
|
+
def [](name)
|
80
|
+
references.find { |ref| ref.name == name }
|
81
|
+
end
|
82
|
+
|
83
|
+
def reference(name, path)
|
84
|
+
Reference.new(self, name, path)
|
85
|
+
end
|
86
|
+
|
87
|
+
def <<(path)
|
88
|
+
raise ArgumentError, 'local paths only' if path =~ /(https?|mailto):/
|
89
|
+
return self if any? { |ref| ref.path == File.expand_path(path) }
|
90
|
+
relative_path = relative_to_index(path)
|
91
|
+
@references << \
|
92
|
+
if path =~ /\.ronn?$/
|
93
|
+
reference manual(path).reference_name, relative_path
|
94
|
+
else
|
95
|
+
reference File.basename(path), relative_path
|
96
|
+
end
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
100
|
+
def add_manual(manual)
|
101
|
+
@manuals[File.expand_path(manual.path)] = manual
|
102
|
+
self << manual.path
|
103
|
+
end
|
104
|
+
|
105
|
+
def manual(path)
|
106
|
+
@manuals[File.expand_path(path)] ||= Document.new(path)
|
107
|
+
end
|
108
|
+
|
109
|
+
def manuals
|
110
|
+
select { |ref| ref.relative? && ref.ronn? }
|
111
|
+
.map { |ref| manual(ref.path) }
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Converting
|
116
|
+
|
117
|
+
def to_text
|
118
|
+
map { |ref| [ref.name, ref.location].join(' ') }.join("\n")
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_a
|
122
|
+
references
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_h
|
126
|
+
to_a.map(&:to_hash)
|
127
|
+
end
|
128
|
+
|
129
|
+
def relative_to_index(path)
|
130
|
+
path = File.expand_path(path)
|
131
|
+
index_dir = File.dirname(File.expand_path(self.path))
|
132
|
+
path.sub(/^#{index_dir}\//, '')
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# An individual index reference. A reference can point to one of a few types
|
137
|
+
# of locations:
|
138
|
+
#
|
139
|
+
# - URLs: "http://man.cx/crontab(5)"
|
140
|
+
# - Relative paths to ronn manuals: "crontab.5.ronn"
|
141
|
+
#
|
142
|
+
# The #url method should be used to obtain the href value for HTML.
|
143
|
+
class Reference
|
144
|
+
attr_reader :name, :location
|
145
|
+
|
146
|
+
def initialize(index, name, location)
|
147
|
+
@index = index
|
148
|
+
@name = name
|
149
|
+
@location = location
|
150
|
+
end
|
151
|
+
|
152
|
+
def manual?
|
153
|
+
name =~ /\([0-9]\w*\)$/
|
154
|
+
end
|
155
|
+
|
156
|
+
def ronn?
|
157
|
+
location =~ /\.ronn?$/
|
158
|
+
end
|
159
|
+
|
160
|
+
def remote?
|
161
|
+
location =~ /^(?:https?|mailto):/
|
162
|
+
end
|
163
|
+
|
164
|
+
def relative?
|
165
|
+
!remote?
|
166
|
+
end
|
167
|
+
|
168
|
+
def url
|
169
|
+
if remote?
|
170
|
+
location
|
171
|
+
else
|
172
|
+
location.chomp('.ronn') + '.html'
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def path
|
177
|
+
File.expand_path(location, File.dirname(@index.path)) if relative?
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|