slimi 0.4.2 → 0.7.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: a503c590465e63f43b6d9ef8047700ff415bfea923df65be323f2d37f5d94b71
4
- data.tar.gz: 4ad486738d38c81b670cb693afa31c9c7f8acb92ab05a844d47cf3df1d0219c8
3
+ metadata.gz: '09a450cbc4d4a536b55bee031d9b76345aee45e82d112563d83cdadc1476a82c'
4
+ data.tar.gz: 781931ba445275b0bc24157df457dde36e49f74b7c48afedcbfa8cb279c6a401
5
5
  SHA512:
6
- metadata.gz: '08dc1c84baa6a338564757e6c28143a77f1ecc03eb4ba7f72bf2af82ce673eb2c573debbbbf5eaf16673b53e07a49dd3493888875e6b9e5016557ab7f8373368'
7
- data.tar.gz: dd3bb74308363284ff648f80d2dcc37fe9b7faa75df2b31210ca4567fa088debca11b5831bb8ec745800bd15e9886e5ff5bf6074bcb6aa673ca826421e10bc31
6
+ metadata.gz: 3810040838f6d5eaf636326eaa4021edd62a810d508b41efe7f27a29702c444a783cd2f0bfbd9dedc535d52738bc2446c55df08e6a6e0acee3cb3743ae7798f8
7
+ data.tar.gz: 625e3d9bd3be61d376364388f6ef86f555fe9065a73d802e907fcb1a6238d4707d42a5d4597acc05794d6e46d4623ebb01007836b2b8bb5ae2884158e3d16ad1
data/.rubocop.yml CHANGED
@@ -24,8 +24,14 @@ RSpec/ImplicitSubject:
24
24
  RSpec/MultipleExpectations:
25
25
  Enabled: false
26
26
 
27
+ RSpec/MultipleMemoizedHelpers:
28
+ Enabled: false
29
+
27
30
  RSpec/NamedSubject:
28
31
  Enabled: false
29
32
 
33
+ Security/Eval:
34
+ Enabled: false
35
+
30
36
  Style/Documentation:
31
37
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.0 - 2022-01-04
6
+
7
+ ### Added
8
+
9
+ - Add `slimi` executable.
10
+
11
+ ### Fixed
12
+
13
+ - Remove preceding white spaces from HTML comment.
14
+ - Fix bug that :generator option was not working.
15
+ - Fix bug that Ruby code in nested line can be problem on compilation.
16
+
17
+ ## 0.6.0 - 2022-01-03
18
+
19
+ ### Added
20
+
21
+ - Support annotate_rendered_view_with_filenames.
22
+
23
+ ### Changed
24
+
25
+ - Rename expression name from slim to slimi.
26
+
27
+ ### Fixed
28
+
29
+ - Fix bug at registering handler to ActionView.
30
+ - Fix Engine options at RailsTemplateHandler.
31
+ - Define missing :generator option at Engine.
32
+
33
+ ## 0.5.1 - 2022-01-02
34
+
35
+ ### Changed
36
+
37
+ - Wrap slim attrvalue by slimi position expression.
38
+
39
+ ## 0.5.0 - 2022-01-02
40
+
41
+ ### Added
42
+
43
+ - Add Slimi::Engine.
44
+
5
45
  ## 0.4.2 - 2021-12-27
6
46
 
7
47
  ### Fixed
data/Gemfile CHANGED
@@ -5,8 +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
10
  gem 'rspec'
10
11
  gem 'rubocop'
11
12
  gem 'rubocop-rspec'
12
- gem 'slim'
data/Gemfile.lock CHANGED
@@ -1,17 +1,48 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- slimi (0.4.2)
4
+ slimi (0.7.0)
5
5
  temple
6
+ thor
7
+ tilt
6
8
 
7
9
  GEM
8
10
  remote: https://rubygems.org/
9
11
  specs:
12
+ actionview (7.0.0)
13
+ activesupport (= 7.0.0)
14
+ builder (~> 3.1)
15
+ erubi (~> 1.4)
16
+ rails-dom-testing (~> 2.0)
17
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
18
+ activesupport (7.0.0)
19
+ concurrent-ruby (~> 1.0, >= 1.0.2)
20
+ i18n (>= 1.6, < 2)
21
+ minitest (>= 5.1)
22
+ tzinfo (~> 2.0)
10
23
  ast (2.4.2)
