Repository: a-h/templ Branch: main Commit: 009ec8882996 Files: 833 Total size: 3.3 MB Directory structure: gitextract_x0_4cpga/ ├── .dockerignore ├── .envrc ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.md │ ├── copilot-instructions.md │ └── workflows/ │ ├── ci.yml │ ├── docs.yaml │ ├── flakehub-publish-tagged.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── .ignore ├── .version ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── benchmarks/ │ ├── react/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ └── index.jsx │ └── templ/ │ ├── README.md │ ├── data.go │ ├── render_test.go │ ├── template.templ │ └── template_templ.go ├── cfg/ │ └── cfg.go ├── cmd/ │ └── templ/ │ ├── fmtcmd/ │ │ ├── main.go │ │ ├── main_test.go │ │ └── testdata.txtar │ ├── generatecmd/ │ │ ├── cmd.go │ │ ├── eventhandler.go │ │ ├── fatalerror.go │ │ ├── main.go │ │ ├── main_test.go │ │ ├── modcheck/ │ │ │ ├── modcheck.go │ │ │ └── modcheck_test.go │ │ ├── proxy/ │ │ │ ├── proxy.go │ │ │ ├── proxy_test.go │ │ │ └── script.js │ │ ├── run/ │ │ │ ├── run_test.go │ │ │ ├── run_unix.go │ │ │ ├── run_windows.go │ │ │ └── testprogram/ │ │ │ ├── go.mod.embed │ │ │ └── main.go │ │ ├── sse/ │ │ │ └── server.go │ │ ├── symlink/ │ │ │ └── symlink_test.go │ │ ├── test-eventhandler/ │ │ │ ├── eventhandler_test.go │ │ │ ├── multiple_errors.templ.error │ │ │ └── single_error.templ.error │ │ ├── testwatch/ │ │ │ ├── generate_test.go │ │ │ └── testdata/ │ │ │ ├── go.mod.embed │ │ │ ├── go.sum │ │ │ ├── main.go │ │ │ ├── templates.templ │ │ │ └── templates_templ.go │ │ └── watcher/ │ │ ├── watch.go │ │ └── watch_test.go │ ├── infocmd/ │ │ └── main.go │ ├── lspcmd/ │ │ ├── httpdebug/ │ │ │ ├── handler.go │ │ │ ├── list.templ │ │ │ └── list_templ.go │ │ ├── lsp_test.go │ │ ├── lspdiff/ │ │ │ └── lspdiff.go │ │ ├── main.go │ │ ├── pls/ │ │ │ └── main.go │ │ ├── proxy/ │ │ │ ├── client.go │ │ │ ├── diagnosticcache.go │ │ │ ├── documentcontents.go │ │ │ ├── documentcontents_test.go │ │ │ ├── import_test.go │ │ │ ├── rewrite.go │ │ │ ├── server.go │ │ │ ├── snippets.go │ │ │ ├── sourcemapcache.go │ │ │ └── sourcemapcache_test.go │ │ └── stdrwc.go │ ├── main.go │ ├── main_test.go │ ├── processor/ │ │ ├── processor.go │ │ └── processor_test.go │ ├── sloghandler/ │ │ └── handler.go │ ├── testproject/ │ │ ├── testdata/ │ │ │ ├── css-classes/ │ │ │ │ └── classes.go │ │ │ ├── go.mod.embed │ │ │ ├── go.sum │ │ │ ├── main.go │ │ │ ├── remotechild.templ │ │ │ ├── remotechild_templ.go │ │ │ ├── remoteparent.templ │ │ │ ├── remoteparent_templ.go │ │ │ ├── templates.templ │ │ │ └── templates_templ.go │ │ └── testproject.go │ └── visualize/ │ ├── sourcemapvisualisation.templ │ ├── sourcemapvisualisation_templ.go │ └── types.go ├── cosign.pub ├── docs/ │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── docs/ │ │ ├── 02-quick-start/ │ │ │ ├── 01-installation.md │ │ │ ├── 02-creating-a-simple-templ-component.md │ │ │ ├── 03-running-your-first-templ-application.md │ │ │ └── _category_.json │ │ ├── 03-syntax-and-usage/ │ │ │ ├── 01-basic-syntax.md │ │ │ ├── 02-elements.md │ │ │ ├── 03-attributes.md │ │ │ ├── 04-expressions.md │ │ │ ├── 05-statements.md │ │ │ ├── 06-if-else.md │ │ │ ├── 07-switch.md │ │ │ ├── 08-loops.md │ │ │ ├── 09-raw-go.md │ │ │ ├── 10-template-composition.md │ │ │ ├── 11-forms.md │ │ │ ├── 12-css-style-management.md │ │ │ ├── 13-script-templates.md │ │ │ ├── 14-comments.md │ │ │ ├── 15-context.md │ │ │ ├── 16-using-with-go-templates.md │ │ │ ├── 17-rendering-raw-html.md │ │ │ ├── 18-render-once.md │ │ │ ├── 19-fragments.md │ │ │ ├── 20-using-react-with-templ.md │ │ │ └── _category_.json │ │ ├── 04-core-concepts/ │ │ │ ├── 01-components.md │ │ │ ├── 02-template-generation.md │ │ │ ├── 03-testing.md │ │ │ ├── 04-view-models.md │ │ │ └── _category_.json │ │ ├── 05-server-side-rendering/ │ │ │ ├── 01-creating-an-http-server-with-templ.md │ │ │ ├── 02-example-counter-application.md │ │ │ ├── 03-htmx.md │ │ │ ├── 04-datastar.md │ │ │ ├── 05-streaming.md │ │ │ └── _category_.json │ │ ├── 06-static-rendering/ │ │ │ ├── 01-generating-static-html-files-with-templ.md │ │ │ ├── 02-blog-example.md │ │ │ ├── 03-deploying-static-files.md │ │ │ └── _category_.json │ │ ├── 07-project-structure/ │ │ │ ├── 01-project-structure.md │ │ │ └── _category_.json │ │ ├── 08-hosting-and-deployment/ │ │ │ ├── 01-hosting-on-aws-lambda.md │ │ │ ├── 02-hosting-using-docker.md │ │ │ └── _category_.json │ │ ├── 09-developer-tools/ │ │ │ ├── 01-cli.md │ │ │ ├── 02-ide-support.md │ │ │ ├── 03-live-reload.md │ │ │ ├── 04-live-reload-with-other-tools.md │ │ │ ├── 05-llm.md │ │ │ ├── 06-cicd.md │ │ │ └── _category_.json │ │ ├── 10-security/ │ │ │ ├── 01-injection-attacks.md │ │ │ ├── 02-content-security-policy.md │ │ │ ├── 03-code-signing.md │ │ │ └── _category_.json │ │ ├── 11-media/ │ │ │ ├── _category_.json │ │ │ └── index.md │ │ ├── 12-integrations/ │ │ │ ├── 01-web-frameworks.md │ │ │ ├── 02-internationalization.md │ │ │ └── _category_.json │ │ ├── 13-experimental/ │ │ │ ├── 01-overview.md │ │ │ ├── 02-urlbuilder.md │ │ │ └── _category_.json │ │ ├── 14-help-and-community/ │ │ │ ├── _category_.json │ │ │ └── index.md │ │ ├── 15-component-libraries/ │ │ │ ├── _category_.json │ │ │ └── index.md │ │ ├── 16-faq/ │ │ │ ├── _category_.json │ │ │ └── index.md │ │ ├── go.mod │ │ ├── go.sum │ │ ├── index.md │ │ └── main.go │ ├── docusaurus.config.js │ ├── package.json │ ├── sidebars.js │ ├── src/ │ │ ├── css/ │ │ │ └── custom.css │ │ └── theme/ │ │ └── prism-include-languages.js │ └── static/ │ ├── .nojekyll │ └── img/ │ └── shadowdom.webm ├── examples/ │ ├── blog/ │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.go │ │ ├── posts.templ │ │ ├── posts_templ.go │ │ └── posts_test.go │ ├── content-security-policy/ │ │ ├── main.go │ │ ├── templates.templ │ │ └── templates_templ.go │ ├── counter/ │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── assets/ │ │ │ ├── css/ │ │ │ │ └── bulma.css │ │ │ └── favicon/ │ │ │ ├── about.txt │ │ │ └── site.webmanifest │ │ ├── cdk/ │ │ │ ├── .gitignore │ │ │ ├── cdk.json │ │ │ └── stack.go │ │ ├── components/ │ │ │ ├── components.templ │ │ │ └── components_templ.go │ │ ├── db/ │ │ │ └── db.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── handlers/ │ │ │ └── default.go │ │ ├── lambda/ │ │ │ └── main.go │ │ ├── main.go │ │ ├── services/ │ │ │ └── count.go │ │ └── session/ │ │ └── session.go │ ├── counter-basic/ │ │ ├── .dockerignore │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── assets/ │ │ │ ├── bulma.css │ │ │ └── favicon/ │ │ │ ├── about.txt │ │ │ └── site.webmanifest │ │ ├── components.templ │ │ ├── components_templ.go │ │ ├── fly.toml │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── external-libraries/ │ │ ├── components.templ │ │ ├── components_templ.go │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── hello-world-ssr/ │ │ ├── hello.templ │ │ ├── hello_templ.go │ │ └── main.go │ ├── hello-world-static/ │ │ ├── hello.templ │ │ ├── hello_templ.go │ │ └── main.go │ ├── htmx-fragments/ │ │ ├── README.md │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.templ │ │ └── main_templ.go │ ├── integration-chi/ │ │ ├── go.mod │ │ ├── go.sum │ │ ├── home.templ │ │ ├── home_templ.go │ │ └── main.go │ ├── integration-echo/ │ │ ├── go.mod │ │ ├── go.sum │ │ ├── home.templ │ │ ├── home_templ.go │ │ └── main.go │ ├── integration-gin/ │ │ ├── gintemplrenderer/ │ │ │ └── renderer.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── home.html │ │ ├── home.templ │ │ ├── home_templ.go │ │ └── main.go │ ├── integration-go-echarts/ │ │ ├── components.templ │ │ ├── components_templ.go │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── integration-gofiber/ │ │ ├── README.md │ │ ├── go.mod │ │ ├── go.sum │ │ ├── home.templ │ │ ├── home_templ.go │ │ └── main.go │ ├── integration-react/ │ │ ├── README.md │ │ ├── components.templ │ │ ├── components_templ.go │ │ ├── flake.nix │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.go │ │ ├── react/ │ │ │ ├── .gitignore │ │ │ ├── components.tsx │ │ │ ├── index.ts │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ └── static/ │ │ └── index.js │ ├── internationalization/ │ │ ├── components.templ │ │ ├── components_templ.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── locales/ │ │ │ ├── de/ │ │ │ │ └── de.yaml │ │ │ ├── en/ │ │ │ │ └── en.yaml │ │ │ ├── locales.go │ │ │ └── zh-cn/ │ │ │ └── zh-cn.yaml │ │ └── main.go │ ├── static-generator/ │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── blog.templ │ │ ├── blog_templ.go │ │ ├── fly.toml │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── streaming/ │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.templ │ │ └── main_templ.go │ ├── suspense/ │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.templ │ │ └── main_templ.go │ ├── syntax-and-usage/ │ │ └── components/ │ │ ├── main.go │ │ ├── templsyntax.templ │ │ └── templsyntax_templ.go │ └── typescript/ │ ├── README.md │ ├── assets/ │ │ └── js/ │ │ └── index.js │ ├── components/ │ │ ├── index.templ │ │ └── index_templ.go │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── ts/ │ ├── package.json │ └── src/ │ └── index.ts ├── flake.nix ├── flush.go ├── flush_test.go ├── fragment.go ├── fragment_test.go ├── generator/ │ ├── generator.go │ ├── generator_test.go │ ├── htmldiff/ │ │ └── diff.go │ ├── rangewriter.go │ ├── rangewriter_test.go │ ├── test-a-href/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-attribute-errors/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-attribute-escaping/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-call/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-cancelled-context/ │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-class-whitespace/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-complex-attributes/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-constant-attribute-escaping/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-context/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-css-expression/ │ │ ├── constants.go │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-css-middleware/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-css-usage/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-doctype/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-doctype-html4/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-element-attributes/ │ │ ├── data.go │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-elseif/ │ │ ├── data.go │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-for/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-form-action/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-fragment/ │ │ ├── complete.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-go-comments/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-go-template-in-templ/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-html/ │ │ ├── data.go │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-html-comment/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-if/ │ │ ├── data.go │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-ifelse/ │ │ ├── data.go │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-import/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-js-unsafe-usage/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-js-usage/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-method/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-once/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-only-scripts/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-primitives/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-raw-elements/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-script-expressions/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-script-inline/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-script-usage/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-script-usage-nonce/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-spread-attributes/ │ │ ├── expected.html │ │ ├── expected_numeric_attributes.html │ │ ├── expected_ordered_attributes.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-string/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-string-errors/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-style-attribute/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-switch/ │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-switchdefault/ │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-templ-element/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-templ-in-go-template/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-text/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-text-whitespace/ │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ ├── test-void/ │ │ ├── expected.html │ │ ├── render_test.go │ │ ├── template.templ │ │ └── template_templ.go │ └── test-whitespace-around-go-keywords/ │ ├── render_test.go │ ├── template.templ │ └── template_templ.go ├── go.mod ├── go.sum ├── handler.go ├── handler_test.go ├── internal/ │ ├── format/ │ │ ├── format_test.go │ │ ├── scriptelement.go │ │ ├── styleelement.go │ │ ├── templ.go │ │ └── testdata/ │ │ ├── all_children_indented__with_nested_indentation__when_close_tag_is_on_new_line.txt │ │ ├── all_children_indented__with_nested_indentation__when_close_tag_is_on_same_line.txt │ │ ├── br_and_hr_all_on_one_line_are_not_placed_on_new_lines.txt │ │ ├── br_elements_are_placed_on_new_lines.txt │ │ ├── children_indented__closing_elm.txt │ │ ├── children_indented__first_child.txt │ │ ├── comments_are_preserved.txt │ │ ├── conditional_expressions_have_the_same_child_indentation_rules_as_regular_elements.txt │ │ ├── conditional_expressions_result_in_all_attrs_indented.txt │ │ ├── conditional_expressions_result_in_all_attrs_indented__2.txt │ │ ├── conditional_expressions_with_else_blocks_are_also_formatted.txt │ │ ├── constant_attributes_prerfer_double_quotes__but_use_single_quotes_if_required.txt │ │ ├── css_is_indented_by_one_level.txt │ │ ├── css_whitespace_is_tidied.txt │ │ ├── cssarguments_multiline.txt │ │ ├── empty_elements_stay_on_the_same_line.txt │ │ ├── for_loops_are_placed_on_a_new_line.txt │ │ ├── formatting_does_not_alter_whitespace.txt │ │ ├── go_expressions_are_formatted_by_the_go_formatter.txt │ │ ├── go_expressions_have_whitespace_normalised.txt │ │ ├── godoc_comments_are_preserved.txt │ │ ├── if_statements_are_placed_on_a_new_line.txt │ │ ├── inline_elements_are_not_placed_on_a_new_line.txt │ │ ├── inline_func_blank_lines_no_whitespace.txt │ │ ├── multiline_string_literal_indentation_preserved.txt │ │ ├── non_empty_elements_with_children_that_are_all_on_the_same_line_are_not_split_into_multiple_lines.txt │ │ ├── raw_go_is_formatted.txt │ │ ├── script_tags_are_not_converted_to_self_closing_elements.txt │ │ ├── scriptarguments_multiline.txt │ │ ├── scriptelement_contents_are_formatted.txt │ │ ├── scriptelements_hyperscript_is_ignored.txt │ │ ├── scriptelements_with_go_code_are_formatted.txt │ │ ├── scriptelements_with_multiple_go_code_sections_are_formatted.txt │ │ ├── scriptelements_within_templ_expressions_are_formatted.txt │ │ ├── spacing_between_string_expressions_is_kept.txt │ │ ├── spacing_between_string_expressions_is_not_magically_added.txt │ │ ├── spacing_between_string_spreads_attributes_is_kept.txt │ │ ├── styleelements_are_formatted.txt │ │ ├── switch_statements_are_placed_on_a_new_line.txt │ │ ├── tables_are_formatted_well.txt │ │ ├── templ_expression_attributes_are_formatted_correctly_when_multiline.txt │ │ ├── templ_expression_elements_are_formatted_the_same_as_other_elements.txt │ │ ├── templatearguments_multiline_with_generics.txt │ │ ├── templatefile_can_be_round_tripped.txt │ │ ├── templatefile_can_start_with_comments.txt │ │ ├── templatefile_can_start_with_comments_and_whitespace.txt │ │ ├── templatefile_can_start_with_multiline_comments_and_whitespace.txt │ │ ├── templatefile_can_start_with_multiple_comments_and_whitespace.txt │ │ ├── templateheader_with_build_tags.txt │ │ ├── templelement_multiline_block_containing_multiline_block.txt │ │ ├── templelement_multiline_block_indentation.txt │ │ ├── templelement_multiline_in_div.txt │ │ ├── templelement_param_spacing.txt │ │ ├── templelement_simple_block_indentation.txt │ │ ├── templelement_simple_in_div.txt │ │ ├── templelement_simple_no_change.txt │ │ ├── void_elements_are_converted_to_self_closing_elements.txt │ │ └── when_an_element_contains_children_that_are_on_new_lines__the_children_are_indented.txt │ ├── htmlfind/ │ │ ├── htmlfind.go │ │ └── htmlfind_test.go │ ├── imports/ │ │ ├── process.go │ │ ├── process_test.go │ │ └── testdata/ │ │ ├── comments.txtar │ │ ├── commentsbeforepackage.txtar │ │ ├── deleteimports.txtar │ │ ├── extraspace.txtar │ │ ├── groups.txtar │ │ ├── groupsmanynewlines.txtar │ │ ├── header.txtar │ │ ├── hyphenatedimport.txtar │ │ ├── namedimportsadd.txtar │ │ ├── namedimportsremoved.txtar │ │ ├── noimports.txtar │ │ ├── noimportscode.txtar │ │ ├── stringexp.txtar │ │ └── twoimports.txtar │ ├── lazyloader/ │ │ ├── docheader.go │ │ ├── docheader_test.go │ │ ├── docheaderparser.go │ │ ├── docheaderparser_test.go │ │ ├── pkgloader.go │ │ ├── pkgloader_test.go │ │ ├── pkgtraverser.go │ │ ├── pkgtraverser_test.go │ │ ├── templdoclazyloader.go │ │ └── templdoclazyloader_test.go │ ├── prettier/ │ │ ├── prettier.go │ │ ├── prettier_test.go │ │ └── testdata.txtar │ ├── skipdir/ │ │ ├── skipdir.go │ │ └── skipdir_test.go │ ├── syncmap/ │ │ ├── map.go │ │ └── map_test.go │ └── syncset/ │ ├── set.go │ └── set_test.go ├── join.go ├── join_test.go ├── js.go ├── js_test.go ├── jsonscript.go ├── jsonscript_test.go ├── jsonstring.go ├── jsonstring_test.go ├── lsp/ │ ├── LICENSE │ ├── README.md │ ├── jsonrpc2/ │ │ ├── codes.go │ │ ├── conn.go │ │ ├── errors.go │ │ ├── handler.go │ │ ├── jsonrpc2.go │ │ ├── jsonrpc2_test.go │ │ ├── message.go │ │ ├── serve.go │ │ ├── serve_test.go │ │ ├── stream.go │ │ ├── wire.go │ │ └── wire_test.go │ ├── protocol/ │ │ ├── base.go │ │ ├── base_test.go │ │ ├── basic.go │ │ ├── basic_test.go │ │ ├── callhierarchy.go │ │ ├── callhierarchy_test.go │ │ ├── capabilities_client.go │ │ ├── capabilities_client_test.go │ │ ├── capabilities_server.go │ │ ├── client.go │ │ ├── context.go │ │ ├── deprecated.go │ │ ├── diagnostics.go │ │ ├── diagnostics_test.go │ │ ├── doc.go │ │ ├── errors.go │ │ ├── general.go │ │ ├── general_test.go │ │ ├── handler.go │ │ ├── language.go │ │ ├── language_test.go │ │ ├── progress.go │ │ ├── progress_test.go │ │ ├── protocol.go │ │ ├── registration.go │ │ ├── registration_test.go │ │ ├── selectionrange.go │ │ ├── semantic_token.go │ │ ├── server.go │ │ ├── text.go │ │ ├── text_test.go │ │ ├── util.go │ │ ├── util_test.go │ │ ├── version.go │ │ ├── window.go │ │ ├── window_test.go │ │ ├── workspace.go │ │ └── workspace_test.go │ ├── uri/ │ │ ├── uri.go │ │ └── uri_test.go │ └── xcontext/ │ └── xcontext.go ├── once.go ├── once_test.go ├── parser/ │ └── v2/ │ ├── allocs_test.go │ ├── benchmarks_test.go │ ├── benchmarktestdata/ │ │ └── benchmark.txt │ ├── calltemplateparser.go │ ├── calltemplateparser_test.go │ ├── childrenparser.go │ ├── childrenparser_test.go │ ├── conditionalattributeparser.go │ ├── cssparser.go │ ├── cssparser_test.go │ ├── diagnostics.go │ ├── diagnostics_test.go │ ├── doctypeparser.go │ ├── doctypeparser_test.go │ ├── elementparser.go │ ├── elementparser_test.go │ ├── expressionparser.go │ ├── expressionparser_test.go │ ├── fallthroughparser.go │ ├── fallthroughparser_test.go │ ├── forexpressionparser.go │ ├── forexpressionparser_test.go │ ├── fuzz.sh │ ├── gocodeparser.go │ ├── gocodeparser_test.go │ ├── gocommentparser.go │ ├── gocommentparser_test.go │ ├── goexpression/ │ │ ├── fuzz.sh │ │ ├── parse.go │ │ ├── parse_test.go │ │ ├── parsebench_test.go │ │ ├── scanner.go │ │ └── testdata/ │ │ └── fuzz/ │ │ ├── FuzzCaseDefault/ │ │ │ ├── 3c6f43d3ec8a900b │ │ │ ├── 986e7bc325c7890c │ │ │ └── d8a9a4cd9fc8cb11 │ │ ├── FuzzExpression/ │ │ │ └── ac5d99902f5e7914 │ │ ├── FuzzFuncs/ │ │ │ └── 46c9ed6c9d427bd2 │ │ └── FuzzIf/ │ │ └── 7a174efc13e3fdd6 │ ├── goparser.go │ ├── htmlcommentparser.go │ ├── htmlcommentparser_test.go │ ├── ifexpressionparser.go │ ├── ifexpressionparser_test.go │ ├── packageparser.go │ ├── packageparser_test.go │ ├── parser.go │ ├── raw.go │ ├── raw_test.go │ ├── scriptparser.go │ ├── scriptparser_test.go │ ├── scriptparsertestdata/ │ │ ├── backtickquote.txt │ │ ├── backtickquote_apostrophe.txt │ │ ├── doublequote.txt │ │ ├── doublequote_apostrophe.txt │ │ ├── escapechars.txt │ │ ├── non_js_script.txt │ │ ├── regexp_literal.txt │ │ ├── showsuccessmessage.txt │ │ ├── singlequote.txt │ │ ├── singlequote_apostrophe.txt │ │ └── terminating_comment.txt │ ├── scripttemplateparser.go │ ├── scripttemplateparser_test.go │ ├── sourcemap.go │ ├── sourcemap_test.go │ ├── stringexpressionparser.go │ ├── stringexpressionparser_test.go │ ├── structure.go │ ├── switchexpressionparser.go │ ├── switchexpressionparser_test.go │ ├── templatefile.go │ ├── templatefile_test.go │ ├── templateparser.go │ ├── templateparser_test.go │ ├── templelementparser.go │ ├── templelementparser_test.go │ ├── testdata/ │ │ └── fuzz/ │ │ └── FuzzScriptParser/ │ │ ├── 0667fe9c719c304f │ │ ├── 21c86d8a2781524b │ │ └── 43cd47dd50874af5 │ ├── textparser.go │ ├── textparser_test.go │ ├── types.go │ ├── visitor/ │ │ ├── visitor.go │ │ └── visitor_test.go │ ├── visitor.go │ └── whitespaceparser.go ├── runtime/ │ ├── buffer.go │ ├── buffer_test.go │ ├── bufferpool.go │ ├── bufferpool_test.go │ ├── builder.go │ ├── builder_test.go │ ├── fuzzing/ │ │ ├── fuzz.templ │ │ ├── fuzz_templ.go │ │ ├── fuzz_test.go │ │ ├── go.mod │ │ ├── go.sum │ │ └── testdata/ │ │ └── fuzz/ │ │ ├── FuzzComponentAny/ │ │ │ ├── 02bc261247f1267d │ │ │ ├── 0e3d2540388fc8bd │ │ │ ├── 0ed510998a1c1a4e │ │ │ └── 926b62a033ecc0fd │ │ └── FuzzComponentString/ │ │ ├── 4a59bdc98ee75491 │ │ ├── 66658924a0ea89b6 │ │ └── 9fc8b4df9a42170c │ ├── runtime.go │ ├── runtime_test.go │ ├── scriptelement.go │ ├── scriptelement_test.go │ ├── styleattribute.go │ ├── styleattribute_test.go │ ├── watchmode.go │ └── watchmode_test.go ├── runtime.go ├── runtime_test.go ├── safehtml/ │ ├── style.go │ └── style_test.go ├── scripttemplate.go ├── scripttemplate_test.go ├── storybook/ │ ├── .gitignore │ ├── _example/ │ │ ├── cdk/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── cdk.go │ │ │ ├── cdk.json │ │ │ └── deploy.sh │ │ ├── go.mod │ │ ├── go.sum │ │ ├── lambda/ │ │ │ └── main.go │ │ ├── local/ │ │ │ └── main.go │ │ ├── run.sh │ │ ├── storybook.go │ │ ├── templates.templ │ │ └── templates_templ.go │ ├── _package.json │ └── storybook.go ├── turbo/ │ ├── stream.go │ ├── stream.templ │ ├── stream_templ.go │ └── stream_test.go ├── url.go ├── url_test.go ├── version.go └── watchmode.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ .git Dockerfile .dockerignore ================================================ FILE: .envrc ================================================ use flake ================================================ FILE: .github/FUNDING.yml ================================================ github: [a-h, joerdav] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Before you begin** Please make sure you're using the latest version of the templ CLI (`go install github.com/a-h/templ/cmd/templ@latest`), and have upgraded your project to use the latest version of the templ runtime (`go get -u github.com/a-h/templ@latest`) **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** A small, self-contained, complete reproduction, uploaded to a GitHub repo, containing the minimum amount of files required to reproduce the behaviour, along with a list of commands that need to be run. Keep it simple. **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots or screen captures to help explain your problem. **Logs** If the issue is related to IDE support, run through the LSP troubleshooting section at https://templ.guide/developer-tools/ide-support/#troubleshooting-1 and include logs from templ **`templ info` output** Run `templ info` and include the output. **Desktop (please complete the following information):** - OS: [e.g. MacOS, Linux, Windows, WSL] - templ CLI version (`templ version`) - Go version (`go version`) - `gopls` version (`gopls version`) **Additional context** Add any other context about the problem here. ================================================ FILE: .github/copilot-instructions.md ================================================ # Coding standards ## Behaviour * Always run `go fmt` after making changes to Go code. * Always run unit tests after making changes to Go code. ## Environment setup * Ensure that the user has direnv installed, and that it is set up correctly in their shell. See https://direnv.net/docs/installation.html * Ensure that the user has Nix installed, and that it is set up correctly. See https://nixos.org/download.html * Ensure that the user has the direnv VS code extension installed, so that the `.envrc` file is automatically loaded when the project is opened in VS Code. ### Background templ has an `.envrc` file that is used to set up the development environment using a tool called `direnv`. There is a VS Code extension available that will automatically load this when you open the project in VS Code. The `.envrc` file uses a Nix flake to set up the environment, so Nix is required to be installed. The version of Go used is defined in the `flake.nix` file. ## Build tasks templ uses the `xc` task runner - https://github.com/joerdav/xc If you run `xc` you can get see a list of the development tasks that can be run, or you can read the `README.md` file and see the `Tasks` section. The most useful tasks for local development are: * `xc install-snapshot` - builds the templ CLI and installs it into `~/bin`. Ensure that this is in your path. * `xc generate` - generates Go code from the templ files in the project. * `xc test` - regenerates all templates, and runs the unit tests. * `xc test-short` - runs shorter tests, avoiding long running tests for filesystem watchers etc. * `xc fmt` - runs `gofmt` to format all Go code. * `xc lint` - run the same linting as run in the CI process. * `xc docs-run` - run the Docusaurus documentation site. templ has a code generation step, this is automatically carried out using `xc test`. Don't install templ globally using `xc install-snapshot` or `go install`. Use the `xc generate` or `xc test-short` tasks to generate the code, which will also run the tests. ## Commit messages The project using https://www.conventionalcommits.org/en/v1.0.0/ Examples: * `feat: support Go comments in templates, fixes #234"` * `fix: ensure that the templ CLI works with Go 1.21, fixes #123` ## Documentation * Documentation is written in Markdown, and is rendered using Docusaurus. The documentation is in the `docs` directory. * Update documentation when the behaviour of templ changes, or when new features are added. ## Writing style * Use American English spelling to match the Go standard library, and HTML spec, e.g. "color". * Use the Oxford comma, e.g. "apples, oranges, and bananas". * Avoid use of emojis everywhere - in code, comments, commit messages, and documentation. * Be "to the point" and precise - avoid unnecessary words, don't use filler words like "just" or "really". * Use the active voice, e.g. "templ generates code" rather than "code is generated by templ". * Don't use emphatic words or phrases like "very", "blazingly fast", etc. ## Coding style * Reduce nesting - i.e. prefer early returns over an `else` block, as per https://danp.net/posts/reducing-go-nesting/ or https://go.dev/doc/effective_go#if * Use line breaks to separate "paragraphs" of code - don't use line breaks in between lines, or at the start/end of functions etc. * Use the `xc fmt` and `xc lint` build tasks to format and lint code before committing. * Don't use unnecessary comments that explain what the code does. * If comments are used, ensure that they are full sentences, and use proper punctuation, including ending with a full stop. * Don't write comments after the end of keywords, e.g. `continue // Only process pairs` ## Tests * Tests for generated code are in the `./generator` directory. Each test is in a subdirectory. * Tests for the templ CLI are in the `./cmd/templ` directory. * Tests for the templ runtime are in the root directory. * Tests for formatting templ files are in `./parser/v2/formattestdata` - it uses txtar to store tests. * The `htmldiff` library does not take whitespace into account, so cannot be used to test output whitespace handling. * Don't attempt to run tests individually, use the `xc test` task to run all tests, because it regenerates templates, and there's minimal performance penalty due to Go's test caching. ## Moving and renaming files * templ files have the `.templ` extension. * If a `project.templ` file is created, after generation a `project_templ.go` file will be created. * If a `project.templ` file is renamed, you must also rename the generated `project_templ.go` file. * If a `project.templ` file is moved, you must also move the generated `project_templ.go` file. * If a `project.templ` file is deleted, you must also delete the generated `project_templ.go` file. # Files * Don't attempt to create helper or utility tests in the `./tmp` or `/tmp` directory. Create unit tests in the relevant directory instead. ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [main] pull_request: branches: [main] permissions: contents: read jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: nixbuild/nix-quick-install-action@v30 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} - name: Restore and save Nix store uses: nix-community/cache-nix-action@v6 with: primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} gc-max-store-size-linux: 1G purge: true purge-prefixes: nix-${{ runner.os }}- purge-primary-key: never - name: Test run: nix develop --command xc test-cover - name: Upload coverage artifact if: github.event_name == 'push' uses: actions/upload-artifact@v4 with: name: coverage path: coverage.out - name: Build run: nix build update-coverage: if: github.event_name == 'push' needs: build runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 - name: Download coverage uses: actions/download-artifact@v4 with: name: coverage - name: Update coverage report uses: ncruces/go-coverage-report@57ac6f0f19874f7afbab596105154f08004f482e with: coverage-file: coverage.out report: 'true' chart: 'true' reuse-go: 'true' lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: nixbuild/nix-quick-install-action@v30 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} - name: Restore and save Nix store uses: nix-community/cache-nix-action@v6 with: primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} gc-max-store-size-linux: 1G purge: true purge-prefixes: nix-${{ runner.os }}- purge-primary-key: never - name: Lint run: nix develop --command xc lint ensure-generated: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: nixbuild/nix-quick-install-action@v30 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} - name: Restore and save Nix store uses: nix-community/cache-nix-action@v6 with: primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} gc-max-store-size-linux: 1G purge: true purge-prefixes: nix-${{ runner.os }}- purge-primary-key: never - name: Generate run: nix develop --command xc ensure-generated ensure-fmt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: nixbuild/nix-quick-install-action@v30 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} - name: Restore and save Nix store uses: nix-community/cache-nix-action@v6 with: primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} gc-max-store-size-linux: 1G purge: true purge-prefixes: nix-${{ runner.os }}- purge-primary-key: never - name: Fmt run: nix develop --command xc fmt - name: Ensure clean run: git diff --exit-code ================================================ FILE: .github/workflows/docs.yaml ================================================ name: Deploy Docs on: release: types: [published] workflow_dispatch: permissions: contents: read pages: write id-token: write concurrency: group: "pages" cancel-in-progress: false defaults: run: shell: bash jobs: build-docs: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 0 - name: Setup Pages id: pages uses: actions/configure-pages@v5 - uses: actions/setup-node@v4 with: node-version: '20' cache: npm cache-dependency-path: "./docs/package-lock.json" - name: Install Node.js dependencies run: | cd docs npm ci - name: Build run: | cd docs npm run build - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: ./docs/build deploy-docs: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build-docs steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .github/workflows/flakehub-publish-tagged.yml ================================================ name: "Publish tags to FlakeHub" on: push: tags: - "v?[0-9]+.[0-9]+.[0-9]+*" workflow_dispatch: inputs: tag: description: "The existing tag to publish to FlakeHub" type: "string" required: true jobs: flakehub-publish: runs-on: "ubuntu-latest" permissions: id-token: "write" contents: "read" steps: - uses: "actions/checkout@v4" with: ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}" - uses: "DeterminateSystems/determinate-nix-action@v3" - uses: "DeterminateSystems/flakehub-push@main" with: visibility: "public" name: "a-h/templ" tag: "${{ inputs.tag }}" include-output-paths: true ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - 'v*' workflow_dispatch: permissions: contents: write packages: write jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-go@v5 with: go-version: 1.23 cache: true - uses: ko-build/setup-ko@v0.7 - uses: sigstore/cosign-installer@v3.7.0 with: cosign-release: v2.2.3 - uses: goreleaser/goreleaser-action@v5 with: version: v1.24.0 args: release --clean env: GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' COSIGN_PASSWORD: '${{ secrets.COSIGN_PASSWORD }}' COSIGN_PRIVATE_KEY: '${{ secrets.COSIGN_PRIVATE_KEY }}' COSIGN_PUBLIC_KEY: '${{ secrets.COSIGN_PUBLIC_KEY }}' ================================================ FILE: .gitignore ================================================ # Output. cmd/templ/templ # Logs. cmd/templ/lspcmd/*log.txt # Go code coverage. coverage.out coverage # Mac filesystem jank. .DS_Store # Docusaurus. docs/build/ docs/resources/_gen/ node_modules/ dist/ # Nix artifacts. result # Editors ## nvim .null-ls* # vscode .vscode/ # Go workspace. go.work # direnv .direnv # templ txt files. *_templ.txt # Example output binaries. /examples/integration-gin/integration-gin /examples/integration-echo/integration-echo ================================================ FILE: .goreleaser.yaml ================================================ builds: - env: - CGO_ENABLED=0 dir: cmd/templ mod_timestamp: '{{ .CommitTimestamp }}' flags: - -trimpath ldflags: - -s -w goos: - linux - windows - darwin checksum: name_template: 'checksums.txt' signs: - id: checksums cmd: cosign stdin: '{{ .Env.COSIGN_PASSWORD }}' output: true artifacts: checksum args: - sign-blob - --yes - --key - env://COSIGN_PRIVATE_KEY - '--output-certificate=${certificate}' - '--output-signature=${signature}' - '${artifact}' archives: - format: tar.gz name_template: >- {{ .ProjectName }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} kos: - repository: ghcr.io/a-h/templ platforms: - linux/amd64 - linux/arm64 tags: - latest - '{{.Tag}}' bare: true docker_signs: - cmd: cosign artifacts: all output: true args: - sign - --yes - --key - env://COSIGN_PRIVATE_KEY - '${artifact}' snapshot: name_template: "{{ incpatch .Version }}-next" changelog: sort: asc filters: exclude: - '^docs:' - '^test:' ================================================ FILE: .ignore ================================================ *_templ.go examples/integration-ct/static/index.js examples/counter/assets/css/bulma.* examples/counter/assets/js/htmx.min.js examples/counter-basic/assets/css/bulma.* examples/typescript/assets/index.js package-lock.json go.sum docs/static/llms.md ================================================ FILE: .version ================================================ 0.3.1002 ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at adrianhesketh@hushail.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to templ ## Vision Enable Go developers to build strongly typed, component-based HTML user interfaces with first-class developer tooling, and a short learning curve. ## Come up with a design and share it Before starting work on any major pull requests or code changes, start a discussion at https://github.com/a-h/templ/discussions or raise an issue. We don't want you to spend time on a PR or feature that ultimately doesn't get merged because it doesn't fit with the project goals, or the design doesn't work for some reason. For issues, it really helps if you provide a reproduction repo, or can create a failing unit test to describe the behaviour. In designs, we need to consider: * Backwards compatibility - Not changing the public API between releases, introducing gradual deprecation - don't break people's code. * Correctness over time - How can we reduce the risk of defects both now, and in future releases? * Threat model - How could each change be used to inject vulnerabilities into web pages? * Go version - We target the oldest supported version of Go as per https://go.dev/doc/devel/release * Automatic migration - If we need to force through a change. * Compile time vs runtime errors - Prefer compile time. * Documentation - New features are only useful if people can understand the new feature, what would the documentation look like? * Examples - How will we demonstrate the feature? ## Project structure templ is structured into a few areas: ### Parser `./parser` The parser directory currently contains both v1 and v2 parsers. The v1 parser is not maintained, it's only used to migrate v1 code over to the v2 syntax. The parser is responsible for parsing templ files into an object model. The types that make up the object model are in `types.go`. Automatic formatting of the types is tested in `types_test.go`. A templ file is parsed into the `TemplateFile` struct object model. ```go type TemplateFile struct { // Header contains comments or whitespace at the top of the file. Header []GoExpression // Package expression. Package Package // Nodes in the file. Nodes []TemplateFileNode } ``` Parsers are individually tested using two types of unit test. One test covers the successful parsing of text into an object. For example, the `HTMLCommentParser` test checks for successful patterns. ```go func TestHTMLCommentParser(t *testing.T) { var tests = []struct { name string input string expected HTMLComment }{ { name: "comment - single line", input: ``, expected: HTMLComment{ Contents: " single line comment ", }, }, { name: "comment - no whitespace", input: ``, expected: HTMLComment{ Contents: "no whitespace between sequence open and close", }, }, { name: "comment - multiline", input: ``, expected: HTMLComment{ Contents: ` multiline comment `, }, }, { name: "comment - with tag", input: ``, expected: HTMLComment{ Contents: `
tag
`, }, }, { name: "comments can contain tags", input: ``, expected: HTMLComment{ Contents: `A
A
B
B
C
C
Data
"))), Header: make(http.Header), Request: &http.Request{ URL: &url.URL{ Scheme: "http", Host: "example.com", }, }, } r.Header.Set("Content-Type", "text/html, charset=utf-8") r.Header.Set("Content-Encoding", "weird-encoding") // Act lh := newTestLogHandler(slog.LevelInfo) log := slog.New(lh) h := New(log, "http", "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"}) err := h.modifyResponse(r) if err != nil { t.Fatalf("unexpected error: %v", err) } // Assert if len(lh.records) != 1 { var sb strings.Builder for _, record := range lh.records { sb.WriteString(record.Message) sb.WriteString("\n") } t.Fatalf("expected 1 log entry, but got %d: \n%s", len(lh.records), sb.String()) } record := lh.records[0] if record.Message != unsupportedContentEncoding { t.Errorf("expected warning message %q, got %q", unsupportedContentEncoding, record.Message) } if record.Level != slog.LevelWarn { t.Errorf("expected warning, got level %v", record.Level) } }) } func newTestLogHandler(level slog.Level) *testLogHandler { return &testLogHandler{ m: new(sync.Mutex), records: nil, level: level, } } type testLogHandler struct { m *sync.Mutex records []slog.Record level slog.Level } func (h *testLogHandler) Enabled(ctx context.Context, l slog.Level) bool { return l >= h.level } func (h *testLogHandler) Handle(ctx context.Context, r slog.Record) error { h.m.Lock() defer h.m.Unlock() if r.Level < h.level { return nil } h.records = append(h.records, r) return nil } func (h *testLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return h } func (h *testLogHandler) WithGroup(name string) slog.Handler { return h } func TestParseNonce(t *testing.T) { for _, tc := range []struct { name string csp string expected string }{ { name: "empty csp", csp: "", expected: "", }, { name: "simple csp", csp: "script-src 'nonce-oLhVst3hTAcxI734qtB0J9Qc7W4qy09C'", expected: "oLhVst3hTAcxI734qtB0J9Qc7W4qy09C", }, { name: "simple csp without single quote", csp: "script-src nonce-oLhVst3hTAcxI734qtB0J9Qc7W4qy09C", expected: "oLhVst3hTAcxI734qtB0J9Qc7W4qy09C", }, { name: "complete csp", csp: "default-src 'self'; frame-ancestors 'self'; form-action 'self'; script-src 'strict-dynamic' 'nonce-4VOtk0Uo1l7pwtC';", expected: "4VOtk0Uo1l7pwtC", }, { name: "mdn example 1", csp: "default-src 'self'", expected: "", }, { name: "mdn example 2", csp: "default-src 'self' *.trusted.com", expected: "", }, { name: "mdn example 3", csp: "default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com", expected: "", }, { name: "mdn example 3 multiple sources", csp: "default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com foo.com 'strict-dynamic' 'nonce-4VOtk0Uo1l7pwtC'", expected: "4VOtk0Uo1l7pwtC", }, } { t.Run(tc.name, func(t *testing.T) { nonce := parseNonce(tc.csp) if nonce != tc.expected { t.Errorf("expected nonce to be %s, but got %s", tc.expected, nonce) } }) } } func TestStreamInsertAfterBodyOpen(t *testing.T) { t.Run("script tags with special characters are not escaped", func(t *testing.T) { input := `Content
` nonce := "test-nonce-123" var output bytes.Buffer err := streamInsertAfterBodyOpen(nonce, strings.NewReader(input), &output) if err != nil { t.Fatalf("unexpected error: %v", err) } result := output.String() if !strings.Contains(result, `nonce="`+nonce+`"`) { t.Errorf("expected nonce attribute to be present with value %s, got: %s", nonce, result) } if !strings.Contains(result, `src="/_templ/reload/script.js"`) { t.Errorf("expected script src to be present, got: %s", result) } }) } ================================================ FILE: cmd/templ/generatecmd/proxy/script.js ================================================ (function() { let templ_reloadSrc = window.templ_reloadSrc || new EventSource("/_templ/reload/events"); templ_reloadSrc.onmessage = (event) => { if (event && event.data === "reload") { window.location.reload(); } }; window.templ_reloadSrc = templ_reloadSrc; window.onbeforeunload = () => window.templ_reloadSrc.close(); })(); ================================================ FILE: cmd/templ/generatecmd/run/run_test.go ================================================ package run_test import ( "context" "embed" "io" "net/http" "os" "path/filepath" "syscall" "testing" "time" "github.com/a-h/templ/cmd/templ/generatecmd/run" ) //go:embed testprogram/* var testprogram embed.FS func TestGoRun(t *testing.T) { if testing.Short() { t.Skip("Skipping test in short mode.") } // Copy testprogram to a temporary directory. dir, err := os.MkdirTemp("", "testprogram") if err != nil { t.Fatalf("failed to make test dir: %v", err) } files, err := testprogram.ReadDir("testprogram") if err != nil { t.Fatalf("failed to read embedded dir: %v", err) } for _, file := range files { srcFileName := "testprogram/" + file.Name() srcData, err := testprogram.ReadFile(srcFileName) if err != nil { t.Fatalf("failed to read src file %q: %v", srcFileName, err) } tgtFileName := filepath.Join(dir, file.Name()) if err = os.WriteFile(tgtFileName, srcData, 0644); err != nil { t.Fatalf("failed to write tgt file %q: %v", tgtFileName, err) } } // Rename the go.mod.embed file to go.mod. if err := os.Rename(filepath.Join(dir, "go.mod.embed"), filepath.Join(dir, "go.mod")); err != nil { t.Fatalf("failed to rename go.mod.embed: %v", err) } tests := []struct { name string cmd string }{ { name: "Well behaved programs get shut down", cmd: "go run .", }, { name: "Badly behaved programs get shut down", cmd: "go run . -badly-behaved", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() cmd, err := run.Run(ctx, dir, tt.cmd) if err != nil { t.Fatalf("failed to run program: %v", err) } time.Sleep(1 * time.Second) pid := cmd.Process.Pid if err := run.KillAll(); err != nil { t.Fatalf("failed to kill all: %v", err) } // Check the parent process is no longer running. if err := cmd.Process.Signal(os.Signal(syscall.Signal(0))); err == nil { t.Fatalf("process %d is still running", pid) } // Check that the child was stopped. body, err := readResponse("http://localhost:7777") if err == nil { t.Fatalf("child process is still running: %s", body) } }) } } func readResponse(url string) (body string, err error) { resp, err := http.Get(url) if err != nil { return body, err } b, err := io.ReadAll(resp.Body) if err != nil { _ = resp.Body.Close() return body, err } if err = resp.Body.Close(); err != nil { return body, err } return string(b), nil } ================================================ FILE: cmd/templ/generatecmd/run/run_unix.go ================================================ //go:build unix package run import ( "context" "errors" "fmt" "os" "os/exec" "strings" "sync" "syscall" "time" ) var ( m = &sync.Mutex{} running = map[string]*exec.Cmd{} ) func KillAll() (err error) { m.Lock() defer m.Unlock() var errs []error for _, cmd := range running { if err := kill(cmd); err != nil { errs = append(errs, fmt.Errorf("failed to kill process %d: %w", cmd.Process.Pid, err)) } } running = map[string]*exec.Cmd{} return errors.Join(errs...) } func kill(cmd *exec.Cmd) (err error) { errs := make([]error, 4) errs[0] = ignoreExited(cmd.Process.Signal(syscall.SIGINT)) errs[1] = ignoreExited(cmd.Process.Signal(syscall.SIGTERM)) errs[2] = ignoreExited(cmd.Wait()) errs[3] = ignoreExited(syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)) return errors.Join(errs...) } func ignoreExited(err error) error { if errors.Is(err, syscall.ESRCH) { return nil } // Ignore *exec.ExitError if _, ok := err.(*exec.ExitError); ok { return nil } return err } func Run(ctx context.Context, workingDir string, input string) (cmd *exec.Cmd, err error) { m.Lock() defer m.Unlock() cmd, ok := running[input] if ok { if err := kill(cmd); err != nil { return cmd, fmt.Errorf("failed to kill process %d: %w", cmd.Process.Pid, err) } delete(running, input) } parts := strings.Fields(input) executable := parts[0] args := []string{} if len(parts) > 1 { args = append(args, parts[1:]...) } cmd = exec.CommandContext(ctx, executable, args...) // Wait for the process to finish gracefully before termination. cmd.WaitDelay = time.Second * 3 cmd.Env = os.Environ() cmd.Dir = workingDir cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} running[input] = cmd err = cmd.Start() return } ================================================ FILE: cmd/templ/generatecmd/run/run_windows.go ================================================ //go:build windows package run import ( "context" "os" "os/exec" "strconv" "strings" "sync" ) var ( m = &sync.Mutex{} running = map[string]*exec.Cmd{} ) func KillAll() (err error) { m.Lock() defer m.Unlock() for _, cmd := range running { kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid)) kill.Stderr = os.Stderr kill.Stdout = os.Stdout err := kill.Run() if err != nil { return err } } running = map[string]*exec.Cmd{} return } func Stop(cmd *exec.Cmd) (err error) { kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid)) kill.Stderr = os.Stderr kill.Stdout = os.Stdout return kill.Run() } func Run(ctx context.Context, workingDir string, input string) (cmd *exec.Cmd, err error) { m.Lock() defer m.Unlock() cmd, ok := running[input] if ok { kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid)) kill.Stderr = os.Stderr kill.Stdout = os.Stdout err := kill.Run() if err != nil { return cmd, err } delete(running, input) } parts := strings.Fields(input) executable := parts[0] args := []string{} if len(parts) > 1 { args = append(args, parts[1:]...) } cmd = exec.Command(executable, args...) cmd.Env = os.Environ() cmd.Dir = workingDir cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr running[input] = cmd err = cmd.Start() return } ================================================ FILE: cmd/templ/generatecmd/run/testprogram/go.mod.embed ================================================ module testprogram go 1.23 ================================================ FILE: cmd/templ/generatecmd/run/testprogram/main.go ================================================ package main import ( "flag" "fmt" "net/http" "os" "os/signal" "syscall" "time" ) // This is a test program. It is used only to test the behaviour of the run package. // The run package is supposed to be able to run and stop programs. Those programs may start // child processes, which should also be stopped when the parent program is stopped. // For example, running `go run .` will compile an executable and run it. // So, this program does nothing. It just waits for a signal to stop. // In "Well behaved" mode, the program will stop when it receives a signal. // In "Badly behaved" mode, the program will ignore the signal and continue running. // The run package should be able to stop the program in both cases. var badlyBehavedFlag = flag.Bool("badly-behaved", false, "If set, the program will ignore the stop signal and continue running.") func main() { flag.Parse() mode := "Well behaved" if *badlyBehavedFlag { mode = "Badly behaved" } fmt.Printf("%s process %d started.\n", mode, os.Getpid()) // Start a web server on a known port so that we can check that this process is // not running, when it's been started as a child process, and we don't know // its pid. go func() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(w, "%d", os.Getpid()) }) err := http.ListenAndServe("127.0.0.1:7777", nil) if err != nil { fmt.Printf("Error running web server: %v\n", err) } }() sigs := make(chan os.Signal, 1) if !*badlyBehavedFlag { signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) } for { select { case <-sigs: fmt.Printf("Process %d received signal. Stopping.\n", os.Getpid()) return case <-time.After(1 * time.Second): fmt.Printf("Process %d still running...\n", os.Getpid()) } } } ================================================ FILE: cmd/templ/generatecmd/sse/server.go ================================================ package sse import ( _ "embed" "fmt" "net/http" "sync" "sync/atomic" "time" ) func New() *Handler { return &Handler{ m: new(sync.Mutex), requests: map[int64]chan event{}, } } type Handler struct { m *sync.Mutex counter int64 requests map[int64]chan event } type event struct { Type string Data string } // Send an event to all connected clients. func (s *Handler) Send(eventType string, data string) { s.m.Lock() defer s.m.Unlock() for _, f := range s.requests { f := f go func(f chan event) { f <- event{ Type: eventType, Data: data, } }(f) } } func (s *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") id := atomic.AddInt64(&s.counter, 1) s.m.Lock() events := make(chan event) s.requests[id] = events s.m.Unlock() defer func() { s.m.Lock() defer s.m.Unlock() delete(s.requests, id) close(events) }() timer := time.NewTimer(0) loop: for { select { case <-timer.C: if _, err := fmt.Fprintf(w, "event: message\ndata: ping\n\n"); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } timer.Reset(time.Second * 5) case e := <-events: if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", e.Type, e.Data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } case <-r.Context().Done(): break loop } w.(http.Flusher).Flush() } } ================================================ FILE: cmd/templ/generatecmd/symlink/symlink_test.go ================================================ package symlink import ( "context" "io" "os" "path" "testing" "github.com/a-h/templ/cmd/templ/generatecmd" "github.com/a-h/templ/cmd/templ/testproject" ) func TestSymlink(t *testing.T) { t.Run("can generate if root is symlink", func(t *testing.T) { // templ generate -f templates.templ dir, err := testproject.Create("github.com/a-h/templ/cmd/templ/testproject") if err != nil { t.Fatalf("failed to create test project: %v", err) } defer func() { if err := os.RemoveAll(dir); err != nil { t.Errorf("failed to remove test project directory: %v", err) } }() symlinkPath := dir + "-symlink" err = os.Symlink(dir, symlinkPath) if err != nil { t.Fatalf("failed to create dir symlink: %v", err) } defer func() { if err = os.Remove(symlinkPath); err != nil { t.Errorf("failed to remove symlink directory: %v", err) } }() // Delete the templates_templ.go file to ensure it is generated. err = os.Remove(path.Join(symlinkPath, "templates_templ.go")) if err != nil { t.Fatalf("failed to remove templates_templ.go: %v", err) } // Run the generate command. err = generatecmd.Run(context.Background(), io.Discard, io.Discard, []string{"-path", symlinkPath}) if err != nil { t.Fatalf("failed to run generate command: %v", err) } // Check the templates_templ.go file was created. _, err = os.Stat(path.Join(symlinkPath, "templates_templ.go")) if err != nil { t.Fatalf("templates_templ.go was not created: %v", err) } }) } ================================================ FILE: cmd/templ/generatecmd/test-eventhandler/eventhandler_test.go ================================================ package testeventhandler import ( "context" "errors" "fmt" "go/scanner" "go/token" "io" "log/slog" "os" "testing" "github.com/a-h/templ/cmd/templ/generatecmd" "github.com/a-h/templ/generator" "github.com/fsnotify/fsnotify" "github.com/google/go-cmp/cmp" ) // extractErrorList unwraps errors until it finds a scanner.ErrorList func extractErrorList(err error) (scanner.ErrorList, bool) { if err == nil { return nil, false } if list, ok := err.(scanner.ErrorList); ok { return list, true } return extractErrorList(errors.Unwrap(err)) } func TestErrorLocationMapping(t *testing.T) { tests := []struct { name string rawFileName string errorPositions []token.Position }{ { name: "single error outputs location in srcFile", rawFileName: "single_error.templ.error", errorPositions: []token.Position{ {Offset: 46, Line: 3, Column: 20}, }, }, { name: "multiple errors all output locations in srcFile", rawFileName: "multiple_errors.templ.error", errorPositions: []token.Position{ {Offset: 41, Line: 3, Column: 15}, {Offset: 101, Line: 7, Column: 22}, {Offset: 126, Line: 10, Column: 1}, }, }, } slog := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) var fw generatecmd.FileWriterFunc fseh := generatecmd.NewFSEventHandler(slog, ".", false, []generator.GenerateOpt{}, false, false, fw, false) for _, test := range tests { t.Run(test.name, func(t *testing.T) { // The raw files cannot end in .templ because they will cause the generator to fail. Instead, // we create a tmp file that ends in .templ only for the duration of the test. rawFile, err := os.Open(test.rawFileName) if err != nil { t.Fatalf("Failed to open file %s: %v", test.rawFileName, err) } defer func() { if err = rawFile.Close(); err != nil { t.Fatalf("Failed to close raw file %s: %v", test.rawFileName, err) } }() file, err := os.CreateTemp("", fmt.Sprintf("*%s.templ", test.rawFileName)) if err != nil { t.Fatalf("Failed to create a tmp file at %s: %v", file.Name(), err) } tempFileName := file.Name() defer func() { _ = file.Close() if err := os.Remove(tempFileName); err != nil { t.Logf("Warning: Failed to remove tmp file %s: %v", tempFileName, err) } }() if _, err = io.Copy(file, rawFile); err != nil { t.Fatalf("Failed to copy contents from raw file %s to tmp %s: %v", test.rawFileName, tempFileName, err) } // Ensure file is synced to disk and file pointer is at the beginning if err = file.Sync(); err != nil { t.Fatalf("Failed to sync file: %v", err) } event := fsnotify.Event{Name: tempFileName, Op: fsnotify.Write} _, err = fseh.HandleEvent(context.Background(), event) if err == nil { t.Fatal("Expected an error but none was thrown") } list, ok := extractErrorList(err) if !ok { t.Fatal("Failed to extract ErrorList from error") } if len(list) != len(test.errorPositions) { t.Fatalf("Expected %d errors but got %d", len(test.errorPositions), len(list)) } for i, err := range list { expected := test.errorPositions[i] expected.Filename = tempFileName if diff := cmp.Diff(expected, err.Pos); diff != "" { t.Errorf("Error position mismatch (-expected +actual):\n%s", diff) } } }) } } ================================================ FILE: cmd/templ/generatecmd/test-eventhandler/multiple_errors.templ.error ================================================ package testeventhandler func invalid(a: string) string { return "foo" } templ multipleError(a: string) { } l ================================================ FILE: cmd/templ/generatecmd/test-eventhandler/single_error.templ.error ================================================ package testeventhandler templ singleError(a: string) { } ================================================ FILE: cmd/templ/generatecmd/testwatch/generate_test.go ================================================ package testwatch import ( "bufio" "bytes" "context" "embed" "errors" "fmt" "io" "net" "net/http" "os" "path/filepath" "strconv" "strings" "testing" "time" "github.com/a-h/templ/cmd/templ/generatecmd" "github.com/a-h/templ/cmd/templ/generatecmd/modcheck" "github.com/a-h/templ/internal/htmlfind" "golang.org/x/net/html" "golang.org/x/sync/errgroup" ) //go:embed testdata/* var testdata embed.FS func createTestProject(moduleRoot string) (dir string, err error) { dir, err = os.MkdirTemp("", "templ_watch_test_*") if err != nil { return dir, fmt.Errorf("failed to make test dir: %w", err) } files, err := testdata.ReadDir("testdata") if err != nil { return dir, fmt.Errorf("failed to read embedded dir: %w", err) } for _, file := range files { src := filepath.Join("testdata", file.Name()) data, err := testdata.ReadFile(src) if err != nil { return dir, fmt.Errorf("failed to read file: %w", err) } target := filepath.Join(dir, file.Name()) if file.Name() == "go.mod.embed" { data = bytes.ReplaceAll(data, []byte("{moduleRoot}"), []byte(moduleRoot)) target = filepath.Join(dir, "go.mod") } err = os.WriteFile(target, data, 0660) if err != nil { return dir, fmt.Errorf("failed to copy file: %w", err) } } return dir, nil } func replaceInFile(name, src, tgt string) error { data, err := os.ReadFile(name) if err != nil { return err } updated := strings.ReplaceAll(string(data), src, tgt) return os.WriteFile(name, []byte(updated), 0660) } func getPort() (port int, err error) { var a *net.TCPAddr a, err = net.ResolveTCPAddr("tcp", "localhost:0") if err != nil { return 0, fmt.Errorf("failed to resolve TCP address: %w", err) } l, err := net.ListenTCP("tcp", a) if err != nil { return 0, fmt.Errorf("failed to listen on TCP: %w", err) } return l.Addr().(*net.TCPAddr).Port, l.Close() } func getHTML(url string) (n *html.Node, err error) { resp, err := http.Get(url) if err != nil { return nil, fmt.Errorf("failed to get %q: %w", url, err) } defer func() { _ = resp.Body.Close() }() return html.Parse(resp.Body) } func TestCanAccessDirect(t *testing.T) { if testing.Short() { return } args, teardown, err := Setup(false) if err != nil { t.Fatalf("failed to setup test: %v", err) } defer teardown(t) // Assert. doc, err := getHTML(args.AppURL) if err != nil { t.Fatalf("failed to read HTML: %v", err) } countElements := htmlfind.All(doc, htmlfind.Element("div", htmlfind.Attr("data-testid", "count"))) if len(countElements) != 1 { t.Fatalf("expected 1 count element, got %d", len(countElements)) } countText := countElements[0].FirstChild.Data actualCount, err := strconv.Atoi(countText) if err != nil { t.Fatalf("got count %q instead of integer", countText) } if actualCount < 1 { t.Errorf("expected count >= 1, got %d", actualCount) } } func TestCanAccessViaProxy(t *testing.T) { if testing.Short() { return } args, teardown, err := Setup(false) if err != nil { t.Fatalf("failed to setup test: %v", err) } defer teardown(t) // Assert. doc, err := getHTML(args.ProxyURL) if err != nil { t.Fatalf("failed to read HTML: %v", err) } countElements := htmlfind.All(doc, htmlfind.Element("div", htmlfind.Attr("data-testid", "count"))) if len(countElements) != 1 { t.Fatalf("expected 1 count element, got %d", len(countElements)) } countText := countElements[0].FirstChild.Data actualCount, err := strconv.Atoi(countText) if err != nil { t.Fatalf("got count %q instead of integer", countText) } if actualCount < 1 { t.Errorf("expected count >= 1, got %d", actualCount) } } type Event struct { Type string Data string } func readSSE(ctx context.Context, url string, sse chan<- Event) (err error) { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return err } req.Header.Set("Cache-Control", "no-cache") req.Header.Set("Accept", "text/event-stream") req.Header.Set("Connection", "keep-alive") client := &http.Client{} resp, err := client.Do(req) if err != nil { return err } var e Event scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { line := scanner.Text() if line == "" { sse <- e e = Event{} continue } if strings.HasPrefix(line, "event: ") { e.Type = line[len("event: "):] } if strings.HasPrefix(line, "data: ") { e.Data = line[len("data: "):] } } return scanner.Err() } func TestFileModificationsResultInSSEWithGzip(t *testing.T) { if testing.Short() { return } args, teardown, err := Setup(false) if err != nil { t.Fatalf("failed to setup test: %v", err) } defer teardown(t) // Start the SSE check. events := make(chan Event) var eventsErr error go func() { eventsErr = readSSE(context.Background(), fmt.Sprintf("%s/_templ/reload/events", args.ProxyURL), events) }() // Assert data is expected. doc, err := getHTML(args.ProxyURL) if err != nil { t.Fatalf("failed to read HTML: %v", err) } modified := htmlfind.All(doc, htmlfind.Element("div", htmlfind.Attr("data-testid", "modification"))) if len(modified) != 1 { t.Fatalf("expected 1 modification element, got %d", len(modified)) } if text := modified[0].FirstChild.Data; text != "Original" { t.Errorf("expected %q, got %q", "Original", text) } // Change file. templFile := filepath.Join(args.AppDir, "templates.templ") err = replaceInFile(templFile, `| File | ||||
|---|---|---|---|---|
| { uri } | Mapping | Source Map | Templ | Go |
| File | ||||
|---|---|---|---|---|
| ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(uri) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/templ/lspcmd/httpdebug/list.templ`, Line: 14, Col: 13} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " | Mapping | Source Map | Templ | Go |