181 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			181 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Python
		
	
	
	
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
 |