deadfire 0.3.0 → 0.5.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.
@@ -5,18 +5,16 @@ module Deadfire
5
5
  class << self
6
6
  def resolve_import_path(line, lineno = 0)
7
7
  path = normalize_path(line)
8
- unless path.end_with?(Parser::CSS_FILE_EXTENSION)
9
- path += Parser::CSS_FILE_EXTENSION
10
- end
11
- import_path = File.join(Deadfire.configuration.root_path, path)
12
-
13
- unless File.exist?(import_path)
14
- raise Deadfire::ImportException.new(import_path, lineno)
8
+ potential = potential_path(path)
9
+ ext = File.extname(path)
10
+
11
+ if ext && valid_file?(potential)
12
+ potential
13
+ else
14
+ possible_paths(path)
15
15
  end
16
-
17
- import_path
18
16
  end
19
-
17
+
20
18
  def normalize_path(line)
21
19
  path = line.split.last
22
20
  path.gsub!("\"", "")
@@ -24,6 +22,28 @@ module Deadfire
24
22
  path.gsub!(";", "")
25
23
  path
26
24
  end
25
+
26
+ private
27
+
28
+ def valid_file_extension?(ext)
29
+ Deadfire::PERMISSIBLE_FILE_EXTENSIONS.include?(ext)
30
+ end
31
+
32
+ def valid_file?(path)
33
+ File.exist?(path)
34
+ end
35
+
36
+ def possible_paths(path)
37
+ Deadfire::PERMISSIBLE_FILE_EXTENSIONS.each do |ext|
38
+ option = File.join(Deadfire.configuration.root_path, path + ext)
39
+ return option if valid_file?(option)
40
+ end
41
+ nil
42
+ end
43
+
44
+ def potential_path(path)
45
+ File.join(Deadfire.configuration.root_path, path)
46
+ end
27
47
  end
28
48
  end
29
49
  end
@@ -1,11 +1,10 @@
1
- # Frozen_string_literal: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Deadfire
4
4
  class ApplyNode
5
5
  attr_reader :node, :mixin_names
6
6
 
7
7
  def initialize(node, mixin_names)
8
- # TODO: mixin name can be single or multiple names, separated by a comma
9
8
  @node = node
10
9
  @mixin_names = fetch_mixin_name_from(mixin_names)
11
10
  end
@@ -13,19 +13,13 @@ module Deadfire
13
13
  end
14
14
 
15
15
  def parse
16
- # top level it's a list of statements
17
- # statements are either rules or at-rules
18
- # rules are selectors + declarations
19
- # at-rules are at-keyword + block
20
- # block is a list of declarations?
21
- # declarations are property + value
22
16
  while !is_at_end?
23
17
  if check(:comment)
24
18
  comment = add_comment
25
- @stylesheet << comment if Deadfire.configuration.keep_comments
19
+ @stylesheet << comment unless Deadfire.configuration.compressed
26
20
  elsif check(:newline)
27
21
  newline = add_newline
28
- @stylesheet << newline if Deadfire.configuration.keep_whitespace
22
+ @stylesheet << newline unless Deadfire.configuration.compressed
29
23
  elsif matches_at_rule?
30
24
  @stylesheet << at_rule_declaration
31
25
  else
@@ -84,10 +78,6 @@ module Deadfire
84
78
  check(:at_rule)
85
79
  end
86
80
 
87
- def matches_nested_rule?
88
- match?(:ampersand)
89
- end
90
-
91
81
  def parse_block
92
82
  block = BlockNode.new
93
83
  block << previous
@@ -111,7 +101,11 @@ module Deadfire
111
101
  def ruleset_declaration
112
102
  values = []
113
103
  while !match?(:left_brace)
114
- values << advance
104
+ unless match?(:comment)
105
+ values << advance
106
+ else
107
+ values << advance unless Deadfire.configuration.compressed
108
+ end
115
109
  end
116
110
 
117
111
  selector = SelectorNode.new(values[0..-1])
@@ -142,14 +136,16 @@ module Deadfire
142
136
 
143
137
  if previous.type == :semicolon
144
138
  if keyword.lexeme == "@apply"
145
- return ApplyNode.new(keyword, values)
139
+ ApplyNode.new(keyword, values)
146
140
  else
