lightningcss 1.33.0-x86_64-linux-gnu → 1.33.0.2-x86_64-linux-gnu

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: 2a034ecd57effe657dddf772892d2f0afa74cd6358f0120bd75c6850e936ea8b
4
- data.tar.gz: 9cca0736400fb61fb3a1fc4a37e6b30a5c7b260acb7cd9135deae714d7cbf17a
3
+ metadata.gz: 87a5af79c2ae53a0dda0b24238474c04534cedca9a4c7250161012d27f91e0e1
4
+ data.tar.gz: 33620cc2e235bb293882979804587b4310f63543eed5c76aeb680e5afff12545
5
5
  SHA512:
6
- metadata.gz: a22044c7bdd58290d675281f4359fe5a2b5240b3767210c14fc1d54fd9f4be88366e8089558655e6deb628770c6682e41ac5dd33b8db632d8089dd947c152da0
7
- data.tar.gz: 033f6788c9b199d0e0444e82b53370781a193e252b3fabaab643c20b76185479b290be2dff3397fb286abfbbb5f6a1c02c0bf99ea453155d213fd43da905bfe1
6
+ metadata.gz: 558056ca4e596e12e1a559d0e91d4aa5696f56800fed1f0500684e9e6bd86403555158cfe56f899cb577483f76ca11f599bee26b669db7773815f098c1d050d1
7
+ data.tar.gz: 87180611a1a270ff150c2575d42672b03a0c414d54f893aa547991d9f3b27a63cf6f3fde498368e77a9326e805f2e6fa80e1f721c91fd2ed16f5b3080742d440
data/README.md CHANGED
@@ -57,13 +57,25 @@ LightningCSS.minify(".a { color: #ff0000 }")
57
57
 
58
58
  #### Targeting browsers
59
59
 
60
- Give the browsers you support, each as its major version. Anything they cannot read is lowered, with the original kept after it so that a browser which understands it still wins.
60
+ Give the browsers you support, each with the oldest version you support. Anything they cannot read is lowered, with the original kept after it so that a browser which understands it still wins.
61
61
 
62
62
  ```ruby
63
63
  LightningCSS.transform(".a { color: lab(50% 40 59) }", targets: { chrome: 80 }, minify: true).code
64
64
  #=> ".a{color:#bf5702;color:lab(50% 40 59)}"
65
65
  ```
66
66
 
67
+ A version is an `Integer` for a major version, or a `Float` or `String` for a minor or patch release. Several features first shipped in a minor Safari release, so the minor version can change the output.
68
+
69
+ ```ruby
70
+ LightningCSS.transform(".a { color: oklch(60% .2 30) }", targets: { safari: 15 }, minify: true).code
71
+ #=> ".a{color:lab(51.6908% 61.9679 48.0969)}"
72
+
73
+ LightningCSS.transform(".a { color: oklch(60% .2 30) }", targets: { safari: "15.4" }, minify: true).code
74
+ #=> ".a{color:oklch(60% .2 30)}"
75
+ ```
76
+
77
+ Prefer a `String` for a minor version above 9, since `16.10` as a `Float` is `16.1`.
78
+
67
79
  #### Bundling
68
80
 
69
81
  `bundle` resolves the `@import` statements a stylesheet was written with by reading the files they name, so it takes a path on disk to the entry stylesheet.
@@ -83,7 +95,7 @@ result.exports
83
95
  #=> {"application" => "_8Z4fiW_application"}
84
96
 
85
97
  result.warnings.first
