Module: QueryStream::TemplateCompiler
- Defined in:
- lib/query_stream/template_compiler.rb
Overview
テンプレートコンパイラモジュール
Constant Summary collapse
- VARIABLE_PATTERN =
変数展開パターン: = key または =key(行内に出現、ドット記法対応)(?<![=\w]) で width=40 や align=right 内の = を除外
/(?<![=\w])=\s*([a-zA-Z_][a-zA-Z0-9_.]*)/- IMAGE_VAR_PATTERN =
画像記法内の変数展開パターン:  /  名前付きキャプチャで gsub ブロック内の $N 上書き問題を回避
/!\[(?<alt>[^\]]*)\]\((?:=\s*)?(?<src>[^)]+)\)(?<attr>\{[^}]*\})?/- IMAGE_EXTENSIONS =
画像の拡張子(リテラル判定用)
%w[png jpg jpeg webp gif svg].freeze
- FENCE_OPEN_PATTERN =
VFM フェンス開始行: :::class-name 形式
/\A:::\s*\{\.[\w-].*\}\s*\z/- FENCE_CLOSE_PATTERN =
- VFM フェンス終了行: 単独の :
/\A:::\s*\z/
Class Method Summary collapse
-
.classify_lines(lines) ⇒ Array<Hash>
テンプレート行を分類する.
-
.contains_variable?(line) ⇒ Boolean
行に変数参照(= key)が含まれるかを判定する.
-
.expand_fence_range(parts, first_dyn, last_dyn) ⇒ Array(Integer, Integer)
動的行の前後にあるフェンス行を repeating 範囲に取り込む フェンス開始→(空行)→動的行 のパターンや 動的行→(空行)→フェンス終了 のパターンも考慮する.
-
.expand_images(line, record) ⇒ String?
画像記法内の変数を展開する.
-
.expand_line(line, record) ⇒ String?
テンプレート行をレコードデータで展開する nil/空文字のキーがあれば行ごとスキップ(nil を返す).
-
.expand_variables(line, record) ⇒ String?
key パターンの変数を展開する(ドット記法対応).
-
.fence_close?(line) ⇒ Boolean
VFM フェンス終了行かを判定する.
-
.fence_open?(line) ⇒ Boolean
VFM フェンス開始行かを判定する.
-
.image_has_variable?(line) ⇒ Boolean
画像記法内に変数参照があるかを判定する.
-
.literal_image?(src) ⇒ Boolean
画像パスがリテラル(拡張子あり)かを判定する.
-
.render(template, records, source_filename: nil, line_number: nil) ⇒ String
テンプレートにレコード群を流し込んでテキストを生成する.
-
.resolve_nested_value(record, key_path) ⇒ Object?
ドット記法のキーパスをたどってネストされた値を取得する.
-
.validate_template_keys!(lines, sample_record, source_filename: nil, line_number: nil) ⇒ Object
テンプレート内のキーがデータに存在するかを検証する.
Class Method Details
.classify_lines(lines) ⇒ Array<Hash>
テンプレート行を分類する
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/query_stream/template_compiler.rb', line 131 def classify_lines(lines) lines.map do |line| stripped = line.strip if stripped.empty? { type: :blank } elsif stripped.match?(FENCE_OPEN_PATTERN) { type: :fence_open, content: line } elsif stripped.match?(FENCE_CLOSE_PATTERN) { type: :fence_close, content: line } elsif contains_variable?(line) { type: :dynamic, content: line } else { type: :static, content: line } end end end |
.contains_variable?(line) ⇒ Boolean
行に変数参照(= key)が含まれるかを判定する
182 183 184 185 186 187 |
# File 'lib/query_stream/template_compiler.rb', line 182 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_fence_range(parts, first_dyn, last_dyn) ⇒ Array(Integer, Integer)
動的行の前後にあるフェンス行を repeating 範囲に取り込むフェンス開始→(空行)→動的行 のパターンや動的行→(空行)→フェンス終了 のパターンも考慮する
165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/query_stream/template_compiler.rb', line 165 def (parts, first_dyn, last_dyn) # 前方拡張: フェンス開始行を取り込む(間に空行があっても可) idx = first_dyn - 1 idx -= 1 if idx >= 0 && parts[idx][:type] == :blank first_dyn = idx if idx >= 0 && parts[idx][:type] == :fence_open # 後方拡張: フェンス終了行を取り込む(間に空行があっても可) idx = last_dyn + 1 idx += 1 if idx < parts.size && parts[idx][:type] == :blank last_dyn = idx if idx < parts.size && parts[idx][:type] == :fence_close [first_dyn, last_dyn] end |
.expand_images(line, record) ⇒ String?
画像記法内の変数を展開する
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/query_stream/template_compiler.rb', line 230 def (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 = resolve_nested_value(record, src) if value.nil? || value.to_s.strip.empty? skip = true match # gsub のブロックからは文字列を返す必要がある else "#{attr}" end end end skip ? nil : result end |
.expand_line(line, record) ⇒ String?
テンプレート行をレコードデータで展開するnil/空文字のキーがあれば行ごとスキップ(nil を返す)
212 213 214 215 216 217 218 219 220 221 222 223 224 |
# File 'lib/query_stream/template_compiler.rb', line 212 def (line, record) result = line.dup # 画像記法の展開(先に処理) result = (result, record) return nil unless result # = key パターンの展開 result = (result, record) return nil unless result result end |
.expand_variables(line, record) ⇒ String?
key パターンの変数を展開する(ドット記法対応)
262 263 264 265 266 267 268 269 270 271 272 273 274 275 |
# File 'lib/query_stream/template_compiler.rb', line 262 def (line, record) result = line.dup result.gsub!(VARIABLE_PATTERN) do |_match| key_path = $1 value = resolve_nested_value(record, key_path) if value.nil? || value.to_s.strip.empty? return nil # 行ごとスキップ end value.to_s end result end |
.fence_close?(line) ⇒ Boolean
VFM フェンス終了行かを判定する
156 |
# File 'lib/query_stream/template_compiler.rb', line 156 def fence_close?(line) = line.strip.match?(FENCE_CLOSE_PATTERN) |
.fence_open?(line) ⇒ Boolean
VFM フェンス開始行かを判定する
151 |
# File 'lib/query_stream/template_compiler.rb', line 151 def fence_open?(line) = line.strip.match?(FENCE_OPEN_PATTERN) |
.image_has_variable?(line) ⇒ Boolean
画像記法内に変数参照があるかを判定する
192 193 194 195 196 197 |
# File 'lib/query_stream/template_compiler.rb', line 192 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
画像パスがリテラル(拡張子あり)かを判定する
202 203 204 205 |
# File 'lib/query_stream/template_compiler.rb', line 202 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
テンプレートにレコード群を流し込んでテキストを生成する
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 122 123 124 125 126 |
# File 'lib/query_stream/template_compiler.rb', line 55 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? parts = classify_lines(lines) return '' if records.empty? # 最初と最後の動的行を特定し、leading / repeating / trailing に三分割する first_dyn = parts.index { it[:type] == :dynamic } # 動的行がないテンプレートは全行をそのまま一度だけ出力 unless first_dyn return parts.map { |p| p[:type] == :blank ? "\n" : p[:content] }.compact.join end last_dyn = parts.rindex { it[:type] == :dynamic } # --- Phase: フェンス行をrepeating範囲に取り込む --- # 動的行の直前にフェンス開始行がある場合、repeating 範囲を前方に拡張する # 動的行の直後にフェンス終了行がある場合、repeating 範囲を後方に拡張する # これにより :::{.book-card} 〜 ::: が各レコードごとに反復される first_dyn, last_dyn = (parts, first_dyn, last_dyn) leading = parts[0...first_dyn] repeating = parts[first_dyn..last_dyn] trailing = last_dyn + 1 < parts.size ? parts[(last_dyn + 1)..] : [] # テーブルテンプレート判定(区切り行 |---|…| の有無、またはデータ行が | で始まる) table_mode = leading.any? { it[:type] == :static && it[:content]&.match?(/^\|[-|:\s]+\|/) } || repeating.any? { it[:type] == :dynamic && it[:content]&.match?(/^\s*\|/) } output = [] # leading: 静的ヘッダーを一度だけ出力 leading.each do |part| case part in { type: :static, content: } then output << content in { type: :blank } then output << "\n" end end # repeating: レコードごとに動的行を展開 records.each_with_index do |record, idx| output << "\n" if idx > 0 && !table_mode repeating.each do |part| case part in { type: :dynamic, content: } = (content, record) output << if in { type: :fence_open, content: } then output << content in { type: :fence_close, content: } then output << content in { type: :static, content: } output << content in { type: :blank } output << "\n" unless table_mode end end end # trailing: 末尾の静的行を一度だけ出力 trailing.each do |part| case part in { type: :static, content: } then output << content in { type: :blank } then output << "\n" end end output.join end |
.resolve_nested_value(record, key_path) ⇒ Object?
ドット記法のキーパスをたどってネストされた値を取得する
281 282 283 284 285 286 287 288 289 |
# File 'lib/query_stream/template_compiler.rb', line 281 def resolve_nested_value(record, key_path) keys = key_path.split('.') value = record keys.each do |k| return nil unless value.is_a?(Hash) value = value[k.to_sym] || value[k.to_s] end value end |
.validate_template_keys!(lines, sample_record, source_filename: nil, line_number: nil) ⇒ Object
テンプレート内のキーがデータに存在するかを検証する
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 |
# File 'lib/query_stream/template_compiler.rb', line 296 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_path)| root_key = key_path.split('.').first.to_sym unless available_keys.include?(root_key) msg = "テンプレートに存在しないキーが記述されています: #{key_path}" QueryStream.logger.error("#{msg}(#{location})") QueryStream.logger.error(" 利用可能なキー: #{available_keys.join(', ')}") raise UnknownKeyError, msg end end # 画像記法内の変数 line.scan(IMAGE_VAR_PATTERN).each do |(_, src, _)| src = src.sub(/\A=\s*/, '').strip next if literal_image?(src) root_key = src.split('.').first.to_sym unless available_keys.include?(root_key) msg = "テンプレートに存在しないキーが記述されています: #{src}" QueryStream.logger.error("#{msg}(#{location})") QueryStream.logger.error(" 利用可能なキー: #{available_keys.join(', ')}") raise UnknownKeyError, msg end end end end |