slimi 0.3.0 → 0.5.0

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: 69032d72b1695bfe2afc7a11dbb965e4abf252a7c42caab252e972bcb45c13f8
4
- data.tar.gz: af3822a5d2375300d4b07f0114c30c933eb215c88bfcee1bd6cc4938d769d76e
3
+ metadata.gz: 94ce8aacd3d29798f0831cedcd033b1706b2ccebe85264cff5321d8671d7fda3
4
+ data.tar.gz: 5cbe5fe9791eafac5951f02853bb9600efb726f10d4a990d96b8ef5bfb2c4c38
5
5
  SHA512:
6
- metadata.gz: b59318e78f3c7c83e67334326992babdc60d3d299ed8973778f7c3c463b1eaaaacbefafe20ee8f7072eff13db72c3a88c5a0cae7f4ecd18629bc9e8a35645599
7
- data.tar.gz: 173fdc474e985a89ead0c473d25bcb2bb4deb586679d17bee18d5384b29e21900e79e03567ed302bce9ac37b81dc4ad29ed3b837eeeac5027d7db57ed3879c4b
6
+ metadata.gz: ba6180bd2e6e890dd2149e19390a4cb030c85ddbaac97f89d259fc9cd476673ea62cc788f84654561642ec6e86055ac5c0ae0cd8d3f4eb86a7225ca200b82d30
7
+ data.tar.gz: 537418a830f3b7d791e98ac107eea63732ede37d531c1d879eb9ded7588ab2a4897569fb05a552e73f543d6a4ce159fe9a66c0f0c2d6cd870e9976d58b6a46a2
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,17 @@ 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
+
15
30
  Style/Documentation:
16
31
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -2,11 +2,43 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.5.0 - 2022-01-02
6
+
7
+ ### Added
8
+
9
+ - Add Slimi::Engine.
10
+
11
+ ## 0.4.2 - 2021-12-27
12
+
13
+ ### Fixed
14
+
15
+ - Fix bug on quoted attribute parser.
16
+ - Remove trailing line-ending from source line of syntax error message.
17
+ - Support multi-line attributes.
18
+
19
+ ## 0.4.1 - 2021-12-26
20
+
21
+ ### Fixed
22
+
23
+ - Fix bug on parsing Ruby attribute value.
24
+ - Fix bug on empty line in text block.
25
+
26
+ ## 0.4.0 - 2021-12-25
27
+
28
+ ### Added
29
+
30
+ - Support :file option on parser for showing correct file path on syntax error.
31
+
32
+ ### Fixed
33
+
34
+ - Fix NameError on unknown line indicator.
35
+ - Fix bug that default parser options are not used.
36
+
5
37
  ## 0.3.0 - 2021-12-24
6
38
 
7
39
  ### Added
8
40
 
9
- - Support unquoted attributes.
41
+ - Support Ruby attributes.
10
42
 
11
43
  ### Fixed
12
44
 
data/Gemfile CHANGED
@@ -6,6 +6,6 @@ source 'https://rubygems.org'
6
6
  gemspec
7
7
 
8
8
  gem 'rake', '~> 13.0'
9
- gem 'rspec', '~> 3.0'
10
- gem 'rubocop', '~> 1.21'
11
- gem 'slim'
9
+ gem 'rspec'
10
+ gem 'rubocop'
11
+ gem 'rubocop-rspec'
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- slimi (0.3.0)
4
+ slimi (0.5.0)
5
5
  temple
6
+ tilt
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
@@ -40,10 +41,9 @@ GEM
40
41
  unicode-display_width (>= 1.4.0, < 3.0)
41
42
  rubocop-ast (1.15.0)
42
43
  parser (>= 3.0.1.1)
44
+ rubocop-rspec (2.6.0)
45
+ rubocop (~> 1.19)
43
46
  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
47
  temple (0.8.2)
48
48
  tilt (2.0.10)
49
49
  unicode-display_width (2.1.0)
@@ -53,9 +53,9 @@ PLATFORMS
53
53
 
54
54
  DEPENDENCIES
55
55
  rake (~> 13.0)
56
- rspec (~> 3.0)
57
- rubocop (~> 1.21)
58
- slim
56
+ rspec
57
+ rubocop
58
+ rubocop-rspec
59
59
  slimi!
60
60
 
61
61
  BUNDLED WITH
