Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyで独自のテンプレートレクサーを作成する

スキューバダイビングの装備を整え、ステンシルを用意してください。今日はテンプレートの世界へ潜ります!

Webページのレンダリングやメール生成を行うほとんどのソフトウェアは、テンプレート機能を使って変数データをテキストドキュメントに埋め込みます。ドキュメントの主要構造は静的なテンプレートとして定義され、プレースホルダーにデータが入力されます。ユーザー名やWebページの内容などの変数データが、レンダリング時にプレースホルダーを置き換えます。

今回のテンプレート探求では、多くのプログラミング言語で利用可能なテンプレート言語「Mustache」のサブセットを実装します。この記事では、さまざまなテンプレート手法を調査します。文字列連結から始め、より複雑なテンプレートを可能にする独自のレクサー(字句解析器)の作成まで進みます。

ネイティブな文字列展開の利用

まずは最小限の例から始めましょう。アプリケーションにプロジェクト名を含むウェルカムメッセージが必要だとします。最も手っ取り早いのは、Rubyの組み込み文字列展開機能を使うことです。

name = "Ruby Magic"
template = "Welcome to #{name}"
# => Welcome to Ruby Magic

簡単ですね。しかし、このテンプレートを複数回再利用したい場合や、ユーザーがテンプレートを更新できるようにしたい場合はどうでしょう?

展開は即座に評価されます。テンプレートを再利用することはできません(ループ内で再定義しない限り)。また、Welcome to #{name} というテンプレートをデータベースに保存し、後で値を埋め込むには、潜在的に危険な eval 関数を使う必要があります。

幸い、Rubyには別の文字列展開方法があります:Kernel#sprintf または String#% です。これらを使えば、テンプレート自体を変更せずに展開済み文字列を得られます。同じテンプレートを何度も再利用でき、任意のRubyコード実行も許しません。早速使ってみましょう。

name = "Ruby Magic"
template = "Welcome to %{name}"

sprintf(template, name: name)
# => "Welcome to Ruby Magic"

template % { name: name }
# => "Welcome to Ruby Magic"

正規表現によるテンプレート処理

上記の解決策は機能しますが、完全ではありません。また、通常必要以上の機能が露出してしまいます。例を見てみましょう。

name = "Ruby Magic"
template = "Welcome to %d"

sprintf(template, name: name)
# => TypeError (can't convert Hash into Integer)

Kernel#sprintfString#% の両方は、データ型を扱う特殊な構文を許可します。それらがすべて渡されるデータと互換性があるわけではありません。この例では、テンプレートは数値のフォーマットを期待していますが、Hashが渡されるため TypeError が発生します。

しかし、私たちにはより強力な道具があります:正規表現を使った独自の展開実装です。正規表現を使えば、Mustache/Handlebars風のカスタム構文を定義できます。

name = "Ruby Magic"
template = "Welcome to {{name}}"
assigns = { "name" => name }

template.gsub(/{{(\w+)}}/) { assigns[$1] }
# => Welcome to Ruby Magic

String#gsub を使って、すべてのプレースホルダー(二重中括弧で囲まれた単語)を assigns ハッシュ内の対応する値で置換します。対応する値がない場合、プレースホルダーは何も挿入せずに削除されます。

このような文字列置換は、プレースホルダーがいくつかの文字列であれば有効な解決策です。しかし、少し複雑になるとすぐに問題に直面します。

例えば、テンプレートに条件分岐が必要だとします。変数の値によって結果を変えたい場合です。

Welcome to
{{name}}!

