deadfire 0.1.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b22d5497d512927d87bb9f0d4166b055281e8c684188b14b2c03b4052256fe0
4
- data.tar.gz: c50413354b6e0504777d2111bf83a9f5e39a97f64f2fa14b292fd0277128f7be
3
+ metadata.gz: 570d2b607521baef50439b60e78f17aafcc9a6a53a258c2248967c6308964d27
4
+ data.tar.gz: ad3dc3a1ff0b925408cf64d73f41d659f7de3fbd2014d6d73ef55829d6246e68
5
5
  SHA512:
6
- metadata.gz: 922f9283b553c8c6f92d4e8de65362f1d75b29e028b5a18ab3444e2727cbc09295c176ec5b0359d879dd11a5f942e626ab9256d1cb2f4c0f39c9c13200aca06f
7
- data.tar.gz: 1bb7bbae48bd1e170c036ad176d3f0dd22b5546b87b2ec874f1fa9d0820b1ba23c7cdd2096caac8673be42d32be4e14063ae44d79d7fc66134d670b41ccc56e7
6
+ metadata.gz: d23aee619e6429b4e3dc964a833f46ba8cba5275aea228d3945a6a240308366e067563a1fea45310a13e69414280f3d831a0e7f298596172f1fd5da44ad67235
7
+ data.tar.gz: 911f68ad428ed00b46a9062cd96af4ff50fb2459aa2fff1be796fac7651b77e88f0e405c2da8b30d38a8d21a01ae5d0c0ace7daa245deac032e6450747f08c76
@@ -6,13 +6,13 @@ jobs:
6
6
  strategy:
7
7
  fail-fast: false
8
8
  matrix:
9
- ruby: [head, 3, 2.7]
9
+ ruby: [head, 3.2, 3.1, 3.0, 2.7]
10
10
  steps:
11
- - uses: actions/checkout@v2
11
+ - uses: actions/checkout@v3
12
12
  - name: Set up Ruby
13
13
  uses: ruby/setup-ruby@v1
14
14
  with:
15
15
  ruby-version: ${{ matrix.ruby }}
16
16
  bundler-cache: true
17
17
  - name: Run tests
