Theme
Version
GitHub PyPI Discord
On this page

HTML attributes

Helpers for Vue-like class/style merging on HTML elements, and the error raised when Python data sets an attribute name that Vue reads as code.

View source

format_attrs function

format_attrs(attrs: Mapping[str, Any]) -> Markup

Format an attribute dict into an HTML attribute string.

  • True renders the bare attribute (disabled); False and None omit the attribute entirely.
  • class and style values may use the structured forms; they are normalized here, so merge_attrs output and hand-built dicts render the same. An empty class or style is omitted.
  • Everything else renders key="value", escaped. A value with __html__ (such as Markup) is escaped too, after decoding its HTML to the text a browser would read, so a " in it cannot end the attribute.

Example::

format_attrs({
    "class": ["btn", {"active": True}],
    "disabled": True,
    "data-id": 3,
})
# -> 'class="btn active" disabled data-id="3"'
View source

merge_attrs function

merge_attrs(*attrs_dicts: Mapping[str, Any] = ()) -> dict[str, Any]

Merge attribute dicts left to right into one dict.

Every key resolves last-one-wins, except class and style: their values from all dicts are collected and combined per normalize_class / normalize_style, so several sources can contribute classes and style properties without overwriting each other.

Key order in the result is the order each key was first seen, so a later override does not move an attribute.

Example::

merge_attrs(
    {"class": "btn", "id": "first"},
    {"class": {"active": True}, "id": "second"},
)
# -> {"class": "btn active", "id": "second"}
View source

normalize_class function

normalize_class(value: ClassValue) -> str

Turn a structured class value into a plain class string.

  • A string is used as-is (stripped).
  • A dict keeps only the keys whose value is truthy.
  • A list may mix strings, dicts, and nested lists. Each item converts to a {class_name: bool} dict (strings split on whitespace, all True) and the dicts merge left to right, so a later falsy entry removes a class added earlier.

Example::

normalize_class(["btn btn-lg", {"active": True, "hidden": False}])
# -> "btn btn-lg active"
View source

normalize_style function

normalize_style(value: StyleValue) -> str

Turn a structured style value into an inline CSS string.

  • A string is used as-is (stripped).
  • A dict renders each entry as property: value;. Property names are used as written (kebab-case); numbers render bare (width: 100).
  • A list may mix strings, dicts, and nested lists. Strings are parsed into property dicts (see parse_string_style) and the dicts merge left to right, so the last value of a property wins.

Two special values steer a merge: None skips the entry (an earlier value for the property stands), and a literal False removes the property entirely, even if set earlier.

Example::

normalize_style([
    "color: red; width: 100px",
    {"color": "green", "width": False},
])
# -> "color: green;"
View source

parse_string_style function

parse_string_style(css_text: str) -> dict[str, Any]

Parse an inline CSS string into a property dict.

Strips /* ... */ comments. A /* inside a quoted value, or right after a backslash, is kept as text. Declarations split on ; except inside parentheses, so url(data:image/png;base64,...) survives. A declaration without a : is dropped.

Example::

parse_string_style("color: red; width: 100px; /* note */")
# -> {"color": "red", "width": "100px"}
View source

VueCodeError class

Bases: TypeError

Raised when Python data sets an attribute name that Vue would read as code.

Vue reads an attribute whose name starts with v-, :, ., ^, @, or # (in any letter case) as a directive, a binding, a listener, or a slot, and evaluates its value as JavaScript. A $c-tr translation binding is code in the same way: Citry writes its values expression into the Vue template. Citry lets only the template author write such names. When one of them comes from Python data instead, the render fails with this error. Python data sets attribute names in three places:

  • a c-bind mapping, such as c-bind="attrs" where attrs holds {"@click": "save()"} or {"$c-tr:greeting": "..."};
  • a c-* attribute whose name, without the c-, starts with one of those prefixes or is a $c-tr binding, such as c-v-if="is_open" or c-$c-tr:greeting="values";
  • an extension's on_attrs_resolved hook that adds such a name, or changes the value of one written in the template.

The check runs while the template renders, on every page. It covers HTML elements and component tags. On <c-slot> it covers a c-bind key and a c-* attribute named like a $c-tr binding, such as c-$c-tr:x, and on <c-fill> it covers a c-bind key such as "v-slot". It runs even on a page that does not start Vue, so the same template never works on one page and fails on another. Citry's own @c-* event bindings and :c-* State bindings are not Vue code, so c-bind may still carry them on an HTML element. <c-slot> and <c-fill> refuse them with RuntimeError.

VueCodeError subclasses TypeError, so code that catches TypeError around a render keeps working.

Example

Write Vue syntax in the template and pass only its data from Python:

<!-- Fails: the mapping sets "@click" -->
<button c-bind="{'@click': 'save()'}">Save</button>

<!-- Works: the listener is written in the template -->
<button @click="save()">Save</button>
Citry version: 0.9.0