yoga 0.0.1 → 0.2.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
  SHA1:
3
- metadata.gz: a6a3b73d2db5477d58eab02a2179a42888bc0ba2
4
- data.tar.gz: 49c440508979558a68d01fb44142b267eae7e176
3
+ metadata.gz: 4999074c914d1566ee18799548c1d437ff5ad83b
4
+ data.tar.gz: 998165e1b73a1d6fef8350b7a5b5f15bfb6295ee
5
5
  SHA512:
6
- metadata.gz: 8a9cac22e56f5c19de82a070d83e94a3a825c5f4666c495d5e9dbe0d5bea87543534acbade0b545ed929985178f4508559a4cb5d3ae5f9278f5c23b3add27231
7
- data.tar.gz: 3e374cd507d6e4d44429300ea4bea0d4f4752c79570ccc6d8117a5ba280193ee2ff1ef0acce064cb4af13247e6b719c141d851aa8ef7026d8dbf2adc5c8eab37
6
+ metadata.gz: 3f0f59113580eac2ac0065651837f3e0bef3044585e48ff61e92c8b9a3cc21faac33a45ab0a98ed908d80de05adcc5bafb66a1e291ca6150c0641c25fbe25947
7
+ data.tar.gz: 9c86dc7896453ebe2e6c5bdb060f664757f9246af44a4e3ff556789b5c0851fe7648f53b8c493d3254babea180195e356226c1d544933b1382d2875e359fbf26
data/.gitignore CHANGED
@@ -6,8 +6,9 @@
6
6
  Gemfile.lock
7
7
  InstalledFiles
8
8
  _yardoc
9
- coverage
9
+ coverage/
10
10
  doc/
11
+ profile/
11
12
  lib/bundler/man
12
13
  pkg
13
14
  rdoc
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --require spec_helper
3
+ --require pry
data/.rubocop.yml ADDED
@@ -0,0 +1,40 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.3
3
+
4
+ Metrics/LineLength:
5
+ Enabled: false
6
+ Metrics/MethodLength:
7
+ Max: 15
8
+ Metrics/BlockLength:
9
+ Exclude:
10
+ - 'spec/*.rb'
11
+ - 'spec/**/*.rb'
12
+ Metrics/AbcSize:
13
+ Max: 25
14
+
15
+ Style/AlignParameters:
16
+ EnforcedStyle: with_fixed_indentation
17
+ Style/SignalException:
18
+ EnforcedStyle: only_fail
19
+ Style/AccessModifierIndentation:
20
+ EnforcedStyle: outdent
21
+ Style/Documentation:
22
+ Enabled: false
23
+ Style/Encoding:
24
+ Enabled: true
25
+ EnforcedStyle: always
26
+ AutoCorrectEncodingComment: "# encoding: utf-8\n"
27
+ Style/RegexpLiteral:
28
+ EnforcedStyle: mixed
29
+ Style/StringLiterals:
30
+ EnforcedStyle: double_quotes
31
+ Style/AlignHash:
32
+ EnforcedLastArgumentHashStyle: ignore_implicit
33
+ Style/AlignArray:
34
+ Enabled: false
35
+ Style/IndentArray:
36
+ Enabled: false
37
+ Style/Alias:
38
+ EnforcedStyle: prefer_alias_method
39
+ Style/ParallelAssignment:
40
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.4.0
4
+ - 2.2.2
5
+ - 2.1.0
6
+ script:
7
+ - bundle exec rubocop --format clang
8
+ - bundle exec rspec spec
data/.yardopts ADDED
@@ -0,0 +1,3 @@
1
+ -m markdown
2
+ --protected
3
+ --private
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at me@medcat.me. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile CHANGED
@@ -1,4 +1,11 @@
1
- source 'https://rubygems.org'
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ source "https://rubygems.org"
2
5
 
3
6
  # Specify your gem's dependencies in yoga.gemspec
4
7
  gemspec
8
+
9
+ gem "mixture"
10
+ gem "pry"
11
+ gem "pry-stack_explorer"
data/README.md CHANGED
@@ -1,12 +1,16 @@
1
1
  # Yoga
2
2
 
