temple 0.10.4 → 0.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3eedd92f18dc086c241e1163c104e1929477068901992a5b56a36783ddbe2b71
4
- data.tar.gz: 87ad60cdfa2a9fbca8c2e499e3dd0ca4b54143cc4e04bbfc07f810bc085a2fdb
3
+ metadata.gz: c6c5b4a365b3b18720e53ab52c9b0983bdf97531b386a372abde0f632c895cc7
4
+ data.tar.gz: 53f567fbe6863cd80b3550d6bc22b7a10451996eda3bad21e640e96089d1915b
5
5
  SHA512:
6
- metadata.gz: 169be5abf462523cc50b21b8b1f5ad16e32dd24d8864a7a0d7c69599224d7f62f5ac939cb3da4db733b173653ac0ee227dfc2dbf8d5d236cc8b507af56dce8bd
7
- data.tar.gz: d34651062a8d38fbfeaa22a6f92df88b9cddaf821b7fecb506a4e7f9cbee9ce5fa06c42e5f8d1dee56e5987a2d390583a1188c7577ad90354737f49d4e435404
6
+ metadata.gz: 1451a467eda76333ffaf68a32be174982a6ae55d8e9eaacd0bb37502cf980a67e0fe734b3d68c914f9abe20ab5068f50723614902d5dee9db479d712e75f75ca
7
+ data.tar.gz: 03c5bbe5bb11d01255c5b27e012401ec71220c67488dbf5a8bd892debc832a0f69748a12696b42fa7e17558389857eba8b855eb28d9c96f2ff60ace80ed79285
@@ -0,0 +1,46 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ push:
13
+ if: github.repository_owner == 'judofyr'
14
+ runs-on: ubuntu-latest
15
+
16
+ environment:
17
+ name: rubygems.org
18
+ url: https://rubygems.org/gems/temple
19
+
20
+ permissions:
21
+ contents: write
22
+ id-token: write
23
+
24
+ steps:
25
+ - name: Harden Runner
26
+ uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
27
+ with:
28
+ egress-policy: audit
29
+
30
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
31
+
32
+ - name: Set up Ruby
33
+ uses: ruby/setup-ruby@eaecf785f6a34567a6d97f686bbb7bccc1ac1e5c # v1.237.0
34
+ with:
35
+ bundler-cache: true
36
+ ruby-version: ruby
37
+
38
+ - name: Publish to RubyGems
39
+ uses: rubygems/release-gem@a25424ba2ba8b387abc8ef40807c2c85b96cbe32 # v1.1.1
40
+
41
+ - name: Create GitHub release
42
+ run: |
43
+ tag_name="$(git describe --tags --abbrev=0)"
44
+ gh release create "${tag_name}" --verify-tag --generate-notes
45
+ env:
46
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -25,10 +25,11 @@ jobs:
25
25
  - '3.2'
26
26
  - '3.3'
27
27
  - '3.4'
28
+ - '4.0'
28
29
  - jruby
29
30
  - truffleruby-head
30
31
  steps:
31
- - uses: actions/checkout@v4
32
+ - uses: actions/checkout@v7
32
33
  - name: Set up Ruby
33
34
  uses: ruby/setup-ruby@v1
34
35
  with:
