You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
633 lines
13 KiB
Ruby
633 lines
13 KiB
Ruby
#!/usr/bin/ruby
|
|
|
|
require 'rubygems'
|
|
require 'nokogiri'
|
|
|
|
require 'bluecloth'
|
|
require 'redcloth'
|
|
|
|
HTML_TEMPLATE='''
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
|
<title>Title</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="stylesheet" href="bootstrap.min.css">
|
|
<link rel="stylesheet" href="bootstrap-theme.min.css">
|
|
<link rel="stylesheet" href="style.css">
|
|
<script src="jquery-1.10.1.min.js"></script>
|
|
<script src="bootstrap.min.js"></script>
|
|
</head>
|
|
<body data-spy="scroll" data-target=".bs-sidebar" data-offset="30"><div class="container"><div class="row">
|
|
|
|
<!-- TOC -->
|
|
<div class="col-md-3" role="complementary"><div class="bs-sidebar hidden-print toc" id="sidebar" data-spy="affix" data-offset-top="0" data-offset-bottom="30"></div></div>
|
|
|
|
<!-- MAIN -->
|
|
<div class="col-md-9" role="main" id="main"></div></div>
|
|
|
|
</div></div></body>
|
|
</html>
|
|
'''
|
|
|
|
class Documentation
|
|
XPATH_NAMESPACES = { 'd' => 'urn:lighttpd.net:lighttpd2/doc1' }
|
|
TAB_WIDTH = 4
|
|
|
|
def initialize(basename)
|
|
@html_doc = Nokogiri::HTML::Document.parse(HTML_TEMPLATE)
|
|
@html_main = @html_doc.xpath('//div[@role="main"]')[0]
|
|
@html_toc = @html_doc.css('#sidebar')[0]
|
|
@title = nil
|
|
|
|
@depth = 0
|
|
@uniqueid = 0
|
|
@current_toc = @toc = []
|
|
|
|
@basename = basename
|
|
|
|
@actions = []
|
|
@setups = []
|
|
@options = []
|
|
end
|
|
|
|
def render_main
|
|
Nokogiri::XML::Builder.with(@html_main) do |html|
|
|
@html = html
|
|
yield
|
|
@html = nil
|
|
end
|
|
end
|
|
|
|
def title
|
|
@title
|
|
end
|
|
|
|
def title=(value)
|
|
@title = value
|
|
@html_doc.xpath('/html/head/title')[0].inner_html = value
|
|
end
|
|
|
|
def to_html_fragment
|
|
@html_main.inner_html
|
|
end
|
|
|
|
def to_html
|
|
@html_doc.to_html
|
|
end
|
|
|
|
def toc
|
|
@toc
|
|
end
|
|
|
|
def actions
|
|
@actions
|
|
end
|
|
|
|
def setups
|
|
@setups
|
|
end
|
|
|
|
def options
|
|
@options
|
|
end
|
|
|
|
def _store_toc(html, toc, rootToc = false)
|
|
return unless toc.length > 0
|
|
html.ul(:class => rootToc ? "nav bs-sidenav" : "nav" ) {
|
|
toc.each do |anchor, title, subtoc, cls|
|
|
html.li(:class => cls || '') {
|
|
html.a({:href => '#' + anchor}, title)
|
|
_store_toc(html, subtoc)
|
|
}
|
|
end
|
|
}
|
|
end
|
|
|
|
def store_toc
|
|
Nokogiri::HTML::Builder.with(@html_toc) do |html|
|
|
_store_toc(html, @toc, true)
|
|
end
|
|
end
|
|
|
|
def _unique_anchor(title)
|
|
id = @uniqueid
|
|
@uniqueid += 1
|
|
("%02x-" % id) + title.downcase.gsub(/[^a-z0-9]+/, '_')
|
|
end
|
|
|
|
# although '.' and ':' are allowed in anchors (IDs), they
|
|
# don't work in CSS selectors like: #anchor:foo and #anchor.bar
|
|
def escape_anchor(anchor)
|
|
anchor.gsub(/[.:]/, '-')
|
|
end
|
|
|
|
def nest(title, anchor = nil, cls = nil)
|
|
attribs = {}
|
|
have_toc = nil != @current_toc
|
|
anchor = (@depth < 3 and have_toc) ? _unique_anchor(title) : nil if anchor == '#'
|
|
if anchor
|
|
anchor = (anchor == '') ? @basename : @basename + '__' + anchor
|
|
anchor = escape_anchor(anchor)
|
|
attribs[:id] = anchor
|
|
end
|
|
|
|
use_toc = have_toc && anchor
|
|
old_toc = @current_toc
|
|
@current_toc = use_toc ? [] : nil
|
|
|
|
@depth += 1
|
|
if cls
|
|
@html.div(:class => cls) {
|
|
@html.send("h#{@depth}", attribs, title)
|
|
yield
|
|
}
|
|
else
|
|
@html.send("h#{@depth}", attribs, title)
|
|
yield
|
|
end
|
|
@depth -= 1
|
|
|
|
old_toc << [ anchor, title, @current_toc, cls ] if use_toc
|
|
@current_toc = old_toc
|
|
return anchor
|
|
end
|
|
|
|
## Code formatting
|
|
def _count_indent(line)
|
|
/^[ \t]*/.match(line)[0].each_char.reduce(0) do |i, c|
|
|
c == ' ' ? i + 1 : (i + TAB_WIDTH) - (i % TAB_WIDTH)
|
|
end
|
|
end
|
|
def _remove_indent(indent, line)
|
|
p = 0
|
|
l = line.length
|
|
i = 0
|
|
while i < indent && p < l
|
|
case line[p]
|
|
when ' '
|
|
i += 1
|
|
when "\t"
|
|
i = (i + TAB_WIDTH) - (i % TAB_WIDTH)
|
|
else
|
|
return line[p..-1]
|
|
end
|
|
p += 1
|
|
end
|
|
return line[p..-1]
|
|
end
|
|
def _format_code(code)
|
|
lines = code.rstrip.lines
|
|
real_lines = lines.grep(/\S/)
|
|
return '' if real_lines.length == 0
|
|
|
|
indent = real_lines.map { |l| _count_indent l }.min
|
|
|
|
code = lines.map { |line| _remove_indent(indent, line).rstrip }.join("\n") + "\n"
|
|
code.gsub(/\A\n+/, "").gsub(/\n\n+/, "\n\n").gsub(/\n+\Z/, "\n")
|
|
end
|
|
|
|
|
|
def _parse_code(xml)
|
|
@html.pre { @html.code { @html << _format_code(xml.inner_html) } }
|
|
end
|
|
|
|
def _parse_markdown(xml)
|
|
md = _format_code(xml.inner_html)
|
|
@html << BlueCloth.new(md).to_html
|
|
end
|
|
|
|
def _parse_textile(xml)
|
|
tx = _format_code(xml.inner_html)
|
|
@html << RedCloth.new(tx).to_html
|
|
end
|
|
|
|
def _parse_html(xml)
|
|
@html << xml.inner_html
|
|
end
|
|
|
|
def _parse_description(xmlParent)
|
|
return unless xmlParent
|
|
xml = xmlParent.xpath('d:description[1]', XPATH_NAMESPACES)[0]
|
|
return unless xml
|
|
|
|
xml.children.each do |child|
|
|
if child.text?
|
|
@html.p child.content.strip
|
|
elsif ['html','textile','markdown'].include? child.name
|
|
self.send('_parse_' + child.name, child)
|
|
else
|
|
raise 'invalid description element ' + child.name
|
|
end
|
|
end
|
|
end
|
|
|
|
def <=>(other)
|
|
ordername <=> other.ordername
|
|
end
|
|
|
|
def ordername=(value)
|
|
@ordername = value
|
|
end
|
|
|
|
def ordername
|
|
@ordername || @basename
|
|
end
|
|
|
|
def basename
|
|
@basename
|
|
end
|
|
|
|
def filename
|
|
basename + '.html'
|
|
end
|
|
|
|
def write_disk(output_directory)
|
|
puts "Writing #{output_directory}: #{filename}"
|
|
File.open(File.join(output_directory, self.filename), "w") { |f| f.write self.to_html }
|
|
end
|
|
end
|
|
|
|
|
|
class ModuleDocumentation < Documentation
|
|
def initialize(filename, xml)
|
|
super(File.basename(filename, '.xml'))
|
|
|
|
self.title = basename
|
|
render_main { _parse_module(xml.root) }
|
|
|
|
store_toc
|
|
end
|
|
|
|
def _parse_short(xmlParent, makeDiv = false)
|
|
return unless xmlParent
|
|
xml = xmlParent.xpath('d:short[1]', XPATH_NAMESPACES)[0]
|
|
return unless xml
|
|
text = xml.content.strip
|
|
return unless text
|
|
|
|
if makeDiv
|
|
@html.p.short text
|
|
else
|
|
@html.text text
|
|
end
|
|
text
|
|
end
|
|
|
|
def _parse_parameters(xml)
|
|
@html.dl {
|
|
xml.xpath('d:parameter', XPATH_NAMESPACES).each do |param|
|
|
@html.dt param['name']
|
|
child = param.element_children[0]
|
|
if child.name == 'short'
|
|
@html.dd { _parse_short param }
|
|
elsif child.name == 'table'
|
|
@html.dd {
|
|
@html.text "A key-value table with the following entries:"
|
|
@html.dl {
|
|
child.element_children.each do |entry|
|
|
@html.dt entry['name']
|
|
@html.dd { _parse_short entry }
|
|
end
|
|
}
|
|
}
|
|
end
|
|
end
|
|
}
|
|
end
|
|
|
|
def _parse_default(xml)
|
|
@html.div(:class => 'default') {
|
|
@html.text "Default value: "
|
|
|
|
child = xml.element_children[0]
|
|
@html.span({:class => child.name}, child.content.strip)
|
|
}
|
|
end
|
|
|
|
def _parse_aso(xml, type)
|
|
name = xml['name']
|
|
raise "#{type} requires a name" unless name
|
|
parameter_names = xml.xpath('d:parameter', XPATH_NAMESPACES).map { |p| p['name'] }
|
|
parameter_names = ['value'] if parameter_names.length == 0 and type == 'option'
|
|
|
|
title = "#{name} (#{type})"
|
|
anchor = "#{type}_#{name}"
|
|
cls = "aso #{type}"
|
|
short = nil
|
|
|
|
anchor = nest(title, anchor, cls) {
|
|
short = _parse_short(xml, true)
|
|
|
|
@html.pre(:class => "template") {
|
|
@html.span.key name
|
|
if parameter_names.length == 1
|
|
@html.text ' '
|
|
@html.span.param parameter_names[0]
|
|
@html.text ';'
|
|
elsif parameter_names.length > 0
|
|
@html.text ' ('
|
|
first = true
|
|
parameter_names.each do |pname|
|
|
@html.text ', ' unless first
|
|
first = false
|
|
@html.span.param pname
|
|
end
|
|
@html.text ');'
|
|
else
|
|
@html.text ';'
|
|
end
|
|
}
|
|
|
|
if type == 'option'
|
|
_parse_default(xml.xpath('d:default', XPATH_NAMESPACES)[0])
|
|
else
|
|
_parse_parameters(xml) if parameter_names.length > 0
|
|
end
|
|
|
|
_parse_description(xml)
|
|
xml.xpath('d:example', XPATH_NAMESPACES).each do |child|
|
|
_parse_example(child)
|
|
end
|
|
}
|
|
|
|
[name, filename + '#' + anchor, short, self]
|
|
end
|
|
|
|
def _parse_action(xml)
|
|
@actions << _parse_aso(xml, 'action')
|
|
end
|
|
|
|
def _parse_setup(xml)
|
|
@setups << _parse_aso(xml, 'setup')
|
|
end
|
|
|
|
def _parse_option(xml)
|
|
@options << _parse_aso(xml, 'option')
|
|
end
|
|
|
|
def _parse_example(xml)
|
|
nest(xml['title'] || 'Example', xml['anchor'], 'example') {
|
|
_parse_description(xml)
|
|
|
|
config = xml.xpath('d:config[1]', XPATH_NAMESPACES)
|
|
_parse_code(config[0])
|
|
}
|
|
end
|
|
|
|
def _parse_section(xml)
|
|
title = xml['title']
|
|
raise 'section requires a title' unless title
|
|
|
|
nest(title, xml['anchor'] || '#', 'section') {
|
|
xml.children.each do |child|
|
|
if child.text?
|
|
text = child.content.strip
|
|
@html.p text if text.length > 0
|
|
elsif ['action','setup','option','html','textile','markdown','example','section'].include? child.name
|
|
self.send('_parse_' + child.name, child)
|
|
else
|
|
raise 'invalid section element ' + child.name
|
|
end
|
|
end
|
|
}
|
|
end
|
|
|
|
def _parse_module(xml)
|
|
raise 'unexpected root node' if xml.name != 'module'
|
|
|
|
self.title = xml['title'] || self.title
|
|
self.ordername = xml['order']
|
|
|
|
nest(title, '', 'module') {
|
|
@html.p {
|
|
@html.text (basename + ' ')
|
|
@short = _parse_short(xml, false)
|
|
}
|
|
_parse_description(xml)
|
|
|
|
xml.element_children.each do |child|
|
|
if ['action','setup','option','example','section'].include? child.name
|
|
self.send('_parse_' + child.name, child)
|
|
elsif ['short', 'description'].include? child.name
|
|
nil # skip
|
|
else
|
|
raise 'invalid module element ' + child.name
|
|
end
|
|
end
|
|
}
|
|
end
|
|
|
|
def short
|
|
@short
|
|
end
|
|
|
|
def link(html_builder)
|
|
html_builder.a({:href => self.filename + '#' + escape_anchor(self.basename)}, self.basename)
|
|
end
|
|
end
|
|
|
|
class ChapterDocumentation < Documentation
|
|
def initialize(filename, xml)
|
|
super(File.basename(filename, '.xml'))
|
|
|
|
render_main { _parse_chapter(xml.root) }
|
|
|
|
store_toc
|
|
end
|
|
|
|
def _parse_example(xml)
|
|
nest(xml['title'] || 'Example', xml['anchor'], 'example') {
|
|
_parse_description(xml)
|
|
|
|
config = xml.xpath('d:config[1]', XPATH_NAMESPACES)
|
|
_parse_code(config[0])
|
|
}
|
|
end
|
|
|
|
def _parse_section(xml)
|
|
title = xml['title']
|
|
raise 'section requires a title' unless title
|
|
|
|
nest(title, xml['anchor'] || '#', 'section') {
|
|
xml.children.each do |child|
|
|
if child.text?
|
|
text = child.content.strip
|
|
@html.p text if text.length > 0
|
|
elsif ['html','textile','markdown','example','section'].include? child.name
|
|
self.send('_parse_' + child.name, child)
|
|
else
|
|
raise 'invalid section element ' + child.name
|
|
end
|
|
end
|
|
}
|
|
end
|
|
|
|
def _parse_chapter(xml)
|
|
raise 'unexpected root node' if xml.name != 'chapter'
|
|
|
|
self.title = xml['title']
|
|
raise 'chapter requires a title' unless self.title
|
|
self.ordername = xml['order']
|
|
|
|
nest(self.title, '', 'chapter') {
|
|
_parse_description(xml)
|
|
|
|
xml.element_children.each do |child|
|
|
if ['example','section'].include? child.name
|
|
self.send('_parse_' + child.name, child)
|
|
elsif ['description'].include? child.name
|
|
nil # skip
|
|
else
|
|
raise 'invalid chapter element ' + child.name
|
|
end
|
|
end
|
|
}
|
|
end
|
|
end
|
|
|
|
class ModuleIndex < Documentation
|
|
def modules_table(modules)
|
|
nest('Modules', 'modules') {
|
|
@html.table(:class => 'table table-striped') {
|
|
@html.tr {
|
|
@html.th "name"
|
|
@html.th "description"
|
|
}
|
|
modules.each do |mod|
|
|
next unless mod.is_a? ModuleDocumentation
|
|
@html.tr {
|
|
@html.td {
|
|
mod.link(@html)
|
|
}
|
|
@html.td mod.short
|
|
}
|
|
end
|
|
}
|
|
|
|
}
|
|
end
|
|
|
|
def aso_html_table(name, list)
|
|
nest(name.capitalize + 's', name + 's') {
|
|
@html.table(:class => 'table table-striped aso') {
|
|
@html.tr {
|
|
@html.th "name"
|
|
@html.th "module"
|
|
@html.th "description"
|
|
}
|
|
list.each do |id, href, short, mod|
|
|
@html.tr {
|
|
@html.td {
|
|
@html.a({:href => href}, id)
|
|
}
|
|
@html.td {
|
|
mod.link(@html)
|
|
}
|
|
@html.td short
|
|
}
|
|
end
|
|
}
|
|
@html.text "none" unless list.length > 0
|
|
}
|
|
end
|
|
|
|
def initialize(modules)
|
|
super('index_modules')
|
|
|
|
actions = []
|
|
setups = []
|
|
options = []
|
|
modules.each do |mod|
|
|
actions += mod.actions
|
|
setups += mod.setups
|
|
options += mod.options
|
|
end
|
|
|
|
render_main do
|
|
nest('Modules overview', '', 'index_modules') {
|
|
modules_table(modules)
|
|
aso_html_table('action', actions.sort)
|
|
aso_html_table('setup', setups.sort)
|
|
aso_html_table('option', options.sort)
|
|
}
|
|
end
|
|
|
|
self.title = "lighttpd2 - all in one"
|
|
store_toc
|
|
end
|
|
end
|
|
|
|
class AllPage < Documentation
|
|
def fix_link(a)
|
|
href = a['href']
|
|
return unless href
|
|
m = /^([^#]*)(#.*)$/.match(href)
|
|
a['href'] = m[2] if m && @href_map[m[1]]
|
|
end
|
|
|
|
def initialize(pages)
|
|
super('all')
|
|
|
|
@href_map = {}
|
|
pages.each do |page|
|
|
@href_map[page.filename] = true
|
|
end
|
|
|
|
render_main do
|
|
pages.each do |page|
|
|
@html << page.to_html_fragment
|
|
@toc += page.toc
|
|
end
|
|
end
|
|
|
|
@html_doc.xpath('//a').each do |a|
|
|
fix_link(a)
|
|
end
|
|
|
|
self.title = "lighttpd2 - all in one"
|
|
store_toc
|
|
end
|
|
end
|
|
|
|
|
|
def loadXML(filename)
|
|
xml = Nokogiri::XML(File.read filename) do |config|
|
|
config.strict.nonet
|
|
end
|
|
|
|
if xml.root.name == 'module'
|
|
ModuleDocumentation.new(filename, xml)
|
|
elsif xml.root.name == 'chapter'
|
|
ChapterDocumentation.new(filename, xml)
|
|
end
|
|
end
|
|
|
|
|
|
if __FILE__ == $0
|
|
output_directory = ARGV[0] || '.'
|
|
|
|
if not system("xmllint --noout --schema doc_schema.xsd *.xml 2>&1")
|
|
STDERR.puts "Couldn't validate XML files"
|
|
exit 1
|
|
end
|
|
|
|
pages = []
|
|
|
|
Dir["*.xml"].each do |file|
|
|
puts "Compiling #{file}"
|
|
pages << loadXML(file)
|
|
end
|
|
|
|
pages.sort!
|
|
pages << ModuleIndex.new(pages)
|
|
|
|
|
|
pages.sort!
|
|
pages << AllPage.new(pages)
|
|
|
|
pages.sort!
|
|
pages.each { |page| page.write_disk(output_directory) }
|
|
end
|