24
+ builder (3.2.4)
25
+ concurrent-ruby (1.1.9)
26
+ crass (1.0.6)
11
27
  diff-lcs (1.4.4)
28
+ erubi (1.10.0)
29
+ i18n (1.8.11)
30
+ concurrent-ruby (~> 1.0)
31
+ loofah (2.13.0)
32
+ crass (~> 1.0.2)
33
+ nokogiri (>= 1.5.9)
34
+ minitest (5.15.0)
35
+ nokogiri (1.12.5-x86_64-linux)
36
+ racc (~> 1.4)
12
37
  parallel (1.21.0)
13
38
  parser (3.0.3.2)
14
39
  ast (~> 2.4.1)
40
+ racc (1.6.0)
41
+ rails-dom-testing (2.0.3)
42
+ activesupport (>= 4.2.0)
43
+ nokogiri (>= 1.6)
44
+ rails-html-sanitizer (1.4.2)
45
+ loofah (~> 2.3)
15
46
  rainbow (3.0.0)
16
47
  rake (13.0.6)
17
48
  regexp_parser (2.2.0)
@@ -43,22 +74,22 @@ GEM
43
74
  rubocop-rspec (2.6.0)
44
75
  rubocop (~> 1.19)
45
76
  ruby-progressbar (1.11.0)
46
- slim (4.1.0)
47
- temple (>= 0.7.6, < 0.9)
48
- tilt (>= 2.0.6, < 2.1)
49
77
  temple (0.8.2)
78
+ thor (1.1.0)
50
79
  tilt (2.0.10)
80
+ tzinfo (2.0.4)
81
+ concurrent-ruby (~> 1.0)
51
82
  unicode-display_width (2.1.0)
52
83
 
53
84
  PLATFORMS
54
85
  x86_64-linux
55
86
 
56
87
  DEPENDENCIES
88
+ actionview
57
89
  rake (~> 13.0)
58
90
  rspec
59
91
  rubocop
60
92
  rubocop-rspec
61
- slim
62
93
  slimi!
63
94
 
64
95
  BUNDLED WITH
data/README.md CHANGED
@@ -5,28 +5,63 @@
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:
16
+ ## Usage
19
17
 
20
- ```
21
- bundle install
22
- ```
18
+ ### Rails
23
19
 
24
- Or install it yourself as:
20
+ Add this line to your application's Gemfile.
25
21
 