{{#if subscribed}}
  Thank you for subscribing to our mailing list.
{{else}}
  Please sign up for our mailing list to be notified about new articles!
{{/if}}

Your friends at
{{company_name}}

正規表現ではこのユースケースをスムーズに処理できません。頑張れば何とかハックできるかもしれませんが、この段階で本格的なテンプレート言語を構築するのが賢明です。

テンプレート言語の構築

テンプレート言語の実装は、他のプログラミング言語の実装と似ています。スクリプト言語と同様に、テンプレート言語にも3つのコンポーネントが必要です:レクサー(字句解析器)、パーサー(構文解析器)、インタープリター(実行器)。これらを順に見ていきましょう。

レクサー(字句解析器)

最初に取り組むタスクは「トークン化」、または「字句解析」と呼ばれます。このプロセスは、自然言語における品詞の識別に非常に似ています。

例えば Ruby is a lovely language という文があります。これは5つの異なるカテゴリの単語で構成されています。各単語のカテゴリを辞書で調べると、名詞動詞冠詞形容詞名詞 のようなリストになります。自然言語処理ではこれらを「品詞」と呼びます。プログラミング言語のような形式言語では、これらは トークン と呼ばれます。

レクサーは、テンプレートを読み取り、テキストストリームを各カテゴリの正規表現セットと照合します。最初に一致したものがトークンのカテゴリを定義し、関連データを付与します。

理論はこれくらいにして、テンプレート言語用のレクサーを実装しましょう。少し楽をするため、Ruby標準ライブラリの strscan を require して StringScanner を使います(以前の号で StringScanner の優れた入門記事があります)。まず、すべてを CONTENT として識別する最小版を作ります。

新しい StringScanner インスタンスを作り、スキャナーが文字列の末尾に達するまで続く until ループで処理させます。

現時点では、複数行にわたるすべての文字 (.*m 修飾子付き) にマッチさせ、すべてに対して1つの CONTENT トークンを返します。トークンは、最初の要素にトークン名、2番目の要素にデータを持つ配列で表現します。最も基本的なレクサーはこんな感じです:

require 'strscan'

module Magicbars
  class Lexer
    def self.tokenize(code)
      new.tokenize(code)
    end

    def tokenize(code)
      scanner = StringScanner.new(code)
      tokens = []

      until scanner.eos?
        tokens << [:CONTENT, scanner.scan(/.*?/m)]
      end

      tokens
    end
  end
end

このコードを Welcome to {{name}} で実行すると、すべてのコードが添付されたちょうど1つの CONTENT トークンのリストが返ってきます。

Magicbars::Lexer.tokenize("Welcome to {{name}}")
=> [["CONTENT", "Welcome to {{name}}"]]

次に、式(プレースホルダー)を検出するようにします。ループ内のコードを修正し、{{}} をそれぞれ OPEN_EXPRESSIONCLOSE としてマッチさせます。

条件分岐を追加して、さまざまなケースをチェックします。

until scanner.eos?
  if scanner.scan(/{{/)
    tokens << [:OPEN_EXPRESSION]
  elsif scanner.scan(/}}/)
    tokens << [:CLOSE]
  elsif scanner.scan(/.*?/m)
    tokens << [:CONTENT, scanner.matched]
  end
end

OPEN_EXPRESSIONCLOSE トークンに中括弧自体を付与するメリットはないので、それらは捨てます。scan 呼び出しが条件の一部になったため、scanner.matched を使って最後のマッチ結果を CONTENT トークンに付与します。

残念ながら、レクサーを再実行しても、以前と同じく1つの CONTENT トークンしか得られません。最後の式を、オープン式の直前までマッチするように修正する必要があります。肯定的先読みアンカーを使った scan_until で、二重中括弧の直前でスキャナーを止めます。ループ内のコードはこうなります:

until scanner.eos?
  if scanner.scan(/{{/)
    tokens << [:OPEN_EXPRESSION]
  elsif scanner.scan(/}}/)
    tokens << [:CLOSE]
  elsif scanner.scan_until(/.*?(?={{|}})/m)
    tokens << [:CONTENT, scanner.matched]
  end
end

再度実行すると、今度は4つのトークンが得られます:

Magicbars::Lexer.tokenize("Welcome to {{name}}")
=> [["CONTENT", "Welcome to "], ["OPEN_EXPRESSION"], ["CONTENT", "name"], ["CLOSE"]]

レクサーは望む結果にかなり近づきました。しかし、name は通常のコンテンツではなく、識別子(識別子)です! 二重中括弧内の文字列は、外部の文字列とは異なる扱いにする必要があります。

ステートマシンの導入

これを実現するため、レクサーを2つの明確な状態を持つステートマシンにします。初期状態は default です。OPEN_EXPRESSION トークンに遭遇すると expression 状態に移行し、CLOSE トークンに遭遇するまでそこに留まり、その後 default 状態に戻ります。

配列を使って現在の状態を管理するメソッドをいくつか追加して、ステートマシンを実装します。

def stack
  @stack ||= []
end

def state
  stack.last || :default
end

def push_state(state)
  stack.push(state)
end

def pop_state
  stack.pop
end

state メソッドは現在の状態、または default を返します。push_state は新しい状態をスタックに積んでレクサーをその状態に移行させます。pop_state は前の状態に戻します。

次に、ループ内の条件分岐を分割し、現在の状態をチェックする条件でラップします。default 状態では OPEN_EXPRESSIONCONTENT トークンを扱います。これにより、CONTENT の正規表現に }} の先読みが不要になるので、それを削除します。expression 状態では CLOSE トークンを扱い、IDENTIFIER 用の新しい正規表現を追加します。もちろん、OPEN_EXPRESSIONpush_stateCLOSEpop_state を呼ぶことで状態遷移も実装します。

