slimi 0.7.8 → 0.8.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: 29937913d2bf6ac3b92dcb1e94964655b79ee48d8e891890ca53618afb3f63db
4
- data.tar.gz: b15a369b677fe16bb18b837cd523dd01f317a05b0135b3f1c690bbc8f2d389fc
3
+ metadata.gz: 1f85a2a7fb060ffa915242323704ec46c52f7bc224084013c08b7832ccaaa4e2
4
+ data.tar.gz: d76831908983154a2f80f8f2ed433ee76e29d5d016eb7a9b29e2b0f292d7f54f
5
5
  SHA512:
6
- metadata.gz: 45fe23fedad4989d7b0241a28ef46de56dca448f9e67fa68cfb47058dfc73055a891c84775e53e0ca89f6dd8e6efe5ce381ad3e5df31d8d0e04300a25ca89e5c
7
- data.tar.gz: 859ee454f3ec3712540981e1ea315b9049aeda13473d42b0f5a0edeba8f186a44ad266925441f354e9c78e2414d6cbaf3cd2e9614f17b2c4c56876998bf60e32
6
+ metadata.gz: 4fac9d1b99931a94dee91a14721e0f657b1abd44f8f71110240d50bed95583793f42fee67bd5c0986857fcc356aae5efc2d20c077569c7658898bbf7af2e69b2
7
+ data.tar.gz: b21697a773720dc4604f7a21df0625df5671c52e7b4f48bcefcfc8bb5941b8f7af3b2776e0dc881d8eef218f53bb4f551d064e5180e68408d5167f5248fc58cf
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.8.0 - 2026-06-13
6
+
7
+ - Support splat attributes.
8
+
5
9
  ## 0.7.5 - 2024-07-25
6
10
 
7
11
  ### Fixed
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- slimi (0.7.8)
4
+ slimi (0.8.0)
5
5
  temple
6
6
  thor
7
7
  tilt
data/README.md CHANGED
@@ -62,7 +62,7 @@ Commands:
62
62
  - [x] Boolean attributes
63
63
  - [x] Attribute merging
64
64
  - [x] Attribute shortcuts
65
- - [ ] Splat attributes
65
+ - [x] Splat attributes
66
66
  - Plugins
67
67
  - [ ] Include partials
68
68
  - [ ] Translator/I18n
data/lib/slimi/engine.rb CHANGED
@@ -24,6 +24,7 @@ module Slimi
24
24
  use Filters::Control
25
25
  use Filters::Output
26
26
  use Filters::Text
27
+ use Filters::Splat
27
28
  html :AttributeSorter
28
29
  html :AttributeMerger
29
30
  use Filters::Attribute
data/lib/slimi/errors.rb CHANGED
@@ -62,5 +62,14 @@ module Slimi
62
62
 
63
63
  class UnknownLineIndicatorError < SlimSyntaxError
64
64
  end
65
+
66
+ class RenderError < BaseError
67
+ end
68
+
69
+ class InvalidAttributeNameError < RenderError
70
+ end
71
+
72
+ class MultipleAttributesError < RenderError
73
+ end
65
74
  end
66
75
  end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ module Filters
