Skip to content

Commit d990063

Browse files
Use render options during rendering
1 parent efa006b commit d990063

File tree

7 files changed

+163
-86
lines changed

7 files changed

+163
-86
lines changed

meta/generate_tag_defs.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,10 @@ def generate_tag_class(output: TextIO, tag: TagInfo):
7070

7171
attr_args = "\n".join(attr_args_gen).strip()
7272
attr_unions = "\n".join(attr_unions_gen).strip()
73-
attr_docs_outer = "\n".join(increase_indent(attr_docs_gen, 4)).strip()
74-
attr_docs_inner = "\n".join(increase_indent(attr_docs_gen, 8)).strip()
73+
attr_docs_outer = "\n".join(increase_indent(attr_docs_gen, " ")).strip()
74+
attr_docs_inner = "\n".join(
75+
increase_indent(attr_docs_gen, " ")
76+
).strip()
7577

7678
# Determine whether the class should mandate keyword-only args
7779
# If there are no named attributes, we set it to '' to avoid a syntax error

pyhtml/__render_options.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,17 @@ def default() -> FullOptions:
7070
indent=" ",
7171
spacing="\n",
7272
)
73+
74+
def union(self, other: "Options | FullOptions") -> "Options":
75+
"""
76+
Union this set of options with the other options, returning a new
77+
`Options` object as the result.
78+
79+
Any non-`None` options in `other` will overwrite the original values.
80+
"""
81+
values = asdict(self)
82+
for field in values:
83+
if (other_value := getattr(other, field)) is not None:
84+
values[field] = other_value
85+
86+
return Options(**values)

pyhtml/__tag_base.py

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33
44
Tag base class, including rendering logic
55
"""
6+
67
from typing import Optional, TypeVar
78

89
from . import __util as util
10+
from .__render_options import FullOptions, Options
911
from .__types import AttributeType, ChildrenType
1012

11-
SelfType = TypeVar('SelfType', bound='Tag')
13+
SelfType = TypeVar("SelfType", bound="Tag")
1214

1315

1416
class Tag:
1517
"""
1618
Base tag class
1719
"""
20+
1821
def __init__(
1922
self,
2023
*children: ChildrenType,
@@ -23,26 +26,32 @@ def __init__(
2326
"""
2427
Create a new tag instance
2528
"""
26-
self.children = util.flatten_list(list(children))
29+
flattened, options = util.flatten_children(list(children))
30+
self.children = flattened
2731
"""Children of this tag"""
2832

2933
self.attributes = util.filter_attributes(attributes)
3034
"""Attributes of this tag"""
3135

36+
self.options = options
37+
"""Render options specified for this element"""
38+
3239
def __call__(
3340
self: SelfType,
3441
*children: ChildrenType,
3542
**attributes: AttributeType,
36-
) -> 'SelfType':
43+
) -> "SelfType":
3744
"""
3845
Create a new tag instance derived from this tag. Its children and
3946
attributes are based on this original tag, but with additional children
4047
appended and additional attributes unioned.
4148
"""
42-
new_children = self.children + util.flatten_list(list(children))
49+
flattened, options = util.flatten_children(list(children))
50+
new_children = self.children + flattened
4351
new_attributes = util.dict_union(self.attributes, attributes)
52+
new_options = self.options.union(options)
4453

45-
return self.__class__(*new_children, **new_attributes)
54+
return self.__class__(*new_children, new_options, **new_attributes)
4655

4756
def __iter__(self) -> None:
4857
"""
@@ -59,7 +68,7 @@ def _get_tag_name(self) -> str:
5968
"""
6069
Returns the name of the tag
6170
"""
62-
return type(self).__name__.removesuffix('_')
71+
return type(self).__name__.removesuffix("_")
6372

6473
def _get_default_attributes(
6574
self,
@@ -94,18 +103,35 @@ def _escape_children(self) -> bool:
94103
"""
95104
return True
96105

97-
def _render(self, indent: int) -> list[str]:
106+
def _render(self, indent: str, options: FullOptions) -> list[str]:
98107
"""
99108
Renders tag and its children to a list of strings where each string is
100-
a single line of output
109+
a single line of output.
110+
111+
Parameters
112+
----------
113+
indent : str
114+
string to use for indentation
115+
options : FullOptions
116+
rendering options
117+
118+
Returns
119+
-------
120+
list[str]
121+
list of lines of output
101122
"""
102-
attributes = util.filter_attributes(util.dict_union(
103-
self._get_default_attributes(self.attributes),
104-
self.attributes,
105-
))
123+
# Determine what the options for this element are
124+
options = options.union(self.options)
125+
126+
attributes = util.filter_attributes(
127+
util.dict_union(
128+
self._get_default_attributes(self.attributes),
129+
self.attributes,
130+
)
131+
)
106132

107133
# Tag and attributes
108-
opening = f"{' ' * indent}<{self._get_tag_name()}"
134+
opening = f"{indent}<{self._get_tag_name()}"
109135

110136
# Add pre-content
111137
if (pre := self._get_tag_pre_content()) is not None:
@@ -126,19 +152,20 @@ def _render(self, indent: int) -> list[str]:
126152
util.render_children(
127153
self.children,
128154
self._escape_children(),
129-
indent + 2,
155+
indent + options.indent,
156+
options,
130157
)
131158
)
132159
# Closing tag
133-
out.append(f"{' ' * indent}</{self._get_tag_name()}>")
160+
out.append(f"{indent}</{self._get_tag_name()}>")
134161

135162
return out
136163

137164
def render(self) -> str:
138165
"""
139166
Render this tag and its contents to a string
140167
"""
141-
return '\n'.join(self._render(0))
168+
return "\n".join(self._render("", Options.default()))
142169

143170
def __str__(self) -> str:
144171
return self.render()
@@ -151,32 +178,36 @@ class SelfClosingTag(Tag):
151178
"""
152179
Self-closing tags don't contain child elements
153180
"""
181+
154182
def __init__(self, **attributes: AttributeType) -> None:
155183
# Self-closing tags don't allow children
156184
super().__init__(**attributes)
157185

158-
def _render(self, indent: int) -> list[str]:
186+
def _render(self, indent: str, options: FullOptions) -> list[str]:
159187
"""
160188
Renders tag and its children to a list of strings where each string is
161189
a single line of output
162190
"""
163-
attributes = util.filter_attributes(util.dict_union(
164-
self._get_default_attributes(self.attributes),
165-
self.attributes,
166-
))
191+
attributes = util.filter_attributes(
192+
util.dict_union(
193+
self._get_default_attributes(self.attributes),
194+
self.attributes,
195+
)
196+
)
167197
if len(attributes):
168198
return [
169-
f"{' ' * indent}<{self._get_tag_name()} "
199+
f"{indent}<{self._get_tag_name()} "
170200
f"{util.render_tag_attributes(attributes)}/>"
171201
]
172202
else:
173-
return [f"{' ' * indent}<{self._get_tag_name()}/>"]
203+
return [f"{indent}<{self._get_tag_name()}/>"]
174204

175205

176206
class WhitespaceSensitiveTag(Tag):
177207
"""
178208
Whitespace-sensitive tags are tags where whitespace needs to be respected.
179209
"""
210+
180211
def __init__(
181212
self,
182213
*children: ChildrenType,
@@ -193,25 +224,30 @@ def __call__( # type: ignore
193224
attributes |= {}
194225
return super().__call__(*children, **attributes)
195226

196-
def _render(self, indent: int) -> list[str]:
197-
attributes = util.filter_attributes(util.dict_union(
198-
self._get_default_attributes(self.attributes),
199-
self.attributes,
200-
))
227+
def _render(self, indent: str, options: FullOptions) -> list[str]:
228+
attributes = util.filter_attributes(
229+
util.dict_union(
230+
self._get_default_attributes(self.attributes),
231+
self.attributes,
232+
)
233+
)
201234

202235
# Tag and attributes
203-
output = f"{' ' * indent}<{self._get_tag_name()}"
236+
output = f"{indent}<{self._get_tag_name()}"
204237

205238
if len(attributes):
206239
output += f" {util.render_tag_attributes(attributes)}>"
207240
else:
208241
output += ">"
209242

210-
output += '\n'.join(util.render_children(
211-
self.children,
212-
self._escape_children(),
213-
0,
214-
))
243+
output += "\n".join(
244+
util.render_children(
245+
self.children,
246+
self._escape_children(),
247+
"",
248+
options,
249+
)
250+
)
215251

216252
output += f"</{self._get_tag_name()}>"
217253
return output.splitlines()

pyhtml/__tags/comment.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
44
Definition for the comment tag.
55
"""
6+
67
from .. import __util as util
8+
from ..__render_options import FullOptions
79
from ..__tag_base import Tag
810

911

@@ -19,6 +21,7 @@ class Comment(Tag):
1921
2022
Note that this does not render as a `<comment>` tag
2123
"""
24+
2225
def __init__(self, text: str) -> None:
2326
"""
2427
An HTML comment.
@@ -35,24 +38,24 @@ def __init__(self, text: str) -> None:
3538
super().__init__()
3639

3740
def __call__(self, *args, **kwargs):
38-
raise TypeError('Comment tags are not callable')
41+
raise TypeError("Comment tags are not callable")
3942

4043
def _get_tag_name(self) -> str:
4144
# Ignore coverage since this is only implemented to satisfy inheritance
4245
# and is never used since we override _render
43-
return '!--' # pragma: no cover
46+
return "!--" # pragma: no cover
4447

45-
def _render(self, indent: int) -> list[str]:
48+
def _render(self, indent: str, options: FullOptions) -> list[str]:
4649
"""
4750
Override of render, to render comments
4851
"""
4952

5053
return util.increase_indent(
51-
['<!--']
54+
["<!--"]
5255
+ util.increase_indent(
5356
util.escape_string(self.comment_data).splitlines(),
54-
indent+2,
57+
indent + options.indent,
5558
)
56-
+ ['-->'],
59+
+ ["-->"],
5760
indent,
5861
)

pyhtml/__tags/dangerous_raw_html.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
44
Definition for the DangerousRawHtml tag.
55
"""
6+
7+
from ..__render_options import FullOptions
68
from ..__tag_base import Tag
79
from ..__util import increase_indent
810

@@ -20,6 +22,7 @@ class DangerousRawHtml(Tag):
2022
2123
Do not use this unless absolutely necessary.
2224
"""
25+
2326
def __init__(self, text: str) -> None:
2427
"""
2528
Raw HTML as a string. This is embedded directly within the rendered
@@ -38,12 +41,12 @@ def __init__(self, text: str) -> None:
3841
super().__init__()
3942

4043
def __call__(self, *args, **kwargs):
41-
raise TypeError('DangerousRawHtml tags are not callable')
44+
raise TypeError("DangerousRawHtml tags are not callable")
4245

4346
def _get_tag_name(self) -> str:
4447
# Ignore coverage since this is only implemented to satisfy inheritance
4548
# and is never used since we override _render
46-
return '!!!DANGEROUS RAW HTML!!!' # pragma: no cover
49+
return "!!!DANGEROUS RAW HTML!!!" # pragma: no cover
4750

48-
def _render(self, indent: int) -> list[str]:
51+
def _render(self, indent: str, options: FullOptions) -> list[str]:
4952
return increase_indent(self.html_data.splitlines(), indent)

pyhtml/__types.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
44
Type definitions
55
"""
6+
67
from collections.abc import Generator, Sequence
78
from typing import TYPE_CHECKING, Union
89

910
if TYPE_CHECKING:
11+
from .__render_options import Options
1012
from .__tag_base import Tag
1113

1214

@@ -21,7 +23,7 @@
2123
"""
2224

2325

24-
ChildElementType = Union['Tag', type['Tag'], str]
26+
ChildElementType = Union["Tag", type["Tag"], str]
2527
"""
2628
Objects that are valid as a child element of an HTML element.
2729
@@ -33,14 +35,16 @@
3335
ChildrenType = Union[
3436
ChildElementType,
3537
Sequence[ChildElementType],
36-
'Generator[ChildElementType, None, None]',
3738
# TODO: Would an `Any` type for the generator return be better, even though
3839
# it would be discarded?
40+
"Generator[ChildElementType, None, None]",
41+
"Options",
3942
]
4043
"""
4144
Objects that are valid when passed to a `Tag` for use as children.
4245
4346
* `ChildElementType`: a singular element
4447
* `list[ChildElementType]`: a list of elements
4548
* `GeneratorType[ChildElementType, None, None]`: a generator of elements
49+
* `Options`: PyHTML render options.
4650
"""

0 commit comments

Comments
 (0)