Class: Gemite::TemplateParser

Inherits:
Object
  • Object
show all
Includes:
AST
Defined in:
lib/gemite/template_parser.rb

Overview

.tz ファイルの生テキストを走査し、以下のノード列(木)を組み立てる:

TplText  - そのまま出力する生HTML
TplExpr  - { expr } 補間。式の評価結果を文字列化して出力する
TplCode  - <?gemite ... ?> ブロック。中身は文として実行するだけで出力はしない
TplIf    - @if/@elseif/@else/@endif (HTML側)。分岐に応じて子ノードを出力する
TplFor   - @for x in xs ... @end (HTML側)。反復して子ノードを出力する
の中身、および @if/@for がコードブロック内で使われた場合は、

Parser(文パーサー)側に委譲する。@if/@endif, @for/@end はテンプレート層・ コード層の両方に現れうるが、defやbeginの素のendはコード層にしか現れない。

Constant Summary collapse

CODE_TAG_OPEN =

コードブロックの開始タグ。文字数に依存する処理はすべてこの定数の .length から動的に算出し、名称変更があってもズレないようにする。

"<?gemite"
CODE_TAG_CLOSE =
"?>"
DIRECTIVE_WITH_CLAUSE =

@if/@elseif/@for は条件式・イテラブル式(行の残り)を伴う。 @else/@endif/@end は節を取らないので、キーワード自体だけを消費する (でないと同じ行に続くHTMLやを誤って飲み込んでしまう)。 行頭(前後の空白のみ許容)に限定することで、HTML本文中の"@"(メールアドレス等)との衝突も避ける。

/\A[ \t]*@(if|elseif|for)\b[ \t]*(.*)\z/
DIRECTIVE_BARE =
/\A[ \t]*@(else|endif|end)\b[ \t]*/

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source) ⇒ TemplateParser

Returns a new instance of TemplateParser.



37
38
39
40
41
42
# File 'lib/gemite/template_parser.rb', line 37

def initialize(source)
  @src = source
  @pos = 0
  @len = source.length
  @line = 1
end

Class Method Details

.parse(source) ⇒ Object



33
34
35
# File 'lib/gemite/template_parser.rb', line 33

def self.parse(source)
  new(source).parse_nodes(nil)
end

Instance Method Details

#parse_nodes(stop_directives) ⇒ Object

stop_directives: nilならEOFまで。配列ならそのディレクティブ名(文字列)に 出会うまで(消費はしない。呼び出し元がexpectする)。



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/gemite/template_parser.rb', line 46

def parse_nodes(stop_directives)
  nodes = []
  text_buf = +""
  text_start_line = @line

  flush = lambda do
    unless text_buf.empty?
      nodes << TplText.new(text_buf)
      text_buf = +""
    end
  end

  until eof?
    # <?gemite ... ?> コードブロック
    if looking_at?(CODE_TAG_OPEN)
      flush.call
      nodes << parse_code_block
      text_start_line = @line
      next
    end

    # { expr } 補間 (失敗したら生テキストとしてフォールバック)
    if peek_char == "{"
      expr_node = try_parse_interpolation
      if expr_node
        flush.call
        nodes << expr_node
        text_start_line = @line
        next
      end
    end

    # 行頭ディレクティブ (@if/@elseif/@else/@endif/@for/@end)
    if at_line_start?
      directive, clause, consumed_len = peek_directive_at_current_line
      if directive
        if stop_directives && stop_directives.include?(directive)
          # ここでは消費しない。呼び出し元が consume_matched_directive で読み進める。
          flush.call
          return nodes
        end

        consume_chars(consumed_len)

        case directive
        when "if"
          flush.call
          nodes << parse_template_if(clause)
          text_start_line = @line
          next
        when "for"
          flush.call
          nodes << parse_template_for(clause)
          text_start_line = @line
          next
        when "elseif", "else", "endif", "end"
          # 対応する開始ディレクティブなしに出現した閉じ/中間ディレクティブ
          raise Gemite::SyntaxError.new("対応する開始タグのない '@#{directive}' です", line: @line)
        end
      end
    end

    ch = advance_char
    text_buf << ch
  end

  if stop_directives
    raise Gemite::SyntaxError.new(
      "'#{stop_directives.map { |d| "@#{d}" }.join("/")}' が見つからないまま終端しました",
      line: text_start_line
    )
  end

  flush.call
  nodes
end