slimi 0.4.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 299f0ea50391c15aed1b99b4fb2cb1a8eae63a3ba97e2abc31cf676472a38cd8
4
- data.tar.gz: 6119285d8a753ab001bf29eff8df574dfab52636fc092d324db50ec7d094e7eb
3
+ metadata.gz: 12b1a25a767a9ca17da4d9d99cf520d857131cd9d28acaf73d18a7696354c694
4
+ data.tar.gz: fee58aeb65387fd4846e74ce6885bb96e97dca9db3f2f95578693e20752249a6
5
5
  SHA512:
6
- metadata.gz: 9269ec0c3c35efd6665a3f7aff882737479b72998b28cc067a80823885316a267958f3bf2a62107c7d56fd31bb12675f17b77053f7372c40fe768e3865775c13
7
- data.tar.gz: 7c03ac659b980eb512c87c0fa8a3bc2b991faac079f71426e67a3ede722455677b3647d7ceb8af286aae3a38730b4e42c40ac07a62662c2db7d34339060a6055
6
+ metadata.gz: d97004ce75686f701d0a27bb8ac200e56956d5556fe4dbc874e523870d4c0467ee6eba5d5a1d7e34fc6d792d6df17ec85df689a297e94be56d44dadd33902c66
7
+ data.tar.gz: d719cc21f5288051ae6e7be83cb1f85e82000dc0e7345160d0087f3d61d9aab9a17ce45978437ceefa6d47ce063034022170e2062fd5404ee4182f245edf0792
data/.rubocop.yml CHANGED
@@ -1,3 +1,6 @@
1
+ require:
2
+ - rubocop-rspec
3
+
1
4
  AllCops:
2
5
  NewCops: enable
3
6
  SuggestExtensions: false
@@ -12,5 +15,21 @@ Lint/InterpolationCheck:
12
15
  Metrics:
13
16
  Enabled: false
14
17
 
18
+ RSpec/AnyInstance:
19
+ Enabled: false
20
+
21
+ RSpec/ImplicitSubject:
22
+ Enabled: false
23
+
24
+ RSpec/MultipleExpectations:
25
+ Enabled: false
26
+
27
+ RSpec/NamedSubject:
28
+ Enabled: false
29
+
30
+ Security/Eval:
31
+ Exclude:
32
+ - 'spec/**/*.rb'
33
+
15
34
  Style/Documentation:
16
35
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.0 - 2022-01-03
6
+
7
+ ### Added
8
+
9
+ - Support annotate_rendered_view_with_filenames.
10
+
11
+ ### Changed
12
+
13
+ - Rename expression name from slim to slimi.
14
+
15
+ ### Fixed
16
+
17
+ - Fix bug at registering handler to ActionView.
18
+ - Fix Engine options at RailsTemplateHandler.
19
+ - Define missing :generator option at Engine.
20
+
21
+ ## 0.5.1 - 2022-01-02
22
+
23
+ ### Changed
24
+
25
+ - Wrap slim attrvalue by slimi position expression.
26
+
27
+ ## 0.5.0 - 2022-01-02
28
+
29
+ ### Added
30
+
31
+ - Add Slimi::Engine.
32
+
33
+ ## 0.4.2 - 2021-12-27
34
+
35
+ ### Fixed
36
+
37
+ - Fix bug on quoted attribute parser.
38
+ - Remove trailing line-ending from source line of syntax error message.
39
+ - Support multi-line attributes.
40
+
5
41
  ## 0.4.1 - 2021-12-26
6
42
 
7
43
  ### Fixed
@@ -24,7 +60,7 @@
24
60
 
25
61
  ### Added
26
62
 
27
- - Support unquoted attributes.
63
+ - Support Ruby attributes.
28
64
 
29
65
  ### Fixed
30
66
 
data/Gemfile CHANGED
@@ -5,7 +5,8 @@ source 'https://rubygems.org'
5
5
  # Specify your gem's dependencies in slimi.gemspec
6
6
  gemspec
7
7
 
8
+ gem 'actionview'
8
9
  gem 'rake', '~> 13.0'
9
- gem 'rspec', '~> 3.0'
10
- gem 'rubocop', '~> 1.21'
11
- gem 'slim'
10
+ gem 'rspec'
11
+ gem 'rubocop'
12
+ gem 'rubocop-rspec'
data/Gemfile.lock CHANGED
@@ -1,17 +1,47 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- slimi (0.4.1)
4
+ slimi (0.6.0)
5
5
  temple
6
+ tilt
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
9
10
  specs:
11
+ actionview (7.0.0)
12
+ activesupport (= 7.0.0)
13
+ builder (~> 3.1)
14
+ erubi (~> 1.4)
15
+ rails-dom-testing (~> 2.0)
16
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
17
+ activesupport (7.0.0)
18
+ concurrent-ruby (~> 1.0, >= 1.0.2)
19
+ i18n (>= 1.6, < 2)
20
+ minitest (>= 5.1)
21
+ tzinfo (~> 2.0)
10
22
  ast (2.4.2)
23
+ builder (3.2.4)
24
+ concurrent-ruby (1.1.9)
25
+ crass (1.0.6)
11
26
  diff-lcs (1.4.4)
27
+ erubi (1.10.0)
28
+ i18n (1.8.11)
29
+ concurrent-ruby (~> 1.0)
30
+ loofah (2.13.0)
31
+ crass (~> 1.0.2)
32
+ nokogiri (>= 1.5.9)
33
+ minitest (5.15.0)
34
+ nokogiri (1.12.5-x86_64-linux)
35
+ racc (~> 1.4)
12
36
  parallel (1.21.0)
