このサイトができるまで


はじめに

以下, obsolteです

このサイトは、記事を Emacs の Org mode で書いて、 Jekyll で HTML に変換しています。 org ファイルそのものに(なるべく)手を加えることなく、 それなりに一貫性を持ってコンテンツを作成するために、 幾つかプラグインを書いています。その解説など。

作ったモノ

コードの詳細は Github をご覧下さい。

使い方

jekyll の _plugin に submodule としてでも放り込んで下さい。

org ファイルを html へ出力する際のタグのカスタマイズをしたい場合には、 _html_tags.yml を作成しておきます(リポジトリに example を置いてあります)。

原稿となる org ファイルには、最低限

#+TITLE:
#+DATE:
#+LAYOUT:

を記載しておいて下さい。これが Jekyll の Front Matter になります。 他にも #+HTML および #+LATEX 以外の Option が書け、 downcase したのち Front Matter として使用できます。

解説(?)

解説するようなモンでもないですけれど。

org ファイルの変換

ファイルの HTML への変換には wallyqs/org-ruby を使っています。

  • 利点: Emacs の org-exporter のカスタマイズを行なう必要が無い。
  • 利点: jekyll に自然に組み込める
  • 欠点: org-exporter での code block 実行結果の export が使えなくなる

org-exporter による「code block 実行結果の埋め込み」を使いたい場合には、 org-exporter での html 出力をカスタマイズした後に、jekyll で処理することになります。 このアプローチは org-jekyll, export blog posts from org-mode でしょう。 また、Github Pages でコンテンツを公開することを考える場合には、 plugin が使えないため、 リンク先のアプローチを取る必要があります。

以前は同様の事を行なっていたのですが、

  1. jekyll を動作させるサーバ上の Emacs のバージョンと手元のバージョンとの乖離
  2. org のファイルと html ファイルの双方をリポジトリで管理する無駄さ

から、結局 org ファイルを直接 jekyll で処理することにしました。

org-ruby の拡張(?)

最新の org-ruby では org-mode の search link を export してくれません。 そんな訳で monkey patch です。 Orgmode::HtmlOutputBuffer.inline_formatting が割と大きな method なので、 小さな monkey patch のつもりが、ほぼコピペになってしまいました…。

#! /usr/bin/env ruby
# -*- mode: ruby; coding: utf-8 -*-
# file: org_monkey.rb
#
# Monkey Patch: Add support org search link
require 'org-ruby'

module Orgmode
  # monkey patch Orgmode::to_html inline_formatter
  class HtmlOutputBuffer < OutputBuffer

    alias_method :_orig_add_line_attributes, :add_line_attributes
    def add_line_attributes(headline)
      _orig_add_line_attributes(headline)
      @output << "<a id=\"#{headline.headline_text}\"><span class='anchor'>_</span></a>"
    end

    alias_method :_orig_inline_formatting, :inline_formatting
    def inline_formatting(str)
      @re_help.rewrite_emphasis str do |marker, s|
        if marker == "=" or marker == "~"
          s = escapeHTML s
          "<#{Tags[marker][:open]}>#{s}</#{Tags[marker][:close]}>"
        else
          quote_tags("<#{Tags[marker][:open]}>") + s +
            quote_tags("</#{Tags[marker][:close]}>")
        end
      end

      if @options[:use_sub_superscripts] then
        @re_help.rewrite_subp str do |type, text|
          if type == "_" then
            quote_tags("<sub>") + text + quote_tags("</sub>")
          elsif type == "^" then
            quote_tags("<sup>") + text + quote_tags("</sup>")
          end
        end
      end

      @re_help.rewrite_links str do |link, defi|
        [link, defi].compact.each do |text|
          # We don't support search links right now. Get rid of it.
          # -> Support search links!!
          text.sub!(/\A(file:[^\s]+)::([^\s]*?)\Z/, "\\1#\\2")
          text.sub!(/\Afile:(?=[^\s]+\Z)/, "")
        end

        # We don't add a description for images in links, because its
        # empty value forces the image to be inlined.
        defi ||= link unless link =~ @re_help.org_image_file_regexp

        if defi =~ @re_help.org_image_file_regexp
          defi = quote_tags "<img src=\"#{defi}\" alt=\"#{defi}\" />"
        end

        if defi
          link = @options[:link_abbrevs][link] if @options[:link_abbrevs].has_key? link
          quote_tags("<a href=\"#{link}\">") + defi + quote_tags("</a>")
        else
          quote_tags "<img src=\"#{link}\" alt=\"#{link}\" />"
        end
      end

      if @output_type == :table_row
        str.gsub! /^\|\s*/, quote_tags("<td>")
        str.gsub! /\s*\|$/, quote_tags("</td>")
        str.gsub! /\s*\|\s*/, quote_tags("</td><td>")
      end

      if @output_type == :table_header
        str.gsub! /^\|\s*/, quote_tags("<th>")
        str.gsub! /\s*\|$/, quote_tags("</th>")
        str.gsub! /\s*\|\s*/, quote_tags("</th><th>")
      end

      if @options[:export_footnotes] then
        @re_help.rewrite_footnote str do |name, defi|
          # TODO escape name for url?
          @footnotes[name] = defi if defi
          quote_tags("<sup><a class=\"footref\" name=\"fnr.#{name}\" href=\"#fn.#{name}\">") +
            name + quote_tags("</a></sup>")
        end
      end

      # Two backslashes \\ at the end of the line make a line break without breaking paragraph.
      if @output_type != :table_row and @output_type != :table_header then
        str.sub! /\\\\$/, quote_tags("<br />")
      end

      escape_string! str
      Orgmode.special_symbols_to_html str
      str = @re_help.restore_code_snippets str
    end

    alias_method :_orig_normalize_lang, :normalize_lang
    def normalize_lang(lang)
      case lang
      when 'conf'
        'ruby'
      else
        _orig_normalize_lang(lang)
      end
    end
  end