5
+ class Splat < Base
6
+ define_options :attr_quote, :format, :merge_attrs, :sort_attrs, use_html_safe: ''.respond_to?(:html_safe?)
7
+
8
+ # @param [Array] exp
9
+ # @return [Array]
10
+ def call(exp)
11
+ @splat_options = nil
12
+ exp = compile(exp)
13
+ if @splat_options
14
+ [:multi, [:code, "#{@splat_options} = #{splat_builder_options.inspect}"], exp]
15
+ else
16
+ exp
17
+ end
18
+ end
19
+
20
+ # @param [Array<Array>] attrs
21
+ # @return [Array]
22
+ def on_html_attrs(*attrs)
23
+ return super unless attrs.any? { |attr| splat?(attr) }
24
+
25
+ @splat_options ||= unique_name
26
+ builder = unique_name
27
+ result = [:multi]
28
+ result << [:code, "#{builder} = ::#{Builder.name}.new(#{@splat_options})"]
29
+ attrs.each do |attr|
30
+ result << compile_attribute(builder, attr)
31
+ end
32
+ result << [:dynamic, "#{builder}.to_s"]
33
+ end
34
+
35
+ private
36
+
37
+ # @param [String] builder
38
+ # @param [Array] attr
39
+ # @return [Array]
40
+ def compile_attribute(builder, attr)
41
+ return [:code, "#{builder}.splat((#{attr[2]}))"] if splat?(attr)
42
+
43
+ _, _, name, value = attr
44
+ if value[0] == :slimi && value[1] == :attrvalue
45
+ # Pass the raw expression and escape flag through, so that the Builder can
46
+ # apply the same merge / boolean / nil / escape rules as Filters::Attribute.
47
+ [:code, "#{builder}.code_attribute(#{name.to_s.inspect}, #{value[2]}, (#{value[3]}))"]
48
+ elsif value == [:multi]
49
+ # Boolean attribute, e.g. `div(*foo disabled)`.
50
+ [:code, "#{builder}.code_attribute(#{name.to_s.inspect}, false, true)"]
51
+ else
52
+ # Static or interpolated value whose escaping is compiled into the capture,
53
+ # so the Builder must not escape it again.
54
+ tmp = unique_name
55
+ [:multi,
56
+ [:capture, tmp, compile(value)],
57
+ [:code, "#{builder}.attribute(#{name.to_s.inspect}, #{tmp})"]]
58
+ end
59
+ end
60
+
61
+ # @param [Array] attr
62
+ # @return [Boolean]
63
+ def splat?(attr)
64
+ attr[0] == :slimi && attr[1] == :splat
65
+ end
66
+
67
+ # @return [Hash]
68
+ def splat_builder_options
69
+ {
70
+ attr_quote: options[:attr_quote],
71
+ format: options[:format],
72
+ merge_attrs: options[:merge_attrs],
73
+ sort_attrs: options[:sort_attrs],
74
+ use_html_safe: options[:use_html_safe]
75
+ }
76
+ end
77
+
78
+ class Builder
79
+ # https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
80
+ INVALID_ATTRIBUTE_NAME_REGEXP = %r{[ \0"'>/=]}.freeze
81
+
82
+ # @param [Hash] options
83
+ def initialize(options)
84
+ @attr_quote = options[:attr_quote]
85
+ @format = options[:format]
86
+ @merge_attrs = options[:merge_attrs] || {}
87
+ @sort_attrs = options.fetch(:sort_attrs, true)
88
+ @use_html_safe = options[:use_html_safe]
89
+ @attributes = {}
90
+ end
91
+
92
+ # Add an attribute value already rendered and escaped by the compiled template.
93
+ # @param [String] name
94
+ # @param [String] value
95
+ def attribute(name, value)
96
+ store(name, value)
97
+ end
98
+
99
+ # Add an attribute value, applying merge / boolean / nil / escape rules.
100
+ # @param [String] name
101
+ # @param [Boolean] escape
102
+ # @param [Object] value
103
+ def code_attribute(name, escape, value)
104
+ if (delimiter = @merge_attrs[name])
105
+ value = value.is_a?(::Array) ? value.flatten.map(&:to_s).reject(&:empty?).join(delimiter) : value.to_s
106
+ store(name, escape_html(escape, value)) unless value.empty?
107
+ elsif value.is_a?(::Hash)
108
+ value.each do |key, nested_value|
109
+ code_attribute("#{name}-#{key}", escape, nested_value)
110
+ end
111
+ elsif value != false && !value.nil?
112
+ store(name, value == true ? true : escape_html(escape, value.to_s))
113
+ end
114
+ end
115
+
116
+ # @param [Hash] hash
117
+ def splat(hash)
118
+ hash.each do |name, value|
119
+ code_attribute(name.to_s, true, value)
120
+ end
121
+ end
122
+
123
+ # @return [String]
124
+ def to_s
125
+ attributes = @sort_attrs ? @attributes.sort_by(&:first) : @attributes
126
+ attributes.map { |name, value| render(name, value) }.join
127
+ end
128
+
129
+ private
130
+
131
+ # @param [String] name
132
+ # @param [String, true] value
133
+ def store(name, value)
134
+ raise Errors::InvalidAttributeNameError, "Invalid attribute name '#{name}' was rendered" if INVALID_ATTRIBUTE_NAME_REGEXP.match?(name)
135
+
136
+ if @attributes.key?(name)
137
+ delimiter = @merge_attrs[name]
138
+ raise Errors::MultipleAttributesError, "Multiple #{name} attributes specified" unless delimiter
139
+
140
+ @attributes[name] = "#{@attributes[name]}#{delimiter}#{value}"
141
+ else
142
+ @attributes[name] = value
143
+ end
144
+ end
145
+
146
+ # @param [String] name
147
+ # @param [String, true] value
148
+ # @return [String]
149
+ def render(name, value)
150
+ if value == true
151
+ if @format == :xhtml
152
+ " #{name}=#{@attr_quote}#{@attr_quote}"
153
+ else
154
+ " #{name}"
155
+ end
156
+ else
157
+ " #{name}=#{@attr_quote}#{value}#{@attr_quote}"
158
+ end
159
+ end
160
+
161
+ # @param [Boolean] escape
162
+ # @param [String] string
163
+ # @return [String]
164
+ def escape_html(escape, string)
165
+ return string unless escape
166
+
167
+ if @use_html_safe
168
+ ::Temple::Utils.escape_html_safe(string)
169
+ else
170
+ ::Temple::Utils.escape_html(string)
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
data/lib/slimi/filters.rb CHANGED
@@ -11,6 +11,7 @@ module Slimi
11
11
  autoload :EndInserter, 'slimi/filters/end_inserter'
12
12
  autoload :Interpolation, 'slimi/filters/interpolation'
13
13
  autoload :Output, 'slimi/filters/output'
14
+ autoload :Splat, 'slimi/filters/splat'
14
15
  autoload :Text, 'slimi/filters/text'
15
16
  autoload :Unposition, 'slimi/filters/unposition'
16
17
  end
data/lib/slimi/parser.rb CHANGED
@@ -38,6 +38,7 @@ module Slimi
38
38
  @attribute_shortcut_regexp = factory.attribute_shortcut_regexp
39
39
  @attribute_delimiter_regexp = factory.attribute_delimiter_regexp
40
40
  @quoted_attribute_regexp = factory.quoted_attribute_regexp
41
+ @splat_attribute_regexp = factory.splat_attribute_regexp
41
42
  @tag_name_regexp = factory.tag_name_regexp
42
43
  @attribute_name_regexp = factory.attribute_name_regexp
43
44
  @ruby_attribute_regexp = factory.ruby_attribute_regexp
@@ -259,9 +260,13 @@ module Slimi
259
260
  attribute_delimiter_closing_part_regexp = /[ \t]*#{attribute_delimiter_closing_regexp}/
260
261
  end
261
262
 
262
- # TODO: Support splat attributes.
263
263
  loop do
264
- if @scanner.skip(@quoted_attribute_regexp)
264
+ if @scanner.skip(@splat_attribute_regexp)
265
+ charpos = @scanner.charpos
266
+ code = parse_ruby_attribute_value(attribute_delimiter_closing)
267
+ syntax_error!(Errors::InvalidEmptyAttributeError) if code.empty?
268
+ attributes << [:slimi, :position, charpos, charpos + code.length, [:slimi, :splat, code]]
269
+ elsif @scanner.skip(@quoted_attribute_regexp)
265
270
  attribute_name = @scanner[1]
266
271
  escape = @scanner[2].empty?
267
272
  quote = @scanner[3]
@@ -676,6 +681,11 @@ module Slimi
676
681
  /#{attribute_name_regexp}[ \t]*=(=?)[ \t]*("|')/
677
682
  end
678
683
 
684
+ # @return [Regexp]
685
+ def splat_attribute_regexp
686
+ /[ \t]*\*(?=[^\s])/
687
+ end
688
+
679
689
  # @return [Regexp]
680
690
  def ruby_attribute_delimiter_regexp
681
691
  ::Regexp.union(@ruby_attribute_delimiters.keys)
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.7.8'
4
+ VERSION = '0.8.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slimi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.8
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryo Nakamura
@@ -85,6 +85,7 @@ files:
85
85
  - lib/slimi/filters/end_inserter.rb
86
86
  - lib/slimi/filters/interpolation.rb
87
87
  - lib/slimi/filters/output.rb
88
+ - lib/slimi/filters/splat.rb
88
89
  - lib/slimi/filters/text.rb
89
90
  - lib/slimi/filters/unposition.rb
90
91
  - lib/slimi/parser.rb