86
- #=> "'deep' is not recognized as a valid pseudo-class. ... at app/assets/stylesheets/layout.css:0:9"
98
+ #=> "'deep' is not recognized as a valid pseudo-class. ... at app/assets/stylesheets/layout.css:1:9"
87
99
  ```
88
100
 
89
101
  #### CSS modules
@@ -141,6 +153,113 @@ LightningCSS.transform_style_attribute("color: #ff0000; border: none", minify: t
141
153
 
142
154
  Having no selectors and no names, it takes neither `scope` nor `css_modules`, and says so when given one.
143
155
 
156
+ #### Parsing
157
+
158
+ `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.
159
+
160
+ ```ruby
161
+ parsed = LightningCSS.parse(".card .title:hover { color: #f00 }")
162
+
163
+ parsed.stylesheet #=> the AST exactly as Lightning CSS serializes it
164
+ parsed.warnings #=> []
165
+ parsed.source #=> ".card .title:hover { color: #f00 }"
166
+ ```
167
+
168
+ 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`.
169
+
170
+ #### Walking the AST
171
+
172
+ `root` answers the stylesheet as a `LightningCSS::Node`, which walks, reads its fields by name, and knows what it sits inside.
173
+
174
+ ```ruby
175
+ root = LightningCSS.parse(source).root
176
+
177
+ root.type #=> "stylesheet"
178
+ root.keys #=> the fields this node carries
179
+ root.fields #=> those fields and their values, without its type and location
180
+ root.child_nodes #=> the nodes directly under it
181
+ root.every("declaration") #=> every declaration in the stylesheet
182
+ root.each #=> an Enumerator over every node
183
+ ```
184
+
185
+ Every object in the tree is a node, so reads chain all the way down.
186
+
187
+ ```ruby
188
+ rule = root.rules.first
189
+
190
+ rule.type #=> "style"
191
+ rule.value.selectors.first.map(&:type) #=> ["class", "combinator", "class", "pseudo-class"]
192
+ rule.value.declarations.declarations.first.value.r
193
+ #=> 255.0
194
+ ```
195
+
196
+ 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`.
197
+
198
+ Inspecting a node shows every field it carries.
199
+
200
+ ```
201
+ #<LightningCSS::Node style value=#<LightningCSS::Node>>
202
+ #<LightningCSS::Node location=0:1 selectors=[... 1 item] declarations=#<LightningCSS::Node declaration-block> rules=[]>
203
+ #<LightningCSS::Node declaration property="color" value=#<LightningCSS::Node rgb>>
204
+ ```
205
+
206
+ Lightning CSS names its fields in camelCase, and a field answers to its snake_case name too.
207
+
208
+ ```ruby
209
+ block.important_declarations # the same field as block.importantDeclarations
210
+ query.media_queries # mediaQueries
211
+ ```
212
+
213
+ 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)`.
214
+
215
+ ```ruby
216
+ rule.value.location
217
+ #=> {"source_index" => 0, "line" => 0, "column" => 1}
218
+ ```
219
+
220
+ `ancestors` answers what a node sits inside, and `rule?` says whether it is a rule.
221
+
222
+ ```ruby
223
+ declaration = root.every("declaration").first
224
+ declaration.ancestors.find(&:rule?).type
225
+ #=> "style"
226
+ ```
227
+
228
+ 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.
229
+
230
+ ```ruby
231
+ declaration => { type: "declaration", property:, value: { type: "rgb", r: } }
232
+ property #=> "color"
233
+ r #=> 255.0
234
+ ```
235
+
236
+ `to_h` and `to_json` answer the AST the node wraps.
237
+
238
+ ```ruby
239
+ declaration.value.to_h
240
+ #=> {"type" => "rgb", "r" => 255.0, "g" => 0.0, "b" => 0.0, "alpha" => 1.0}
241
+ ```
242
+
243
+ `LightningCSS::Visitor` answers a node with the method named after its type, and walks through anything nothing answers.
244
+
245
+ ```ruby
246
+ class Colors < LightningCSS::Visitor
247
+ def visit_style_rule(node)
248
+ puts node.value.location["line"]
249
+
250
+ visit_children(node)
251
+ end
252
+
253
+ def visit_declaration(node)
254
+ puts node.property
255
+ end
256
+ end
257
+
258
+ Colors.new.visit(LightningCSS.parse(source))
259
+ ```
260
+
261
+ 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`.
262
+
144
263
  #### Reusing options
145
264
 
