Recursive AI

Parse and edit CSS

All onehtml recipes

Parse a stylesheet into rules and declarations with source ranges, walk nested @media rules, and change a declaration or add a rule without reformatting the rest of the file.

Stylesheet.Parse implements CSS Syntax Level 3. Malformed CSS never throws: invalid parts become Invalid nodes. Only the hostile-input size limits throw (CssLimitExceededException). Every node keeps its source range, so you can read the original text (PreludeText, ValueText, Raw) as well as the parsed component values. This example uses the same site.css as Compute CSS styles:

CSS
body { font-family: Georgia, serif; color: #222 }
.card h2 { font-size: 1.5rem }
nav ul { display: flex; gap: 1rem }
@media (max-width: 600px) {
  nav ul { display: block }
}
C#
using RecursiveAI.OneHtml.Css;
using RecursiveAI.OneHtml.Editing;
 
string css = File.ReadAllText("site.css");
Stylesheet sheet = Stylesheet.Parse(css, "https://library.example/css/site.css");
 
void Print(CssNode rule, string indent)
{
    if (rule.Kind == CssNodeKind.AtRule)
    {
        Console.WriteLine($"{indent}@{rule.Name} {rule.PreludeText.ToString().Trim()}");
        foreach (CssNode child in rule.Rules)
        {
            Print(child, indent + "  ");
        }
        return;
    }
    Console.WriteLine($"{indent}{rule.PreludeText.ToString().Trim()}");
    foreach (CssNode declaration in rule.Declarations)
    {
        Console.WriteLine($"{indent}  {declaration.Name}: {declaration.ValueText.ToString().Trim()}{(declaration.IsImportant ? " !important" : "")}");
    }
}
 
foreach (CssNode rule in sheet.Rules)
{
    Print(rule, "");
}
 
// Edit the stylesheet the same lossless way as HTML: only the touched declaration changes
CssNode cardHeading = sheet.Rules.First(r => r.PreludeText.ToString().Trim() == ".card h2");
BatchResult result = StylesheetEditing.Apply(sheet, new[]
{
    SheetEdit.SetDeclaration(sheet, cardHeading, "font-size", "1.75rem"),
    SheetEdit.AppendRule(sheet, ".card h2 a { text-decoration: none }"),
});
Console.WriteLine($"Succeeded: {result.Succeeded}");
Console.Write(result.Sheets.Single().NewSheet.ToSourceText());
Text
body
  font-family: Georgia, serif
  color: #222
.card h2
  font-size: 1.5rem
nav ul
  display: flex
  gap: 1rem
@media (max-width: 600px)
  nav ul
    display: block
Succeeded: True
body { font-family: Georgia, serif; color: #222 }
.card h2 { font-size: 1.75rem }
nav ul { display: flex; gap: 1rem }
@media (max-width: 600px) {
  nav ul { display: block }
}
.card h2 a { text-decoration: none }

Notes:

  • sheet.Rules lists the top-level rules. A rule’s Kind is QualifiedRule (a style rule) or AtRule. For at-rules, Name is the at-keyword and AtRuleKind says which one (Media, Supports, Layer, Import and others). Rules and Declarations give the children.
  • ToSourceText() returns the sheet’s text with its original formatting. Serialize() writes the CSSOM serialization instead.
  • StylesheetEditing.Apply edits the way HTML edits do. It splices only the touched declaration or rule, copies the file’s spacing and indentation for new rules, and parses the result again to verify it. The other operations are RemoveDeclaration, InsertRuleBefore, InsertRuleAfter, ReplaceRule and RemoveRule.
  • To edit a <style> element or a linked sheet together with the page, InlineStylesheets.Of(doc) returns the page’s <style> sheets, LinkedStylesheet.Create(url, bytes) wraps a linked one, and new EditBatch(doc, edits, sheetEdits, linkedSheets).Apply() runs HTML and CSS edits as one transaction.
  • Inline style="" attributes are edited with Edit.SetStyle and Edit.RemoveStyle on the element (see Edit HTML without reformatting it).

Parsing details are in CSS syntax, and how stylesheet edits are written and verified is at the end of Editing.