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 +4 -4
- data/.gitignore +2 -1
- data/.rspec +3 -0
- data/.rubocop.yml +40 -0
- data/.travis.yml +8 -0
- data/.yardopts +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -1
- data/README.md +14 -7
- data/Rakefile +7 -0
- data/lib/yoga.rb +11 -0
- data/lib/yoga/errors.rb +61 -0
- data/lib/yoga/location.rb +178 -0
- data/lib/yoga/node.rb +63 -0
- data/lib/yoga/parser.rb +33 -0
- data/lib/yoga/parser/helpers.rb +234 -0
- data/lib/yoga/scanner.rb +156 -0
- data/lib/yoga/token.rb +68 -0
- data/lib/yoga/utils.rb +37 -0
- data/lib/yoga/version.rb +7 -1
- data/script/bench +38 -0
- data/yoga.gemspec +19 -12
- metadata +84 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4999074c914d1566ee18799548c1d437ff5ad83b
|
|
4
|
+
data.tar.gz: 998165e1b73a1d6fef8350b7a5b5f15bfb6295ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3f0f59113580eac2ac0065651837f3e0bef3044585e48ff61e92c8b9a3cc21faac33a45ab0a98ed908d80de05adcc5bafb66a1e291ca6150c0641c25fbe25947
|
|
7
|
+
data.tar.gz: 9c86dc7896453ebe2e6c5bdb060f664757f9246af44a4e3ff556789b5c0851fe7648f53b8c493d3254babea180195e356226c1d544933b1382d2875e359fbf26
|
data/.gitignore
CHANGED
data/.rspec
ADDED
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
data/.yardopts
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -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
data/README.md
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# Yoga
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
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
|
data/lib/yoga/errors.rb
ADDED
|
@@ -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
|
data/lib/yoga/parser.rb
ADDED
|
@@ -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
|
data/lib/yoga/scanner.rb
ADDED
|
@@ -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
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
|
-
#
|
|
2
|
-
|
|
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
|
|
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 = ["
|
|
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.
|
|
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.
|
|
22
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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:
|
|
96
|
+
version: '0.47'
|
|
97
|
+
description:
|
|
42
98
|
email:
|
|
43
|
-
-
|
|
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.
|
|
145
|
+
rubygems_version: 2.5.2
|
|
77
146
|
signing_key:
|
|
78
147
|
specification_version: 4
|
|
79
|
-
summary:
|
|
148
|
+
summary: Ruby scanner and parser helpers.
|
|
80
149
|
test_files: []
|
|
81
|
-
has_rdoc:
|