Skip to content

Commit 58e221c

Browse files
authored
Push to 0.3.3 and update release workflow (#19)
1 parent 1074d30 commit 58e221c

File tree

10 files changed

+261
-110
lines changed

10 files changed

+261
-110
lines changed

.github/workflows/publish.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,3 @@ jobs:
2929

3030
- name: Publish package distributions to PyPI
3131
uses: pypa/gh-action-pypi-publish@release/v1
32-
33-
- name: Publish package distributions to TestPyPI
34-
uses: pypa/gh-action-pypi-publish@release/v1
35-
with:
36-
repository-url: https://test.pypi.org/legacy/

BetterMD/__init__.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .elements import *
1+
from . import elements
22
from .html import CustomHTML
33
from .markdown import CustomMarkdown
44
from .rst import CustomRst
@@ -13,7 +13,7 @@ def read(self) -> str: ...
1313
class HTML:
1414
@staticmethod
1515
def from_string(html:'str'):
16-
return Symbol.from_html(html)
16+
return elements.Symbol.from_html(html)
1717

1818
@staticmethod
1919
def from_file(file: 'Readable'):
@@ -22,7 +22,7 @@ def from_file(file: 'Readable'):
2222
except Exception as e:
2323
raise IOError(f"Error reading HTML file: {e}")
2424

25-
return Symbol.from_html(text)
25+
return elements.Symbol.from_html(text)
2626

2727
@staticmethod
2828
def from_url(url:'str'):
@@ -35,7 +35,7 @@ def from_url(url:'str'):
3535
except Exception as e:
3636
raise IOError(f"Error reading HTML from URL: {e}")
3737

38-
ret = Symbol.from_html(text)
38+
ret = elements.Symbol.from_html(text)
3939

4040
if len(ret) == 1:
4141
return ret[0]
@@ -45,7 +45,7 @@ def from_url(url:'str'):
4545
class MD:
4646
@staticmethod
4747
def from_string(md:'str'):
48-
return Symbol.from_md(md)
48+
return elements.Symbol.from_md(md)
4949

5050
@staticmethod
5151
def from_file(file: 'Readable'):
@@ -54,7 +54,7 @@ def from_file(file: 'Readable'):
5454
except Exception as e:
5555
raise IOError(f"Error reading Markdown file: {e}")
5656

57-
return Symbol.from_md(text)
57+
return elements.Symbol.from_md(text)
5858

5959
@staticmethod
6060
def from_url(url):
@@ -64,7 +64,7 @@ def from_url(url):
6464
except Exception as e:
6565
raise IOError(f"Error reading Markdown from URL: {e}")
6666

67-
return Symbol.from_md(text)
67+
return elements.Symbol.from_md(text)
6868

6969

70-
__all__ = ["HTML", "MD", "Symbol", "Collection", "HTMLParser", "MDParser", "CustomHTML", "CustomMarkdown", "CustomRst", "enable_debug_mode"]
70+
__all__ = ["HTML", "MD", "elements", "Collection", "HTMLParser", "MDParser", "CustomHTML", "CustomMarkdown", "CustomRst", "enable_debug_mode"]

BetterMD/elements/col.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ class Col(Symbol):
1313
md = ""
1414
html = "col"
1515
rst = ""
16-
self_closing = True
16+
type = "void"

BetterMD/elements/document.py

Lines changed: 109 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
if t.TYPE_CHECKING:
44
from .symbol import Symbol
5+
from ..typing import ATTR_TYPES
56

67
T1 = t.TypeVar("T1")
78
T2 = t.TypeVar("T2")
@@ -10,6 +11,40 @@
1011

1112
ARGS = t.ParamSpec("ARGS")
1213

14+
class HashableList(t.Generic[T1]):
15+
def __init__(self, lst:'list[T1]'):
16+
self.lst = lst
17+
18+
def __hash__(self):
19+
# Convert list to tuple for hashing
20+
return hash(tuple(self.lst))
21+
22+
def __eq__(self, other):
23+
if not isinstance(other, HashableList):
24+
return False
25+
return self.lst == other.lst
26+
27+
def __repr__(self):
28+
return f"HashableList({self.lst})"
29+
30+
31+
class HashableDict(t.Generic[T1, T2]):
32+
def __init__(self, dct:'dict[T1, T2]'):
33+
self.dct = dct
34+
35+
def __hash__(self):
36+
# Convert list to tuple for hashing
37+
return hash(tuple(self.dct.items()))
38+
39+
def __eq__(self, other):
40+
if not isinstance(other, HashableDict):
41+
return False
42+
return self.dct == other.dct
43+
44+
def __repr__(self):
45+
return f"HashableList({self.dct})"
46+
47+
1348
class GetProtocol(t.Protocol, t.Generic[T1, T2]):
1449
def get(self, key: 'T1', ) -> 'T2': ...
1550

@@ -20,18 +55,21 @@ def copy(self) -> 'T1': ...
2055
class Copy:
2156
def __init__(self, data):
2257
self.data = data
23-
58+
2459
def copy(self):
2560
return self.data
2661

2762
T5 = t.TypeVar("T5", bound=CopyProtocol)
63+
HASHABLE_ATTRS = str | bool | int | float | HashableList['HASHABLE_ATTRS'] | HashableDict[str, 'HASHABLE_ATTRS']
2864

2965
class Fetcher(t.Generic[T1, T2, T5]):
30-
def __init__(self, data: 'GetProtocol[T1, T2]', default:'T5'=Copy(None)):
66+
def __init__(self, data: 't.Union[GetProtocol[T1, T2], dict[T1, T2]]', default:'T5'=Copy(None)):
3167
self.data = data
3268
self.default = default.copy() if isinstance(default, CopyProtocol) else default
3369

3470
def __getitem__(self, name:'T1') -> 'T2|T5':
71+
if isinstance(self.data, dict):
72+
return self.data.get(name, self.default)
3573
return self.data.get(name, self.default)
3674
class InnerHTML:
3775
def __init__(self, inner):
@@ -40,35 +78,54 @@ def __init__(self, inner):
4078
self.ids: 'dict[str|None, list[Symbol]]' = {}
4179
self.classes: 'dict[str, list[Symbol]]' = {}
4280
self.tags: 'dict[type[Symbol], list[Symbol]]' = {}
81+
self.attrs: 'dict[str, dict[HASHABLE_ATTRS, list[Symbol]]]' = {}
82+
self.text: 'dict[str, list[Symbol]]' = {}
4383

4484
self.children_ids: 'dict[str|None, list[Symbol]]' = {}
4585
self.children_classes: 'dict[str, list[Symbol]]' = {}
4686
self.children_tags: 'dict[type[Symbol], list[Symbol]]' = {}
87+
self.children_attrs: 'dict[str, dict[str, list[Symbol]]]' = {}
88+
self.children_text: 'dict[str, list[Symbol]]' = {}
4789

48-
def add_elm(self, elm:'Symbol'):
49-
"""
50-
Add an element to the children indexes and merge the element's own indexes
51-
recursively into aggregate indexes.
90+
def add_elm(self, elm: 'Symbol'):
91+
def make_hashable(v):
92+
if isinstance(v, list):
93+
return HashableList(v)
94+
elif isinstance(v, dict):
95+
return HashableDict(v)
96+
return v
5297

53-
Args:
54-
elm: Symbol element to add to the indexes.
55-
"""
5698
self.children_ids.setdefault(elm.get_prop("id", None), []).append(elm)
5799
[self.children_classes.setdefault(c, []).append(elm) for c in elm.classes]
58100
self.children_tags.setdefault(type(elm), []).append(elm)
59101

60-
def concat(d1: 'dict[T1|T3, list[T2|T4]]', *d2: 'dict[T3, list[T4]]', **kwargs):
61-
ret = {**kwargs}
102+
# Normalize keys when adding to children_attrs
103+
for prop, value in elm.props.items():
104+
key = make_hashable(value)
105+
self.children_attrs.setdefault(prop, {}).setdefault(key, []).append(elm)
62106

63-
for dict in list(d2) + [d1]:
64-
for k, v in dict.items():
107+
self.children_text.setdefault(elm.text, []).append(elm)
108+
109+
def concat(d1: 'dict', *d2: 'dict'):
110+
ret = {}
111+
112+
for dict_ in list(d2) + [d1]:
113+
for k, v in dict_.items():
65114
ret.setdefault(k, []).extend(v)
66115

67116
return ret
68117

118+
# Normalize keys in elm.props for attrs merging
119+
normalized_props = {
120+
prop: {make_hashable(value): [elm] for value in values}
121+
for prop, values in elm.props.items()
122+
}
123+
69124
self.ids = concat(self.ids, elm.inner_html.ids, {elm.get_prop("id", None): [elm]})
70125
self.classes = concat(self.classes, elm.inner_html.classes, {c: [elm] for c in elm.classes})
71126
self.tags = concat(self.tags, elm.inner_html.tags, {type(elm): [elm]})
127+
self.attrs = concat(self.attrs, elm.inner_html.attrs, normalized_props)
128+
self.text = concat(self.text, elm.inner_html.text, {elm.text: [elm]})
72129

73130
def get_elements_by_id(self, id: 'str'):
74131
return self.ids.get(id, [])
@@ -77,7 +134,45 @@ def get_elements_by_class_name(self, class_name: 'str'):
77134
return self.classes.get(class_name, [])
78135

79136
def get_elements_by_tag_name(self, tag: 'str'):
80-
return self.tags.get(tag, [])
137+
# Find the tag class by name
138+
for tag_class, elements in self.tags.items():
139+
if tag_class.__name__.lower() == tag.lower():
140+
return elements
141+
return []
142+
143+
def find(self, key:'str'):
144+
if key.startswith("#"):
145+
return self.get_elements_by_id(key[1:])
146+
elif key.startswith("."):
147+
return self.get_elements_by_class_name(key[1:])
148+
else:
149+
return self.get_elements_by_tag_name(key)
150+
151+
def get_by_text(self, text:'str'):
152+
return self.text.get(text, [])
153+
154+
def get_by_attr(self, attr:'str', value:'str'):
155+
return self.attrs.get(attr, {}).get(value, [])
156+
157+
def advanced_find(self, tag:'str', attrs:'dict[t.Literal["text"] | str, str | bool | int | float | tuple[str, str | bool | int | float] | list[str | bool | int | float | tuple[str, str | bool | int | float]]]' = {}):
158+
def check_attr(e:'Symbol', k:'str', v:'str | bool | int | float | tuple[str, str | bool | int | float]'):
159+
prop = e.get_prop(k)
160+
if isinstance(prop, list):
161+
return v in prop
162+
163+
if isinstance(prop, dict):
164+
return v in list(prop.items())
165+
166+
return prop == v
167+
168+
tags = self.find(tag)
169+
if "text" in attrs:
170+
text = attrs.pop("text")
171+
tags = filter(lambda e: e.text == text, tags)
172+
173+
for k, v in attrs.items():
174+
tags = filter(lambda e: check_attr(e, k, v) if not isinstance(v, list) else all([check_attr(e, k, i) for i in v]), tags)
175+
return list(tags)
81176

82177
@property
83178
def id(self):

BetterMD/elements/style.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def to_html(self, inner, symbol, parent):
6363

6464

6565
class Style(Symbol):
66-
def __init__(self, styles:'dict[str, ATTR_TYPES]'=None, classes:'list[str]'=None, inner:'list[Symbol]'=None, *, style: t.Optional[StyleDict] = None, raw: str = "",**props):
66+
def __init__(self, *, style: t.Optional[StyleDict] = None, raw: str = "",**props):
6767
"""
6868
Styles with intuitive nested structure
6969
@@ -73,7 +73,7 @@ def __init__(self, styles:'dict[str, ATTR_TYPES]'=None, classes:'list[str]'=None
7373
inner: Child symbols
7474
**props: Additional properties
7575
"""
76-
super().__init__(styles, classes, inner, **props)
76+
super().__init__(**props)
7777
self.style: 'StyleDict' = style or {}
7878
self.raw: 'str' = raw
7979

BetterMD/elements/symbol.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111
import itertools as it
1212

13+
T = t.TypeVar("T", bound=ATTR_TYPES)
14+
T1 = t.TypeVar("T1", bound=t.Union[ATTR_TYPES, t.Any])
15+
1316

1417
set_recursion_limit(10000)
1518

@@ -18,7 +21,6 @@ class Symbol:
1821
prop_list: 'list[str]' = []
1922
md: 't.Union[str, CustomMarkdown]' = ""
2023
rst: 't.Union[str, CustomRst]' = ""
21-
nl:'bool' = False
2224
type: 't.Literal["block", "void", "inline"]' = "inline"
2325

2426
collection = Collection()
@@ -32,27 +34,30 @@ def __init_subclass__(cls, **kwargs) -> None:
3234
cls._cuuid = it.count()
3335
super().__init_subclass__(**kwargs)
3436

35-
def __init__(self, styles:'dict[str,str]'=None, classes:'list[str]'=None, inner:'list[Symbol]'=None, **props:'ATTR_TYPES'):
37+
def __init__(self, inner:'list[Symbol]'=None, **props:'ATTR_TYPES'):
3638
cls = type(self)
3739

3840
self.parent:'Symbol' = None
3941
self.prepared:'bool' = False
4042
self.html_written_props = ""
4143
self.document = InnerHTML(self)
4244

43-
if styles is None:
44-
styles = {}
45-
if classes is None:
46-
classes = []
4745
if inner is None:
4846
inner = []
4947

50-
self.styles: 'dict[str, str]' = styles
51-
self.classes: 'list[str]' = classes
5248
self.children:'List[Symbol]' = List(inner) or List()
5349
self.props: 'dict[str, ATTR_TYPES]' = props
5450
self.nuuid = next(cls._cuuid)
5551

52+
@property
53+
def styles(self):
54+
return self.props.get("style", {})
55+
56+
@property
57+
def classes(self):
58+
return self.props.get("class", [])
59+
60+
5661
@property
5762
def uuid(self):
5863
return f"{type(self).__name__}-{self.nuuid}"
@@ -64,7 +69,7 @@ def text(self) -> 'str':
6469

6570
return "".join([e.text for e in self.children])
6671

67-
def copy(self, styles:'dict[str,str]'=None, classes:'list[str]'=None, inner:'list[Symbol]'=None):
72+
def copy(self, styles:'dict[str,str]'=None):
6873
if inner is None:
6974
inner = []
7075
if styles is None:
@@ -212,10 +217,10 @@ def handle_element(element:'ELEMENT|TEXT'):
212217
attributes = text["attributes"]
213218

214219
# Handle class attribute separately if it exists
215-
classes = []
220+
classes:'list[str]' = []
216221
if "class" in attributes:
217222
classes = attributes["class"].split() if isinstance(attributes["class"], str) else attributes["class"]
218-
del attributes["class"]
223+
attributes["class"] = classes
219224

220225
# Handle style attribute separately if it exists
221226
styles = {}
@@ -225,19 +230,20 @@ def handle_element(element:'ELEMENT|TEXT'):
225230
styles = dict(item.split(":") for item in style_str.split(";") if ":" in item)
226231
elif isinstance(style_str, dict):
227232
styles = style_str
228-
del attributes["style"]
233+
attributes["style"] = styles
229234

230235
inner=[handle_element(elm) for elm in text["children"]]
231236

232237
return cls(
233-
styles=styles,
234-
classes=classes,
235238
inner=inner,
236239
**attributes
237240
)
238241

239-
def get_prop(self, prop, default=""):
240-
return self.props.get(prop, default)
242+
def get_prop(self, prop:'str', default: 'T1'=None) -> 'ATTR_TYPES| T1':
243+
try:
244+
return self.props.get(prop, default) if default is not None else self.props.get(prop)
245+
except Exception as e:
246+
raise e
241247

242248
def set_prop(self, prop, value):
243249
self.props[prop] = value

0 commit comments

Comments
 (0)