diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/category.rb | 20 | ||||
-rw-r--r-- | lib/config.rb | 51 | ||||
-rw-r--r-- | lib/license.rb | 17 | ||||
-rw-r--r-- | lib/page.rb | 103 | ||||
-rw-r--r-- | lib/page_index.rb | 121 | ||||
-rw-r--r-- | lib/page_local.rb | 122 | ||||
-rw-r--r-- | lib/page_remote.rb | 59 | ||||
-rw-r--r-- | lib/pandoc.rb | 96 | ||||
-rw-r--r-- | lib/person.rb | 32 | ||||
-rw-r--r-- | lib/sitegen.rb | 98 | ||||
-rw-r--r-- | lib/siteutil.rb | 27 |
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('&', '&') + .gsub('>', '>') + .gsub('<', '<') + 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 |