18
- run: bundle exec rake
18
+ run: bundle exec rake
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- deadfire (0.1.0)
4
+ deadfire (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A lightweight CSS preprocessor.
4
4
 
5
- Use plain ol' CSS with a little bit of @import, @apply and nestings.
5
+ Use plain ol' CSS with a little bit of @import and @apply.
6
6
 
7
7
  CSS is a staple technology when building web applications. With the introduction of LESS, SASS, SCSS it made CSS easier to maintain. However, most of these tools are no longer supported, maintained or have far too many features (wait... that's a bad thing?).
8
8
 
@@ -14,7 +14,6 @@ Deadfire can be used with or without a CSS framework.
14
14
 
15
15
  - [x] @import
16
16
  - [x] [@apply](https://tabatkins.github.io/specs/css-apply-rule/)
17
- - [x] [nesting](https://drafts.csswg.org/css-nesting-1)
18
17
 
19
18
  ### @import
20
19
 
@@ -55,20 +54,16 @@ Output;
55
54
 
56
55
  The CSS apply rule was [proposed to be included into CSS](https://tabatkins.github.io/specs/css-apply-rule/) however it was abandoned. Mixins simplify applying existing css to a new class.
57
56
 
58
- All mixins must be declared on the `:root` element or preloaded via the `Deadfire.mixins` method. Using a mixin before it's declared will raise an `EarlyApplyException`. Ideally the `:root` element should appear near the top of the document.
59
-
60
57
  Let's see an example of how to declare mixins and use the @apply directive.
61
58
 
62
59
  ```CSS
63
- :root {
64
- --font-bold: {
65
- font-weight: bold;
66
- }
67
-
68
- --btn: {
69
- padding-bottom: 10px;
70
- text-align: center;
71
- }
60
+ .font-bold: {
61
+ font-weight: bold;
62
+ }
63
+
64
+ .btn: {
65
+ padding-bottom: 10px;
66
+ text-align: center;
72
67
  }
73
68
  ```
74
69
 
@@ -76,34 +71,19 @@ How can we use mixins? Using @apply...
76
71
 
77
72
  ```CSS
78
73
  .btn-blue {
79
- @apply --btn --font-bold;
74
+ @apply .btn .font-bold;
80
75
  }
81
76
 
82
77
  .homepage-hero {
83
- @apply --font-bold;
78
+ @apply .font-bold;
84
79
  }
85
80
  ```
86
81
 
87
- ### nesting
88
-
89
- Nesting adds the ability to nest one style rule inside another.
82
+ ### Fault tolerant
90
83
 
91
- NOTE: This feature is still a work in progress.
84
+ When Deadfire encounters an error, such as a missing mixin or other issues, it does not immediately raise an error that would halt the execution. Instead, it continues processing the CSS code and collects the encountered errors. These errors are then reported through the ErrorReporter class, allowing you to handle or display them as needed.
92
85
 
93
- ```CSS
94
- /* & can be used on its own */
95
- .btn {
96
- color: blue;
97
- & > .homepage { color: red; }
98
- }
99
- ```
100
-
101
- This is expanded to:
102
-
103
- ```CSS
104
- .btn { color: blue; }
105
- .btn > .homepage { color: red; }
106
- ```
86
+ By adopting this fault-tolerant approach, Deadfire aims to provide more flexibility and resilience when dealing with CSS code that may contain errors or inconsistencies. It allows you to gather information about the encountered issues and take appropriate actions based on the reported errors.
107
87
 
108
88
  ## Installation
109
89
 
@@ -123,7 +103,25 @@ Or install it yourself as:
123
103
 
124
104
  ## Deadfire + Ruby on Rails
125
105
 
126
- After adding Deadfire gem to your rails application, open the file `config/initializers/assets.rb` to setup your Sprocket and the asset pipeline to use the new preprocessor.
106
+ After adding Deadfire gem to your rails application, open the file `config/initializers/assets.rb` or create a new initializer `config/initializers/deadfire.rb`.
107
+
108
+ ### Propshaft
109
+
110
+ To setup Propshaft to use Deadfire as a preprocessor:
111
+
112
+ ```ruby
113
+ # config/initializers/assets.rb
114
+ class DeadfireCompiler < Propshaft::Compiler
115
+ def compile(logical_path, input)
116
+ Deadfire.parse(input, root_path: Rails.root.join("app", "assets", "stylesheets"))
117
+ end
118
+ end
119
+
120
+ Rails.application.config.assets.compilers << ["text/css", DeadfireCompiler]
121
+ ```
122
+ ### Sprockets
123
+
124
+ To setup Sprocket to use Deadfire as a preprocessor:
127
125
 
128
126
  ```ruby
129
127
  # config/initializers/assets.rb
@@ -156,4 +154,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
156
154
 
157
155
  ## Code of Conduct
158
156
 
159
- Everyone interacting in the Deadfire project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/hahmed/deadfire/blob/master/CODE_OF_CONDUCT.md).
157
+ Everyone interacting in the Deadfire project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/hahmed/deadfire/blob/master/CODE_OF_CONDUCT.md).
@@ -13,7 +13,7 @@ gemfile(true) do
13
13
  end
14
14
 
15
15
  css = <<~CSS
16
-
16
+ /* My very first css file! */
17
17
  body {
18
18
  font-family: helvetica, arial, sans-serif;
19
19
  font-size: calc(1.3em + 0.5vw);
@@ -27,7 +27,10 @@ body {
27
27
  h1 {
28
28
  font-size: 40px;
29
29
  }
30
-
30
+ /* Just
31
+ a
32
+ random
33
+ comment */
31
34
  a {
32
35
  font-size: 1em;
33
36
  vertical-align: baseline;
@@ -41,9 +44,7 @@ a {
41
44
  text-decoration-width: 0.1rem
42
45
  }
43
46
 
44
- .button--block {
45
- min-width:100% !important
46
- }
47
+ .button--block { min-width:100% !important }
47
48
 
48
49
  code {
49
50
  font-family: Roboto Mono;
@@ -51,6 +52,8 @@ code {
51
52
  color: gray;
52
53
  }
53
54
 
55
+ /* I like code blocks!!!!!======= */
56
+
54
57
  .banner {
55
58
  border: 1px solid #ccc;
56
59
  }
@@ -0,0 +1,23 @@
1
+ require "bundler/inline"
2
+ require 'benchmark'
3
+
4
+ gemfile(true) do
5
+ source "https://rubygems.org"
6
+
7
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
8
+
9
+ gem "deadfire", path: "./" #github: "hahmed/deadfire", branch: "main"
10
+ end
11
+
12
+ css = File.read("./benchmarks/tailwind.css")
13
+
14
+ puts css.inspect
15
+ puts "---"
16
+
17
+
18
+ time = Benchmark.measure do
19
+ parser = Deadfire::ParserEngine.new(css)
20
+ parser.parse
21
+ end
22
+
23
+ puts time.real
data/bin/console CHANGED
@@ -6,9 +6,22 @@ require "deadfire"
6
6
  # You can add fixtures and/or initialization code here to make experimenting
7
7
  # with your gem easier. You can also use a different console, if you like.
8
8
 
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
9
+ # simple css test
10
+ css = <<-CSS
11
+ @page :first { margin: 1cm; }
12
+ CSS
13
+
14
+ # @charset "UTF-8";
15
+ # @viewport { width: device-width; }
16
+ # @font-face {
17
+ # font-family: "Open Sans";
18
+ # }
19
+
20
+ puts css.inspect
21
+
22
+ parser = Deadfire::ParserEngine.new(css)
23
+ parser.print_ast
24
+ puts parser.parse
12
25
 
13
26
  require "irb"
14
27
  IRB.start(__FILE__)
data/changelog.md CHANGED
@@ -1,5 +1,25 @@
1
1
  ## Changelog
2
+ ### 0.3.0 (15 November 2023)
3
+
4
+ - Redo the parser by splitting up the tokenizer, parser, interpreter and generator phases which makes each step simpler. It's still faster than sassc but much slower than it was previously which is something I hope to address soon.
5
+ - Key thing to note is that Deadfire now only handles @imports and mixins. Nesting is now a feature of CSS, which means Deadfire is much simpler as a result.
6
+ - In the next version I will drop the old parser and it's tests.
7
+ - Add support for Ruby 3.2
8
+ - Updated docs and added an example on how to get Deadfire working with Propshaft.
9
+
10
+ ### 0.2.0 (25 October 2022)
11
+
12
+ - Added build for ruby 3.0
13
+ - StringIO is now hidden and only visible on the buffer class.
14
+ - Fixed a bug with a css ruleset after a nested block was being ignored, example;
15
+ ```
16
+ .title {
17
+ color: blue;
18
+ & .text { padding: 3px; }
19
+ }
20
+ .image { padding: 2px; }
21
+ ```
2
22
 
3
23
  ### 0.1.0 (17 October 2022)
4
24
 
5
- Initial release
25
+ Initial release
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deadfire
4
+ class AstPrinter # :nodoc:
5
+ def initialize
6
+ @indentation = 0
7
+ end
8
+
9
+ def print(node)
10
+ node.accept(self)
11
+ end
12
+
13
+ def visit_stylesheet_node(node)
14
+ puts "StylesheetNode"
15
+ node.statements.each do |statement|
16
+ # something
17
+ end
18
+ end
19
+
20
+ def visit_at_rule_node(node)
21
+ puts "AtRuleNode"
22
+ puts " AtKeyword: #{node.at_keyword.lexeme}"
23
+ node.value.each do |value|
24
+ puts " Value: #{value}"
25
+ end
26
+ if node.block
27
+ visit_block_node(node.block)
28
+ end
29
+ end
30
+
31
+ def visit_block_node(node)
32
+ puts "BlockNode"
33
+ node.declarations.each do |declaration|
34
+ case declaration
35
+ when FrontEnd::Token
36
+ puts " Declaration: #{declaration.lexeme}"
37
+ when FrontEnd::AtRuleNode
38
+ visit_at_rule_node(declaration)
39
+ when FrontEnd::RulesetNode
40
+ visit_ruleset_node(declaration)
41
+ end
42
+ end
43
+ end
44
+
45
+ def visit_ruleset_node(node)
46
+ puts "RulesetNode"
47
+ puts " Selector: #{node.selector}"
48
+ if node.block
49
+ visit_block_node(node.block)
50
+ end
51
+ end
52
+
53
+ def visit_comment_node(node)
54
+ puts "CommentNode"
55
+ puts " Comment: #{node.comment.lexeme}"
56
+ end
57
+ end
58
+ end
@@ -2,12 +2,13 @@
2
2
 
3
3
  module Deadfire
4
4
  class Configuration
5
- attr_reader :directories, :root_path, :keep_comments
5
+ attr_reader :directories, :root_path, :keep_comments, :keep_whitespace
6
6
 
7
7
  def initialize
8
8
  @directories = []
9
9
  @root_path = ""
10
10
  @keep_comments = true
11
+ @keep_whitespace = true
11
12
  end
12
13
 
13
14
  def root_path=(value)
@@ -20,5 +21,9 @@ module Deadfire
20
21
  def keep_comments=(value)
21
22
  @keep_comments = value
22
23
  end
24
+
25
+ def keep_whitespace=(value)
26
+ @keep_whitespace = value
27
+ end
23
28
  end
24
- end
29
+ end
@@ -2,9 +2,36 @@
2
2
  require "stringio"
3
3
 
4
4
  module Deadfire
5
- class CssBuffer < StringIO
5
+ class CssBuffer
6
+ attr_reader :lineno, :buffer
7
+
6
8
  def initialize(content)
7
- super(content)
9
+ @content = StringIO.new(content)
10
+ @buffer = []
11
+ @lineno = 0
12
+ end
13
+
14
+ def gets(skip_buffer: false)
15
+ output = content.gets
16
+ if output && !skip_buffer
17
+ buffer << output
18
+ end
19
+ @lineno += 1
20
+ output
21
+ end
22
+
23
+ def peek
24
+ output = content.gets
25
+ content.ungetc(output)
26
+ output
8
27
  end
28
+
29
+ def eof?
30
+ content.eof?
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :content
9
36
  end
10
- end
37
+ end
@@ -0,0 +1,66 @@
1
+ # Frozen_string_literal: true
2
+ require "stringio"
3
+
4
+ module Deadfire
5
+ class CssGenerator # :nodoc:
6
+ def initialize(tree)
7
+ @tree = tree
8
+ @output = StringIO.new # TODO: write to file instead of string buffer in temp folder
9
+ end
10
+
11
+ def generate
12
+ @tree.accept(self)
13
+ @output.string
14
+ end
15
+
16
+ def visit_stylesheet_node(node)
17
+ node.statements.each { |child| child.accept(self) }.join("\n")
18
+ end
19
+
20
+ def visit_at_rule_node(node)
21
+ @output << node.at_keyword.lexeme
22
+ @output << " "
23
+ node.value.each do |value|
24
+ @output << value.lexeme
25
+ end
26
+
27
+ if node.block
28
+ visit_block_node(node.block)
29
+ end
30
+ end
31
+
32
+ def visit_ruleset_node(node)
33
+ @output << node.selector.selector
34
+ @output << " "
35
+
36
+ visit_block_node(node.block)
37
+ end
38
+
39
+ def visit_block_node(node)
40
+ node.declarations.each do |declaration|
41
+ case declaration
42
+ when ApplyNode
43
+ visit_apply_node(declaration)
44
+ when FrontEnd::BlockNode
45
+ visit_block_node(declaration)
46
+ when FrontEnd::AtRuleNode
47
+ visit_at_rule_node(declaration)
48
+ else
49
+ @output << declaration.lexeme
50
+ end
51
+ end
52
+ end
53
+
54
+ def visit_newline_node(node)
55
+ @output << node.text
56
+ end
57
+
58
+ def visit_apply_node(node)
59
+ @output << node.node.lexeme
60
+ end
61
+
62
+ def visit_comment_node(node)
63
+ @output << node.comment.lexeme if Deadfire.configuration.keep_comments
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deadfire
4
+ class ErrorReporter # :nodoc:
5
+ attr_reader :errors
6
+
7
+ def initialize
8
+ @errors = []
9
+ end
10
+
11
+ def error(line, message)
12
+ @errors << Error.new(line, message)
13
+ end
14
+
15
+ def errors?
16
+ @errors.any?
17
+ end
18
+
19
+ private
20
+
21
+ # create error struct with line and message
22
+ Error = Struct.new(:line, :message)
23
+ end
24
+ end
@@ -39,4 +39,32 @@ module Deadfire
39
39
  super(msg)
40
40
  end
41
41
  end
42
+
43
+ class SyntaxError < StandardError
44
+ def initialize(message = "", lineno = "", original_line = "")
45
+ msg = if message
46
+ "#{original_line}\nline: #{lineno}: #{message}"
47
+ else
48
+ "Syntax "
49
+ end
50
+
51
+ super(msg)
52
+ end
53
+ end
54
+
55
+ class ErrorsList
56
+ attr_reader :errors
57
+
58
+ def initialize
59
+ @errors = []
60
+ end
61
+
62
+ def add(message:, lineno:, original_line:)
63
+ @errors << SyntaxError.new(message, lineno, original_line)
64
+ end
65
+
66
+ def empty?
67
+ @errors.empty?
68
+ end
69
+ end
42
70
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deadfire
4
+ class FilenameHelper
5
+ class << self
6
+ def resolve_import_path(line, lineno = 0)
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)
15
+ end
16
+
17
+ import_path
18
+ end
19
+
20
+ def normalize_path(line)
21
+ path = line.split.last
22
+ path.gsub!("\"", "")
23
+ path.gsub!("\'", "")
24
+ path.gsub!(";", "")
25
+ path
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,44 @@
1
+ # Frozen_string_literal: true
2
+
3
+ module Deadfire
4
+ class ApplyNode
5
+ attr_reader :node, :mixin_names
6
+
7
+ def initialize(node, mixin_names)
8
+ # TODO: mixin name can be single or multiple names, separated by a comma
9
+ @node = node
10
+ @mixin_names = fetch_mixin_name_from(mixin_names)
11
+ end
12
+
13
+ def accept(visitor)
14
+ visitor.visit_apply_node(self)
15
+ end
16
+
17
+ def lineno
18
+ node.lineno
19
+ end
20
+
21
+ private
22
+
23
+ def fetch_mixin_name_from(tokens)
24
+ @_cached_mixin_name ||= begin
25
+ names = []
26
+ current = []
27
+ tokens.each do |token|
28
+ case token.type
29
+ when :comma
30
+ names << current.join("")
31
+ current = []
32
+ current << token.lexeme
33
+ when :whitespace
34
+ # ignore whitespace
35
+ else
36
+ current << token.lexeme
37
+ end
38
+ end
39
+ names << current.join("")
40
+ names
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deadfire
4
+ module FrontEnd
5
+ class AtRuleNode < BaseNode
6
+ attr_reader :at_keyword, :value, :block
7
+
8
+ def initialize(at_keyword, value, block)
9
+ @at_keyword = at_keyword
10
+ @value = value
11
+ @block = block
12
+ end
13
+
14
+ def accept(visitor)
15
+ visitor.visit_at_rule_node(self)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deadfire
4
+ module FrontEnd
5
+ class BaseNode
6
+ def accept
7
+ raise NotImplementedError
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deadfire
4
+ module FrontEnd
5
+ class BlockNode < BaseNode
6
+ attr_reader :declarations
7
+
8
+ def initialize
9
+ @declarations = []
10
+ end
11
+
12
+ def <<(node)
13
+ @declarations << node
14
+ end
15
+
16
+ def accept(visitor)
17
+ visitor.visit_block_node(self)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deadfire
4
+ module FrontEnd
5
+ class CommentNode < BaseNode
6
+ attr_reader :comment
7
+
8
+ def initialize(comment)
9
+ @comment = comment
10
+ end
11
+
12
+ def accept(visitor)
13
+ visitor.visit_comment_node(self)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deadfire
4
+ module FrontEnd
5
+ class NewlineNode < BaseNode
6
+ attr_reader :text
7
+
8
+ def initialize(text)
9
+ @text = text
10
+ end
11
+
12
+ def accept(visitor)
13
+ visitor.visit_newline_node(self)
14
+ end
15
+ end
16
+ end
17
+ end