deadfire 0.2.0 → 0.4.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: 681339c949e040e68156a6f391c2a0cb87a50fa84e7cff27e3d8dfe7e746b61a
4
- data.tar.gz: 952f7522e6034261f9dd2b7912deea46fa92e43a068fa53b6ff2658db8e0cd36
3
+ metadata.gz: 82f838eee7d8e2ef83b302d7de509c428af8584567892296fd3011df42cb0990
4
+ data.tar.gz: 1a4b828032eda5dae38d3da5f820081402f7e66f919b2720ff8f9be65d6d8bf0
5
5
  SHA512:
6
- metadata.gz: 7b72cecd27d13dfbcb84f1c06b19b4a0ef615df59d5cba0a4ffa544e423660dc24f8b445e9b92ef06f58fc71ef12140867e5c0fd4e45d9cbf28dd9fae15b64f6
7
- data.tar.gz: 4c81de35669b3e560d8ee5103cd94a2ba45f703b27c64b1c5a26dc46dfefd35ac1f0825c26667b7d8c3e76ee27df0764dad571ddc0c4d652e88228cd494921d1
6
+ metadata.gz: a724bdb7b939e5b4404ee5cee4266ad6c278ac5d4e7fc74f560884f4b30fc123c3492a52abd5028adbc9f579dd690beb26d0de2460e8cb3b8e482d44ce77e733
7
+ data.tar.gz: e353841bfb20e25ff0ff1556d555443546783a744cf96b2a2149862cf8dbebb31bb9c48f50d68f20355ea036c36e22ca4ac9f059413df500974f4888849673bd
@@ -6,7 +6,7 @@ jobs:
6
6
  strategy:
7
7
  fail-fast: false
8
8
  matrix:
9
- ruby: [head, 3.1, 3.0, 2.7]
9
+ ruby: [head, 3.3, 3.2, 3.1, 3.0, 2.7]
10
10
  steps:
11
11
  - uses: actions/checkout@v3
12
12
  - name: Set up Ruby
@@ -15,4 +15,4 @@ jobs:
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.2.0)
4
+ deadfire (0.4.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
 
@@ -51,59 +50,65 @@ Output;
51
50
 
52
51
  ### @apply
53
52
 
54
- @apply inlines your classes into your custom css.
53
+ The `@apply` directive inlines your classes into your custom CSS, simplifying the process of applying existing styles to a new class.
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
- Let's see an example of how to declare mixins and use the @apply directive.
57
+ Let's take a look at an example of how to use the @apply directive. Note that all utility classes are automatically cached.
61
58
 
62
59
  ```CSS
63
- :root {
64
- --font-bold: {
65
- font-weight: bold;
66
- }
60
+ .font-bold: {
61
+ font-weight: bold;
62
+ }
67
63
 
68
- --btn: {
69
- padding-bottom: 10px;
70
- text-align: center;
71
- }
64
+ .btn: {
65
+ padding-bottom: 10px;
66
+ text-align: center;
72
67
  }
73
68
  ```
74
69
 
75
- How can we use mixins? Using @apply...
70
+ Re-use the styles 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
82
+ ### Nesting
88
83
 
89
- Nesting adds the ability to nest one style rule inside another.
84
+ The CSS nesting feature, as described in the CSS Nesting specification (https://drafts.csswg.org/css-nesting/), allows for more intuitive and concise styling by enabling the nesting of CSS rules within one another. This feature simplifies the structure of stylesheets and improves readability.
90
85
 
91
- NOTE: This feature is still a work in progress.
86
+ Now that nesting has been upstreamed to CSS, meaning it is now a part of the official CSS specification. As a result, Deafire will leverage the native CSS nesting feature instead of implementing this feature (which was the original goal of this project).
92
87
 
93
- ```CSS
94
- /* & can be used on its own */
95
- .btn {
96
- color: blue;
97
- & > .homepage { color: red; }
88
+ Example:
89
+
90
+ ```css
91
+ .foo {
92
+ color: green;
93
+ }
94
+ .foo .bar {
95
+ font-size: 1.4rem;
96
+ }
97
+
98
+ /* can be simplified to */
99
+ .foo {
100
+ color: green;
101
+ .bar {
102
+ font-size: 1.4rem;
103
+ }
98
104
  }
99
105
  ```
100
106
 
101
- This is expanded to:
107
+ ### Fault tolerant
102
108
 
103
- ```CSS
104
- .btn { color: blue; }
105
- .btn > .homepage { color: red; }
106
- ```
109
+ 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.
110
+
111
+ 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
112
 
108
113
  ## Installation
109
114
 
@@ -123,7 +128,25 @@ Or install it yourself as:
123
128
 
124
129
  ## Deadfire + Ruby on Rails
125
130
 
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.
131
+ After adding Deadfire gem to your rails application, open the file `config/initializers/assets.rb` or create a new initializer `config/initializers/deadfire.rb`.
132
+
133
+ ### Propshaft
134
+
135
+ To setup Propshaft to use Deadfire as a preprocessor:
136
+
137
+ ```ruby
138
+ # config/initializers/assets.rb
139
+ class DeadfireCompiler < Propshaft::Compiler
140
+ def compile(logical_path, input)
141
+ Deadfire.parse(input, root_path: Rails.root.join("app", "assets", "stylesheets"))
142
+ end
143
+ end
144
+
145
+ Rails.application.config.assets.compilers << ["text/css", DeadfireCompiler]
146
+ ```
147
+ ### Sprockets
148
+
149
+ To setup Sprocket to use Deadfire as a preprocessor:
127
150
 
128
151
  ```ruby
129
152
  # config/initializers/assets.rb
@@ -156,4 +179,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
156
179
 
157
180
  ## Code of Conduct
158
181
 
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).
182
+ 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).
@@ -8,12 +8,13 @@ gemfile(true) do
8
8
  gem "sassc"