data/README.md CHANGED
@@ -5,28 +5,18 @@
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
-
32
- TBD.
@@ -0,0 +1,37 @@
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
+ merge_attrs: { 'class' => ' ' },
13
+ pretty: false,
14
+ sort_attrs: true
15
+ )
16
+
17
+ use Parser
18
+ use Filters::Unposition
19
+ use Filters::Embedded
20
+ use Filters::Interpolation
21
+ use Filters::DoInserter
22
+ use Filters::EndInserter
23
+ use Filters::Control
24
+ use Filters::Output
25
+ use Filters::Text
26
+ html :AttributeSorter
27
+ html :AttributeMerger
28
+ use Filters::Attribute
29
+ use(:AttributeRemover) { ::Temple::HTML::AttributeRemover.new(remove_empty_attrs: options[:merge_attrs].keys) }
30
+ html :Pretty
31
+ filter :Escapable
32
+ filter :ControlFlow
33
+ filter :MultiFlattener
34
+ filter :StaticMerger
35
+ generator :StringBuffer
36
+ end
37
+ 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
 
@@ -57,7 +60,7 @@ module Slimi
57
60
  class UnexpectedTextAfterClosedTagError < SlimSyntaxError
58
61
  end
59
62
 
60
- class UnknownLineIndicator < SlimSyntaxError
63
+ class UnknownLineIndicatorError < SlimSyntaxError
61
64
  end
62
65
  end
