mirror of
https://github.com/johnkerl/miller.git
synced 2026-01-23 10:15:36 +00:00
389 lines
14 KiB
Ruby
Executable file
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(" ");
|
|
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 " "}
|
|
output_handle.write "• "
|
|
# 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("&", "&").gsub("<", "<").gsub(">", ">").rstrip
|
|
end
|
|
|
|
# ================================================================
|
|
# Top-down programming style, please.
|
|
main()
|