| from __future__ import annotations |
|
|
| from collections.abc import Callable, MutableMapping |
| import dataclasses as dc |
| from typing import Any, Literal |
| import warnings |
|
|
|
|
| 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(slots=True) |
| 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( |
| "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] |
| return token |
|
|