miller/doc/poki
2020-01-15 22:05:52 -05:00

389 lines
14 KiB
Ruby
Executable file

#!/usr/bin/env ruby
# ================================================================
# Poki: A poor man's wiki generator
# Code: https://github.com/johnkerl/poki
# Docs: http://johnkerl.org/poki/doc
# Customized for Miller 2019-09-15
# John Kerl
# kerl.john.r@gmail.com
# 2015-04-26
# ================================================================
$us = File.basename $0
$default_config_file_name = "poki-cfg.json"
$output_file_mode_during_edit = 0644;
$output_file_mode_after_edit = 0444;
require 'getoptlong'
require 'fileutils'
require 'json'
# ================================================================
def usage()
$stderr.puts <<EOF
Usage: #{$us} [options]
Options:
-c {config file} Default #{$default_config_file_name}
-h|--help Print this message
Please see ./doc in poki's source directory
https://github.com/johnkerl/poki
https://johnkerl.org/poki/doc
EOF
end
# ================================================================
def main()
config_file_name = $default_config_file_name
opts = GetoptLong.new(
[ '-c', GetoptLong::REQUIRED_ARGUMENT ],
[ '-t', GetoptLong::REQUIRED_ARGUMENT ],
[ '-p', GetoptLong::REQUIRED_ARGUMENT ],
[ '-h','--help', GetoptLong::NO_ARGUMENT ]
)
begin
opts.each do |opt, arg|
case opt
when '-c'; config_file_name = arg
when '-h'; usage
when '--help'; usage
end
end
rescue GetoptLong::Error
usage
end
usage if ARGV.length != 0
overall_config = load_config_file(config_file_name)
generate_pages(overall_config)
end
# ================================================================
# Sample config file:
# {
# "main_title": "Main Title",
# "sections": [
# {
# "external_name": "Overview",
# "internal_name": "overview",
# "pages": [
# {
# "external_name": "About",
# "file_name": "index.html"
# },
# {
# "external_name": "Repo",
# "link": "http://github.com/foo/bar"
# }
# ]
# }
# ]
# }
def load_config_file(config_file_name)
JSON.load File.read config_file_name
end
# ================================================================
# Note: we use map.fetch('key') throughout so it will raise KeyError on missing
# values, rathre than map['key'] which will return nil.
def generate_pages(overall_config)
for section_config in overall_config.fetch('sections')
for page_config in section_config.fetch('pages')
# External links aren't associated with a local input HTML file.
next unless page_config['file_name'] # nullable
input_html_path = "#{overall_config.fetch('content_file_prefix')}#{page_config.fetch('file_name')}"
output_html_name = page_config.fetch('file_name')
generate_page(overall_config, section_config, page_config, input_html_path, output_html_name)
end
end
end
# ----------------------------------------------------------------
def generate_page(overall_config, section_config, page_config, input_html_path, output_html_name)
if test(?f, output_html_name)
FileUtils.chmod($output_file_mode_during_edit, output_html_name)
end
template_file_name = overall_config.fetch('template_file_name')
page_title = page_config.fetch('external_name')
File.open(output_html_name, "w", $output_file_mode_during_edit) do |output_handle|
File.readlines(template_file_name).each do |template_line|
# Template directive: Mark autogenerated pages as autogenerated.
if template_line =~ /POKI_PUT_AUTOGEN_DISCLAIMER_HERE/
output_handle.puts(template_line.sub('POKI_PUT_AUTOGEN_DISCLAIMER_HERE', ''))
output_handle.puts "<!-- PAGE GENERATED FROM #{template_file_name} and #{input_html_path} BY #{$us}. -->"
output_handle.puts "<!-- PLEASE MAKE CHANGES THERE AND THEN RE-RUN #{$us}. -->"
# Template directive: For navbar.
elsif template_line =~ /POKI_PUT_NAVBAR_HERE/
output_handle.puts(template_line.sub('POKI_PUT_NAVBAR_HERE', ''))
generate_navbar(overall_config, section_config, page_config, output_html_name, output_handle)
# Template directive: Page titles in browser window title, as well as page-content title.
elsif template_line =~ /POKI_PUT_TITLE_HERE/
output_handle.puts(template_line.sub('POKI_PUT_TITLE_HERE', page_title))
# Template directive: Page body.
elsif template_line =~ /POKI_PUT_BODY_HERE/
output_handle.puts(template_line.sub('POKI_PUT_BODY_HERE', ''))
generate_page_body(overall_config, section_config, page_config, input_html_path, output_handle)
# Absent any other directives, copy the template file to the output file.
else
output_handle.write(template_line)
end
end
FileUtils.chmod($output_file_mode_after_edit, output_html_name)
puts "Generated #{output_html_name}"
end
end
# ----------------------------------------------------------------
def generate_navbar(overall_config, section_config, page_config, output_html_name, output_handle)
template_file_name = overall_config.fetch('template_file_name')
output_handle.puts "<!-- NAVBAR GENERATED FROM #{template_file_name} BY #{$us} -->"
output_handle.puts("<br/>");
for other_section_config in overall_config.fetch('sections')
other_section_internal_name = other_section_config.fetch('internal_name')
other_section_external_name = other_section_config.fetch('external_name')
bs = ''
be = ''
if (other_section_internal_name == section_config.fetch('internal_name'))
bs = '<b>'
be = '</b>'
end
other_section_pages_config = other_section_config.fetch('pages')
top_page_for_section = other_section_pages_config[0]
top_page_for_section_file_name = top_page_for_section.fetch('file_name')
# xxx needs css for no underlining ...
output_handle.puts("<a class=\"poki-navbar-element\" href=\"#{top_page_for_section_file_name}\">#{bs}#{other_section_external_name}#{be}</a>");
output_handle.puts("&nbsp;");
end
output_handle.puts("<br/>");
for other_page_config in section_config.fetch('pages')
other_page_file_name = other_page_config['file_name'] # nullable
other_page_link_name = other_page_config['link'] # nullable
other_page_external_name = other_page_config.fetch('external_name')
if other_page_link_name != nil
url = other_page_link_name.sub(/^ext:/, '')
output_handle.puts "<br/><a href=\"#{url}\">#{other_page_link_name}</a>"
else
bs = ""
be = ""
if File.basename(output_html_name) == other_page_file_name
# Bold the current file in the navbar.
bs = "<b>"
be = "</b>"
end
output_handle.puts "<br/><a href=\"#{other_page_file_name}\">#{bs}#{other_page_external_name}#{be}</a>"
end
end
end
# ----------------------------------------------------------------
def generate_page_body(overall_config, section_config, page_config, input_html_path, output_handle)
output_handle.puts "<!-- BODY COPIED FROM #{input_html_path} BY #{$us} -->"
File.readlines(input_html_path).each do |content_line|
# Page-content directive: table of contents.
if content_line =~ /POKI_PUT_TOC_HERE/
generate_toc(overall_config, section_config, page_config, input_html_path, output_handle)
# Page-content directive: include other file (do HTML escapes)
elsif content_line =~ /POKI_INCLUDE_ESCAPED\(([^)]+)\)HERE/
included_file_name = $1
include_escaped(included_file_name, output_handle)
# Page-content directive: include other file (do HTML escapes) and print its output
elsif content_line =~ /POKI_INCLUDE_AND_RUN_ESCAPED\(([^)]+)\)HERE/
included_file_name = $1
cmd = File.readlines(included_file_name).join('')
run_command(cmd, output_handle, true)
# Page-content directive: run a script which generates HTML and print its output
elsif content_line =~ /POKI_RUN_UNESCAPED\(([^)]+)\)HERE/
included_file_name = $1
cmd = File.readlines(included_file_name).join('')
run_command(cmd, output_handle, false)
# Page-content directive: include other file (do HTML escapes) and print its output
elsif content_line =~ /POKI_RUN_CONTENT_GENERATOR\(([^)]+)\)HERE/
cmd = $1
run_content_generator(cmd, output_handle)
# Page-content directive: format as if included from a file.
elsif content_line =~ /POKI_CARDIFY\(([^)]+)\)HERE/
content_line = $1
cardify(content_line, output_handle, true)
elsif content_line =~ /POKI_CARDIFY{{([^)]+)}}HERE/
content_line = $1
cardify(content_line, output_handle, true)
elsif content_line =~ /POKI_CARDIFY{{(.+)}}HERE/
content_line = $1
cardify(content_line, output_handle, true)
# Page-content directive: include other file (do HTML escapes)
elsif content_line =~ /POKI_RUN_COMMAND{{(.+)}}HERE/
cmd = $1
run_command(cmd, output_handle, true)
# Page-content directive: include other file (do HTML escapes)
elsif content_line =~ /POKI_RUN_COMMAND_TOLERATING_ERROR{{(.+)}}HERE/
cmd = $1
run_command_tolerating_error(cmd, output_handle)
# Page-content directive: link to sibling in pageset.
elsif content_line =~ /POKI_PUT_LINK_FOR_PAGE\(([^)]+)\)HERE/
other_page_link = $1
other_page_name = other_page_link.sub(/#.*/, '')
other_page_title = nil
for loop_section_config in overall_config.fetch('sections')
for loop_page_config in loop_section_config.fetch('pages')
loop_page_file_name = loop_page_config['file_name'] # nullable
loop_page_external_name = loop_page_config.fetch('external_name')
if other_page_name == loop_page_file_name
other_page_title = loop_page_external_name
end
end
end
if other_page_title.nil?
raise "Couldn't find page title for \"#{other_page_name}\"."
end
href = "<a href=\"#{other_page_link}\">#{other_page_title}</a>"
content_line.sub!(/POKI_PUT_LINK_FOR_PAGE\([^)]+\)HERE/, href)
output_handle.puts(content_line)
# Page-content directive: automark <h1>, <h2>, etc. with jump-to tags so they can be found by the
# table of contents. Example: <h1>Title</h1> becomes <h1>Title</h1> <a id="#Title">
elsif content_line =~ /<h([1-9])>(.*)<\/h[1-9]>/
tag = $2.strip
# If there's a section with a space in it (e.g. "Naming conventions") then
# make the href text the same but underscorify the link itself (e.g.
# "Naming_conventions").
output_handle.print("<a id=\"#{tag.gsub(' ', '_')}\"/>")
output_handle.write(content_line)
else
output_handle.write(content_line)
end
end
end
# ----------------------------------------------------------------
def generate_toc(overall_config, section_config, page_config, input_html_path, output_handle)
# Read all the <h1>, <h2>, <h3>, etc. from the current file and create internal links
tags = []
depths = {}
File.readlines(input_html_path).each do |scan_line|
if scan_line =~ /^<h([1-9])>(.*)<\/h[1-9]>/
depth = $1
tag = $2.strip
tags << tag
depths[tag] = depth.to_i - 1
end
end
page_title = page_config.fetch('external_name')
page_subtitle = page_config['external_subname'] # nullable
output_handle.puts('<div class="pokitoc">')
if page_subtitle.nil?
output_handle.puts("<center><titleinbody>#{page_title}</titleinbody></center>")
else
output_handle.puts("<center><titleinbody>#{page_title}: #{page_subtitle}</titleinbody></center>")
end
tags.each do |tag|
# Indent h2 more than h1, h3 more than h2, etc.
depths[tag].times{output_handle.write "&nbsp;&nbsp;&nbsp;&nbsp;"}
output_handle.write "&bull;&nbsp;"
# If there's a section with a space in it (e.g. "Naming conventions") then
# make the href text the same but underscorify the link itself (e.g.
# "Naming_conventions").
output_handle.write "<a href=\"##{tag.gsub(' ', '_')}\">#{tag}</a>"
output_handle.puts "<br/>"
end
output_handle.puts("</div>")
output_handle.puts("<p/>")
end
# ----------------------------------------------------------------
def include_escaped(included_file_name, output_handle)
write_card(File.readlines(included_file_name), output_handle, true)
end
# ----------------------------------------------------------------
def run_command(cmd, output_handle, do_escape)
cmd_output = `#{cmd} 2>&1`
status = $?.to_i
if status != 0
raise "\"#{cmd}\" exited with non-zero code #{status}."
end
write_card(['$ '+cmd] + cmd_output.split(/\n/), output_handle, do_escape)
end
# ----------------------------------------------------------------
def run_command_tolerating_error(cmd, output_handle)
cmd_output = `#{cmd} 2>&1`
write_card(['$ '+cmd] + cmd_output.split(/\n/), output_handle, true)
end
# ----------------------------------------------------------------
def run_content_generator(cmd, output_handle)
cmd_output = `#{cmd} 2>&1`
status = $?.to_i
if status != 0
raise "\"#{cmd}\" exited with non-zero code #{status}."
end
output_handle.puts(cmd_output)
end
# ----------------------------------------------------------------
def cardify(content_line, output_handle, do_escape)
write_card([content_line], output_handle, do_escape)
end
# ----------------------------------------------------------------
def write_card(content_lines, output_handle, do_escape)
output_handle.puts('<p/>')
output_handle.puts('<div class="pokipanel">')
output_handle.puts('<pre>')
content_lines.each do |content_line|
if do_escape
output_handle.puts(html_escape_line(content_line))
else
output_handle.puts(content_line)
end
end
output_handle.puts('</pre>')
output_handle.puts('</div>')
output_handle.puts('<p/>')
end
# ----------------------------------------------------------------
def html_escape_line(line)
line.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").rstrip
end
# ================================================================
# Top-down programming style, please.
main()