lightningcss 1.33.0 → 1.33.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09297541239f07c2b22b3f699d90d366ad9479f28ad17a80fc3fba33c7b05b45'
4
- data.tar.gz: 7ed51af76692654c75dc7b5692380e4954413e76e6fcc404f0fc339bcec64216
3
+ metadata.gz: 7b5579e4627956aba7821a46282bf3ee3bb2a3267c9c83932fd4a95633c44172
4
+ data.tar.gz: 9fc12ae254f802644a823671c253dbd6db169f5eb574a438b4a697c71b33e0be
5
5
  SHA512:
6
- metadata.gz: 340e848e41f76321291393c84dde862a56511c24d07a4f495d2ca89dd5f84bd8d71a9ae4f4957f8503fa4c2055db835eea511ae4810df2a7193e2516dfe38502
7
- data.tar.gz: be421633d75dd27dd469aa1fa837b94c0abc0d2e48ad90641b9129b5a95ec88c3f0045dad93f4be2b0e4080d3884ad7e495d4f6b2441396c50eeb434b11b6d82
6
+ metadata.gz: 8aefa9384d3a4f5086869d5aa3aa4b7f5f0c7b99e0840fdfb1f076d7c129432de5ca145bf65e9af68cb71104d2ff5f7235dbfbb3fc68d7576702a6f1f154bca2
7
+ data.tar.gz: 60b8b2520cb4e0244ce8c0d5d72ad98d2fa81f1fa6fc5753a511e7850c2c45e030d90267a4acde99abce70bfb5ad662688023e431e9c22fab2617157dd2f1248
data/README.md CHANGED
@@ -141,6 +141,113 @@ LightningCSS.transform_style_attribute("color: #ff0000; border: none", minify: t
141
141
 
142
142
  Having no selectors and no names, it takes neither `scope` nor `css_modules`, and says so when given one.
143
143
 
144
+ #### Parsing
145
+
146
+ `parse` answers what Lightning CSS read a stylesheet as, without printing anything. It takes `filename` and `error_recovery`, the two options that change how a stylesheet is read.
147
+
148
+ ```ruby
149
+ parsed = LightningCSS.parse(".card .title:hover { color: #f00 }")
150
+
151
+ parsed.stylesheet #=> the AST exactly as Lightning CSS serializes it
152
+ parsed.warnings #=> []
153
+ parsed.source #=> ".card .title:hover { color: #f00 }"
154
+ ```
155
+
156
+ The AST is Lightning CSS's own, so its shapes are the ones its [JavaScript visitors](https://lightningcss.dev/transforms.html) see and [`lightningcss/node/ast.d.ts`](https://github.com/parcel-bundler/lightningcss/blob/master/node/ast.d.ts) describes. It is what Lightning CSS understood, not what was written: `#f00` reads as an `rgb` with `r` of `255.0`, and a `0` margin reads as `0px`.
157
+
158
+ #### Walking the AST
159
+
160
+ `root` answers the stylesheet as a `LightningCSS::Node`, which walks, reads its fields by name, and knows what it sits inside.
161
+
162
+ ```ruby
163
+ root = LightningCSS.parse(source).root
164
+
165
+ root.type #=> "stylesheet"
166
+ root.keys #=> the fields this node carries
167
+ root.fields #=> those fields and their values, without its type and location
168
+ root.child_nodes #=> the nodes directly under it
169
+ root.every("declaration") #=> every declaration in the stylesheet
170
+ root.each #=> an Enumerator over every node
171
+ ```
172
+
173
+ Every object in the tree is a node, so reads chain all the way down.
174
+
175
+ ```ruby
176
+ rule = root.rules.first
177
+
178
+ rule.type #=> "style"
179
+ rule.value.selectors.first.map(&:type) #=> ["class", "combinator", "class", "pseudo-class"]
180
+ rule.value.declarations.declarations.first.value.r
181
+ #=> 255.0
182
+ ```
183
+
184
+ Most objects say what they are with their own `type`. The stylesheet, a declaration block, and a declaration do not, so a node names those `"stylesheet"`, `"declaration-block"`, and `"declaration"` from where it sits. Anything else without a `type`, like the `value` a rule wraps, answers `nil`.
185
+
186
+ Inspecting a node shows every field it carries.
187
+
188
+ ```
189
+ #<LightningCSS::Node style value=#<LightningCSS::Node>>
190
+ #<LightningCSS::Node location=0:1 selectors=[... 1 item] declarations=#<LightningCSS::Node declaration-block> rules=[]>
191
+ #<LightningCSS::Node declaration property="color" value=#<LightningCSS::Node rgb>>
192
+ ```
193
+
194
+ Lightning CSS names its fields in camelCase, and a field answers to its snake_case name too.
195
+
196
+ ```ruby
197
+ block.important_declarations # the same field as block.importantDeclarations
198
+ query.media_queries # mediaQueries
199
+ ```
200
+
201
+ Lightning CSS records where each rule starts and nothing else, so `location` is a rule's line, counted from 0, and its column, counted from 1 in UTF-16 code units. There are no end positions, so a node has no `slice` and there is no `at(offset)`.
202
+
203
+ ```ruby
204
+ rule.value.location
205
+ #=> {"source_index" => 0, "line" => 0, "column" => 1}
206
+ ```
207
+
208
+ `ancestors` answers what a node sits inside, and `rule?` says whether it is a rule.
209
+
210
+ ```ruby
211
+ declaration = root.every("declaration").first
212
+ declaration.ancestors.find(&:rule?).type
213
+ #=> "style"
214
+ ```
215
+
216
+ Nodes pattern match, and nest, since a field holding an object comes back as a node. Patterns take snake_case names, and a node's `type` matches even when Lightning CSS gave it none.
217
+
218
+ ```ruby
219
+ declaration => { type: "declaration", property:, value: { type: "rgb", r: } }
220
+ property #=> "color"
221
+ r #=> 255.0
222
+ ```
223
+
224
+ `to_h` and `to_json` answer the AST the node wraps.
225
+
226
+ ```ruby
227
+ declaration.value.to_h
228
+ #=> {"type" => "rgb", "r" => 255.0, "g" => 0.0, "b" => 0.0, "alpha" => 1.0}
229
+ ```
230
+
231
+ `LightningCSS::Visitor` answers a node with the method named after its type, and walks through anything nothing answers.
232
+
233
+ ```ruby
234
+ class Colors < LightningCSS::Visitor
235
+ def visit_style_rule(node)
236
+ puts node.value.location["line"]
237
+
238
+ visit_children(node)
239
+ end
240
+
241
+ def visit_declaration(node)
242
+ puts node.property
243
+ end
244
+ end
245
+
246
+ Colors.new.visit(LightningCSS.parse(source))
247
+ ```
248
+
249
+ A rule is answered as one, as in `visit_style_rule`, `visit_media_rule`, or `visit_namespace_rule`. Lightning CSS calls the rule `style`, but `namespace` and `nesting` are also parts of a selector, so `visit_namespace` answers `svg|rect` and `visit_namespace_rule` answers `@namespace`. Hyphens become underscores, so a `pseudo-class` is answered by `visit_pseudo_class`.
250
+
144
251
  #### Reusing options
145
252
 
146
253
  `LightningCSS::Transformer` holds a set of options to use across many stylesheets. Options given to a call are merged over the ones it was built with.
@@ -155,6 +262,8 @@ transformer.with(scope: "[s]")
155
262
 
156
263
  It answers `call` as well, so it can be handed to anything expecting something callable.
157
264
 
265
+ `transformer.parse` reads with the `filename` and `error_recovery` it holds and leaves the rest, which only matter to printing.
266
+
158
267
  ### Options
159
268
 
160
269
  | Option | Type | Description |
@@ -23,6 +23,8 @@ typedef struct LightningCssResult {
23
23
 
24
24
  struct LightningCssResult lightningcss_transform(const char *code, const char *options_json);
25
25
 
26
+ struct LightningCssResult lightningcss_parse(const char *code, const char *options_json);
27
+
26
28
  struct LightningCssResult lightningcss_transform_style_attribute(const char *code,
27
29
  const char *options_json);
28
30
 
@@ -90,6 +90,12 @@ static VALUE rb_transform(VALUE self, VALUE code, VALUE options) {
90
90
  return call(lightningcss_transform, code, options);
91
91
  }
92
92
 
93
+ static VALUE rb_parse(VALUE self, VALUE code, VALUE options) {
94
+ (void) self;
95
+
96
+ return call(lightningcss_parse, code, options);
97
+ }
98
+
93
99
  static VALUE rb_transform_style_attribute(VALUE self, VALUE code, VALUE options) {
94
100
  (void) self;
95
101
 
@@ -123,6 +129,7 @@ void Init_lightningcss(void) {
123
129
  rb_eOptionError = rb_define_class_under(rb_mLightningCSS, "OptionError", rb_eError);
124
130
  rb_eBundleError = rb_define_class_under(rb_mLightningCSS, "BundleError", rb_eError);
125
131
 
132
+ rb_define_singleton_method(rb_mBackend, "parse", rb_parse, 2);
126
133
  rb_define_singleton_method(rb_mBackend, "transform", rb_transform, 2);
127
134
  rb_define_singleton_method(rb_mBackend, "transform_style_attribute", rb_transform_style_attribute, 2);
128
135
  rb_define_singleton_method(rb_mBackend, "bundle", rb_bundle, 2);
@@ -8,6 +8,11 @@ module LightningCSS
8
8
  unavailable(__method__)
9
9
  end
10
10
 
11
+ #: (String, String) -> String
12
+ def parse(_code, _options_json)
13
+ unavailable(__method__)
14
+ end
15
+
11
16
  #: (String, String) -> String
12
17
  def transform_style_attribute(_code, _options_json)
13
18
  unavailable(__method__)
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LightningCSS
4
+ # One object in the AST Lightning CSS parsed a stylesheet to.
5
+ #
6
+ # root = LightningCSS.parse(".a { color: red }").root
7
+ #
8
+ # root.type #=> "stylesheet"
9
+ # root.every("declaration") #=> every declaration in the stylesheet
10
+ # root.rules.first.value.location #=> {"source_index" => 0, "line" => 0, "column" => 1}
11
+ #
12
+ # Every object in the tree is a node, so reads chain all the way down. Most say what they are
13
+ # with their own `type`. The stylesheet, a declaration block, and a declaration do not, so a node
14
+ # names those from where it sits.
15
+ #
16
+ class Node
17
+ include Enumerable #[LightningCSS::Node]
18
+
19
+ POSITION = ["type", "loc"].freeze #: Array[String]
20
+ DECLARATIONS = ["declarations", "importantDeclarations"].freeze #: Array[String]
21
+
22
+ attr_reader :parent #: LightningCSS::Node?
23
+ attr_reader :field #: String?
24
+
25
+ #: (Hash[String, untyped], ?LightningCSS::Node?, ?String?) -> void
26
+ def initialize(attributes, parent = nil, field = nil)
27
+ @attributes = attributes
28
+ @parent = parent
29
+ @field = field
30
+ end
31
+
32
+ #: () -> String?
33
+ def type
34
+ attributes.fetch("type") { inferred_type }
35
+ end
36
+
37
+ #: () -> String?
38
+ def underscored_type
39
+ return nil unless (name = type)
40
+
41
+ name = name.tr("-", "_").downcase
42
+
43
+ rule? ? "#{name}_rule" : name
44
+ end
45
+
46
+ #: () -> bool
47
+ def rule?
48
+ field == "rules" && attributes.key?("type")
49
+ end
50
+
51
+ #: () -> Hash[String, Integer]?
52
+ def location
53
+ attributes["loc"]
54
+ end
55
+
56
+ #: (String) -> untyped
57
+ def [](key)
58
+ attributes[key]
59
+ end
60
+
61
+ #: () -> Array[String]
62
+ def keys
63
+ attributes.keys
64
+ end
65
+
66
+ #: () -> Hash[String, untyped]
67
+ def to_h
68
+ attributes
69
+ end
70
+
71
+ #: (?untyped) -> String
72
+ def to_json(state = nil)
73
+ state ? attributes.to_json(state) : attributes.to_json
74
+ end
75
+
76
+ #: (Array[Symbol]?) -> Hash[Symbol, untyped]
77
+ def deconstruct_keys(keys)
78
+ found = {} #: Hash[Symbol, untyped]
79
+
80
+ if keys
81
+ keys.each do |name|
82
+ key = field_for(name.to_s)
83
+
84
+ if key
85
+ found[name] = wrap(attributes[key], key)
86
+ elsif name == :type && type
87
+ found[name] = type
88
+ end
89
+ end
90
+ else
91
+ found[:type] = type if type
92
+ attributes.each_key { |key| found[key.to_sym] = wrap(attributes[key], key) }
93
+ end
94
+
95
+ found
96
+ end
97
+
98
+ #: () -> Array[LightningCSS::Node]
99
+ def child_nodes
100
+ @child_nodes ||= attributes.flat_map { |key, value| key == "loc" ? [] : nodes_in(value, key) }.freeze
101
+ end
102
+
103
+ #: () { (LightningCSS::Node) -> void } -> void
104
+ #: () -> Enumerator[LightningCSS::Node, void]
105
+ def each(&)
106
+ return enum_for(:each) unless block_given?
107
+
108
+ yield self
109
+
110
+ child_nodes.each { |child| child.each(&) }
111
+ end
112
+
113
+ #: () -> Array[LightningCSS::Node]
114
+ def ancestors
115
+ parent ? [parent, *parent.ancestors] : []
116
+ end
117
+
118
+ #: (String) -> Array[LightningCSS::Node]
119
+ def every(type)
120
+ each.select { |node| node.type == type || node.underscored_type == type }
121
+ end
122
+
123
+ #: () -> Hash[String, untyped]
124
+ def fields
125
+ attributes.except(*POSITION)
126
+ end
127
+
128
+ #: () -> String
129
+ def inspect
130
+ described = fields.map { |key, value| described_field(key, value) }
131
+ position = location
132
+ described.unshift("location=#{position["line"]}:#{position["column"]}") if position
133
+
134
+ "#<#{self.class.name}#{" #{type}" if type}#{" #{described.join(" ")}" unless described.empty?}>"
135
+ end
136
+
137
+ #: (Symbol, *untyped) -> untyped
138
+ def method_missing(name, *arguments)
139
+ key = field_for(name.to_s)
140
+
141
+ return super unless key
142
+
143
+ wrap(attributes[key], key)
144
+ end
145
+
146
+ #: (Symbol, ?bool) -> bool
147
+ def respond_to_missing?(name, include_private = false)
148
+ !field_for(name.to_s).nil? || super
149
+ end
150
+
151
+ protected
152
+
153
+ attr_reader :attributes #: Hash[String, untyped]
154
+
155
+ private
156
+
157
+ #: () -> String?
158
+ def inferred_type
159
+ if parent.nil? && attributes.key?("rules") && attributes.key?("sources")
160
+ "stylesheet"
161
+ elsif attributes.key?("importantDeclarations")
162
+ "declaration-block"
163
+ elsif DECLARATIONS.include?(field) && attributes.key?("property")
164
+ "declaration"
165
+ end
166
+ end
167
+
168
+ #: (untyped, String) -> Array[LightningCSS::Node]
169
+ def nodes_in(value, key)
170
+ case value
171
+ when Hash
172
+ [Node.new(value, self, key)]
173
+ when Array
174
+ value.flat_map { |item| nodes_in(item, key) }
175
+ else
176
+ []
177
+ end
178
+ end
179
+
180
+ #: (String, untyped) -> String
181
+ def described_field(key, value)
182
+ case value
183
+ when Array
184
+ "#{key}=#{value.empty? ? "[]" : "[... #{counted(value)}]"}"
185
+ when Hash
186
+ type = Node.new(value, self, key).type
187
+
188
+ "#{key}=#<#{self.class.name}#{" #{type}" if type}>"
189
+ else
190
+ "#{key}=#{value.inspect}"
191
+ end
192
+ end
193
+
194
+ #: (Array[untyped]) -> String
195
+ def counted(items)
196
+ "#{items.length} #{items.length == 1 ? "item" : "items"}"
197
+ end
198
+
199
+ #: (String) -> String?
200
+ def field_for(name)
201
+ return name if attributes.key?(name)
202
+
203
+ camelized = name.gsub(/_([a-z\d])/) { Regexp.last_match(1).to_s.upcase }
204
+
205
+ camelized if attributes.key?(camelized)
206
+ end
207
+
208
+ #: (untyped, String) -> untyped
209
+ def wrap(value, key)
210
+ case value
211
+ when Hash
212
+ key == "loc" ? value : Node.new(value, self, key)
213
+ when Array
214
+ value.map { |item| wrap(item, key) }
215
+ else
216
+ value
217
+ end
218
+ end
219
+ end
220
+ end
@@ -26,6 +26,11 @@ module LightningCSS
26
26
  :targets
27
27
  ].freeze #: Array[Symbol]
28
28
 
29
+ PARSE = [
30
+ :filename,
31
+ :error_recovery
32
+ ].freeze #: Array[Symbol]
33
+
29
34
  BUNDLE = [
30
35
  :minify,
31
36
  :error_recovery,
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LightningCSS
4
+ # What a stylesheet parsed to.
5
+ #
6
+ # `stylesheet` is the AST exactly as Lightning CSS serializes it, and `root` is the same tree as a
7
+ # `LightningCSS::Node` to walk. `warnings` are the ones a transform would have reported.
8
+ #
9
+ class ParseResult
10
+ attr_reader :source #: String?
11
+ attr_reader :stylesheet #: Hash[String, untyped]
12
+ attr_reader :warnings #: Array[String]
13
+
14
+ #: (String, ?String?) -> LightningCSS::ParseResult
15
+ def self.from_json(payload, source = nil)
16
+ parsed = JSON.parse(payload)
17
+
18
+ new(
19
+ source: source,
20
+ stylesheet: parsed.fetch("stylesheet"),
21
+ warnings: parsed.fetch("warnings")
22
+ )
23
+ end
24
+
25
+ #: (stylesheet: Hash[String, untyped], warnings: Array[String], ?source: String?) -> void
26
+ def initialize(stylesheet:, warnings:, source: nil)
27
+ @source = source
28
+ @stylesheet = stylesheet.freeze
29
+ @warnings = warnings.freeze
30
+
31
+ freeze
32
+ end
33
+
34
+ #: () -> LightningCSS::Node
35
+ def root
36
+ Node.new(stylesheet)
37
+ end
38
+
39
+ #: () -> bool
40
+ def warnings?
41
+ !warnings.empty?
42
+ end
43
+
44
+ #: () -> String
45
+ def inspect
46
+ parts = ["rules=#{stylesheet.fetch("rules").length}"]
47
+ parts << "warnings=#{warnings.inspect}" if warnings?
48
+
49
+ "#<#{self.class.name} #{parts.join(" ")}>"
50
+ end
51
+ end
52
+ end
@@ -28,6 +28,11 @@ module LightningCSS
28
28
 
29
29
  alias call transform
30
30
 
31
+ #: (String, ?filename: String?, ?error_recovery: bool) -> LightningCSS::ParseResult
32
+ def parse(code, **overrides)
33
+ LightningCSS.parse(code, **options.slice(*Options::PARSE), **overrides)
34
+ end
35
+
31
36
  #: (String, ?minify: bool, ?error_recovery: bool, ?targets: browsers?, ?css_modules: css_modules?, ?scope: String?) -> LightningCSS::Result
32
37
  def bundle(path, **overrides)
33
38
  LightningCSS.bundle(path, **options, **overrides)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LightningCSS
4
- VERSION = "1.33.0"
4
+ VERSION = "1.33.0.1"
5
5
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LightningCSS
4
+ # Walks an AST, answering each node with the method named after it and walking through anything
5
+ # nothing answers.
6
+ #
7
+ # class Colors < LightningCSS::Visitor
8
+ # def visit_declaration(node)
9
+ # puts node.value.type if node.property == "color"
10
+ # end
11
+ # end
12
+ #
13
+ # Colors.new.visit(LightningCSS.parse(".a { color: red }"))
14
+ #
15
+ # A rule is answered as one, as in `visit_style_rule` or `visit_media_rule`, since `style`,
16
+ # `nesting`, and `namespace` are also what a selector is made of.
17
+ #
18
+ class Visitor
19
+ #: (LightningCSS::Node | LightningCSS::ParseResult) -> void
20
+ def visit(node)
21
+ node = node.root if node.is_a?(ParseResult)
22
+
23
+ method_name = "visit_#{node.underscored_type}" if node.underscored_type
24
+
25
+ method_name && respond_to?(method_name) ? public_send(method_name, node) : visit_children(node)
26
+
27
+ nil
28
+ end
29
+
30
+ #: (LightningCSS::Node) -> void
31
+ def visit_children(node)
32
+ node.child_nodes.each { |child| visit(child) }
33
+
34
+ nil
35
+ end
36
+ end
37
+ end
data/lib/lightningcss.rb CHANGED
@@ -15,6 +15,9 @@ end
15
15
 
16
16
  require_relative "lightningcss/options"
17
17
  require_relative "lightningcss/result"
18
+ require_relative "lightningcss/node"
19
+ require_relative "lightningcss/parse_result"
20
+ require_relative "lightningcss/visitor"
18
21
  require_relative "lightningcss/transformer"
19
22
 
20
23
  # Ruby bindings for Lightning CSS.
@@ -28,14 +31,27 @@ require_relative "lightningcss/transformer"
28
31
  # LightningCSS.transform(".title { color: red }", scope: "[data-scope-abc]").code
29
32
  # #=> ".title[data-scope-abc]{color:red}"
30
33
  #
34
+ # `parse` returns the AST instead, to walk with `LightningCSS::Node` and `LightningCSS::Visitor`:
35
+ #
36
+ # LightningCSS.parse(".a { color: red }").root.every("declaration").first.property
37
+ # #=> "color"
38
+ #
31
39
  # `LightningCSS::Options` is what the options are read by, and `LightningCSS::Transformer` holds a
32
40
  # set of them to reuse across many stylesheets.
41
+ #
33
42
  module LightningCSS
34
43
  #: (String, ?filename: String?, ?minify: bool, ?error_recovery: bool, ?targets: browsers?, ?css_modules: css_modules?, ?scope: String?) -> LightningCSS::Result
35
44
  def self.transform(code, **options)
36
45
  Result.from_json(Backend.transform(code.to_s, Options.serialize(options)))
37
46
  end
38
47
 
48
+ #: (String, ?filename: String?, ?error_recovery: bool) -> LightningCSS::ParseResult
49
+ def self.parse(code, **options)
50
+ serialized = Options.serialize(options, Options::PARSE, "a parse")
51
+
52
+ ParseResult.from_json(Backend.parse(code.to_s, serialized), code.to_s)
53
+ end
54
+
39
55
  #: (String, ?minify: bool, ?error_recovery: bool, ?targets: browsers?, ?css_modules: css_modules?, ?scope: String?) -> LightningCSS::Result
40
56
  def self.bundle(path, **options)
41
57
  serialized = Options.serialize(options, Options::BUNDLE, "a bundle")
data/rust/Cargo.lock CHANGED
@@ -90,6 +90,9 @@ name = "bitflags"
90
90
  version = "2.13.1"
91
91
  source = "registry+https://github.com/rust-lang/crates.io-index"
92
92
  checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
93
+ dependencies = [
94
+ "serde_core",
95
+ ]
93
96
 
94
97
  [[package]]
95
98
  name = "bitvec"
@@ -245,6 +248,7 @@ dependencies = [
245
248
  "dtoa-short",
246
249
  "itoa",
247
250
  "phf 0.13.1",
251
+ "serde",
248
252
  "smallvec",
249
253
  ]
250
254
 
@@ -511,6 +515,7 @@ dependencies = [
511
515
  "serde",
512
516
  "serde-content",
513
517
  "smallvec",
518
+ "static-self",
514
519
  ]
515
520
 
516
521
  [[package]]
@@ -599,7 +604,9 @@ dependencies = [
599
604
  "phf_codegen",
600
605
  "precomputed-hash",
601
606
  "rustc-hash",
607
+ "serde",
602
608
  "smallvec",
609
+ "static-self",
603
610
  ]
604
611
 
605
612
  [[package]]
@@ -1002,6 +1009,31 @@ name = "smallvec"
1002
1009
  version = "1.15.2"
1003
1010
  source = "registry+https://github.com/rust-lang/crates.io-index"
1004
1011
  checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
1012
+ dependencies = [
1013
+ "serde",
1014
+ ]
1015
+
1016
+ [[package]]
1017
+ name = "static-self"
1018
+ version = "0.1.2"
1019
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1020
+ checksum = "f6635404b73efc136af3a7956e53c53d4f34b2f16c95a15c438929add0f69412"
1021
+ dependencies = [
1022
+ "indexmap",
1023
+ "smallvec",
1024
+ "static-self-derive",
1025
+ ]
1026
+
1027
+ [[package]]
1028
+ name = "static-self-derive"
1029
+ version = "0.1.1"
1030
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1031
+ checksum = "5268c96d4b907c558a9a52d8492522d6c7b559651a5e1d8f2d551e461b9425d5"
1032
+ dependencies = [
1033
+ "proc-macro2",
1034
+ "quote",
1035
+ "syn 1.0.109",
1036
+ ]
1005
1037
 
1006
1038
  [[package]]
1007
1039
  name = "strsim"
data/rust/Cargo.toml CHANGED
@@ -15,7 +15,7 @@ path = "src/lib.rs"
15
15
  crate-type = ["cdylib", "staticlib", "rlib"]
16
16
 
17
17
  [dependencies]
18
- lightningcss = { version = "=1.0.0-alpha.72", features = ["visitor"] }
18
+ lightningcss = { version = "=1.0.0-alpha.72", features = ["visitor", "serde"] }
19
19
  serde = { version = "1", features = ["derive"] }
20
20
  serde_json = "1"
21
21
 
data/rust/src/lib.rs CHANGED
@@ -21,7 +21,7 @@ use lightningcss::bundler::{BundleErrorKind, Bundler, FileProvider};
21
21
  use lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, StyleAttribute, StyleSheet};
22
22
  use lightningcss::visitor::Visit;
23
23
 
24
- use crate::options::{TransformOptions, TransformResult};
24
+ use crate::options::{ParseResult, TransformOptions, TransformResult};
25
25
  use crate::scope::Scoper;
26
26
 
27
27
  pub const VERSION: &str = env!("GEM_VERSION");
@@ -125,6 +125,26 @@ unsafe fn borrow_options(pointer: *const c_char) -> Result<TransformOptions, Fai
125
125
  serde_json::from_str(json).map_err(|error| Failure::option(format!("Invalid options: {error}")))
126
126
  }
127
127
 
128
+ fn parse_source<'i>(code: &'i str, options: &TransformOptions) -> Result<ParseResult<StyleSheet<'i>>, Failure> {
129
+ let collected = Arc::new(RwLock::new(Vec::new()));
130
+
131
+ let parser_options = ParserOptions {
132
+ filename: options.filename.clone().unwrap_or_default(),
133
+ error_recovery: options.error_recovery,
134
+ warnings: Some(collected.clone()),
135
+ ..ParserOptions::default()
136
+ };
137
+
138
+ let stylesheet = StyleSheet::parse(code, parser_options).map_err(|error| Failure::parse(error.to_string()))?;
139
+
140
+ let warnings = collected
141
+ .read()
142
+ .map(|warnings| warnings.iter().map(|warning| warning.to_string()).collect())
143
+ .unwrap_or_default();
144
+
145
+ Ok(ParseResult { stylesheet, warnings })
146
+ }
147
+
128
148
  fn transform_source(code: &str, options: &TransformOptions) -> Result<TransformResult, Failure> {
129
149
  let filename = options.filename.clone().unwrap_or_default();
130
150
  let css_modules = match &options.css_modules {
@@ -295,7 +315,7 @@ fn transform_attribute(code: &str, options: &TransformOptions) -> Result<Transfo
295
315
  })
296
316
  }