end

任意の org ファイルを Jekyll で扱うために

Jekyll の原稿として org ファイルを使用する際の不満は、

  1. Jekyll の原稿として認識してもらうためには、 org ファイルに yaml で書かれた Front Matter が必要。 しかしながら、これは org としては美しくない。
  2. Front Matter に記述する内容は、 そもそも org ファイルに存在する内容が多い( #+TITLE とか)。 つまり情報が重複している。

でした。

これらの不満を解消してくれるプラグインとしては、 既に eggcaker/jekyll-org があります。 ただし、これは Post 専用なので、 Jekyll の blog 記事の範疇に無いページ( いわゆる Page 等)は対象としていません。

そんな訳で、結局自分でプラグインを書きました。

Jekyll::Utils.has_yaml_header? の上書き

org ファイルに Front Matter 相当の情報が存在することを前提として処理、 すなわち、拡張子が .org の場合は常に true を返すようにします。

#!/usr/bin/env ruby
# -*- mode: ruby; coding: utf-8 -*-
# file: org_utils.rb
module Jekyll
  # Judge the file is Org mode?
  module Utils
    alias_method :_orig_has_yaml_header?, :has_yaml_header?

    def has_yaml_header?(file)
      if File.extname(file) =~ /org/
        true
      else
        _orig_has_yaml_header?(file)
      end
    end
  end
end

Jekyll::Convertible の上書き

org ファイルの #+TITLE 等を FRONTMATTER として扱った上で、 html に出力します。 また #+TITLE が 2 回出力されるのを防ぐために、 org-ruby で処理する前に #+TITLE を削除しています。

#! /usr/bin/env ruby
# -*- mode: ruby; coding: utf-8 -*-
require 'org-ruby'

module Jekyll
  # Handling Org options as YAML front matter, escape liquid tag
  module Convertible
    alias_method :_orig_read_yaml, :read_yaml

    def read_yaml(base, name, opts = {})
      if name =~ /org$/
        content = File.read(site.in_source_dir(base, name),
                            merged_file_read_opts(opts))
        if File.exist?('_html_tags.yml')
          org = Orgmode::Parser.new(content,
                                            markup_file: '_html_tags.yml')
        else
          org = Orgmode::Parser.new(content)
        end
        yaml_front_matter = {}
        org.in_buffer_settings.each_pair do |k, v|
          yaml_front_matter.merge!(k.downcase => v)
        end
        # remove '#+HTML'
        yaml_front_matter = yaml_front_matter.delete_if { |k, v| k == 'html' }
        # remove '#+LATEX'
        yaml_front_matter = yaml_front_matter.delete_if { |k, v| k == 'latex' }
        self.data = SafeYAML.load(yaml_front_matter.to_yaml + "---\n")
        # remove '#+TITLE' avoid double exporting
        org.in_buffer_settings.delete_if {|k, v| k == 'TITLE' }
        if yaml_front_matter.key?('liquid')
          self.content = org.to_html
          self.content = self.content.gsub('&#8216;', "'")
          self.content = self.content.gsub('&#8217;', "'")
        else
          self.content = <<ORG
#{org.to_html.gsub('{','&#123;').gsub('{','&#125;')}
ORG
        end
      else
        _orig_read_yaml(base, name, opts)
      end
    end
  end
end

Jekyll::OrgConverter の追加

Converter の追加は Jekyll 本家のドキュメントにもあるので、 割と簡単です。実際の処理は上書きした Jekyll::Convertible で行なわれています。 ただ、search link を処理するために、最後にリンクを置換しています。

#!/usr/bin/env ruby
# -*- mode: ruby; coding: utf-8 -*-
# file: org.rb
require 'org-ruby'

module Jekyll
  # Add New Converter handling org-mode
  # main logic -> @see org_convertible.rb
  class OrgConverter < Converter
    safe true
    priority :low

    def matches(ext)
      ext =~ /^\.org$/i
    end

    def output_ext(ext)
      '.html'
    end

    def convert(content)
      # ad hoc file link conversion
      content.gsub(/<a href="([^(http:\/\/|https:\/\/|mailto:)]\S+)\.org/,
                   "<a href=\"\\1.html")
    end
  end
end

まとめ…?

まあ、ちゃんと動いているみたいですし、これで良いのかなぁ。

お気付きの点などございましたら、Issues へお願いします。