import time from textwrap import dedent import jinja2 import pytest from jinja2.exceptions import TemplateSyntaxError from markupsafe import Markup import jinjax @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_render_simple(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "Greeting.jinja").write_text( """ {#def message #} <div class="greeting [&_a]:flex">{{ message }}</div> """ ) html = catalog.render("Greeting", message="Hello world!") assert html == Markup('<div class="greeting [&_a]:flex">Hello world!</div>') @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_render_source(catalog, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined source = '{#def message #}\n<div class="greeting [&_a]:flex">{{ message }}</div>' expected = Markup('<div class="greeting [&_a]:flex">Hello world!</div>') html = catalog.render("Greeting", message="Hello world!", _source=source) assert expected == html # Legacy html = catalog.render("Greeting", message="Hello world!", __source=source) assert expected == html @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_render_content(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "Card.jinja").write_text(""" <section class="card"> {{ content }} </section> """) content = '<button type="button">Close</button>' expected = Markup(f'<section class="card">\n{content}\n</section>') html = catalog.render("Card", _content=content) print(html) assert expected == html # Legacy html = catalog.render("Card", __content=content) assert expected == html @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) @pytest.mark.parametrize( "source, expected", [ ("<Title>Hi</Title><Title>Hi</Title>", "<h1>Hi</h1><h1>Hi</h1>"), ("<Icon /><Icon />", '<i class="icon"></i><i class="icon"></i>'), ("<Title>Hi</Title><Icon />", '<h1>Hi</h1><i class="icon"></i>'), ("<Icon /><Title>Hi</Title>", '<i class="icon"></i><h1>Hi</h1>'), ], ) def test_render_mix_of_contentful_and_contentless_components( catalog, folder, source, expected, autoescape, undefined, ): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "Icon.jinja").write_text('<i class="icon"></i>') (folder / "Title.jinja").write_text("<h1>{{ content }}</h1>") (folder / "Page.jinja").write_text(source) html = catalog.render("Page") assert html == Markup(expected) @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_composition(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "Greeting.jinja").write_text( """ {#def message #} <div class="greeting [&_a]:flex">{{ message }}</div> """ ) (folder / "CloseBtn.jinja").write_text( """ {#def disabled=False -#} <button type="button"{{ " disabled" if disabled else "" }}>×</button> """ ) (folder / "Card.jinja").write_text( """ <section class="card"> {{ content }} <CloseBtn disabled /> </section> """ ) (folder / "Page.jinja").write_text( """ {#def message #} <Card> <Greeting :message="message" /> <button type="button">Close</button> </Card> """ ) html = catalog.render("Page", message="Hello") print(html) assert ( """ <section class="card"> <div class="greeting [&_a]:flex">Hello</div> <button type="button">Close</button> <button type="button" disabled>×</button> </section> """.strip() in html ) @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_just_properties(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "Lorem.jinja").write_text( """ {#def ipsum=False #} <p>lorem {{ "ipsum" if ipsum else "lorem" }}</p> """ ) (folder / "Layout.jinja").write_text( """ <main> {{ content }} </main> """ ) (folder / "Page.jinja").write_text( """ <Layout> <Lorem ipsum /> <p>meh</p> <Lorem /> </Layout> """ ) html = catalog.render("Page") print(html) assert ( """ <main> <p>lorem ipsum</p> <p>meh</p> <p>lorem lorem</p> </main> """.strip() in html ) @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_global_values(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "Global.jinja").write_text("""{{ globalvar }}""") message = "Hello world!" catalog.jinja_env.globals["globalvar"] = message html = catalog.render("Global") print(html) assert message in html @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_required_attr_are_required(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "Greeting.jinja").write_text( """ {#def message #} <div class="greeting">{{ message }}</div> """ ) with pytest.raises(jinjax.MissingRequiredArgument): catalog.render("Greeting") @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_subfolder(catalog, folder, autoescape, undefined): """Components can be organized in subfolders and called using the dot notation. """ catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined sub = folder / "ui" sub.mkdir() (folder / "Meh.jinja").write_text("<ui.Tab>Meh</ui.Tab>") (sub / "Tab.jinja").write_text('<div class="tab">{{ content }}</div>') html = catalog.render("Meh") assert html == Markup('<div class="tab">Meh</div>') @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_default_attr(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "Greeting.jinja").write_text( """ {#def message="Hello", world=False #} <div>{{ message }}{% if world %} World{% endif %}</div> """ ) (folder / "Page.jinja").write_text( """ <Greeting /> <Greeting message="Hi" /> <Greeting :world="False" /> <Greeting :world="True" /> <Greeting world /> """ ) html = catalog.render("Page", message="Hello") print(html) assert ( """ <div>Hello</div> <div>Hi</div> <div>Hello</div> <div>Hello World</div> <div>Hello World</div> """.strip() in html ) @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_raw_content(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "Code.jinja").write_text(""" <pre class="code"> {{ content|e }} </pre> """) (folder / "Page.jinja").write_text(""" <Code> {% raw -%} {#def message="Hello", world=False #} <Header /> <div>{{ message }}{% if world %} World{% endif %}</div> {%- endraw %} </Code> """) html = catalog.render("Page") print(html) assert ( """ <pre class="code"> {#def message="Hello", world=False #} <Header /> <div>{{ message }}{% if world %} World{% endif %}</div> </pre> """.strip() in html ) @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_multiple_raw(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "C.jinja").write_text(""" <div {{ attrs.render() }}></div> """) (folder / "Page.jinja").write_text(""" <C id="1" /> {% raw -%} <C id="2" /> {%- endraw %} <C id="3" /> {% raw %}<C id="4" />{% endraw %} <C id="5" /> """) html = catalog.render("Page", message="Hello") print(html) assert ( """ <div id="1"></div> <C id="2" /> <div id="3"></div> <C id="4" /> <div id="5"></div> """.strip() in html ) @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_check_for_unclosed(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "Lorem.jinja").write_text(""" {#def ipsum=False #} <p>lorem {{ "ipsum" if ipsum else "lorem" }}</p> """) (folder / "Page.jinja").write_text(""" <main> <Lorem ipsum> </main> """) with pytest.raises(TemplateSyntaxError): try: catalog.render("Page") except TemplateSyntaxError as err: print(err) raise @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_dict_as_attr(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "CitiesList.jinja").write_text(""" {#def cities #} {% for city, country in cities.items() -%} <p>{{ city }}, {{ country }}</p> {%- endfor %} """) (folder / "Page.jinja").write_text(""" <CitiesList :cities="{ 'Lima': 'Peru', 'New York': 'USA', }" /> """) html = catalog.render("Page") assert html == Markup("<p>Lima, Peru</p><p>New York, USA</p>") @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_do_not_mess_with_external_jinja_env(folder_t, folder, autoescape, undefined): """Fix https://github.com/jpsca/jinjax/issues/19 """ (folder_t / "greeting.html").write_text("Jinja still works") (folder / "Greeting.jinja").write_text("JinjaX works") jinja_env = jinja2.Environment( loader=jinja2.FileSystemLoader(folder_t), extensions=["jinja2.ext.i18n"], ) jinja_env.globals = {"glo": "bar"} jinja_env.filters = {"fil": lambda x: x} jinja_env.tests = {"tes": lambda x: x} jinja_env.autoescape = autoescape jinja_env.undefined = undefined catalog = jinjax.Catalog( jinja_env=jinja_env, extensions=["jinja2.ext.debug"], globals={"xglo": "foo"}, filters={"xfil": lambda x: x}, tests={"xtes": lambda x: x}, ) catalog.add_folder(folder) html = catalog.render("Greeting") assert html == Markup("JinjaX works") assert catalog.jinja_env.globals["catalog"] == catalog assert catalog.jinja_env.globals["glo"] == "bar" assert catalog.jinja_env.globals["xglo"] == "foo" assert catalog.jinja_env.filters["fil"] assert catalog.jinja_env.filters["xfil"] assert catalog.jinja_env.tests["tes"] assert catalog.jinja_env.tests["xtes"] assert "jinja2.ext.InternationalizationExtension" in catalog.jinja_env.extensions assert "jinja2.ext.DebugExtension" in catalog.jinja_env.extensions assert "jinja2.ext.ExprStmtExtension" in catalog.jinja_env.extensions tmpl = jinja_env.get_template("greeting.html") assert tmpl.render() == "Jinja still works" assert jinja_env.globals["catalog"] == catalog assert jinja_env.globals["glo"] == "bar" assert "xglo" not in jinja_env.globals assert jinja_env.filters["fil"] assert "xfil" not in jinja_env.filters assert jinja_env.tests["tes"] assert "xtes" not in jinja_env.tests assert "jinja2.ext.InternationalizationExtension" in jinja_env.extensions assert "jinja2.ext.DebugExtension" not in jinja_env.extensions @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_auto_reload(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "Layout.jinja").write_text(""" <html> {{ content }} </html> """) (folder / "Foo.jinja").write_text(""" <Layout> <p>Foo</p> <Bar></Bar> </Layout> """) bar_file = folder / "Bar.jinja" bar_file.write_text("<p>Bar</p>") html1 = catalog.render("Foo") print(bar_file.stat().st_mtime) print(html1, "\n") assert ( """ <html> <p>Foo</p> <p>Bar</p> </html> """.strip() in html1 ) # Give it some time so the st_mtime are different time.sleep(0.1) catalog.auto_reload = False bar_file.write_text("<p>Ignored</p>") print(bar_file.stat().st_mtime) html2 = catalog.render("Foo") print(html2, "\n") catalog.auto_reload = True bar_file.write_text("<p>Updated</p>") print(bar_file.stat().st_mtime) html3 = catalog.render("Foo") print(html3, "\n") assert html1 == html2 assert ( """ <html> <p>Foo</p> <p>Updated</p> </html> """.strip() in html3 ) @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_subcomponents(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined """Issue https://github.com/jpsca/jinjax/issues/32""" (folder / "Page.jinja").write_text(""" {#def message #} <html> <p>lorem ipsum</p> <Subcomponent /> {{ message }} </html> """) (folder / "Subcomponent.jinja").write_text(""" <p>foo bar</p> """) html = catalog.render("Page", message="<3") if autoescape: expected = """ <html> <p>lorem ipsum</p> <p>foo bar</p> <3 </html>""" else: expected = """ <html> <p>lorem ipsum</p> <p>foo bar</p> <3 </html>""" assert html == Markup(expected.strip()) @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_colon_in_attrs(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "C.jinja").write_text(""" <div {{ attrs.render() }}></div> """) (folder / "Page.jinja").write_text(""" <C hx-on:click="show = !show" /> """) html = catalog.render("Page", message="Hello") print(html) assert """<div hx-on:click="show = !show"></div>""" in html @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_template_globals(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "Input.jinja").write_text(""" {# def name, value #}<input type="text" name="{{name}}" value="{{value}}"> """) (folder / "CsrfToken.jinja").write_text(""" <input type="hidden" name="csrft" value="{{csrf_token}}"> """) (folder / "Form.jinja").write_text(""" <form><CsrfToken/>{{content}}</form> """) (folder / "Page.jinja").write_text(""" {# def value #} <Form><Input name="foo" :value="value"/></Form> """) html = catalog.render("Page", value="bar", _globals={"csrf_token": "abc"}) print(html) assert """<input type="hidden" name="csrft" value="abc">""" in html @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_template_globals_update_cache(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "CsrfToken.jinja").write_text( """<input type="hidden" name="csrft" value="{{csrf_token}}">""" ) (folder / "Page.jinja").write_text("""<CsrfToken/>""") html = catalog.render("Page", _globals={"csrf_token": "abc"}) print(html) assert """<input type="hidden" name="csrft" value="abc">""" in html html = catalog.render("Page", _globals={"csrf_token": "xyz"}) print(html) assert """<input type="hidden" name="csrft" value="xyz">""" in html @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_alpine_sintax(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "Greeting.jinja").write_text(""" {#def message #} <button @click="alert('{{ message }}')">Say Hi</button>""") html = catalog.render("Greeting", message="Hello world!") print(html) expected = """<button @click="alert('Hello world!')">Say Hi</button>""" assert html == Markup(expected) @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_alpine_sintax_in_component(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "Button.jinja").write_text( """<button {{ attrs.render() }}>{{ content }}</button>""" ) (folder / "Greeting.jinja").write_text( """<Button @click="alert('Hello world!')">Say Hi</Button>""" ) html = catalog.render("Greeting") print(html) expected = """<button @click="alert('Hello world!')">Say Hi</button>""" assert html == Markup(expected) @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_autoescaped_attrs(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "CheckboxItem.jinja").write_text( """<div {{ attrs.render(class="relative") }}></div>""" ) (folder / "Page.jinja").write_text( """<CheckboxItem class="border border-red-500" />""" ) html = catalog.render("Page") print(html) expected = """<div class="border border-red-500 relative"></div>""" assert html == Markup(expected) @pytest.mark.parametrize( "template", [ pytest.param( dedent( """ {# def href, hx_target="#maincontent", hx_swap="innerHTML show:body:top", hx_push_url=true, #} <a href="{{href}}" hx-get="{{href}}" hx-target="{{hx_target}}" hx-swap="{{hx_swap}}" {% if hx_push_url %}hx-push-url="true"{% endif %}> {{- content -}} </a> """ ), id="no comment", ), pytest.param( dedent( """ {# def href, hx_target="#maincontent", # css selector hx_swap="innerHTML show:body:top", hx_push_url=true, #} <a href="{{href}}" hx-get="{{href}}" hx-target="{{hx_target}}" hx-swap="{{hx_swap}}" {% if hx_push_url %}hx-push-url="true"{% endif %}> {{- content -}} </a> """ ), id="comment with # on line", ), pytest.param( dedent( """ {# def href, # url of the target page hx_target="#maincontent", # css selector hx_swap="innerHTML show:body:top", # browse on top of the page hx_push_url=true, # replace the url of the browser #} <a href="{{href}}" hx-get="{{href}}" hx-target="{{hx_target}}" hx-swap="{{hx_swap}}" {% if hx_push_url %}hx-push-url="true"{% endif %}> {{- content -}} </a> """ ), id="many comments", ), pytest.param( dedent( """ {# def href: str, # url of the target page hx_target: str = "#maincontent", # css selector hx_swap: str = "innerHTML show:body:top", # browse on top of the page hx_push_url: bool = true, # replace the url #} <a href="{{href}}" hx-get="{{href}}" hx-target="{{hx_target}}" hx-swap="{{hx_swap}}" {% if hx_push_url %}hx-push-url="true"{% endif %}> {{- content -}} </a> """ ), id="many comments and typing", ), ], ) @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_strip_comment(catalog, folder, autoescape, template, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "A.jinja").write_text(template) (folder / "Page.jinja").write_text("""<A href="/yolo">Yolo</A>""") html = catalog.render("Page") print(html) expected = """ <a href="/yolo" hx-get="/yolo" hx-target="#maincontent" hx-swap="innerHTML show:body:top" hx-push-url="true">Yolo</a>""".strip() assert html == Markup(expected) def test_vue_like_syntax(catalog, folder): (folder / "Test.jinja").write_text(""" {#def a, b, c, d #} {{ a }} {{ b }} {{ c }} {{ d }} """) (folder / "Caller.jinja").write_text( """<Test :a="2+2" b="2+2" :c="{'lorem': 'ipsum'}" :d="false" />""" ) html = catalog.render("Caller") print(html) expected = """4 2+2 {'lorem': 'ipsum'} False""".strip() assert html == Markup(expected) def test_jinja_like_syntax(catalog, folder): (folder / "Test.jinja").write_text(""" {#def a, b, c, d #} {{ a }} {{ b }} {{ c }} {{ d }} """) (folder / "Caller.jinja").write_text( """<Test a={{ 2+2 }} b="2+2" c={{ {'lorem': 'ipsum'} }} d={{ false }} />""" ) html = catalog.render("Caller") print(html) expected = """4 2+2 {'lorem': 'ipsum'} False""".strip() assert html == Markup(expected) def test_mixed_syntax(catalog, folder): (folder / "Test.jinja").write_text(""" {#def a, b, c, d #} {{ a }} {{ b }} {{ c }} {{ d }} """) (folder / "Caller.jinja").write_text( """<Test :a={{ 2+2 }} b="{{2+2}}" :c={{ {'lorem': 'ipsum'} }} :d={{ false }} />""" ) html = catalog.render("Caller") print(html) expected = """4 {{2+2}} {'lorem': 'ipsum'} False""".strip() assert html == Markup(expected) @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_slots(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "Component.jinja").write_text( """ <p>{{ content }}</p> <p>{{ content("first") }}</p> <p>{{ content("second") }}</p> <p>{{ content("antoher") }}</p> <p>{{ content() }}</p> """.strip() ) (folder / "Messages.jinja").write_text( """ <Component> {% if _slot == "first" %}Hello World {%- elif _slot == "second" %}Lorem Ipsum {%- elif _slot == "meh" %}QWERTYUIOP {%- else %}Default{% endif %} </Component> """.strip() ) html = catalog.render("Messages") print(html) expected = """ <p>Default</p> <p>Hello World</p> <p>Lorem Ipsum</p> <p>Default</p> <p>Default</p> """.strip() assert html == Markup(expected) @pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) def test_kebab_cased_component_names(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape catalog.jinja_env.undefined = undefined (folder / "a_tricky-FOLDER").mkdir() (folder / "kebab-folder").mkdir() (folder / "kebab-cased.jinja").write_text("kebab") (folder / "a_tricky-FOLDER" / "Greeting.jinja").write_text("pascal") (folder / "kebab-folder" / "kebab-cased.jinja").write_text("superkebab") assert catalog.render("KebabCased") == Markup("kebab") assert catalog.render("a_tricky-FOLDER.Greeting") == Markup("pascal") assert catalog.render("KebabFolder.KebabCased") == Markup("superkebab")