diff --git a/pyhtml/__tag_base.py b/pyhtml/__tag_base.py index 9864afa..c66a0c9 100644 --- a/pyhtml/__tag_base.py +++ b/pyhtml/__tag_base.py @@ -121,7 +121,12 @@ def _escape_children(self) -> bool: """ return True - def _render(self, indent: str, options: FullRenderOptions) -> list[str]: + def _render( + self, + indent: str, + options: FullRenderOptions, + skip_indent: bool = False, + ) -> list[str]: """ Renders tag and its children to a list of strings where each string is a single line of output. @@ -132,6 +137,8 @@ def _render(self, indent: str, options: FullRenderOptions) -> list[str]: string to use for indentation options : FullOptions rendering options + skip_indent : bool + whether to skip indentation for this element Returns ------- @@ -148,8 +155,13 @@ def _render(self, indent: str, options: FullRenderOptions) -> list[str]: ) ) + # Indentation to use before opening tag + indent_pre = "" if skip_indent else indent + # Indentation to use before closing tag + indent_post = "" if skip_indent or options.indent is None else indent + # Tag and attributes - opening = f"{indent}<{self._get_tag_name()}" + opening = f"{indent_pre}<{self._get_tag_name()}" # Add pre-content if (pre := self._get_tag_pre_content()) is not None: @@ -177,7 +189,7 @@ def _render(self, indent: str, options: FullRenderOptions) -> list[str]: return [ opening, *children, - f"{indent}{closing}", + f"{indent_post}{closing}", ] else: # Children must have at least one line, since we would have @@ -190,7 +202,12 @@ def _render(self, indent: str, options: FullRenderOptions) -> list[str]: # Add the closing tag onto the end return [ *out[:-1], - out[-1] + options.spacing + closing, + # Only include post indentation if it's on a different line + # to the pre indentation + (indent_post if len(out) > 1 else "") + + out[-1] + + options.spacing + + closing, ] def render(self) -> str: @@ -217,11 +234,17 @@ def __init__( # Self-closing tags don't allow children super().__init__(*options, **attributes) - def _render(self, indent: str, options: FullRenderOptions) -> list[str]: + def _render( + self, + indent: str, + options: FullRenderOptions, + skip_indent: bool = False, + ) -> list[str]: """ Renders tag and its children to a list of strings where each string is a single line of output """ + indent_str = "" if skip_indent else indent attributes = util.filter_attributes( util.dict_union( self._get_default_attributes(self.attributes), @@ -230,18 +253,18 @@ def _render(self, indent: str, options: FullRenderOptions) -> list[str]: ) if len(attributes): return [ - f"{indent}<{self._get_tag_name()} " + f"{indent_str}<{self._get_tag_name()} " f"{util.render_tag_attributes(attributes)}/>" ] else: - return [f"{indent}<{self._get_tag_name()}/>"] + return [f"{indent_str}<{self._get_tag_name()}/>"] @deprecated( "Overload `_get_default_render_options` to return " "`RenderOptions(indent=None, spacing='')` instead" ) -class WhitespaceSensitiveTag(Tag): +class WhitespaceSensitiveTag(Tag): # pragma: no cover """ Whitespace-sensitive tags are tags where whitespace needs to be respected. """ diff --git a/pyhtml/__tags/comment.py b/pyhtml/__tags/comment.py index fbf8205..20fa460 100644 --- a/pyhtml/__tags/comment.py +++ b/pyhtml/__tags/comment.py @@ -45,7 +45,12 @@ def _get_tag_name(self) -> str: # and is never used since we override _render return "!--" # pragma: no cover - def _render(self, indent: str, options: FullRenderOptions) -> list[str]: + def _render( + self, + indent: str, + options: FullRenderOptions, + skip_indent: bool = False, + ) -> list[str]: """ Override of render, to render comments """ diff --git a/pyhtml/__tags/dangerous_raw_html.py b/pyhtml/__tags/dangerous_raw_html.py index 6d60470..0a8c79f 100644 --- a/pyhtml/__tags/dangerous_raw_html.py +++ b/pyhtml/__tags/dangerous_raw_html.py @@ -47,7 +47,12 @@ def _get_tag_name(self) -> str: # and is never used since we override _render return "!!!DANGEROUS RAW HTML!!!" # pragma: no cover - def _render(self, indent: str, options: FullRenderOptions) -> list[str]: + def _render( + self, + indent: str, + options: FullRenderOptions, + skip_indent: bool = False, + ) -> list[str]: return self.html_data.splitlines() def _get_default_render_options(self) -> RenderOptions: diff --git a/pyhtml/__util.py b/pyhtml/__util.py index 5974d5a..bcfe758 100644 --- a/pyhtml/__util.py +++ b/pyhtml/__util.py @@ -101,17 +101,24 @@ def render_inline_element( """ from .__tag_base import Tag + skip_indent = options.spacing is not None and "\n" not in options.spacing + if isinstance(ele, Tag): - return ele._render(indent, options) + return ele._render(indent, options, skip_indent) elif isinstance(ele, type) and issubclass(ele, Tag): e = ele() - return e._render(indent, options) + return e._render(indent, options, skip_indent) else: # Remove newlines from strings when inline rendering if escape_strings: - return increase_indent([escape_string(str(ele))], indent) + return increase_indent( + [escape_string(line) for line in str(ele).splitlines()], + "" if skip_indent else indent, + ) else: - return increase_indent([str(ele)], indent) + return increase_indent( + str(ele).splitlines(), "" if skip_indent else indent + ) def render_children( @@ -135,7 +142,6 @@ def render_children( else: # Custom spacing if len(rendered) == 0: - rendered_child[0] = rendered_child[0].strip() rendered = rendered_child else: *r_head, r_tail = rendered @@ -145,8 +151,8 @@ def render_children( # Join it all nicely rendered = [ *r_head, - # Remove leading whitespace caused by indentation rules - r_tail + options.spacing + c_head.lstrip(), + # Join using spacing as separator + r_tail + options.spacing + c_head, *c_tail, ] return rendered diff --git a/pyproject.toml b/pyproject.toml index 5d2e783..1682f4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pyhtml-enhanced" -version = "2.2.2" +version = "2.2.3" description = "A library for building HTML documents with a simple and learnable syntax" readme = "README.md" license = "MIT" diff --git a/tests/basic_rendering_test.py b/tests/basic_rendering_test.py index 0f59aac..b5b351d 100644 --- a/tests/basic_rendering_test.py +++ b/tests/basic_rendering_test.py @@ -46,7 +46,7 @@ def test_renders_elements_with_children(): ) -def test_renders_deeply_nested_children(): +def test_renders_nested_children(): doc = body( div( span("Hello world"), @@ -66,6 +66,30 @@ def test_renders_deeply_nested_children(): ) +def test_renders_deeply_nested_children(): + doc = body( + div( + span( + div("Hello world"), + ), + ), + ) + + assert str(doc) == "\n".join( + [ + "
", + "Paragraph
" + + +def test_extra_space_is_respected_in_paragraphs(): + doc = p.p(" Paragraph ") + assert str(doc) == "Paragraph
" + + +def test_paragraphs_render_in_body(): + doc = p.body( + p.p("Paragraph"), + ) + assert str(doc) == "\n".join( + [ + "", + "Paragraph
", + "", + ] + ) diff --git a/tests/whitespace_sensitive_test.py b/tests/whitespace_sensitive_test.py index 9eb2951..220478d 100644 --- a/tests/whitespace_sensitive_test.py +++ b/tests/whitespace_sensitive_test.py @@ -21,7 +21,8 @@ def test_textarea(): def test_indentation_ignored(): - assert str(p.body(p.pre("hello\nworld"))) == '\n'.join([ + doc = p.body(p.pre("hello\nworld")) + assert str(doc) == '\n'.join([ "", "hello",
"world",