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,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