146
265
  `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 +274,8 @@ transformer.with(scope: "[s]")
155
274
 
156
275
  It answers `call` as well, so it can be handed to anything expecting something callable.
157
276
 
277
+ `transformer.parse` reads with the `filename` and `error_recovery` it holds and leaves the rest, which only matter to printing.
278
+
158
279
  ### Options
159
280
 
160
281
  | Option | Type | Description |
@@ -162,7 +283,7 @@ It answers `call` as well, so it can be handed to anything expecting something c
162
283
  | `filename` | `String` | The name to use in errors and warnings. |
163
284
  | `minify` | `bool` | Whether to print the result as small as it goes. |
164
285
  | `error_recovery` | `bool` | Whether to carry on past a rule it cannot read. Off by default, so such a rule raises. |
165
- | `targets` | `Hash` | The browsers being compiled for, each as its major version. |
286
+ | `targets` | `Hash` | The browsers being compiled for, each with the oldest version supported. |
166
287
  | `css_modules` | `bool`, `Hash` | Whether to compile as a CSS module, and how. |
167
288
  | `scope` | `String` | A selector fragment to narrow every rule by. |
168
289
 
@@ -216,13 +337,40 @@ result = LightningCSS.transform(".a:deep(.b) { color: red }", minify: true)
216
337
  result.warnings?
217
338
  #=> true
218
339
 
219
- result.warnings.first
220
- #=> "'deep' is not recognized as a valid pseudo-class. Did you mean '::deep' (pseudo-element) or is this a typo? at :0:9"
340
+ result.warnings.first.to_s
341
+ #=> "'deep' is not recognized as a valid pseudo-class. Did you mean '::deep' (pseudo-element) or is this a typo? at :1:9"
221
342
 
222
343
  result.code
223
344
  #=> ".a:deep(.b){color:red}"
224
345
  ```
225
346
 
347
+ ### Locations
348
+
349
+ A warning is a `LightningCSS::Warning` with a `message`, a `type`, and a `location`, the same as a diagnostic in Prism or Herb. A `LightningCSS::Error` carries the same `type` and `location`, and keeps the location in its message.
350
+
351
+ A `LightningCSS::Location` counts lines from 1 and columns from 0, the same as Prism, Herb, and Yerba, and prints the way an editor reads it:
352
+
353
+ ```ruby
354
+ css = ".a { color: red; }\n.b, { color: blue; }\n"
355
+
356
+ begin
357
+ LightningCSS.transform(css, filename: "main.css")
358
+ rescue LightningCSS::ParseError => error
359
+ error.message #=> "Invalid empty selector at main.css:2:5"
360
+ error.type #=> "SelectorError"
361
+ error.location #=> #<data LightningCSS::Location filename="main.css", line=2, column=4>
362
+ end
363
+
364
+ warning = LightningCSS.transform(css, filename: "main.css", error_recovery: true).warnings.first
365
+
366
+ warning.message #=> "Invalid empty selector"
367
+ warning.type #=> "SelectorError"
368
+ warning.location.line #=> 2
369
+ warning.to_s #=> "Invalid empty selector at main.css:2:5"
370
+ ```
371
+
372
+ A warning converts to the string it always was wherever a `String` is expected and is equal to it, so `warnings.include?("...")` still works. An error without a location, such as an `OptionError`, has `nil` for `type` and `location`.
373
+
226
374
  ### Development
227
375
 