13
37
  parser (3.0.3.2)
14
38
  ast (~> 2.4.1)
39
+ racc (1.6.0)
40
+ rails-dom-testing (2.0.3)
41
+ activesupport (>= 4.2.0)
42
+ nokogiri (>= 1.6)
43
+ rails-html-sanitizer (1.4.2)
44
+ loofah (~> 2.3)
15
45
  rainbow (3.0.0)
16
46
  rake (13.0.6)
17
47
  regexp_parser (2.2.0)
@@ -40,22 +70,24 @@ GEM
40
70
  unicode-display_width (>= 1.4.0, < 3.0)
41
71
  rubocop-ast (1.15.0)
42
72
  parser (>= 3.0.1.1)
73
+ rubocop-rspec (2.6.0)
74
+ rubocop (~> 1.19)
43
75
  ruby-progressbar (1.11.0)
44
- slim (4.1.0)
45
- temple (>= 0.7.6, < 0.9)
46
- tilt (>= 2.0.6, < 2.1)
47
76
  temple (0.8.2)
48
77
  tilt (2.0.10)
78
+ tzinfo (2.0.4)
79
+ concurrent-ruby (~> 1.0)
49
80
  unicode-display_width (2.1.0)
50
81
 
51
82
  PLATFORMS
52
83
  x86_64-linux
53
84
 
54
85
  DEPENDENCIES
86
+ actionview
55
87
  rake (~> 13.0)
56
- rspec (~> 3.0)
57
- rubocop (~> 1.21)
58
- slim
88
+ rspec
89
+ rubocop
90
+ rubocop-rspec
59
91
  slimi!
60
92
 
61
93
  BUNDLED WITH
data/README.md CHANGED
@@ -5,28 +5,56 @@
5
5
 
