slimi 0.4.1 → 0.6.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: 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