9
9
  gem "deadfire", github: "hahmed/deadfire", branch: "main"
10
10
  gem "syntax_tree-css"
11
+ gem "sass-embedded"
11
12
 
12
13
  gem "benchmark-ips"
13
14
  end
14
15
 
15
16
  css = <<~CSS
16
-
17
+ /* My very first css file! */
17
18
  body {
18
19
  font-family: helvetica, arial, sans-serif;
19
20
  font-size: calc(1.3em + 0.5vw);
@@ -25,9 +26,12 @@ body {
25
26
  }
26
27
 
27
28
  h1 {
28
- font-size: 40px;
29
+ font-size: 40px;
29
30
  }
30
-
31
+ /* Just
32
+ a
33
+ random
34
+ comment */
31
35
  a {
32
36
  font-size: 1em;
33
37
  vertical-align: baseline;
@@ -41,9 +45,7 @@ a {
41
45
  text-decoration-width: 0.1rem
42
46
  }
43
47
 
44
- .button--block {
45
- min-width:100% !important
46
- }
48
+ .button--block { min-width:100% !important }
47
49
 
48
50
  code {
49
51
  font-family: Roboto Mono;
@@ -51,26 +53,59 @@ code {
51
53
  color: gray;
52
54
  }
53
55
 
56
+ /* I like code blocks!!!!!======= */
57
+
54
58
  .banner {
55
59
  border: 1px solid #ccc;
56
60
  }
57
61
  CSS
58
62
 
59
- def dartsass
60
- system "sass benchmarks/input.scss output.css", exception: true
61
- end
62
-
63
63
  Benchmark.ips do |x|
64
64
  x.config(:time => 5, :warmup => 2)
65
65
 
66
- x.report("dartsass") { dartsass }
66
+ # x.report("dartsass") { dartsass }
67
67
  x.report("deadfire") { Deadfire.parse(css) }
68
68
  x.report("sassc") { SassC::Engine.new(css).render }
69
69
  x.report("sytanx_tree") { SyntaxTree::CSS.parse(css) }
70
+ x.report("dart sass") { Sass.compile_string(css) }
70
71
  x.compare!
71
72
  end
72
73
 
73
- # FYI
74
+ # May 2024: Re-added dart sass
75
+ # Warming up --------------------------------------
76
+ # deadfire 172.000 i/100ms
77
+ # sassc 85.000 i/100ms
78
+ # sytanx_tree 79.000 i/100ms
79
+ # dart sass 520.000 i/100ms
80
+ # Calculating -------------------------------------
81
+ # deadfire 1.680k (± 0.9%) i/s - 8.428k in 5.018094s
82
+ # sassc 816.292 (± 0.2%) i/s - 4.165k in 5.102378s
83
+ # sytanx_tree 750.421 (± 1.9%) i/s - 3.792k in 5.054908s
84
+ # dart sass 5.225k (± 4.7%) i/s - 26.520k in 5.090927s
85
+
86
+ # Comparison:
87
+ # dart sass: 5224.7 i/s
88
+ # deadfire: 1679.7 i/s - 3.11x slower
89
+ # sassc: 816.3 i/s - 6.40x slower
90
+ # sytanx_tree: 750.4 i/s - 6.96x slower
91
+
92
+ # Nov 2023: (Note: removed dart sass because I don't have it installed, need to re-run again)
93
+ # Warming up --------------------------------------
94
+ # deadfire 116.000 i/100ms
95
+ # sassc 69.000 i/100ms
96
+ # sytanx_tree 64.000 i/100ms
97
+ # Calculating -------------------------------------
98
+ # deadfire 1.164k (± 1.2%) i/s - 5.916k in 5.084777s
99
+ # sassc 695.721 (± 1.3%) i/s - 3.519k in 5.059025s
100
+ # sytanx_tree 635.684 (± 3.3%) i/s - 3.200k in 5.040489s
101
+
102
+ # Comparison:
103
+ # deadfire: 1163.6 i/s
104
+ # sassc: 695.7 i/s - 1.67x slower
105
+ # sytanx_tree: 635.7 i/s - 1.83x slower
106
+
107
+
108
+ # Sep 2022:
74
109
  # Warming up --------------------------------------
75
110
  # dartsass 1.000 i/100ms
76
111
  # deadfire 1.088k i/100ms
@@ -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,42 @@
1
1
  ## Changelog
2
+ ### 0.5.0 (current)
3
+
4
+ ### 0.4.0 (18 May 2024)
5
+ - Fix parsing comments that have 2 stars e.g. /**
6
+ - Adds a logger and a default setting that suppresses the logs which can be configured to report errors.
7
+ - Fixes issue with import's not parsing correctly when there is no ending semicolon.
8
+ - Added ci for ruby 3.3
9
+ - Add support for importing .scss files, making it easier to migrate from other libraries.
10
+ ```
11
+ @import "nav"
12
+ @import "sidebar.scss"
13
+ .image { padding: 2px; }
14
+ ```
15
+ Deadfire will look for the file nav.css, then nav.scss in the `config.root_path` in the case when a file extension is not included.
16
+
17
+ - Simplify the configuration by having one option called compressed instead of keep_newlines and keep_comments.
18
+
19
+ ### 0.3.0 (15 November 2023)
20
+
21
+ - 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.
22
+ - 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.
23
+ - In the next version I will drop the old parser and it's tests.
24
+ - Add support for Ruby 3.2
25
+ - Updated docs and added an example on how to get Deadfire working with Propshaft.
26
+
27
+ ### 0.2.0 (25 October 2022)
28
+
29
+ - Added build for ruby 3.0
30
+ - StringIO is now hidden and only visible on the buffer class.
31
+ - Fixed a bug with a css ruleset after a nested block was being ignored, example;
32
+ ```
33
+ .title {
34
+ color: blue;
35
+ & .text { padding: 3px; }
36
+ }
37
+ .image { padding: 2px; }
38
+ ```
2
39
 
3
40
  ### 0.1.0 (17 October 2022)
4
41
 
5
- Initial release
42
+ 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
@@ -1,24 +1,37 @@
1
1
  # frozen_string_literal: true
2
+ require "logger"
2
3
 
3
4
  module Deadfire
4
5
  class Configuration
5
- attr_reader :directories, :root_path, :keep_comments
6
+ attr_reader :directories, :root_path, :compressed, :logger, :supressed
6
7
 
7
8
  def initialize
8
9
  @directories = []
9
10
  @root_path = ""
10
- @keep_comments = true
11
+ @compressed = false
12
+ @logger = Logger.new(STDOUT, level: :warn)
13
+ @supressed = true
11
14
  end
12
15
 
13
16
  def root_path=(value)
17
+ return if value.nil?
18
+
14
19
  unless Dir.exist?(value)
15
20
  raise DirectoryNotFoundError.new("Root not found #{value}")
16
21
  end
17
22
  @root_path = value
18
23
  end
19
24
 
20
- def keep_comments=(value)
21
- @keep_comments = value
25
+ def compressed=(value)
26
+ @compressed = value unless value.nil?
27
+ end
28
+
29
+ def logger=(value)
30
+ @logger = value
31
+ end
32
+
33
+ def supressed=(value)
34
+ @supressed = value
22
35
  end
23
36
  end
24
- end
37
+ end
@@ -0,0 +1,65 @@
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
+ node.value.each do |value|
23
+ @output << value.lexeme
24
+ end
25
+
26
+ if node.block
27
+ visit_block_node(node.block)
28
+ end
29
+ end
30
+
31
+ def visit_ruleset_node(node)
32
+ @output << node.selector.selector
33
+ @output << " "
34
+
35
+ visit_block_node(node.block)
36
+ end
37
+
38
+ def visit_block_node(node)
39
+ node.declarations.each do |declaration|
40
+ case declaration
41
+ when ApplyNode
42
+ visit_apply_node(declaration)
43
+ when FrontEnd::BlockNode
44
+ visit_block_node(declaration)
45
+ when FrontEnd::AtRuleNode
46
+ visit_at_rule_node(declaration)
47
+ else
48
+ @output << declaration.lexeme
49
+ end
50
+ end
51
+ end
52
+
53
+ def visit_newline_node(node)
54
+ @output << node.text
55
+ end
56
+
57
+ def visit_apply_node(node)
58
+ @output << node.node.lexeme
59
+ end
60
+
61
+ def visit_comment_node(node)
62
+ @output << node.comment.lexeme unless Deadfire.configuration.compressed
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,30 @@
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
+ error = Error.new(line, message)
13
+ Deadfire.configuration.logger.error(error.to_s) unless Deadfire.configuration.supressed
14
+ @errors << error
15
+ end
16
+
17
+ def errors?
18
+ @errors.any?
19
+ end
20
+
21
+ private
22
+
23
+ # create error struct with line and message
24
+ Error = Struct.new(:line, :message) do
25
+ def to_s
26
+ "Line #{line}: #{message}"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Deadfire
4
4
  class DirectoryNotFoundError < StandardError; end
5
- class Error < StandardError; end
6
5
 
7
6
  class DuplicateImportException < StandardError
8
7
  def initialize(filename = "", lineno = "")
@@ -27,16 +26,4 @@ module Deadfire
27
26
  super(msg)
28
27
  end
29
28
  end
30
-
31
- class EarlyApplyException < StandardError
32
- def initialize(input = "", lineno = "")
33
- msg = if input
34
- "Error with input: `#{input}` line: #{lineno}"
35
- else
36
- "Apply called too early in css. There are no mixins defined."
37
- end
38
-
39
- super(msg)
40
- end
41
- end
42
- end
29
+ end
@@ -0,0 +1,49 @@
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
+ 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
+ end
16
+ end
17
+
18
+ def normalize_path(line)
19
+ path = line.split.last
20
+ path.gsub!("\"", "")
21
+ path.gsub!("\'", "")
22
+ path.gsub!(";", "")
23
+ path
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
47
+ end
48
+ end
49
+ end