6
6
  Yet another implementation for [Slim](https://github.com/slim-template/slim) template language.
7
7
 
8
- Slimi is used by [Slimcop](https://github.com/r7kamura/slimcop).
8
+ ## Introduction
9
9
 
10
- ## Installation
10
+ Slimi provides almost the same functionality as Slim, with a few additional useful features,
11
+ such as generating AST with detailed location information about embedded Ruby codes.
11
12
 
12
- Add this line to your application's Gemfile:
13
+ Originally, Slimi was developed for [Slimcop](https://github.com/r7kamura/slimcop), a RuboCop runner for Slim template.
14
+ It uses Slimi to apply `rubocop --auto-correct` to embedded Ruby codes in Slim template.
13
15
 
14
- ```ruby
15
- gem 'slimi'
16
- ```
17
-
18
- And then execute:
19
-
20
- ```
21
- bundle install
22
- ```
16
+ ## Usage
23
17
 
24
- Or install it yourself as:
18
+ Just replace `gem 'slim'` with `gem 'slimi'` in your application's Gemfile.
25
19
 
20
+ ```ruby
21
+ gem 'slimi'
26
22
  ```
27
- gem install slimi
28
- ```
29
-
30
- ## Usage
31
23
 
32
- TBD.
24
+ ## Compatibility
25
+
26
+ - Line indicators
27
+ - [x] Vebatim text
28
+ - [x] Inline HTML
29
+ - [x] Control
30
+ - [x] Output
31
+ - [x] HTML comment
32
+ - [x] Code comment
33
+ - [x] IE conditional comment
34
+ - Tags
35
+ - [x] Doctype declaration
36
+ - [x] Closed tags
37
+ - [x] Trailing and leading white space
38
+ - [x] Inline tags
39
+ - [x] Text content
40
+ - [x] Dynamic content
41
+ - [x] Tag shortcuts
42
+ - [ ] Dynamic tags
43
+ - Attributes
44
+ - [x] Attributes wrapper
45
+ - [x] Quoted attributes
46
+ - [x] Ruby attributes
47
+ - [x] Boolean attributes
48
+ - [x] Attribute merging
49
+ - [x] Attribute shortcuts
50
+ - [ ] Splat attributes
51
+ - Plugins
52
+ - [ ] Include partials
53
+ - [ ] Translator/I18n
54
+ - [ ] Logic-less mode
55
+ - [ ] Smart text mode
56
+ - Etc.
57
+ - [ ] CLI tools
58
+ - Slimi-only features
59
+ - [x] Embedded Ruby code location
60
+ - [x] annotate_rendered_view_with_filenames
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temple'
4
+
5
+ module Slimi
6
+ # Convert Slim code into Ruby code.
7
+ class Engine < ::Temple::Engine
8
+ define_options(
9
+ attr_quote: '"',
10
+ default_tag: 'div',
11
+ format: :xhtml,
12
+ generator: ::Temple::Generators::StringBuffer,
13
+ merge_attrs: { 'class' => ' ' },
14
+ pretty: false,
15
+ sort_attrs: true
16
+ )
17
+
18
+ use Parser
19
+ use Filters::Unposition
20
+ use Filters::Embedded
21
+ use Filters::Interpolation
22
+ use Filters::DoInserter
23
+ use Filters::EndInserter
24
+ use Filters::Control
25
+ use Filters::Output
26
+ use Filters::Text
27
+ html :AttributeSorter
28
+ html :AttributeMerger
29
+ use Filters::Attribute
30
+ use(:AttributeRemover) { ::Temple::HTML::AttributeRemover.new(remove_empty_attrs: options[:merge_attrs].keys) }
31
+ html :Pretty
32
+ use Filters::Amble
33
+ filter :Escapable
34
+ filter :ControlFlow
35
+ filter :MultiFlattener
36
+ filter :StaticMerger
37
+ generator :StringBuffer
38
+ end
39
+ end
data/lib/slimi/errors.rb CHANGED
@@ -23,7 +23,7 @@ module Slimi
23
23
  def to_s
24
24
  <<~TEXT
25
25
  #{error_type} at #{@file_path}:#{@line_number}:#{@column}
26
- #{@line}
26
+ #{@line.rstrip}
27
27
  #{' ' * (@column - 1)}^
28
28
  TEXT
29
29
  end
@@ -36,6 +36,9 @@ module Slimi
36
36
  end
37
37
  end
38
38
 
39
+ class AttributeClosingDelimiterNotFoundError < SlimSyntaxError
40
+ end
41
+
39
42
  class InvalidEmptyAttributeError < SlimSyntaxError
40
43
  end
41
44
 
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ module Filters
5
+ # Support Rails annotate_rendered_view_with_filenames feature.
6
+ class Amble < Base
7
+ define_options(
8
+ :postamble,
9
+ :preamble
10
+ )
11
+
12
+ # @param [Array] expression
13
+ # @return [Array]
14
+ def call(expression)
15
+ result = %i[multi]
16
+ result << [:static, options[:preamble]] if options[:preamble]
17
+ result << expression
18
+ result << [:static, options[:postamble]] if options[:postamble]
19
+ result
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ module Filters
5
+ # Handle `[:slimi, :attributes, ...]`.
6
+ class Attribute < Base
7
+ define_options :merge_attrs
8
+
9
+ # @param [Array<Array>] expressions
10
+ # @return [Array]
11
+ def on_html_attrs(*expressions)
12
+ [:multi, *expressions.map { |expression| compile(expression) }]
13
+ end
14
+
15
+ # @param [String] name
16
+ # @param [Array] value
17
+ # @return [Array]
18
+ def on_html_attr(name, value)
19
+ if value[0] == :slimi && value[1] == :attrvalue && !options[:merge_attrs][name]
20
+ escape = value[2]
21
+ code = value[3]
22
+ case code
23
+ when 'true'
24
+ [:html, :attr, name, [:multi]]
25
+ when 'false', 'nil'
26
+ [:multi]
27
+ else
28
+ tmp = unique_name
29
+ [:multi,
30
+ [:code, "#{tmp} = #{code}"],
31
+ [:if, tmp,
32
+ [:if, "#{tmp} == true",
33
+ [:html, :attr, name, [:multi]],
34
+ [:html, :attr, name, [:escape, escape, [:dynamic, tmp]]]]]]
35
+ end
36
+ else
37
+ @attr = name
38
+ super
39
+ end
40
+ end
41
+
42
+ # @param [Boolean] escape
43
+ # @param [String] code\
44
+ # @return [Array]\
45
+ def on_slimi_attrvalue(escape, code)
46
+ if (delimiter = options[:merge_attrs][@attr])
47
+ tmp = unique_name
48
+ [:multi,
49
+ [:code, "#{tmp} = #{code}"],
50
+ [:if, "Array === #{tmp}",
51
+ [:multi,
52
+ [:code, "#{tmp} = #{tmp}.flatten"],
53
+ [:code, "#{tmp}.map!(&:to_s)"],
54
+ [:code, "#{tmp}.reject!(&:empty?)"],
55
+ [:escape, escape, [:dynamic, "#{tmp}.join(#{delimiter.inspect})"]]],
56
+ [:escape, escape, [:dynamic, tmp]]]]
57
+ else
58
+ [:escape, escape, [:dynamic, code]]
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temple'
4
+
5
+ module Slimi
6
+ module Filters
7
+ # Pass-through some expressions which are unknown for Temple.
8
+ class Base < ::Temple::HTML::Filter
9
+ # @param [String] code
10
+ # @param [Array] expression
11
+ # @return [Array]
12
+ def on_slimi_control(code, expression)
13
+ [:slimi, :control, code, compile(expression)]
14
+ end
15
+
16
+ # @param [String] type
17
+ # @param [String] code
18
+ # @param [Array] expression
19
+ # @param [Array] attributes
20
+ # @return [Array]
21
+ def on_slimi_embedded(type, expression, attributes)
22
+ [:slimi, :embedded, type, compile(expression), attributes]
23
+ end
24
+
25
+ # @param [Boolean] escape
26
+ # @param [String] code
27
+ # @param [Array] expression
28
+ # @return [Array]
29
+ def on_slimi_output(escape, code, expression)
30
+ [:slimi, :output, escape, code, compile(expression)]
31
+ end
32
+
33
+ # @param [String] type
34
+ # @param [Array] expression
35
+ # @return [Array]
36
+ def on_slimi_text(type, expression)
37
+ [:slimi, :text, type, compile(expression)]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ module Filters
5
+ # Handle `[:slimi, :control, code, multi]`.
6
+ class Control < Base
7
+ # @param [String] code
8
+ # @param [Array] multi
9
+ # @return [Array]
10
+ def on_slimi_control(code, multi)
11
+ [
12
+ :multi,
13
+ [:code, code],
14
+ compile(multi)
15
+ ]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ module Filters
5
+ # Append missing `do` to embedded Ruby code.
6
+ class DoInserter < Base
7
+ VALID_RUBY_LINE_REGEXP = /(\A(if|unless|else|elsif|when|begin|rescue|ensure|case)\b)|\bdo\s*(\|[^|]*\|\s*)?\Z/.freeze
8
+
9
+ # @param [String] code
10
+ # @param [Array] expressio
11
+ # @return [Array]
12
+ def on_slimi_control(code, expression)
13
+ code += ' do' unless code.match?(VALID_RUBY_LINE_REGEXP) || empty_exp?(expression)
14
+ [:slimi, :control, code, compile(expression)]
15
+ end
16
+
17
+ # @param [Boolean] escape
18
+ # @param [String] code
19
+ # @param [Array] expression
20
+ # @return [Array]
21
+ def on_slimi_output(escape, code, expression)
22
+ code += ' do' unless code.match?(VALID_RUBY_LINE_REGEXP) || empty_exp?(expression)
23
+ [:slimi, :output, escape, code, compile(expression)]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ module Filters
5
+ # @api private
6
+ class TextCollector < Base
7
+ def call(exp)
8
+ @collected = ''
9
+ super(exp)
10
+ @collected
11
+ end
12
+
13
+ def on_slimi_interpolate(text)
14
+ @collected << text
15
+ nil
16
+ end
17
+ end
18
+
19
+ # @api private
20
+ class NewlineCollector < Base
21
+ def call(exp)
22
+ @collected = [:multi]
23
+ super(exp)
24
+ @collected
25
+ end
26
+
27
+ def on_newline
28
+ @collected << [:newline]
29
+ nil
30
+ end
31
+ end
32
+
33
+ # @api private
34
+ class OutputProtector < Base
35
+ def call(exp)
36
+ @protect = []
37
+ @collected = ''
38
+ @tag = "%#{object_id.abs.to_s(36)}%"
39
+ super(exp)
40
+ @collected
41
+ end
42
+
43
+ def on_static(text)
44
+ @collected << text
45
+ nil
46
+ end
47
+
48
+ def on_slimi_output(escape, text, content)
49
+ @collected << @tag
50
+ @protect << [:slimi, :output, escape, text, content]
51
+ nil
52
+ end
53
+
54
+ def unprotect(text)
55
+ block = [:multi]
56
+ while text =~ /#{@tag}/
57
+ block << [:static, Regexp.last_match.pre_match]
58
+ block << @protect.shift
59
+ text = Regexp.last_match.post_match
60
+ end
61
+ block << [:static, text]
62
+ end
63
+ end
64
+
65
+ # Temple filter which processes embedded engines
66
+ # @api private
67
+ class Embedded < Base
68
+ @engines = {}
69
+
70
+ class << self
71
+ attr_reader :engines
72
+
73
+ # Register embedded engine
74
+ #
75
+ # @param [String] name Name of the engine
76
+ # @param [Class] klass Engine class
77
+ # @param option_filter List of options to pass to engine.
78
+ # Last argument can be default option hash.
79
+ def register(name, klass, *option_filter)
80
+ name = name.to_sym
81
+ local_options = option_filter.last.respond_to?(:to_hash) ? option_filter.pop.to_hash : {}
82
+ define_options(name, *option_filter)
83
+ klass.define_options(name)
84
+ engines[name.to_sym] = proc do |options|
85
+ klass.new({}.update(options).delete_if { |k, _v| !option_filter.include?(k) && k != name }.update(local_options))
86
+ end
87
+ end
88
+
89
+ def create(name, options)
90
+ constructor = engines[name] || raise(Temple::FilterError, "Embedded engine #{name} not found")
91
+ constructor.call(options)
92
+ end
93
+ end
94
+
95
+ define_options :enable_engines, :disable_engines
96
+
97
+ def initialize(opts = {})
98
+ super
99
+ @engines = {}
100
+ @enabled = normalize_engine_list(options[:enable_engines])
101
+ @disabled = normalize_engine_list(options[:disable_engines])
102
+ end
103
+
104
+ def on_slimi_embedded(name, body, attrs)
105
+ name = name.to_sym
106
+ raise(Temple::FilterError, "Embedded engine #{name} is disabled") unless enabled?(name)
107
+
108
+ @engines[name] ||= self.class.create(name, options)
109
+ @engines[name].on_slimi_embedded(name, body, attrs)
110
+ end
111
+
112
+ def enabled?(name)
113
+ (!@enabled || @enabled.include?(name)) &&
114
+ (!@disabled || !@disabled.include?(name))
115
+ end
116
+
117
+ protected
118
+
119
+ def normalize_engine_list(list)
120
+ raise(ArgumentError, 'Option :enable_engines/:disable_engines must be String or Symbol list') unless !list || list.is_a?(Array)
121
+
122
+ list&.map(&:to_sym)
123
+ end
124
+
125
+ class Engine < Base
126
+ protected
127
+
128
+ def collect_text(body)
129
+ @text_collector ||= TextCollector.new
130
+ @text_collector.call(body)
131
+ end
132
+
133
+ def collect_newlines(body)
134
+ @newline_collector ||= NewlineCollector.new
135
+ @newline_collector.call(body)
136
+ end
137
+ end
138
+
139
+ # Basic tilt engine
140
+ class TiltEngine < Engine
141
+ def on_slimi_embedded(engine, body, _attrs)
142
+ tilt_engine = Tilt[engine] || raise(Temple::FilterError, "Tilt engine #{engine} is not available.")
143
+ tilt_options = options[engine.to_sym] || {}
144
+ tilt_options[:default_encoding] ||= 'utf-8'
145
+ [:multi, tilt_render(tilt_engine, tilt_options, collect_text(body)), collect_newlines(body)]
146
+ end
147
+
148
+ protected
149
+
150
+ def tilt_render(tilt_engine, tilt_options, text)
151
+ [:static, tilt_engine.new(tilt_options) { text }.render]
152
+ end
153
+ end
154
+
155
+ # Sass engine which supports :pretty option
156
+ class SassEngine < TiltEngine
157
+ define_options :pretty
158
+
159
+ protected
160
+
161
+ def tilt_render(tilt_engine, tilt_options, text)
162
+ text = tilt_engine.new(tilt_options.merge(
163
+ style: options[:pretty] ? :expanded : :compressed,
164
+ cache: false
165
+ )) { text }.render
166
+ text = text.chomp
167
+ [:static, text]
168
+ end
169
+ end
170
+
171
+ # Static template with interpolated ruby code
172
+ class InterpolateTiltEngine < TiltEngine
173
+ def collect_text(body)
174
+ output_protector.call(interpolation.call(body))
175
+ end
176
+
177
+ def tilt_render(tilt_engine, tilt_options, text)
178
+ output_protector.unprotect(tilt_engine.new(tilt_options) { text }.render)
179
+ end
180
+
181
+ private
182
+
183
+ def interpolation
184
+ @interpolation ||= Interpolation.new
185
+ end
186
+
187
+ def output_protector
188
+ @output_protector ||= OutputProtector.new
189
+ end
190
+ end
191
+
192
+ # Tag wrapper engine
193
+ # Generates a html tag and wraps another engine (specified via :engine option)
194
+ class TagEngine < Engine
195
+ disable_option_validator!
196
+
197
+ def on_slimi_embedded(engine, body, attrs)
198
+ unless options[:attributes].empty?
199
+ options[:attributes].map do |k, v|
200
+ attrs << [:html, :attr, k, [:static, v]]
201
+ end
202
+ end
203
+
204
+ if options[:engine]
205
+ opts = {}.update(options)
206
+ opts.delete(:engine)
207
+ opts.delete(:tag)
208
+ opts.delete(:attributes)
209
+ @engine ||= options[:engine].new(opts)
210
+ body = @engine.on_slimi_embedded(engine, body, attrs)
211
+ end
212
+
213
+ [:html, :tag, options[:tag], attrs, body]
214
+ end
215
+ end
216
+
217
+ # Javascript wrapper engine.
218
+ # Like TagEngine, but can wrap content in html comment or cdata.
219
+ class JavaScriptEngine < TagEngine
220
+ disable_option_validator!
221
+
222
+ set_options tag: :script, attributes: {}
223
+
224
+ def on_slimi_embedded(engine, body, attrs)
225
+ super(engine, [:html, :js, body], attrs)
226
+ end
227
+ end
228
+
229
+ # Embeds ruby code
230
+ class RubyEngine < Engine
231
+ def on_slimi_embedded(_engine, body, _attrs)
232
+ [:multi, [:newline], [:code, "#{collect_text(body)}\n"]]
233
+ end
234
+ end
235
+
236
+ # These engines are executed at compile time, embedded ruby is interpolated
237
+ register :markdown, InterpolateTiltEngine
238
+ register :textile, InterpolateTiltEngine
239
+ register :rdoc, InterpolateTiltEngine
240
+
241
+ # These engines are executed at compile time
242
+ register :coffee, JavaScriptEngine, engine: TiltEngine
243
+ register :less, TagEngine, tag: :style, attributes: { type: 'text/css' }, engine: TiltEngine
244
+ register :sass, TagEngine, :pretty, tag: :style, attributes: { type: 'text/css' }, engine: SassEngine
245
+ register :scss, TagEngine, :pretty, tag: :style, attributes: { type: 'text/css' }, engine: SassEngine
246
+
247
+ # Embedded javascript/css
248
+ register :javascript, JavaScriptEngine
249
+ register :css, TagEngine, tag: :style, attributes: { type: 'text/css' }
250
+
251
+ # Embedded ruby code
252
+ register :ruby, RubyEngine
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ module Filters
5
+ # Append missing `end` line to embedded Ruby code in control block.
6
+ class EndInserter < Base
7
+ # @param [Array<Array>] expressions
8
+ def on_multi(*expressions)
9
+ result = [:multi]
10
+ prev_indent = false
11
+ expressions.each do |source|
12
+ expression = Expression.new(source)
13
+ if expression.control?
14
+ raise ::Temple::FilterError, 'Explicit end statements are forbidden.' if expression.end?
15
+
16
+ result << code_end if prev_indent && !expression.else?
17
+
18
+ prev_indent = expression.if?
19
+ elsif !expression.newline? && prev_indent
20
+ result << code_end
21
+ prev_indent = false
22
+ end
23
+
24
+ result << compile(source)
25
+ end
26
+
27
+ result << code_end if prev_indent
28
+ result
29
+ end
30
+
31
+ private
32
+
33
+ # @return [Array]
34
+ def code_end
35
+ [:code, 'end']
36
+ end
37
+
38
+ class Expression
39
+ IF_REGEXP = /\A(if|begin|unless|else|elsif|when|rescue|ensure)\b|\bdo\s*(\|[^|]*\|)?\s*$/.freeze
40
+
41
+ ELSE_REGEXP = /\A(else|elsif|when|rescue|ensure)\b/.freeze
42
+
43
+ END_REGEXP = /\Aend\b/.freeze
44
+
45
+ # @param [Array] expression
46
+ def initialize(expression)
47
+ @expression = expression
48
+ end
49
+
50
+ # @return [Boolean]
51
+ def control?
52
+ @expression[0] == :slimi && @expression[1] == :control
53
+ end
54
+
55
+ # @return [Boolean]
56
+ def if?
57
+ @expression[2].match?(IF_REGEXP)
58
+ end
59
+
60
+ # @return [Boolean]
61
+ def else?
62
+ @expression[2].match?(ELSE_REGEXP)
63
+ end
64
+
65
+ # @return [Boolean]
66
+ def end?
67
+ @expression[2].match?(END_REGEXP)
68
+ end
69
+
70
+ # @return [Boolean]
71
+ def newline?
72
+ @expression[0] == :newline
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -4,29 +4,7 @@ require 'strscan'
4
4
 
5
5
  module Slimi
6
6
  module Filters
7
- class Interpolation
8
- def initialize(*); end
9
-
10
- def call(node)
11
- convert(node)
12
- end
13
-
14
- private
15
-
16
- def convert(value)
17
- if value.instance_of?(::Array)
18
- if value[0] == :slimi && value[1] == :interpolate
19
- on_slimi_interpolate(value[2], value[3], value[4])
20
- else
21
- value.map do |element|
22
- call(element)
23
- end
24
- end
25
- else
26
- value
27
- end
28
- end
29
-
7
+ class Interpolation < Base
30
8
  # @param [Integer] begin_
31
9
  # @param [Integer] end_
32
10
  # @return [Array] S-expression.
@@ -47,7 +25,7 @@ module Slimi
47
25
  else
48
26
  escape = false
49
27
  end
50
- block << [:slimi, :position, begin2, begin2 + code.length, [:slim, :output, escape, code, [:multi]]]
28
+ block << [:slimi, :position, begin2, begin2 + code.length, [:slimi, :output, escape, code, [:multi]]]
51
29
  elsif (value = scanner.scan(/([#\\]?[^#\\]*([#\\][^\\\#{][^#\\]*)*)/)) # rubocop:disable Lint/DuplicateBranch
52
30
  block << [:static, value]
53
31
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ module Filters
5
+ # Handle `[:slimi, :output, escape, code, multi]`.
6
+ class Output < Base
7
+ define_options :disable_capture
8
+
9
+ IF_REGEXP = /\A(if|unless)\b|\bdo\s*(\|[^|]*\|)?\s*$/.freeze
10
+
11
+ # @param [Boolean] escape
12
+ # @param [String] code
13
+ # @param [Array] multi
14
+ # @return [Array]
15
+ def on_slimi_output(escape, code, multi)
16
+ if code.match?(IF_REGEXP)
17
+ tmp = unique_name
18
+ [
19
+ :multi,
20
+ [:block, "#{tmp} = #{code}", options[:disable_capture] ? compile(multi) : [:capture, unique_name, compile(multi)]],
21
+ [:escape, escape, [:dynamic, tmp]]
22
+ ]
23
+ else
24
+ [
25
+ :multi,
26
+ [:escape, escape, [:dynamic, code]],
27
+ multi
28
+ ]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ module Filters
5
+ # Handle `[:slimi, :text, multi]`.
6
+ class Text < Base
7
+ # @param [Symbol] _type
8
+ # @param [Array] multi
9
+ # @return [Array]
10
+ def on_slimi_text(_type, multi)
11
+ compile(multi)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -2,29 +2,13 @@
2
2
 
3
3
  module Slimi
4
4
  module Filters
5
- class Unposition
6
- def initialize(*); end
7
-
8
- # @param [Array] node S-expression.
9
- # @return [Array] S-expression.
10
- def call(node)
11
- convert(node)
12
- end
13
-
14
- private
15
-
16
- def convert(value)
17
- if value.instance_of?(::Array)
18
- if value[0] == :slimi && value[1] == :position
19
- call(value[4])
20
- else
21
- value.map do |element|
22
- call(element)
23
- end
24
- end
25
- else
26
- value
27
- end
5
+ class Unposition < Base
6
+ # @param [Integer] _begin
7
+ # @param [Integer] _end
8
+ # @param [Array] expression
9
+ # @return [Array]
10
+ def on_slimi_position(_begin, _end, expression)
11
+ compile(expression)
28
12
  end
29
13
  end
30
14
  end
data/lib/slimi/filters.rb CHANGED
@@ -2,7 +2,16 @@
2
2
 
3
3
  module Slimi
4
4
  module Filters
5
+ autoload :Amble, 'slimi/filters/amble'
6
+ autoload :Attribute, 'slimi/filters/attribute'
7
+ autoload :Base, 'slimi/filters/base'
8
+ autoload :Control, 'slimi/filters/control'
9
+ autoload :DoInserter, 'slimi/filters/do_inserter'
10
+ autoload :Embedded, 'slimi/filters/embedded'
11
+ autoload :EndInserter, 'slimi/filters/end_inserter'
5
12
  autoload :Interpolation, 'slimi/filters/interpolation'
13
+ autoload :Output, 'slimi/filters/output'
14
+ autoload :Text, 'slimi/filters/text'
6
15
  autoload :Unposition, 'slimi/filters/unposition'
7
16
  end
8
17
  end
data/lib/slimi/parser.rb CHANGED
@@ -115,7 +115,7 @@ module Slimi
115
115
 
116
116
  embedded_template_engine_name = @scanner[1]
117
117
  attributes = parse_attributes
118
- @stacks.last << [:slim, :embedded, embedded_template_engine_name, parse_text_block, attributes]
118
+ @stacks.last << [:slimi, :embedded, embedded_template_engine_name, parse_text_block, attributes]
119
119
  end
120
120
 
121
121
  # @return [Boolean]
@@ -150,14 +150,14 @@ module Slimi
150
150
  block = [:multi]
151
151
  @stacks.last.insert(-2, [:static, ' ']) if with_leading_white_space2
152
152
  @scanner.skip(/[ \t]+/)
153
- tag << with_position { [:slim, :output, escape, parse_broken_lines, block] }
153
+ tag << with_position { [:slimi, :output, escape, parse_broken_lines, block] }
154
154
  @stacks.last << [:static, ' '] if with_trailing_white_space2
155
155
  @stacks << block
156
156
  elsif @scanner.skip(%r{[ \t]*/[ \t]*})
157
157
  syntax_error!(Errors::UnexpectedTextAfterClosedTagError) unless @scanner.match?(/\r?\n/)
158
158
  else
159
159
  @scanner.skip(/[ \t]+/)
160
- tag << [:slim, :text, :inline, parse_text_block]
160
+ tag << [:slimi, :text, :inline, parse_text_block]
161
161
  end
162
162
  true
163
163
  else
@@ -220,18 +220,24 @@ module Slimi
220
220
  value = +''
221
221
  count = 0
222
222
  loop do
223
- if @scanner.match?(/#{quote}/) && count.zero?
224
- end_ = @scanner.charpos
225
- @scanner.pos += @scanner.matched_size
226
- break
227
- end
228
-
229
- if @scanner.skip(/\{/)
223
+ if @scanner.match?(/#{quote}/)
224
+ if count.zero?
225
+ end_ = @scanner.charpos
226
+ @scanner.pos += @scanner.matched_size
227
+ break
228
+ else
229
+ @scanner.pos += @scanner.matched_size
230
+ value << @scanner.matched
231
+ end
232
+ elsif @scanner.skip(/\{/)
230
233
  count += 1
234
+ value << @scanner.matched
231
235
  elsif @scanner.skip(/\}/)
232
236
  count -= 1
237
+ value << @scanner.matched
238
+ else
239
+ value << @scanner.scan(/[^{}#{quote}]*/)
233
240
  end
234
- value << @scanner.scan(/[^{}#{quote}]*/)
235
241
  end
236
242
  [:slimi, :interpolate, begin_, end_, value]
237
243
  end
@@ -263,9 +269,10 @@ module Slimi
263
269
  elsif @scanner.skip(@ruby_attribute_regexp)
264
270
  attribute_name = @scanner[1]
265
271
  escape = @scanner[2].empty?
272
+ charpos = @scanner.charpos
266
273
  attribute_value = parse_ruby_attribute_value(attribute_delimiter_closing)
267
274
  syntax_error!(Errors::InvalidEmptyAttributeError) if attribute_value.empty?
268
- attributes << [:html, :attr, attribute_name, [:slim, :attrvalue, escape, attribute_value]]
275
+ attributes << [:html, :attr, attribute_name, [:slimi, :position, charpos, charpos + attribute_value.length, [:slimi, :attrvalue, escape, attribute_value]]]
269
276
  elsif !attribute_delimiter_closing_part_regexp
270
277
  break
271
278
  elsif @scanner.skip(boolean_attribute_regexp)
@@ -273,7 +280,10 @@ module Slimi
273
280
  elsif @scanner.skip(attribute_delimiter_closing_part_regexp) # rubocop:disable Lint/DuplicateBranch
274
281
  break
275
282
  else
276
- raise ::NotImplementedError
283
+ @scanner.skip(/[ \t]+/)
284
+ expect_line_ending
285
+
286
+ syntax_error!(Errors::AttributeClosingDelimiterNotFoundError) if @scanner.eos?
277
287
  end
278
288
  end
279
289
 
@@ -324,7 +334,7 @@ module Slimi
324
334
  def parse_html_comment
325
335
  if @scanner.skip(%r{/!})
326
336
  text_block = parse_text_block
327
- text = [:slim, :text, :verbatim, text_block]
337
+ text = [:slimi, :text, :verbatim, text_block]
328
338
  @stacks.last << [:html, :comment, text]
329
339
  true
330
340
  else
@@ -371,7 +381,7 @@ module Slimi
371
381
  def parse_verbatim_text_block_inner
372
382
  if @scanner.skip(/([|']) ?/)
373
383
  with_trailing_white_space = @scanner[1] == "'"
374
- @stacks.last << [:slim, :text, :verbatim, parse_text_block]
384
+ @stacks.last << [:slimi, :text, :verbatim, parse_text_block]
375
385
  @stacks.last << [:static, ' '] if with_trailing_white_space
376
386
  true
377
387
  else
@@ -410,7 +420,7 @@ module Slimi
410
420
  if @scanner.skip(/-/)
411
421
  block = [:multi]
412
422
  @scanner.skip(/[ \t]+/)
413
- @stacks.last << with_position { [:slim, :control, parse_broken_lines, block] }
423
+ @stacks.last << with_position { [:slimi, :control, parse_broken_lines, block] }
414
424
  @stacks << block
415
425
  true
416
426
  else
@@ -433,7 +443,7 @@ module Slimi
433
443
  block = [:multi]
434
444
  @stacks.last << [:static, ' '] if with_trailing_white_space
435
445
  @scanner.skip(/[ \t]+/)
436
- @stacks.last << with_position { [:slim, :output, escape, parse_broken_lines, block] }
446
+ @stacks.last << with_position { [:slimi, :output, escape, parse_broken_lines, block] }
437
447
  @stacks.last << [:static, ' '] if with_leading_white_space
438
448
  @stacks << block
439
449
  else
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ # Render Slim template in response to requests from Rails.
5
+ class RailsTemplateHandler
6
+ # @param [ActionView::Template] template
7
+ # @param [String, nil] source
8
+ # @return [String]
9
+ def call(template, source = nil)
10
+ Renderer.new(
11
+ source: source,
12
+ template: template
13
+ ).call
14
+ end
15
+
16
+ # Render HTML from given source and options.
17
+ class Renderer
18
+ # @param [String] source
19
+ # @param [ActionView::Template] template
20
+ def initialize(
21
+ source:,
22
+ template:
23
+ )
24
+ @source = source
25
+ @template = template
26
+ end
27
+
28
+ # @return [String]
29
+ def call
30
+ engine.call(source)
31
+ end
32
+
33
+ private
34
+
35
+ # @return [Slimi::Engine]
36
+ def engine
37
+ Engine.new(engine_options)
38
+ end
39
+
40
+ # @return [Hash{Symbol => Object}]
41
+ def engine_options
42
+ engine_default_options.merge(engine_amble_options)
43
+ end
44
+
45
+ # @return [Hash{Symbol => Object}]
46
+ def engine_default_options
47
+ {
48
+ generator: ::Temple::Generators::RailsOutputBuffer,
49
+ streaming: true,
50
+ use_html_safe: true
51
+ }
52
+ end
53
+
54
+ # @return [Hash{Symbol => Object}]
55
+ def engine_amble_options
56
+ if with_annotate_rendered_view_with_filenames?
57
+ {
58
+ postamble: "<!-- END #{@template.short_identifier} -->\n",
59
+ preamble: "<!-- BEGIN #{@template.short_identifier} -->\n"
60
+ }
61
+ else
62
+ {}
63
+ end
64
+ end
65
+
66
+ # @return [String]
67
+ def source
68
+ @source || @template.source
69
+ end
70
+
71
+ # @return [Boolean]
72
+ def with_annotate_rendered_view_with_filenames?
73
+ ::ActionView::Base.try(:annotate_rendered_view_with_filenames) && @template.format == :html
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ class Railtie < ::Rails::Railtie
5
+ initializer 'Register Slimi template handler' do
6
+ ::ActiveSupport.on_load(:action_view) do
7
+ ::ActionView::Template.register_template_handler(
8
+ :slim,
9
+ RailsTemplateHandler.new
10
+ )
11
+ end
12
+ end
13
+ end
14
+ end
data/lib/slimi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Slimi
4
- VERSION = '0.4.1'
4
+ VERSION = '0.6.0'
5
5
  end
data/lib/slimi.rb CHANGED
@@ -3,8 +3,12 @@
3
3
  require_relative 'slimi/version'
4
4
 
5
5
  module Slimi
6
+ autoload :Engine, 'slimi/engine'
6
7
  autoload :Errors, 'slimi/errors'
7
8
  autoload :Filters, 'slimi/filters'
8
9
  autoload :Parser, 'slimi/parser'
10
+ autoload :RailsTemplateHandler, 'slimi/rails_template_handler'
9
11
  autoload :Range, 'slimi/range'
10
12
  end
13
+
14
+ require_relative 'slimi/railtie' if defined?(Rails)
data/slimi.gemspec CHANGED
@@ -32,4 +32,5 @@ Gem::Specification.new do |spec|
32
32
  }
33
33
 
34
34
  spec.add_dependency 'temple'
35
+ spec.add_dependency 'tilt'
35
36
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slimi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryo Nakamura
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-12-26 00:00:00.000000000 Z
11
+ date: 2022-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: temple
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: tilt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  description:
28
42
  email:
29
43
  - r7kamura@gmail.com
@@ -43,11 +57,23 @@ files:
43
57
  - bin/console
44
58
  - bin/setup
45
59
  - lib/slimi.rb
60
+ - lib/slimi/engine.rb
46
61
  - lib/slimi/errors.rb
47
62
  - lib/slimi/filters.rb
63
+ - lib/slimi/filters/amble.rb
64
+ - lib/slimi/filters/attribute.rb
65
+ - lib/slimi/filters/base.rb
66
+ - lib/slimi/filters/control.rb
67
+ - lib/slimi/filters/do_inserter.rb
68
+ - lib/slimi/filters/embedded.rb
69
+ - lib/slimi/filters/end_inserter.rb
48
70
  - lib/slimi/filters/interpolation.rb
71
+ - lib/slimi/filters/output.rb
72
+ - lib/slimi/filters/text.rb
49
73
  - lib/slimi/filters/unposition.rb
50
74
  - lib/slimi/parser.rb
75
+ - lib/slimi/rails_template_handler.rb
76
+ - lib/slimi/railtie.rb
51
77
  - lib/slimi/range.rb
52
78
  - lib/slimi/version.rb
53
79
  - slimi.gemspec