Hone logo
Hone
Problems

CSS Selector Parser

This challenge requires you to build a JavaScript function that can parse a CSS selector string into a structured representation. A well-formed parser is a fundamental building block for many web development tools, including style engines, linters, and JavaScript frameworks that manipulate DOM elements.

Problem Description

Your task is to create a JavaScript function parseCSSSelector(selectorString) that takes a string representing a CSS selector and returns a JavaScript object representing its parsed structure. The parser should handle basic CSS selector types, including:

  • Type Selectors: div, p, a
  • Class Selectors: .my-class, .another-class
  • ID Selectors: #unique-id
  • Attribute Selectors: [type="text"], [data-attribute], [href^="https"]
  • Combinators:
    • Descendant Combinator: div p (space)
    • Child Combinator: ul > li
    • Adjacent Sibling Combinator: h1 + p
    • General Sibling Combinator: h2 ~ p
  • Pseudo-classes: :hover, :nth-child(2n+1) (basic support for simple arguments like numbers, keywords, and simple expressions)
  • Pseudo-elements: ::before, ::after (though strictly speaking, pseudo-elements are not part of selectors, they are often handled by parsers)

The output should be a hierarchical structure that accurately reflects the relationships between the different parts of the selector.

Key Requirements:

  • The function must accept a single argument: selectorString (a string).
  • The function must return a JavaScript object.
  • The structure of the returned object should be consistent and clearly represent the selector's components and their relationships.
  • The parser should be robust enough to handle valid CSS selector syntax.
  • Handle whitespace appropriately (leading/trailing whitespace in the overall selector and around combinators).

Expected Behavior:

The parser should break down the selector into its constituent parts and group them logically. For instance, a selector like div.my-class > #main p[data-id] should be parsed into a structure that shows:

  1. A div element.
  2. Which has a class my-class.
  3. This is a parent of another element.
  4. That element has an ID of main.
  5. Inside #main, there's a p element.
  6. Which has an attribute data-id.

Edge Cases to Consider:

  • Empty selector string.
  • Selectors with only whitespace.
  • Selectors with multiple combinators in sequence (e.g., div p > span).
  • Selectors with complex attribute selectors (e.g., [data-attribute="value attribute"]).
  • Selectors with multiple classes on the same element (e.g., .class1.class2).
  • Pseudo-classes with arguments that need parsing.

Examples

Example 1:

Input: "div.my-class > #main p[data-id]"
Output:
{
  "type": "selector",
  "combinator": ">",
  "left": {
    "type": "selector",
    "left": {
      "type": "type",
      "name": "div"
    },
    "right": {
      "type": "class",
      "name": "my-class"
    }
  },
  "right": {
    "type": "selector",
    "combinator": " ", // Descendant combinator
    "left": {
      "type": "id",
      "name": "main"
    },
    "right": {
      "type": "selector",
      "left": {
        "type": "type",
        "name": "p"
      },
      "right": {
        "type": "attribute",
        "name": "data-id"
      }
    }
  }
}

Explanation: This output represents the hierarchical structure of the selector. The > combinator indicates a direct child relationship. The descendant combinator (space) shows that the p element is somewhere within the #main element. Each component (type, class, ID, attribute) is clearly identified.

Example 2:

Input: "h1 + p:hover::after"
Output:
{
  "type": "selector",
  "combinator": "+",
  "left": {
    "type": "type",
    "name": "h1"
  },
  "right": {
    "type": "selector",
    "left": {
      "type": "type",
      "name": "p"
    },
    "right": {
      "type": "pseudo-class",
      "name": "hover"
    },
    "pseudoElement": {
      "type": "pseudo-element",
      "name": "after"
    }
  }
}

Explanation: This shows an h1 element followed by an adjacent sibling p element. The p element also has a :hover pseudo-class and a ::after pseudo-element.

Example 3:

Input: ".container .item[data-value='important']"
Output:
{
  "type": "selector",
  "combinator": " ",
  "left": {
    "type": "selector",
    "left": {
      "type": "class",
      "name": "container"
    },
    "right": {
      "type": "class",
      "name": "item"
    }
  },
  "right": {
    "type": "selector",
    "left": {
      "type": "class",
      "name": "item"
    },
    "right": {
      "type": "attribute",
      "name": "data-value",
      "operator": "=",
      "value": "important"
    }
  }
}

Explanation: This example demonstrates chaining of selectors and an attribute selector with a specific value and operator. Note how multiple classes are parsed as separate components within a selector block.

Constraints

  • The input selectorString will be a string.
  • The selectorString will not contain invalid CSS syntax that would typically cause a browser to ignore the rule (e.g., unmatched parentheses, illegal characters within identifiers).
  • Assume the input selectors are valid for the scope of this problem, focusing on structural parsing rather than full CSS validation.
  • The parser should aim for reasonable performance, but extreme optimization is not the primary goal for this challenge.

Notes

  • Consider how you will represent the different types of selector components (e.g., type, class, ID, attribute, combinator, pseudo-class, pseudo-element) in your output object.
  • The order of parsing matters. You'll likely want to process combinators to establish the hierarchical relationships.
  • Handling whitespace correctly is crucial for accurate parsing of combinators.
  • For attribute selectors, you'll need to parse the attribute name, optional operator (e.g., =, ^=, $=, *=, ~=, |=), and the attribute value.
  • Pseudo-classes and pseudo-elements can have arguments. For this challenge, focus on simple arguments like keywords or basic expressions.
Loading editor...
javascript