diff --git a/.clang-format-ignore b/.clang-format-ignore
index 7052180e6..4223febaa 100644
--- a/.clang-format-ignore
+++ b/.clang-format-ignore
@@ -5,5 +5,6 @@ src/ast_pretty_print.c
src/errors.c
src/include/ast_nodes.h
src/include/ast_pretty_print.h
+src/include/css_parser.h
src/include/errors.h
src/visitor.c
diff --git a/.github/workflows/build-gems.yml b/.github/workflows/build-gems.yml
index df6338b44..dc3cbf95f 100644
--- a/.github/workflows/build-gems.yml
+++ b/.github/workflows/build-gems.yml
@@ -75,6 +75,14 @@ jobs:
with:
bundler-cache: false
+ - name: Set up Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ toolchain: stable
+
+ - name: Build CSS Parser
+ run: cd src/css && cargo build --release
+
- name: bundle install
run: bundle install
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 86b9f824a..b93787b09 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -34,12 +34,26 @@ jobs:
with:
bundler-cache: true
+ - name: Set up Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ toolchain: stable
+
+ - name: Install Rust WASM target
+ run: rustup target add wasm32-unknown-emscripten
+
+ - name: Build CSS Parser
+ run: cd src/css && cargo build --release
+
- name: bundle install
run: bundle install
- name: Render Templates
run: bundle exec rake templates
+ - name: make all
+ run: make all
+
- name: Compile Herb
run: bundle exec rake make
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 3be067faf..5c3966e0a 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -53,6 +53,17 @@ jobs:
with:
bundler-cache: true
+ - name: Set up Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ toolchain: stable
+
+ - name: Install Rust WASM target
+ run: rustup target add wasm32-unknown-emscripten
+
+ - name: Build CSS Parser
+ run: cd src/css && cargo build --release
+
- name: bundle install
run: bundle install
diff --git a/.github/workflows/javascript.yml b/.github/workflows/javascript.yml
index f7bd70a60..d577afbc1 100644
--- a/.github/workflows/javascript.yml
+++ b/.github/workflows/javascript.yml
@@ -41,6 +41,17 @@ jobs:
with:
bundler-cache: true
+ - name: Set up Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ toolchain: stable
+
+ - name: Install Rust WASM target
+ run: rustup target add wasm32-unknown-emscripten
+
+ - name: Build CSS Parser
+ run: cd src/css && cargo build --release
+
- name: bundle install
run: bundle install
diff --git a/.gitignore b/.gitignore
index 8261787ab..5d0d055d2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -134,3 +134,7 @@ docs/docs/public/c-reference
# NPM
**/node_modules/**/*
+
+# Rust (CSS Parser)
+src/css/target/
+src/include/css_parser.h
diff --git a/Makefile b/Makefile
index 137f736d9..bf2e90fbc 100644
--- a/Makefile
+++ b/Makefile
@@ -37,6 +37,10 @@ prism_build = $(prism_path)/build
prism_flags = -I$(prism_include)
prism_ldflags = $(prism_build)/libprism.a
+css_parser_dir = src/css
+css_parser_lib = $(css_parser_dir)/target/release/libherb_css_parser.a
+css_parser_flags = -I$(css_parser_dir)
+
# Enable strict warnings
warning_flags = -Wall -Wextra -Werror -pedantic
@@ -50,42 +54,44 @@ production_flags = $(warning_flags) -O3 -march=native -flto
shared_library_flags = -fPIC
# Default build mode (change this as needed)
-flags = $(warning_flags) $(debug_flags) $(prism_flags) -std=c99
+flags = $(warning_flags) $(debug_flags) $(prism_flags) $(css_parser_flags) -std=c99
# Separate test compilation flags
-test_flags = $(debug_flags) $(prism_flags) -std=gnu99
+test_flags = $(debug_flags) $(prism_flags) $(css_parser_flags) -std=gnu99
# Shared library build (if needed)
-shared_flags = $(production_flags) $(shared_library_flags) $(prism_flags)
+shared_flags = $(production_flags) $(shared_library_flags) $(prism_flags) $(css_parser_flags)
ifeq ($(os),Linux)
+ css_parser_ldflags = $(css_parser_lib) -ldl -lpthread -lm
test_cflags = $(test_flags) -I/usr/include/check
- test_ldflags = -L/usr/lib/x86_64-linux-gnu -lcheck -lm -lsubunit $(prism_ldflags)
+ test_ldflags = -L/usr/lib/x86_64-linux-gnu -lcheck -lm -lsubunit $(prism_ldflags) $(css_parser_ldflags)
cc = clang-21
clang_format = clang-format-21
clang_tidy = clang-tidy-21
endif
ifeq ($(os),Darwin)
+ css_parser_ldflags = $(css_parser_lib) -lresolv -framework Security -framework CoreFoundation
brew_prefix := $(shell brew --prefix check)
test_cflags = $(test_flags) -I$(brew_prefix)/include
- test_ldflags = -L$(brew_prefix)/lib -lcheck -lm $(prism_ldflags)
+ test_ldflags = -L$(brew_prefix)/lib -lcheck -lm $(prism_ldflags) $(css_parser_ldflags)
llvm_path = $(shell brew --prefix llvm@21)
cc = $(llvm_path)/bin/clang
clang_format = $(llvm_path)/bin/clang-format
clang_tidy = $(llvm_path)/bin/clang-tidy
endif
-all: templates prism $(exec) $(lib_name) $(static_lib_name) test wasm
+all: templates prism css_parser $(exec) $(lib_name) $(static_lib_name) test wasm
-$(exec): $(objects)
- $(cc) $(objects) $(flags) $(ldflags) $(prism_ldflags) -o $(exec)
+$(exec): $(objects) $(css_parser_lib)
+ $(cc) $(objects) $(flags) $(ldflags) $(prism_ldflags) $(css_parser_ldflags) -o $(exec)
-$(lib_name): $(objects)
- $(cc) -shared $(objects) $(shared_flags) $(ldflags) $(prism_ldflags) -o $(lib_name)
+$(lib_name): $(objects) $(css_parser_lib)
+ $(cc) -shared $(objects) $(shared_flags) $(ldflags) $(prism_ldflags) $(css_parser_ldflags) -o $(lib_name)
# cp $(lib_name) $(ruby_extension)
-$(static_lib_name): $(objects)
+$(static_lib_name): $(objects) $(css_parser_lib)
ar rcs $(static_lib_name) $(objects)
src/%.o: src/%.c templates
@@ -102,6 +108,7 @@ clean:
rm -rf $(objects) $(test_objects) $(extension_objects) lib/herb/*.bundle tmp
rm -rf $(prism_path)
rake prism:clean
+ cd $(css_parser_dir) && cargo clean
bundle_install:
bundle install
@@ -113,6 +120,12 @@ prism: bundle_install
cd $(prism_path) && ruby templates/template.rb && make static && cd -
rake prism:vendor
+css_parser: $(css_parser_lib)
+
+$(css_parser_lib):
+ cd $(css_parser_dir) && cargo build --release
+ cbindgen --config $(css_parser_dir)/cbindgen.toml --output src/include/css_parser.h $(css_parser_dir)
+
format:
$(clang_format) -i $(project_and_extension_files)
diff --git a/config.yml b/config.yml
index 83b8e1d04..297a3d9e5 100644
--- a/config.yml
+++ b/config.yml
@@ -712,3 +712,35 @@ nodes:
- name: statements
type: array
kind: Node
+
+ - name: CSSDeclarationNode
+ fields:
+ - name: property
+ type: string
+
+ - name: value
+ type: string
+
+ - name: CSSRuleNode
+ fields:
+ - name: selector
+ type: string
+
+ - name: declarations
+ type: array
+ kind: CSSDeclarationNode
+
+ - name: CSSStyleNode
+ fields:
+ - name: content
+ type: string
+
+ - name: rules
+ type: array
+ kind: CSSRuleNode
+
+ - name: valid
+ type: boolean
+
+ - name: parse_error
+ type: string
diff --git a/ext/herb/extconf.rb b/ext/herb/extconf.rb
index cb215e568..5be4b4e2a 100644
--- a/ext/herb/extconf.rb
+++ b/ext/herb/extconf.rb
@@ -10,10 +10,13 @@
include_path = File.expand_path("../../src/include", __dir__)
prism_path = File.expand_path("../../vendor/prism", __dir__)
+css_parser_path = File.expand_path("../../src/css", __dir__)
prism_src_path = "#{prism_path}/src"
prism_include_path = "#{prism_path}/include"
+css_parser_lib = "#{css_parser_path}/target/release/libherb_css_parser.a"
+
$VPATH << "$(srcdir)/../../src"
$VPATH << "$(srcdir)/../../src/util"
$VPATH << prism_src_path
@@ -23,9 +26,17 @@
$INCFLAGS << " -I#{include_path}"
$INCFLAGS << " -I#{prism_src_path}"
$INCFLAGS << " -I#{prism_src_path}/util"
+$INCFLAGS << " -I#{css_parser_path}"
$CFLAGS << " -DPRISM_EXPORT_SYMBOLS=static "
+$LDFLAGS << " #{css_parser_lib}"
+if RUBY_PLATFORM.match?(/darwin/)
+ $LDFLAGS << " -lresolv -framework Security -framework CoreFoundation"
+elsif RUBY_PLATFORM.match?(/linux/)
+ $LDFLAGS << " -ldl -lpthread -lm"
+end
+
herb_src_files = Dir.glob("#{$srcdir}/../../src/**/*.c").map { |file| file.delete_prefix("../../../../ext/herb/") }.sort
prism_main_files = %w[
diff --git a/javascript/packages/formatter/src/format-printer.ts b/javascript/packages/formatter/src/format-printer.ts
index 327fef82c..47fd02f21 100644
--- a/javascript/packages/formatter/src/format-printer.ts
+++ b/javascript/packages/formatter/src/format-printer.ts
@@ -91,6 +91,9 @@ import {
ERBInNode,
XMLDeclarationNode,
CDATANode,
+ CSSStyleNode,
+ CSSRuleNode,
+ CSSDeclarationNode,
Token
} from "@herb-tools/core"
@@ -1192,6 +1195,20 @@ export class FormatPrinter extends Printer {
if (node.end_node) this.visit(node.end_node)
}
+ visitCSSStyleNode(node: CSSStyleNode) {
+ if (node.content) {
+ this.push(node.content)
+ }
+ }
+
+ visitCSSRuleNode(_node: CSSRuleNode) {
+ // CSS rules are contained within CSSStyleNode, not rendered separately
+ }
+
+ visitCSSDeclarationNode(_node: CSSDeclarationNode) {
+ // CSS declarations are contained within CSSRuleNode, not rendered separately
+ }
+
// --- Element Formatting Analysis Helpers ---
/**
diff --git a/javascript/packages/printer/scripts/generate-node-tests.mjs b/javascript/packages/printer/scripts/generate-node-tests.mjs
index f85e8a189..664a17648 100644
--- a/javascript/packages/printer/scripts/generate-node-tests.mjs
+++ b/javascript/packages/printer/scripts/generate-node-tests.mjs
@@ -52,6 +52,8 @@ function generateNodeTest(nodeInfo) {
astType = astType.replace(/^HTML/, 'HTML_')
} else if (astType.startsWith('ERB')) {
astType = astType.replace(/^ERB/, 'ERB_')
+ } else if (astType.startsWith('CSS')) {
+ astType = astType.replace(/^CSS/, 'C_S_S_')
}
astType = astType
@@ -148,6 +150,7 @@ async function main() {
const kebabCase = nodeTypeName
.replace(/^HTML/, 'html-')
.replace(/^ERB/, 'erb-')
+ .replace(/^CSS/, 'css-')
.replace(/([A-Z])/g, '-$1')
.toLowerCase()
.replace(/^-/, '')
diff --git a/javascript/packages/printer/src/identity-printer.ts b/javascript/packages/printer/src/identity-printer.ts
index ad592cc2d..c0ee60e77 100644
--- a/javascript/packages/printer/src/identity-printer.ts
+++ b/javascript/packages/printer/src/identity-printer.ts
@@ -354,6 +354,20 @@ export class IdentityPrinter extends Printer {
}
}
+ visitCSSStyleNode(node: Nodes.CSSStyleNode): void {
+ if (node.content) {
+ this.write(node.content)
+ }
+ }
+
+ visitCSSRuleNode(node: Nodes.CSSRuleNode): void {
+ // CSS rules are contained within CSSStyleNode, not rendered separately
+ }
+
+ visitCSSDeclarationNode(node: Nodes.CSSDeclarationNode): void {
+ // CSS declarations are contained within CSSRuleNode, not rendered separately
+ }
+
visitERBCaseMatchNode(node: Nodes.ERBCaseMatchNode): void {
this.printERBNode(node)
diff --git a/javascript/packages/printer/test/nodes/css-declaration-node.test.ts b/javascript/packages/printer/test/nodes/css-declaration-node.test.ts
new file mode 100644
index 000000000..84c63f2d7
--- /dev/null
+++ b/javascript/packages/printer/test/nodes/css-declaration-node.test.ts
@@ -0,0 +1,36 @@
+import dedent from "dedent"
+import { describe, test, beforeAll } from "vitest"
+
+import { Herb } from "@herb-tools/node-wasm"
+import { CSSDeclarationNode } from "@herb-tools/core"
+
+import { expectNodeToPrint, expectPrintRoundTrip, createLocation } from "../helpers/printer-test-helpers.js"
+
+describe("CSSDeclarationNode Printing", () => {
+ beforeAll(async () => {
+ await Herb.load()
+ })
+
+ test("CSS declarations are not printed independently", () => {
+ const node = CSSDeclarationNode.from({
+ type: "AST_CSS_DECLARATION_NODE",
+ location: createLocation(),
+ errors: [],
+ property: "color",
+ value: "red"
+ })
+
+ expectNodeToPrint(node, "")
+ })
+
+ test("CSS declarations are part of style tag content", () => {
+ expectPrintRoundTrip(dedent`
+
+ `)
+ })
+})
diff --git a/javascript/packages/printer/test/nodes/css-rule-node.test.ts b/javascript/packages/printer/test/nodes/css-rule-node.test.ts
new file mode 100644
index 000000000..e2582c1d5
--- /dev/null
+++ b/javascript/packages/printer/test/nodes/css-rule-node.test.ts
@@ -0,0 +1,35 @@
+import dedent from "dedent"
+import { describe, test, beforeAll } from "vitest"
+
+import { Herb } from "@herb-tools/node-wasm"
+import { CSSRuleNode } from "@herb-tools/core"
+
+import { expectNodeToPrint, expectPrintRoundTrip, createLocation } from "../helpers/printer-test-helpers.js"
+
+describe("CSSRuleNode Printing", () => {
+ beforeAll(async () => {
+ await Herb.load()
+ })
+
+ test("CSS rules are not printed independently", () => {
+ const node = CSSRuleNode.from({
+ type: "AST_CSS_RULE_NODE",
+ location: createLocation(),
+ errors: [],
+ selector: "body",
+ declarations: []
+ })
+
+ expectNodeToPrint(node, "")
+ })
+
+ test("CSS rules are part of style tag content", () => {
+ expectPrintRoundTrip(dedent`
+
+ `)
+ })
+})
diff --git a/javascript/packages/printer/test/nodes/css-style-node.test.ts b/javascript/packages/printer/test/nodes/css-style-node.test.ts
new file mode 100644
index 000000000..987e60134
--- /dev/null
+++ b/javascript/packages/printer/test/nodes/css-style-node.test.ts
@@ -0,0 +1,98 @@
+import dedent from "dedent"
+import { describe, test, beforeAll } from "vitest"
+
+import { Herb } from "@herb-tools/node-wasm"
+import { CSSStyleNode } from "@herb-tools/core"
+
+import { expectNodeToPrint, expectPrintRoundTrip, createLocation } from "../helpers/printer-test-helpers.js"
+
+describe("CSSStyleNode Printing", () => {
+ beforeAll(async () => {
+ await Herb.load()
+ })
+
+ test("can print from node with simple CSS", () => {
+ const cssContent = dedent`
+ body {
+ background: white;
+ color: black;
+ }
+ `
+
+ const node = CSSStyleNode.from({
+ type: "AST_CSS_STYLE_NODE",
+ location: createLocation(),
+ errors: [],
+ content: cssContent,
+ rules: [],
+ valid: true,
+ parse_error: ""
+ })
+
+ expectNodeToPrint(node, cssContent)
+ })
+
+ test("can print from node with complex CSS", () => {
+ const cssContent = dedent`
+ .container {
+ display: flex;
+ gap: 1rem;
+ }
+
+ @media (max-width: 768px) {
+ .container {
+ flex-direction: column;
+ }
+ }
+ `
+
+ const node = CSSStyleNode.from({
+ type: "AST_CSS_STYLE_NODE",
+ location: createLocation(),
+ errors: [],
+ content: cssContent,
+ rules: [],
+ valid: true,
+ parse_error: ""
+ })
+
+ expectNodeToPrint(node, cssContent)
+ })
+
+ test("can print from source with simple style tag", () => {
+ expectPrintRoundTrip(dedent`
+
+ `)
+ })
+
+ test("can print from source with media query", () => {
+ expectPrintRoundTrip(dedent`
+
+ `)
+ })
+
+ test("can print from source with CSS custom properties", () => {
+ expectPrintRoundTrip(dedent`
+
+ `)
+ })
+
+ test("can print empty style tag", () => {
+ expectPrintRoundTrip("")
+ })
+})
diff --git a/lib/herb/engine/compiler.rb b/lib/herb/engine/compiler.rb
index 3ed9124a7..92a1fe266 100644
--- a/lib/herb/engine/compiler.rb
+++ b/lib/herb/engine/compiler.rb
@@ -137,6 +137,10 @@ def visit_whitespace_node(node)
add_text(node.value.value) if node.value
end
+ def visit_css_style_node(node)
+ add_text(node.content)
+ end
+
def visit_html_comment_node(node)
add_text(node.comment_start.value)
visit_all(node.children)
diff --git a/sig/herb/ast/nodes.rbs b/sig/herb/ast/nodes.rbs
index bc41595ad..9b95a1b70 100644
--- a/sig/herb/ast/nodes.rbs
+++ b/sig/herb/ast/nodes.rbs
@@ -966,5 +966,90 @@ module Herb
# : (?Integer) -> String
def tree_inspect: (?Integer) -> String
end
+
+ class CSSDeclarationNode < Node
+ attr_reader property: String
+
+ attr_reader value: String
+
+ # : (String, Location, Array[Herb::Errors::Error], String, String) -> void
+ def initialize: (String, Location, Array[Herb::Errors::Error], String, String) -> void
+
+ # : () -> serialized_css_declaration_node
+ def to_hash: () -> serialized_css_declaration_node
+
+ # : (Visitor) -> void
+ def accept: (Visitor) -> void
+
+ # : () -> Array[Herb::AST::Node?]
+ def child_nodes: () -> Array[Herb::AST::Node?]
+
+ # : () -> Array[Herb::AST::Node]
+ def compact_child_nodes: () -> Array[Herb::AST::Node]
+
+ # : () -> String
+ def inspect: () -> String
+
+ # : (?Integer) -> String
+ def tree_inspect: (?Integer) -> String
+ end
+
+ class CSSRuleNode < Node
+ attr_reader selector: String
+
+ attr_reader declarations: Array[Herb::AST::CSSDeclarationNode]
+
+ # : (String, Location, Array[Herb::Errors::Error], String, Array[Herb::AST::CSSDeclarationNode]) -> void
+ def initialize: (String, Location, Array[Herb::Errors::Error], String, Array[Herb::AST::CSSDeclarationNode]) -> void
+
+ # : () -> serialized_css_rule_node
+ def to_hash: () -> serialized_css_rule_node
+
+ # : (Visitor) -> void
+ def accept: (Visitor) -> void
+
+ # : () -> Array[Herb::AST::Node?]
+ def child_nodes: () -> Array[Herb::AST::Node?]
+
+ # : () -> Array[Herb::AST::Node]
+ def compact_child_nodes: () -> Array[Herb::AST::Node]
+
+ # : () -> String
+ def inspect: () -> String
+
+ # : (?Integer) -> String
+ def tree_inspect: (?Integer) -> String
+ end
+
+ class CSSStyleNode < Node
+ attr_reader content: String
+
+ attr_reader rules: Array[Herb::AST::CSSRuleNode]
+
+ attr_reader valid: bool
+
+ attr_reader parse_error: String
+
+ # : (String, Location, Array[Herb::Errors::Error], String, Array[Herb::AST::CSSRuleNode], bool, String) -> void
+ def initialize: (String, Location, Array[Herb::Errors::Error], String, Array[Herb::AST::CSSRuleNode], bool, String) -> void
+
+ # : () -> serialized_css_style_node
+ def to_hash: () -> serialized_css_style_node
+
+ # : (Visitor) -> void
+ def accept: (Visitor) -> void
+
+ # : () -> Array[Herb::AST::Node?]
+ def child_nodes: () -> Array[Herb::AST::Node?]
+
+ # : () -> Array[Herb::AST::Node]
+ def compact_child_nodes: () -> Array[Herb::AST::Node]
+
+ # : () -> String
+ def inspect: () -> String
+
+ # : (?Integer) -> String
+ def tree_inspect: (?Integer) -> String
+ end
end
end
diff --git a/sig/herb/engine/compiler.rbs b/sig/herb/engine/compiler.rbs
index 926dd9e3d..e839f1a1d 100644
--- a/sig/herb/engine/compiler.rbs
+++ b/sig/herb/engine/compiler.rbs
@@ -29,6 +29,8 @@ module Herb
def visit_whitespace_node: (untyped node) -> untyped
+ def visit_css_style_node: (untyped node) -> untyped
+
def visit_html_comment_node: (untyped node) -> untyped
def visit_html_doctype_node: (untyped node) -> untyped
diff --git a/sig/herb/visitor.rbs b/sig/herb/visitor.rbs
index 768f810b4..1d66f302f 100644
--- a/sig/herb/visitor.rbs
+++ b/sig/herb/visitor.rbs
@@ -105,5 +105,14 @@ module Herb
# : (Herb::AST::ERBInNode) -> void
def visit_erb_in_node: (Herb::AST::ERBInNode) -> void
+
+ # : (Herb::AST::CSSDeclarationNode) -> void
+ def visit_css_declaration_node: (Herb::AST::CSSDeclarationNode) -> void
+
+ # : (Herb::AST::CSSRuleNode) -> void
+ def visit_css_rule_node: (Herb::AST::CSSRuleNode) -> void
+
+ # : (Herb::AST::CSSStyleNode) -> void
+ def visit_css_style_node: (Herb::AST::CSSStyleNode) -> void
end
end
diff --git a/src/css/Cargo.lock b/src/css/Cargo.lock
new file mode 100644
index 000000000..ad9697dd7
--- /dev/null
+++ b/src/css/Cargo.lock
@@ -0,0 +1,1289 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "ahash"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
+dependencies = [
+ "getrandom 0.2.16",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "getrandom 0.3.4",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
+dependencies = [
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "base64-simd"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5"
+dependencies = [
+ "simd-abstraction",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "bytecheck"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
+dependencies = [
+ "bytecheck_derive",
+ "ptr_meta",
+ "simdutf8",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "cbindgen"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb"
+dependencies = [
+ "clap",
+ "heck",
+ "indexmap",
+ "log",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "syn 2.0.108",
+ "tempfile",
+ "toml",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "clap"
+version = "4.5.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623"
+dependencies = [
+ "clap_builder",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "const-str"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21077772762a1002bb421c3af42ac1725fa56066bfc53d9a55bb79905df2aaf3"
+dependencies = [
+ "const-str-proc-macro",
+]
+
+[[package]]
+name = "const-str-proc-macro"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e1e0fdd2e5d3041e530e1b21158aeeef8b5d0e306bc5c1e3d6cf0930d10e25a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "cssparser"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be934d936a0fbed5bcdc01042b770de1398bf79d0e192f49fa7faea0e99281e"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "phf",
+ "smallvec",
+]
+
+[[package]]
+name = "cssparser-color"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556c099a61d85989d7af52b692e35a8d68a57e7df8c6d07563dc0778b3960c9f"
+dependencies = [
+ "cssparser",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
+dependencies = [
+ "quote",
+ "syn 2.0.108",
+]
+
+[[package]]
+name = "dashmap"
+version = "5.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
+dependencies = [
+ "cfg-if",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
+
+[[package]]
+name = "data-url"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193"
+dependencies = [
+ "matches",
+]
+
+[[package]]
+name = "dtoa"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash 0.7.8",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hashbrown"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "herb_css_parser"
+version = "0.7.5"
+dependencies = [
+ "cbindgen",
+ "lightningcss",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.0",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.177"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
+
+[[package]]
+name = "lightningcss"
+version = "1.0.0-alpha.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b407ca668368d1d5a86cea58ac82d9f9f9ca4bac1e9dce6f16f875f0f081a911"
+dependencies = [
+ "ahash 0.8.12",
+ "bitflags",
+ "const-str",
+ "cssparser",
+ "cssparser-color",
+ "dashmap",
+ "data-encoding",
+ "getrandom 0.3.4",
+ "indexmap",
+ "itertools",
+ "lazy_static",
+ "lightningcss-derive",
+ "parcel_selectors",
+ "parcel_sourcemap",
+ "pastey",
+ "pathdiff",
+ "rayon",
+ "serde",
+ "serde-content",
+ "smallvec",
+]
+
+[[package]]
+name = "lightningcss-derive"
+version = "1.0.0-alpha.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c12744d1279367caed41739ef094c325d53fb0ffcd4f9b84a368796f870252"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "outref"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4"
+
+[[package]]
+name = "parcel_selectors"
+version = "0.28.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54fd03f1ad26cb6b3ec1b7414fa78a3bd639e7dbb421b1a60513c96ce886a196"
+dependencies = [
+ "bitflags",
+ "cssparser",
+ "log",
+ "phf",
+ "phf_codegen",
+ "precomputed-hash",
+ "rustc-hash",
+ "smallvec",
+]
+
+[[package]]
+name = "parcel_sourcemap"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "485b74d7218068b2b7c0e3ff12fbc61ae11d57cb5d8224f525bd304c6be05bbb"
+dependencies = [
+ "base64-simd",
+ "data-url",
+ "rkyv",
+ "serde",
+ "serde_json",
+ "vlq",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "pastey"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
+name = "phf"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "ptr_meta"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
+dependencies = [
+ "ptr_meta_derive",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
+[[package]]
+name = "rayon"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "rend"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
+dependencies = [
+ "bytecheck",
+]
+
+[[package]]
+name = "rkyv"
+version = "0.7.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b"
+dependencies = [
+ "bitvec",
+ "bytecheck",
+ "bytes",
+ "hashbrown 0.12.3",
+ "ptr_meta",
+ "rend",
+ "rkyv_derive",
+ "seahash",
+ "tinyvec",
+ "uuid",
+]
+
+[[package]]
+name = "rkyv_derive"
+version = "0.7.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
+name = "rustix"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "seahash"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-content"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3753ca04f350fa92d00b6146a3555e63c55388c9ef2e11e09bce2ff1c0b509c6"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "simd-abstraction"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987"
+dependencies = [
+ "outref",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "siphasher"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.108"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "tempfile"
+version = "3.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.4",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "vlq"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.1+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "winnow"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+]
diff --git a/src/css/Cargo.toml b/src/css/Cargo.toml
new file mode 100644
index 000000000..62afba42b
--- /dev/null
+++ b/src/css/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "herb_css_parser"
+version = "0.7.5"
+edition = "2021"
+
+[lib]
+crate-type = ["staticlib"]
+
+[dependencies]
+lightningcss = "1.0.0-alpha.59"
+
+[build-dependencies]
+cbindgen = "0.27"
diff --git a/src/css/build.rs b/src/css/build.rs
new file mode 100644
index 000000000..4076048b7
--- /dev/null
+++ b/src/css/build.rs
@@ -0,0 +1,14 @@
+extern crate cbindgen;
+
+use std::env;
+
+fn main() {
+ let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
+
+ cbindgen::Builder::new()
+ .with_crate(crate_dir)
+ .with_config(cbindgen::Config::from_file("cbindgen.toml").unwrap())
+ .generate()
+ .expect("Unable to generate bindings")
+ .write_to_file("../include/css_parser.h");
+}
diff --git a/src/css/cbindgen.toml b/src/css/cbindgen.toml
new file mode 100644
index 000000000..459b576e2
--- /dev/null
+++ b/src/css/cbindgen.toml
@@ -0,0 +1,13 @@
+language = "C"
+include_guard = "HERB_CSS_H"
+autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
+header = "/* Herb CSS Parser - Lightning CSS Integration */"
+include_version = true
+namespace = "herb"
+cpp_compat = true
+
+[export]
+include = ["CSSParseResult"]
+
+[export.rename]
+"CSSParseResult" = "css_parse_result_T"
diff --git a/src/css/src/lib.rs b/src/css/src/lib.rs
new file mode 100644
index 000000000..871ce6dbf
--- /dev/null
+++ b/src/css/src/lib.rs
@@ -0,0 +1,189 @@
+use lightningcss::stylesheet::{ParserOptions, StyleSheet};
+use lightningcss::rules::CssRule;
+use std::ffi::{CStr, CString};
+use std::os::raw::c_char;
+use std::ptr;
+
+#[repr(C)]
+pub struct CSSDeclaration {
+ pub property: *mut c_char,
+ pub value: *mut c_char,
+}
+
+#[repr(C)]
+pub struct CSSRule {
+ pub selector: *mut c_char,
+ pub declarations: *mut *mut CSSDeclaration,
+ pub declaration_count: usize,
+}
+
+#[repr(C)]
+pub struct CSSParseResult {
+ pub success: bool,
+ pub error_message: *mut c_char,
+ pub rules: *mut *mut CSSRule,
+ pub rule_count: usize,
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn herb_css_parse(css_input: *const c_char) -> *mut CSSParseResult {
+ if css_input.is_null() {
+ return create_error_result_ptr("Input CSS is null");
+ }
+
+ let c_str = unsafe { CStr::from_ptr(css_input) };
+ let css_str = match c_str.to_str() {
+ Ok(s) => s,
+ Err(_) => {
+ return create_error_result_ptr("Invalid UTF-8 in CSS input");
+ }
+ };
+
+ let result = Box::new(parse_css_to_rules(css_str));
+ Box::into_raw(result)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn herb_css_validate(css_input: *const c_char) -> bool {
+ if css_input.is_null() {
+ return false;
+ }
+
+ let c_str = unsafe { CStr::from_ptr(css_input) };
+ let css_str = match c_str.to_str() {
+ Ok(s) => s,
+ Err(_) => return false,
+ };
+
+ StyleSheet::parse(css_str, ParserOptions::default()).is_ok()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn herb_css_free_result(result: *mut CSSParseResult) {
+ if result.is_null() {
+ return;
+ }
+
+ let result = unsafe { Box::from_raw(result) };
+
+ if !result.error_message.is_null() {
+ unsafe {
+ let _ = CString::from_raw(result.error_message);
+ }
+ }
+
+ if !result.rules.is_null() {
+ unsafe {
+ for i in 0..result.rule_count {
+ let rule_ptr = *result.rules.offset(i as isize);
+ if !rule_ptr.is_null() {
+ let rule = Box::from_raw(rule_ptr);
+ if !rule.selector.is_null() {
+ let _ = CString::from_raw(rule.selector);
+ }
+ if !rule.declarations.is_null() {
+ for j in 0..rule.declaration_count {
+ let decl_ptr = *rule.declarations.offset(j as isize);
+ if !decl_ptr.is_null() {
+ let decl = Box::from_raw(decl_ptr);
+ if !decl.property.is_null() {
+ let _ = CString::from_raw(decl.property);
+ }
+ if !decl.value.is_null() {
+ let _ = CString::from_raw(decl.value);
+ }
+ }
+ }
+ let _ = Vec::from_raw_parts(rule.declarations, rule.declaration_count, rule.declaration_count);
+ }
+ }
+ }
+ let _ = Vec::from_raw_parts(result.rules, result.rule_count, result.rule_count);
+ }
+ }
+}
+
+fn parse_css_to_rules(css_str: &str) -> CSSParseResult {
+ match StyleSheet::parse(css_str, ParserOptions::default()) {
+ Ok(stylesheet) => {
+ let mut rules_vec: Vec<*mut CSSRule> = Vec::new();
+
+ for rule in &stylesheet.rules.0 {
+ if let CssRule::Style(style_rule) = rule {
+ let selector_str = format!("{:?}", style_rule.selectors);
+ let selector = CString::new(selector_str.trim()).unwrap_or_else(|_| CString::new("").unwrap());
+
+ let mut decls_vec: Vec<*mut CSSDeclaration> = Vec::new();
+
+ for decl in &style_rule.declarations.declarations {
+ let prop_name = decl.property_id().name().to_string();
+ let prop_value = format!("{:?}", decl);
+
+ let property = CString::new(prop_name).unwrap_or_else(|_| CString::new("").unwrap());
+ let value = CString::new(prop_value).unwrap_or_else(|_| CString::new("").unwrap());
+
+ let css_decl = Box::new(CSSDeclaration {
+ property: property.into_raw(),
+ value: value.into_raw(),
+ });
+
+ decls_vec.push(Box::into_raw(css_decl));
+ }
+
+ let decl_count = decls_vec.len();
+
+ let decls_ptr = if decl_count > 0 {
+ let mut boxed = decls_vec.into_boxed_slice();
+ let ptr = boxed.as_mut_ptr();
+ std::mem::forget(boxed);
+ ptr
+ } else {
+ ptr::null_mut()
+ };
+
+ let css_rule = Box::new(CSSRule {
+ selector: selector.into_raw(),
+ declarations: decls_ptr,
+ declaration_count: decl_count,
+ });
+
+ rules_vec.push(Box::into_raw(css_rule));
+ }
+ }
+
+ let rule_count = rules_vec.len();
+ let rules_ptr = if rule_count > 0 {
+ let mut boxed = rules_vec.into_boxed_slice();
+ let ptr = boxed.as_mut_ptr();
+ std::mem::forget(boxed);
+ ptr
+ } else {
+ ptr::null_mut()
+ };
+
+ CSSParseResult {
+ success: true,
+ error_message: ptr::null_mut(),
+ rules: rules_ptr,
+ rule_count,
+ }
+ }
+ Err(e) => create_error_result(&format!("CSS parse error: {:?}", e)),
+ }
+}
+
+fn create_error_result(error: &str) -> CSSParseResult {
+ let error_message = CString::new(error).unwrap_or_else(|_| CString::new("Unknown error").unwrap());
+
+ CSSParseResult {
+ success: false,
+ error_message: error_message.into_raw(),
+ rules: ptr::null_mut(),
+ rule_count: 0,
+ }
+}
+
+fn create_error_result_ptr(error: &str) -> *mut CSSParseResult {
+ let result = Box::new(create_error_result(error));
+ Box::into_raw(result)
+}
diff --git a/src/css_node_helpers.c b/src/css_node_helpers.c
new file mode 100644
index 000000000..d6503d472
--- /dev/null
+++ b/src/css_node_helpers.c
@@ -0,0 +1,100 @@
+#include "include/css_node_helpers.h"
+#include "include/ast_nodes.h"
+#include "include/css_parser.h"
+#include "include/util.h"
+#include "include/util/hb_array.h"
+
+#include
+#include
+
+AST_CSS_STYLE_NODE_T* create_css_style_node(
+ const char* css_content,
+ position_T start_position,
+ position_T end_position
+) {
+ if (!css_content) {
+ return ast_css_style_node_init(
+ "",
+ hb_array_init(0),
+ false,
+ "No CSS content provided",
+ start_position,
+ end_position,
+ hb_array_init(0)
+ );
+ }
+
+ struct css_parse_result_T* result = herb_css_parse(css_content);
+
+ if (!result) {
+ return ast_css_style_node_init(
+ css_content,
+ hb_array_init(0),
+ false,
+ "Failed to parse CSS",
+ start_position,
+ end_position,
+ hb_array_init(0)
+ );
+ }
+
+ AST_CSS_STYLE_NODE_T* node;
+
+ if (result->success) {
+ hb_array_T* rules = hb_array_init(result->rule_count);
+
+ for (size_t i = 0; i < result->rule_count; i++) {
+ struct CSSRule* css_rule = result->rules[i];
+
+ hb_array_T* declarations = hb_array_init(css_rule->declaration_count);
+
+ for (size_t j = 0; j < css_rule->declaration_count; j++) {
+ struct CSSDeclaration* css_decl = css_rule->declarations[j];
+
+ AST_CSS_DECLARATION_NODE_T* decl_node = ast_css_declaration_node_init(
+ herb_strdup(css_decl->property),
+ herb_strdup(css_decl->value),
+ start_position,
+ end_position,
+ hb_array_init(0)
+ );
+
+ hb_array_append(declarations, decl_node);
+ }
+
+ AST_CSS_RULE_NODE_T* rule_node = ast_css_rule_node_init(
+ herb_strdup(css_rule->selector),
+ declarations,
+ start_position,
+ end_position,
+ hb_array_init(0)
+ );
+
+ hb_array_append(rules, rule_node);
+ }
+
+ node = ast_css_style_node_init(
+ herb_strdup(css_content),
+ rules,
+ true,
+ "",
+ start_position,
+ end_position,
+ hb_array_init(0)
+ );
+ } else {
+ node = ast_css_style_node_init(
+ herb_strdup(css_content),
+ hb_array_init(0),
+ false,
+ result->error_message ? herb_strdup(result->error_message) : herb_strdup("Unknown CSS parse error"),
+ start_position,
+ end_position,
+ hb_array_init(0)
+ );
+ }
+
+ herb_css_free_result(result);
+
+ return node;
+}
diff --git a/src/include/css_node_helpers.h b/src/include/css_node_helpers.h
new file mode 100644
index 000000000..9063ab330
--- /dev/null
+++ b/src/include/css_node_helpers.h
@@ -0,0 +1,21 @@
+#ifndef HERB_CSS_NODE_HELPERS_H
+#define HERB_CSS_NODE_HELPERS_H
+
+#include "ast_nodes.h"
+#include "position.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+AST_CSS_STYLE_NODE_T* create_css_style_node(
+ const char* css_content,
+ position_T start_position,
+ position_T end_position
+);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/include/parser_helpers.h b/src/include/parser_helpers.h
index b3ab98300..996dd0811 100644
--- a/src/include/parser_helpers.h
+++ b/src/include/parser_helpers.h
@@ -28,6 +28,13 @@ void parser_append_literal_node_from_buffer(
position_T start
);
+void parser_append_css_node_from_buffer(
+ const parser_T* parser,
+ hb_buffer_T* buffer,
+ hb_array_T* children,
+ position_T start
+);
+
bool parser_in_svg_context(const parser_T* parser);
foreign_content_type_T parser_get_foreign_content_type(hb_string_T tag_name);
diff --git a/src/main.c b/src/main.c
index 52b1f370c..3c8f6a522 100644
--- a/src/main.c
+++ b/src/main.c
@@ -4,6 +4,7 @@
#include "include/ast_node.h"
#include "include/ast_nodes.h"
#include "include/ast_pretty_print.h"
+#include "include/css_parser.h"
#include "include/extract.h"
#include "include/herb.h"
#include "include/io.h"
@@ -11,6 +12,7 @@
#include "include/util/hb_buffer.h"
#include
+#include
#include
#include
@@ -43,6 +45,7 @@ int main(const int argc, char* argv[]) {
printf("./herb ruby [file] - Extract Ruby from a file\n");
printf("./herb html [file] - Extract HTML from a file\n");
printf("./herb prism [file] - Extract Ruby from a file and parse the Ruby source with Prism\n");
+ printf("./herb css [file] - Parse CSS using Lightning CSS\n");
return 1;
}
@@ -153,6 +156,40 @@ int main(const int argc, char* argv[]) {
return 0;
}
+ if (strcmp(argv[1], "css") == 0) {
+ printf("CSS Input:\n%s\n\n", source);
+
+ struct css_parse_result_T* result = herb_css_parse(source);
+ clock_gettime(CLOCK_MONOTONIC, &end);
+
+ if (result->success) {
+ printf("CSS parsed successfully!\n");
+ printf("Found %zu CSS rule(s):\n\n", result->rule_count);
+
+ for (size_t i = 0; i < result->rule_count; i++) {
+ struct CSSRule* rule = result->rules[i];
+ printf("Rule %zu:\n", i + 1);
+ printf(" Selector: %s\n", rule->selector);
+ printf(" Declarations (%zu):\n", rule->declaration_count);
+
+ for (size_t j = 0; j < rule->declaration_count; j++) {
+ struct CSSDeclaration* decl = rule->declarations[j];
+ printf(" %s: %s\n", decl->property, decl->value);
+ }
+ printf("\n");
+ }
+ } else {
+ printf("CSS Parse Error:\n%s\n\n", result->error_message);
+ }
+
+ print_time_diff(start, end, "parsing CSS");
+
+ herb_css_free_result(result);
+ free(source);
+
+ return result->success ? 0 : 1;
+ }
+
printf("Unknown Command: %s\n", argv[1]);
return 1;
}
diff --git a/src/parser.c b/src/parser.c
index 3cc25791a..297d07212 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -1021,6 +1021,7 @@ static void parser_parse_foreign_content(parser_T* parser, hb_array_T* children,
hb_buffer_init(&content, 1024);
position_T start = parser->current_token->location.start;
hb_string_T expected_closing_tag = parser_get_foreign_content_closing_tag(parser->foreign_content_type);
+ bool is_style_tag = (parser->foreign_content_type == FOREIGN_CONTENT_STYLE);
if (hb_string_is_empty(expected_closing_tag)) {
parser_exit_foreign_content(parser);
@@ -1031,7 +1032,11 @@ static void parser_parse_foreign_content(parser_T* parser, hb_array_T* children,
while (!token_is(parser, TOKEN_EOF)) {
if (token_is(parser, TOKEN_ERB_START)) {
- parser_append_literal_node_from_buffer(parser, &content, children, start);
+ if (is_style_tag) {
+ parser_append_css_node_from_buffer(parser, &content, children, start);
+ } else {
+ parser_append_literal_node_from_buffer(parser, &content, children, start);
+ }
AST_ERB_CONTENT_NODE_T* erb_node = parser_parse_erb_tag(parser);
hb_array_append(children, erb_node);
@@ -1057,7 +1062,12 @@ static void parser_parse_foreign_content(parser_T* parser, hb_array_T* children,
if (next_token) { token_free(next_token); }
if (is_potential_match) {
- parser_append_literal_node_from_buffer(parser, &content, children, start);
+ if (is_style_tag) {
+ parser_append_css_node_from_buffer(parser, &content, children, start);
+ } else {
+ parser_append_literal_node_from_buffer(parser, &content, children, start);
+ }
+
parser_exit_foreign_content(parser);
free(content.value);
@@ -1071,7 +1081,12 @@ static void parser_parse_foreign_content(parser_T* parser, hb_array_T* children,
token_free(token);
}
- parser_append_literal_node_from_buffer(parser, &content, children, start);
+ if (is_style_tag) {
+ parser_append_css_node_from_buffer(parser, &content, children, start);
+ } else {
+ parser_append_literal_node_from_buffer(parser, &content, children, start);
+ }
+
parser_exit_foreign_content(parser);
free(content.value);
}
diff --git a/src/parser_helpers.c b/src/parser_helpers.c
index f34d97864..2b6dbd950 100644
--- a/src/parser_helpers.c
+++ b/src/parser_helpers.c
@@ -1,6 +1,7 @@
#include "include/parser_helpers.h"
#include "include/ast_node.h"
#include "include/ast_nodes.h"
+#include "include/css_node_helpers.h"
#include "include/errors.h"
#include "include/html_util.h"
#include "include/lexer.h"
@@ -138,6 +139,21 @@ void parser_append_literal_node_from_buffer(
hb_buffer_clear(buffer);
}
+void parser_append_css_node_from_buffer(
+ const parser_T* parser,
+ hb_buffer_T* buffer,
+ hb_array_T* children,
+ position_T start
+) {
+ if (hb_buffer_length(buffer) == 0) { return; }
+
+ AST_CSS_STYLE_NODE_T* css_node =
+ create_css_style_node(hb_buffer_value(buffer), start, parser->current_token->location.start);
+
+ if (children != NULL) { hb_array_append(children, css_node); }
+ hb_buffer_clear(buffer);
+}
+
token_T* parser_advance(parser_T* parser) {
token_T* token = parser->current_token;
parser->current_token = lexer_next_token(parser->lexer);
diff --git a/test/c/main.c b/test/c/main.c
index ef4238806..e3d6422b4 100644
--- a/test/c/main.c
+++ b/test/c/main.c
@@ -11,6 +11,7 @@ TCase *io_tests(void);
TCase *lex_tests(void);
TCase *token_tests(void);
TCase *util_tests(void);
+TCase *css_tests(void);
Suite *herb_suite(void) {
Suite *suite = suite_create("Herb Suite");
@@ -25,6 +26,7 @@ Suite *herb_suite(void) {
suite_add_tcase(suite, lex_tests());
suite_add_tcase(suite, token_tests());
suite_add_tcase(suite, util_tests());
+ suite_add_tcase(suite, css_tests());
return suite;
}
diff --git a/test/c/test_css.c b/test/c/test_css.c
new file mode 100644
index 000000000..7b1891e50
--- /dev/null
+++ b/test/c/test_css.c
@@ -0,0 +1,104 @@
+#include "../../src/include/css_parser.h"
+#include "include/test.h"
+
+#include
+#include
+#include
+#include
+
+TEST(test_css_parse_valid)
+ const char* css = "body { color: red; padding: 10px; }";
+ struct css_parse_result_T* result = herb_css_parse(css);
+
+ ck_assert_ptr_nonnull(result);
+ ck_assert(result->success);
+ ck_assert_ptr_null(result->error_message);
+ ck_assert_ptr_nonnull(result->rules);
+ ck_assert_uint_gt(result->rule_count, 0);
+
+ ck_assert_uint_eq(result->rule_count, 1);
+
+ struct CSSRule* rule = result->rules[0];
+ ck_assert_ptr_nonnull(rule->selector);
+
+ ck_assert_uint_gt(rule->declaration_count, 0);
+
+ herb_css_free_result(result);
+END
+
+TEST(test_css_parse_invalid)
+ const char* css = "body { @@@invalid }"; // Invalid CSS
+ struct css_parse_result_T* result = herb_css_parse(css);
+
+ ck_assert_ptr_nonnull(result);
+ ck_assert(!result->success);
+ ck_assert_ptr_nonnull(result->error_message);
+ ck_assert_ptr_null(result->rules);
+
+ herb_css_free_result(result);
+END
+
+TEST(test_css_parse_declarations)
+ const char* css = "body {\n color: red;\n padding: 10px;\n}";
+ struct css_parse_result_T* result = herb_css_parse(css);
+
+ ck_assert_ptr_nonnull(result);
+ ck_assert(result->success);
+ ck_assert_ptr_nonnull(result->rules);
+ ck_assert_uint_eq(result->rule_count, 1);
+
+ struct CSSRule* rule = result->rules[0];
+ ck_assert_uint_eq(rule->declaration_count, 2);
+
+ herb_css_free_result(result);
+END
+
+TEST(test_css_validate_valid)
+ const char* css = "body { color: red; }";
+ bool valid = herb_css_validate(css);
+
+ ck_assert(valid);
+END
+
+TEST(test_css_validate_invalid)
+ const char* css = "body { @@@invalid }";
+ bool valid = herb_css_validate(css);
+
+ ck_assert(!valid);
+END
+
+TEST(test_css_parse_multiple_rules)
+ const char* css = "body { color: red; } p { margin: 5px; }";
+ struct css_parse_result_T* result = herb_css_parse(css);
+
+ ck_assert_ptr_nonnull(result);
+ ck_assert(result->success);
+ ck_assert_uint_eq(result->rule_count, 2);
+
+ herb_css_free_result(result);
+END
+
+TEST(test_css_parse_empty)
+ const char* css = "";
+ struct css_parse_result_T* result = herb_css_parse(css);
+
+ ck_assert_ptr_nonnull(result);
+ ck_assert(result->success);
+ ck_assert_uint_eq(result->rule_count, 0);
+
+ herb_css_free_result(result);
+END
+
+TCase* css_tests(void) {
+ TCase *css = tcase_create("CSS Parser (Lightning CSS)");
+
+ tcase_add_test(css, test_css_parse_valid);
+ tcase_add_test(css, test_css_parse_invalid);
+ tcase_add_test(css, test_css_parse_declarations);
+ tcase_add_test(css, test_css_parse_multiple_rules);
+ tcase_add_test(css, test_css_parse_empty);
+ tcase_add_test(css, test_css_validate_valid);
+ tcase_add_test(css, test_css_validate_invalid);
+
+ return css;
+}
diff --git a/test/snapshots/parser/script_style_test/test_0013_style_tag_with_child_selector_3675980b06d0732a42d3e3d4da50428f.txt b/test/snapshots/parser/script_style_test/test_0013_style_tag_with_child_selector_3675980b06d0732a42d3e3d4da50428f.txt
index a979c4e5f..4e2a43805 100644
--- a/test/snapshots/parser/script_style_test/test_0013_style_tag_with_child_selector_3675980b06d0732a42d3e3d4da50428f.txt
+++ b/test/snapshots/parser/script_style_test/test_0013_style_tag_with_child_selector_3675980b06d0732a42d3e3d4da50428f.txt
@@ -11,8 +11,19 @@
│
├── tag_name: "style" (location: (1:1)-(1:6))
├── body: (1 item)
- │ └── @ LiteralNode (location: (1:7)-(1:39))
- │ └── content: ".parent > .child { color: red; }"
+ │ └── @ CSSStyleNode (location: (1:7)-(1:39))
+ │ ├── content: ".parent > .child { color: red; }"
+ │ ├── rules: (1 item)
+ │ │ └── @ CSSRuleNode (location: (1:7)-(1:39))
+ │ │ ├── selector: "SelectorList([Selector(.parent > .child, specificity = 0x800)])"
+ │ │ └── declarations: (1 item)
+ │ │ └── @ CSSDeclarationNode (location: (1:7)-(1:39))
+ │ │ ├── property: "color"
+ │ │ └── value: "Color(RGBA(RGBA { red: 255, green: 0, blue: 0, alpha: 255 }))"
+ │ │
+ │ │
+ │ ├── valid: true
+ │ └── parse_error: ""
│
├── close_tag:
│ └── @ HTMLCloseTagNode (location: (1:39)-(1:47))
diff --git a/test/snapshots/parser/script_style_test/test_0014_style_tag_with_attribute_selector_containing_HTML_6024cebc93b5103ba789a52ba13c2f01.txt b/test/snapshots/parser/script_style_test/test_0014_style_tag_with_attribute_selector_containing_HTML_6024cebc93b5103ba789a52ba13c2f01.txt
index 2e48b66e5..b03caf942 100644
--- a/test/snapshots/parser/script_style_test/test_0014_style_tag_with_attribute_selector_containing_HTML_6024cebc93b5103ba789a52ba13c2f01.txt
+++ b/test/snapshots/parser/script_style_test/test_0014_style_tag_with_attribute_selector_containing_HTML_6024cebc93b5103ba789a52ba13c2f01.txt
@@ -11,8 +11,19 @@
│
├── tag_name: "style" (location: (1:1)-(1:6))
├── body: (1 item)
- │ └── @ LiteralNode (location: (1:7)-(1:57))
- │ └── content: "input[placeholder=\"\"] { color: blue; }"
+ │ └── @ CSSStyleNode (location: (1:7)-(1:57))
+ │ ├── content: "input[placeholder=\"\"] { color: blue; }"
+ │ ├── rules: (1 item)
+ │ │ └── @ CSSRuleNode (location: (1:7)-(1:57))
+ │ │ ├── selector: "SelectorList([Selector(input[placeholder=\"\"], specificity = 0x401)])"
+ │ │ └── declarations: (1 item)
+ │ │ └── @ CSSDeclarationNode (location: (1:7)-(1:57))
+ │ │ ├── property: "color"
+ │ │ └── value: "Color(RGBA(RGBA { red: 0, green: 0, blue: 255, alpha: 255 }))"
+ │ │
+ │ │
+ │ ├── valid: true
+ │ └── parse_error: ""
│
├── close_tag:
│ └── @ HTMLCloseTagNode (location: (1:57)-(1:65))
diff --git a/test/snapshots/parser/script_style_test/test_0015_style_tag_with_content_property_containing_HTML_aa75bbfce10aaa99576a8041bc62e42a.txt b/test/snapshots/parser/script_style_test/test_0015_style_tag_with_content_property_containing_HTML_aa75bbfce10aaa99576a8041bc62e42a.txt
index 6b8bcbe04..9bae5773a 100644
--- a/test/snapshots/parser/script_style_test/test_0015_style_tag_with_content_property_containing_HTML_aa75bbfce10aaa99576a8041bc62e42a.txt
+++ b/test/snapshots/parser/script_style_test/test_0015_style_tag_with_content_property_containing_HTML_aa75bbfce10aaa99576a8041bc62e42a.txt
@@ -11,8 +11,19 @@
│
├── tag_name: "style" (location: (1:1)-(1:6))
├── body: (1 item)
- │ └── @ LiteralNode (location: (1:7)-(1:51))
- │ └── content: ".icon::before { content: \"★\"; }"
+ │ └── @ CSSStyleNode (location: (1:7)-(1:51))
+ │ ├── content: ".icon::before { content: \"★\"; }"
+ │ ├── rules: (1 item)
+ │ │ └── @ CSSRuleNode (location: (1:7)-(1:51))
+ │ │ ├── selector: "SelectorList([Selector(.icon:before, specificity = 0x401)])"
+ │ │ └── declarations: (1 item)
+ │ │ └── @ CSSDeclarationNode (location: (1:7)-(1:51))
+ │ │ ├── property: "content"
+ │ │ └── value: "Custom(CustomProperty { name: Unknown(Ident(\"content\")), value: TokenList([Token(String(\"★\"))]) })"
+ │ │
+ │ │
+ │ ├── valid: true
+ │ └── parse_error: ""
│
├── close_tag:
│ └── @ HTMLCloseTagNode (location: (1:51)-(1:59))
diff --git a/test/snapshots/parser/script_style_test/test_0016_style_tag_with_ERB_interpolation_e7608ec944a2eed2de2082144b6ccbff.txt b/test/snapshots/parser/script_style_test/test_0016_style_tag_with_ERB_interpolation_e7608ec944a2eed2de2082144b6ccbff.txt
index b7105dc8a..a6802964f 100644
--- a/test/snapshots/parser/script_style_test/test_0016_style_tag_with_ERB_interpolation_e7608ec944a2eed2de2082144b6ccbff.txt
+++ b/test/snapshots/parser/script_style_test/test_0016_style_tag_with_ERB_interpolation_e7608ec944a2eed2de2082144b6ccbff.txt
@@ -11,8 +11,11 @@
│
├── tag_name: "style" (location: (1:1)-(1:6))
├── body: (5 items)
- │ ├── @ LiteralNode (location: (1:7)-(1:13))
- │ │ └── content: ".user-"
+ │ ├── @ CSSStyleNode (location: (1:7)-(1:13))
+ │ │ ├── content: ".user-"
+ │ │ ├── rules: []
+ │ │ ├── valid: false
+ │ │ └── parse_error: "CSS parse error: Error { kind: EndOfInput, loc: Some(ErrorLocation { filename: \"\", line: 0, column: 7 }) }"
│ │
│ ├── @ ERBContentNode (location: (1:13)-(1:28))
│ │ ├── tag_opening: "<%=" (location: (1:13)-(1:16))
@@ -21,8 +24,11 @@
│ │ ├── parsed: true
│ │ └── valid: true
│ │
- │ ├── @ LiteralNode (location: (1:28)-(1:38))
- │ │ └── content: " { color: "
+ │ ├── @ CSSStyleNode (location: (1:28)-(1:38))
+ │ │ ├── content: " { color: "
+ │ │ ├── rules: []
+ │ │ ├── valid: false
+ │ │ └── parse_error: "CSS parse error: Error { kind: SelectorError(EmptySelector), loc: Some(ErrorLocation { filename: \"\", line: 0, column: 2 }) }"
│ │
│ ├── @ ERBContentNode (location: (1:38)-(1:57))
│ │ ├── tag_opening: "<%=" (location: (1:38)-(1:41))
@@ -31,8 +37,11 @@
│ │ ├── parsed: true
│ │ └── valid: true
│ │
- │ └── @ LiteralNode (location: (1:57)-(1:60))
- │ └── content: "; }"
+ │ └── @ CSSStyleNode (location: (1:57)-(1:60))
+ │ ├── content: "; }"
+ │ ├── rules: []
+ │ ├── valid: false
+ │ └── parse_error: "CSS parse error: Error { kind: EndOfInput, loc: Some(ErrorLocation { filename: \"\", line: 0, column: 4 }) }"
│
├── close_tag:
│ └── @ HTMLCloseTagNode (location: (1:60)-(1:68))
diff --git a/test/snapshots/parser/script_style_test/test_0017_style_tag_with_complex_CSS_including_HTML-like_content_2029b62a2d67c13cdb6a6109966d1952.txt b/test/snapshots/parser/script_style_test/test_0017_style_tag_with_complex_CSS_including_HTML-like_content_2029b62a2d67c13cdb6a6109966d1952.txt
index b2aed1e72..c9c95e1a2 100644
--- a/test/snapshots/parser/script_style_test/test_0017_style_tag_with_complex_CSS_including_HTML-like_content_2029b62a2d67c13cdb6a6109966d1952.txt
+++ b/test/snapshots/parser/script_style_test/test_0017_style_tag_with_complex_CSS_including_HTML-like_content_2029b62a2d67c13cdb6a6109966d1952.txt
@@ -11,8 +11,27 @@
│ │
│ ├── tag_name: "style" (location: (1:1)-(1:6))
│ ├── body: (1 item)
- │ │ └── @ LiteralNode (location: (1:7)-(9:0))
- │ │ └── content: "\n /* Comment with HTML
*/\n .tooltip::after {\n content: \"\";\n }\n .container > .item:first-child {\n background: url(\"data:image/svg+xml,\");\n }\n"
+ │ │ └── @ CSSStyleNode (location: (1:7)-(9:0))
+ │ │ ├── content: "\n /* Comment with HTML
*/\n .tooltip::after {\n content: \"\";\n }\n .container > .item:first-child {\n background: url(\"data:image/svg+xml,\");\n }\n"
+ │ │ ├── rules: (2 items)
+ │ │ │ ├── @ CSSRuleNode (location: (1:7)-(9:0))
+ │ │ │ │ ├── selector: "SelectorList([Selector(.tooltip:after, specificity = 0x401)])"
+ │ │ │ │ └── declarations: (1 item)
+ │ │ │ │ └── @ CSSDeclarationNode (location: (1:7)-(9:0))
+ │ │ │ │ ├── property: "content"
+ │ │ │ │ └── value: "Custom(CustomProperty { name: Unknown(Ident(\"content\")), value: TokenList([Token(String(\"\"))]) })"
+ │ │ │ │
+ │ │ │ │
+ │ │ │ └── @ CSSRuleNode (location: (1:7)-(9:0))
+ │ │ │ ├── selector: "SelectorList([Selector(.container > .item:first-child, specificity = 0xc00)])"
+ │ │ │ └── declarations: (1 item)
+ │ │ │ └── @ CSSDeclarationNode (location: (1:7)-(9:0))
+ │ │ │ ├── property: "background"
+ │ │ │ └── value: "Background([Background { image: Url(Url { url: \"data:image/svg+xml,\", loc: Location { line: 7, column: 17 } }), color: RGBA(RGBA { red: 0, green: 0, blue: 0, alpha: 0 }), position: BackgroundPosition { x: Length(Percentage(Percentage(0.0))), y: Length(Percentage(Percentage(0.0))) }, repeat: BackgroundRepeat { x: Repeat, y: Repeat }, size: Explicit { width: Auto, height: Auto }, attachment: Scroll, origin: PaddingBox, clip: BorderBox }])"
+ │ │ │
+ │ │ │
+ │ │ ├── valid: true
+ │ │ └── parse_error: ""
│ │
│ ├── close_tag:
│ │ └── @ HTMLCloseTagNode (location: (9:0)-(9:8))
diff --git a/test/snapshots/parser/script_style_test/test_0020_style_tag_with_media_query_and_nested_selectors_85e3c3a8a155fa6bba717f6f71bd20d9.txt b/test/snapshots/parser/script_style_test/test_0020_style_tag_with_media_query_and_nested_selectors_85e3c3a8a155fa6bba717f6f71bd20d9.txt
index 70941b845..0412c6658 100644
--- a/test/snapshots/parser/script_style_test/test_0020_style_tag_with_media_query_and_nested_selectors_85e3c3a8a155fa6bba717f6f71bd20d9.txt
+++ b/test/snapshots/parser/script_style_test/test_0020_style_tag_with_media_query_and_nested_selectors_85e3c3a8a155fa6bba717f6f71bd20d9.txt
@@ -11,8 +11,11 @@
│ │
│ ├── tag_name: "style" (location: (1:1)-(1:6))
│ ├── body: (1 item)
- │ │ └── @ LiteralNode (location: (1:7)-(7:0))
- │ │ └── content: "\n @media (max-width: 768px) {\n .nav > ul > li {\n display: block;\n }\n }\n"
+ │ │ └── @ CSSStyleNode (location: (1:7)-(7:0))
+ │ │ ├── content: "\n @media (max-width: 768px) {\n .nav > ul > li {\n display: block;\n }\n }\n"
+ │ │ ├── rules: []
+ │ │ ├── valid: true
+ │ │ └── parse_error: ""
│ │
│ ├── close_tag:
│ │ └── @ HTMLCloseTagNode (location: (7:0)-(7:8))
diff --git a/test/snapshots/parser/script_style_test/test_0024_script_tag_followed_by_style_tag_with_complex_content_d81018eb4e7c7c6fc05c8b35a8cfd494.txt b/test/snapshots/parser/script_style_test/test_0024_script_tag_followed_by_style_tag_with_complex_content_d81018eb4e7c7c6fc05c8b35a8cfd494.txt
index 58162c8ce..3058d83fd 100644
--- a/test/snapshots/parser/script_style_test/test_0024_script_tag_followed_by_style_tag_with_complex_content_d81018eb4e7c7c6fc05c8b35a8cfd494.txt
+++ b/test/snapshots/parser/script_style_test/test_0024_script_tag_followed_by_style_tag_with_complex_content_d81018eb4e7c7c6fc05c8b35a8cfd494.txt
@@ -38,8 +38,19 @@
│ │
│ ├── tag_name: "style" (location: (6:1)-(6:6))
│ ├── body: (1 item)
- │ │ └── @ LiteralNode (location: (6:7)-(10:0))
- │ │ └── content: "\n .mobile > .header {\n font-size: 14px;\n }\n"
+ │ │ └── @ CSSStyleNode (location: (6:7)-(10:0))
+ │ │ ├── content: "\n .mobile > .header {\n font-size: 14px;\n }\n"
+ │ │ ├── rules: (1 item)
+ │ │ │ └── @ CSSRuleNode (location: (6:7)-(10:0))
+ │ │ │ ├── selector: "SelectorList([Selector(.mobile > .header, specificity = 0x800)])"
+ │ │ │ └── declarations: (1 item)
+ │ │ │ └── @ CSSDeclarationNode (location: (6:7)-(10:0))
+ │ │ │ ├── property: "font-size"
+ │ │ │ └── value: "FontSize(Length(Dimension(Px(14.0))))"
+ │ │ │
+ │ │ │
+ │ │ ├── valid: true
+ │ │ └── parse_error: ""
│ │
│ ├── close_tag:
│ │ └── @ HTMLCloseTagNode (location: (10:0)-(10:8))
diff --git a/test/snapshots/parser/script_style_test/test_0027_style_tag_with_CSS_custom_properties_and_calc_5b9a8e819022c7cd704610880a30afd3.txt b/test/snapshots/parser/script_style_test/test_0027_style_tag_with_CSS_custom_properties_and_calc_5b9a8e819022c7cd704610880a30afd3.txt
index 7aee9ffab..685ff83b0 100644
--- a/test/snapshots/parser/script_style_test/test_0027_style_tag_with_CSS_custom_properties_and_calc_5b9a8e819022c7cd704610880a30afd3.txt
+++ b/test/snapshots/parser/script_style_test/test_0027_style_tag_with_CSS_custom_properties_and_calc_5b9a8e819022c7cd704610880a30afd3.txt
@@ -11,8 +11,31 @@
│ │
│ ├── tag_name: "style" (location: (1:1)-(1:6))
│ ├── body: (1 item)
- │ │ └── @ LiteralNode (location: (1:7)-(9:0))
- │ │ └── content: "\n :root {\n --arrow: \"<\";\n --content: \">\";\n }\n .element {\n width: calc(100% - 20px);\n }\n"
+ │ │ └── @ CSSStyleNode (location: (1:7)-(9:0))
+ │ │ ├── content: "\n :root {\n --arrow: \"<\";\n --content: \">\";\n }\n .element {\n width: calc(100% - 20px);\n }\n"
+ │ │ ├── rules: (2 items)
+ │ │ │ ├── @ CSSRuleNode (location: (1:7)-(9:0))
+ │ │ │ │ ├── selector: "SelectorList([Selector(:root, specificity = 0x400)])"
+ │ │ │ │ └── declarations: (2 items)
+ │ │ │ │ ├── @ CSSDeclarationNode (location: (1:7)-(9:0))
+ │ │ │ │ │ ├── property: "--arrow"
+ │ │ │ │ │ └── value: "Custom(CustomProperty { name: Custom(DashedIdent(\"--arrow\")), value: TokenList([Token(String(\"<\"))]) })"
+ │ │ │ │ │
+ │ │ │ │ └── @ CSSDeclarationNode (location: (1:7)-(9:0))
+ │ │ │ │ ├── property: "--content"
+ │ │ │ │ └── value: "Custom(CustomProperty { name: Custom(DashedIdent(\"--content\")), value: TokenList([Token(String(\">\"))]) })"
+ │ │ │ │
+ │ │ │ │
+ │ │ │ └── @ CSSRuleNode (location: (1:7)-(9:0))
+ │ │ │ ├── selector: "SelectorList([Selector(.element, specificity = 0x400)])"
+ │ │ │ └── declarations: (1 item)
+ │ │ │ └── @ CSSDeclarationNode (location: (1:7)-(9:0))
+ │ │ │ ├── property: "width"
+ │ │ │ └── value: "Width(LengthPercentage(Calc(Function(Calc(Sum(Value(Percentage(Percentage(1.0))), Value(Dimension(Px(-20.0)))))))))"
+ │ │ │
+ │ │ │
+ │ │ ├── valid: true
+ │ │ └── parse_error: ""
│ │
│ ├── close_tag:
│ │ └── @ HTMLCloseTagNode (location: (9:0)-(9:8))
diff --git a/test/snapshots/parser/script_style_test/test_0036_style_tag_with_quotes_containing_HTML-like_content_e270b9ac006614d91dd6f0ac8fa94fa0.txt b/test/snapshots/parser/script_style_test/test_0036_style_tag_with_quotes_containing_HTML-like_content_e270b9ac006614d91dd6f0ac8fa94fa0.txt
index ec5e6efcc..192ddf3bf 100644
--- a/test/snapshots/parser/script_style_test/test_0036_style_tag_with_quotes_containing_HTML-like_content_e270b9ac006614d91dd6f0ac8fa94fa0.txt
+++ b/test/snapshots/parser/script_style_test/test_0036_style_tag_with_quotes_containing_HTML-like_content_e270b9ac006614d91dd6f0ac8fa94fa0.txt
@@ -11,8 +11,19 @@
│ │
│ ├── tag_name: "style" (location: (1:1)-(1:6))
│ ├── body: (1 item)
- │ │ └── @ LiteralNode (location: (1:7)-(1:33))
- │ │ └── content: ".icon::before { content: \""
+ │ │ └── @ CSSStyleNode (location: (1:7)-(1:33))
+ │ │ ├── content: ".icon::before { content: \""
+ │ │ ├── rules: (1 item)
+ │ │ │ └── @ CSSRuleNode (location: (1:7)-(1:33))
+ │ │ │ ├── selector: "SelectorList([Selector(.icon:before, specificity = 0x401)])"
+ │ │ │ └── declarations: (1 item)
+ │ │ │ └── @ CSSDeclarationNode (location: (1:7)-(1:33))
+ │ │ │ ├── property: "content"
+ │ │ │ └── value: "Custom(CustomProperty { name: Unknown(Ident(\"content\")), value: TokenList([Token(String(\"\"))]) })"
+ │ │ │
+ │ │ │
+ │ │ ├── valid: true
+ │ │ └── parse_error: ""
│ │
│ ├── close_tag:
│ │ └── @ HTMLCloseTagNode (location: (1:33)-(1:41))
diff --git a/test/snapshots/parser/script_style_test/test_0045_style_tag_with_ERB_in_CSS_strings_a74d4a4f4488689aceb2070a2901e6d2.txt b/test/snapshots/parser/script_style_test/test_0045_style_tag_with_ERB_in_CSS_strings_a74d4a4f4488689aceb2070a2901e6d2.txt
index d6bbfa970..74fd32efd 100644
--- a/test/snapshots/parser/script_style_test/test_0045_style_tag_with_ERB_in_CSS_strings_a74d4a4f4488689aceb2070a2901e6d2.txt
+++ b/test/snapshots/parser/script_style_test/test_0045_style_tag_with_ERB_in_CSS_strings_a74d4a4f4488689aceb2070a2901e6d2.txt
@@ -11,8 +11,19 @@
│ │
│ ├── tag_name: "style" (location: (1:1)-(1:6))
│ ├── body: (5 items)
- │ │ ├── @ LiteralNode (location: (1:7)-(3:25))
- │ │ │ └── content: "\n .dynamic::before {\n content: \"Generated: "
+ │ │ ├── @ CSSStyleNode (location: (1:7)-(3:25))
+ │ │ │ ├── content: "\n .dynamic::before {\n content: \"Generated: "
+ │ │ │ ├── rules: (1 item)
+ │ │ │ │ └── @ CSSRuleNode (location: (1:7)-(3:25))
+ │ │ │ │ ├── selector: "SelectorList([Selector(.dynamic:before, specificity = 0x401)])"
+ │ │ │ │ └── declarations: (1 item)
+ │ │ │ │ └── @ CSSDeclarationNode (location: (1:7)-(3:25))
+ │ │ │ │ ├── property: "content"
+ │ │ │ │ └── value: "Custom(CustomProperty { name: Unknown(Ident(\"content\")), value: TokenList([Token(String(\"Generated: \"))]) })"
+ │ │ │ │
+ │ │ │ │
+ │ │ │ ├── valid: true
+ │ │ │ └── parse_error: ""
│ │ │
│ │ ├── @ ERBContentNode (location: (3:25)-(3:42))
│ │ │ ├── tag_opening: "<%=" (location: (3:25)-(3:28))
@@ -21,8 +32,11 @@
│ │ │ ├── parsed: true
│ │ │ └── valid: true
│ │ │
- │ │ ├── @ LiteralNode (location: (3:42)-(4:21))
- │ │ │ └── content: "\";\n background: url('"
+ │ │ ├── @ CSSStyleNode (location: (3:42)-(4:21))
+ │ │ │ ├── content: "\";\n background: url('"
+ │ │ │ ├── rules: []
+ │ │ │ ├── valid: false
+ │ │ │ └── parse_error: "CSS parse error: Error { kind: EndOfInput, loc: Some(ErrorLocation { filename: \"\", line: 1, column: 22 }) }"
│ │ │
│ │ ├── @ ERBContentNode (location: (4:21)-(4:39))
│ │ │ ├── tag_opening: "<%=" (location: (4:21)-(4:24))
@@ -31,8 +45,11 @@
│ │ │ ├── parsed: true
│ │ │ └── valid: true
│ │ │
- │ │ └── @ LiteralNode (location: (4:39)-(6:0))
- │ │ └── content: "');\n }\n"
+ │ │ └── @ CSSStyleNode (location: (4:39)-(6:0))
+ │ │ ├── content: "');\n }\n"
+ │ │ ├── rules: []
+ │ │ ├── valid: false
+ │ │ └── parse_error: "CSS parse error: Error { kind: EndOfInput, loc: Some(ErrorLocation { filename: \"\", line: 2, column: 1 }) }"
│ │
│ ├── close_tag:
│ │ └── @ HTMLCloseTagNode (location: (6:0)-(6:8))
diff --git a/test/snapshots/parser/tags_test/test_0034_style_tag_with_nested_div_and_CSS_selectors_78cfabdfd368bd486b01a414bd09cead.txt b/test/snapshots/parser/tags_test/test_0034_style_tag_with_nested_div_and_CSS_selectors_78cfabdfd368bd486b01a414bd09cead.txt
index c2ddc704e..e1807e9b5 100644
--- a/test/snapshots/parser/tags_test/test_0034_style_tag_with_nested_div_and_CSS_selectors_78cfabdfd368bd486b01a414bd09cead.txt
+++ b/test/snapshots/parser/tags_test/test_0034_style_tag_with_nested_div_and_CSS_selectors_78cfabdfd368bd486b01a414bd09cead.txt
@@ -11,8 +11,11 @@
│
├── tag_name: "style" (location: (1:1)-(1:6))
├── body: (1 item)
- │ └── @ LiteralNode (location: (1:7)-(1:40))
- │ └── content: ".class { color: red; }
"
+ │ └── @ CSSStyleNode (location: (1:7)-(1:40))
+ │ ├── content: ".class { color: red; }
"
+ │ ├── rules: []
+ │ ├── valid: false
+ │ └── parse_error: "CSS parse error: Error { kind: SelectorError(EmptySelector), loc: Some(ErrorLocation { filename: \"\", line: 0, column: 1 }) }"
│
├── close_tag:
│ └── @ HTMLCloseTagNode (location: (1:40)-(1:48))
diff --git a/test/snapshots/parser/tags_test/test_0035_style_tag_with_CSS_greater_than_selector_aeb1512ea553ab369a3129d25e6677b6.txt b/test/snapshots/parser/tags_test/test_0035_style_tag_with_CSS_greater_than_selector_aeb1512ea553ab369a3129d25e6677b6.txt
index 9d351cb89..ad3db7aad 100644
--- a/test/snapshots/parser/tags_test/test_0035_style_tag_with_CSS_greater_than_selector_aeb1512ea553ab369a3129d25e6677b6.txt
+++ b/test/snapshots/parser/tags_test/test_0035_style_tag_with_CSS_greater_than_selector_aeb1512ea553ab369a3129d25e6677b6.txt
@@ -11,8 +11,19 @@
│
├── tag_name: "style" (location: (1:1)-(1:6))
├── body: (1 item)
- │ └── @ LiteralNode (location: (1:7)-(1:38))
- │ └── content: ".parent > .child { margin: 0; }"
+ │ └── @ CSSStyleNode (location: (1:7)-(1:38))
+ │ ├── content: ".parent > .child { margin: 0; }"
+ │ ├── rules: (1 item)
+ │ │ └── @ CSSRuleNode (location: (1:7)-(1:38))
+ │ │ ├── selector: "SelectorList([Selector(.parent > .child, specificity = 0x800)])"
+ │ │ └── declarations: (1 item)
+ │ │ └── @ CSSDeclarationNode (location: (1:7)-(1:38))
+ │ │ ├── property: "margin"
+ │ │ └── value: "Margin(Margin { top: LengthPercentage(Dimension(Px(0.0))), right: LengthPercentage(Dimension(Px(0.0))), bottom: LengthPercentage(Dimension(Px(0.0))), left: LengthPercentage(Dimension(Px(0.0))) })"
+ │ │
+ │ │
+ │ ├── valid: true
+ │ └── parse_error: ""
│
├── close_tag:
│ └── @ HTMLCloseTagNode (location: (1:38)-(1:46))
diff --git a/test/snapshots/parser/tags_test/test_0036_style_tag_with_CSS_attribute_selectors_containing_HTML-like_content_6024cebc93b5103ba789a52ba13c2f01.txt b/test/snapshots/parser/tags_test/test_0036_style_tag_with_CSS_attribute_selectors_containing_HTML-like_content_6024cebc93b5103ba789a52ba13c2f01.txt
index 2e48b66e5..b03caf942 100644
--- a/test/snapshots/parser/tags_test/test_0036_style_tag_with_CSS_attribute_selectors_containing_HTML-like_content_6024cebc93b5103ba789a52ba13c2f01.txt
+++ b/test/snapshots/parser/tags_test/test_0036_style_tag_with_CSS_attribute_selectors_containing_HTML-like_content_6024cebc93b5103ba789a52ba13c2f01.txt
@@ -11,8 +11,19 @@
│
├── tag_name: "style" (location: (1:1)-(1:6))
├── body: (1 item)
- │ └── @ LiteralNode (location: (1:7)-(1:57))
- │ └── content: "input[placeholder=\"\"] { color: blue; }"
+ │ └── @ CSSStyleNode (location: (1:7)-(1:57))
+ │ ├── content: "input[placeholder=\"\"] { color: blue; }"
+ │ ├── rules: (1 item)
+ │ │ └── @ CSSRuleNode (location: (1:7)-(1:57))
+ │ │ ├── selector: "SelectorList([Selector(input[placeholder=\"\"], specificity = 0x401)])"
+ │ │ └── declarations: (1 item)
+ │ │ └── @ CSSDeclarationNode (location: (1:7)-(1:57))
+ │ │ ├── property: "color"
+ │ │ └── value: "Color(RGBA(RGBA { red: 0, green: 0, blue: 255, alpha: 255 }))"
+ │ │
+ │ │
+ │ ├── valid: true
+ │ └── parse_error: ""
│
├── close_tag:
│ └── @ HTMLCloseTagNode (location: (1:57)-(1:65))
diff --git a/test/snapshots/parser/tags_test/test_0037_style_tag_with_CSS_content_property_containing_HTML_818730ef059d97de51c35b83d28174da.txt b/test/snapshots/parser/tags_test/test_0037_style_tag_with_CSS_content_property_containing_HTML_818730ef059d97de51c35b83d28174da.txt
index 64be156a9..da1f0f1ba 100644
--- a/test/snapshots/parser/tags_test/test_0037_style_tag_with_CSS_content_property_containing_HTML_818730ef059d97de51c35b83d28174da.txt
+++ b/test/snapshots/parser/tags_test/test_0037_style_tag_with_CSS_content_property_containing_HTML_818730ef059d97de51c35b83d28174da.txt
@@ -11,8 +11,19 @@
│
├── tag_name: "style" (location: (1:1)-(1:6))
├── body: (1 item)
- │ └── @ LiteralNode (location: (1:7)-(1:60))
- │ └── content: ".element::before { content: \"Generated
\"; }"
+ │ └── @ CSSStyleNode (location: (1:7)-(1:60))
+ │ ├── content: ".element::before { content: \"Generated
\"; }"
+ │ ├── rules: (1 item)
+ │ │ └── @ CSSRuleNode (location: (1:7)-(1:60))
+ │ │ ├── selector: "SelectorList([Selector(.element:before, specificity = 0x401)])"
+ │ │ └── declarations: (1 item)
+ │ │ └── @ CSSDeclarationNode (location: (1:7)-(1:60))
+ │ │ ├── property: "content"
+ │ │ └── value: "Custom(CustomProperty { name: Unknown(Ident(\"content\")), value: TokenList([Token(String(\"Generated
\"))]) })"
+ │ │
+ │ │
+ │ ├── valid: true
+ │ └── parse_error: ""
│
├── close_tag:
│ └── @ HTMLCloseTagNode (location: (1:60)-(1:68))
diff --git a/test/snapshots/parser/tags_test/test_0038_style_tag_with_media_queries_and_nested_rules_d3ae213f84137865eff551bb3d76bd9d.txt b/test/snapshots/parser/tags_test/test_0038_style_tag_with_media_queries_and_nested_rules_d3ae213f84137865eff551bb3d76bd9d.txt
index d0bd6586a..423971807 100644
--- a/test/snapshots/parser/tags_test/test_0038_style_tag_with_media_queries_and_nested_rules_d3ae213f84137865eff551bb3d76bd9d.txt
+++ b/test/snapshots/parser/tags_test/test_0038_style_tag_with_media_queries_and_nested_rules_d3ae213f84137865eff551bb3d76bd9d.txt
@@ -11,8 +11,11 @@
│
├── tag_name: "style" (location: (1:1)-(1:6))
├── body: (1 item)
- │ └── @ LiteralNode (location: (1:7)-(1:72))
- │ └── content: "@media (max-width: 768px) { .class > .nested { display: none; } }"
+ │ └── @ CSSStyleNode (location: (1:7)-(1:72))
+ │ ├── content: "@media (max-width: 768px) { .class > .nested { display: none; } }"
+ │ ├── rules: []
+ │ ├── valid: true
+ │ └── parse_error: ""
│
├── close_tag:
│ └── @ HTMLCloseTagNode (location: (1:72)-(1:80))
diff --git a/test/snapshots/parser/tags_test/test_0040_style_tag_with_ERB_interpolation_bba0dcc89d67e474bb211be3f0d2409c.txt b/test/snapshots/parser/tags_test/test_0040_style_tag_with_ERB_interpolation_bba0dcc89d67e474bb211be3f0d2409c.txt
index c8eebaa24..c22666c93 100644
--- a/test/snapshots/parser/tags_test/test_0040_style_tag_with_ERB_interpolation_bba0dcc89d67e474bb211be3f0d2409c.txt
+++ b/test/snapshots/parser/tags_test/test_0040_style_tag_with_ERB_interpolation_bba0dcc89d67e474bb211be3f0d2409c.txt
@@ -11,8 +11,11 @@
│
├── tag_name: "style" (location: (1:1)-(1:6))
├── body: (5 items)
- │ ├── @ LiteralNode (location: (1:7)-(1:13))
- │ │ └── content: ".user-"
+ │ ├── @ CSSStyleNode (location: (1:7)-(1:13))
+ │ │ ├── content: ".user-"
+ │ │ ├── rules: []
+ │ │ ├── valid: false
+ │ │ └── parse_error: "CSS parse error: Error { kind: EndOfInput, loc: Some(ErrorLocation { filename: \"\", line: 0, column: 7 }) }"
│ │
│ ├── @ ERBContentNode (location: (1:13)-(1:27))
│ │ ├── tag_opening: "<%=" (location: (1:13)-(1:16))
@@ -21,8 +24,11 @@
│ │ ├── parsed: true
│ │ └── valid: true
│ │
- │ ├── @ LiteralNode (location: (1:27)-(1:48))
- │ │ └── content: " > .content { color: "
+ │ ├── @ CSSStyleNode (location: (1:27)-(1:48))
+ │ │ ├── content: " > .content { color: "
+ │ │ ├── rules: []
+ │ │ ├── valid: false
+ │ │ └── parse_error: "CSS parse error: Error { kind: SelectorError(EmptySelector), loc: Some(ErrorLocation { filename: \"\", line: 0, column: 2 }) }"
│ │
│ ├── @ ERBContentNode (location: (1:48)-(1:66))
│ │ ├── tag_opening: "<%=" (location: (1:48)-(1:51))
@@ -31,8 +37,11 @@
│ │ ├── parsed: true
│ │ └── valid: true
│ │
- │ └── @ LiteralNode (location: (1:66)-(1:69))
- │ └── content: "; }"
+ │ └── @ CSSStyleNode (location: (1:66)-(1:69))
+ │ ├── content: "; }"
+ │ ├── rules: []
+ │ ├── valid: false
+ │ └── parse_error: "CSS parse error: Error { kind: EndOfInput, loc: Some(ErrorLocation { filename: \"\", line: 0, column: 4 }) }"
│
├── close_tag:
│ └── @ HTMLCloseTagNode (location: (1:69)-(1:77))
diff --git a/test/snapshots/parser/tags_test/test_0046_style_tag_with_complex_CSS_containing_HTML-like_selectors_72f0d1b607c34d65ded310921c9faed9.txt b/test/snapshots/parser/tags_test/test_0046_style_tag_with_complex_CSS_containing_HTML-like_selectors_72f0d1b607c34d65ded310921c9faed9.txt
index 2d3ee73d3..3849a661a 100644
--- a/test/snapshots/parser/tags_test/test_0046_style_tag_with_complex_CSS_containing_HTML-like_selectors_72f0d1b607c34d65ded310921c9faed9.txt
+++ b/test/snapshots/parser/tags_test/test_0046_style_tag_with_complex_CSS_containing_HTML-like_selectors_72f0d1b607c34d65ded310921c9faed9.txt
@@ -14,8 +14,27 @@
│ │
│ ├── tag_name: "style" (location: (1:9)-(1:14))
│ ├── body: (1 item)
- │ │ └── @ LiteralNode (location: (1:15)-(11:8))
- │ │ └── content: "\n /* CSS comment */\n\n .component > .header {\n content: \"\";\n }\n\n .component[data-type=\"\"] {\n background: url(\"data:image/svg+xml,\");\n }\n "
+ │ │ └── @ CSSStyleNode (location: (1:15)-(11:8))
+ │ │ ├── content: "\n /* CSS comment */\n\n .component > .header {\n content: \"\";\n }\n\n .component[data-type=\"\"] {\n background: url(\"data:image/svg+xml,\");\n }\n "
+ │ │ ├── rules: (2 items)
+ │ │ │ ├── @ CSSRuleNode (location: (1:15)-(11:8))
+ │ │ │ │ ├── selector: "SelectorList([Selector(.component > .header, specificity = 0x800)])"
+ │ │ │ │ └── declarations: (1 item)
+ │ │ │ │ └── @ CSSDeclarationNode (location: (1:15)-(11:8))
+ │ │ │ │ ├── property: "content"
+ │ │ │ │ └── value: "Custom(CustomProperty { name: Unknown(Ident(\"content\")), value: TokenList([Token(String(\"\"))]) })"
+ │ │ │ │
+ │ │ │ │
+ │ │ │ └── @ CSSRuleNode (location: (1:15)-(11:8))
+ │ │ │ ├── selector: "SelectorList([Selector(.component[data-type=\"\"], specificity = 0x800)])"
+ │ │ │ └── declarations: (1 item)
+ │ │ │ └── @ CSSDeclarationNode (location: (1:15)-(11:8))
+ │ │ │ ├── property: "background"
+ │ │ │ └── value: "Background([Background { image: Url(Url { url: \"data:image/svg+xml,\", loc: Location { line: 9, column: 25 } }), color: RGBA(RGBA { red: 0, green: 0, blue: 0, alpha: 0 }), position: BackgroundPosition { x: Length(Percentage(Percentage(0.0))), y: Length(Percentage(Percentage(0.0))) }, repeat: BackgroundRepeat { x: Repeat, y: Repeat }, size: Explicit { width: Auto, height: Auto }, attachment: Scroll, origin: PaddingBox, clip: BorderBox }])"
+ │ │ │
+ │ │ │
+ │ │ ├── valid: true
+ │ │ └── parse_error: ""
│ │
│ ├── close_tag:
│ │ └── @ HTMLCloseTagNode (location: (11:8)-(11:16))
diff --git a/wasm/Makefile b/wasm/Makefile
index c3720907c..acb3bbdb4 100644
--- a/wasm/Makefile
+++ b/wasm/Makefile
@@ -14,6 +14,9 @@ PRISM_MAIN_SOURCES = $(wildcard $(PRISM_PATH)/src/*.c)
PRISM_UTIL_SOURCES = $(wildcard $(PRISM_PATH)/src/util/*.c)
ALL_C_SOURCES = $(C_SOURCES) $(PRISM_MAIN_SOURCES) $(PRISM_UTIL_SOURCES)
+CSS_PARSER_DIR = ../src/css
+CSS_PARSER_LIB = $(CSS_PARSER_DIR)/target/wasm32-unknown-emscripten/release/libherb_css_parser.a
+
OBJ_DIR = obj
C_OBJECTS = $(patsubst ../src/%.c,$(OBJ_DIR)/herb/%.o,$(C_SOURCES))
CPP_OBJECTS = $(patsubst %.cpp,$(OBJ_DIR)/%.o,$(CPP_SOURCES))
@@ -27,7 +30,9 @@ PRISM_INCLUDE = $(PRISM_PATH)/include
PRISM_SRC = $(PRISM_PATH)/src
PRISM_UTIL = $(PRISM_PATH)/src/util
-CFLAGS = -I$(INCLUDE_DIR) -I$(PRISM_INCLUDE) -I$(PRISM_SRC) -I$(PRISM_UTIL) -DPRISM_STATIC=1 -DPRISM_EXPORT_SYMBOLS=static
+CSS_PARSER_INCLUDE = $(CSS_PARSER_DIR)
+
+CFLAGS = -I$(INCLUDE_DIR) -I$(PRISM_INCLUDE) -I$(PRISM_SRC) -I$(PRISM_UTIL) -I$(CSS_PARSER_INCLUDE) -DPRISM_STATIC=1 -DPRISM_EXPORT_SYMBOLS=static
WASM_FLAGS = -s WASM=1 \
-s SINGLE_FILE=1 \
-s EXPORT_ES6=1 \
@@ -37,7 +42,11 @@ WASM_FLAGS = -s WASM=1 \
-s ERROR_ON_UNDEFINED_SYMBOLS=0 \
--bind
-all: $(BROWSER_WASM_OUTPUT) $(NODE_WASM_OUTPUT)
+all: css_parser $(BROWSER_WASM_OUTPUT) $(NODE_WASM_OUTPUT)
+
+.PHONY: css_parser
+css_parser:
+ cd $(CSS_PARSER_DIR) && cargo build --release --target wasm32-unknown-emscripten
$(OBJ_DIR):
mkdir -p $(OBJ_DIR)/herb
@@ -59,13 +68,13 @@ $(OBJ_DIR)/prism/util/%.o: $(PRISM_PATH)/src/util/%.c | $(OBJ_DIR)
@mkdir -p $(@D)
emcc -c $< -o $@ $(CFLAGS)
-$(BROWSER_WASM_OUTPUT): $(ALL_OBJECTS)
+$(BROWSER_WASM_OUTPUT): $(ALL_OBJECTS) $(CSS_PARSER_LIB)
mkdir -p $(BROWSER_BUILD_DIR)
- em++ $(ALL_OBJECTS) $(CFLAGS) $(WASM_FLAGS) -s ENVIRONMENT='web' -o $(BROWSER_WASM_OUTPUT)
+ em++ $(ALL_OBJECTS) $(CSS_PARSER_LIB) $(CFLAGS) $(WASM_FLAGS) -s ENVIRONMENT='web' -o $(BROWSER_WASM_OUTPUT)
-$(NODE_WASM_OUTPUT): $(ALL_OBJECTS)
+$(NODE_WASM_OUTPUT): $(ALL_OBJECTS) $(CSS_PARSER_LIB)
mkdir -p $(NODE_BUILD_DIR)
- em++ $(ALL_OBJECTS) $(CFLAGS) $(WASM_FLAGS) -s ENVIRONMENT='node' -o $(NODE_WASM_OUTPUT)
+ em++ $(ALL_OBJECTS) $(CSS_PARSER_LIB) $(CFLAGS) $(WASM_FLAGS) -s ENVIRONMENT='node' -o $(NODE_WASM_OUTPUT)
clean: clean_objects clean_browser clean_node