Module: Vivlio::Starter::CLI::PreProcessCommands::DataRender::TemplateCompiler

Defined in:
lib/vivlio/starter/cli/pre_process/data_render/template_compiler.rb

Overview

テンプレートコンパイラモジュール

Constant Summary collapse

VARIABLE_PATTERN =

変数展開パターン: = key(行内に出現)

/(?<!=)=\s+([a-zA-Z_][a-zA-Z0-9_]*)/
IMAGE_VAR_PATTERN =

画像記法内の変数展開パターン: ![](key) / ![](= key) 名前付きキャプチャで gsub ブロック内の $N 上書き問題を回避

/!\[(?<alt>[^\]]*)\]\((?:=\s*)?(?<src>[^)]+)\)(?<attr>\{[^}]*\})?/
IMAGE_EXTENSIONS =

画像の拡張子(リテラル判定用)

%w[png jpg jpeg webp gif svg].freeze

Class Method Summary collapse

Class Method Details

.classify_lines(lines) ⇒ Array<Hash>

テンプレート行を分類する

Parameters:

  • lines (Array<String>)

    テンプレートの行リスト

Returns:

  • (Array<Hash>)

    分類済み行リスト



82
83
84
85
86
87
88
89
90
91
92
# File 'lib/vivlio/starter/cli/pre_process/data_render/template_compiler.rb', line 82

def classify_lines(lines)
  lines.map do |line|
    if line.strip.empty?
      { type: :blank }
    elsif contains_variable?(line)
      { type: :dynamic, content: line }
    else
      { type: :static, content: line }
    end
  end
end

.contains_variable?(line) ⇒ Boolean

行に変数参照(= key)が含まれるかを判定する

Parameters:

  • line (String)

    テンプレート行

Returns:

  • (Boolean)


97
98
99
100
101
102
# File 'lib/vivlio/starter/cli/pre_process/data_render/template_compiler.rb', line 97

def contains_variable?(line)
  return true if line.match?(VARIABLE_PATTERN)
  return true if line.match?(IMAGE_VAR_PATTERN) && image_has_variable?(line)

  false
end

.expand_images(line, record) ⇒ String?

画像記法内の変数を展開する

Parameters:

  • line (String)

    テンプレート行

  • record (Hash)

    データレコード

Returns:

  • (String, nil)

    展開後の行、またはスキップ時 nil



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/vivlio/starter/cli/pre_process/data_render/template_compiler.rb', line 145

def expand_images(line, record)
  result = line.dup
  skip = false

  result.gsub!(IMAGE_VAR_PATTERN) do |match|
    md   = Regexp.last_match
    alt  = md[:alt]
    src  = md[:src].sub(/\A=\s*/, '').strip
    attr = md[:attr] || ''

    if literal_image?(src)
      # 拡張子ありはリテラルとしてそのまま出力
      match
    else
      # 変数として展開
      value = record[src.to_sym]
      if value.nil? || value.to_s.strip.empty?
        skip = true
        match # gsub のブロックからは文字列を返す必要がある
      else
        "![#{alt}](#{value})#{attr}"
      end
    end
  end

  skip ? nil : result
end

.expand_line(line, record) ⇒ String?

テンプレート行をレコードデータで展開するnil/空文字のキーがあれば行ごとスキップ(nil を返す)

Parameters:

  • line (String)

    テンプレート行

  • record (Hash)

    データレコード

Returns:

  • (String, nil)

    展開後の行、またはスキップ時 nil



127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/vivlio/starter/cli/pre_process/data_render/template_compiler.rb', line 127

def expand_line(line, record)
  result = line.dup

  # 画像記法の展開(先に処理)
  result = expand_images(result, record)
  return nil unless result

  # = key パターンの展開
  result = expand_variables(result, record)
  return nil unless result

  result
end

.expand_variables(line, record) ⇒ String?

key パターンの変数を展開する

Parameters:

  • line (String)

    テンプレート行

  • record (Hash)

    データレコード

Returns:

  • (String, nil)

    展開後の行、またはスキップ時 nil



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/vivlio/starter/cli/pre_process/data_render/template_compiler.rb', line 177

def expand_variables(line, record)
  result = line.dup

  result.gsub!(VARIABLE_PATTERN) do |_match|
    key = ::Regexp.last_match(1).to_sym
    value = record[key]
    if value.nil? || value.to_s.strip.empty?
      return nil # 行ごとスキップ
    end

    value.to_s
  end

  result
