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.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +186 -0
  4. data/.ruby-version +1 -0
  5. data/CONTRIBUTING.md +85 -0
  6. data/Gemfile +5 -0
  7. data/Gemfile.lock +105 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +275 -0
  10. data/Rakefile +10 -0
  11. data/bin/poe-css +14 -0
  12. data/demo/Gemfile +11 -0
  13. data/demo/Gemfile.lock +45 -0
  14. data/demo/app.rb +21 -0
  15. data/demo/views/index.haml +105 -0
  16. data/lib/dependencies.rb +11 -0
  17. data/lib/poe-css.rb +253 -0
  18. data/lib/poe-css/generator.rb +108 -0
  19. data/lib/poe-css/parser.rb +177 -0
  20. data/lib/poe-css/preprocessor.rb +186 -0
  21. data/lib/poe-css/simplifier.rb +201 -0
  22. data/lib/poe-css/version.rb +5 -0
  23. data/poe-css.gemspec +32 -0
  24. data/tests/poecss/all-clauses-1.output +23 -0
  25. data/tests/poecss/all-clauses-1.poecss +24 -0
  26. data/tests/poecss/alternation-1.output +8 -0
  27. data/tests/poecss/alternation-1.poecss +4 -0
  28. data/tests/poecss/nesting-1.output +8 -0
  29. data/tests/poecss/nesting-1.poecss +8 -0
  30. data/tests/poecss/nesting-2.output +30 -0
  31. data/tests/poecss/nesting-2.poecss +23 -0
  32. data/tests/poecss/nesting-3.output +15 -0
  33. data/tests/poecss/nesting-3.poecss +15 -0
  34. data/tests/poecss/nesting-4.output +14 -0
  35. data/tests/poecss/nesting-4.poecss +13 -0
  36. data/tests/poecss/performance-1.output +3 -0
  37. data/tests/poecss/performance-1.poecss +59 -0
  38. data/tests/poecss/performance-2.output +60 -0
  39. data/tests/poecss/performance-2.poecss +75 -0
  40. data/tests/poecss/rule-ordering-1.output +25 -0
  41. data/tests/poecss/rule-ordering-1.poecss +30 -0
  42. data/tests/poecss/rule-ordering-2.output +3 -0
  43. data/tests/poecss/rule-ordering-2.poecss +29 -0
  44. data/tests/poecss/rule-ordering-3.output +30 -0
  45. data/tests/poecss/rule-ordering-3.poecss +29 -0
  46. data/tests/poecss/rule-ordering-4.output +20 -0
  47. data/tests/poecss/rule-ordering-4.poecss +30 -0
  48. data/tests/poecss/rule-ordering-5.output +8 -0
  49. data/tests/poecss/rule-ordering-5.poecss +9 -0
  50. data/tests/poecss/rule-ordering-6.output +7 -0
  51. data/tests/poecss/rule-ordering-6.poecss +14 -0
  52. data/tests/poecss/simple-1.output +7 -0
  53. data/tests/poecss/simple-1.poecss +8 -0
  54. data/tests/poecss/simplification-1.output +6 -0
  55. data/tests/poecss/simplification-1.poecss +7 -0
  56. data/tests/poecss/simplification-2.output +6 -0
  57. data/tests/poecss/simplification-2.poecss +8 -0
  58. data/tests/poecss/simplification-3.output +2 -0
  59. data/tests/poecss/simplification-3.poecss +3 -0
  60. data/tests/poecss/simplification-4.output +3 -0
  61. data/tests/poecss/simplification-4.poecss +4 -0
  62. data/tests/poecss/simplification-5.output +3 -0
  63. data/tests/poecss/simplification-5.poecss +8 -0
  64. data/tests/poecss/simplification-6.output +2 -0
  65. data/tests/poecss/simplification-6.poecss +4 -0
  66. data/tests/preprocessor/arity-mismatch-1.error.preprocessor +4 -0
  67. data/tests/preprocessor/arity-mismatch-2.error.preprocessor +4 -0
  68. data/tests/preprocessor/complex.output +20 -0
  69. data/tests/preprocessor/complex.preprocessor +19 -0
  70. data/tests/preprocessor/empty.output +0 -0
  71. data/tests/preprocessor/empty.preprocessor +0 -0
  72. data/tests/preprocessor/macro-newlines.output +5 -0
  73. data/tests/preprocessor/macro-newlines.preprocessor +9 -0
  74. data/tests/preprocessor/macro.output +3 -0
  75. data/tests/preprocessor/macro.preprocessor +7 -0
  76. data/tests/preprocessor/missing-identifier-1.error.preprocessor +1 -0
  77. data/tests/preprocessor/missing-identifier-2.error.preprocessor +1 -0
  78. data/tests/preprocessor/normal-newlines.output +2 -0
  79. data/tests/preprocessor/normal-newlines.preprocessor +2 -0
  80. data/tests/preprocessor/parse-error-1.error.preprocessor +1 -0
  81. data/tests/preprocessor/parse-error-2.error.preprocessor +1 -0
  82. data/tests/preprocessor/parse-error-3.error.preprocessor +4 -0
  83. data/tests/preprocessor/simple.output +1 -0
  84. data/tests/preprocessor/simple.preprocessor +3 -0
  85. data/tests/run-all-tests.rb +60 -0
  86. data/tests/unit/simplify-bounds-test.rb +57 -0
  87. data/tests/unit/stricter-query-test.rb +24 -0
  88. 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