slimi 0.1.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b30b237b1d1e68744ac90e65d148397a5099a4702482286768afd6db8409a484
4
- data.tar.gz: 055a9e297a5467e35c01d7468ab5d5083df89ef8762bb8c92d3074956751675b
3
+ metadata.gz: 05cfa3a429ac2da0e1241e65e69a40f9171079cddb9451a6d7186726a5cb498c
4
+ data.tar.gz: 68826c3fe78dc87bfd704cc38a9222da1adeed36a8c0d0e903bb4f3077ed698d
5
5
  SHA512:
6
- metadata.gz: 43d27cd7dc20e1de1d744ae87110bec52727d30786c252cd02de8f31cdc7f86fe4d7cc08d005b5c430c73d1ab68176dc0a73e5ae2376722febfb12f4193dfd68
7
- data.tar.gz: 4ab35e06cb00a9b50cbca0dd34797ad281777fba885141cf41709c6e15f24c8c87a99b55d466ce8c31546a464d507e1a476a3b1d6aa5345a770572a9151b5cd0
6
+ metadata.gz: 9c729933bc060a21df2e55db525d28a8e519526995ec5b7fb3defd17ecb11c71fc7e6ec664c162396f6a8be9175549dd842c7c5f9be76c68afadadc9e3e442be
7
+ data.tar.gz: 60b3b1ad54a01bcbc2154e83b11efe3ac1b3fb5723773f04aabff9d1957dec60cb8afd431630b78dc8e13fa86d109d86a78c66005bafb10c23e0965368d9bd80
data/.rubocop.yml CHANGED
@@ -6,6 +6,9 @@ AllCops:
6
6
  Layout/LineLength:
7
7
  Enabled: false
8
8
 
9
+ Lint/InterpolationCheck:
10
+ Enabled: false
11
+
9
12
  Metrics:
10
13
  Enabled: false
11
14
 
data/CHANGELOG.md CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.4.0 - 2021-12-25
6
+
7
+ ### Added
8
+
9
+ - Support :file option on parser for showing correct file path on syntax error.
10
+
11
+ ### Fixed
12
+
13
+ - Fix NameError on unknown line indicator.
14
+ - Fix bug that default parser options are not used.
15
+
16
+ ## 0.3.0 - 2021-12-24
17
+
18
+ ### Added
19
+
20
+ - Support unquoted attributes.
21
+
22
+ ### Fixed
23
+
24
+ - Fix bug about blank line handling.
25
+
26
+ ## 0.2.0 - 2021-12-23
27
+
28
+ ### Added
29
+
30
+ - Support embedded template.
31
+ - Show useful message at syntax error.
32
+
33
+ ## 0.1.1 - 2021-12-21
34
+
35
+ ### Fixed
36
+
37
+ - Fix bug that Slimi::Interpolation was not working.
38
+
5
39
  ## 0.1.0 - 2021-12-20
6
40
 
41
+ ### Added
42
+
7
43
  - Initial release.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- slimi (0.1.0)
4
+ slimi (0.4.0)
5
5
  temple
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # Slimi
2
2
 
