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.
lighttpd2/doc/compile.rb

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