if state == :default
  if scanner.scan(/{{/)
    tokens << [:OPEN_EXPRESSION]
    push_state :expression
  elsif scanner.scan_until(/.*?(?={{)/m)
    tokens << [:CONTENT, scanner.matched]
  end
elsif state == :expression
  if scanner.scan(/}}/)
    tokens << [:CLOSE]
    pop_state
  elsif scanner.scan(/[\w\-]+/)
    tokens << [:IDENTIFIER, scanner.matched]
  end
end

これらの変更により、レクサーは例を正しくトークン化できるようになりました。

Magicbars::Lexer.tokenize("Welcome to {{name}}")
# => [["CONTENT", "Welcome to "], ["OPEN_EXPRESSION"], ["IDENTIFIER", "name"], ["CLOSE"]]

さらに難易度を上げる

より高度な例に進みましょう。複数の式とブロックを使用する例です。

Welcome to {{name}}!

{{#if subscribed}}
  Thank you for subscribing to our mailing list.
{{else}}
  Please sign up for our mailing list to be notified about new articles!
{{/if}}

Your friends at {{company_name}}

この例をパースできないのは当然です。動作させるには、不足しているトークンを追加し、最後の式以降のコンテンツを処理できるようにする必要があります。ループ内のコードはこんな感じになります:

if state == :default
  if scanner.scan(/{{#/)
    tokens << [:OPEN_BLOCK]
    push_state :expression
  elsif scanner.scan(/{{//)
    tokens << [:OPEN_END_BLOCK]
    push_state :expression
  elsif scanner.scan(/{{else/)
    tokens << [:OPEN_INVERSE]
    push_state :expression
  elsif scanner.scan(/{{/)
    tokens << [:OPEN_EXPRESSION]
    push_state :expression
  elsif scanner.scan_until(/.*?(?={{)/m)
    tokens << [:CONTENT, scanner.matched]
  else
    tokens << [:CONTENT, scanner.rest]
    scanner.terminate
  end
elsif state == :expression
  if scanner.scan(/\s+/)
    # 空白は無視
  elsif scanner.scan(/}}/)
    tokens << [:CLOSE]
    pop_state
  elsif scanner.scan(/[\w\-]+/)
    tokens << [:IDENTIFIER, scanner.matched]
  else
    scanner.terminate
  end
end

条件の順序がある程度重要であることに注意してください。最初にマッチした正規表現が採用されるため、より具体的な式を汎用的なものより前に置く必要があります。ブロック用の専用オープントークンのコレクションがその好例です。

最終版のレクサーを使うと、例は以下のようにトークン化されます:

[
  ["CONTENT", "Welcome to "],
  ["OPEN_EXPRESSION"],
  ["IDENTIFIER", "name"],
  ["CLOSE"],
  ["CONTENT", "!\n\n"],
  ["OPEN_BLOCK"],
  ["IDENTIFIER", "if"],
  ["IDENTIFIER", "subscribed"],
  ["CLOSE"],
  ["CONTENT", "\n  Thank you for subscribing to our mailing list.\n"],
  ["OPEN_INVERSE"],
  ["CLOSE"],
  ["CONTENT", "\n  Please sign up for our mailing list to be notified about new articles!\n"],
  ["OPEN_END_BLOCK"],
  ["IDENTIFIER", "if"],
  ["CLOSE"],
  ["CONTENT", "\n\nYour friends at "],
  ["OPEN_EXPRESSION"],
  ["IDENTIFIER", "company_name"],
  ["CLOSE"],
  ["CONTENT", "\n"]
]

これで、7種類のトークンを識別できるようになりました:

トークン
OPEN_BLOCK {{#
OPEN_END_BLOCK {{/
OPEN_INVERSE {{else
OPEN_EXPRESSION {{
CONTENT 式の外側にあるもの(通常のHTMLやテキスト)
CLOSE }}
IDENTIFIER 単語文字、数字、_- で構成される識別子

次のステップは、トークンストリームの構造を解析し、抽象構文木(AST)に変換するパーサーの実装ですが、それはまた別の機会に。

今後の展望

文字列展開を使った基本的なテンプレートシステムの実装方法をいくつか見て、その限界にぶつかったところで、本格的なテンプレートシステムの実装を始めました。

今回は、テンプレートを解析してさまざまなトークンタイプを特定するレクサーを実装しました。次回の Ruby Magic では、パーサーとインタープリターを実装し、展開済み文字列を生成するところまで進めます。


  1. Rubyのメソッド入門:defキーワードで独自のメソッドを定義する方法

    Rubyにおける「メソッド」とは、特定の目的のためにひとまとめにされた、1行以上のRubyコードのことです。 このコードのまとまりには名前が付けられており、一度定義してしまえば、同じコードを何度も書いたりコピー&ペーストしたりすることなく、必要なときにいつでも再利用できます。 メソッドの主な役割 メソッドの目的は、大きく分けて以下のようなものがあります。 情報を取得する オブジェクトを変更・生成する データをフィルタリング・整形する 具体例 例1: Arrayオブジェクトに対するsizeメソッドは、配列内の要素数を返します(情報の取得)。 例2: popメソッドは、配列から最後の要素を取り

  2. Ruby 2.6の新機能9選|コード例でわかる注目ポイントを徹底解説

    Ruby 2.6には、開発者の生産性を高める新しい機能やパフォーマンス改善が多数盛り込まれています。 本記事では、Ruby 2.6で導入された9つの注目新機能を、実際のコード例とともにわかりやすく紹介します。最新のRuby動向をキャッチアップしたい方は、ぜひ最後までご覧ください。 1. 無限Range(Endless Range) Ruby 2.5以前でもFloat::INFINITYを使えば終端のない範囲を表現できましたが、Ruby 2.6ではさらに直感的な記法が使えるようになりました。 新しい無限Rangeは次のように書きます。 (1..) 通常のRangeが(1..10)のように終端