slimi 0.1.0 → 0.4.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: 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