3
- TODO: Write a gem description
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/yoga`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
4
6
 
5
7
  ## Installation
6
8
 
7
9
  Add this line to your application's Gemfile:
8
10
 
9
- gem 'yoga'
11
+ ```ruby
12
+ gem 'yoga'
13
+ ```
10
14
 
11
15
  And then execute:
12
16
 
@@ -20,10 +24,13 @@ Or install it yourself as:
20
24
 
21
25
  TODO: Write usage instructions here
22
26
 
27
+ ## Development
28
+
29
+ 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.
30
+
31
+ 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 tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
23
33
  ## Contributing
24
34
 
25
- 1. Fork it ( https://github.com/[my-github-username]/yoga/fork )
26
- 2. Create your feature branch (`git checkout -b my-new-feature`)
27
- 3. Commit your changes (`git commit -am 'Add some feature'`)
28
- 4. Push to the branch (`git push origin my-new-feature`)
29
- 5. Create a new Pull Request
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/yoga. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
36
+
data/Rakefile CHANGED
@@ -1,2 +1,9 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
1
4
  require "bundler/gem_tasks"
5
+ require "rspec/core/rake_task"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
2
8
 
9
+ task default: :spec
data/lib/yoga.rb CHANGED
@@ -1,5 +1,16 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
1
4
  require "yoga/version"
5
+ require "yoga/errors"
6
+ require "yoga/utils"
7
+ require "yoga/location"
8
+ require "yoga/node"
9
+ require "yoga/parser"
10
+ require "yoga/scanner"
11
+ require "yoga/token"
2
12
 
13
+ # A parser and scanner helper.
3
14
  module Yoga
4
15
  # Your code goes here...
5
16
  end
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Yoga
5
+ # An error originating from the Yoga class. All behavior here is private
6
+ # to the Yoga module.
7
+ #
8
+ # @api private
9
+ # @private
10
+ class Error < ::StandardError
11
+ # Initialize the error.
12
+ #
13
+ # @api private
14
+ def initialize(data)
15
+ data.each { |k, v| instance_variable_set(:"@#{k}", v) }
16
+ super(generate_message)
17
+ end
18
+
19
+ private
20
+
21
+ # Generates a message for the exception.
22
+ #
23
+ # @return [::String]
24
+ def generate_message
25
+ message
26
+ end
27
+ end
28
+
29
+ # An error that has a corresponding location information.
30
+ #
31
+ # @api private
32
+ class LocationError < Error
33
+ attr_reader :location
34
+ end
35
+
36
+ # An error that occurred with parsing.
37
+ #
38
+ # @api private
39
+ class ParseError < LocationError; end
40
+
41
+ # An unexpected token was encountered while parsing.
42
+ #
43
+ # @api private
44
+ class UnexpectedTokenError < ParseError
45
+ # (see Error#generate_message)
46
+ private def generate_message
47
+ "Unexpected #{@got}, expected one of #{@expected.to_a.join(', ')} " \
48
+ "at #{@location}"
49
+ end
50
+ end
51
+
52
+ # A shift was requested, but was invalid.
53
+ #
54
+ # @api private
55
+ class InvalidShiftError < ParseError
56
+ # (see Error#generate_message)
57
+ private def generate_message
58
+ "Parser shifted, but no tokens remain at #{@location}"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,178 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Yoga
5
+ # A location in a file. This can be used for debugging purposes.
6
+ class Location
7
+ # The file the location is positioned in. This should just be a string
8
+ # that uniquely identifies the file from all possible values of the
9
+ # location.
10
+ #
11
+ # @return [::String]
12
+ attr_reader :file
13
+
14
+ # The line the location is on. This can be a range of lines, or a single
15
+ # line.
16
+ #
17
+ # @return [::Range]
18
+ attr_reader :line
19
+
20
+ # The column the location on. This can be a range of columns, or a single
21
+ # column.
22
+ #
23
+ # @return [::Range]
24
+ attr_reader :column
25
+
26
+ # A "hash" of the location. This is a number that is meant to roughly
27
+ # represent the value of this location. Used primarily for the Hash
28
+ # class.
29
+ #
30
+ # @api private
31
+ # @return [::Numeric]
32
+ attr_reader :hash
33
+
34
+ # Creates a "default" location. This is a location that can
35
+ # be given if the location is unknown.
36
+ #
37
+ # @param file [::String] The file. See {#file}.
38
+ # @return [Location]
39
+ def self.default(file = "<unknown>")
40
+ new(file, 0..0, 0..0)
41
+ end
42
+
43
+ # Initialize the location with the given information.
44
+ #
45
+ # @param file [::String] The file. See {#file}.
46
+ # @param line [::Range, ::Numeric] The line. See {#line}.
47
+ # @param column [::Range, ::Numeric] The column. See {#column}.
48
+ def initialize(file, line, column)
49
+ @file = file.freeze
50
+ @line = ensure_range(line).freeze
51
+ @column = ensure_range(column).freeze
52
+ @hash = [@file, @line, @column].hash
53
+ freeze
54
+ end
55
+
56
+ # Creates a string representing this location in a file.
57
+ #
58
+ # @return [::String]
59
+ def to_s
60
+ "#{file}:#{range(line)}.#{range(column)}"
61
+ end
62
+
63
+ # Pretty inspect.
64
+ #
65
+ # @return [::String]
66
+ def inspect
67
+ "#<#{self.class} #{self}>"
68
+ end
69
+
70
+ # Determines if the other object is equal to the current instance. This
71
+ # checks `equal?` first to determine if they are strict equals; otherwise,
72
+ # it checks if the other is a {Location}. If it is, it checks that the
73
+ # properties are equal.
74
+ #
75
+ # @example
76
+ # a # => #<Location file="a" line=1..1 column=5..20>
77
+ # a == a # => true
78
+ # @example
79
+ # a # => #<Location file="a" line=1..1 column=5..20>
80
+ # b # => #<Location file="a" line=1..1 column=5..20>
81
+ # a == b # => true
82
+ # @example
83
+ # a # => #<Location file="a" line=1..1 column=5..20>
84
+ # b # => #<Location file="b" line=1..1 column=6..20>
85
+ # a == b # => false
86
+ #
87
+ # @param other [Object]
88
+ # @return [Boolean]
89
+ def ==(other)
90
+ equal?(other) || other.is_a?(Location) && @file == other.file &&
91
+ @line == other.line && @column == other.column
92
+ end
93
+
94
+ # Unions this location with another location. This creates a new location,
95
+ # with the two locations combined. A conflict in the file name causes an
96
+ # error to be raised.
97
+ #
98
+ # @example
99
+ # a = Location.new("a", 1..1, 5..10)
100
+ # a.union(a) # => #<Location file="a" line=1..1 column=5..10>
101
+ # a.union(a) == a # => true
102
+ # a.union(a).equal?(a) # => false
103
+ # @example
104
+ # a = Location.new("a", 1..1, 5..10)
105
+ # b = Location.new("a", 1..1, 6..20)
106
+ # a.union(b) # => #<Location file="a" line=1..1 column=5..20>
107
+ # @example
108
+ # a = Location.new("a", 1..5, 3..10)
109
+ # b = Location.new("b", 1..3, 2..20)
110
+ # a.union(b) # => #<Location file="a" line=1..5 column=2..20>
111
+ # @example
112
+ # a # => #<Location ...>
113
+ # b # => #<Location ...>
114
+ # a.union(b) == b.union(a)
115
+ #
116
+ # @raise [::ArgumentError] if other isn't a {Location}.
117
+ # @raise [::ArgumentError] if other's file isn't the receiver's file.
118
+ # @param others [Location]
119
+ # @return [Location]
120
+ def union(*others)
121
+ others.each do |other|
122
+ fail ArgumentError, "Expected #{self.class}, got #{other.class}" \
123
+ unless other.is_a?(Location)
124
+ fail ArgumentError, "Expected other to have the same file" unless
125
+ file == other.file
126
+ end
127
+
128
+ line = construct_range([@line, *others.map(&:line)])
129
+ column = construct_range([@column, *others.map(&:column)])
130
+
131
+ Location.new(@file, line, column)
132
+ end
133
+
134
+ alias_method :|, :union
135
+
136
+ private
137
+
138
+ # Creates a range from a list of ranges. This takes the lowest starting
139
+ # value, and the greatest ending value, and creates a new range from that.
140
+ #
141
+ # @param from [<::Range<::Numeric>>]
142
+ # @return [::Range<::Numeric>]
143
+ def construct_range(from)
144
+ first = from.map(&:first).min
145
+ last = from.map(&:last).max
146
+
147
+ first..last
148
+ end
149
+
150
+ # Ensures that the given value is a range. If it's numeric, it's turned
151
+ # into a range with the same starting and ending values. If it's a range,
152
+ # it's returned. Otherwise, it fails.
153
+ #
154
+ # @param value [::Numeric, ::Range] The value to turn into a range.
155
+ # @return [::Range]
156
+ def ensure_range(value)
157
+ case value
158
+ when ::Numeric
159
+ value..value
160
+ when ::Range
161
+ value
162
+ else
163
+ fail ArgumentError, "Unexpected #{value.class}, expected Range"
164
+ end
165
+ end
166
+
167
+ # Returns a string version of the range. If there is no distance between
168
+ # the starting and ending for the given range, it returns a string of the
169
+ # value. Otherwise, it returns `<starting>-<ending>`.
170
+ #
171
+ # @param value [::Range<::Numeric>]
172
+ # @return [::String]
173
+ def range(value)
174
+ return value.first.to_s unless value.first != value.last
175
+ "#{value.first}-#{value.last}"
176
+ end
177
+ end
178
+ end
data/lib/yoga/node.rb ADDED
@@ -0,0 +1,63 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require "mixture"
5
+
6
+ module Yoga
7
+ # A parser node. This can be subclassed, or used as is.
8
+ class Node
9
+ include Mixture::Model
10
+
11
+ # @!attribute [r] location
12
+ # The location of the node. This is normally dependant on the locations
13
+ # of the tokens that make up this node.
14
+ #
15
+ # @return [Yoga::Location]
16
+ attribute :location, type: Yoga::Location
17
+
18
+ # Initialize the node with the given attributes. The `:kind` and
19
+ # `:location` attributes are required.
20
+ #
21
+ # @param attributes [Hash] The attributes.
22
+ # @option attributes [::Symbol] :kind The kind of the node.
23
+ # @option attributes [Yoga::Location] :location The location of the node.
24
+ def initialize(attributes)
25
+ self.attributes = attributes
26
+ freeze
27
+ end
28
+
29
+ # Creates a new node with the updated attributes. If any unknown
30
+ # attributes are used, it fails.
31
+ #
32
+ # @param (see #initialize)
33
+ # @option (see #initialize)
34
+ def update(attributes)
35
+ self.class.new(self.attributes.merge(attributes))
36
+ end
37
+
38
+ # Returns whether or not the other object is equal to this one.
39
+ # If the other object is this opbject, it returns true; otherwise,
40
+ # if the other object is an instance of this object's class, and
41
+ # the attributes are equal, it returns true; otherwise, it returns
42
+ # false.
43
+ #
44
+ # @param other [::Object]
45
+ # @return [Boolean]
46
+ def ==(other)
47
+ equal?(other) || (other.is_a?(self.class) && \
48
+ attributes == other.attributes)
49
+ end
50
+
51
+ # Prevents all calls to {#update}. This is used on nodes that should
52
+ # never be updated. This is a stop-gap measure for incorrectly
53
+ # configured projects. This, in line with {#update}, creates
54
+ # a duplicate node.
55
+ #
56
+ # @return [Yoga::Node]
57
+ def prevent_update
58
+ node = dup
59
+ node.singleton_class.send(:undef_method, :update)
60
+ node
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require "yoga/parser/helpers"
5
+
6
+ module Yoga
7
+ # A parsing helper.
8
+ #
9
+ # This uses the `@tokens` and `@_root` instance variables.
10
+ module Parser
11
+ # Initialize the parser.
12
+ #
13
+ # @param tokens [::Enumerable<Yoga::Token>]
14
+ def initialize(tokens)
15
+ @tokens = tokens
16
+ end
17
+
18
+ # Performs the parsing.
19
+ #
20
+ # @return [Yoga::Node]
21
+ def call
22
+ @_root ||= parse_root
23
+ end
24
+
25
+ # Internal ruby construct.
26
+ #
27
+ # @private
28
+ # @api private
29
+ def self.included(base)
30
+ base.send :include, Parser::Helpers
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,234 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Yoga
5
+ module Parser
6
+ # helpers that manage the state of the parser. This can be things like
7
+ # peeking, shifting, and erroring.
8
+ module Helpers
9
+ # The class methods for helpers for the parser. These are defined
10
+ # on the class of the parser.
11
+ module ClassMethods
12
+ # The default value for a switch. This matches no tokens and
13
+ # performs no actions. This cannot be modified.
14
+ #
15
+ # @return [{::Symbol => ::Proc}]
16
+ DEFAULT_SWITCH = {}.freeze
17
+
18
+ # Predefines a switch logic. This is used to do set checking for the
19
+ # keys instead of array checking. This reduces the time it takes for
20
+ # a switch statement to be performed.
21
+ #
22
+ # @param name [::Symbol] The name of the switch. This is used in the
23
+ # method to look up the switch.
24
+ # @param mapping [{::Symbol, <::Symbol> => ::Proc}, nil] A
25
+ # mapping of symbol(s) to callables. If no value is given, then it
26
+ # returns the current switch for the given name.
27
+ # @return [{::Symbol => {::Symbol => ::Proc}}]
28
+ #
29
+ # @overload switch(name)
30
+ # Retrieves the switch map for the given name. This is basically
31
+ # a lookup table for the peek token. The keys represent the peek
32
+ # token kind, and the values are the actions that should be taken
33
+ # for the given token kind.
34
+ #
35
+ # @param name [::Symbol] The name of the switch map. This is
36
+ # typically the same as the first set name and the node name for
37
+ # the rule.
38
+ # @return [{::Symbol => ::Proc}] The switch map.
39
+ # @overload switch(name, mapping)
40
+ # Sets the switch map for the given name. This does some internal
41
+ # mapping before finalizing the mapping. First, it converts all
42
+ # values into a proc; then, it expands all array (or enumerable)
43
+ # keys into seperate entries. It then sets the first set using the
44
+ # name of the switch as the name of the first set, and the keys of
45
+ # the switch map as the first set.
46
+ #
47
+ # @example
48
+ # Parser.switch(:Definition,
49
+ # :":" => proc { parse_directive },
50
+ # :enum => proc { parse_enum },
51
+ # :def => proc { parse_function },
52
+ # :class => proc { parse_class },
53
+ # :struct => proc { parse_struct })
54
+ #
55
+ # @param name [::Symbol] The name of the switch map. A first set
56
+ # with the same name is created as well.
57
+ # @param mapping [{::Symbol, ::Enumerable<::Symbol> =>
58
+ # ::Proc, ::Symbol}] The mapping.
59
+ # @return [void]
60
+ def switch(name, mapping = nil)
61
+ @_switches ||= {}
62
+ name = name.intern
63
+ return @_switches[name] || DEFAULT_SWITCH unless mapping
64
+ switch = {}
65
+ mapping.each do |k, v|
66
+ v = v.is_a?(::Proc) ? v : proc { |*a| send(v, *a) }
67
+ Array(k).each { |n| switch[n] = v }
68
+ end
69
+
70
+ first(name, switch.keys)
71
+ @_switches[name] = switch
72
+ end
73
+
74
+ # The first sets. The key is the name of a node, and the value
75
+ # is a set of all tokens that can be at the start of that node.
76
+ #
77
+ # @return [{::Symbol => ::Set<::Symbol>}]
78
+ def firsts
79
+ @firsts ||= Hash.new { |h, k| h[k] = Set.new }
80
+ end
81
+
82
+ # @overload first(name)
83
+ # Retrieves the first set for the given node name. If none exists,
84
+ # a `KeyError` is raised.
85
+ #
86
+ # @api private
87
+ # @param name [::Symbol] The name of the node to look up the first
88
+ # set for.
89
+ # @return [::Set<::Symbol>] The first set
90
+ # @raise [::KeyError] If the node has no defined first set.
91
+ # @overload first(name, tokens)
92
+ # Merges the given tokens into the first set of the name of the
93
+ # node given.
94
+ #
95
+ # @api private
96
+ # @param name [::Symbol] The name of the node that the first set is
97
+ # for.
98
+ # @param tokens [<::Symbol>] The tokens that act as the first set
99
+ # for the node.
100
+ # @return [void]
101
+ def first(name, tokens = nil)
102
+ if tokens
103
+ firsts[name] = Set.new(Array(tokens))
104
+ else
105
+ firsts.fetch(name)
106
+ end
107
+ end
108
+ end
109
+
110
+ # Peeks to the next token. If peeking would cause a `StopIteration`
111
+ # error, it instead returns the last value that was peeked.
112
+ #
113
+ # @return [Yoga::Token]
114
+ def peek
115
+ if next?
116
+ @_last = @tokens.peek
117
+ else
118
+ @_last
119
+ end
120
+ end
121
+
122
+ # Checks if the next token exists. It does this by checking for a
123
+ # `StopIteration` error. This is actually really slow, but there's
124
+ # not much else I can do.
125
+ #
126
+ # @return [::Boolean]
127
+ def next?
128
+ @tokens.peek
129
+ true
130
+ rescue StopIteration
131
+ false
132
+ end
133
+
134
+ # Switches the function executed based on the given nodes. If the
135
+ # peeked node matches one of the mappings, it calls the resulting
136
+ # block or method.
137
+ #
138
+ # If the peeked token is not in the map, then {#error} is called.
139
+ #
140
+ # @see ClassMethods#switch
141
+ # @param name [::Symbol] The name of the switch to use.
142
+ # @return [::Object] The result of calling the block.
143
+ def switch(name, *param)
144
+ switch = self.class.switch(name)
145
+ block = switch.fetch(peek.kind) { error(switch.keys) }
146
+ instance_exec(*param, &block)
147
+ end
148
+
149
+ # rubocop:disable Metrics/CyclomaticComplexity
150
+ # rubocop:disable Metrics/PerceivedComplexity
151
+
152
+ # "Collects" a set of nodes until a terminating token. It yields
153
+ # until the peek is the token.
154
+ #
155
+ # @param ending [::Symbol] The terminating token.
156
+ # @param join [::Symbol, nil] The token that joins each of the
157
+ # children. This is the comma between arguments.
158
+ # @return [::Array] The collected nodes from the yielding process.
159
+ def collect(ending, join = nil)
160
+ children = []
161
+ join = Utils.flatten_into_set([join]) if join
162
+ ending = Utils.flatten_into_set([ending]) if ending
163
+
164
+ return [] if (ending && peek?(ending)) || (!ending && !join)
165
+
166
+ children << yield
167
+ while (join && expect(join)) && !(ending && peek?(ending))
168
+ children << yield
169
+ end
170
+
171
+ children
172
+ end
173
+
174
+ # rubocop:enable Metrics/CyclomaticComplexity
175
+ # rubocop:enable Metrics/PerceivedComplexity
176
+
177
+ # Checks to see if any of the given kinds includes the next token.
178
+ #
179
+ # @param tokens [<::Symbol>] The possible kinds.
180
+ # @return [::Boolean]
181
+ def peek?(*tokens)
182
+ tokens = Utils.flatten_into_set(tokens)
183
+ tokens.include?(peek.kind)
184
+ end
185
+
186
+ # Shifts to the next token, and returns the old token.
187
+ #
188
+ # @return [Yoga::Token]
189
+ def shift
190
+ @tokens.next
191
+ rescue ::StopIteration
192
+ fail InvalidShiftError, location: peek.location
193
+ end
194
+
195
+ # Sets up an expectation for a given token. If the next token is
196
+ # an expected token, it shifts, returning the token; otherwise,
197
+ # it {#error}s with the token information.
198
+ #
199
+ # @param tokens [<::Symbol>] The expected tokens.
200
+ # @return [Yoga::Token]
201
+ def expect(*tokens)
202
+ tokens = Utils.flatten_into_set(tokens)
203
+ return shift if peek?(*tokens)
204
+ error(tokens.flatten)
205
+ end
206
+
207
+ # Retrieves the first set for the given node name.
208
+ #
209
+ # @param name [::Symbol]
210
+ # @return [::Set<::Symbol>]
211
+ def first(name)
212
+ self.class.first(name)
213
+ end
214
+
215
+ # Errors, noting the expected tokens, the given token, the location
216
+ # of given tokens. It does this by failing.
217
+ #
218
+ # @param tokens [<::Symbol>] The expected tokens.
219
+ # @return [void]
220
+ def error(tokens)
221
+ fail Yoga::UnexpectedTokenError, expected: tokens, got: peek.kind,
222
+ location: peek.location
223
+ end
224
+
225
+ # Internal ruby construct.
226
+ #
227
+ # @private
228
+ # @api private
229
+ def self.included(base)
230
+ base.extend ClassMethods
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,156 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Yoga
5
+ # A scanner. This performs scanning over a series of tokens.
6
+ # It is built to lazily scan whenever it is required, instead
7
+ # of all at once. This integrates nicely with the parser.
8
+ module Scanner
9
+ # Initializes the scanner with the given source. Once the
10
+ # source is set, it shouldn't be changed.
11
+ #
12
+ # @param source [::String] The source.
13
+ def initialize(source)
14
+ @source = source
15
+ @line = 1
16
+ @last_line_at = 0
17
+ end
18
+
19
+ # @overload call(&block)
20
+ # For every token that is scanned, the block is yielded to.
21
+ #
22
+ # @yieldparam token [Scanner::Token]
23
+ # @return [self]
24
+ # @overload call
25
+ # Returns an enumerable over the tokens in the scanner.
26
+ #
27
+ # @return [::Enumerable<Scanner::Token>]
28
+ def call
29
+ return to_enum(:call) unless block_given?
30
+ @scanner = StringScanner.new(@source)
31
+ @line = 1
32
+
33
+ until @scanner.eos?
34
+ value = scan
35
+ yield value if value.is_a?(Token)
36
+ end
37
+
38
+ yield Token.eof(location)
39
+ self
40
+ end
41
+
42
+ # The scanning method. This should return one of two values: a {Token},
43
+ # or `true`. `nil` should _never_ be returned. This performs an
44
+ # incremental scan of the document; it returns one token at a time. If
45
+ # something matched, but should not emit a token, `true` should be
46
+ # returned. The implementing class should mark this as private or
47
+ # protected.
48
+ #
49
+ # @abstract
50
+ # Please implement this method in order to make the class a scanner.
51
+ # @return [Yoga::Token, true]
52
+ def scan
53
+ fail NotImplementedError, "Please implement #{self.class}#scan"
54
+ end
55
+
56
+ private
57
+
58
+ # Returns a location at the given location. If a size is given, it reduces
59
+ # the column number by the size and returns the size from that.
60
+ #
61
+ # @example
62
+ # @scanner.string # => "hello"
63
+ # @line # => 1
64
+ # @scanner.charpos # => 5
65
+ # location # => #<Yoga::Location <anon>:1.6>
66
+ # location(5) # => #<Yoga::Location <anon>:1.1-6
67
+ # @param size [::Numeric] The size of the token.
68
+ # @return [Yoga::Location]
69
+ def location(size = 0)
70
+ start = (@scanner.charpos - @last_line_at) + 1
71
+ column = (start - size)..start
72
+ Location.new(file, current_line, column)
73
+ end
74
+
75
+ # Creates a scanner token with the given name and source. This grabs the
76
+ # location using {#location}, setting the size to the size of the source
77
+ # text. The source is frozen before initializing the token.
78
+ #
79
+ # @example
80
+ # emit(:<, "<") # => #<Yoga::Token kind=:< source="<">
81
+ # @return [Yoga::Token]
82
+ def emit(kind, source = @scanner[0])
83
+ Token.new(kind.freeze, source.freeze, location(source.length))
84
+ end
85
+
86
+ # Attempts to match the given token. The first argument can be a string,
87
+ # a symbol, or a regular expression. If the matcher is a symbol, it's
88
+ # coerced into a regular expression, with a forward negative assertion for
89
+ # any alphanumeric characters, to prevent partial matches (see
90
+ # {#symbol_negative_assertion}). If the matcher is a regular expression,
91
+ # it is left alone. Otherwise, `#to_s` is called and passed to
92
+ # `Regexp.escape`. If the text is matched at the current position, a token
93
+ # is returned; otherwise, nil is returned.
94
+ #
95
+ # @param matcher [::Symbol, ::Regexp, #to_s]
96
+ # @param kind [::Symbol] The kind of token to emit. This defaults to a
97
+ # symbol version of the matcher.
98
+ # @return [Yoga::Token, nil]
99
+ def match(matcher, kind = :"#{matcher}")
100
+ matcher = case matcher
101
+ when ::Symbol then /#{::Regexp.escape(matcher.to_s)}#{symbol_negative_assertion}/
102
+ when ::Regexp then matcher
103
+ else /#{::Regexp.escape(matcher.to_s)}/
104
+ end
105
+
106
+ ((kind && emit(kind)) || true) if @scanner.scan(matcher)
107
+ end
108
+
109
+ # A regular expression to match all kinds of lines. All of them.
110
+ #
111
+ # @return [::Regexp]
112
+ LINE_MATCHER = /\r\n|\n\r|\n|\r/
113
+
114
+ # Matches a line. This is separate in order to allow internal logic,
115
+ # such as line counting and caching, to be performed.
116
+ #
117
+ # @return [Boolean] If the line was matched.
118
+ def match_line(kind = false)
119
+ match(LINE_MATCHER, kind).tap do |t|
120
+ break unless t
121
+ @line += 1
122
+ @last_line_at = @scanner.charpos
123
+ end
124
+ end
125
+
126
+ # Returns the number of lines that have been covered so far in the scanner.
127
+ # I recommend replacing this with an instance variable that caches the
128
+ # result of it, so that whenever you scan a new line, it just increments
129
+ # the line count.
130
+ #
131
+ # @return [::Numeric]
132
+ def current_line
133
+ # @scanner.string[0..@scanner.charpos].scan(/\A|\r\n|\n\r|\n|\r/).size
134
+ @line
135
+ end
136
+
137
+ # The negative assertion used for converting a symbol matcher to a regular
138
+ # expression. This is used to prevent premature matching of other
139
+ # identifiers. For example, if `module` is a keyword, and `moduleA` is
140
+ # an identifier, this negative assertion allows the following expression
141
+ # to properly match as such: `match(:module) || module(/[a-zA-Z], :IDENT)`.
142
+ #
143
+ # @return [#to_s]
144
+ def symbol_negative_assertion
145
+ "(?![a-zA-Z])"
146
+ end
147
+
148
+ # The file of the scanner. This can be overwritten to provide a descriptor
149
+ # for the file.
150
+ #
151
+ # @return [::String]
152
+ def file
153
+ @file ||= "<anon>"
154
+ end
155
+ end
156
+ end
data/lib/yoga/token.rb ADDED
@@ -0,0 +1,68 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Yoga
5
+ # A token that is used in scanning. This is emitted when a lexical
6
+ # structure is encountered; e.g., for a `{`.
7
+ class Token
8
+ # The kind of the token. For any explicit matches (e.g. `"{"`),
9
+ # this is the exact match (e.g. `:"{"`). For any regular matches
10
+ # (e.g. an identifier), this is in all caps (e.g. `:IDENTIFIER`).
11
+ #
12
+ # @return [::Symbol]
13
+ attr_reader :kind
14
+
15
+ # The lexeme that this token is associated. This is what the token
16
+ # matched directly from the source.
17
+ #
18
+ # @return [::String]
19
+ attr_reader :value
20
+
21
+ # The exact location the token was taken from. This only spans one
22
+ # line.
23
+ #
24
+ # @return [Location]
25
+ attr_reader :location
26
+
27
+ # A "hash" of the token. This is a number that is meant to roughly
28
+ # represent the value of this token. Used primarily for the Hash
29
+ # class.
30
+ #
31
+ # @api private
32
+ # @return [::Numeric]
33
+ attr_reader :hash
34
+
35
+ # Creates an `EOF`-kind token. This creates the token at the given
36
+ # location.
37
+ #
38
+ # @param location [Location]
39
+ # @return [Token]
40
+ def self.eof(location = Location.default)
41
+ new(:EOF, "", location)
42
+ end
43
+
44
+ # Initializes the token with the given kind, value, and location.
45
+ #
46
+ # @param kind [::Symbol] The kind of token. See {#kind}.
47
+ # @param value [::String] The value of the token. See {#value}.
48
+ # @param location [Location] The location of the token. See {#location}.
49
+ def initialize(kind, value, location)
50
+ @kind = kind
51
+ @value = value.freeze
52
+ @location = location
53
+ @hash = [@kind, @value, @location].hash
54
+ freeze
55
+ end
56
+
57
+ # Determines if this object is equal to another object. It first checks
58
+ # for equality using `equal?`, then checks if the other is a token. If
59
+ # the other object is a token, it checks that the attributes equate.
60
+ #
61
+ # @param other [::Object]
62
+ # @return [Boolean]
63
+ def ==(other)
64
+ equal?(other) || other.is_a?(Token) && @kind == other.kind &&
65
+ @value == other.value && @location == other.location
66
+ end
67
+ end
68
+ end
data/lib/yoga/utils.rb ADDED
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Yoga
5
+ # Utilities used with the Yoga module.
6
+ #
7
+ # @api private
8
+ module Utils
9
+ module_function
10
+
11
+ # Takes an array of tokens or set/array of tokens and turns it into a
12
+ # single set.
13
+ #
14
+ # @param tokens [<Yoga::Token, ::Set<Yoga::Token>, ::Enumerable>]
15
+ # The array to flatten into a set.
16
+ # @return [::Set<Yoga::Token>]
17
+ def flatten_into_set(tokens)
18
+ flatten_into_array(tokens).to_set
19
+ end
20
+
21
+ # Takes an array of tokens or a set/array of tokens and turns it into a
22
+ # single array.
23
+ #
24
+ # @param tokens [<Yoga::Token>, ::Set<Yoga::Token>, ::Enumerable]
25
+ # The array to flatten into a set.
26
+ # @return [<Yoga::Token>]
27
+ def flatten_into_array(tokens)
28
+ tokens.flat_map do |part|
29
+ if part.is_a?(::Enumerable)
30
+ flatten_into_array(part)
31
+ else
32
+ part
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
data/lib/yoga/version.rb CHANGED
@@ -1,3 +1,9 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
1
4
  module Yoga