data/CHANGES CHANGED
@@ -1,3 +1,15 @@
1
+ 0.10.7
2
+
3
+ * Replace `=~` with `.match?` where possible (#160)
4
+
5
+ 0.10.6
6
+
7
+ * Make StaticAnalyzer recognize shorthand syntax as dynamic (#149)
8
+
9
+ 0.10.5
10
+
11
+ * Allow using prism as the parser engine (#156)
12
+
1
13
  0.10.4
2
14
 
3
15
  * Fix Ruby 3.4 compatibility (#152)
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Temple
2
2
  ======
3
3
 
4
- [![test](https://github.com/judofyr/temple/actions/workflows/test.yml/badge.svg)](https://github.com/judofyr/temple/actions/workflows/test.yml) [![Code Climate](https://codeclimate.com/github/judofyr/temple.svg)](https://codeclimate.com/github/judofyr/temple) [![Gem Version](https://badge.fury.io/rb/temple.svg)](https://rubygems.org/gems/temple) [![Yard Docs](https://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/gems/temple/frames)
4
+ [![test](https://github.com/judofyr/temple/actions/workflows/test.yml/badge.svg)](https://github.com/judofyr/temple/actions/workflows/test.yml) [![Maintainability](https://qlty.sh/gh/judofyr/projects/temple/maintainability.svg)](https://qlty.sh/gh/judofyr/projects/temple) [![Gem Version](https://badge.fury.io/rb/temple.svg)](https://rubygems.org/gems/temple) [![Yard Docs](https://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/gems/temple/frames)
5
5
 
6
6
  Temple is an abstraction and a framework for compiling templates to pure Ruby.
7
7
  It's all about making it easier to experiment, implement and optimize template
@@ -10,7 +10,7 @@ module Temple
10
10
  exps.each do |exp|
11
11
  if exp.first == :code
12
12
  if code
13
- code << '; ' unless code =~ /\n\Z/
13
+ code << '; ' unless /\n\Z/.match?(code)
14
14
  code << exp.last
15
15
  else
16
16
  code = exp.last.dup
@@ -6,7 +6,7 @@ module Temple
6
6
  # @api public
7
7
  class RemoveBOM < Parser
8
8
  def call(s)
9
- return s if s.encoding.name !~ /^UTF-(8|16|32)(BE|LE)?/
9
+ return s unless /^UTF-(8|16|32)(BE|LE)?/.match?(s.encoding.name)
10
10
  s.gsub(Regexp.new("\\A\uFEFF".encode(s.encoding.name)), ''.freeze)
11
11
  end
12
12
  end
@@ -4,7 +4,7 @@ module Temple
4
4
  # Convert [:dynamic, code] to [:static, text] if code is static Ruby expression.
5
5
  class StaticAnalyzer < Filter
6
6
  def call(exp)
7
- # Optimize only when Ripper is available.
7
+ # Optimize only when a parser engine is available.
8
8
  if ::Temple::StaticAnalyzer.available?
9
9
  super
10
10
  else
@@ -1,14 +1,70 @@
1
1
  # frozen_string_literal: true
2
- begin
3
- require 'ripper'
4
- rescue LoadError
5
- end
2
+ require 'temple/parser_engine'
6
3
 
7
4
  module Temple
8
5
  module Filters
9
6
  # Compile [:dynamic, "foo#{bar}"] to [:multi, [:static, 'foo'], [:dynamic, 'bar']]
10
7
  class StringSplitter < Filter
11
- if defined?(Ripper) && Ripper.respond_to?(:lex)
8
+ if PARSER_ENGINE == :prism
9
+ class << self
10
+ # `code` param must be valid string literal
11
+ def compile(code)
12
+ compiled = try_compile(code)
13
+ raise(FilterError, "Expected string literal") unless compiled
14
+ compiled
15
+ end
16
+
17
+ def try_compile(code)
18
+ result = Prism.parse(code)
19
+ return unless result.success?
20
+
21
+ stmts = result.value.statements.body
22
+ return if stmts.size != 1
23
+
24
+ stack = [stmts.first]
25
+ compiled = []
26
+
27
+ until stack.empty?
28
+ case (node = stack.pop).type
29
+ when :interpolated_string_node
30
+ stack.concat(node.parts.reverse)
31
+ when :string_node
32
+ if !(unescaped = node.unescaped).empty?
33
+ compiled << [:static, unescaped]
34
+ end
35
+ when :embedded_statements_node
36
+ if (stmts = node.statements) && !(slice = stmts.slice).empty?
37
+ compiled << [:dynamic, slice]
38
+ end
39
+ when :embedded_variable_node
40
+ compiled << [:dynamic, node.variable.slice]
41
+ else
42
+ return
43
+ end
44
+ end
45
+
46
+ compiled
47
+ end
48
+ end
49
+
50
+ def on_dynamic(code)
51
+ return [:dynamic, code] if code.include?("\n")
52
+
53
+ compiled = StringSplitter.try_compile(code)
54
+ return [:dynamic, code] unless compiled
55
+
56
+ temple = [:multi]
57
+ compiled.each do |type, content|
58
+ case type
59
+ when :static
60
+ temple << [:static, content]
61
+ when :dynamic
62
+ temple << on_dynamic(content)
63
+ end
64
+ end
65
+ temple
66
+ end
67
+ elsif PARSER_ENGINE == :ripper
12
68
  class << self
13
69
  # `code` param must be valid string literal
14
70
  def compile(code)
@@ -26,7 +26,7 @@ module Temple
26
26
 
27
27
  def on_static(content)
28
28
  return [:static, content] unless @pretty
29
- unless @pre_tags && @pre_tags =~ content
29
+ unless @pre_tags && @pre_tags.match?(content)
30
30
  content = content.sub(/\A\s*\n?/, "\n".freeze) if @indent_next
31
31
  content = content.gsub("\n".freeze, indent)
32
32
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Temple
4
+ temple_parser_engine = ENV['TEMPLE_PARSER_ENGINE']
5
+
6
+ prism_available =
7
+ if !temple_parser_engine || temple_parser_engine == 'prism'
8
+ begin
9
+ require 'prism'
10
+ true
11
+ rescue LoadError
12
+ end
13
+ end
14
+
15
+ ripper_available =
16
+ if !temple_parser_engine || temple_parser_engine == 'ripper'
17
+ begin
18
+ require 'ripper'
19
+ Ripper.respond_to?(:lex)
20
+ rescue LoadError
21
+ end
22
+ end
23
+
24
+ PARSER_ENGINE =
25
+ if prism_available
26
+ :prism
27
+ elsif ripper_available
28
+ :ripper
29
+ else
30
+ :none
31
+ end
32
+
33
+ private_constant :PARSER_ENGINE
34
+ end
@@ -1,76 +1,146 @@
1
1
  # frozen_string_literal: true
2
- begin
3
- require 'ripper'
4
- rescue LoadError
5
- end
2
+ require 'temple/parser_engine'
6
3
 
7
4
  module Temple
8
5
  module StaticAnalyzer
9
- STATIC_TOKENS = [
10
- :on_tstring_beg, :on_tstring_end, :on_tstring_content,
11
- :on_embexpr_beg, :on_embexpr_end,
12
- :on_lbracket, :on_rbracket,
13
- :on_qwords_beg, :on_words_sep, :on_qwords_sep,
14
- :on_lparen, :on_rparen,
15
- :on_lbrace, :on_rbrace, :on_label,
16
- :on_int, :on_float, :on_imaginary,
17
- :on_comma, :on_sp, :on_ignored_nl,
18
- ].freeze
19
-
20
- DYNAMIC_TOKENS = [
21
- :on_ident, :on_period,
22
- ].freeze
23
-
24
- STATIC_KEYWORDS = [
25
- 'true', 'false', 'nil',
26
- ].freeze
27
-
28
- STATIC_OPERATORS = [
29
- '=>',
30
- ].freeze
31
-
32
- class << self
33
- def available?
34
- defined?(Ripper) && Ripper.respond_to?(:lex)
35
- end
6
+ if PARSER_ENGINE == :prism
7
+ class << self
8
+ def available?
9
+ true
10
+ end
11
+
12
+ def static?(code)
13
+ return false if code.nil? || code.strip.empty?
14
+ (result = Prism.parse(code)).success? && static_node?(result.value)
15
+ end
16
+
17
+ def syntax_error?(code)
18
+ !Prism.parse_success?(code)
19
+ end
20
+
21
+ private
36
22
 
37
- def static?(code)
38
- return false if code.nil? || code.strip.empty?
39
- return false if syntax_error?(code)
40
-
41
- Ripper.lex(code).each do |_, token, str|
42
- case token
43
- when *STATIC_TOKENS
44
- # noop
45
- when :on_kw
46
- return false unless STATIC_KEYWORDS.include?(str)
47
- when :on_op
48
- return false unless STATIC_OPERATORS.include?(str)
49
- when *DYNAMIC_TOKENS
50
- return false
23
+ def static_node?(node)
24
+ case node.type
25
+ when :program_node
26
+ static_node?(node.statements)
27
+ when :parentheses_node
28
+ (stmts = node.body).is_a?(Prism::StatementsNode) &&
29
+ static_node?(stmts)
30
+ when :statements_node
31
+ node.body.size == 1 && static_node?(node.body.first)
32
+ when :interpolated_string_node
33
+ node.parts.all? { |part| static_node?(part) }
34
+ when :embedded_statements_node
35
+ (stmts = node.statements) && static_node?(stmts)
36
+ when :array_node
37
+ node.elements.all? { |elem| static_node?(elem) }
38
+ when :hash_node, :keyword_hash_node
39
+ node.elements.all? { |elem| static_node?(elem) }
40
+ when :assoc_node
41
+ static_node?(node.key) && static_node?(node.value)
42
+ when :string_node, :integer_node, :float_node, :imaginary_node,
43
+ :rational_node, :symbol_node, :true_node, :false_node, :nil_node
44
+ true
51
45
  else
52
- return false
46
+ false
53
47
  end
54
48
  end
55
- true
56
49
  end
50
+ elsif PARSER_ENGINE == :ripper
51
+ STATIC_TOKENS = [
52
+ :on_tstring_beg, :on_tstring_end, :on_tstring_content,
53
+ :on_embexpr_beg, :on_embexpr_end,
54
+ :on_lbracket, :on_rbracket,
55
+ :on_qwords_beg, :on_words_sep, :on_qwords_sep,
56
+ :on_lparen, :on_rparen,
57
+ :on_lbrace, :on_rbrace, :on_label,
58
+ :on_int, :on_float, :on_imaginary,
59
+ :on_comma, :on_sp, :on_ignored_nl,
60
+ ].freeze
57
61
 
58
- def syntax_error?(code)
59
- SyntaxChecker.new(code).parse
60
- false
61
- rescue SyntaxChecker::ParseError
62
- true
63
- end
64
- end
62
+ DYNAMIC_TOKENS = [
63
+ :on_ident, :on_period,
64
+ ].freeze
65
65
 
66
- if defined?(Ripper)
67
- class SyntaxChecker < Ripper
68
- class ParseError < StandardError; end
66
+ STATIC_KEYWORDS = [
67
+ 'true', 'false', 'nil',
68
+ ].freeze
69
69
 
70
- private
70
+ STATIC_OPERATORS = [
71
+ '=>',
72
+ ].freeze
73
+
74
+ class << self
75
+ def available?
76
+ true
77
+ end
71
78
 
72
- def on_parse_error(*)
73
- raise ParseError
79
+ def static?(code)
80
+ return false if code.nil? || code.strip.empty? || syntax_error?(code)
81
+
82
+ Ripper.lex(code).each do |_, token, str|
83
+ case token
84
+ when *STATIC_TOKENS
85
+ # noop
86
+ when :on_kw
87
+ return false unless STATIC_KEYWORDS.include?(str)
88
+ when :on_op
89
+ return false unless STATIC_OPERATORS.include?(str)
90
+ when *DYNAMIC_TOKENS
91
+ return false
92
+ else
93
+ return false
94
+ end
95
+ end
96
+
97
+ !ShorthandSyntaxChecker.shorthand?(code)
98
+ end
99
+
100
+ def syntax_error?(code)
101
+ SyntaxChecker.new(code).parse
102
+ false
103
+ rescue SyntaxChecker::ParseError
104
+ true
105
+ end
106
+
107
+ class SyntaxChecker < Ripper
108
+ class ParseError < StandardError; end
109
+
110
+ private
111
+
112
+ def on_parse_error(*)
113
+ raise ParseError
114
+ end
115
+ end
116
+
117
+ class ShorthandSyntaxChecker < Ripper
118
+ class << self
119
+ def shorthand?(code)
120
+ instance = new(code)
121
+ instance.parse
122
+ instance.shorthand
123
+ end
124
+ end
125
+
126
+ attr_reader :shorthand
127
+
128
+ def initialize(*)
129
+ super
130
+ @shorthand = nil
131
+ end
132
+
133
+ private
134
+
135
+ def on_assoc_new(key, value)
136
+ @shorthand = true if value.nil?
137
+ end
138
+ end
139
+ end
140
+ else
141
+ class << self
142
+ def available?
143
+ false
74
144
  end
75
145
  end
76
146
  end
data/lib/temple/utils.rb CHANGED
@@ -77,7 +77,7 @@ module Temple
77
77
  def indent_dynamic(text, indent_next, indent, pre_tags = nil)
78
78
  text = text.to_s
79
79
  safe = text.respond_to?(:html_safe?) && text.html_safe?
80
- return text if pre_tags && text =~ pre_tags
80
+ return text if pre_tags && pre_tags.match?(text)
81
81
 
82
82
  level = text.scan(/^\s*/).map(&:size).min
83
83
  text = text.gsub(/(?!\A)^\s{#{level}}/, '') if level > 0
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Temple
3
- VERSION = '0.10.4'
3
+ VERSION = '0.10.7'
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: temple
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.4
4
+ version: 0.10.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Magnus Holm
8
8
  - Daniel Mendler
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-24 00:00:00.000000000 Z
11
+ date: 2026-08-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tilt
@@ -73,6 +73,7 @@ executables: []
73
73
  extensions: []
74
74
  extra_rdoc_files: []
75
75
  files:
76
+ - ".github/workflows/release.yml"
76
77
  - ".github/workflows/test.yml"
77
78
  - ".gitignore"
78
79
  - ".yardopts"
@@ -126,6 +127,7 @@ files:
126
127
  - lib/temple/mixins/options.rb
127
128
  - lib/temple/mixins/template.rb
128
129
  - lib/temple/parser.rb
130
+ - lib/temple/parser_engine.rb
129
131
  - lib/temple/static_analyzer.rb
130
132
  - lib/temple/templates.rb
131
133
  - lib/temple/templates/rails.rb