3
3
  [![test](https://github.com/r7kamura/slimi/actions/workflows/test.yml/badge.svg)](https://github.com/r7kamura/slimi/actions/workflows/test.yml)
4
+ [![](https://badge.fury.io/rb/slimi.svg)](https://rubygems.org/gems/slimi)
4
5
 
5
6
  Yet another implementation for [Slim](https://github.com/slim-template/slim) template language.
6
7
 
7
- The current goal is to generate AST with detailed location information so that the embedded Ruby code in slim templates can be auto-corrected by Linter such as RuboCop.
8
+ Slimi is used by [Slimcop](https://github.com/r7kamura/slimcop).
8
9
 
9
10
  ## Installation
10
11
 
@@ -29,21 +30,3 @@ gem install slimi
29
30
  ## Usage
30
31
 
31
32
  TBD.
32
-
33
- ## Development
34
-
35
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
36
-
37
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
38
-
39
- ## Contributing
40
-
41
- Bug reports and pull requests are welcome on GitHub at https://github.com/r7kamura/slimi. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/r7kamura/slimi/blob/main/CODE_OF_CONDUCT.md).
42
-
43
- ## License
44
-
45
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
46
-
47
- ## Code of Conduct
48
-
49
- Everyone interacting in the Slimi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/r7kamura/slimi/blob/main/CODE_OF_CONDUCT.md).
data/lib/slimi/errors.rb CHANGED
@@ -5,25 +5,59 @@ module Slimi
5
5
  class BaseError < StandardError
6
6
  end
7
7
 
8
- class ParserError < BaseError
8
+ class SlimSyntaxError < BaseError
9
+ # @param [Integer] column
10
+ # @param [String] file_path
11
+ # @param [String] line
12
+ # @param [Integer] line_number
13
+ def initialize(column:, file_path:, line:, line_number:)
14
+ super()
15
+ @column = column
16
+ @file_path = file_path
17
+ @line = line
18
+ @line_number = line_number
19
+ end
20
+
21
+ # @note Override.
22
+ # @return [String]
23
+ def to_s
24
+ <<~TEXT
25
+ #{error_type} at #{@file_path}:#{@line_number}:#{@column}
26
+ #{@line}
27
+ #{' ' * (@column - 1)}^
28
+ TEXT
29
+ end
30
+
31
+ private
32
+
33
+ # @return [String]
34
+ def error_type
35
+ self.class.to_s.split('::').last
36
+ end
37
+ end
38
+
39
+ class InvalidEmptyAttributeError < SlimSyntaxError
40
+ end
41
+
42
+ class LineEndingNotFoundError < SlimSyntaxError
9
43
  end
10
44
 
11
- class LineEndingNotFoundError < ParserError
45
+ class MalformedIndentationError < SlimSyntaxError
12
46
  end
13
47
 
14
- class MalformedIndentationError < ParserError
48
+ class RubyAttributeClosingDelimiterNotFoundError < SlimSyntaxError
15
49
  end
16
50
 
17
- class UnexpectedEosError < ParserError
51
+ class UnexpectedEosError < SlimSyntaxError
18
52
  end
19
53
 
20
- class UnexpectedIndentationError < ParserError
54
+ class UnexpectedIndentationError < SlimSyntaxError
21
55
  end
22
56
 
23
- class UnexpectedTextAfterClosedTagError < ParserError
57
+ class UnexpectedTextAfterClosedTagError < SlimSyntaxError
24
58
  end
25
59
 
26
- class UnknownLineIndicator < ParserError
60
+ class UnknownLineIndicatorError < SlimSyntaxError
27
61
  end
28
62
  end
29
63
  end
@@ -1,34 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'strscan'
4
- require 'temple'
5
4
 
6
5
  module Slimi
7
6
  module Filters
8
- class Interpolation < ::Temple::Filter
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
+
9
30
  # @param [Integer] begin_
10
31
  # @param [Integer] end_
11
32
  # @return [Array] S-expression.
12
- def on_slimi_interpolate(begin_, end_, string)
33
+ def on_slimi_interpolate(begin_, _end_, string)
13
34
  block = [:multi]
14
35
  scanner = ::StringScanner.new(string)
15
36
  until scanner.eos?
16
37
  charpos = scanner.charpos
17
- if (value = scanner.scan(/\\#\{/) || scanner.scan(/([#\\]?[^#\\]*([#\\][^\\\#{][^#\\]*)*)/))
38
+ if (value = scanner.scan(/\\#\{/))
18
39
  block << [:static, value]
19
40
  elsif scanner.scan(/#\{((?>[^{}]|(\{(?>[^{}]|\g<1>)*\}))*)\}/)
20
41
  code = scanner[1]
21
42
  begin2 = begin_ + charpos + 2
22
- end2 = end_ + scanner.charpos - 1
23
43
  if code.start_with?('{') && code.end_with?('}')
24
44
  escape = true
25
45
  code = code[1..-2]
26
46
  begin2 -= 1
27
- end2 -= 1
28
47
  else
29
48
  escape = false
30
49
  end
31
- block << [:slimi, :position, begin2, end2, [:slim, :output, escape, code, [:multi]]]
50
+ block << [:slimi, :position, begin2, begin2 + code.length, [:slim, :output, escape, code, [:multi]]]
51
+ elsif (value = scanner.scan(/([#\\]?[^#\\]*([#\\][^\\\#{][^#\\]*)*)/)) # rubocop:disable Lint/DuplicateBranch
52
+ block << [:static, value]
32
53
  end
33
54
  end
34
55
  block
data/lib/slimi/parser.rb CHANGED
@@ -6,22 +6,30 @@ 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
  '[' => ']',
12
13
  '{' => '}'
13
14
  },
15
+ code_attr_delims: {
16
+ '(' => ')',
17
+ '[' => ']',
18
+ '{' => '}'
19
+ },
14
20
  shortcut: {
15
21
  '#' => { attr: 'id' },
16
22
  '.' => { attr: 'class' }
17
23
  }
18
24
  )
19
25
 
20
- def initialize(options = {})
26
+ def initialize(_options = {})
21
27
  super
28
+ @file_path = options[:file] || '(__TEMPLATE__)'
22
29
  factory = Factory.new(
23
30
  attribute_delimiters: options[:attr_list_delims] || {},
24
31
  default_tag: options[:default_tag] || 'div',
32
+ ruby_attribute_delimiters: options[:code_attr_delims] || {},
25
33
  shortcut: options[:shortcut] || {}
26
34
  )
27
35
  @attribute_delimiters = factory.attribute_delimiters
@@ -32,6 +40,10 @@ module Slimi
32
40
  @quoted_attribute_regexp = factory.quoted_attribute_regexp
33
41
  @tag_name_regexp = factory.tag_name_regexp
34
42
  @attribute_name_regexp = factory.attribute_name_regexp
43
+ @ruby_attribute_regexp = factory.ruby_attribute_regexp
44
+ @ruby_attribute_delimiter_regexp = factory.ruby_attribute_delimiter_regexp
45
+ @ruby_attribute_delimiters = factory.ruby_attribute_delimiters
46
+ @embedded_template_regexp = factory.embedded_template_regexp
35
47
  end
36
48
 
37
49
  def call(source)
@@ -45,19 +57,32 @@ module Slimi
45
57
  private
46
58
 
47
59
  def parse_block
60
+ return if parse_blank_line
61
+
48
62
  parse_indent
49
63
 
50
- parse_line_ending ||
51
- parse_html_comment ||
64
+ parse_html_comment ||
52
65
  parse_html_conditional_comment ||
53
66
  parse_slim_comment_block ||
54
67
  parse_verbatim_text_block ||
55
68
  parse_inline_html ||
56
69
  parse_code_block ||
57
70
  parse_output_block ||
71
+ parse_embedded_template ||
58
72
  parse_doctype ||
59
73
  parse_tag ||
60
- raise(Errors::UnknownLineIndicatorError)
74
+ syntax_error!(Errors::UnknownLineIndicatorError)
75
+ end
76
+
77
+ # Parse blank line.
78
+ # @return [Boolean] True if it could parse a blank line.
79
+ def parse_blank_line
80
+ if @scanner.skip(/[ \t]*$/)
81
+ parse_line_ending
82
+ true
83
+ else
84
+ false
85
+ end
61
86
  end
62
87
 
63
88
  def parse_indent
@@ -66,7 +91,7 @@ module Slimi
66
91
  @indents << indent if @indents.empty?
67
92
 
68
93
  if indent > @indents.last
69
- raise Errors::UnexpectedIndentationError unless expecting_indentation?
94
+ syntax_error!(Errors::UnexpectedIndentationError) unless expecting_indentation?
70
95
 
71
96
  @indents << indent
72
97
  else
@@ -77,10 +102,22 @@ module Slimi
77
102
  @stacks.pop
78
103
  end
79
104
 
80
- raise Errors::MalformedIndentationError if indent != @indents.last
105
+ syntax_error!(Errors::MalformedIndentationError) if indent != @indents.last
81
106
  end
82
107
  end
83
108
 
109
+ # Parse embedded template lines.
110
+ # e.g.
111
+ # ruby:
112
+ # a = b + c
113
+ def parse_embedded_template
114
+ return unless @scanner.skip(@embedded_template_regexp)
115
+
116
+ embedded_template_engine_name = @scanner[1]
117
+ attributes = parse_attributes
118
+ @stacks.last << [:slim, :embedded, embedded_template_engine_name, parse_text_block, attributes]
119
+ end
120
+
84
121
  # @return [Boolean]
85
122
  def parse_tag
86
123
  parse_tag_inner && expect_line_ending
@@ -117,7 +154,7 @@ module Slimi
117
154
  @stacks.last << [:static, ' '] if with_trailing_white_space2
118
155
  @stacks << block
119
156
  elsif @scanner.skip(%r{[ \t]*/[ \t]*})
120
- raise Errors::UnexpectedTextAfterClosedTagError unless @scanner.match?(/\r?\n/)
157
+ syntax_error!(Errors::UnexpectedTextAfterClosedTagError) unless @scanner.match?(/\r?\n/)
121
158
  else
122
159
  @scanner.skip(/[ \t]+/)
123
160
  tag << [:slim, :text, :inline, parse_text_block]
@@ -163,8 +200,6 @@ module Slimi
163
200
  marker = @scanner[1]
164
201
  attribute_value = @scanner[2]
165
202
  attribute_names = @attribute_shortcuts[marker]
166
- raise 'Illegal shortcut' unless attribute_names
167
-
168
203
  attribute_names.map do |attribute_name|
169
204
  result << [:html, :attr, attribute_name.to_s, [:static, attribute_value]]
170
205
  end
@@ -218,12 +253,19 @@ module Slimi
218
253
  attribute_delimiter_closing_part_regexp = /[ \t]*#{attribute_delimiter_closing_regexp}/
219
254
  end
220
255
 
256
+ # TODO: Support splat attributes.
221
257
  loop do
222
258
  if @scanner.skip(@quoted_attribute_regexp)
223
259
  attribute_name = @scanner[1]
224
260
  escape = @scanner[2].empty?
225
261
  quote = @scanner[3]
226
262
  attributes << [:html, :attr, attribute_name, [:escape, escape, parse_quoted_attribute_value(quote)]]
263
+ elsif @scanner.skip(@ruby_attribute_regexp)
264
+ attribute_name = @scanner[1]
265
+ escape = @scanner[2].empty?
266
+ attribute_value = parse_ruby_attribute_value(attribute_delimiter_closing)
267
+ syntax_error!(Errors::InvalidEmptyAttributeError) if attribute_value.empty?
268
+ attributes << [:html, :attr, attribute_name, [:slim, :attrvalue, escape, attribute_value]]
227
269
  elsif !attribute_delimiter_closing_part_regexp
228
270
  break
229
271
  elsif @scanner.skip(boolean_attribute_regexp)
@@ -238,6 +280,46 @@ module Slimi
238
280
  attributes
239
281
  end
240
282
 
283
+ # Parse Ruby attribute value part.
284
+ # e.g. div class=foo
285
+ # ^^^
286
+ # `- Ruby attribute value
287
+ # @param [String] attribute_delimiter_closing
288
+ # @return [String]
289
+ def parse_ruby_attribute_value(attribute_delimiter_closing)
290
+ ending_regexp = /\s/
291
+ ending_regexp = ::Regexp.union(ending_regexp, attribute_delimiter_closing) if attribute_delimiter_closing
292
+ count = 0
293
+ attribute_value = +''
294
+ opening_delimiter = nil
295
+ closing_delimiter = nil
296
+ loop do
297
+ break if count.zero? && @scanner.match?(ending_regexp)
298
+
299
+ if @scanner.skip(/([,\\])\r?\n/)
300
+ attribute_value << @scanner[1] << "\n"
301
+ else
302
+ if count.positive?
303
+ if opening_delimiter && @scanner.skip(::Regexp.escape(opening_delimiter))
304
+ count += 1
305
+ elsif closing_delimiter && @scanner.skip(::Regexp.escape(closing_delimiter))
306
+ count -= 1
307
+ end
308
+ elsif @scanner.skip(@ruby_attribute_delimiter_regexp)
309
+ count = 1
310
+ opening_delimiter = @scanner.matched
311
+ closing_delimiter = @ruby_attribute_delimiters[opening_delimiter]
312
+ end
313
+ if (character = @scanner.scan(/./))
314
+ attribute_value << character
315
+ end
316
+ end
317
+ end
318
+ syntax_error!(Errors::RubyAttributeClosingDelimiterNotFoundError) if count != 0
319
+
320
+ attribute_value
321
+ end
322
+
241
323
  # @return [Boolean]
242
324
  def parse_html_comment
243
325
  if @scanner.skip(%r{/!})
@@ -452,7 +534,7 @@ module Slimi
452
534
  result = +''
453
535
  result << @scanner.scan(/.*/)
454
536
  while result.end_with?(',') || result.end_with?('\\')
455
- raise Errors::UnexpectedEosError unless @scanner.scan(/\r?\n/)
537
+ syntax_error!(Errors::UnexpectedEosError) unless @scanner.scan(/\r?\n/)
456
538
 
457
539
  result << "\n"
458
540
  result << @scanner.scan(/.*/)
@@ -468,17 +550,47 @@ module Slimi
468
550
  [:slimi, :position, begin_, end_, inner]
469
551
  end
470
552
 
553
+ # @param [Class] syntax_error_class A child class of Slimi::Errors::SlimSyntaxError.
554
+ # @raise [Slimi::Errors::SlimSyntaxError]
555
+ def syntax_error!(syntax_error_class)
556
+ range = Range.new(index: @scanner.charpos, source: @scanner.string)
557
+ raise syntax_error_class.new(
558
+ column: range.column,
559
+ file_path: @file_path,
560
+ line: range.line,
561
+ line_number: range.line_number
562
+ )
563
+ end
564
+
471
565
  # Convert human-friendly options into machine-friendly objects.
472
566
  class Factory
567
+ EMBEDDED_TEMPLAE_ENGINE_NAMES = %w[
568
+ coffee
569
+ css
570
+ javascript
571
+ less
572
+ markdown
573
+ rdoc
574
+ ruby
575
+ sass
576
+ scss
577
+ textile
578
+ ].freeze
579
+
473
580
  # @return [Hash]
474
581
  attr_reader :attribute_delimiters
475
582
 
583
+ # @return [Hash]
584
+ attr_reader :ruby_attribute_delimiters
585
+
476
586
  # @param [Hash] attribute_delimiters
477
587
  # @param [String] default_tag
588
+ # @param [Hash] ruby_attribute_delimiters
478
589
  # @param [Hash] shortcut
479
- def initialize(attribute_delimiters:, default_tag:, shortcut:)
590
+ def initialize(attribute_delimiters:, default_tag:, ruby_attribute_delimiters:, shortcut:)
480
591
  @attribute_delimiters = attribute_delimiters
481
592
  @default_tag = default_tag
593
+ @ruby_attribute_delimiters = ruby_attribute_delimiters
482
594
  @shortcut = shortcut
483
595
  end
484
596
 
@@ -534,11 +646,26 @@ module Slimi
534
646
  %r{(#{markers_regexp}+)((?:\p{Word}|-|/\d+|:(\w|-)+)*)}
535
647
  end
536
648
 
649
+ # @return [Regexp]
650
+ def ruby_attribute_regexp
651
+ /#{attribute_name_regexp}[ \t]*=(=?)[ \t]*/
652
+ end
653
+
654
+ # @return [Regexp]
655
+ def embedded_template_regexp
656
+ /(#{::Regexp.union(EMBEDDED_TEMPLAE_ENGINE_NAMES)})(?:[ \t]*(?:(.*)))?:([ \t]*)/
657
+ end
658
+
537
659
  # @return [Regexp]
538
660
  def quoted_attribute_regexp
539
661
  /#{attribute_name_regexp}[ \t]*=(=?)[ \t]*("|')/
540
662
  end
541
663
 
664
+ # @return [Regexp]
665
+ def ruby_attribute_delimiter_regexp
666
+ ::Regexp.union(@ruby_attribute_delimiters.keys)
667
+ end
668
+
542
669
  # @return [Regexp] Pattern that matches to tag header part.
543
670
  def tag_name_regexp
544
671
  markers = tag_shortcuts.keys.sort_by { |marker| -marker.size }
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimi
4
+ # Get line-based information from source code and its index.
5
+ class Range
6
+ # @param [Integer] index 0-indexed per-character index.
7
+ # @param [String] source
8
+ def initialize(index:, source:)
9
+ @index = index
10
+ @source = source
11
+ end
12
+
13
+ # @return [Integer] 1-indexed column index.
14
+ def column
15
+ (@index - line_beginning_index) + 1
16
+ end
17
+
18
+ def line
19
+ @source[line_beginning_index...line_ending_index]
20
+ end
21
+
22
+ # @return [Integer] 1-indexed line index.
23
+ def line_number
24
+ @source[0..@index].scan(/^/).length
25
+ end
26
+
27
+ private
28
+
29
+ # @return [Integer]
30
+ def line_beginning_index
31
+ @source.rindex(/^/, @index) || 0
32
+ end
33
+
34
+ # @return [Integer]
35
+ def line_ending_index
36
+ @source.index(/$/, @index)
37
+ end
38
+ end
39
+ 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.1.0'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/slimi.rb CHANGED
@@ -6,4 +6,5 @@ module Slimi
6
6
  autoload :Errors, 'slimi/errors'
7
7
  autoload :Filters, 'slimi/filters'
8
8
  autoload :Parser, 'slimi/parser'
9
+ autoload :Range, 'slimi/range'
9
10
  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.1.0
4
+ version: 0.4.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-19 00:00:00.000000000 Z
11
+ date: 2021-12-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: temple
@@ -48,6 +48,7 @@ files:
48
48
  - lib/slimi/filters/interpolation.rb
49
49
  - lib/slimi/filters/unposition.rb
50
50
  - lib/slimi/parser.rb
51
+ - lib/slimi/range.rb
51
52
  - lib/slimi/version.rb
52
53
  - slimi.gemspec
53
54
  homepage: https://github.com/r7kamura/slimi