2
- VERSION = "0.0.1"
5
+ # The version of the module.
6
+ #
7
+ # @return [::String]
8
+ VERSION = "0.2.0"
3
9
  end
data/script/bench ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require "benchmark/ips"
6
+ require "ruby-prof"
7
+ require "yoga/utils"
8
+
9
+ VALUES = [
10
+ [[[[:x], :a], :b]],
11
+ [[Set[[:a], :x], :a]],
12
+ [[[[[[[[[[:x], :a], :c], :d], :e], :f], :d], :a], :a]]
13
+ ].freeze
14
+
15
+ if ARGV[0] == "mark"
16
+ Benchmark.ips do |bench|
17
+ VALUES.each_with_index do |e, i|
18
+ bench.report(i) do |times|
19
+ i = 0
20
+ while i < times
21
+ Yoga::Utils.flatten_into_set(e)
22
+ i += 1
23
+ end
24
+ end
25
+ end
26
+ end
27
+ else
28
+ result = RubyProf.profile do
29
+ VALUES.each do |e|
30
+ 100_000.times do
31
+ Yoga::Utils.flatten_into_set(e)
32
+ end
33
+ end
34
+ end
35
+
36
+ printer = RubyProf::MultiPrinter.new(result)
37
+ printer.print(path: "profile/flatten", profile: "flatten")
38
+ end
data/yoga.gemspec CHANGED
@@ -1,23 +1,30 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+ lib = File.expand_path("../lib", __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'yoga/version'
5
+ require "yoga/version"
5
6
 
6
7
  Gem::Specification.new do |spec|
7
8
  spec.name = "yoga"
8
9
  spec.version = Yoga::VERSION
9
10
  spec.authors = ["Jeremy Rodi"]
10
- spec.email = ["redjazz96@gmail.com"]
11
- spec.summary = %q{A lexer.}
12
- spec.description = %q{A lexer.}
13
- spec.homepage = ""
14
- spec.license = "MIT"
11
+ spec.email = ["me@medcat.me"]
15
12
 
16
- spec.files = `git ls-files -z`.split("\x0")
13
+ spec.summary = "Ruby scanner and parser helpers."
14
+ spec.homepage = "https://github.com/medcat/yoga"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "bin"
17
20
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
21
  spec.require_paths = ["lib"]
20
22
 
21
- spec.add_development_dependency "bundler", "~> 1.6"
22
- spec.add_development_dependency "rake"
23
+ spec.add_dependency "mixture", "~> 0.6"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.13"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ spec.add_development_dependency "simplecov", "~> 0.13"
29
+ spec.add_development_dependency "rubocop", "~> 0.47"
23
30
  end
metadata CHANGED
@@ -1,61 +1,130 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yoga
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Rodi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-16 00:00:00.000000000 Z
11
+ date: 2017-03-08 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mixture
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.6'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - "~>"
18
32
  - !ruby/object:Gem::Version
19
- version: '1.6'
33
+ version: '1.13'
20
34
  type: :development
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
38
  - - "~>"
25
39
  - !ruby/object:Gem::Version
26
- version: '1.6'
40
+ version: '1.13'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rake
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
- - - ">="
45
+ - - "~>"
32
46
  - !ruby/object:Gem::Version
33
- version: '0'
47
+ version: '10.0'
34
48
  type: :development
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
- - - ">="
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.13'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.13'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.47'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
39
95
  - !ruby/object:Gem::Version
40
- version: '0'
41
- description: A lexer.
96
+ version: '0.47'
97
+ description:
42
98
  email:
43
- - redjazz96@gmail.com
99
+ - me@medcat.me
44
100
  executables: []
45
101
  extensions: []
46
102
  extra_rdoc_files: []
47
103
  files:
48
104
  - ".gitignore"
105
+ - ".rspec"
106
+ - ".rubocop.yml"
107
+ - ".travis.yml"
108
+ - ".yardopts"
109
+ - CODE_OF_CONDUCT.md
49
110
  - Gemfile
50
111
  - LICENSE.txt
51
112
  - README.md
52
113
  - Rakefile
53
114
  - lib/yoga.rb
115
+ - lib/yoga/errors.rb
116
+ - lib/yoga/location.rb
117
+ - lib/yoga/node.rb
118
+ - lib/yoga/parser.rb
119
+ - lib/yoga/parser/helpers.rb
120
+ - lib/yoga/scanner.rb
121
+ - lib/yoga/token.rb
122
+ - lib/yoga/utils.rb
54
123
  - lib/yoga/version.rb
124
+ - script/bench
55
125
  - yoga.gemspec
56
- homepage: ''
57
- licenses:
58
- - MIT
126
+ homepage: https://github.com/medcat/yoga
127
+ licenses: []
59
128
  metadata: {}
60
129
  post_install_message:
61
130
  rdoc_options: []
@@ -73,9 +142,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
73
142
  version: '0'
74
143
  requirements: []
75
144
  rubyforge_project:
76
- rubygems_version: 2.2.2
145
+ rubygems_version: 2.5.2
77
146
  signing_key:
78
147
  specification_version: 4
79
- summary: A lexer.
148
+ summary: Ruby scanner and parser helpers.
80
149
  test_files: []
81
- has_rdoc: