Skip to content

Styling

momapy.styling

Classes and functions for styling layout elements using style sheets.

This subpackage provides a CSS-like styling system for layout elements, supporting:

  • Style sheets with selectors (type, class, id, child, descendant, compound)
  • Style collections that group related style properties
  • CSS parsing from files or strings
  • Style application to layout elements

Examples:

from momapy.styling import StyleSheet, apply_style_sheet

stylesheet = StyleSheet.from_file("styles.css")
styled_element = apply_style_sheet(element, stylesheet)

Modules:

Name Description
core

Implementation of the CSS-like styling system.

Classes:

Name Description
ChildSelector

Selector that matches elements that are direct children of a parent.

ClassSelector

Selector that matches elements by class (including subclasses).

CompoundSelector

Selector that matches only if all of its component selectors match (AND logic).

DescendantSelector

Selector that matches elements that are descendants of an ancestor.

IdSelector

Selector that matches elements by their id attribute.

NotSelector

Selector that matches if none of its component selectors match (NOT logic).

OrSelector

Selector that matches if any of its component selectors match (OR logic).

Selector

Abstract base class for CSS-like selectors.

StyleCollection

A dictionary-based collection of style properties.

StyleSheet

A dictionary-based stylesheet mapping selectors to style collections.

TypeSelector

Selector that matches elements by their exact class name.

Functions:

Name Description
apply_style_collection

Apply a StyleCollection to a layout element.

apply_style_sheet

Apply a StyleSheet to a layout element or map layout recursively.

combine_style_sheets

Merge multiple StyleSheets into a single StyleSheet.

get_stylable_attributes

Return the CSS-style names of stylable attributes of a layout element.

ChildSelector dataclass

ChildSelector(parent_selector: Selector, child_selector: Selector)

Bases: Selector

Selector that matches elements that are direct children of a parent.

Attributes:

Name Type Description

Examples:

selector = ChildSelector(TypeSelector("Group"), TypeSelector("Rectangle"))
selector.match(rect, [group])  # True if rect is direct child of group

Parameters:

Name Type Description Default
parent_selector Selector

The parent selector

required
child_selector Selector

The child selector

required

Methods:

Name Description
select

Check if the object is a direct child matching the criteria.

select

select(obj: LayoutElement | Builder, ancestors: Collection[LayoutElement | Builder]) -> bool

Check if the object is a direct child matching the criteria.

Parameters:

Name Type Description Default
obj LayoutElement | Builder

The layout element or builder to test.

required
ancestors Collection[LayoutElement | Builder]

List of ancestor elements.

required

Returns:

Type Description
bool

True if the object matches child_selector and its immediate

bool

parent matches parent_selector.

Source code in src/momapy/styling/core.py
def select(
    self,
    obj: momapy.core.elements.LayoutElement | momapy.builder.Builder,
    ancestors: collections.abc.Collection[
        momapy.core.elements.LayoutElement | momapy.builder.Builder
    ],
) -> bool:
    """Check if the object is a direct child matching the criteria.

    Args:
        obj: The layout element or builder to test.
        ancestors: List of ancestor elements.

    Returns:
        True if the object matches child_selector and its immediate
        parent matches parent_selector.
    """
    if not ancestors:
        return False
    return self.child_selector.select(
        obj, ancestors
    ) and self.parent_selector.select(ancestors[-1], ancestors[:-1])

ClassSelector dataclass

ClassSelector(class_name: str)

Bases: Selector

Selector that matches elements by class (including subclasses).

Attributes:

Name Type Description

Examples:

selector = ClassSelector("Shape")
selector.match(some_rectangle)  # True if Rectangle is a Shape subclass

Parameters:

Name Type Description Default
class_name str

The name of the class

required

Methods:

Name Description
select

Check if the object is an instance of the specified class.

select

select(obj: LayoutElement | Builder, ancestors: Collection[LayoutElement | Builder]) -> bool

Check if the object is an instance of the specified class.

Parameters:

Name Type Description Default
obj LayoutElement | Builder

The layout element or builder to test.

required
ancestors Collection[LayoutElement | Builder]

List of ancestor elements (unused for class matching).

required

Returns:

Type Description
bool

True if the object is an instance of class_name or its subclasses.

Source code in src/momapy/styling/core.py
def select(
    self,
    obj: momapy.core.elements.LayoutElement | momapy.builder.Builder,
    ancestors: collections.abc.Collection[
        momapy.core.elements.LayoutElement | momapy.builder.Builder
    ],
) -> bool:
    """Check if the object is an instance of the specified class.

    Args:
        obj: The layout element or builder to test.
        ancestors: List of ancestor elements (unused for class matching).

    Returns:
        True if the object is an instance of class_name or its subclasses.
    """
    for cls in type(obj).__mro__:
        cls_name = cls.__name__
        if cls_name == self.class_name or cls_name == f"{self.class_name}Builder":
            return True
    return False

CompoundSelector dataclass

CompoundSelector(selectors: tuple[Selector, ...])

Bases: Selector

Selector that matches only if all of its component selectors match (AND logic).

Attributes:

Name Type Description

Examples:

selector = CompoundSelector((TypeSelector("Rectangle"), ClassSelector("Colored")))
selector.match(element)  # True if element is Rectangle AND Colored

Parameters:

Name Type Description Default
selectors tuple[Selector, ...]

The tuple of conjunct selectors

required

Methods:

Name Description
select

Check if all selectors in the tuple match.

select

select(obj: LayoutElement | Builder, ancestors: Collection[LayoutElement | Builder]) -> bool

Check if all selectors in the tuple match.

Parameters:

Name Type Description Default
obj LayoutElement | Builder

The layout element or builder to test.

required
ancestors Collection[LayoutElement | Builder]

List of ancestor elements.

required

Returns:

Type Description
bool

True if all selectors match the object.

Source code in src/momapy/styling/core.py
def select(
    self,
    obj: momapy.core.elements.LayoutElement | momapy.builder.Builder,
    ancestors: collections.abc.Collection[
        momapy.core.elements.LayoutElement | momapy.builder.Builder
    ],
) -> bool:
    """Check if all selectors in the tuple match.

    Args:
        obj: The layout element or builder to test.
        ancestors: List of ancestor elements.

    Returns:
        True if all selectors match the object.
    """
    return all([selector.select(obj, ancestors) for selector in self.selectors])

DescendantSelector dataclass

DescendantSelector(ancestor_selector: Selector, descendant_selector: Selector)

Bases: Selector

Selector that matches elements that are descendants of an ancestor.

Attributes:

Name Type Description

Examples:

selector = DescendantSelector(TypeSelector("Group"), TypeSelector("Text"))
selector.match(text, [subgroup, group])  # True if text is somewhere inside group

Parameters:

Name Type Description Default
ancestor_selector Selector

The ancestor selector

required
descendant_selector Selector

The descendant selector

required

Methods:

Name Description
select

Check if the object is a descendant matching the criteria.

select

select(obj: LayoutElement | Builder, ancestors: Collection[LayoutElement | Builder]) -> bool

Check if the object is a descendant matching the criteria.

Parameters:

Name Type Description Default
obj LayoutElement | Builder

The layout element or builder to test.

required
ancestors Collection[LayoutElement | Builder]

List of ancestor elements.

required

Returns:

Type Description
bool

True if the object matches descendant_selector and any ancestor

bool

matches ancestor_selector.

Source code in src/momapy/styling/core.py
def select(
    self,
    obj: momapy.core.elements.LayoutElement | momapy.builder.Builder,
    ancestors: collections.abc.Collection[
        momapy.core.elements.LayoutElement | momapy.builder.Builder
    ],
) -> bool:
    """Check if the object is a descendant matching the criteria.

    Args:
        obj: The layout element or builder to test.
        ancestors: List of ancestor elements.

    Returns:
        True if the object matches descendant_selector and any ancestor
        matches ancestor_selector.
    """
    if not ancestors:
        return False
    return self.descendant_selector.select(obj, ancestors) and any(
        [
            self.ancestor_selector.select(ancestor, ancestors[:i])
            for i, ancestor in enumerate(ancestors)
        ]
    )

IdSelector dataclass

IdSelector(id_: str)

Bases: Selector

Selector that matches elements by their id attribute.

Attributes:

Name Type Description

Examples:

selector = IdSelector("main_node")
selector.match(element)  # True if element.id_ == "main_node"

Parameters:

Name Type Description Default
id_ str

The id

required

Methods:

Name Description
select

Check if the object has the specified id.

select

select(obj: LayoutElement | Builder, ancestors: Collection[LayoutElement | Builder]) -> bool

Check if the object has the specified id.

Parameters:

Name Type Description Default
obj LayoutElement | Builder

The layout element or builder to test.

required
ancestors Collection[LayoutElement | Builder]

List of ancestor elements (unused for id matching).

required

Returns:

Type Description
bool

True if the object has an id_ attribute matching the selector.

Source code in src/momapy/styling/core.py
def select(
    self,
    obj: momapy.core.elements.LayoutElement | momapy.builder.Builder,
    ancestors: collections.abc.Collection[
        momapy.core.elements.LayoutElement | momapy.builder.Builder
    ],
) -> bool:
    """Check if the object has the specified id.

    Args:
        obj: The layout element or builder to test.
        ancestors: List of ancestor elements (unused for id matching).

    Returns:
        True if the object has an id_ attribute matching the selector.
    """
    return hasattr(obj, "id_") and obj.id_ == self.id_

NotSelector dataclass

NotSelector(selectors: tuple[Selector, ...])

Bases: Selector

Selector that matches if none of its component selectors match (NOT logic).

Attributes:

Name Type Description

Examples:

selector = NotSelector((TypeSelector("Hidden"),))
selector.match(element)  # True if element is NOT of type Hidden

Parameters:

Name Type Description Default
selectors tuple[Selector, ...]

The tuple of negated conjunct selectors

required

Methods:

Name Description
select

Check if none of the selectors in the tuple match.

select

select(obj: LayoutElement | Builder, ancestors: Collection[LayoutElement | Builder]) -> bool

Check if none of the selectors in the tuple match.

Parameters:

Name Type Description Default
obj LayoutElement | Builder

The layout element or builder to test.

required
ancestors Collection[LayoutElement | Builder]

List of ancestor elements.

required

Returns:

Type Description
bool

True if no selector matches the object.

Source code in src/momapy/styling/core.py
def select(
    self,
    obj: momapy.core.elements.LayoutElement | momapy.builder.Builder,
    ancestors: collections.abc.Collection[
        momapy.core.elements.LayoutElement | momapy.builder.Builder
    ],
) -> bool:
    """Check if none of the selectors in the tuple match.

    Args:
        obj: The layout element or builder to test.
        ancestors: List of ancestor elements.

    Returns:
        True if no selector matches the object.
    """
    return not any([selector.select(obj, ancestors) for selector in self.selectors])

OrSelector dataclass

OrSelector(selectors: tuple[Selector, ...])

Bases: Selector

Selector that matches if any of its component selectors match (OR logic).

Attributes:

Name Type Description

Examples:

selector = OrSelector((TypeSelector("Rectangle"), TypeSelector("Circle")))
selector.match(some_shape)  # True if shape is Rectangle OR Circle

Parameters:

Name Type Description Default
selectors tuple[Selector, ...]

The tuple of disjunct selectors

required

Methods:

Name Description
select

Check if any selector in the tuple matches.

select

