« ^ »

HugoはどうやってEmacsのorg-mode形式のファイルからHTMLを生成しているのか?

所要時間: 約 7分

静的サイトジェネレーターであるHugoはページの元になるファイルとしてOrgmode形式のファイルを使用できる。Org modeはEmacsで用いられる軽量マークアップ言語と、それに関連するシステム全体のことを指す。Emacs上ではEmacsLispによってOrg mode形式の解析などが行われる。一方HugoはGo言語で実装されているため、Org modeのコードをEmacs Lispのまま流用することはできない。HugoではどのようにしてOrg modeを利用しているのかと疑問に思った。そのためHugoの内部でOrg modeをHTMLに変換する方法を調べることにした。

TL;DR

  • HugoでOrg-modeからHTMLを生成する方法を調べた。
  • github.com/niklasfasching/go-org を用いてHTMLの生成を行ってることがわかった。
  • Hugoのorg-modeのconvertで行っている。

目的

静的サイトジェネレーターHugoではページの元になるファイルとしてOrg-modeのファイルを使用できる。内部ではどのようにOrg-modeをHTMLに変換しているのか気になったので調べた。

変換にはgo-orgを使っている

変換処理はhugo/markup/org/convert.go で実装しており、github.com/niklasfasching/go-org を利用してOrg-modeからHTMLに変換している。

変換処理

Org-modeからHTMLへの変換処理のみを抽出すると次のような処理となる。

main.go

org.New, org.NewHTMLWriterを用いて変換を行う。

package main

import (
	"bytes"
	"fmt"
	"github.com/niklasfasching/go-org/org"
	"os"
)

func main() {

	config := org.New()
	writer := org.NewHTMLWriter()

	content, err := os.ReadFile("./foo.org")
	reader := bytes.NewReader(content)
	parser := config.Parse(reader, "foo")
	html, err := parser.Write(writer)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(html))
}

go.mod

module github.com/TakesxiSximada/symdon/pages/posts/1626346909

go 1.16

require (
	github.com/niklasfasching/go-org v1.5.0
	golang.org/x/net v0.7.0 // indirect
)

[WIP] go-orgの改行処理の挙動を修正する

go-orgでレンダリングした文章の改行位置に微妙な空白が挿入される。文章中 の改行文字が空白文字として処理されるため、文章の中途半端なところに空白 文字が含まれてしまう。これはウェブブラウザの仕様ではあるのだが、日本語 の文章を記述する立場からすると、すこぶる都合が悪い。そのため、この空白 を挿入されないようになんとかできないかを考えてみたい。

ファイル名を引数に取る簡単な変換処理の実装

orgファイルがどのようなHTMLになるのかを素早く(可能な限りインタラクティ ブに)確認できると、作業効率が向上する。今回は手間をあまりかけずにそれ を実現するために、引数にファイルへのパスを渡すとそのファイルをhtmlに変 換し標準出力に表示するプログラムを実装した。

convert_html_simple.go::

package main

import (
	"bytes"
	"fmt"
	"os"

	"github.com/niklasfasching/go-org/org"
)

func main() {
	fileName := os.Args[1]
	fp, err := os.ReadFile(fileName)
	if err != nil {
		panic(err)
	}
	r := bytes.NewReader(fp)

	c := org.New()
	p := c.Parse(r, "foo")

	w := org.NewHTMLWriter()
	html, err := p.Write(w)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(html))
}

通常の状態で実行する

テスト用に2つの文章のブロックがあるファイルを作成した。

simple.org::

テストテストテストテストテスト
テストテストテストテストテスト
テストテストテストテストテスト
テストテストテストテストテスト


テストテストテストテストテスト
テストテストテストテストテスト
テストテストテストテストテスト
テストテストテストテストテスト


#+begin_src golang
func main () {
  return nil
}
#+end_src

このファイルを先程の変換プログラムに渡す。

go run convert_html_simple.go simple.org
<p>テストテストテストテストテスト
テストテストテストテストテスト
テストテストテストテストテスト
テストテストテストテストテスト</p>
<p>
テストテストテストテストテスト
テストテストテストテストテスト
テストテストテストテストテスト
テストテストテストテストテスト</p>
<div class="src src-golang">
<div class="highlight">
<pre>
func main () {
  return nil
}
</pre>
</div>
</div>