147
141
  values << previous # add the semicolon to the values
148
- return AtRuleNode.new(keyword, values, nil)
142
+ AtRuleNode.new(keyword, values, nil)
149
143
  end
144
+ elsif is_at_end?
145
+ AtRuleNode.new(keyword, values, nil)
146
+ else
147
+ AtRuleNode.new(keyword, values[0..-1], parse_block) # remove the left brace, because it's not a value, but part of the block
150
148
  end
151
-
152
- AtRuleNode.new(keyword, values[0..-1], parse_block) # remove the left brace, because it's not a value, but part of the block
153
149
  end
154
150
  end
155
151
  end
@@ -75,26 +75,15 @@ module Deadfire
75
75
  end
76
76
 
77
77
  def add_at_rule(literal = nil)
78
- selector = [current_char]
79
-
80
- while Spec::CSS_AT_RULES.none? { |kwrd| kwrd == selector.join + peek } && !at_end?
81
- break if peek == NEWLINE
82
- selector << advance
83
- end
84
-
85
- # final char in at-rule
86
- selector << advance
87
-
88
- current_at_rule = selector.join
89
- at_rule = Spec::CSS_AT_RULES.find { |kwrd| kwrd == current_at_rule }
78
+ at_rule = determine_at_rule
90
79
 
91
80
  if peek == NEWLINE
92
81
  @line += 1
93
82
  @error_reporter.error(@line, "at-rule cannot be on multiple lines.")
94
- add_token(:at_rule, current_at_rule)
83
+ add_token(:at_rule, at_rule)
95
84
  elsif at_rule
96
85
  token = add_token(:at_rule, "at_#{at_rule[1..-1]}")
97
- if at_rule == "@import"
86
+ if at_rule == Spec::IMPORT
98
87
  prescan_import_rule(token.last)
99
88
  else
100
89
  token
@@ -172,32 +161,38 @@ module Deadfire
172
161
  end
173
162
 
174
163
  def add_forward_slash_or_comment
175
- if peek == "*"
176
- advance # consume the *
177
- while peek != "*" && peek_next != "/" && !at_end?
178
- @line += 1 if peek == NEWLINE
179
- advance
180
- end
164
+ return add_token(:forward_slash) unless peek == "*"
181
165
 
182
- if at_end? && peek != "*"
166
+ advance # consume the *
167
+ while true
168
+ if at_end?
183
169
  @error_reporter.error(@line, "Unterminated comment on line #{@line}.")
184
- else
185
- advance # consume the *
186
- advance # consume the /
170
+ break
187
171
  end
188
- add_token(:comment) # Add the comment anyway, but report an error.
189
- else
190
- add_token(:forward_slash)
172
+
173
+ case peek
174
+ when NEWLINE
175
+ @line += 1
176
+ when "*"
177
+ if peek_next == "/"
178
+ advance # consume the *
179
+ advance # consume the /
180
+ break
181
+ end
182
+ end
183
+
184
+ advance
191
185
  end
186
+ add_token(:comment) # Add the comment anyway, but report an error.
192
187
  end
193
188
 
194
189
  def add_whitespace_token
195
- add_token(:whitespace) if Deadfire.configuration.keep_whitespace
190
+ add_token(:whitespace) unless Deadfire.configuration.compressed
196
191
  end
197
192
 
198
193
  def add_newline_token
199
194
  @line += 1
200
- add_token(:newline) if Deadfire.configuration.keep_whitespace
195
+ add_token(:newline) unless Deadfire.configuration.compressed
201
196
  end
202
197
 
203
198
  def current_char_position
@@ -229,6 +224,21 @@ module Deadfire
229
224
  (char >= "a" && char <= "z") || (char >= "A" && char <= "Z")
230
225
  end
231
226
 
227
+ def determine_at_rule
228
+ selector = [current_char]
229
+
230
+ while Spec::CSS_AT_RULES.none? { |kwrd| kwrd == selector.join + peek } && !at_end?
231
+ break if peek == NEWLINE
232
+ selector << advance
233
+ end
234
+
235
+ # final char in at-rule
236
+ selector << advance
237
+
238
+ current_at_rule = selector.join
239
+ Spec::CSS_AT_RULES.find { |kwrd| kwrd == current_at_rule }
240
+ end
241
+
232
242
  def prescan_import_rule(token)
