import ast from typing import Tuple, Dict, Any, Any import pytest from pyalect import apply_dialects, DialectError from idom import html def eval_html(src, variables=None): tree = apply_dialects(src, "html") if len(tree.body) > 1 or not isinstance(tree.body[0], ast.Expr): raise ValueError(f"Expected a single expression, not {src!r}") code = compile(ast.Expression(tree.body[0].value), "<string>", "eval") return eval(code, {"html": html}, variables) def make_html_dialect_test(*expectations: Tuple[str, Dict[str, Any], Any]): def make_ids(exp): source, variables = exp[:2] source_repr = repr(source) if len(source_repr) > 30: source_repr = source_repr[:30] + "'..." variables_repr = repr(variables) if len(variables_repr) > 30: variables_repr = variables_repr[:30] + "...}" return source_repr + "-" + variables_repr @pytest.mark.parametrize("expect", expectations, ids=make_ids) def test_html_dialect(expect): source, variables, result = expect assert eval_html(source, variables) == result return test_html_dialect test_simple_htm_template = make_html_dialect_test( ('html(f"<div />")', {}, {"tagName": "div"}) ) test_value_children = make_html_dialect_test( ('html(f"<div>foo</div>")', {}, {"tagName": "div", "children": ["foo"]}), ( 'html(f"<div><span/></div>")', {}, {"tagName": "div", "children": [{"tagName": "span"}]}, ), ) test_expression_children = make_html_dialect_test( ( 'html(f"<div>{value}</div>")', {"value": "foo"}, {"tagName": "div", "children": ["foo"]}, ), ( """html(f"<div>{html(f'<span/>')}</div>")""", {}, {"tagName": "div", "children": [{"tagName": "span"}]}, ), ) test_preserve_whitespace_between_text_values = make_html_dialect_test( ( """html(f"<div> a {'b'} c </div>")""", {}, {"tagName": "div", "children": [" a ", "b", " c "]}, ) ) test_collapse_whitespace_lines_in_text = make_html_dialect_test( ( r'html(f"<div> \n a b c \n </div>")', {}, {"tagName": "div", "children": ["a b c"]}, ), ( r"""html(f"<div>a \n {'b'} \n c \n </div>")""", {}, {"tagName": "div", "children": ["a", "b", "c"]}, ), ) test_value_tag = make_html_dialect_test( ('html(f"<div/>")', {}, {"tagName": "div"}), ('html(f"<div />")', {}, {"tagName": "div"}), ("""html(f"<'div' />")""", {}, {"tagName": "div"}), ("""html(f'<"div" />')""", {}, {"tagName": "div"}), ) test_expression_tag = make_html_dialect_test( ('html(f"<{tag} />")', {"tag": "div"}, {"tagName": "div"}) ) test_boolean_prop = make_html_dialect_test( ('html(f"<div foo />")', {}, {"tagName": "div", "attributes": {"foo": True}}), ("""html(f"<div 'foo' />")""", {}, {"tagName": "div", "attributes": {"foo": True}}), ("""html(f'<div "foo" />')""", {}, {"tagName": "div", "attributes": {"foo": True}}), ) test_value_prop_name = make_html_dialect_test( ('html(f"<div foo=1 />")', {}, {"tagName": "div", "attributes": {"foo": "1"}}), ( """html(f'<div "foo"=1 />')""", {}, {"tagName": "div", "attributes": {"foo": "1"}}, ), ( """html(f"<div 'foo'=1 />")""", {}, {"tagName": "div", "attributes": {"foo": "1"}}, ), ( """html(f"<div foo='1' />")""", {}, {"tagName": "div", "attributes": {"foo": "1"}}, ), ( """html(f'<div foo="1" />')""", {}, {"tagName": "div", "attributes": {"foo": "1"}}, ), ) test_expression_prop_value = make_html_dialect_test( ( """html(f"<div foo={a} />")""", {"a": 1.23}, {"tagName": "div", "attributes": {"foo": 1.23}}, ), ( """html(f'<div "foo"={a} />')""", {"a": 1.23}, {"tagName": "div", "attributes": {"foo": 1.23}}, ), ( """html(f"<div 'foo'={a} />")""", {"a": 1.23}, {"tagName": "div", "attributes": {"foo": 1.23}}, ), ( """html(f"<div foo={a:.2} />")""", {"a": 1.23}, {"tagName": "div", "attributes": {"foo": "1.2"}}, ), ) test_concatenated_prop_value = make_html_dialect_test( ( """html(f"<div foo={a}{'2'} />")""", {"a": "1"}, {"tagName": "div", "attributes": {"foo": "12"}}, ), ( """html(f"<div foo=0/{a}/{'2'} />")""", {"a": "1"}, {"tagName": "div", "attributes": {"foo": "0/1/2"}}, ), ) test_slash_in_prop_value = make_html_dialect_test( ( """html(f"<div foo=/bar/quux />")""", {}, {"tagName": "div", "attributes": {"foo": "/bar/quux"}}, ) ) test_spread = make_html_dialect_test( ( """html(f"<div ...{foo} ...{({'bar': 2})} />")""", {"foo": {"foo": 1}}, {"tagName": "div", "attributes": {"foo": 1, "bar": 2}}, ) ) test_comments = make_html_dialect_test( ( '''html( f""" <div> before <!-- multiple lines, {"variables"} and "quotes get ignored --> after </div> """ )''', {}, {"tagName": "div", "children": ["before", "after"]}, ), ( """html(f"<div><!-->slight deviation from HTML comments<--></div>")""", {}, {"tagName": "div"}, ), ) test_component = make_html_dialect_test( ( 'html(f"<{MyComponentWithChildren}>hello<//>")', {"MyComponentWithChildren": lambda children: html.div(children + ["world"])}, {"tagName": "div", "children": ["hello", "world"]}, ), ( 'html(f"<{MyComponentWithAttributes} x=2 y=3 />")', { "MyComponentWithAttributes": lambda x, y: html.div( {"x": int(x) * 2, "y": int(y) * 2} ) }, {"tagName": "div", "attributes": {"x": 4, "y": 6}}, ), ( 'html(f"<{MyComponentWithAttributesAndChildren} x=2 y=3>hello<//>")', { "MyComponentWithAttributesAndChildren": lambda x, y, children: html.div( {"x": int(x) * 2, "y": int(y) * 2}, children + ["world"] ) }, { "tagName": "div", "attributes": {"x": 4, "y": 6}, "children": ["hello", "world"], }, ), ) def test_tag_errors(): with pytest.raises(DialectError, match="no token found"): apply_dialects('html(f"< >")', "html") with pytest.raises(DialectError, match="no token found"): apply_dialects('html(f"<>")', "html") with pytest.raises(DialectError, match="no token found"): apply_dialects("""html(f"<'")""", "html") with pytest.raises(DialectError, match="unexpected end of data"): apply_dialects('html(f"<")', "html") def test_attribute_name_errors(): with pytest.raises(DialectError, match="expression not allowed"): apply_dialects('html(f"<div {1}>")', "html") with pytest.raises(DialectError, match="unexpected end of data"): apply_dialects('html(f"<div ")', "html") with pytest.raises(DialectError, match="no token found"): apply_dialects("""html(f"<div '")""", "html") def test_attribute_value_errors(): with pytest.raises(DialectError, match="invalid character"): apply_dialects("""html(f"<div 'a'x")""", "html") with pytest.raises(DialectError, match="unexpected end of data"): apply_dialects('html(f"<div a{1}")', "html") with pytest.raises(DialectError, match="unexpected end of data"): apply_dialects('html(f"<div a")', "html") with pytest.raises(DialectError, match="unexpected end of data"): apply_dialects('html(f"<div a=")', "html") def test_structural_errors(): with pytest.raises(DialectError, match="all opened tags not closed"): apply_dialects('html(f"<div>")', "html")