pタグで括られた文章のブロックが2つ出力された。この挙動は通常正しいもの とも考えられる。例えば以下の英語の文章を考える。

<p>This is
a pen.</p>

isの直後に改行を挿入している。この改行を何も存在しないように処理してしまうと、isとaが繋がり以下のような文章ができあがってしまう。

This isa pen.

これでは明らかに不便だ。そのため改行は空白として処理されるようになっている。ただし日本語の文章の場合は事情が異なる。日本語の場合には単語を空白で区切らないため、改行を空白で処理してしまうと、不要なところに空白が入った文章になる。日本語の文章を改行を混ぜすに記述すれば済む話ではあるが、本来は文を処理するプログラムのどこかで、空白の要不要を適切に判断し挿入が必要な場合のみ空白が挿入されるべきだ。文中にマルチバイト文字が含まれているかどうかを判断基準に出来るだろうか。マルチバイト文字が含まれているが改行の空白変換は必要になるケースとしては、次のような文章が考えられる。

Hi, しむどん san. How
are you?

文中にマルチバイト文字が含まれる英文は普通に存在する。またエモーティコンのような顔文字を使用している場合にも同様だろう。そのためマルチバイト文字が含まれるかどうかは、改行文字の空白変換の必要性の判断基準にはならない。改行の前後の文字が特定の文字であれば改行を除去し、そうでなければ空白を挿入するという提案がbugzillaにはあるが作成時期が13年前、最終更新が10年前だった。この処理はある意味では正しいように思える。提案はブラウザの挙動として変更を検討するものだが、現状では変換処理で対応するほうが良いだろう。単純に改行文字をそのまま処理するのではなく、生成される要素自体を変更するほうが筋が良いようにも思える。先程の出力例を考えると、次のように出力を明示的に切り替えることができると、CSSにより挙動を変更しやすくて都合がよさそうだ。

<p><span>テストテストテストテストテスト</span><span>テストテストテストテストテスト</span><span>テストテストテストテストテスト</span><span>テストテストテストテストテスト</span></p>
<p><span>テストテストテストテストテスト</span><span>テストテストテストテストテスト</span><span>テストテストテストテストテスト</span><span>テストテストテストテストテスト</span></p>

とりあえずパラグラフ内の改行を挿入しないように修正してみる

go-orgを以下のように変更することでパラグラフ内の改行を挿入しないように修正できることがわかった。

diff -u go/pkg/mod/github.com/niklasfasching/go-org\@v1.5.0/org/paragraph.go.old /Users/sximada/go/pkg/mod/github.com/niklasfasching/go-org\@v1.5.0/org/paragraph.go
--- go/pkg/mod/github.com/niklasfasching/[email protected]/org/paragraph.go.old	2022-05-30 09:56:15.000000000 +0900
+++ go/pkg/mod/github.com/niklasfasching/[email protected]/org/paragraph.go	2022-05-30 09:57:51.000000000 +0900
@@ -36,7 +36,7 @@
 		lines = append(lines, strings.Repeat(" ", int(lvl))+d.tokens[i].content)
 	}
 	consumed := i - start
-	return consumed, Paragraph{d.parseInline(strings.Join(lines, "\n"))}
+	return consumed, Paragraph{d.parseInline(strings.Join(lines, ""))}
 }

 func (d *Document) parseHorizontalRule(i int, parentStop stopFn) (int, Node) {

Diff finished.  Mon May 30 09:58:27 2022

実行すると次のようになる。

go run convert_html_simple.go simple.org
<p>テストテストテストテストテストテストテストテストテストテストテストテストテストテストテストテストテストテストテストテスト</p>
<p>テストテストテストテストテストテストテストテストテストテストテストテストテストテストテストテストテストテストテストテスト</p>
<div class="src src-golang">
<div class="highlight">
<pre>
func main () {
  return nil
}
</pre>
</div>
</div>

挙動としては期待値どおりだ。しかしこれでは英文を記述する際に行末の単語と行頭の単語が繋ってしまう。そのためこれらをうまく切り替えられればならない。

参考