1from __future__ import annotations
2
3from collections.abc import Callable, MutableMapping
4import dataclasses as dc
5from typing import Any, Literal
6import warnings
7
8from markdown_it._compat import DATACLASS_KWARGS
9
10
11def convert_attrs(value: Any) -> Any:
12 """Convert Token.attrs set as ``None`` or ``[[key, value], ...]`` to a dict.
13
14 This improves compatibility with upstream markdown-it.
15 """
16 if not value:
17 return {}
18 if isinstance(value, list):
19 return dict(value)
20 return value
21
22
23@dc.dataclass(**DATACLASS_KWARGS)
24class Token:
25 type: str
26 """Type of the token (string, e.g. "paragraph_open")"""
27
28 tag: str
29 """HTML tag name, e.g. 'p'"""
30
31 nesting: Literal[-1, 0, 1]
32 """Level change (number in {-1, 0, 1} set), where:
33 - `1` means the tag is opening
34 - `0` means the tag is self-closing
35 - `-1` means the tag is closing
36 """
37
38 attrs: dict[str, str | int | float] = dc.field(default_factory=dict)
39 """HTML attributes.
40 Note this differs from the upstream "list of lists" format,
41 although than an instance can still be initialised with this format.
42 """
43
44 map: list[int] | None = None
45 """Source map info. Format: `[ line_begin, line_end ]`"""
46
47 level: int = 0
48 """Nesting level, the same as `state.level`"""
49
50 children: list[Token] | None = None
51 """Array of child nodes (inline and img tokens)."""
52
53 content: str = ""
54 """Inner content, in the case of a self-closing tag (code, html, fence, etc.),"""
55
56 markup: str = ""
57 """'*' or '_' for emphasis, fence string for fence, etc."""
58
59 info: str = ""
60 """Additional information:
61 - Info string for "fence" tokens
62 - The value "auto" for autolink "link_open" and "link_close" tokens
63 - The string value of the item marker for ordered-list "list_item_open" tokens
64 """
65
66 meta: dict[Any, Any] = dc.field(default_factory=dict)
67 """A place for plugins to store any arbitrary data"""
68
69 block: bool = False
70 """True for block-level tokens, false for inline tokens.
71 Used in renderer to calculate line breaks
72 """
73
74 hidden: bool = False
75 """If true, ignore this element when rendering.
76 Used for tight lists to hide paragraphs.
77 """
78
79 def __post_init__(self) -> None:
80 self.attrs = convert_attrs(self.attrs)
81
82 def attrIndex(self, name: str) -> int:
83 warnings.warn( # noqa: B028
84 "Token.attrIndex should not be used, since Token.attrs is a dictionary",
85 UserWarning,
86 )
87 if name not in self.attrs:
88 return -1
89 return list(self.attrs.keys()).index(name)
90
91 def attrItems(self) -> list[tuple[str, str | int | float]]:
92 """Get (key, value) list of attrs."""
93 return list(self.attrs.items())
94
95 def attrPush(self, attrData: tuple[str, str | int | float]) -> None:
96 """Add `[ name, value ]` attribute to list. Init attrs if necessary."""
97 name, value = attrData
98 self.attrSet(name, value)
99
100 def attrSet(self, name: str, value: str | int | float) -> None:
101 """Set `name` attribute to `value`. Override old value if exists."""
102 self.attrs[name] = value
103
104 def attrGet(self, name: str) -> None | str | int | float:
105 """Get the value of attribute `name`, or null if it does not exist."""
106 return self.attrs.get(name, None)
107
108 def attrJoin(self, name: str, value: str) -> None:
109 """Join value to existing attribute via space.
110 Or create new attribute if not exists.
111 Useful to operate with token classes.
112 """
113 if name in self.attrs:
114 current = self.attrs[name]
115 if not isinstance(current, str):
116 raise TypeError(
117 f"existing attr 'name' is not a str: {self.attrs[name]}"
118 )
119 self.attrs[name] = f"{current} {value}"
120 else:
121 self.attrs[name] = value
122
123 def copy(self, **changes: Any) -> Token:
124 """Return a shallow copy of the instance."""
125 return dc.replace(self, **changes)
126
127 def as_dict(
128 self,
129 *,
130 children: bool = True,
131 as_upstream: bool = True,
132 meta_serializer: Callable[[dict[Any, Any]], Any] | None = None,
133 filter: Callable[[str, Any], bool] | None = None,
134 dict_factory: Callable[..., MutableMapping[str, Any]] = dict,
135 ) -> MutableMapping[str, Any]:
136 """Return the token as a dictionary.
137
138 :param children: Also convert children to dicts
139 :param as_upstream: Ensure the output dictionary is equal to that created by markdown-it
140 For example, attrs are converted to null or lists
141 :param meta_serializer: hook for serializing ``Token.meta``
142 :param filter: A callable whose return code determines whether an
143 attribute or element is included (``True``) or dropped (``False``).
144 Is called with the (key, value) pair.
145 :param dict_factory: A callable to produce dictionaries from.
146 For example, to produce ordered dictionaries instead of normal Python
147 dictionaries, pass in ``collections.OrderedDict``.
148
149 """
150 mapping = dict_factory((f.name, getattr(self, f.name)) for f in dc.fields(self))
151 if filter:
152 mapping = dict_factory((k, v) for k, v in mapping.items() if filter(k, v))
153 if as_upstream and "attrs" in mapping:
154 mapping["attrs"] = (
155 None
156 if not mapping["attrs"]
157 else [[k, v] for k, v in mapping["attrs"].items()]
158 )
159 if meta_serializer and "meta" in mapping:
160 mapping["meta"] = meta_serializer(mapping["meta"])
161 if children and mapping.get("children", None):
162 mapping["children"] = [
163 child.as_dict(
164 children=children,
165 filter=filter,
166 dict_factory=dict_factory,
167 as_upstream=as_upstream,
168 meta_serializer=meta_serializer,
169 )
170 for child in mapping["children"]
171 ]
172 return mapping
173
174 @classmethod
175 def from_dict(cls, dct: MutableMapping[str, Any]) -> Token:
176 """Convert a dict to a Token."""
177 token = cls(**dct)
178 if token.children:
179 token.children = [cls.from_dict(c) for c in token.children] # type: ignore[arg-type]
180 return token