select(obj: LayoutElement | Builder, ancestors: Collection[LayoutElement | Builder]) -> bool

Check if any selector in the tuple matches.

Parameters:

Name Type Description Default
obj LayoutElement | Builder

The layout element or builder to test.

required
ancestors Collection[LayoutElement | Builder]

List of ancestor elements.

required

Returns:

Type Description
bool

True if any selector matches the object.

Source code in src/momapy/styling/core.py
def select(
    self,
    obj: momapy.core.elements.LayoutElement | momapy.builder.Builder,
    ancestors: collections.abc.Collection[
        momapy.core.elements.LayoutElement | momapy.builder.Builder
    ],
) -> bool:
    """Check if any selector in the tuple matches.

    Args:
        obj: The layout element or builder to test.
        ancestors: List of ancestor elements.

    Returns:
        True if any selector matches the object.
    """
    return any([selector.select(obj, ancestors) for selector in self.selectors])

Selector dataclass

Selector()

Bases: object

Abstract base class for CSS-like selectors.

Selectors determine whether a layout element matches specific criteria. All selector types inherit from this class and implement the select() method.

Methods:

Name Description
select

Check if the layout element matches this selector.

select abstractmethod

select(obj: LayoutElement | Builder, ancestors: Collection[LayoutElement | Builder]) -> bool

Check if the layout element matches this selector.

Parameters:

Name Type Description Default
obj LayoutElement | Builder

The layout element or builder to test.

required
ancestors Collection[LayoutElement | Builder]

List of ancestor elements.

required

Returns:

Type Description
bool

True if the element matches, False otherwise.

Source code in src/momapy/styling/core.py
@abc.abstractmethod
def select(
    self,
    obj: momapy.core.elements.LayoutElement | momapy.builder.Builder,
    ancestors: collections.abc.Collection[
        momapy.core.elements.LayoutElement | momapy.builder.Builder
    ],
) -> bool:
    """Check if the layout element matches this selector.

    Args:
        obj: The layout element or builder to test.
        ancestors: List of ancestor elements.

    Returns:
        True if the element matches, False otherwise.
    """
    pass

StyleCollection

Bases: dict

A dictionary-based collection of style properties.

StyleCollection maps attribute names to their values, similar to a CSS declaration block. It is used within StyleSheets to store styles for specific selectors.

Examples:

styles = StyleCollection({"fill": "red", "stroke_width": 2})
styles["fill"]

StyleSheet

Bases: dict

A dictionary-based stylesheet mapping selectors to style collections.

StyleSheet extends dict to map Selector objects to StyleCollection objects. Supports merging stylesheets using the | operator.

Examples:

from momapy.styling import StyleSheet, TypeSelector
ss = StyleSheet({TypeSelector("Rectangle"): StyleCollection({"fill": "blue"})})

# Merge stylesheets
combined = ss | another_stylesheet

Methods:

Name Description
from_file

Parse and return a StyleSheet from a CSS file.

from_files

Parse and combine multiple CSS files into a single StyleSheet.

from_string

Parse and return a StyleSheet from a CSS string.

from_file classmethod

from_file(file_path: str | PathLike) -> StyleSheet

Parse and return a StyleSheet from a CSS file.

Relative @import paths inside the CSS file are resolved against the directory of file_path, not against the process working directory.

Parameters:

Name Type Description Default
file_path str | PathLike

Path to the CSS file to parse.

required

Returns:

Type Description
StyleSheet

A StyleSheet containing the parsed selectors and style collections.

Raises:

Type Description
ParseException

If the CSS file is malformed.

Source code in src/momapy/styling/core.py
@classmethod
def from_file(cls, file_path: str | os.PathLike) -> "StyleSheet":
    """Parse and return a StyleSheet from a CSS file.

    Relative ``@import`` paths inside the CSS file are resolved against
    the directory of ``file_path``, not against the process working
    directory.

    Args:
        file_path: Path to the CSS file to parse.

    Returns:
        A StyleSheet containing the parsed selectors and style collections.

    Raises:
        pyparsing.ParseException: If the CSS file is malformed.
    """
    path = pathlib.Path(file_path).resolve()
    doc_parser = _make_document_parser(path.parent)
    style_sheet = doc_parser.parse_file(str(path), parse_all=True)[0]
    return style_sheet

from_files classmethod

from_files(file_paths: Collection[str]) -> StyleSheet

Parse and combine multiple CSS files into a single StyleSheet.

Parameters:

Name Type Description Default
file_paths Collection[str]

Collection of paths to CSS files.

required

Returns:

Type Description
StyleSheet

A merged StyleSheet containing all parsed styles.

Source code in src/momapy/styling/core.py
@classmethod
def from_files(cls, file_paths: collections.abc.Collection[str]) -> "StyleSheet":
    """Parse and combine multiple CSS files into a single StyleSheet.

    Args:
        file_paths: Collection of paths to CSS files.

    Returns:
        A merged StyleSheet containing all parsed styles.
    """
    style_sheets = []
    for file_path in file_paths:
        style_sheet = StyleSheet.from_file(file_path)
        style_sheets.append(style_sheet)
    style_sheet = combine_style_sheets(style_sheets)
    return style_sheet

from_string classmethod

from_string(s: str) -> StyleSheet

Parse and return a StyleSheet from a CSS string.

Parameters:

Name Type Description Default
s str

CSS string to parse.

required

Returns:

Type Description
StyleSheet

A StyleSheet containing the parsed selectors and style collections.

Raises:

Type Description
ParseException

If the CSS string is malformed.

Examples:

css = "Rectangle { fill: red; }"
ss = StyleSheet.from_string(css)
Source code in src/momapy/styling/core.py
@classmethod
def from_string(cls, s: str) -> "StyleSheet":
    """Parse and return a StyleSheet from a CSS string.

    Args:
        s: CSS string to parse.

    Returns:
        A StyleSheet containing the parsed selectors and style collections.

    Raises:
        pyparsing.ParseException: If the CSS string is malformed.

    Examples:
        ```python
        css = "Rectangle { fill: red; }"
        ss = StyleSheet.from_string(css)
        ```
    """
    style_sheet = _css_document.parse_string(s, parse_all=True)[0]
    return style_sheet

TypeSelector dataclass

TypeSelector(class_name: str)

Bases: Selector

Selector that matches elements by their exact class name.

Attributes:

Name Type Description

Examples:

selector = TypeSelector("Rectangle")
selector.match(some_rectangle)  # True if type is Rectangle

Parameters:

Name Type Description Default
class_name str

The name of the class

required

Methods:

Name Description
select

Check if the object's class name matches exactly.

select

select(obj: LayoutElement | Builder, ancestors: Collection[LayoutElement | Builder]) -> bool

Check if the object's class name matches exactly.

Parameters:

Name Type Description Default
obj LayoutElement | Builder

The layout element or builder to test.

required
ancestors Collection[LayoutElement | Builder]

List of ancestor elements (unused for type matching).

required

Returns:

Type Description
bool

True if the object's class name or builder name matches.

Source code in src/momapy/styling/core.py
def select(
    self,
    obj: momapy.core.elements.LayoutElement | momapy.builder.Builder,
    ancestors: collections.abc.Collection[
        momapy.core.elements.LayoutElement | momapy.builder.Builder
    ],
) -> bool:
    """Check if the object's class name matches exactly.

    Args:
        obj: The layout element or builder to test.
        ancestors: List of ancestor elements (unused for type matching).

    Returns:
        True if the object's class name or builder name matches.
    """
    obj_cls_name = type(obj).__name__
    return (
        obj_cls_name == self.class_name
        or obj_cls_name == f"{self.class_name}Builder"
    )

apply_style_collection

apply_style_collection(layout_element: LayoutElement | Builder, style_collection: StyleCollection, strict: bool = True) -> LayoutElement | Builder

Apply a StyleCollection to a layout element.

Parameters:

Name Type Description Default
layout_element LayoutElement | Builder

The element or builder to apply styles to.

required
style_collection StyleCollection

The styles to apply.

required
strict bool

If True, raises AttributeError for invalid attributes. If False, silently ignores them.

True

Returns:

Type Description
LayoutElement | Builder

The modified layout element or builder.

Raises:

Type Description
AttributeError

If strict=True and an attribute doesn't exist on the element.

Source code in src/momapy/styling/core.py
def apply_style_collection(
    layout_element: (momapy.core.elements.LayoutElement | momapy.builder.Builder),
    style_collection: StyleCollection,
    strict: bool = True,
) -> momapy.core.elements.LayoutElement | momapy.builder.Builder:
    """Apply a StyleCollection to a layout element.

    Args:
        layout_element: The element or builder to apply styles to.
        style_collection: The styles to apply.
        strict: If True, raises AttributeError for invalid attributes.
            If False, silently ignores them.

    Returns:
        The modified layout element or builder.

    Raises:
        AttributeError: If strict=True and an attribute doesn't exist on the element.
    """
    if not isinstance(layout_element, momapy.builder.Builder):
        layout_element = momapy.builder.builder_from_object(layout_element)
        is_builder = False
    else:
        is_builder = True
    for attribute, value in style_collection.items():
        if hasattr(layout_element, attribute):
            setattr(layout_element, attribute, value)
        else:
            if strict:
                raise AttributeError(
                    f"{type(layout_element)} object has no attribute '{attribute}'"
                )
    if is_builder:
        return layout_element
    return momapy.builder.object_from_builder(layout_element)

apply_style_sheet

apply_style_sheet(map_or_layout_element: Map | LayoutElement | Builder, style_sheet: StyleSheet, strict: bool = True, ancestors: Collection[LayoutElement | Builder] = None) -> Map | LayoutElement | Builder

Apply a StyleSheet to a layout element or map layout recursively.

Parameters:

Name Type Description Default
map_or_layout_element Map | LayoutElement | Builder

The map, element, or builder to style.

required
style_sheet StyleSheet

The stylesheet to apply.

required
strict bool

If True, raises errors for invalid attributes.

True
ancestors Collection[LayoutElement | Builder]

Internal list of ancestor elements for selector matching.

None

Returns:

Type Description
Map | LayoutElement | Builder

The modified map, layout element, or builder.

Source code in src/momapy/styling/core.py
def apply_style_sheet(
    map_or_layout_element: (
        momapy.core.map.Map
        | momapy.core.elements.LayoutElement
        | momapy.builder.Builder
    ),
    style_sheet: StyleSheet,
    strict: bool = True,
    ancestors: collections.abc.Collection[
        momapy.core.elements.LayoutElement | momapy.builder.Builder
    ] = None,
) -> momapy.core.map.Map | momapy.core.elements.LayoutElement | momapy.builder.Builder:
    """Apply a StyleSheet to a layout element or map layout recursively.

    Args:
        map_or_layout_element: The map, element, or builder to style.
        style_sheet: The stylesheet to apply.
        strict: If True, raises errors for invalid attributes.
        ancestors: Internal list of ancestor elements for selector matching.

    Returns:
        The modified map, layout element, or builder.
    """
    if not isinstance(map_or_layout_element, momapy.builder.Builder):
        map_or_layout_element = momapy.builder.builder_from_object(
            map_or_layout_element
        )
        is_builder = False
    else:
        is_builder = True
    if momapy.builder.isinstance_or_builder(map_or_layout_element, momapy.core.map.Map):
        layout_element = map_or_layout_element.layout
    else:
        layout_element = map_or_layout_element
    if style_sheet is not None:
        if ancestors is None:
            ancestors = []
        for selector, style_collection in style_sheet.items():
            if selector.select(layout_element, ancestors):
                apply_style_collection(
                    layout_element=layout_element,
                    style_collection=style_collection,
                    strict=strict,
                )
        ancestors = ancestors + [layout_element]
        for child in layout_element.children():
            apply_style_sheet(
                map_or_layout_element=child,
                style_sheet=style_sheet,
                strict=strict,
                ancestors=ancestors,
            )
    if is_builder:
        return map_or_layout_element
    return momapy.builder.object_from_builder(map_or_layout_element)

combine_style_sheets

combine_style_sheets(style_sheets: Collection[StyleSheet]) -> StyleSheet

Merge multiple StyleSheets into a single StyleSheet.

Parameters:

Name Type Description Default
style_sheets Collection[StyleSheet]

Collection of StyleSheets to merge.

required

Returns:

Type Description
StyleSheet

A combined StyleSheet, or None if the input is empty.

Source code in src/momapy/styling/core.py
def combine_style_sheets(
    style_sheets: collections.abc.Collection[StyleSheet],
) -> StyleSheet:
    """Merge multiple StyleSheets into a single StyleSheet.

    Args:
        style_sheets: Collection of StyleSheets to merge.

    Returns:
        A combined StyleSheet, or None if the input is empty.
    """
    if not style_sheets:
        return None
    output_style_sheet = style_sheets[0]
    for style_sheet in style_sheets[1:]:
        output_style_sheet |= style_sheet
    return output_style_sheet

get_stylable_attributes

get_stylable_attributes(layout_element_or_class: LayoutElement | type, presentation_only: bool = False) -> list[str]

Return the CSS-style names of stylable attributes of a layout element.

Returns the names of all dataclass fields that can be set via a stylesheet (i.e., via apply_style_collection), formatted as CSS property names (with hyphens instead of underscores).

Parameters:

Name Type Description Default
layout_element_or_class LayoutElement | type

A layout element instance or class to inspect.

required
presentation_only bool

If True, only return presentation attributes (fill, stroke, font, transform, etc.). Defaults to False.

False

Returns:

Type Description
list[str]

A sorted list of CSS-style attribute names.

Raises:

Type Description
TypeError

If the argument is not a LayoutElement class or instance.

Source code in src/momapy/styling/core.py
def get_stylable_attributes(
    layout_element_or_class: momapy.core.elements.LayoutElement | type,
    presentation_only: bool = False,
) -> list[str]:
    """Return the CSS-style names of stylable attributes of a layout element.

    Returns the names of all dataclass fields that can be set via a
    stylesheet (i.e., via ``apply_style_collection``), formatted as
    CSS property names (with hyphens instead of underscores).

    Args:
        layout_element_or_class: A layout element instance or class to
            inspect.
        presentation_only: If True, only return presentation attributes
            (fill, stroke, font, transform, etc.). Defaults to False.

    Returns:
        A sorted list of CSS-style attribute names.

    Raises:
        TypeError: If the argument is not a LayoutElement class or
            instance.
    """
    if isinstance(layout_element_or_class, type):
        layout_element_class = layout_element_or_class
    else:
        layout_element_class = type(layout_element_or_class)
    if not (
        dataclasses.is_dataclass(layout_element_class)
        and issubclass(layout_element_class, momapy.core.elements.LayoutElement)
    ):
        raise TypeError(
            f"Expected a LayoutElement class or instance, got {layout_element_class}"
        )
    stylable_attributes = []
    for field in dataclasses.fields(layout_element_class):
        if field.name.startswith("_"):
            continue
        if presentation_only and not _is_presentation_attribute(field.name):
            continue
        css_name = field.name.rstrip("_").replace("_", "-")
        stylable_attributes.append(css_name)
    stylable_attributes.sort()
    return stylable_attributes