0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AAoM-01:Pug テンプレートエンジン

0
Posted at

AAoM-01:Pug テンプレートエンジン

本記事は「Agentic Adventures of MoonBit(AAoM)」シリーズの第 1 回です。
MoonBit と AI の支援を活用し、実用的なライブラリを一つ完成させるまでの過程を、できるだけ透明に記録していきます。

今回は Pug テンプレートエンジン を MoonBit で実装しました。
開発には Claude Code(Opus 4.5) を使用し、MoonBit 用に用意した system prompt と IDE の支援機能を組み合わせています。

基本的な流れは非常にシンプルです。

要件を私が説明する

Claude がコードを書く

私がレビューし、修正・改善する

Claude は moon new によるプロジェクト生成、
moon.mod.json / moon.pkg.json の理解、
MoonBit らしいコードの生成において、かなり高い精度を示しました。

課題

生の HTML は冗長で、ネスト構造を手書きするのは面倒かつミスが起きやすいものです。
Pug(旧 Jade) は、インデントベースの簡潔な記法によって、この問題を解決します。

doctype html
html
  head
    title My Site
  body
    h1#greeting.hero Hello, World!

今回の目標は、MoonBit で Pug → HTML のコンパイラを実装することです。
対応範囲は、Pug の主要機能を網羅することを目指しました。

タグ、属性

文字列補間

条件分岐、ループ

mixin

include / extends によるテンプレート継承

アプローチ

まず Claude に Pug の公式仕様を読ませ、テストを先に書かせました。
以下はその一例です。

test "case with fall through" {
  let pug =
    #|case num
    #|  when 0
    #|  when 1
    #|  when 2
    #|    p Small number
    #|  default
    #|    p Large number
  let locals = Locals::new()
  locals.set("num", "1")
  let html = render_with_locals(pug, locals)
  inspect(html, content="<p>Small number</p>")
}

最終的に 18 個のブラックボックステストファイルに 153 個のテストが含まれています。
この テスト駆動(TDD)スタイルにより、仕様の抜け漏れやエッジケースを早期に洗い出すことができました。

実装が始まった後は、生成されたテストを都度レビューしながら進めています。

コンパイラの構成は、比較的オーソドックスです。

lexer.mbt

parser.mbt

render.mbt

実装上のポイント
● ファイルシステム対応(include / extends)

ファイル読み込み周りは Claude が苦手とする部分だったため、
@moonbitlang/x/fs を使って明示的に補強しました。

最初に Claude が提案した API は以下のようなものでした。

let registry = TemplateRegistry::new()
registry.register("includes/head.pug", @fs.read_file_to_string("example/includes/head.pug"))
registry.register("includes/foot.pug", @fs.read_file_to_string("example/includes/foot.pug"))
let html = render_with_registry(source, Locals::new(), registry)

最終的には、より自然な API に整理しました。

let html = render_file("example/index.pug")

render_file は入力ファイルのパスを基準に、
include / extends を自動的に解決します。

● JavaScript 式の補間

最も難しかったのは、#{msg.toUpperCase()} のような
JavaScript 式の評価です。

MoonBit は複数バックエンド(Wasm / Native / JS)を想定しているため、
JS インタプリタを実装するのは現実的ではありません。

そこで、JS バックエンドでは直接 eval を呼び出す設計にしました(疑似コード)。

extern "js" fn eval(expr : String) -> String = "(expr) => eval(expr)"

JS 以外のバックエンドでは
「JS バックエンドのみ対応」と明示的にエラーを出すようにしています。

● CLI(コマンドラインインターフェース)

最後に CLI を実装しました。
この部分はほぼ Claude に任せ、私は実行結果を確認しながら微調整しています。

結果

最終的に、以下の機能を備えた 実用的な Pug テンプレートエンジンが完成しました。

タグ、ID、クラス、属性

インデントベースのネスト

文字列補間(#{} / !{})

条件分岐(if / else if / else / unless)

ループ(each / while)

引数・ブロック付き mixin

テンプレート継承(include / extends / block)

JSON locals を受け取る CLI

テストはすべて成功しています
(共通テスト 140、JS 専用テスト 13)。

● CLI 使用例

$ echo '{"name": "World"}' > data.json
$ echo 'h1 Hello #{name}!' > greeting.pug
$ moon run cmd/main -- -O data.json greeting.pug
<h1>Hello World!</h1>

● コンパイル済みテンプレートの再利用

テンプレートは一度コンパイルすれば、
異なる locals で何度でもレンダリングできます。

test "compile api" {
  // Compile template once
  let template = @pug.compile("p Hello #{name}!")

  // Render with different locals
  let locals1 = @pug.Locals::new()
  locals1.set("name", "Alice")
  inspect(template.render(locals1), content="<p>Hello Alice!</p>")
  let locals2 = @pug.Locals::new()
  locals2.set("name", "Bob")
  inspect(template.render(locals2), content="<p>Hello Bob!</p>")
}

API は Pug 本家に近い設計になっています。

振り返り

テスト先行は非常に効果的
Claude に仕様を読ませてテストを書かせることで、抜け漏れを防げました。

MoonBit のパターンマッチは AST 処理を明快にします。

ファイル I/O やエラーハンドリングなど、
Claude が MoonBit に不慣れな部分は、人間側の補助がまだ必要です。

私はよく Claude に
「まだ実装されていない Pug の機能は?」
と聞き、TODO を洗い出させてから実装させています。

所要時間

実作業時間はおよそ 6 時間でした。
テスト生成:数分
コア機能:3 時間
include / extends:1 時間
JS 補間・CLI:2 時間

Code is available on github.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?