end

.image_has_variable?(line) ⇒ Boolean

画像記法内に変数参照があるかを判定する

Parameters:

  • line (String)

    テンプレート行

Returns:

  • (Boolean)


107
108
109
110
111
112
# File 'lib/vivlio/starter/cli/pre_process/data_render/template_compiler.rb', line 107

def image_has_variable?(line)
  line.scan(IMAGE_VAR_PATTERN).any? do |(_, src, _)|
    src = src.sub(/\A=\s*/, '').strip
    !literal_image?(src)
  end
end

.literal_image?(src) ⇒ Boolean

画像パスがリテラル(拡張子あり)かを判定する

Parameters:

  • src (String)

    画像パス文字列

Returns:

  • (Boolean)


117
118
119
120
# File 'lib/vivlio/starter/cli/pre_process/data_render/template_compiler.rb', line 117

def literal_image?(src)
  ext = File.extname(src).delete_prefix('.').downcase
  IMAGE_EXTENSIONS.include?(ext)
end

.render(template, records, source_filename: nil, line_number: nil) ⇒ String

テンプレートにレコード群を流し込んで Markdown を生成する

Parameters:

  • template (String)

    テンプレートの内容

  • records (Array<Hash>)

    データレコード群

  • source_filename (String, nil) (defaults to: nil)

    エラー報告用ファイル名

  • line_number (Integer, nil) (defaults to: nil)

    エラー報告用行番号

Returns:

  • (String)

    展開後の Markdown



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
# File 'lib/vivlio/starter/cli/pre_process/data_render/template_compiler.rb', line 47

def render(template, records, source_filename: nil, line_number: nil)
  lines = template.lines
  validate_template_keys!(lines, records.first, source_filename:, line_number:) if records.any?

  # テンプレート行を「反復行」と「静的行」に分類する
  # = key を含む行は反復行、含まない行は静的行
  parts = classify_lines(lines)

  output = []
  records.each_with_index do |record, idx|
    # レコード間に空行を挿入(最初のレコード以外)
    output << "\n" if idx.positive?

    parts.each do |part|
      case part
      in { type: :static, content: }
        # 静的行は最初のレコードでのみ出力
        output << content if idx.zero?
      in { type: :dynamic, content: }
        # 動的行はレコードごとに展開
        expanded = expand_line(content, record)
        output << expanded if expanded
      in { type: :blank }
        # テンプレート内の空行は出力
        output << "\n"
      end
    end
  end

  output.join
end

.validate_template_keys!(lines, sample_record, source_filename: nil, line_number: nil) ⇒ Object

テンプレート内のキーがデータに存在するかを検証する

Parameters:

  • lines (Array<String>)

    テンプレートの行リスト

  • sample_record (Hash)

    サンプルレコード(最初の1件)

  • source_filename (String, nil) (defaults to: nil)

    エラー報告用ファイル名

  • line_number (Integer, nil) (defaults to: nil)

    エラー報告用行番号



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/vivlio/starter/cli/pre_process/data_render/template_compiler.rb', line 198

def validate_template_keys!(lines, sample_record, source_filename: nil, line_number: nil)
  return unless sample_record

  location = source_filename ? "#{source_filename}:#{line_number}" : ''
  available_keys = sample_record.keys

  lines.each do |line|
    # = key パターン
    line.scan(VARIABLE_PATTERN).each do |(key)|
      key_sym = key.to_sym
      next if available_keys.include?(key_sym)

      Common.log_error("テンプレートに存在しないキーが記述されています(#{location})")
      Common.log_error("  キー: #{key}")
      Common.log_error("  利用可能なキー: #{available_keys.join(', ')}")
      raise DataRenderError, "テンプレートに存在しないキーが記述されています: #{key}"
    end

    # 画像記法内の変数
    line.scan(IMAGE_VAR_PATTERN).each do |(_, src, _)|
      src = src.sub(/\A=\s*/, '').strip
      next if literal_image?(src)

      key_sym = src.to_sym
      next if available_keys.include?(key_sym)

      Common.log_error("テンプレートに存在しないキーが記述されています(#{location})")
      Common.log_error("  キー: #{src}")
      Common.log_error("  利用可能なキー: #{available_keys.join(', ')}")
      raise DataRenderError, "テンプレートに存在しないキーが記述されています: #{src}"
    end
  end
end