233
243
  # we want to get all the text between the @import and the semicolon
234
244
  # so we can parse the file and add it to the ast
@@ -237,29 +247,33 @@ module Deadfire
237
247
  while peek != ";" && !at_end?
238
248
  advance
239
249
  end
250
+
251
+ add_token(:text)
240
252
 
241
253
  if at_end?
242
- @error_reporter.error(@line, "Unterminated import rule.")
254
+ @error_reporter.error(@line, "Imports must be terminated correctly with a ';'.")
243
255
  return
244
256
  end
245
257
 
246
- add_token(:text)
247
- advance # remove the semicolon
248
-
249
258
  text_token = @tokens.last
250
259
 
251
260
  text = text_token.lexeme.gsub(/\\|"/, '')
252
261
  file = FilenameHelper.resolve_import_path(text, @line)
253
262
 
254
- # file is ready for scanning
255
- content = File.read(file)
256
- scanner = Scanner.new(content, @error_reporter)
263
+ if file
264
+ # file is ready for scanning
265
+ content = File.read(file)
266
+ scanner = Scanner.new(content, @error_reporter)
257
267
 
258
- @tokens.pop # remove the text token
259
- @tokens.pop # remove the at_rule token
268
+ advance # remove the semicolon
269
+ @tokens.pop # remove the text token
270
+ @tokens.pop # remove the at_rule token
260
271
 
261
- imported_tokens = scanner.tokenize[0..-2]
262
- @tokens.concat imported_tokens
272
+ imported_tokens = scanner.tokenize[0..-2]
273
+ @tokens.concat imported_tokens
274
+ else
275
+ @error_reporter.error(@line, "File not found '#{text}'")
276
+ end
263
277
  end
264
278
  end
265
279
  end
@@ -13,7 +13,7 @@ module Deadfire
13
13
  visitor.visit_stylesheet_node(self)
14
14
  end
15
15
 
16
- def << (node)
16
+ def <<(node)
17
17
  @statements << node
18
18
  end
19
19
  end
@@ -1,12 +1,10 @@
1
- # Frozen_string_literal: true
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Deadfire
4
4
  class Interpreter # :nodoc:
5
- singleton_class.attr_accessor :cached_apply_rules
6
- self.cached_apply_rules = Hash.new { |h, k| h[k] = nil }
7
-
8
- def initialize(error_reporter)
5
+ def initialize(error_reporter, asset_loader)
9
6
  @error_reporter = error_reporter
7
+ @asset_loader = asset_loader
10
8
  end
11
9
 
12
10
  def interpret(node)
@@ -27,8 +25,8 @@ module Deadfire
27
25
  if node.block
28
26
  visit_block_node(node.block, node)
29
27
 
30
- unless Interpreter.cached_apply_rules[node.selector.selector]
31
- Interpreter.cached_apply_rules[node.selector.selector] = node.block if node.selector.cacheable?
28
+ unless @asset_loader.cached_css(node.selector.selector)
29
+ @asset_loader.cache_css(node.selector.selector, node.block) if node.selector.cacheable?
32
30
  end
33
31
  end
34
32
  end
@@ -44,10 +42,6 @@ module Deadfire
44
42
  end
45
43
  end
46
44
 
47
- def visit_declaration_node(node)
48
- node.accept(self)
49
- end
50
-
51
45
  def visit_comment_node(node)
52
46
  # node.accept(self)
53
47
  end
@@ -64,9 +58,7 @@ module Deadfire
64
58
  def apply_mixin(mixin, node)
65
59
  updated_declarations = []
66
60
  mixin.mixin_names.each do |mixin_name|
67
- if Interpreter.cached_apply_rules[mixin_name]
68
- cached_block = Interpreter.cached_apply_rules[mixin_name]
69
-
61
+ if cached_block = @asset_loader.cached_css(mixin_name)
70
62
  # NOTE: remove the left and right brace but we probably don't need to do this, how can this be simplified?
71
63
  cached_block.declarations[1...-1].each do |cached_declaration|
72
64
  updated_declarations << cached_declaration
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deadfire
4
+ class MixinParser # :nodoc:
5
+ attr_reader :mixins
6
+
7
+ def initialize
8
+ @mixins = {}
9
+ end
10
+
11
+ def interpret(node)
12
+ node.accept(self)
13
+ end
14
+
15
+ def visit_stylesheet_node(node)
16
+ node.statements.each { |child| child.accept(self) }
17
+ end
18
+
19
+ def visit_at_rule_node(node)
20
+ if node.block
21
+ visit_block_node(node.block, node)
22
+ end
23
+ end
24
+
25
+ def visit_ruleset_node(node)
26
+ if node.block
27
+ visit_block_node(node.block, node)
28
+
29
+ if node.selector.cacheable?
30
+ if @mixins.key?(node.selector.selector)
31
+ Deadfire.config.logger.warn("Mixin '#{node.selector.selector}' will be overrided with a new value.")
32
+ end
33
+
34
+ @mixins[node.selector.selector] = node.block
35
+ end
36
+ end
37
+ end
38
+
39
+ def visit_block_node(node, parent)
40
+ end
41
+
42
+ def visit_comment_node(node)
43
+ end
44
+
45
+ def visit_apply_node(node)
46
+ end
47
+
48
+ def visit_newline_node(node)
49
+ end
50
+ end
51
+ end
@@ -7,12 +7,13 @@ module Deadfire
7
7
  def initialize(content, options = {})
8
8
  @error_reporter = ErrorReporter.new
9
9
  @options = {}
10
+ @asset_loader = AssetLoader.new(options[:filename])
10
11
  @scanner = FrontEnd::Scanner.new(content, error_reporter)
11
12
  end
12
13
 
13
14
  def parse
14
15
  ast = _parse
15
- interpreter = Interpreter.new(error_reporter)
16
+ interpreter = Interpreter.new(error_reporter, @asset_loader)
16
17
  ast.statements.each do |node|
17
18
  interpreter.interpret(node)
18
19
  end
@@ -31,6 +32,15 @@ module Deadfire
31
32
  @error_reporter.errors?
32
33
  end
33
34
 
35
+ def load_mixins
36
+ ast = _parse
37
+ parser = MixinParser.new
38
+ ast.statements.each do |node|
39
+ parser.interpret(node)
40
+ end
41
+ parser.mixins
42
+ end
43
+
34
44
  private
35
45
 
36
46
  def _parse
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+ require "rails"
3
+ require "propshaft"
4
+
5
+ module Deadfire
6
+ class Railties < ::Rails::Railtie
7
+ config.deadfire = ActiveSupport::OrderedOptions.new
8
+ config.deadfire.root_path = nil
9
+ config.deadfire.excluded_files = []
10
+
11
+ initializer :deadfire do |app|
12
+ Deadfire.configure do |deadfire_config|
13
+ deadfire_config.root_path = config.deadfire.root_path || app.root.join("app", "assets", "stylesheets")
14
+ deadfire_config.excluded_files = config.deadfire.excluded_files
15
+ deadfire_config.compressed = config.assets.compressed
16
+ deadfire_config.logger = config.assets.logger || Rails.logger
17
+ end
18
+ end
19
+
20
+ class DeadfireCompiler < ::Propshaft::Compiler
21
+ def compile(logical_path, input)
22
+ path = logical_path.path.to_s
23
+
24
+ return input if Deadfire.config.excluded_files.include?(path)
25
+
26
+ # by default, all files in the will be preprocessed
27
+ if Deadfire.config.asset_registry.settings.empty?
28
+ all_files = Dir.glob("#{Deadfire.config.root_path}/**/*.css")
29
+ Deadfire.config.preprocess(*all_files)
30
+ end
31
+
32
+ Deadfire.parse(input, filename: logical_path.logical_path.to_s)
33
+ end
34
+ end
35
+
36
+ config.assets.compilers << ["text/css", DeadfireCompiler]
37
+ end
38
+ end
data/lib/deadfire/spec.rb CHANGED
@@ -17,35 +17,36 @@ module Deadfire
17
17
  # value = any-value [ ',' S* any-value ]*;
18
18
  # any-value = IDENT | STRING | NUMBER | PERCENTAGE | DIMENSION | COLOR | URI | FUNCTION any-value* ')' | '(' any-value* ')' | '[' any-value* ']' | '{' any-value* '}' | ';';
19
19
 
20
- # -- SASS features
21
-
22
- # Example: @apply button;
23
- # apply-rule = "@apply" S* mixin-name S* ";" S*
24
- # mixin-name = IDENT
25
-
26
- # Example: button { color: red; &.active { color: blue } }
27
- # nested-selector = selector S* "{" S* declaration-list S* "}"
28
- # selector = simple-selector [ "&" simple-selector ]
29
- # simple-selector = element-name [ "#" id ] [ "." class ]*
30
- # element-name = IDENT
31
- # id = IDENT
32
- # class = "." IDENT
33
- # declaration-list = declaration [ ";" S* declaration ]*
20
+ CHARSET = "@charset"
21
+ IMPORT = "@import"
22
+ MEDIA = "@media"
23
+ PAGE = "@page"
24
+ FONT_FACE = "@font-face"
25
+ KEYFRAMES = "@keyframes"
26
+ WEBKIT_KEYFRAMES = "@-webkit-keyframes"
27
+ SUPPORTS = "@supports"
28
+ NAMESPACE = "@namespace"
29
+ COUNTER_STYLE = "@counter-style"
30
+ VIEWPORT = "@viewport"
31
+ DOCUMENT = "@document"
32
+ APPLY = "@apply"
33
+ LAYER = "@layer"
34
34
 
35
35
  CSS_AT_RULES = [
36
- "@charset",
37
- "@import",
38
- "@media",
39
- "@page",
40
- "@font-face",
41
- "@keyframes",
42
- "@supports",
43
- "@namespace",
44
- "@counter-style",
45
- "@viewport",
46
- "@document",
47
- "@apply",
48
- "@layer"
36
+ CHARSET,
37
+ IMPORT,
38
+ MEDIA,
39
+ PAGE,
40
+ FONT_FACE,
41
+ KEYFRAMES,
42
+ WEBKIT_KEYFRAMES,
43
+ SUPPORTS,
44
+ NAMESPACE,
45
+ COUNTER_STYLE,
46
+ VIEWPORT,
47
+ DOCUMENT,
48
+ APPLY,
49
+ LAYER
49
50
  ]
50
51
 
51
52
  CSS_SELECTORS = [
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Deadfire
4
- VERSION = "0.3.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/deadfire.rb CHANGED
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "deadfire/ast_printer"
4
- require_relative "deadfire/css_buffer"
4
+ require_relative "deadfire/asset_loader"
5
+ require_relative "deadfire/asset_registry"
5
6
  require_relative "deadfire/css_generator"
6
7
  require_relative "deadfire/configuration"
7
8
  require_relative "deadfire/errors"
8
9
  require_relative "deadfire/error_reporter"
9
10
  require_relative "deadfire/interpreter"
10
- require_relative "deadfire/parser"
11
+ require_relative "deadfire/mixin_parser"
11
12
  require_relative "deadfire/parser_engine"
12
13
  require_relative "deadfire/spec"
13
14
  require_relative "deadfire/filename_helper"
@@ -26,6 +27,8 @@ require_relative "deadfire/front_end/selector_node"
26
27
  require_relative "deadfire/front_end/stylesheet_node"
27
28
 
28
29
  module Deadfire
30
+ PERMISSIBLE_FILE_EXTENSIONS = [".css", ".scss"].freeze
31
+
29
32
  class << self
30
33
  attr_reader :config
31
34
 
@@ -42,9 +45,15 @@ module Deadfire
42
45
  end
43
46
 
44
47
  def parse(content, options = {})
45
- parser = ParserEngine.new(content)
46
- # TODO: hook into a logger and report the errors
48
+ configure do |config|
49
+ config.root_path = options[:root_path]
50
+ config.compressed = options[:compressed]
51
+ end
52
+
53
+ parser = ParserEngine.new(content, filename: options[:filename])
47
54
  parser.parse
48
55
  end
49
56
  end
50
57
  end
58
+
59
+ require_relative "deadfire/railtie"