297
317
 
298
- fn to_result(outcome: Result<TransformResult, Failure>) -> LightningCssResult {
318
+ fn to_result(outcome: Result<impl serde::Serialize, Failure>) -> LightningCssResult {
299
319
  match outcome {
300
320
  Ok(result) => match serde_json::to_string(&result) {
301
321
  Ok(json) => LightningCssResult::ok(json),
@@ -323,6 +343,21 @@ pub unsafe extern "C" fn lightningcss_transform(
323
343
  to_result(transform_source(code, &options))
324
344
  }
325
345
 
346
+ #[no_mangle]
347
+ pub unsafe extern "C" fn lightningcss_parse(code: *const c_char, options_json: *const c_char) -> LightningCssResult {
348
+ let code = match borrow_str(code, "code") {
349
+ Ok(code) => code,
350
+ Err(failure) => return LightningCssResult::err(failure),
351
+ };
352
+
353
+ let options = match borrow_options(options_json) {
354
+ Ok(options) => options,
355
+ Err(failure) => return LightningCssResult::err(failure),
356
+ };
357
+
358
+ to_result(parse_source(code, &options))
359
+ }
360
+
326
361
  #[no_mangle]
327
362
  pub unsafe extern "C" fn lightningcss_transform_style_attribute(
328
363
  code: *const c_char,
data/rust/src/options.rs CHANGED
@@ -89,6 +89,12 @@ impl TransformOptions {
89
89
  }
90
90
  }
91
91
 
92
+ #[derive(Debug, Serialize)]
93
+ pub struct ParseResult<T> {
94
+ pub stylesheet: T,
95
+ pub warnings: Vec<String>,
96
+ }
97
+
92
98
  #[derive(Debug, Serialize)]
93
99
  pub struct TransformResult {
94
100
  pub code: String,
@@ -6,6 +6,9 @@ module LightningCSS
6
6
  # : (String, String) -> String
7
7
  def transform: (String, String) -> String
8
8
 
9
+ # : (String, String) -> String
10
+ def parse: (String, String) -> String
11
+
9
12
  # : (String, String) -> String
10
13
  def transform_style_attribute: (String, String) -> String
11
14
 
@@ -0,0 +1,104 @@
1
+ # Generated from lib/lightningcss/node.rb with RBS::Inline
2
+
3
+ module LightningCSS
4
+ # One object in the AST Lightning CSS parsed a stylesheet to.
5
+ #
6
+ # root = LightningCSS.parse(".a { color: red }").root
7
+ #
8
+ # root.type #=> "stylesheet"
9
+ # root.every("declaration") #=> every declaration in the stylesheet
10
+ # root.rules.first.value.location #=> {"source_index" => 0, "line" => 0, "column" => 1}
11
+ #
12
+ # Every object in the tree is a node, so reads chain all the way down. Most say what they are
13
+ # with their own `type`. The stylesheet, a declaration block, and a declaration do not, so a node
14
+ # names those from where it sits.
15
+ class Node
16
+ include Enumerable[LightningCSS::Node]
17
+
18
+ POSITION: Array[String]
19
+
20
+ DECLARATIONS: Array[String]
21
+
22
+ attr_reader parent: LightningCSS::Node?
23
+
24
+ attr_reader field: String?
25
+
26
+ # : (Hash[String, untyped], ?LightningCSS::Node?, ?String?) -> void
27
+ def initialize: (Hash[String, untyped], ?LightningCSS::Node?, ?String?) -> void
28
+
29
+ # : () -> String?
30
+ def type: () -> String?
31
+
32
+ # : () -> String?
33
+ def underscored_type: () -> String?
34
+
35
+ # : () -> bool
36
+ def rule?: () -> bool
37
+
38
+ # : () -> Hash[String, Integer]?
39
+ def location: () -> Hash[String, Integer]?
40
+
41
+ # : (String) -> untyped
42
+ def []: (String) -> untyped
43
+
44
+ # : () -> Array[String]
45
+ def keys: () -> Array[String]
46
+
47
+ # : () -> Hash[String, untyped]
48
+ def to_h: () -> Hash[String, untyped]
49
+
50
+ # : (?untyped) -> String
51
+ def to_json: (?untyped) -> String
52
+
53
+ # : (Array[Symbol]?) -> Hash[Symbol, untyped]
54
+ def deconstruct_keys: (Array[Symbol]?) -> Hash[Symbol, untyped]
55
+
56
+ # : () -> Array[LightningCSS::Node]
57
+ def child_nodes: () -> Array[LightningCSS::Node]
58
+
59
+ # : () { (LightningCSS::Node) -> void } -> void
60
+ # : () -> Enumerator[LightningCSS::Node, void]
61
+ def each: () { (LightningCSS::Node) -> void } -> void
62
+ | () -> Enumerator[LightningCSS::Node, void]
63
+
64
+ # : () -> Array[LightningCSS::Node]
65
+ def ancestors: () -> Array[LightningCSS::Node]
66
+
67
+ # : (String) -> Array[LightningCSS::Node]
68
+ def every: (String) -> Array[LightningCSS::Node]
69
+
70
+ # : () -> Hash[String, untyped]
71
+ def fields: () -> Hash[String, untyped]
72
+
73
+ # : () -> String
74
+ def inspect: () -> String
75
+
76
+ # : (Symbol, *untyped) -> untyped
77
+ def method_missing: (Symbol, *untyped) -> untyped
78
+
79
+ # : (Symbol, ?bool) -> bool
80
+ def respond_to_missing?: (Symbol, ?bool) -> bool
81
+
82
+ attr_reader attributes: Hash[String, untyped]
83
+
84
+ private
85
+
86
+ # : () -> String?
87
+ def inferred_type: () -> String?
88
+
89
+ # : (untyped, String) -> Array[LightningCSS::Node]
90
+ def nodes_in: (untyped, String) -> Array[LightningCSS::Node]
91
+
92
+ # : (String, untyped) -> String
93
+ def described_field: (String, untyped) -> String
94
+
95
+ # : (Array[untyped]) -> String
96
+ def counted: (Array[untyped]) -> String
97
+
98
+ # : (String) -> String?
99
+ def field_for: (String) -> String?
100
+
101
+ # : (untyped, String) -> untyped
102
+ def wrap: (untyped, String) -> untyped
103
+ end
104
+ end
@@ -13,6 +13,8 @@ module LightningCSS
13
13
 
14
14
  STYLE_ATTRIBUTE: Array[Symbol]
15
15
 
16
+ PARSE: Array[Symbol]
17
+
16
18
  BUNDLE: Array[Symbol]
17
19
 
18
20
  attr_reader to_h: Hash[Symbol, untyped]
@@ -0,0 +1,30 @@
1
+ # Generated from lib/lightningcss/parse_result.rb with RBS::Inline
2
+
3
+ module LightningCSS
4
+ # What a stylesheet parsed to.
5
+ #
6
+ # `stylesheet` is the AST exactly as Lightning CSS serializes it, and `root` is the same tree as a
7
+ # `LightningCSS::Node` to walk. `warnings` are the ones a transform would have reported.
8
+ class ParseResult
9
+ attr_reader source: String?
10
+
11
+ attr_reader stylesheet: Hash[String, untyped]
12
+
13
+ attr_reader warnings: Array[String]
14
+
15
+ # : (String, ?String?) -> LightningCSS::ParseResult
16
+ def self.from_json: (String, ?String?) -> LightningCSS::ParseResult
17
+
18
+ # : (stylesheet: Hash[String, untyped], warnings: Array[String], ?source: String?) -> void
19
+ def initialize: (stylesheet: Hash[String, untyped], warnings: Array[String], ?source: String?) -> void
20
+
21
+ # : () -> LightningCSS::Node
22
+ def root: () -> LightningCSS::Node
23
+
24
+ # : () -> bool
25
+ def warnings?: () -> bool
26
+
27
+ # : () -> String
28
+ def inspect: () -> String
29
+ end
30
+ end
@@ -21,6 +21,9 @@ module LightningCSS
21
21
 
22
22
  alias call transform
23
23
 
24
+ # : (String, ?filename: String?, ?error_recovery: bool) -> LightningCSS::ParseResult
25
+ def parse: (String, ?filename: String?, ?error_recovery: bool) -> LightningCSS::ParseResult
26
+
24
27
  # : (String, ?minify: bool, ?error_recovery: bool, ?targets: browsers?, ?css_modules: css_modules?, ?scope: String?) -> LightningCSS::Result
25
28
  def bundle: (String, ?minify: bool, ?error_recovery: bool, ?targets: browsers?, ?css_modules: css_modules?, ?scope: String?) -> LightningCSS::Result
26
29
 
@@ -0,0 +1,24 @@
1
+ # Generated from lib/lightningcss/visitor.rb with RBS::Inline
2
+
3
+ module LightningCSS
4
+ # Walks an AST, answering each node with the method named after it and walking through anything
5
+ # nothing answers.
6
+ #
7
+ # class Colors < LightningCSS::Visitor
8
+ # def visit_declaration(node)
9
+ # puts node.value.type if node.property == "color"
10
+ # end
11
+ # end
12
+ #
13
+ # Colors.new.visit(LightningCSS.parse(".a { color: red }"))
14
+ #
15
+ # A rule is answered as one, as in `visit_style_rule` or `visit_media_rule`, since `style`,
16
+ # `nesting`, and `namespace` are also what a selector is made of.
17
+ class Visitor
18
+ # : (LightningCSS::Node | LightningCSS::ParseResult) -> void
19
+ def visit: (LightningCSS::Node | LightningCSS::ParseResult) -> void
20
+
21
+ # : (LightningCSS::Node) -> void
22
+ def visit_children: (LightningCSS::Node) -> void
23
+ end
24
+ end
data/sig/lightningcss.rbs CHANGED
@@ -11,12 +11,20 @@
11
11
  # LightningCSS.transform(".title { color: red }", scope: "[data-scope-abc]").code
12
12
  # #=> ".title[data-scope-abc]{color:red}"
13
13
  #
14
+ # `parse` returns the AST instead, to walk with `LightningCSS::Node` and `LightningCSS::Visitor`:
15
+ #
16
+ # LightningCSS.parse(".a { color: red }").root.every("declaration").first.property
17
+ # #=> "color"
18
+ #
14
19
  # `LightningCSS::Options` is what the options are read by, and `LightningCSS::Transformer` holds a
15
20
  # set of them to reuse across many stylesheets.
16
21
  module LightningCSS
17
22
  # : (String, ?filename: String?, ?minify: bool, ?error_recovery: bool, ?targets: browsers?, ?css_modules: css_modules?, ?scope: String?) -> LightningCSS::Result
18
23
  def self.transform: (String, ?filename: String?, ?minify: bool, ?error_recovery: bool, ?targets: browsers?, ?css_modules: css_modules?, ?scope: String?) -> LightningCSS::Result
19
24
 
25
+ # : (String, ?filename: String?, ?error_recovery: bool) -> LightningCSS::ParseResult
26
+ def self.parse: (String, ?filename: String?, ?error_recovery: bool) -> LightningCSS::ParseResult
27
+
20
28
  # : (String, ?minify: bool, ?error_recovery: bool, ?targets: browsers?, ?css_modules: css_modules?, ?scope: String?) -> LightningCSS::Result
21
29
  def self.bundle: (String, ?minify: bool, ?error_recovery: bool, ?targets: browsers?, ?css_modules: css_modules?, ?scope: String?) -> LightningCSS::Result
22
30
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lightningcss
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.33.0
4
+ version: 1.33.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marco Roth
@@ -26,10 +26,13 @@ files:
26
26
  - lib/lightningcss.rb
27
27
  - lib/lightningcss/backend.rb
28
28
  - lib/lightningcss/errors.rb
29
+ - lib/lightningcss/node.rb
29
30
  - lib/lightningcss/options.rb
31
+ - lib/lightningcss/parse_result.rb
30
32
  - lib/lightningcss/result.rb
31
33
  - lib/lightningcss/transformer.rb
32
34
  - lib/lightningcss/version.rb
35
+ - lib/lightningcss/visitor.rb
33
36
  - licenses/README.md
34
37
  - licenses/lightningcss-MPL-2.0.txt
35
38
  - lightningcss.gemspec
@@ -44,11 +47,14 @@ files:
44
47
  - sig/lightningcss.rbs
45
48
  - sig/lightningcss/backend.rbs
46
49
  - sig/lightningcss/errors.rbs
50
+ - sig/lightningcss/node.rbs
47
51
  - sig/lightningcss/options.rbs
52
+ - sig/lightningcss/parse_result.rbs
48
53
  - sig/lightningcss/result.rbs
49
54
  - sig/lightningcss/transformer.rbs
50
55
  - sig/lightningcss/types.rbs
51
56
  - sig/lightningcss/version.rbs
57
+ - sig/lightningcss/visitor.rbs
52
58
  homepage: https://github.com/marcoroth/lightningcss-ruby
53
59
  licenses:
54
60
  - MIT