from __future__ import annotations from collections.abc import Callable, MutableMapping import dataclasses as dc from typing import Any, Literal import warnings from markdown_it._compat import DATACLASS_KWARGS def convert_attrs(value: Any) -> Any: """Convert Token.attrs set as ``None`` or ``[[key, value], ...]`` to a dict. This improves compatibility with upstream markdown-it. """ if not value: return {} if isinstance(value, list): return dict(value) return value @dc.dataclass(**DATACLASS_KWARGS) class Token: type: str """Type of the token (string, e.g. "paragraph_open")""" tag: str """HTML tag name, e.g. 'p'""" nesting: Literal[-1, 0, 1] """Level change (number in {-1, 0, 1} set), where: - `1` means the tag is opening - `0` means the tag is self-closing - `-1` means the tag is closing """ attrs: dict[str, str | int | float] = dc.field(default_factory=dict) """HTML attributes. Note this differs from the upstream "list of lists" format, although than an instance can still be initialised with this format. """ map: list[int] | None = None """Source map info. Format: `[ line_begin, line_end ]`""" level: int = 0 """Nesting level, the same as `state.level`""" children: list[Token] | None = None """Array of child nodes (inline and img tokens).""" content: str = "" """Inner content, in the case of a self-closing tag (code, html, fence, etc.),""" markup: str = "" """'*' or '_' for emphasis, fence string for fence, etc.""" info: str = "" """Additional information: - Info string for "fence" tokens - The value "auto" for autolink "link_open" and "link_close" tokens - The string value of the item marker for ordered-list "list_item_open" tokens """ meta: dict[Any, Any] = dc.field(default_factory=dict) """A place for plugins to store any arbitrary data""" block: bool = False """True for block-level tokens, false for inline tokens. Used in renderer to calculate line breaks """ hidden: bool = False """If true, ignore this element when rendering. Used for tight lists to hide paragraphs. """ def __post_init__(self) -> None: self.attrs = convert_attrs(self.attrs) def attrIndex(self, name: str) -> int: warnings.warn( # noqa: B028 "Token.attrIndex should not be used, since Token.attrs is a dictionary", UserWarning, ) if name not in self.attrs: return -1 return list(self.attrs.keys()).index(name) def attrItems(self) -> list[tuple[str, str | int | float]]: """Get (key, value) list of attrs.""" return list(self.attrs.items()) def attrPush(self, attrData: tuple[str, str | int | float]) -> None: """Add `[ name, value ]` attribute to list. Init attrs if necessary.""" name, value = attrData self.attrSet(name, value) def attrSet(self, name: str, value: str | int | float) -> None: """Set `name` attribute to `value`. Override old value if exists.""" self.attrs[name] = value def attrGet(self, name: str) -> None | str | int | float: """Get the value of attribute `name`, or null if it does not exist.""" return self.attrs.get(name, None) def attrJoin(self, name: str, value: str) -> None: """Join value to existing attribute via space. Or create new attribute if not exists. Useful to operate with token classes. """ if name in self.attrs: current = self.attrs[name] if not isinstance(current, str): raise TypeError( f"existing attr 'name' is not a str: {self.attrs[name]}" ) self.attrs[name] = f"{current} {value}" else: self.attrs[name] = value def copy(self, **changes: Any) -> Token: """Return a shallow copy of the instance.""" return dc.replace(self, **changes) def as_dict( self, *, children: bool = True, as_upstream: bool = True, meta_serializer: Callable[[dict[Any, Any]], Any] | None = None, filter: Callable[[str, Any], bool] | None = None, dict_factory: Callable[..., MutableMapping[str, Any]] = dict, ) -> MutableMapping[str, Any]: """Return the token as a dictionary. :param children: Also convert children to dicts :param as_upstream: Ensure the output dictionary is equal to that created by markdown-it For example, attrs are converted to null or lists :param meta_serializer: hook for serializing ``Token.meta`` :param filter: A callable whose return code determines whether an attribute or element is included (``True``) or dropped (``False``). Is called with the (key, value) pair. :param dict_factory: A callable to produce dictionaries from. For example, to produce ordered dictionaries instead of normal Python dictionaries, pass in ``collections.OrderedDict``. """ mapping = dict_factory((f.name, getattr(self, f.name)) for f in dc.fields(self)) if filter: mapping = dict_factory((k, v) for k, v in mapping.items() if filter(k, v)) if as_upstream and "attrs" in mapping: mapping["attrs"] = ( None if not mapping["attrs"] else [[k, v] for k, v in mapping["attrs"].items()] ) if meta_serializer and "meta" in mapping: mapping["meta"] = meta_serializer(mapping["meta"]) if children and mapping.get("children", None): mapping["children"] = [ child.as_dict( children=children, filter=filter, dict_factory=dict_factory, as_upstream=as_upstream, meta_serializer=meta_serializer, ) for child in mapping["children"] ] return mapping @classmethod def from_dict(cls, dct: MutableMapping[str, Any]) -> Token: """Convert a dict to a Token.""" token = cls(**dct) if token.children: token.children = [cls.from_dict(c) for c in token.children] # type: ignore[arg-type] return token