css-sanitizer 0.1.1

Policy-driven CSS sanitization on top of lightningcss
Documentation
# css-sanitizer

Policy-driven CSS sanitization on top of [lightningcss](https://lightningcss.dev/).

This crate exposes `lightningcss` directly and lets you sanitize rules, selectors,
properties, and descriptors through a custom policy trait. It is an AST policy engine,
not a built-in preset sanitizer.

## Install

```toml
[dependencies]
css-sanitizer = "0.1.1"
```

## Example

```bash
cargo run --example sanitize_strings
```

## Core model

- `CssSanitizationPolicy` is the main extension point.
- `clean_declaration_list_with_policy()` and `clean_stylesheet_with_policy()` parse, sanitize, and serialize strings.
- `sanitize_declaration_block_ast()` and `sanitize_stylesheet_ast()` mutate parsed `lightningcss` ASTs in place.
- `lightningcss` is re-exported so callers can work against the same AST types.

Default trait methods are fail-open. If you want a strict sanitizer, your policy must
explicitly return `NodeAction::Drop` for anything you do not want to keep.

## Quick start

```rust
use css_sanitizer::{
    clean_stylesheet_with_policy, CssSanitizationPolicy, NodeAction, PropertyContext,
    RuleContext,
};
use css_sanitizer::lightningcss::rules::CssRule;

struct StyleColorOnly;

impl CssSanitizationPolicy for StyleColorOnly {
    fn visit_rule(
        &self,
        rule: &mut CssRule<'_>,
        _ctx: RuleContext,
    ) -> NodeAction {
        match rule {
            CssRule::Style(_) => NodeAction::Continue,
            _ => NodeAction::Drop,
        }
    }

    fn visit_property(
        &self,
        property: &mut css_sanitizer::lightningcss::properties::Property<'_>,
        _ctx: PropertyContext,
    ) -> NodeAction {
        if property.property_id().name() == "color" {
            NodeAction::Continue
        } else {
            NodeAction::Drop
        }
    }
}

let safe = clean_stylesheet_with_policy(
    "@import url('evil.css'); .card { color: red; position: fixed }",
    &StyleColorOnly,
);

assert!(!safe.contains("@import"));
assert!(safe.contains("color"));
assert!(!safe.contains("position"));
```

## In-place AST sanitization

```rust
use css_sanitizer::{
    sanitize_stylesheet_ast, CssSanitizationPolicy, NodeAction, RuleContext,
};
use css_sanitizer::lightningcss::rules::CssRule;
use css_sanitizer::lightningcss::stylesheet::{ParserOptions, StyleSheet};

struct NoImports;

impl CssSanitizationPolicy for NoImports {
    fn visit_rule(
        &self,
        rule: &mut CssRule<'_>,
        _ctx: RuleContext,
    ) -> NodeAction {
        if matches!(rule, CssRule::Import(_)) {
            NodeAction::Drop
        } else {
            NodeAction::Continue
        }
    }
}

let mut stylesheet =
    StyleSheet::parse("@import url('evil.css'); .card { color: blue }", ParserOptions::default())
        .expect("stylesheet should parse");

sanitize_stylesheet_ast(&mut stylesheet, &NoImports);

let output = stylesheet
    .to_css(Default::default())
    .expect("stylesheet should serialize")
    .code;

assert!(!output.contains("@import"));
assert!(output.contains(".card"));
```

## What the sanitizer walks

The built-in walker already handles:

- full stylesheet rule lists
- nested style rules
- `@media`, `@supports`, `@container`, `@scope`, `@starting-style`
- `@keyframes`
- `@font-face`
- `@font-palette-values`
- `@font-feature-values` and its sub-rules
- `@page` and page margin rules
- `@counter-style`
- `@viewport`
- selector lists on style-like rules
- normal properties and `!important` declarations
- descriptor-style nodes exposed by `lightningcss`

Empty rules created by filtering are removed during traversal.

## API surface

- `CssSanitizationPolicy`
- `NodeAction`
- `RuleContext`
- `SelectorContext`
- `PropertyContext`
- `DescriptorContext`
- `sanitize_declaration_block_ast()`
- `sanitize_stylesheet_ast()`
- `clean_declaration_list_with_policy()`
- `clean_stylesheet_with_policy()`
- `pub use lightningcss`

## Security notes

- This crate does not ship a safe default policy.
- Selector scoping, `@import`, remote URLs, `!important`, `var()`, and unknown rules are all policy decisions.
- `var(--x)` still cannot be resolved statically across external cascade boundaries unless your own policy or environment model provides that information.

## Publishing

```bash
cargo xtask publish-dry
cargo xtask publish
```

## License

Apache-2.0