summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/category.rb20
-rw-r--r--lib/config.rb51
-rw-r--r--lib/license.rb17
-rw-r--r--lib/page.rb103
-rw-r--r--lib/page_index.rb121
-rw-r--r--lib/page_local.rb122
-rw-r--r--lib/page_remote.rb59
-rw-r--r--lib/pandoc.rb96
-rw-r--r--lib/person.rb32
-rw-r--r--lib/sitegen.rb98
-rw-r--r--lib/siteutil.rb27
11 files changed, 746 insertions, 0 deletions
diff --git a/lib/category.rb b/lib/category.rb
new file mode 100644
index 0000000..b60c82e
--- /dev/null
+++ b/lib/category.rb
@@ -0,0 +1,20 @@
+# coding: utf-8
+require 'config'
+
+class Category
+ def initialize(abbr)
+ @abbr = abbr
+ end
+ def abbr
+ @abbr
+ end
+ def name
+ Config::get.category_name(@abbr)
+ end
+ def html
+ return "<a class=\"tag #{abbr}\" href=\"/tags/#{abbr}.html\">#{name}</a>"
+ end
+ def atom
+ return "<category term=\"#{abbr}\" label=\"#{name}\" />"
+ end
+end
diff --git a/lib/config.rb b/lib/config.rb
new file mode 100644
index 0000000..4690559
--- /dev/null
+++ b/lib/config.rb
@@ -0,0 +1,51 @@
+# coding: utf-8
+require 'yaml'
+
+require 'uri'
+
+class Config
+ def self.get
+ return @config ||= Config::new('config.yaml')
+ end
+ def initialize(filename)
+ @data = YAML::load(File::read(filename))
+ end
+ def url
+ return @url ||= URI::parse(@data['url'])
+ end
+ def html_suffixes
+ return @data['html_suffixes']
+ end
+ # Licenses
+ def default_license
+ return @default_license ||= @data['default_license']
+ end
+ def license_uri(name)
+ str = @data['license_uris'][name]
+ if str.nil?
+ return nil
+ end
+ return URI::parse(str)
+ end
+ # People
+ def default_author
+ return @default_person ||= @data['default_author']
+ end
+ def person_uri(name)
+ str = @data['person_uris'][name]
+ if str.nil?
+ return nil
+ end
+ return URI::parse(str)
+ end
+ def person_email(name)
+ return @data['person_emails'][name]
+ end
+ # Categories
+ def categories
+ return @data['categories'].keys
+ end
+ def category_name(abbr)
+ return @data['categories'][abbr]
+ end
+end
diff --git a/lib/license.rb b/lib/license.rb
new file mode 100644
index 0000000..da68f30
--- /dev/null
+++ b/lib/license.rb
@@ -0,0 +1,17 @@
+# coding: utf-8
+require 'config'
+
+class License
+ def initialize(name)
+ @name = name
+ end
+ def name
+ @name
+ end
+ def url
+ Config::get.license_uri(@name)
+ end
+ def html
+ "<a href=\"#{url}\">#{name}</a>"
+ end
+end
diff --git a/lib/page.rb b/lib/page.rb
new file mode 100644
index 0000000..20f9701
--- /dev/null
+++ b/lib/page.rb
@@ -0,0 +1,103 @@
+# coding: utf-8
+require 'erb'
+require 'set'
+
+require 'category'
+
+class Page
+ # Page is an abstract class.
+ #
+ # Implementors must implement several methods:
+ #
+ # def url => URI
+ # def atom_title => String
+ # def atom_author => Person
+ # def atom_content => html | nil
+ # def atom_rights => html | nil
+ #
+ # def page_categories => String | Enumerable<String>
+ #
+ # def page_published => Time | nil
+ # def page_updated => Time | nil
+ # def page_years => Enumerable<Fixnum>
+
+ def atom_categories # => Enumerable<Category>
+ if @categories.nil?
+ raw = page_categories
+ if raw.is_a?(String)
+ raw = raw.split
+ end
+ @categories = raw.map{|abbr|Category.new(abbr)}
+ end
+ @categories
+ end
+
+ def atom_published # => Time | nil
+ if @published.nil?
+ unless page_published.nil?
+ @published = page_published
+ else
+ unless page_updated.nil?
+ @published = page_updated
+ end
+ end
+ # sanity check
+ unless page_published.nil? or page_updated.nil?
+ if page_updated < page_published
+ @published = page_updated
+ end
+ end
+ end
+ @published
+ end
+
+ def atom_updated # => Time | nil
+ if @updated.nil?
+ unless page_updated.nil?
+ @updated = page_updated
+ else
+ unless page_published.nil?
+ @updated = page_published
+ end
+ end
+ end
+ @updated
+ end
+
+ def years # => Enumerable<Fixnum>
+ if @years.nil?
+ if atom_published.nil? || atom_updated.nil?
+ @years = Set[]
+ else
+ first = atom_published.year
+ last = atom_updated.year
+
+ years = page_years
+ years.add(first)
+ years.add(last)
+
+ @years = Set[*years.select{|i|i >= first && i <= last}]
+ end
+ end
+ @years
+ end
+
+ def index_class
+ return ''
+ end
+ def index_link(cururl, depth)
+ # FIXME: This code is super gross.
+ ret = " * <span><a class=\"#{index_class}\" href=\"#{cururl.route_to(url)}\" title=\"Published on #{atom_published.strftime('%Y-%m-%d')}"
+ if atom_updated != atom_published
+ ret += " (updated on #{atom_updated.strftime('%Y-%m-%d')})"
+ end
+ ret += "\">#{atom_title}</a></span><span>"
+ atom_categories.each do |t|
+ ret += t.html
+ end
+ ret += "</span>\n"
+ end
+end
+
+ERB::new(File::read("tmpl/page.atom.erb")).def_method(Page, 'atom()', "tmpl/page.atom.erb")
+ERB::new(File::read("tmpl/page.html.erb")).def_method(Page, 'html()', "tmpl/page.html.erb")
diff --git a/lib/page_index.rb b/lib/page_index.rb
new file mode 100644
index 0000000..66b528a
--- /dev/null
+++ b/lib/page_index.rb
@@ -0,0 +1,121 @@
+# coding: utf-8
+require 'erb'
+require 'set'
+require 'yaml'
+
+require 'config'
+require 'page_local'
+require 'page_remote'
+require 'person'
+
+class IndexPage < LocalPage
+ def initialize(dirname)
+ super(dirname)
+ end
+
+ def _metadata
+ if @metadata.nil?
+ yamlfile = local_infile+"/index.yaml"
+ if File::exist?(yamlfile)
+ @metadata = YAML::load(File::read(yamlfile))
+ else
+ @metadata = {}
+ end
+ end
+ @metadata
+ end
+ def _ls
+ @ls ||= Dir::entries(local_infile)
+ .select{|fname|not fname.start_with?(".")}
+ .map{|fname|"#{local_infile}/#{fname}"}
+ .select{|path|Dir::exist?(path) or Config::get.html_suffixes.include?(File::extname(path).gsub(/^[.]/, ''))}
+ end
+ def index_pages
+ if @pages.nil?
+ @pages = Set[]
+ for path in _ls
+ @pages.add( Dir::exist?(path) ? IndexPage::new(path) : LocalPage::new(path) )
+ end
+ for data in (_metadata['external'] || [])
+ @pages.add(RemotePage::new(data))
+ end
+ end
+ @pages
+ end
+ def index_pages_leaves
+ ret = Set[]
+ index_pages.each do |page|
+ if page.is_a?(IndexPage)
+ ret.merge(page.index_pages)
+ else
+ ret.add(page)
+ end
+ end
+ return ret
+ end
+
+ def index_link(cururl, depth)
+ ret = ''
+ unless depth <= 1
+ ret += "<h#{depth}>[#{atom_title}](#{cururl.route_to(url)})</h#{depth}>\n\n"
+ end
+ for page in index_pages.select{|page|not page.is_a?(IndexPage)}.sort_by{|page|page.atom_published}
+ ret += page.index_link(cururl, depth+1)
+ end
+ ret += "\n"
+ for page in index_pages.select{|page|page.is_a?(IndexPage)}.sort_by{|page|page.atom_title}
+ ret += page.index_link(cururl, depth+1)
+ end
+ ret += "\n"
+ return ret.gsub(/\n\n+/, "\n\n")
+ end
+ def atom_title
+ _metadata['title']
+ end
+ def atom_author
+ Person::new(_metadata['author'] || Config::get.default_author)
+ end
+
+ def local_intype
+ return 'markdown'
+ end
+ def local_outfile
+ local_infile.sub(/^src/, 'out')+"/index.html"
+ end
+ def local_depends
+ if @depends.nil?
+ basename = local_infile.sub(/^src/, 'out')
+ deps = Set[local_infile]
+ yamlfile = local_infile+"/index.yaml"
+ if File::exist?(yamlfile)
+ deps.add(yamlfile)
+ end
+ index_pages.select{|p|!p.is_a?(RemotePage)}.each{|p|deps.merge(p.local_depends[''])}
+ @depends = {
+ '' => deps,
+ "#{basename}/index.html" => deps.clone.merge(["tmpl/index.md.erb", "tmpl/page.html.erb"]),
+ "#{basename}/index.atom" => deps.clone.merge(["tmpl/index.atom.erb", "tmpl/page.atom.erb"]),
+ }
+ end
+ @depends
+ end
+ def url
+ @outurl ||= Config::get.url + local_outfile.sub(/^out/, '').sub(/\/index\.html$/, '/')
+ end
+ def local_srcurl
+ return nil
+ end
+
+ def page_published
+ return nil
+ end
+ def page_updated
+ return nil
+ end
+ def page_years
+ return Set[]
+ end
+end
+
+ERB::new(File::read("tmpl/index.atom.erb")).def_method(IndexPage, 'atom()', "tmpl/index.atom.erb")
+ERB::new(File::read("tmpl/index.md.erb")).def_method(IndexPage, 'local_input()', "tmpl/index.md.erb")
diff --git a/lib/page_local.rb b/lib/page_local.rb
new file mode 100644
index 0000000..7121c8e
--- /dev/null
+++ b/lib/page_local.rb
@@ -0,0 +1,122 @@
+# coding: utf-8
+require 'date'
+require 'set'
+
+require 'config'
+require 'license'
+require 'page'
+require 'pandoc'
+require 'person'
+require 'sitegen'
+
+class LocalPage < Page
+ def initialize(infile)
+ @infile = infile
+ Sitegen::add(self)
+ end
+
+ # Some of this code looks a little weird because it is
+ # super-aggressively lazy-evaluated and cached.
+
+ def local_infile ; @infile ; end
+ def local_input ; @input ||= File::read(local_infile); end
+ def local_intype
+ types = {
+ 'md' => 'markdown'
+ }
+ ext = File::extname(local_infile).gsub(/^[.]/, '')
+ return types[ext] || ext
+ end
+ def _pandoc
+ if @pandoc.nil?
+ @pandoc = Pandoc::load(local_intype, local_input)
+
+ if @pandoc['pandoc_format']
+ @pandoc = Pandoc::load(@pandoc['pandoc_format'], local_input)
+ end
+ end
+ @pandoc
+ end
+
+ # Query simple document metadata
+ def atom_author ; @author ||= Person::new( _pandoc['author'] || Config::get.default_author) ; end
+ def atom_title ; @title ||= _pandoc['title'] || local_input.split("\n",2).first ; end
+ def html_class ; @class ||= _pandoc['class'] ; end
+ def html_head_extra ; @head ||= _pandoc['html_head_extra'] ; end
+ def local_license ; @license ||= License::new(_pandoc['license'] || Config::get.default_license); end
+ def page_categories ; @cats ||= _pandoc['categories'] || [] ; end
+
+ def atom_content
+ if @content.nil?
+ @content = ''
+ # Only insert the title if it came from Pandoc metadata;
+ # if the title was inferred from the the body content,
+ # then it is already in the page.
+ unless _pandoc['title'].nil?
+ @content += "<h1 class=title>#{atom_title}</h1>\n"
+ end
+
+ # Insert the body
+ @content = _pandoc.to('html5 '+(_pandoc['pandoc_flags']||''))
+ end
+ @content
+ end
+
+ def atom_rights
+ # TODO: simplify year spans
+ @rights ||= "<p>The content of this page is Copyright © #{years.sort.join(', ')} #{atom_author.html}.</p>\n" +
+ "<p>This page is licensed under the #{local_license.html} license.</p>"
+ end
+
+ def _gitdates
+ @gitdates ||= `git log --format='%cI' -- #{local_infile}`.split("\n").select{|s|!s.empty?}.map{|s|DateTime::iso8601(s).to_time}
+ end
+
+ def page_published
+ if @_published.nil?
+ raw = _pandoc['published']
+ @_published = DateTime::parse(raw).to_time unless raw.nil?
+ end
+ if @_published.nil?
+ @_published = _gitdates.sort.first
+ end
+ @_published
+ end
+
+ def page_updated
+ if @_updated.nil?
+ raw = _pandoc['updated']
+ @_updated = DateTime::parse(raw).to_time unless raw.nil?
+ end
+ if @_updated.nil?
+ @updated = _gitdates.sort.last
+ end
+ @_updated
+ end
+
+ def page_years
+ @years ||= Set[*_gitdates.map{|dt|dt.year}]
+ end
+
+ def local_outfile
+ local_infile.sub(/^src/, 'out').sub(/\.[^\/.]*$/, '.html')
+ end
+ def local_depends
+ if @depends.nil?
+ basename = local_infile.sub(/^src/, 'out').sub(/\.[^\/.]*$/, '')
+ @depends = {
+ '' => Set[local_infile],
+ "#{basename}.html" => Set[local_infile, "tmpl/page.html.erb"],
+ #"#{basename}.atom" => Set[local_infile, "tmpl/page.atom.erb"]
+ }
+ end
+ @depends
+ end
+
+ def local_srcurl
+ @srcurl ||= Config::get.url + local_infile.sub(/^src/, '')
+ end
+ def url
+ @outurl ||= Config::get.url + local_outfile.sub(/^out/, '')
+ end
+end
diff --git a/lib/page_remote.rb b/lib/page_remote.rb
new file mode 100644
index 0000000..5425944
--- /dev/null
+++ b/lib/page_remote.rb
@@ -0,0 +1,59 @@
+# coding: utf-8
+require 'date'
+
+require 'config'
+require 'page'
+
+class RemotePage < Page
+ def initialize(metadata)
+ @metadata = metadata
+ end
+
+ def url
+ return Config::get.url + @metadata['url']
+ end
+
+ def atom_title
+ @metadata['title']
+ end
+
+ def atom_author
+ Person::new(@metadata['author'] || Config::get.default_author)
+ end
+
+ def atom_content
+ return nil
+ end
+
+ def atom_rights
+ return nil
+ end
+
+ def page_categories
+ @metadata['categories'] || []
+ end
+
+ def page_published
+ str = @metadata['published']
+ if str.nil?
+ return nil
+ end
+ return DateTime::parse(str).to_time
+ end
+
+ def page_updated
+ str = @metadata['updated']
+ if str.nil?
+ return nil
+ end
+ return DateTime::parse(str).to_time
+ end
+
+ def page_years
+ return []
+ end
+
+ def index_class
+ return 'external'
+ end
+end
diff --git a/lib/pandoc.rb b/lib/pandoc.rb
new file mode 100644
index 0000000..f0bf3f6
--- /dev/null
+++ b/lib/pandoc.rb
@@ -0,0 +1,96 @@
+# coding: utf-8
+require 'open3'
+require 'json'
+
+module Pandoc
+ def self.prog
+ @prog ||= 'pandoc'
+ end
+ def self.prog=(val)
+ @prog = val
+ end
+ def self.load(fmt, input)
+ cmd = Pandoc::prog + " -t json"
+ unless fmt.nil?
+ cmd += " -f " + fmt
+ end
+ str = input
+ if str.respond_to? :read
+ str = str.read
+ end
+ json = ''
+ errors = ''
+ Open3::popen3(cmd) do |stdin, stdout, stderr|
+ stdin.puts(str)
+ stdin.close
+ json = stdout.read
+ errors = stderr.read
+ end
+ unless errors.empty?
+ raise errors
+ end
+ return Pandoc::AST::new(json)
+ end
+
+ class AST
+ def initialize(json)
+ @js = JSON::parse(json)
+ end
+
+ def [](key)
+ Pandoc::AST::js2sane(@js["meta"][key])
+ end
+
+ def js
+ @js
+ end
+
+ def to(format)
+ cmd = Pandoc::prog + " -f json -t " + format.to_s
+ output = ''
+ errors = ''
+ Open3::popen3(cmd) do |stdin, stdout, stderr|
+ stdin.puts @js.to_json
+ stdin.close
+ output = stdout.read
+ errors = stderr.read
+ end
+ unless errors.empty?
+ raise errors
+ end
+ return output
+ end
+
+ def self.js2sane(js)
+ if js.nil?
+ return js
+ end
+ case js["t"]
+ when "MetaMap"
+ Hash[js["c"].map{|k,v| [k, js2sane(v)]}]
+ when "MetaList"
+ js["c"].map{|c| js2sane(c)}
+ when "MetaBool"
+ js["c"]
+ when "MetaString"
+ js["c"]
+ when "MetaInlines"
+ js["c"].map{|c| js2sane(c)}.join()
+ when "MetaBlocks"
+ js["c"].map{|c| js2sane(c)}.join("\n")
+ when "Str"
+ js["c"]
+ when "Space"
+ " "
+ when "RawInline"
+ js["c"][1]
+ when "RawBlock"
+ js["c"][1]
+ when "Para"
+ js["c"].map{|c| js2sane(c)}.join()
+ else
+ throw js["t"]
+ end
+ end
+ end
+end
diff --git a/lib/person.rb b/lib/person.rb
new file mode 100644
index 0000000..6ad1569
--- /dev/null
+++ b/lib/person.rb
@@ -0,0 +1,32 @@
+# coding: utf-8
+require 'config'
+
+class Person
+ def initialize(name)
+ @name = name
+ end
+ def name
+ @name
+ end
+ def uri
+ Config::get.person_uri(@name)
+ end
+ def email
+ Config::get.person_email(@name)
+ end
+ def html
+ if not email.nil?
+ return "<a href=\"mailto:#{email}\">#{name}</a>"
+ elsif not uri.nil?
+ return "<a href=\"#{uri}\">#{name}</a>"
+ else
+ return name
+ end
+ end
+ def atom
+ ret = ""
+ ret += "<name>#{name}</name>" unless name.nil?
+ ret += "<uri>#{uri}</uri>" unless uri.nil?
+ ret += "<email>#{email}</email>" unless email.nil?
+ end
+end
diff --git a/lib/sitegen.rb b/lib/sitegen.rb
new file mode 100644
index 0000000..ca0c4bf
--- /dev/null
+++ b/lib/sitegen.rb
@@ -0,0 +1,98 @@
+# coding: utf-8
+require 'date'
+require 'fileutils'
+require 'set'
+
+module Sitegen
+ def self.init
+ @mk = {}
+ @want = Set[]
+ end
+ def self.add(page)
+ @deps = nil
+ page.local_depends.keys.each do |filename|
+ @mk[filename] = page unless filename.empty?
+ end
+ end
+ def self.pages
+ @mk.values.to_set
+ end
+ def self.want(filename)
+ @deps = nil
+ @want.add(filename)
+ end
+ def self.dependencies
+ if @deps.nil?
+ libfiles = Dir::entries('lib').select{|s|s!='..'}.map{|s|"lib/#{s}"}.to_set
+ ret = {}
+ ret[:all] = @want
+ @want.each do |filename|
+ ret[filename] = libfiles.clone.merge(@mk[filename].local_depends[filename])
+ end
+ @deps = ret
+ end
+ @deps
+ end
+ def self.Makefile()
+ str = ''
+ dependencies.each do |target, deps|
+ str += "#{target.to_s}: #{deps.sort.join(' ')}\n"
+ end
+ return str
+ end
+
+ def self.make(target)
+ newest = Time::new(0)
+ (dependencies[target] || []).each do |dep|
+ ts = make(dep)
+ newest = ts if ts > newest
+ end
+ unless target.is_a?(String)
+ return Time::now
+ end
+ if File::exist?(target)
+ ctime = File::ctime(target)
+ if ctime > newest
+ return ctime
+ end
+ end
+ generate(target)
+ return File::ctime(target)
+ end
+
+ def self.generate(target)
+ case
+ when @mk[target].nil?
+ raise "No rule to make target '#{target}'. Stop."
+ when target.end_with?(".atom")
+ puts "atom #{target}"
+ write_atomic(target) do |file|
+ file.puts('<?xml version="1.0" encoding="utf-8"?>')
+ file.print(@mk[target].atom)
+ end
+ when target.end_with?(".html")
+ puts "html #{target}"
+ write_atomic(target) do |file|
+ file.print(@mk[target].html)
+ end
+ else
+ raise "No rule to make target '#{target}'. Stop."
+ end
+ end
+
+ def self.write_atomic(outfilename)
+ tmpfilename = "#{File::dirname(outfilename)}/.tmp#{File::basename(outfilename)}"
+
+ FileUtils::mkdir_p(File::dirname(tmpfilename))
+ tmpfile = File::new(tmpfilename, 'wb')
+ begin
+ yield tmpfile
+ rescue Exception => e
+ tmpfile.close
+ File::unlink(tmpfilename)
+ raise e
+ end
+ tmpfile.close
+ File::rename(tmpfilename, outfilename)
+ end
+end
diff --git a/lib/siteutil.rb b/lib/siteutil.rb
new file mode 100644
index 0000000..3f36ff2
--- /dev/null
+++ b/lib/siteutil.rb
@@ -0,0 +1,27 @@
+# coding: utf-8
+require 'config'
+require 'sitegen'
+
+module Sitegen
+ def self.html_escape(html)
+ html
+ .gsub('&', '&amp;')
+ .gsub('>', '&gt;')
+ .gsub('<', '&lt;')
+ end
+
+ def self.breadcrumbs(url)
+ path = url.path
+ path = "/" if path == ""
+ bc = []
+ while true
+ a = 'out'+path
+ b = ('out'+path+'/index.html').gsub('//', '/')
+ page = @mk[a] || @mk[b]
+ bc.unshift("<a href=\"#{url.route_to(page.url)}\">#{page.atom_title}</a>")
+ break if path == "/"
+ path = File::dirname(path)
+ end
+ return bc.join(' » ')
+ end
+end