temple 0.10.4 → 0.10.5

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: 991a0b24405b8214645f3587f421886aa2f70d5e1dcb7a60e63174e8314bf0cd
4
+ data.tar.gz: 04ab064c8aa17d4baf2457cc6f97ae4f71ee801eeaa50ad0e2a1b4537e953bbc
5
5
  SHA512:
6
- metadata.gz: 169be5abf462523cc50b21b8b1f5ad16e32dd24d8864a7a0d7c69599224d7f62f5ac939cb3da4db733b173653ac0ee227dfc2dbf8d5d236cc8b507af56dce8bd
7
- data.tar.gz: d34651062a8d38fbfeaa22a6f92df88b9cddaf821b7fecb506a4e7f9cbee9ce5fa06c42e5f8d1dee56e5987a2d390583a1188c7577ad90354737f49d4e435404
6
+ metadata.gz: ba9e9141c61962b62b87247a77e70415145681ed35b590ba649781750098813597c40a334d45cfe7e3ae24a15cfe1d09ee32f6788c1aa600851432a8b6fe2c60
7
+ data.tar.gz: 88b97523630a72fa8d61d46a8e857cb3aaa26db025f0f919a52afef3b89fb372f89f3dafdfa4f2ce18a3c823003fad985f1a5d6516bbf8c63223f26a0159e302
@@ -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 }}
@@ -28,7 +28,7 @@ jobs:
28
28
  - jruby
29
29
  - truffleruby-head
30
30
  steps:
31
- - uses: actions/checkout@v4
31
+ - uses: actions/checkout@v6
32
32
  - name: Set up Ruby
33
33
  uses: ruby/setup-ruby@v1
34
34
  with:
data/CHANGES CHANGED
@@ -1,3 +1,7 @@
1
+ 0.10.5
2
+
3
+ * Allow using prism as the parser engine (#156)
4
+
1
5
  0.10.4
2
6
 
3
7
  * Fix Ruby 3.4 compatibility (#152)
@@ -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)
@@ -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,122 @@
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
6
+ if PARSER_ENGINE == :prism
7
+ class << self
8
+ def available?
9
+ true
10
+ end
19
11
 
20
- DYNAMIC_TOKENS = [
21
- :on_ident, :on_period,
22
- ].freeze
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
23
16
 
24
- STATIC_KEYWORDS = [
25
- 'true', 'false', 'nil',
26
- ].freeze
17
+ def syntax_error?(code)
18
+ !Prism.parse_success?(code)
19
+ end
27
20
 
28
- STATIC_OPERATORS = [
29
- '=>',
30
- ].freeze
21
+ private
31
22
 
32
- class << self
33
- def available?
34
- defined?(Ripper) && Ripper.respond_to?(:lex)
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
45
+ else
46
+ false
47
+ end
48
+ end
35
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
36
61
 
37
- def static?(code)
38
- return false if code.nil? || code.strip.empty?
39
- return false if syntax_error?(code)
62
+ DYNAMIC_TOKENS = [
63
+ :on_ident, :on_period,
64
+ ].freeze
40
65
 
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
51
- else
52
- return false
66
+ STATIC_KEYWORDS = [
67
+ 'true', 'false', 'nil',
68
+ ].freeze
69
+
70
+ STATIC_OPERATORS = [
71
+ '=>',
72
+ ].freeze
73
+
74
+ class << self
75
+ def available?
76
+ true
77
+ end
78
+
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
53
95
  end
96
+ true
54
97
  end
55
- true
56
- end
57
98
 
58
- def syntax_error?(code)
59
- SyntaxChecker.new(code).parse
60
- false
61
- rescue SyntaxChecker::ParseError
62
- true
63
- end
64
- end
99
+ def syntax_error?(code)
100
+ SyntaxChecker.new(code).parse
101
+ false
102
+ rescue SyntaxChecker::ParseError
103
+ true
104
+ end
65
105
 
66
- if defined?(Ripper)
67
- class SyntaxChecker < Ripper
68
- class ParseError < StandardError; end
106
+ class SyntaxChecker < Ripper
107
+ class ParseError < StandardError; end
69
108
 
70
- private
109
+ private
71
110
 
72
- def on_parse_error(*)
73
- raise ParseError
111
+ def on_parse_error(*)
112
+ raise ParseError
113
+ end
114
+ end
115
+ end
116
+ else
117
+ class << self
118
+ def available?
119
+ false
74
120
  end
75
121
  end
76
122
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Temple
3
- VERSION = '0.10.4'
3
+ VERSION = '0.10.5'
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.5
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-07 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