228
376
  The gem is a C extension over a Rust crate. `rust/` builds a static library and generates the C header with [cbindgen](https://github.com/mozilla/cbindgen), `ext/lightningcss/` wraps it, and `lib/` is the Ruby API over that.
@@ -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
 
@@ -43,12 +43,12 @@ static VALUE error_class_for(enum LightningCssErrorCode code) {
43
43
 
44
44
  static VALUE unwrap(struct LightningCssResult result) {
45
45
  if (result.error) {
46
- VALUE message = make_utf8_string(result.error);
46
+ VALUE payload = make_utf8_string(result.error);
47
47
  VALUE error_class = error_class_for(result.code);
48
48
 
49
49
  lightningcss_result_free(result);
50
50
 
51
- rb_raise(error_class, "%s", StringValueCStr(message));
51
+ rb_exc_raise(rb_funcall(error_class, rb_intern("from_json"), 1, payload));
52
52
  }
53
53
 
54
54
  if (!result.value) {
@@ -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);
Binary file
Binary file
Binary file
Binary file
@@ -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__)
@@ -1,7 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LightningCSS
4
- class Error < StandardError; end
4
+ class Error < StandardError
5
+ attr_reader :type #: String?
6
+ attr_reader :location #: LightningCSS::Location?
7
+
8
+ #: (String) -> instance
9
+ def self.from_json(payload)
10
+ parsed = JSON.parse(payload)
11
+ location = parsed["location"] && Location.from_hash(parsed["location"])
12
+ message = parsed.fetch("message")
13
+
14
+ new(location ? "#{message} at #{location}" : message, type: parsed["type"], location: location)
15
+ end
16
+
17
+ #: (?String?, ?type: String?, ?location: LightningCSS::Location?) -> void
18
+ def initialize(message = nil, type: nil, location: nil)
19
+ super(message)
20
+
21
+ @type = type
22
+ @location = location
23
+ end
24
+ end
25
+
5
26
  class ParseError < Error; end
6
27
  class OptionError < Error; end
7
28
  class BundleError < Error; end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LightningCSS
4
+ Location = Data.define(
5
+ :filename, #: String?
6
+ :line, #: Integer
7
+ :column #: Integer
8
+ )
9
+
10
+ class Location
11
+ #: (Hash[String, untyped]) -> LightningCSS::Location
12
+ def self.from_hash(hash)
13
+ new(filename: hash["filename"], line: hash.fetch("line"), column: hash.fetch("column"))
14
+ end
15
+
16
+ #: () -> String
17
+ def to_s
18
+ "#{filename}:#{line}:#{column + 1}"
19
+ end
20
+ end
21
+ end
@@ -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[LightningCSS::Warning]
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").map { |warning| Warning.from_hash(warning) }
22
+ )
23
+ end
24
+
25
+ #: (stylesheet: Hash[String, untyped], warnings: Array[LightningCSS::Warning], ?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.map(&:to_s).inspect}" if warnings?
48
+
49
+ "#<#{self.class.name} #{parts.join(" ")}>"
50
+ end
51
+ end
52
+ end
@@ -13,7 +13,7 @@ module LightningCSS
13
13
  class Result
14
14
  attr_reader :code #: String
15
15
  attr_reader :exports #: Hash[String, String]?
16
- attr_reader :warnings #: Array[String]
16
+ attr_reader :warnings #: Array[LightningCSS::Warning]
17
17
 
18
18
  #: (String) -> LightningCSS::Result
19
19
  def self.from_json(payload)
@@ -22,11 +22,11 @@ module LightningCSS
22
22
  new(
23
23
  code: parsed.fetch("code"),
24
24
  exports: parsed["exports"],
25
- warnings: parsed.fetch("warnings")
25
+ warnings: parsed.fetch("warnings").map { |warning| Warning.from_hash(warning) }
26
26
  )
27
27
  end
28
28
 
29
- #: (code: String, warnings: Array[String], ?exports: Hash[String, String]?) -> void
29
+ #: (code: String, warnings: Array[LightningCSS::Warning], ?exports: Hash[String, String]?) -> void
30
30
  def initialize(code:, warnings:, exports: nil)
31
31
  @code = code
32
32
  @exports = exports
@@ -49,7 +49,7 @@ module LightningCSS
49
49
  def inspect
50
50
  parts = ["code=#{code.inspect}"]
51
51
  parts << "exports=#{exports.inspect}" if exports
52
- parts << "warnings=#{warnings.inspect}" if warnings?
52
+ parts << "warnings=#{warnings.map(&:to_s).inspect}" if warnings?
53
53
 
54
54
  "#<#{self.class.name} #{parts.join(" ")}>"
55
55
  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.2"
5
5
  end