26
- ```
27
- gem install slimi
22
+ ```ruby
23
+ gem 'slimi'
28
24
  ```
29
25
 
30
- ## Usage
26
+ This will cause `app/views/**/*.slim` files to be rendered by Slimi.
27
+
28
+ ## Compatibility
31
29
 
32
- TBD.
30
+ - Line indicators
31
+ - [x] Vebatim text
32
+ - [x] Inline HTML
33
+ - [x] Control
34
+ - [x] Output
35
+ - [x] HTML comment
36
+ - [x] Code comment
37
+ - [x] IE conditional comment
38
+ - Tags
39
+ - [x] Doctype declaration
40
+ - [x] Closed tags
41
+ - [x] Trailing and leading white space
42
+ - [x] Inline tags
43
+ - [x] Text content
44
+ - [x] Dynamic content
45
+ - [x] Tag shortcuts
46
+ - [ ] Dynamic tags
47
+ - Attributes
48
+ - [x] Attributes wrapper
49
+ - [x] Quoted attributes
50
+ - [x] Ruby attributes
51
+ - [x] Boolean attributes
52
+ - [x] Attribute merging
53
+ - [x] Attribute shortcuts
54
+ - [ ] Splat attributes
55
+ - Plugins
56
+ - [ ] Include partials
57
+ - [ ] Translator/I18n
58
+ - [ ] Logic-less mode
59
+ - [ ] Smart text mode
60
+ - CLI
61
+ - [x] Convert Slim to Ruby
62
+ - [x] Convert Slim to HTML
63
+ - [x] Convert Slim to ERB
64
+ - Slimi-only features
65
+ - [x] Embedded Ruby code location
66
+ - [x] Support for annotate_rendered_view_with_filenames
67
+ - [x] Convert Slim to Temple expression by CLI
data/exe/slimi ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+ require 'slimi'
6
+
7
+ Slimi::Cli.start
data/lib/slimi/cli.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module Slimi
6
+ # Provide CLI features.
7
+ class Cli < ::Thor
8
+ desc 'compile', 'Convert Slim into Ruby'
9
+ def compile
10
+ slim = $stdin.read
11
+ ruby = Engine.new.call(slim)
12
+ puts ruby
13
+ end
14
+
15
+ desc 'erb', 'Convert Slim into ERB'
16
+ def erb
17
+ slim = $stdin.read
18
+ expression = ErbConverter.new.call(slim)
19
+ puts expression
20
+ end
21
+
22
+ desc 'parse', 'Convert Slim into Temple expression'
23
+ def parse
24
+ slim = $stdin.read
25
+ expression = Parser.new.call(slim)
26
+ pp expression
27
+ end
28
+
29
+ desc 'render', 'Convert Slim into HTML'
30
+ def render
31
+ slim = $stdin.read
32
+ ruby = Engine.new.call(slim)
33
+ result = eval(ruby)
34
+ puts result
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'temple'
4
+
5
+ module Slimi
6
+ # Convert Slim into Ruby.
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::Embedded
20
+ use Filters::Interpolation
21
+ use Filters::Unposition
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
+ use(:Generator) { options[:generator] }
38
+ end
39
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ # Convert Slim into ERB.
5
+ class ErbConverter < Engine
6
+ replace :StaticMerger, ::Temple::Filters::CodeMerger
7
+ replace :Generator, Temple::Generators::ERB
8
+ end
9
+ end
@@ -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,49 @@
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 [Integer] begin_
34
+ # @param [Integer] end_
35
+ # @param [Array] expression
36
+ # @return [Array]
37
+ def on_slimi_position(begin_, end_, expression)
38
+ [:slimi, :position, begin_, end_, compile(expression)]
39
+ end
40
+
41
+ # @param [String] type
42
+ # @param [Array] expression
43
+ # @return [Array]
44
+ def on_slimi_text(type, expression)
45
+ [:slimi, :text, type, compile(expression)]
46
+ end
47
+ end
48
+ end
49
+ 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,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tilt'
4
+
5
+ module Slimi
6
+ module Filters
7
+ # @api private
8
+ class TextCollector < Base
9
+ def call(exp)
10
+ @collected = ''
11
+ super(exp)
12
+ @collected
13
+ end
14
+
15
+ def on_slimi_interpolate(text)
16
+ @collected << text
17
+ nil
18
+ end
19
+ end
20
+
21
+ # @api private
22
+ class NewlineCollector < Base
23
+ def call(exp)
24
+ @collected = [:multi]
25
+ super(exp)
26
+ @collected
27
+ end
28
+
29
+ def on_newline
30
+ @collected << [:newline]
31
+ nil
32
+ end
33
+ end
34
+
35
+ # @api private
36
+ class OutputProtector < Base
37
+ def call(exp)
38
+ @protect = []
39
+ @collected = ''
40
+ @tag = "%#{object_id.abs.to_s(36)}%"
41
+ super(exp)
42
+ @collected
43
+ end
44
+
45
+ def on_static(text)
46
+ @collected << text
47
+ nil
48
+ end
49
+
50
+ def on_slimi_output(escape, text, content)
51
+ @collected << @tag
52
+ @protect << [:slimi, :output, escape, text, content]
53
+ nil
54
+ end
55
+
56
+ def unprotect(text)
57
+ block = [:multi]
58
+ while text =~ /#{@tag}/
59
+ block << [:static, Regexp.last_match.pre_match]
60
+ block << @protect.shift
61
+ text = Regexp.last_match.post_match
62
+ end
63
+ block << [:static, text]
64
+ end
65
+ end
66
+
67
+ # Temple filter which processes embedded engines
68
+ # @api private
69
+ class Embedded < Base
70
+ @engines = {}
71
+
72
+ class << self
73
+ attr_reader :engines
74
+
75
+ # Register embedded engine
76
+ #
77
+ # @param [String] name Name of the engine
78
+ # @param [Class] klass Engine class
79
+ # @param option_filter List of options to pass to engine.
80
+ # Last argument can be default option hash.
81
+ def register(name, klass, *option_filter)
82
+ name = name.to_sym
83
+ local_options = option_filter.last.respond_to?(:to_hash) ? option_filter.pop.to_hash : {}
84
+ define_options(name, *option_filter)
85
+ klass.define_options(name)
86
+ engines[name.to_sym] = proc do |options|
87
+ klass.new({}.update(options).delete_if { |k, _v| !option_filter.include?(k) && k != name }.update(local_options))
88
+ end
89
+ end
90
+
91
+ def create(name, options)
92
+ constructor = engines[name] || raise(Temple::FilterError, "Embedded engine #{name} not found")
93
+ constructor.call(options)
94
+ end
95
+ end
96
+
97
+ define_options :enable_engines, :disable_engines
98
+
99
+ def initialize(opts = {})
100
+ super
101
+ @engines = {}
102
+ @enabled = normalize_engine_list(options[:enable_engines])
103
+ @disabled = normalize_engine_list(options[:disable_engines])
104
+ end
105
+
106
+ def on_slimi_embedded(name, body, attrs)
107
+ name = name.to_sym
108
+ raise(Temple::FilterError, "Embedded engine #{name} is disabled") unless enabled?(name)
109
+
110
+ @engines[name] ||= self.class.create(name, options)
111
+ @engines[name].on_slimi_embedded(name, body, attrs)
112
+ end
113
+
114
+ def enabled?(name)
115
+ (!@enabled || @enabled.include?(name)) &&
116
+ (!@disabled || !@disabled.include?(name))
117
+ end
118
+
119
+ protected
120
+
121
+ def normalize_engine_list(list)
122
+ raise(ArgumentError, 'Option :enable_engines/:disable_engines must be String or Symbol list') unless !list || list.is_a?(Array)
123
+
124
+ list&.map(&:to_sym)
125
+ end
126
+
127
+ class Engine < Base
128
+ protected
129
+
130
+ def collect_text(body)
131
+ @text_collector ||= TextCollector.new
132
+ @text_collector.call(body)
133
+ end
134
+
135
+ def collect_newlines(body)
136
+ @newline_collector ||= NewlineCollector.new
137
+ @newline_collector.call(body)
138
+ end
139
+ end
140
+
141
+ # Basic tilt engine
142
+ class TiltEngine < Engine
143
+ def on_slimi_embedded(engine, body, _attrs)
144
+ tilt_engine = Tilt[engine] || raise(Temple::FilterError, "Tilt engine #{engine} is not available.")
145
+ tilt_options = options[engine.to_sym] || {}
146
+ tilt_options[:default_encoding] ||= 'utf-8'
147
+ [:multi, tilt_render(tilt_engine, tilt_options, collect_text(body)), collect_newlines(body)]
148
+ end
149
+
150
+ protected
151
+
152
+ def tilt_render(tilt_engine, tilt_options, text)
153
+ [:static, tilt_engine.new(tilt_options) { text }.render]
154
+ end
155
+ end
156
+
157
+ # Sass engine which supports :pretty option
158
+ class SassEngine < TiltEngine
159
+ define_options :pretty
160
+
161
+ protected
162
+
163
+ def tilt_render(tilt_engine, tilt_options, text)
164
+ text = tilt_engine.new(tilt_options.merge(
165
+ style: options[:pretty] ? :expanded : :compressed,
166
+ cache: false
167
+ )) { text }.render
168
+ text = text.chomp
169
+ [:static, text]
170
+ end
171
+ end
172
+
173
+ # Static template with interpolated ruby code
174
+ class InterpolateTiltEngine < TiltEngine
175
+ def collect_text(body)
176
+ output_protector.call(interpolation.call(body))
177
+ end
178
+
179
+ def tilt_render(tilt_engine, tilt_options, text)
180
+ output_protector.unprotect(tilt_engine.new(tilt_options) { text }.render)
181
+ end
182
+
183
+ private
184
+
185
+ def interpolation
186
+ @interpolation ||= Interpolation.new
187
+ end
188
+
189
+ def output_protector
190
+ @output_protector ||= OutputProtector.new
191
+ end
192
+ end
193
+
194
+ # Tag wrapper engine
195
+ # Generates a html tag and wraps another engine (specified via :engine option)
196
+ class TagEngine < Engine
197
+ disable_option_validator!
198
+
199
+ def on_slimi_embedded(engine, body, attrs)
200
+ unless options[:attributes].empty?
201
+ options[:attributes].map do |k, v|
202
+ attrs << [:html, :attr, k, [:static, v]]
203
+ end
204
+ end
205
+
206
+ if options[:engine]
207
+ opts = {}.update(options)
208
+ opts.delete(:engine)
209
+ opts.delete(:tag)
210
+ opts.delete(:attributes)
211
+ @engine ||= options[:engine].new(opts)
212
+ body = @engine.on_slimi_embedded(engine, body, attrs)
213
+ end
214
+
215
+ [:html, :tag, options[:tag], attrs, body]
216
+ end
217
+ end
218
+
219
+ # Javascript wrapper engine.
220
+ # Like TagEngine, but can wrap content in html comment or cdata.
221
+ class JavaScriptEngine < TagEngine
222
+ disable_option_validator!
223
+
224
+ set_options tag: :script, attributes: {}
225
+
226
+ def on_slimi_embedded(engine, body, attrs)
227
+ super(engine, [:html, :js, body], attrs)
228
+ end
229
+ end
230
+
231
+ # Embeds ruby code
232
+ class RubyEngine < Engine
233
+ def on_slimi_embedded(_engine, body, _attrs)
234
+ [:multi, [:newline], [:code, "#{collect_text(body)}\n"]]
235
+ end
236
+ end
237
+
238
+ # These engines are executed at compile time, embedded ruby is interpolated
239
+ register :markdown, InterpolateTiltEngine
240
+ register :textile, InterpolateTiltEngine
241
+ register :rdoc, InterpolateTiltEngine
242
+
243
+ # These engines are executed at compile time
244
+ register :coffee, JavaScriptEngine, engine: TiltEngine
245
+ register :less, TagEngine, tag: :style, attributes: { type: 'text/css' }, engine: TiltEngine
246
+ register :sass, TagEngine, :pretty, tag: :style, attributes: { type: 'text/css' }, engine: SassEngine
247
+ register :scss, TagEngine, :pretty, tag: :style, attributes: { type: 'text/css' }, engine: SassEngine
248
+
249
+ # Embedded javascript/css
250
+ register :javascript, JavaScriptEngine
251
+ register :css, TagEngine, tag: :style, attributes: { type: 'text/css' }
252
+
253
+ # Embedded ruby code
254
+ register :ruby, RubyEngine
255
+ end
256
+ end
257
+ 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
@@ -269,9 +269,10 @@ module Slimi
269
269
  elsif @scanner.skip(@ruby_attribute_regexp)
270
270
  attribute_name = @scanner[1]
271
271
  escape = @scanner[2].empty?
272
+ charpos = @scanner.charpos
272
273
  attribute_value = parse_ruby_attribute_value(attribute_delimiter_closing)
273
274
  syntax_error!(Errors::InvalidEmptyAttributeError) if attribute_value.empty?
274
- 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]]]
275
276
  elsif !attribute_delimiter_closing_part_regexp
276
277
  break
277
278
  elsif @scanner.skip(boolean_attribute_regexp)
@@ -331,9 +332,9 @@ module Slimi
331
332
 
332
333
  # @return [Boolean]
333
334
  def parse_html_comment
334
- if @scanner.skip(%r{/!})
335
+ if @scanner.skip(%r{/![ \t]*})
335
336
  text_block = parse_text_block
336
- text = [:slim, :text, :verbatim, text_block]
337
+ text = [:slimi, :text, :verbatim, text_block]
337
338
  @stacks.last << [:html, :comment, text]
338
339
  true
339
340
  else
@@ -380,7 +381,7 @@ module Slimi
380
381
  def parse_verbatim_text_block_inner
381
382
  if @scanner.skip(/([|']) ?/)
382
383
  with_trailing_white_space = @scanner[1] == "'"
383
- @stacks.last << [:slim, :text, :verbatim, parse_text_block]
384
+ @stacks.last << [:slimi, :text, :verbatim, parse_text_block]
384
385
  @stacks.last << [:static, ' '] if with_trailing_white_space
385
386
  true
386
387
  else
@@ -419,7 +420,7 @@ module Slimi
419
420
  if @scanner.skip(/-/)
420
421
  block = [:multi]
421
422
  @scanner.skip(/[ \t]+/)
422
- @stacks.last << with_position { [:slim, :control, parse_broken_lines, block] }
423
+ @stacks.last << with_position { [:slimi, :control, parse_broken_lines, block] }
423
424
  @stacks << block
424
425
  true
425
426
  else
@@ -442,7 +443,7 @@ module Slimi
442
443
  block = [:multi]
443
444
  @stacks.last << [:static, ' '] if with_trailing_white_space
444
445
  @scanner.skip(/[ \t]+/)
445
- @stacks.last << with_position { [:slim, :output, escape, parse_broken_lines, block] }
446
+ @stacks.last << with_position { [:slimi, :output, escape, parse_broken_lines, block] }
446
447
  @stacks.last << [:static, ' '] if with_leading_white_space
447
448
  @stacks << block
448
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.2'
4
+ VERSION = '0.7.0'
5
5
  end
data/lib/slimi.rb CHANGED
@@ -3,8 +3,14 @@
3
3
  require_relative 'slimi/version'
4
4
 
5
5
  module Slimi
6
+ autoload :Cli, 'slimi/cli'
7
+ autoload :Engine, 'slimi/engine'
8
+ autoload :ErbConverter, 'slimi/erb_converter'
6
9
  autoload :Errors, 'slimi/errors'
7
10
  autoload :Filters, 'slimi/filters'
8
11
  autoload :Parser, 'slimi/parser'
12
+ autoload :RailsTemplateHandler, 'slimi/rails_template_handler'
9
13
  autoload :Range, 'slimi/range'
10
14
  end
15
+
16
+ require_relative 'slimi/railtie' if defined?(Rails)
data/slimi.gemspec CHANGED
@@ -32,4 +32,6 @@ Gem::Specification.new do |spec|
32
32
  }
33
33
 
34
34
  spec.add_dependency 'temple'
35
+ spec.add_dependency 'thor'
36
+ spec.add_dependency 'tilt'
35
37
  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.2
4
+ version: 0.7.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-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: temple
@@ -24,10 +24,39 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
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'
41
+ - !ruby/object:Gem::Dependency
42
+ name: tilt
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
27
55
  description:
28
56
  email:
29
57
  - r7kamura@gmail.com
30
- executables: []
58
+ executables:
59
+ - slimi
31
60
  extensions: []
32
61
  extra_rdoc_files: []
33
62
  files:
@@ -42,12 +71,27 @@ files:
42
71
  - Rakefile
43
72
  - bin/console
44
73
  - bin/setup
74
+ - exe/slimi
45
75
  - lib/slimi.rb
76
+ - lib/slimi/cli.rb
77
+ - lib/slimi/engine.rb
78
+ - lib/slimi/erb_converter.rb
46
79
  - lib/slimi/errors.rb
47
80
  - lib/slimi/filters.rb
81
+ - lib/slimi/filters/amble.rb
82
+ - lib/slimi/filters/attribute.rb
83
+ - lib/slimi/filters/base.rb
84
+ - lib/slimi/filters/control.rb
85
+ - lib/slimi/filters/do_inserter.rb
86
+ - lib/slimi/filters/embedded.rb
87
+ - lib/slimi/filters/end_inserter.rb
48
88
  - lib/slimi/filters/interpolation.rb
89
+ - lib/slimi/filters/output.rb
90
+ - lib/slimi/filters/text.rb
49
91
  - lib/slimi/filters/unposition.rb
50
92
  - lib/slimi/parser.rb
93
+ - lib/slimi/rails_template_handler.rb
94
+ - lib/slimi/railtie.rb
51
95
  - lib/slimi/range.rb
52
96
  - lib/slimi/version.rb
53
97
  - slimi.gemspec