poe-css 0.0.1
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 +7 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +186 -0
- data/.ruby-version +1 -0
- data/CONTRIBUTING.md +85 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +105 -0
- data/LICENSE.txt +22 -0
- data/README.md +275 -0
- data/Rakefile +10 -0
- data/bin/poe-css +14 -0
- data/demo/Gemfile +11 -0
- data/demo/Gemfile.lock +45 -0
- data/demo/app.rb +21 -0
- data/demo/views/index.haml +105 -0
- data/lib/dependencies.rb +11 -0
- data/lib/poe-css.rb +253 -0
- data/lib/poe-css/generator.rb +108 -0
- data/lib/poe-css/parser.rb +177 -0
- data/lib/poe-css/preprocessor.rb +186 -0
- data/lib/poe-css/simplifier.rb +201 -0
- data/lib/poe-css/version.rb +5 -0
- data/poe-css.gemspec +32 -0
- data/tests/poecss/all-clauses-1.output +23 -0
- data/tests/poecss/all-clauses-1.poecss +24 -0
- data/tests/poecss/alternation-1.output +8 -0
- data/tests/poecss/alternation-1.poecss +4 -0
- data/tests/poecss/nesting-1.output +8 -0
- data/tests/poecss/nesting-1.poecss +8 -0
- data/tests/poecss/nesting-2.output +30 -0
- data/tests/poecss/nesting-2.poecss +23 -0
- data/tests/poecss/nesting-3.output +15 -0
- data/tests/poecss/nesting-3.poecss +15 -0
- data/tests/poecss/nesting-4.output +14 -0
- data/tests/poecss/nesting-4.poecss +13 -0
- data/tests/poecss/performance-1.output +3 -0
- data/tests/poecss/performance-1.poecss +59 -0
- data/tests/poecss/performance-2.output +60 -0
- data/tests/poecss/performance-2.poecss +75 -0
- data/tests/poecss/rule-ordering-1.output +25 -0
- data/tests/poecss/rule-ordering-1.poecss +30 -0
- data/tests/poecss/rule-ordering-2.output +3 -0
- data/tests/poecss/rule-ordering-2.poecss +29 -0
- data/tests/poecss/rule-ordering-3.output +30 -0
- data/tests/poecss/rule-ordering-3.poecss +29 -0
- data/tests/poecss/rule-ordering-4.output +20 -0
- data/tests/poecss/rule-ordering-4.poecss +30 -0
- data/tests/poecss/rule-ordering-5.output +8 -0
- data/tests/poecss/rule-ordering-5.poecss +9 -0
- data/tests/poecss/rule-ordering-6.output +7 -0
- data/tests/poecss/rule-ordering-6.poecss +14 -0
- data/tests/poecss/simple-1.output +7 -0
- data/tests/poecss/simple-1.poecss +8 -0
- data/tests/poecss/simplification-1.output +6 -0
- data/tests/poecss/simplification-1.poecss +7 -0
- data/tests/poecss/simplification-2.output +6 -0
- data/tests/poecss/simplification-2.poecss +8 -0
- data/tests/poecss/simplification-3.output +2 -0
- data/tests/poecss/simplification-3.poecss +3 -0
- data/tests/poecss/simplification-4.output +3 -0
- data/tests/poecss/simplification-4.poecss +4 -0
- data/tests/poecss/simplification-5.output +3 -0
- data/tests/poecss/simplification-5.poecss +8 -0
- data/tests/poecss/simplification-6.output +2 -0
- data/tests/poecss/simplification-6.poecss +4 -0
- data/tests/preprocessor/arity-mismatch-1.error.preprocessor +4 -0
- data/tests/preprocessor/arity-mismatch-2.error.preprocessor +4 -0
- data/tests/preprocessor/complex.output +20 -0
- data/tests/preprocessor/complex.preprocessor +19 -0
- data/tests/preprocessor/empty.output +0 -0
- data/tests/preprocessor/empty.preprocessor +0 -0
- data/tests/preprocessor/macro-newlines.output +5 -0
- data/tests/preprocessor/macro-newlines.preprocessor +9 -0
- data/tests/preprocessor/macro.output +3 -0
- data/tests/preprocessor/macro.preprocessor +7 -0
- data/tests/preprocessor/missing-identifier-1.error.preprocessor +1 -0
- data/tests/preprocessor/missing-identifier-2.error.preprocessor +1 -0
- data/tests/preprocessor/normal-newlines.output +2 -0
- data/tests/preprocessor/normal-newlines.preprocessor +2 -0
- data/tests/preprocessor/parse-error-1.error.preprocessor +1 -0
- data/tests/preprocessor/parse-error-2.error.preprocessor +1 -0
- data/tests/preprocessor/parse-error-3.error.preprocessor +4 -0
- data/tests/preprocessor/simple.output +1 -0
- data/tests/preprocessor/simple.preprocessor +3 -0
- data/tests/run-all-tests.rb +60 -0
- data/tests/unit/simplify-bounds-test.rb +57 -0
- data/tests/unit/stricter-query-test.rb +24 -0
- metadata +292 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module POECSS
|
4
|
+
module Generator
|
5
|
+
class << self
|
6
|
+
def generate_poe_rules(clauses)
|
7
|
+
clauses.map { |clause|
|
8
|
+
visibility_command_clauses, nonvisibility_command_clauses = [
|
9
|
+
clause.command_clauses
|
10
|
+
].flatten.partition { |c| c.command_key == 'show' || c.command_key == 'hide' }
|
11
|
+
visibility_command_clause = visibility_command_clauses.first
|
12
|
+
if visibility_command_clause.nil?
|
13
|
+
raise ArgumentError, "Each generated clause must have a Show or Hide command.\n\n#{clause}"
|
14
|
+
end
|
15
|
+
|
16
|
+
visibility_header = visibility_command_clause.command_key.capitalize
|
17
|
+
|
18
|
+
match_rules = clause.match_clauses.map { |c|
|
19
|
+
args = c.match_arguments
|
20
|
+
arguments =
|
21
|
+
case c.match_key
|
22
|
+
when 'itemlevel'
|
23
|
+
[ 'ItemLevel', args[:operator], args[:level] ]
|
24
|
+
when 'droplevel'
|
25
|
+
[ 'DropLevel', args[:operator], args[:level] ]
|
26
|
+
when 'quality'
|
27
|
+
[ 'Quality', args[:operator], args[:quality] ]
|
28
|
+
when 'rarity'
|
29
|
+
[ 'Rarity', args[:operator], args[:rarity] ]
|
30
|
+
when 'class'
|
31
|
+
[ 'Class', args[:substrings].sort.map(&:inspect) ]
|
32
|
+
when 'basetype'
|
33
|
+
[ 'BaseType', args[:substrings].sort.map(&:inspect) ]
|
34
|
+
when 'corrupted'
|
35
|
+
[ 'Corrupted', bool(args[:corrupted]) ]
|
36
|
+
when 'elderitem'
|
37
|
+
[ 'ElderItem', bool(args[:elder_item]) ]
|
38
|
+
when 'height'
|
39
|
+
[ 'Height', args[:operator], args[:height] ]
|
40
|
+
when 'identified'
|
41
|
+
[ 'Identified', bool(args[:identified]) ]
|
42
|
+
when 'linkedsockets'
|
43
|
+
[ 'LinkedSockets', args[:operator], args[:sockets] ]
|
44
|
+
when 'shapedmap'
|
45
|
+
[ 'ShapedMap', bool(args[:shaped_map]) ]
|
46
|
+
when 'shaperitem'
|
47
|
+
[ 'ShaperItem', bool(args[:shaper_item]) ]
|
48
|
+
when 'socketgroup'
|
49
|
+
[ 'SocketGroup', args[:sub_socket_groups].sort_by { |s| [ -s.length, s ] }.map(&:inspect) ]
|
50
|
+
when 'sockets'
|
51
|
+
[ 'Sockets', args[:operator], args[:sockets] ]
|
52
|
+
when 'width'
|
53
|
+
[ 'Width', args[:operator], args[:width] ]
|
54
|
+
else
|
55
|
+
raise ArgumentError, c.match_key
|
56
|
+
end
|
57
|
+
|
58
|
+
arguments.flatten.join(' ')
|
59
|
+
}
|
60
|
+
|
61
|
+
nonvisibility_commands = nonvisibility_command_clauses.map { |c|
|
62
|
+
arg = c.command_argument
|
63
|
+
arguments =
|
64
|
+
case c.command_key
|
65
|
+
when 'setbordercolor'
|
66
|
+
[ 'SetBorderColor', color_from_spec(arg[:color]) ]
|
67
|
+
when 'settextcolor'
|
68
|
+
[ 'SetTextColor', color_from_spec(arg[:color]) ]
|
69
|
+
when 'setbackgroundcolor'
|
70
|
+
[ 'SetBackgroundColor', color_from_spec(arg[:color]) ]
|
71
|
+
when 'playalertsound'
|
72
|
+
[ 'PlayAlertSound', arg[:sound_id], arg[:volume] ]
|
73
|
+
when 'playalertsoundpositional'
|
74
|
+
[ 'PlayAlertSoundPositional', arg[:sound_id], arg[:volume] ]
|
75
|
+
when 'setfontsize'
|
76
|
+
[ 'SetFontSize', arg[:font_size] ]
|
77
|
+
else
|
78
|
+
raise ArgumentError, c.command_key
|
79
|
+
end
|
80
|
+
|
81
|
+
arguments.flatten.compact.join(' ')
|
82
|
+
}
|
83
|
+
|
84
|
+
[
|
85
|
+
visibility_header.to_s,
|
86
|
+
match_rules.sort.map { |r| " #{r}" }.join("\n"),
|
87
|
+
nonvisibility_commands.sort.map { |r| " #{r}" }.join("\n")
|
88
|
+
].reject(&:empty?).join("\n") + "\n"
|
89
|
+
}.join("\n")
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def color_from_spec(spec)
|
95
|
+
case spec
|
96
|
+
when RGBColorSpec
|
97
|
+
(%i[r g b].map { |c| spec[c] } + [ spec.a || 255 ]).join(' ')
|
98
|
+
else
|
99
|
+
raise ArgumentError, spec
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def bool(b)
|
104
|
+
(!b.nil? ? b : true).to_s.capitalize
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module POECSS
|
4
|
+
module Parser
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def parse_input(s)
|
8
|
+
input = s.strip
|
9
|
+
tree = Parser.new.parse(input, reporter: Parslet::ErrorReporter::Deepest.new)
|
10
|
+
Transformer.new.apply(tree)
|
11
|
+
rescue Parslet::ParseFailed => error
|
12
|
+
raise ParseError.new(:parser, error.parse_failure_cause)
|
13
|
+
end
|
14
|
+
|
15
|
+
class Parser < Parslet::Parser
|
16
|
+
def stri(str)
|
17
|
+
str.split('').map { |char| match["#{char.upcase}#{char.downcase}"] }.reduce(:>>)
|
18
|
+
end
|
19
|
+
|
20
|
+
rule(:newline) { match("\n").repeat(1) }
|
21
|
+
rule(:space) { match('\s').repeat(1) }
|
22
|
+
rule(:space?) { space.maybe }
|
23
|
+
rule(:comma) { str(',') >> space? }
|
24
|
+
rule(:pipe) { str('|') >> space? }
|
25
|
+
rule(:open_brace) { str('{') >> space? }
|
26
|
+
rule(:close_brace) { str('}') >> space? }
|
27
|
+
|
28
|
+
rule(:operator) { (str('>=') | str('<=') | str('>') | str('<') | str('=')).as(:operator) >> space? }
|
29
|
+
|
30
|
+
rule(:integer) { match('[0-9]').repeat(1).as(:integer) >> space? }
|
31
|
+
rule(:rarity) { (stri('Normal') | stri('Magic') | stri('Rare') | stri('Unique')).as(:rarity) >> space? }
|
32
|
+
rule(:string) {
|
33
|
+
((str('"') >> match(%([a-zA-Z0-9' ])).repeat(1).as(:string) >> str('"')) | match(%([a-zA-Z0-9'])).repeat(1).as(:string)) >> space?
|
34
|
+
}
|
35
|
+
rule(:strings) { string.repeat(1).as(:strings) }
|
36
|
+
rule(:rgb_color_spec) {
|
37
|
+
stri('RGB') >> stri('A').maybe >> str('(') >> space? >>
|
38
|
+
integer.as(:r) >> comma >>
|
39
|
+
integer.as(:g) >> comma >>
|
40
|
+
integer.as(:b) >>
|
41
|
+
(comma >> integer.as(:a)).maybe >>
|
42
|
+
str(')') >> space?
|
43
|
+
}
|
44
|
+
rule(:color_spec) {
|
45
|
+
rgb_color_spec.as(:rgb)
|
46
|
+
}
|
47
|
+
rule(:sound_spec) {
|
48
|
+
integer.as(:sound_id) >> integer.as(:volume).maybe
|
49
|
+
}
|
50
|
+
rule(:boolean) { (stri('true') | stri('false')).as(:boolean) >> space? }
|
51
|
+
|
52
|
+
rule(:match_item_level) {
|
53
|
+
stri('ItemLevel').as(:match_key) >> space? >> (operator.maybe.as(:operator) >> integer.as(:level)).as(:match_arguments)
|
54
|
+
}
|
55
|
+
rule(:match_drop_level) {
|
56
|
+
stri('DropLevel').as(:match_key) >> space? >> (operator.maybe.as(:operator) >> integer.as(:level)).as(:match_arguments)
|
57
|
+
}
|
58
|
+
rule(:match_quality) {
|
59
|
+
stri('Quality').as(:match_key) >> space? >> (operator.maybe.as(:operator) >> integer.as(:quality)).as(:match_arguments)
|
60
|
+
}
|
61
|
+
rule(:match_rarity) {
|
62
|
+
stri('Rarity').as(:match_key) >> space? >> (operator.maybe.as(:operator) >> rarity.as(:rarity)).as(:match_arguments)
|
63
|
+
}
|
64
|
+
rule(:match_class) {
|
65
|
+
stri('Class').as(:match_key) >> space? >> strings.as(:substrings).as(:match_arguments)
|
66
|
+
}
|
67
|
+
rule(:match_base_type) {
|
68
|
+
stri('BaseType').as(:match_key) >> space? >> strings.as(:substrings).as(:match_arguments)
|
69
|
+
}
|
70
|
+
rule(:match_sockets) {
|
71
|
+
stri('Sockets').as(:match_key) >> space? >> (operator.maybe.as(:operator) >> integer.as(:sockets)).as(:match_arguments)
|
72
|
+
}
|
73
|
+
rule(:match_linked_sockets) {
|
74
|
+
stri('LinkedSockets').as(:match_key) >> space? >> (operator.maybe.as(:operator) >> integer.as(:sockets)).as(:match_arguments)
|
75
|
+
}
|
76
|
+
rule(:match_socket_group) {
|
77
|
+
stri('SocketGroup').as(:match_key) >> space? >> strings.as(:sub_socket_groups).as(:match_arguments)
|
78
|
+
}
|
79
|
+
rule(:match_height) {
|
80
|
+
stri('Height').as(:match_key) >> space? >> (operator.maybe.as(:operator) >> integer.as(:height)).as(:match_arguments)
|
81
|
+
}
|
82
|
+
rule(:match_width) {
|
83
|
+
stri('Width').as(:match_key) >> space? >> (operator.maybe.as(:operator) >> integer.as(:width)).as(:match_arguments)
|
84
|
+
}
|
85
|
+
rule(:match_identified) {
|
86
|
+
stri('Identified').as(:match_key) >> space? >> boolean.maybe.as(:identified).as(:match_arguments)
|
87
|
+
}
|
88
|
+
rule(:match_corrupted) {
|
89
|
+
stri('Corrupted').as(:match_key) >> space? >> boolean.maybe.as(:corrupted).as(:match_arguments)
|
90
|
+
}
|
91
|
+
rule(:match_elder_item) {
|
92
|
+
stri('ElderItem').as(:match_key) >> space? >> boolean.maybe.as(:elder_item).as(:match_arguments)
|
93
|
+
}
|
94
|
+
rule(:match_shaped_map) {
|
95
|
+
stri('ShapedMap').as(:match_key) >> space? >> boolean.maybe.as(:shaped_map).as(:match_arguments)
|
96
|
+
}
|
97
|
+
rule(:match_shaper_item) {
|
98
|
+
stri('ShaperItem').as(:match_key) >> space? >> boolean.maybe.as(:shaper_item).as(:match_arguments)
|
99
|
+
}
|
100
|
+
|
101
|
+
rule(:match_clause) {
|
102
|
+
(
|
103
|
+
match_item_level |
|
104
|
+
match_drop_level |
|
105
|
+
match_quality |
|
106
|
+
match_rarity |
|
107
|
+
match_class |
|
108
|
+
match_base_type |
|
109
|
+
match_sockets |
|
110
|
+
match_linked_sockets |
|
111
|
+
match_socket_group |
|
112
|
+
match_height |
|
113
|
+
match_width |
|
114
|
+
match_identified |
|
115
|
+
match_corrupted |
|
116
|
+
match_elder_item |
|
117
|
+
match_shaped_map |
|
118
|
+
match_shaper_item
|
119
|
+
)
|
120
|
+
}
|
121
|
+
|
122
|
+
rule(:command_set_border_color) { stri('SetBorderColor').as(:command_key) >> space? >> color_spec.as(:color).as(:command_argument) }
|
123
|
+
rule(:command_set_text_color) { stri('SetTextColor').as(:command_key) >> space? >> color_spec.as(:color).as(:command_argument) }
|
124
|
+
rule(:command_set_bg_color) { stri('SetBackgroundColor').as(:command_key) >> space? >> color_spec.as(:color).as(:command_argument) }
|
125
|
+
rule(:command_play_alert_sound) { stri('PlayAlertSound').as(:command_key) >> space? >> sound_spec.as(:command_argument) }
|
126
|
+
rule(:command_play_alert_sound_positional) { stri('PlayAlertSoundPositional').as(:command_key) >> space? >> sound_spec.as(:command_argument) }
|
127
|
+
rule(:command_set_font_size) { stri('SetFontSize').as(:command_key) >> space? >> integer.as(:font_size).as(:command_argument) }
|
128
|
+
|
129
|
+
rule(:command_clause) {
|
130
|
+
(
|
131
|
+
(stri('Show').as(:command_key) >> space?) |
|
132
|
+
(stri('Hide').as(:command_key) >> space?) |
|
133
|
+
command_set_border_color |
|
134
|
+
command_set_text_color |
|
135
|
+
command_set_bg_color |
|
136
|
+
command_play_alert_sound |
|
137
|
+
command_play_alert_sound_positional |
|
138
|
+
command_set_font_size
|
139
|
+
)
|
140
|
+
}
|
141
|
+
|
142
|
+
rule(:match_alternation) {
|
143
|
+
((match_clause >> comma).repeat(0) >> match_clause).as(:match_clauses)
|
144
|
+
}
|
145
|
+
|
146
|
+
rule(:clause) {
|
147
|
+
((match_alternation >> pipe).repeat(0) >> match_alternation).as(:match_alternations) >> open_brace >>
|
148
|
+
(command_clause | clause).repeat(1).as(:inner_clauses) >> close_brace
|
149
|
+
}
|
150
|
+
rule(:clauses) { clause.repeat(0) }
|
151
|
+
root(:clauses)
|
152
|
+
end
|
153
|
+
|
154
|
+
class Transformer < Parslet::Transform
|
155
|
+
rule(integer: simple(:i)) { Integer(i) }
|
156
|
+
rule(operator: simple(:o)) { o.to_s }
|
157
|
+
rule(rarity: simple(:o)) { o.to_s }
|
158
|
+
rule(string: simple(:s)) { s.to_s }
|
159
|
+
rule(strings: sequence(:s)) { s }
|
160
|
+
rule(boolean: simple(:b)) { b.to_s.downcase == 'true'.downcase }
|
161
|
+
rule(sockets: simple(:g)) { g.to_s.upcase }
|
162
|
+
rule(rgb: { r: simple(:r), g: simple(:g), b: simple(:b), a: simple(:a) }) { RGBColorSpec.new(r, g, b, a) }
|
163
|
+
rule(rgb: { r: simple(:r), g: simple(:g), b: simple(:b) }) { RGBColorSpec.new(r, g, b, nil) }
|
164
|
+
|
165
|
+
rule(sub_socket_groups: sequence(:groups)) {
|
166
|
+
{ sub_socket_groups: groups.map(&:upcase) }
|
167
|
+
}
|
168
|
+
|
169
|
+
rule(match_key: simple(:k), match_arguments: subtree(:a)) { MatchClause.new(k.to_s.downcase, a) }
|
170
|
+
rule(command_key: simple(:k), command_argument: subtree(:a)) { CommandClause.new(k.to_s.downcase, a) }
|
171
|
+
rule(command_key: simple(:k)) { CommandClause.new(k.to_s.downcase, nil) }
|
172
|
+
rule(match_alternations: subtree(:alternations), inner_clauses: subtree(:inner_clauses)) {
|
173
|
+
Clause.new([ alternations ].flatten.map { |match_clauses| [ match_clauses[:match_clauses] ].flatten }, inner_clauses)
|
174
|
+
}
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module POECSS
|
4
|
+
module Preprocessor
|
5
|
+
IDENTIFIER_CHAR_REGEX = '[0-9a-zA-Z\-_]'
|
6
|
+
|
7
|
+
class DefinitionParser < Parslet::Parser
|
8
|
+
rule(:newline) { str("\n").repeat(1) }
|
9
|
+
rule(:space) { match('\s').repeat(1) }
|
10
|
+
rule(:space?) { space.maybe }
|
11
|
+
rule(:comma) { str(',') >> space? }
|
12
|
+
rule(:open_brace) { str('{') >> space? }
|
13
|
+
rule(:close_brace) { str('}') >> space? }
|
14
|
+
rule(:open_paren) { str('(') >> space? }
|
15
|
+
rule(:close_paren) { str(')') >> space? }
|
16
|
+
|
17
|
+
rule(:identifier) { str('@') >> match(IDENTIFIER_CHAR_REGEX.to_s).repeat >> space? }
|
18
|
+
|
19
|
+
rule(:macro_prototype) {
|
20
|
+
(
|
21
|
+
identifier.as(:macro_name) >> open_paren >>
|
22
|
+
((identifier.as(:argument) >> comma).repeat >> identifier.as(:argument)).repeat(0, 1).as(:argument_list) >>
|
23
|
+
close_paren
|
24
|
+
).as(:macro_prototype)
|
25
|
+
}
|
26
|
+
rule(:block_in_macro) { open_brace >> (block_in_macro | match('[^{}]')).repeat(0) >> close_brace }
|
27
|
+
rule(:macro_definition) { (macro_prototype >> block_in_macro.as(:macro_replacement)).as(:macro) }
|
28
|
+
|
29
|
+
rule(:constant_definition) {
|
30
|
+
(identifier.as(:identifier) >> str(':') >> space? >> match('[^\n]').repeat(1).as(:replacement)).as(:constant_definition)
|
31
|
+
}
|
32
|
+
|
33
|
+
rule(:non_newline_char) { match('[^\n]') }
|
34
|
+
rule(:nonmacro_line) { (non_newline_char.repeat(0) >> newline | non_newline_char.repeat(1)) }
|
35
|
+
rule(:program) {
|
36
|
+
(match('\s').repeat(0) >> (constant_definition | macro_definition | nonmacro_line.as(:nonmacro_line))).repeat(0)
|
37
|
+
}
|
38
|
+
|
39
|
+
root(:program)
|
40
|
+
end
|
41
|
+
|
42
|
+
ConstantDefinition = Struct.new(:identifier, :replacement)
|
43
|
+
ArgumentList = Struct.new(:arguments)
|
44
|
+
MacroDefinition = Struct.new(:identifier, :argument_list, :replacement)
|
45
|
+
|
46
|
+
class DefinitionTransformer < Parslet::Transform
|
47
|
+
rule(nonmacro_line: simple(:l)) { l.to_s }
|
48
|
+
|
49
|
+
rule(constant_definition: { identifier: simple(:i), replacement: simple(:r) }) { ConstantDefinition.new(i.to_s, r.to_s) }
|
50
|
+
rule(argument: simple(:a)) { a.to_s }
|
51
|
+
rule(macro: {
|
52
|
+
macro_prototype: {
|
53
|
+
macro_name: simple(:name),
|
54
|
+
argument_list: sequence(:args)
|
55
|
+
},
|
56
|
+
macro_replacement: simple(:r)
|
57
|
+
}) { MacroDefinition.new(name.to_s, ArgumentList.new(args), r.to_s.strip[1..-2]) }
|
58
|
+
end
|
59
|
+
|
60
|
+
class << self
|
61
|
+
def compile(input)
|
62
|
+
stripped_input = input.strip
|
63
|
+
return '' if stripped_input.empty?
|
64
|
+
|
65
|
+
r = parse_definition(stripped_input)
|
66
|
+
tree = DefinitionTransformer.new.apply(r)
|
67
|
+
|
68
|
+
constants = tree.select { |n| n.is_a?(ConstantDefinition) }
|
69
|
+
macros = tree.select { |n| n.is_a?(MacroDefinition) }
|
70
|
+
strings = tree.select { |n| n.is_a?(String) }
|
71
|
+
|
72
|
+
constants_by_id =
|
73
|
+
begin
|
74
|
+
grouping = constants.group_by(&:identifier)
|
75
|
+
if (id, = grouping.find { |_, defs| defs.length > 1 })
|
76
|
+
raise ArgumentError, "Multiple definitions of #{id} found."
|
77
|
+
end
|
78
|
+
|
79
|
+
grouping.transform_values(&:first).to_h
|
80
|
+
end
|
81
|
+
|
82
|
+
macros_by_id =
|
83
|
+
begin
|
84
|
+
grouping = macros.group_by(&:identifier)
|
85
|
+
if (id, = grouping.find { |_, defs| defs.length > 1 })
|
86
|
+
raise ArgumentError, "Multiple definitions of #{id} found."
|
87
|
+
end
|
88
|
+
|
89
|
+
grouping.transform_values(&:first).to_h
|
90
|
+
end
|
91
|
+
|
92
|
+
interpolate_macros(constants_by_id, macros_by_id, strings.join(''))
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def parse_definition(input)
|
98
|
+
DefinitionParser.new.parse(input, reporter: Parslet::ErrorReporter::Deepest.new)
|
99
|
+
rescue Parslet::ParseFailed => error
|
100
|
+
raise ParseError.new(:preprocessor, error.parse_failure_cause)
|
101
|
+
end
|
102
|
+
|
103
|
+
class UsageParser < Parslet::Parser
|
104
|
+
rule(:comma) { str(',') }
|
105
|
+
rule(:open_paren) { str('(') }
|
106
|
+
rule(:close_paren) { str(')') }
|
107
|
+
|
108
|
+
rule(:identifier) { str('@') >> match(IDENTIFIER_CHAR_REGEX.to_s).repeat(1) }
|
109
|
+
|
110
|
+
rule(:argument) { match('[^,)]').repeat(1).as(:argument) }
|
111
|
+
rule(:macro_use) {
|
112
|
+
identifier.as(:macro_name) >> open_paren >>
|
113
|
+
((argument >> comma).repeat >> argument).repeat(0, 1).as(:argument_list) >>
|
114
|
+
close_paren
|
115
|
+
}
|
116
|
+
|
117
|
+
rule(:program) {
|
118
|
+
(macro_use.as(:macro_use) | identifier.as(:constant_use) | match('.').as(:text)).repeat(0)
|
119
|
+
}
|
120
|
+
|
121
|
+
root(:program)
|
122
|
+
end
|
123
|
+
|
124
|
+
def interpolate_macros(constants_by_id, macros_by_id, input)
|
125
|
+
this = self
|
126
|
+
|
127
|
+
transform = Parslet::Transform.new do
|
128
|
+
rule(text: simple(:s)) { s.to_s }
|
129
|
+
rule(constant_use: simple(:s)) {
|
130
|
+
identifier = s.to_s.strip
|
131
|
+
constant = constants_by_id[identifier]
|
132
|
+
raise ArgumentError, "Unknown constant #{identifier}." unless constant
|
133
|
+
constant.replacement
|
134
|
+
}
|
135
|
+
|
136
|
+
rule(argument: simple(:a)) { a.to_s.strip }
|
137
|
+
rule(
|
138
|
+
macro_use: {
|
139
|
+
macro_name: simple(:name),
|
140
|
+
argument_list: sequence(:args)
|
141
|
+
}
|
142
|
+
) {
|
143
|
+
identifier = name.to_s.strip
|
144
|
+
macro = macros_by_id[identifier]
|
145
|
+
raise ArgumentError, "Unknown macro #{identifier}." unless macro
|
146
|
+
if macro.argument_list.arguments.length != args.length
|
147
|
+
raise ArgumentError, "Got #{args.length} arguments to a #{macro.argument_list.arguments.length}-argument macro."
|
148
|
+
end
|
149
|
+
|
150
|
+
argument_bindings = macro.argument_list.arguments.zip(args)
|
151
|
+
.map { |argument_name, value| [ argument_name, ConstantDefinition.new(argument_name, value) ] }
|
152
|
+
.to_h
|
153
|
+
this.send(
|
154
|
+
:interpolate_macros,
|
155
|
+
constants_by_id.merge(argument_bindings),
|
156
|
+
macros_by_id,
|
157
|
+
macro.replacement
|
158
|
+
)
|
159
|
+
}
|
160
|
+
end
|
161
|
+
|
162
|
+
string = input
|
163
|
+
|
164
|
+
loop do
|
165
|
+
tree =
|
166
|
+
begin
|
167
|
+
UsageParser.new.parse(string, reporter: Parslet::ErrorReporter::Deepest.new)
|
168
|
+
rescue Parslet::ParseFailed => error
|
169
|
+
warn error.parse_failure_cause.ascii_tree
|
170
|
+
raise
|
171
|
+
end
|
172
|
+
|
173
|
+
new_string = transform.apply(tree).join('')
|
174
|
+
|
175
|
+
if new_string == string
|
176
|
+
break
|
177
|
+
end
|
178
|
+
|
179
|
+
string = new_string
|
180
|
+
end
|
181
|
+
|
182
|
+
string
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|