Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,113 @@ LightningCSS.transform_style_attribute("color: #ff0000; border: none", minify: t

Having no selectors and no names, it takes neither `scope` nor `css_modules`, and says so when given one.

#### Parsing

`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.

```ruby
parsed = LightningCSS.parse(".card .title:hover { color: #f00 }")

parsed.stylesheet #=> the AST exactly as Lightning CSS serializes it
parsed.warnings #=> []
parsed.source #=> ".card .title:hover { color: #f00 }"
```

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`.

#### Walking the AST

`root` answers the stylesheet as a `LightningCSS::Node`, which walks, reads its fields by name, and knows what it sits inside.

```ruby
root = LightningCSS.parse(source).root

root.type #=> "stylesheet"
root.keys #=> the fields this node carries
root.fields #=> those fields and their values, without its type and location
root.child_nodes #=> the nodes directly under it
root.every("declaration") #=> every declaration in the stylesheet
root.each #=> an Enumerator over every node
```

Every object in the tree is a node, so reads chain all the way down.

```ruby
rule = root.rules.first

rule.type #=> "style"
rule.value.selectors.first.map(&:type) #=> ["class", "combinator", "class", "pseudo-class"]
rule.value.declarations.declarations.first.value.r
#=> 255.0
```

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`.

Inspecting a node shows every field it carries.

```
#<LightningCSS::Node style value=#<LightningCSS::Node>>
#<LightningCSS::Node location=0:1 selectors=[... 1 item] declarations=#<LightningCSS::Node declaration-block> rules=[]>
#<LightningCSS::Node declaration property="color" value=#<LightningCSS::Node rgb>>
```

Lightning CSS names its fields in camelCase, and a field answers to its snake_case name too.

```ruby
block.important_declarations # the same field as block.importantDeclarations
query.media_queries # mediaQueries
```

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)`.

```ruby
rule.value.location
#=> {"source_index" => 0, "line" => 0, "column" => 1}
```

`ancestors` answers what a node sits inside, and `rule?` says whether it is a rule.

```ruby
declaration = root.every("declaration").first
declaration.ancestors.find(&:rule?).type
#=> "style"
```

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.

```ruby
declaration => { type: "declaration", property:, value: { type: "rgb", r: } }
property #=> "color"
r #=> 255.0
```

`to_h` and `to_json` answer the AST the node wraps.

```ruby
declaration.value.to_h
#=> {"type" => "rgb", "r" => 255.0, "g" => 0.0, "b" => 0.0, "alpha" => 1.0}
```

`LightningCSS::Visitor` answers a node with the method named after its type, and walks through anything nothing answers.

```ruby
class Colors < LightningCSS::Visitor
def visit_style_rule(node)
puts node.value.location["line"]

visit_children(node)
end

def visit_declaration(node)
puts node.property
end
end

Colors.new.visit(LightningCSS.parse(source))
```

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`.

#### Reusing options

`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.
Expand All @@ -155,6 +262,8 @@ transformer.with(scope: "[s]")

It answers `call` as well, so it can be handed to anything expecting something callable.

`transformer.parse` reads with the `filename` and `error_recovery` it holds and leaves the rest, which only matter to printing.

### Options

| Option | Type | Description |
Expand Down
7 changes: 7 additions & 0 deletions ext/lightningcss/lightningcss.c
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ static VALUE rb_transform(VALUE self, VALUE code, VALUE options) {
return call(lightningcss_transform, code, options);
}

static VALUE rb_parse(VALUE self, VALUE code, VALUE options) {
(void) self;

return call(lightningcss_parse, code, options);
}

static VALUE rb_transform_style_attribute(VALUE self, VALUE code, VALUE options) {
(void) self;

Expand Down Expand Up @@ -123,6 +129,7 @@ void Init_lightningcss(void) {
rb_eOptionError = rb_define_class_under(rb_mLightningCSS, "OptionError", rb_eError);
rb_eBundleError = rb_define_class_under(rb_mLightningCSS, "BundleError", rb_eError);

rb_define_singleton_method(rb_mBackend, "parse", rb_parse, 2);
rb_define_singleton_method(rb_mBackend, "transform", rb_transform, 2);
rb_define_singleton_method(rb_mBackend, "transform_style_attribute", rb_transform_style_attribute, 2);
rb_define_singleton_method(rb_mBackend, "bundle", rb_bundle, 2);
Expand Down
16 changes: 16 additions & 0 deletions lib/lightningcss.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

require_relative "lightningcss/options"
require_relative "lightningcss/result"
require_relative "lightningcss/node"
require_relative "lightningcss/parse_result"
require_relative "lightningcss/visitor"
require_relative "lightningcss/transformer"

# Ruby bindings for Lightning CSS.
Expand All @@ -28,14 +31,27 @@
# LightningCSS.transform(".title { color: red }", scope: "[data-scope-abc]").code
# #=> ".title[data-scope-abc]{color:red}"
#
# `parse` returns the AST instead, to walk with `LightningCSS::Node` and `LightningCSS::Visitor`:
#
# LightningCSS.parse(".a { color: red }").root.every("declaration").first.property
# #=> "color"
#
# `LightningCSS::Options` is what the options are read by, and `LightningCSS::Transformer` holds a
# set of them to reuse across many stylesheets.
#
module LightningCSS
#: (String, ?filename: String?, ?minify: bool, ?error_recovery: bool, ?targets: browsers?, ?css_modules: css_modules?, ?scope: String?) -> LightningCSS::Result
def self.transform(code, **options)
Result.from_json(Backend.transform(code.to_s, Options.serialize(options)))
end

#: (String, ?filename: String?, ?error_recovery: bool) -> LightningCSS::ParseResult
def self.parse(code, **options)
serialized = Options.serialize(options, Options::PARSE, "a parse")

ParseResult.from_json(Backend.parse(code.to_s, serialized), code.to_s)
end

#: (String, ?minify: bool, ?error_recovery: bool, ?targets: browsers?, ?css_modules: css_modules?, ?scope: String?) -> LightningCSS::Result
def self.bundle(path, **options)
serialized = Options.serialize(options, Options::BUNDLE, "a bundle")
Expand Down
5 changes: 5 additions & 0 deletions lib/lightningcss/backend.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ def transform(_code, _options_json)
unavailable(__method__)
end

#: (String, String) -> String
def parse(_code, _options_json)
unavailable(__method__)
end

#: (String, String) -> String
def transform_style_attribute(_code, _options_json)
unavailable(__method__)
Expand Down
Loading
Loading