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"
Regexp
テンプレートへのアプローチ
上記のソリューションは機能しますが、絶対確実ではなく、通常よりも多くの機能を公開します。例を見てみましょう:
name = "Ruby Magic"
template = "Welcome to %d"
sprintf(template, name: name)
# => TypeError (can't convert Hash into Integer)
両方のKernel#sprintf
およびString#%
特別な構文でさまざまなタイプのデータを処理できるようにします。それらのすべてが、渡すデータと互換性があるわけではありません。この例では、テンプレートは数値をフォーマットすることを想定していますが、ハッシュが渡され、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つのコンポーネントが必要です。これらを1つずつ見ていきます。
レクサー
私たちが取り組む必要のある最初のタスクは、トークン化、または字句解析と呼ばれます。このプロセスは、自然言語で単語カテゴリを識別するのと非常によく似ています。
Ruby is a lovely language
のような例を見てください 。文は、異なるカテゴリの5つの単語で構成されています。それらがどのカテゴリであるかを識別するには、辞書を使用してすべての単語のカテゴリを検索します。これにより、次のようなリストが作成されます。名詞 、動詞 、記事 、形容詞 、名詞 。自然言語処理では、これらを「品詞」と呼びます。プログラミング言語などの形式言語では、トークンと呼ばれます。 。
レクサーは、テンプレートを読み取り、テキストのストリームを特定の順序で各カテゴリの正規表現のセットと照合することで機能します。最初に一致するものは、トークンのカテゴリを定義し、それに関連するデータを添付します。
この少しの理論が邪魔にならないように、テンプレート言語用のレクサーを実装しましょう。少し簡単にするために、StringScanner
を使用します strscan
を要求する Rubyの標準ライブラリから。 (ちなみに、StringScanner
の優れたイントロがあります 以前のエディションの1つで。)最初のステップとして、すべてを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_EXPRESSION
として およびCLOSE
。
これを行うには、さまざまなケースをチェックする条件を追加します。
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_EXPRESSION
に中括弧を付けることに付加価値はありません およびCLOSE
トークンなので、ドロップします。 scan
として 呼び出しは条件の一部になりました。scanner.matched
を使用します 最後の一致の結果をCONTENT
に添付します トークン。
残念ながら、レクサーを再実行しても、CONTENT
は1つしか取得されません。 以前のようなトークン。開いている式まですべてを一致させるために、最後の式を変更する必要があります。これを行うには、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_EXPRESSION
を処理します およびCONTENT
トークン。これは、CONTENT
の正規表現も意味します }}
は必要ありません もう先読みなので、ドロップします。 expression
内 状態、CLOSE
を処理します トークンを作成し、IDENTIFIER
の新しい正規表現を追加します 。もちろん、push_state
を追加して状態遷移も実装します。 OPEN_EXPRESSION
の呼び出し およびpop_state
CLOSE
を呼び出す 。
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+/)
# Ignore whitespace
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 | 識別子は、単語の文字、数字、_ で構成されます 、および- |
次のステップは、トークンストリームの構造を理解し、それを抽象構文ツリーに変換しようとするパーサーを実装することですが、それはまた別の機会です。
今後の道
文字列補間を使用して基本的なテンプレートシステムを実装するさまざまな方法を検討することから、独自のテンプレート言語への旅を始めました。最初のアプローチの限界に達したとき、適切なテンプレートシステムの実装を開始しました。
今のところ、テンプレートを分析し、さまざまなタイプのトークンを把握するレクサーを実装しました。 Ruby Magicの次のエディションでは、補間された文字列を生成するためのパーサーとインタープリターを実装することで、旅を続けます。
-
Rubyの関数とメソッド:独自の関数を定義する方法
Rubyメソッドとは何ですか? メソッドは、特定の目的のためにグループ化された1行または複数行のRubyコードです。 このグループ化されたコードには名前が付けられているため、コードを再度記述したり、コピーして貼り付けたりすることなく、いつでも使用できます。 メソッドの目的は次のとおりです : 情報を取得します。 オブジェクトを変更または作成します。 フィルターとフォーマットのデータ。 例1 : サイズ Arrayのメソッド オブジェクトは要素の数を示します(情報を取得します)。 例2 : pop メソッドは、配列から最後の要素を削除します(オブジェクトを変更します)。
-
Ruby2.6の9つの新機能
Rubyの新しいバージョンには、新しい機能とパフォーマンスの改善が含まれています。 変更についていきますか? 見てみましょう! 無限の範囲 Ruby 2.5以前のバージョンは、すでに1つの形式の無限範囲をサポートしています( Float ::INFINITY を使用) )、しかしRuby2.6はこれを次のレベルに引き上げます。 新しい無限の範囲 次のようになります: (1..) これは、(1..10)のような終了値がないため、通常の範囲とは異なります。 。 使用例 : [a, b, c].zip(1..) # [[a, 1], [b, 2], [c, 3]] [1,2,3,