poe-css 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+
5
+ task :spec do
6
+ system('cd tests && bundle exec ruby run-all-tests.rb')
7
+ end
8
+
9
+ task default: :spec
10
+ task test: :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require_relative '../lib/dependencies'
6
+
7
+ input =
8
+ if ARGV.first
9
+ File.read(ARGV.first)
10
+ else
11
+ STDIN.read
12
+ end
13
+
14
+ puts POECSS.compile(input)
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ ruby '2.4.2'
4
+
5
+ source 'http://rubygems.org'
6
+
7
+ gem 'haml'
8
+ gem 'parslet'
9
+ gem 'rerun'
10
+ gem 'sinatra', '~> 1.3'
11
+ gem 'sinatra-param'
@@ -0,0 +1,45 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ ffi (1.9.18)
5
+ haml (5.0.3)
6
+ temple (>= 0.8.0)
7
+ tilt
8
+ listen (3.1.5)
9
+ rb-fsevent (~> 0.9, >= 0.9.4)
10
+ rb-inotify (~> 0.9, >= 0.9.7)
11
+ ruby_dep (~> 1.2)
12
+ parslet (1.8.1)
13
+ rack (1.6.8)
14
+ rack-protection (1.5.3)
15
+ rack
16
+ rb-fsevent (0.10.2)
17
+ rb-inotify (0.9.10)
18
+ ffi (>= 0.5.0, < 2)
19
+ rerun (0.11.0)
20
+ listen (~> 3.0)
21
+ ruby_dep (1.5.0)
22
+ sinatra (1.4.8)
23
+ rack (~> 1.5)
24
+ rack-protection (~> 1.4)
25
+ tilt (>= 1.3, < 3)
26
+ sinatra-param (1.4.0)
27
+ sinatra (~> 1.3)
28
+ temple (0.8.0)
29
+ tilt (2.0.8)
30
+
31
+ PLATFORMS
32
+ ruby
33
+
34
+ DEPENDENCIES
35
+ haml
36
+ parslet
37
+ rerun
38
+ sinatra (~> 1.3)
39
+ sinatra-param
40
+
41
+ RUBY VERSION
42
+ ruby 2.4.2p198
43
+
44
+ BUNDLED WITH
45
+ 1.15.3
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'haml'
4
+ require 'sinatra'
5
+ require 'sinatra/param'
6
+
7
+ require_relative '../../lib/dependencies'
8
+
9
+ get '/compile' do
10
+ param :input, String, required: true
11
+
12
+ parsed_clauses = POECSS::Parsing.parse_input(params[:input])
13
+ result = POECSS::Generator.generate_poe_rules(parsed_clauses)
14
+
15
+ content_type 'text/plain'
16
+ body result
17
+ end
18
+
19
+ get '/' do
20
+ haml :index
21
+ end
@@ -0,0 +1,105 @@
1
+ :css
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ }
6
+
7
+ .u-flexCenter {
8
+ display: flex;
9
+ width: 100%;
10
+ align-items: top;
11
+ justify-content: center;
12
+ }
13
+
14
+ .u-maxWidth {
15
+ width: 100%;
16
+ }
17
+
18
+ body {
19
+ padding: 0.5em;
20
+ }
21
+
22
+ .pane {
23
+ padding: 0.5em;
24
+ }
25
+
26
+ .output {
27
+ white-space: pre;
28
+ font-family: monospace;
29
+ }
30
+
31
+ h1 {
32
+ margin-bottom: 0.25em;
33
+ }
34
+
35
+ .u-flexCenter
36
+ .pane.u-maxWidth
37
+ %h1 Input
38
+ %textarea#input.u-maxWidth(style="height: 50vh")
39
+ :plain
40
+ @black: RGB(0, 0, 0)
41
+ @white: RGB(255, 255, 255)
42
+
43
+ @t1-gem-text-color: RGB(30, 200, 200)
44
+ @t1-gem-border-color: RGB(30, 150, 180)
45
+ @t1-gem-bg-color: @white
46
+
47
+ @volume: 300
48
+ @t1-drop-sound: 6 @volume
49
+ @unique-drop-sound: 3 @volume
50
+ @value-drop-sound: 2 @volume
51
+
52
+ @gem-styling() {
53
+ SetTextColor @t1-gem-text-color
54
+ SetBorderColor @t1-gem-border-color
55
+ }
56
+
57
+ Class Gems {
58
+ Hide
59
+ SetFontSize 36
60
+ SetBorderColor @black
61
+
62
+ Quality >= 1 {
63
+ Show
64
+ SetFontSize 40
65
+ SetBorderColor @t1-gem-border-color
66
+ }
67
+
68
+ BaseType "Detonate Mines" "Added Chaos Damage" "Vaal" "Enhance" | Quality >= 14 {
69
+ Show
70
+ SetFontSize 40
71
+ @gem-styling()
72
+ PlayAlertSound @value-drop-sound
73
+ }
74
+
75
+ BaseType "Portal" "Empower" "Enlighten" "Vaal Haste" "Vaal Grace" "Item Quantity" "Vaal Breach" {
76
+ Show
77
+ SetFontSize 45
78
+ @gem-styling()
79
+ PlayAlertSound @unique-drop-sound
80
+ }
81
+ }
82
+
83
+ .pane.u-maxWidth
84
+ %h1 Output
85
+ .output#output
86
+
87
+ :javascript
88
+ var inputNode = document.getElementById('input');
89
+ var outputNode = document.getElementById('output');
90
+
91
+ function compile() {
92
+ var input = inputNode.value;
93
+
94
+ fetch('/compile?input=' + encodeURIComponent(input))
95
+ .then(result => {
96
+ if (result.ok) {
97
+ result.text().then(o => { outputNode.innerHTML = o; })
98
+ } else {
99
+ outputNode.innerHTML = 'Parse Error';
100
+ }
101
+ });
102
+ };
103
+
104
+ inputNode.addEventListener('keyup', compile);
105
+ compile();
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parslet'
4
+ require 'pp'
5
+ require 'set'
6
+
7
+ require_relative 'poe-css'
8
+ require_relative 'poe-css/generator'
9
+ require_relative 'poe-css/parser'
10
+ require_relative 'poe-css/preprocessor'
11
+ require_relative 'poe-css/simplifier'
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ module POECSS
4
+ Clause = Struct.new(:match_alternations, :inner_clauses)
5
+ MatchClause = Struct.new(:match_key, :match_arguments)
6
+ CommandClause = Struct.new(:command_key, :command_argument)
7
+ RGBColorSpec = Struct.new(:r, :g, :b, :a)
8
+ SimpleClause = Struct.new(:match_clauses, :command_clauses)
9
+
10
+ class ParseError < StandardError
11
+ def initialize(source, parse_trace)
12
+ program_input = parse_trace.pos.instance_variable_get(:@string)
13
+ deepest_error = deepest_error(parse_trace)
14
+
15
+ line_no, col_no = deepest_error.source.line_and_column(deepest_error.pos)
16
+
17
+ program_lines = program_input.split("\n")
18
+
19
+ line = program_lines[line_no - 1]
20
+
21
+ super([
22
+ "Parse error in #{source} step:",
23
+ '',
24
+ program_lines[line_no - 2],
25
+ line,
26
+ (' ' * (col_no - 1)) + '^'
27
+ ].compact.join("\n"))
28
+ end
29
+
30
+ def deepest_error(node)
31
+ queue = [ [ node, 0 ] ]
32
+ deepest_error = nil
33
+
34
+ while !queue.empty?
35
+ node, depth = queue.shift
36
+
37
+ if node.children.empty?
38
+ if deepest_error.nil? || deepest_error.last < depth
39
+ deepest_error = [ node, depth ]
40
+ end
41
+ else
42
+ node.children.each do |c|
43
+ queue << [ c, depth + 1 ]
44
+ end
45
+ end
46
+ end
47
+
48
+ deepest_error.first
49
+ end
50
+ end
51
+
52
+ class SimpleClause
53
+ def inspect
54
+ matches = 'Match (' + match_clauses.map { |s| [ s.match_key, s.match_arguments.inspect ].compact.join(' ') }.join(' ') + ')'
55
+ commands = '{ ' + command_clauses.map { |c| [ c.command_key, c.command_argument ].compact.join(' ') }.join(', ') + ' }'
56
+ '<' + [ matches, commands ].join(' ') + '>'
57
+ end
58
+ end
59
+
60
+ class RGBColorSpec
61
+ def inspect
62
+ "RGBA(#{[ r, g, b, a ].map(&:inspect).join(', ')})"
63
+ end
64
+ end
65
+
66
+ class << self
67
+ def compile(input)
68
+ # Strip comments, which is anything after a # in any line. No
69
+ # preprocessor or POE CSS syntax uses the #, and it doesn't appear in any
70
+ # strings (it could, but no item has a # in it), so it's safe to
71
+ # disregard all syntax when removing comments.
72
+ input_without_comments = input.split("\n").map { |l| l.gsub(/#.*$/, '') }.join("\n")
73
+
74
+ # Use the preprocessor to expand constants and macros.
75
+ preprocessed_program = Preprocessor.compile(input_without_comments)
76
+
77
+ # Hand the contents over to the main parser, which returns a list of Clauses.
78
+ parsed_clauses = Parser.parse_input(preprocessed_program)
79
+
80
+ # These Clauses, each of which can contain alternations (ORs) or nested
81
+ # clauses, get transformed into SimpleClauses. A SimpleClause is just a
82
+ # list of ANDed match clauses and a list of commands. The nesting and
83
+ # alternations have been expanded. We also simplify clauses here to
84
+ # minimize inputs into the next step as well as to collapse/minimize
85
+ # duplicated match and command keys for speed/simplicity.
86
+ expanded_clauses = expand_clauses(parsed_clauses).map { |r| Simplifier.simplify_clause(r) }.compact
87
+
88
+ # SimpleClauses are then converted into a list of rules in else-if form,
89
+ # the logical flow style used by PoE. The datatype is still SimpleClause,
90
+ # but the clauses have all been transformed and expanded to meet the
91
+ # else-if form (I call them rules to distinguish them conceptually from
92
+ # clauses).
93
+ else_ifed_rules = clauses_to_else_if_rules(expanded_clauses)
94
+
95
+ # Finally, rules that will never be hit or contribute nothing to to final
96
+ # result are deleted. This does not remove every useless clause but is
97
+ # likely good enough to suffice.
98
+ rules_without_dead_rules = rules_without_dead_rules(else_ifed_rules)
99
+
100
+ # Finally, we hand the results to the generator, which outputs them in
101
+ # PoE item filter format.
102
+ Generator.generate_poe_rules(rules_without_dead_rules)
103
+ end
104
+
105
+ private
106
+
107
+ def expand_clauses(clauses)
108
+ result_clauses = []
109
+
110
+ clauses.each do |c|
111
+ explore_clause(result_clauses, c, SimpleClause.new([], []))
112
+ end
113
+
114
+ result_clauses
115
+ end
116
+
117
+ def explore_clause(result_clauses, clause, clause_context)
118
+ commands, nested_clauses = clause.inner_clauses.partition { |c| c.is_a?(CommandClause) }
119
+
120
+ clause.match_alternations.each do |match_clauses|
121
+ simple_clause = SimpleClause.new((clause_context.match_clauses + match_clauses).to_set, clause_context.command_clauses + commands)
122
+ result_clauses << simple_clause
123
+
124
+ nested_clauses.each do |n|
125
+ explore_clause(result_clauses, n, simple_clause)
126
+ end
127
+ end
128
+ end
129
+
130
+ def rules_without_dead_rules(input_rules)
131
+ rules = input_rules
132
+
133
+ output_rules = []
134
+
135
+ loop do
136
+ output_rules = []
137
+
138
+ rules.each do |r|
139
+ if output_rules.last&.command_clauses == r.command_clauses && implies(output_rules.last, r)
140
+ output_rules[-1] = r
141
+ else
142
+ output_rules << r
143
+ end
144
+ end
145
+
146
+ break if rules.length == output_rules.length
147
+
148
+ rules = output_rules
149
+ end
150
+
151
+ output_rules
152
+ end
153
+
154
+ # Return whether or not clause A being true implies that clause B must be true as well.
155
+ def implies(clause_a, clause_b)
156
+ simplified_clause_a = Simplifier.simplify_clause(clause_a)
157
+ simplified_clause_b = Simplifier.simplify_clause(clause_b)
158
+
159
+ return false if simplified_clause_a.nil? || simplified_clause_b.nil?
160
+
161
+ clause_a_matches = simplified_clause_a.match_clauses.group_by(&:match_key)
162
+ clause_b_matches = simplified_clause_b.match_clauses.group_by(&:match_key)
163
+
164
+ # If B has any match keys that aren't part of A, there's no way A can imply B.
165
+ return false unless clause_b_matches.keys.to_set.subset?(clause_a_matches.keys.to_set)
166
+
167
+ shared_keys = clause_a_matches.keys & clause_b_matches.keys
168
+
169
+ shared_keys.all? { |k|
170
+ a_matches = clause_a_matches[k]
171
+ b_matches = clause_b_matches[k]
172
+
173
+ # If the simplifier simplifies A && B to just A, it means that B was
174
+ # superfluous to the comparison. Specifically, A && B = A, which means
175
+ # that when A is true, B must be true. Therefore, A implies B.
176
+ joined_clause = Simplifier.simplify_clause(SimpleClause.new(a_matches + b_matches, []))
177
+ joined_clause && joined_clause.match_clauses == a_matches
178
+ }
179
+ end
180
+
181
+ def clauses_to_else_if_rules(clauses)
182
+ output_rules = [ SimpleClause.new([], []) ]
183
+
184
+ # When encountering a pair of duplicates, this eliminates the earlier one in the array, since later ones have more weight.
185
+ unique_clauses = clauses.reverse.uniq.reverse
186
+
187
+ # This is a modified power set algorithm adapted from the one viewable
188
+ # here (https://github.com/sagivo/powerset/blob/master/powerset.rb).
189
+ # This algorithm has the important property that the order of elements in
190
+ # each sublist is the same as the order of elements in the original list,
191
+ # which is crucial for maintaining the priority ordering of clauses.
192
+ # Secondly, each sublist is actually the clause concatenation (ANDed) of
193
+ # all the clauses that would be in the sublist, and impossible clauses
194
+ # are ignored. Finally, clauses that are generated are unique, but if
195
+ # there is a duplicate, the earlier one is deleted because the later one
196
+ # has higher precedence. We don't want to suppress the later one because
197
+ # it would keep the weaker precedence, and we don't want to keep it
198
+ # because we want to minimize the size of the output list. Powerset is a
199
+ # 2^N operation, so we want to use deduplication and simplification
200
+ # whenever possible to reduce the search space.
201
+ #
202
+ # Why do this at all? Well, the way to convert a list of clauses, any
203
+ # subset of which may match a given item, to a list of specific else-if
204
+ # rules is to consider the power set of the clauses, which form every
205
+ # possible case that an item may fall into. You can then turn each
206
+ # sublist into a single ANDed clause and sort the result by specificity
207
+ # from most to least specific (so that the more specific rules are closer
208
+ # to the top, whereas the reverse would suppress the rule). This is an
209
+ # easy algorithm, but has the disadvantage of generating all 2^N
210
+ # combinations before applying the observations above to minimize the
211
+ # size of the result set. For a set of 24 clauses, that's already 16
212
+ # million combinations. That's why we mix the filtering in during the
213
+ # actual powerset algorithm.
214
+
215
+ unique_clauses.each do |clause|
216
+ current_length = output_rules.length
217
+
218
+ (0...current_length).each do |i|
219
+ rule = output_rules[i]
220
+
221
+ concatenated_clause = Simplifier.simplify_clause(
222
+ SimpleClause.new(
223
+ rule.match_clauses + clause.match_clauses,
224
+ rule.command_clauses + clause.command_clauses
225
+ )
226
+ )
227
+
228
+ if concatenated_clause
229
+ output_rules.delete(concatenated_clause)
230
+ output_rules << concatenated_clause
231
+ end
232
+ end
233
+ end
234
+
235
+ # Our list is now sorted in specificity order from least to most
236
+ # specific. We reverse the list and then filter out rules which would be
237
+ # subsumed by rules earlier in the output list. These rules are useless
238
+ # since they would be caught by the existing rule.
239
+
240
+ rules = output_rules.drop(1) # Removes the original empty sublist.
241
+
242
+ final_output = []
243
+
244
+ rules.reverse.each do |r|
245
+ if final_output.none? { |existing_rule| implies(r, existing_rule) }
246
+ final_output << r
247
+ end
248
+ end
249
+
250
+ final_output
251
+ end
252
+ end
253
+ end