63
66
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ module Filters
5
+ # Handle `[:slimi, :attributes, ...]`.
6
+ class Attribute < ::Temple::HTML::Filter
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] == :slim && 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_slim_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,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ module Filters
5
+ # Handle `[:slim, :control, code, multi]`.
6
+ class Control < ::Temple::HTML::Filter
7
+ # @param [String] code
8
+ # @param [Array] multi
9
+ # @return [Array]
10
+ def on_slim_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 < ::Temple::HTML::Filter
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_slim_control(code, expression)
13
+ code += ' do' unless code.match?(VALID_RUBY_LINE_REGEXP) || empty_exp?(expression)
14
+ [:slim, :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_slim_output(escape, code, expression)
22
+ code += ' do' unless code.match?(VALID_RUBY_LINE_REGEXP) || empty_exp?(expression)
23
+ [:slim, :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 < ::Temple::HTML::Filter
7
+ def call(exp)
8
+ @collected = ''
9
+ super(exp)
10
+ @collected
11
+ end
12
+
13
+ def on_slim_interpolate(text)
14
+ @collected << text
15
+ nil
16
+ end
17
+ end
18
+
19
+ # @api private
20
+ class NewlineCollector < ::Temple::HTML::Filter
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 < ::Temple::HTML::Filter
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_slim_output(escape, text, content)
49
+ @collected << @tag
50
+ @protect << [:slim, :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 < ::Temple::HTML::Filter
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_slim_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_slim_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 < ::Temple::HTML::Filter
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_slim_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_slim_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_slim_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_slim_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_slim_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 < ::Temple::HTML::Filter
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] == :slim && @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
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ module Filters
5
+ # Handle `[:slim, :output, escape, code, multi]`.
6
+ class Output < ::Temple::HTML::Filter
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_slim_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 `[:slim, :text, multi]`.
6
+ class Text < ::Temple::HTML::Filter
7
+ # @param [Symbol] _type
8
+ # @param [Array] multi
9
+ # @return [Array]
10
+ def on_slim_text(_type, multi)
11
+ compile(multi)
12
+ end
13
+ end
14
+ end
15
+ end
data/lib/slimi/filters.rb CHANGED
@@ -2,7 +2,14 @@
2
2
 
3
3
  module Slimi
4
4
  module Filters
5
+ autoload :Attribute, 'slimi/filters/attribute'
6
+ autoload :Control, 'slimi/filters/control'
7
+ autoload :DoInserter, 'slimi/filters/do_inserter'
8
+ autoload :Embedded, 'slimi/filters/embedded'
9
+ autoload :EndInserter, 'slimi/filters/end_inserter'
5
10
  autoload :Interpolation, 'slimi/filters/interpolation'
11
+ autoload :Output, 'slimi/filters/output'
12
+ autoload :Text, 'slimi/filters/text'
6
13
  autoload :Unposition, 'slimi/filters/unposition'
7
14
  end
8
15
  end
data/lib/slimi/parser.rb CHANGED
@@ -6,6 +6,7 @@ require 'temple'
6
6
  module Slimi
7
7
  class Parser < ::Temple::Parser
8
8
  define_options(
9
+ :file,
9
10
  attr_list_delims: {
10
11
  '(' => ')',
11
12
  '[' => ']',
@@ -22,8 +23,9 @@ module Slimi
22
23
  }
23
24
  )
24
25
 
25
- def initialize(options = {})
26
+ def initialize(_options = {})
26
27
  super
28
+ @file_path = options[:file] || '(__TEMPLATE__)'
27
29
  factory = Factory.new(
28
30
  attribute_delimiters: options[:attr_list_delims] || {},
29
31
  default_tag: options[:default_tag] || 'div',
@@ -69,7 +71,7 @@ module Slimi
69
71
  parse_embedded_template ||
70
72
  parse_doctype ||
71
73
  parse_tag ||
72
- raise(Errors::UnknownLineIndicatorError)
74
+ syntax_error!(Errors::UnknownLineIndicatorError)
73
75
  end
74
76
 
75
77
  # Parse blank line.
@@ -198,8 +200,6 @@ module Slimi
198
200
  marker = @scanner[1]
199
201
  attribute_value = @scanner[2]
200
202
  attribute_names = @attribute_shortcuts[marker]
201
- raise 'Illegal shortcut' unless attribute_names
202
-
203
203
  attribute_names.map do |attribute_name|
204
204
  result << [:html, :attr, attribute_name.to_s, [:static, attribute_value]]
205
205
  end
@@ -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
@@ -273,7 +279,10 @@ module Slimi
273
279
  elsif @scanner.skip(attribute_delimiter_closing_part_regexp) # rubocop:disable Lint/DuplicateBranch
274
280
  break
275
281
  else
276
- raise ::NotImplementedError
282
+ @scanner.skip(/[ \t]+/)
283
+ expect_line_ending
284
+
285
+ syntax_error!(Errors::AttributeClosingDelimiterNotFoundError) if @scanner.eos?
277
286
  end
278
287
  end
279
288
 
@@ -300,12 +309,12 @@ module Slimi
300
309
  attribute_value << @scanner[1] << "\n"
301
310
  else
302
311
  if count.positive?
303
- if opening_delimiter && @scanner.skip(::Regexp.escape(opening_delimiter))
312
+ if opening_delimiter && @scanner.match?(/#{::Regexp.escape(opening_delimiter)}/)
304
313
  count += 1
305
- elsif closing_delimiter && @scanner.skip(::Regexp.escape(closing_delimiter))
314
+ elsif closing_delimiter && @scanner.match?(/#{::Regexp.escape(closing_delimiter)}/)
306
315
  count -= 1
307
316
  end
308
- elsif @scanner.skip(@ruby_attribute_delimiter_regexp)
317
+ elsif @scanner.match?(@ruby_attribute_delimiter_regexp)
309
318
  count = 1
310
319
  opening_delimiter = @scanner.matched
311
320
  closing_delimiter = @ruby_attribute_delimiters[opening_delimiter]
@@ -503,9 +512,13 @@ module Slimi
503
512
  interpolate = parse_interpolate_line
504
513
  result << interpolate if interpolate
505
514
 
506
- loop do
507
- break unless @scanner.match?(/\r?\n[ \t]*/)
515
+ until @scanner.eos?
516
+ if @scanner.skip(/\r?\n[ \t]*(?=\r?\n)/)
517
+ result << [:newline]
518
+ next
519
+ end
508
520
 
521
+ @scanner.match?(/\r?\n[ \t]*/)
509
522
  indent = indent_from_last_match
510
523
  break if indent <= @indents.last
511
524
 
@@ -556,7 +569,7 @@ module Slimi
556
569
  range = Range.new(index: @scanner.charpos, source: @scanner.string)
557
570
  raise syntax_error_class.new(
558
571
  column: range.column,
559
- file_path: '(__TEMPLATE__)',
572
+ file_path: @file_path,
560
573
  line: range.line,
561
574
  line_number: range.line_number
562
575
  )
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ class RailsTemplateHandler
5
+ def initialize
6
+ @engine = Engine.new
7
+ end
8
+
9
+ def call(template, source = nil)
10
+ source ||= template.source
11
+ @engine.call(source)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
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
+ RailsTemplateHandler.new
9
+ )
10
+ end
11
+ end
12
+ end
13
+ 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.3.0'
4
+ VERSION = '0.5.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.3.0
4
+ version: 0.5.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-23 00:00:00.000000000 Z
11
+ date: 2022-01-01 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,21 @@ 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/attribute.rb
64
+ - lib/slimi/filters/control.rb
65
+ - lib/slimi/filters/do_inserter.rb
66
+ - lib/slimi/filters/embedded.rb
67
+ - lib/slimi/filters/end_inserter.rb
48
68
  - lib/slimi/filters/interpolation.rb
69
+ - lib/slimi/filters/output.rb
70
+ - lib/slimi/filters/text.rb
49
71
  - lib/slimi/filters/unposition.rb
50
72
  - lib/slimi/parser.rb
73
+ - lib/slimi/rails_template_handler.rb
74
+ - lib/slimi/railtie.rb
51
75
  - lib/slimi/range.rb
52
76
  - lib/slimi/version.rb
53
77
  - slimi.gemspec