diff options
Diffstat (limited to 'vendor/github.com/a-h/templ')
30 files changed, 2508 insertions, 0 deletions
diff --git a/vendor/github.com/a-h/templ/.dockerignore b/vendor/github.com/a-h/templ/.dockerignore new file mode 100644 index 0000000..17896fe --- /dev/null +++ b/vendor/github.com/a-h/templ/.dockerignore @@ -0,0 +1,3 @@ +.git +Dockerfile +.dockerignore diff --git a/vendor/github.com/a-h/templ/.gitignore b/vendor/github.com/a-h/templ/.gitignore new file mode 100644 index 0000000..0338eda --- /dev/null +++ b/vendor/github.com/a-h/templ/.gitignore @@ -0,0 +1,28 @@ +# 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* + +# Go workspace. +go.work diff --git a/vendor/github.com/a-h/templ/.goreleaser.yaml b/vendor/github.com/a-h/templ/.goreleaser.yaml new file mode 100644 index 0000000..456187c --- /dev/null +++ b/vendor/github.com/a-h/templ/.goreleaser.yaml @@ -0,0 +1,72 @@ +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:' diff --git a/vendor/github.com/a-h/templ/.ignore b/vendor/github.com/a-h/templ/.ignore new file mode 100644 index 0000000..21cb25e --- /dev/null +++ b/vendor/github.com/a-h/templ/.ignore @@ -0,0 +1,7 @@ +*_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 diff --git a/vendor/github.com/a-h/templ/.version b/vendor/github.com/a-h/templ/.version new file mode 100644 index 0000000..baee64f --- /dev/null +++ b/vendor/github.com/a-h/templ/.version @@ -0,0 +1 @@ +0.2.747
\ No newline at end of file diff --git a/vendor/github.com/a-h/templ/CODE_OF_CONDUCT.md b/vendor/github.com/a-h/templ/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..08340d3 --- /dev/null +++ b/vendor/github.com/a-h/templ/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# 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. diff --git a/vendor/github.com/a-h/templ/CONTRIBUTING.md b/vendor/github.com/a-h/templ/CONTRIBUTING.md new file mode 100644 index 0000000..e98d31f --- /dev/null +++ b/vendor/github.com/a-h/templ/CONTRIBUTING.md @@ -0,0 +1,244 @@ +# 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: `<!-- single line comment -->`, + expected: HTMLComment{ + Contents: " single line comment ", + }, + }, + { + name: "comment - no whitespace", + input: `<!--no whitespace between sequence open and close-->`, + expected: HTMLComment{ + Contents: "no whitespace between sequence open and close", + }, + }, + { + name: "comment - multiline", + input: `<!-- multiline + comment + -->`, + expected: HTMLComment{ + Contents: ` multiline + comment + `, + }, + }, + { + name: "comment - with tag", + input: `<!-- <p class="test">tag</p> -->`, + expected: HTMLComment{ + Contents: ` <p class="test">tag</p> `, + }, + }, + { + name: "comments can contain tags", + input: `<!-- <div> hello world </div> -->`, + expected: HTMLComment{ + Contents: ` <div> hello world </div> `, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + input := parse.NewInput(tt.input) + result, ok, err := htmlComment.Parse(input) + if err != nil { + t.Fatalf("parser error: %v", err) + } + if !ok { + t.Fatalf("failed to parse at %d", input.Index()) + } + if diff := cmp.Diff(tt.expected, result); diff != "" { + t.Errorf(diff) + } + }) + } +} +``` + +Alongside each success test, is a similar test to check that invalid syntax is detected. + +```go +func TestHTMLCommentParserErrors(t *testing.T) { + var tests = []struct { + name string + input string + expected error + }{ + { + name: "unclosed HTML comment", + input: `<!-- unclosed HTML comment`, + expected: parse.Error("expected end comment literal '-->' not found", + parse.Position{ + Index: 26, + Line: 0, + Col: 26, + }), + }, + { + name: "comment in comment", + input: `<!-- <-- other --> -->`, + expected: parse.Error("comment contains invalid sequence '--'", parse.Position{ + Index: 8, + Line: 0, + Col: 8, + }), + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + input := parse.NewInput(tt.input) + _, _, err := htmlComment.Parse(input) + if diff := cmp.Diff(tt.expected, err); diff != "" { + t.Error(diff) + } + }) + } +} +``` + +### Generator + +The generator takes the object model and writes out Go code that produces the expected output. Any changes to Go code output by templ are made in this area. + +Testing of the generator is carried out by creating a templ file, and a matching expected output file. + +For example, `./generator/test-a-href` contains a templ file of: + +```templ +package testahref + +templ render() { + <a href="javascript:alert('unaffected');">Ignored</a> + <a href={ templ.URL("javascript:alert('should be sanitized')") }>Sanitized</a> + <a href={ templ.SafeURL("javascript:alert('should not be sanitized')") }>Unsanitized</a> +} +``` + +It also contains an expected output file. + +```html +<a href="javascript:alert('unaffected');">Ignored</a> +<a href="about:invalid#TemplFailedSanitizationURL">Sanitized</a> +<a href="javascript:alert('should not be sanitized')">Unsanitized</a> +``` + +These tests contribute towards the code coverage metrics by building an instrumented test CLI program. See the `test-cover` task in the `README.md` file. + +### CLI + +The command line interface for templ is used to generate Go code from templ files, format templ files, and run the LSP. + +The code for this is at `./cmd/templ`. + +Testing of the templ command line is done with unit tests to check the argument parsing. + +The `templ generate` command is tested by generating templ files in the project, and testing that the expected output HTML is present. + +### Runtime + +The runtime is used by generated code, and by template authors, to serve template content over HTTP, and to carry out various operations. + +It is in the root directory of the project at `./runtime.go`. The runtime is unit tested, as well as being tested as part of the `generate` tests. + +### LSP + +The LSP is structured within the command line interface, and proxies commands through to the `gopls` LSP. + +### Docs + +The docs are a Docusaurus project at `./docs`. + +## Coding + +### 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: + +* `install-snapshot` - this builds the templ CLI and installs it into `~/bin`. Ensure that this is in your path. +* `test` - this regenerates all templates, and runs the unit tests. +* `fmt` - run the `gofmt` tool to format all Go code. +* `lint` - run the same linting as run in the CI process. +* `docs-run` - run the Docusaurus documentation site. + +### Commit messages + +The project using https://www.conventionalcommits.org/en/v1.0.0/ + +Examples: + +* `feat: support Go comments in templates, fixes #234"` + +### 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 `fmt` and `lint` build tasks to format and lint your code before submitting a PR. + diff --git a/vendor/github.com/a-h/templ/LICENSE b/vendor/github.com/a-h/templ/LICENSE new file mode 100644 index 0000000..15e6fb8 --- /dev/null +++ b/vendor/github.com/a-h/templ/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Adrian Hesketh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/a-h/templ/README.md b/vendor/github.com/a-h/templ/README.md new file mode 100644 index 0000000..e3087f0 --- /dev/null +++ b/vendor/github.com/a-h/templ/README.md @@ -0,0 +1,171 @@ +![templ](https://github.com/a-h/templ/raw/main/templ.png) + +## An HTML templating language for Go that has great developer tooling. + +![templ](ide-demo.gif) + + +## Documentation + +See user documentation at https://templ.guide + +<p align="center"> +<a href="https://pkg.go.dev/github.com/a-h/templ"><img src="https://pkg.go.dev/badge/github.com/a-h/templ.svg" alt="Go Reference" /></a> +<a href="https://xcfile.dev"><img src="https://xcfile.dev/badge.svg" alt="xc compatible" /></a> +<a href="https://raw.githack.com/wiki/a-h/templ/coverage.html"><img src="https://github.com/a-h/templ/wiki/coverage.svg" alt="Go Coverage" /></a> +<a href="https://goreportcard.com/report/github.com/a-h/templ"><img src="https://goreportcard.com/badge/github.com/a-h/templ" alt="Go Report Card" /></a< +</p> + +## Tasks + +### build + +Build a local version. + +```sh +go run ./get-version > .version +cd cmd/templ +go build +``` + +### nix-update-gomod2nix + +```sh +gomod2nix +``` + +### install-snapshot + +Build and install current version. + +```sh +# Remove templ from the non-standard ~/bin/templ path +# that this command previously used. +rm -f ~/bin/templ +# Clear LSP logs. +rm -f cmd/templ/lspcmd/*.txt +# Update version. +go run ./get-version > .version +# Install to $GOPATH/bin or $HOME/go/bin +cd cmd/templ && go install +``` + +### build-snapshot + +Use goreleaser to build the command line binary using goreleaser. + +```sh +goreleaser build --snapshot --clean +``` + +### generate + +Run templ generate using local version. + +```sh +go run ./cmd/templ generate -include-version=false +``` + +### test + +Run Go tests. + +```sh +go run ./get-version > .version +go run ./cmd/templ generate -include-version=false +go test ./... +``` + +### test-short + +Run Go tests. + +```sh +go run ./get-version > .version +go run ./cmd/templ generate -include-version=false +go test ./... -short +``` + +### test-cover + +Run Go tests. + +```sh +# Create test profile directories. +mkdir -p coverage/fmt +mkdir -p coverage/generate +mkdir -p coverage/version +mkdir -p coverage/unit +# Build the test binary. +go build -cover -o ./coverage/templ-cover ./cmd/templ +# Run the covered generate command. +GOCOVERDIR=coverage/fmt ./coverage/templ-cover fmt . +GOCOVERDIR=coverage/generate ./coverage/templ-cover generate -include-version=false +GOCOVERDIR=coverage/version ./coverage/templ-cover version +# Run the unit tests. +go test -cover ./... -coverpkg ./... -args -test.gocoverdir="$PWD/coverage/unit" +# Display the combined percentage. +go tool covdata percent -i=./coverage/fmt,./coverage/generate,./coverage/version,./coverage/unit +# Generate a text coverage profile for tooling to use. +go tool covdata textfmt -i=./coverage/fmt,./coverage/generate,./coverage/version,./coverage/unit -o coverage.out +# Print total +go tool cover -func coverage.out | grep total +``` + +### test-cover-watch + +```sh +gotestsum --watch -- -coverprofile=coverage.out +``` + +### benchmark + +Run benchmarks. + +```sh +go run ./cmd/templ generate -include-version=false && go test ./... -bench=. -benchmem +``` + +### fmt + +Format all Go and templ code. + +```sh +gofmt -s -w . +go run ./cmd/templ fmt . +``` + +### lint + +```sh +golangci-lint run --verbose +``` + +### push-release-tag + +Push a semantic version number to Github to trigger the release process. + +```sh +./push-tag.sh +``` + +### docs-run + +Run the development server. + +Directory: docs + +```sh +npm run start +``` + +### docs-build + +Build production docs site. + +Directory: docs + +```sh +npm run build +``` + diff --git a/vendor/github.com/a-h/templ/SECURITY.md b/vendor/github.com/a-h/templ/SECURITY.md new file mode 100644 index 0000000..8241f55 --- /dev/null +++ b/vendor/github.com/a-h/templ/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Supported Versions + +The latest version of templ is supported. + +## Reporting a Vulnerability + +Use the "Security" tab in Github and fill out the "Report a vulnerability" form. diff --git a/vendor/github.com/a-h/templ/cosign.pub b/vendor/github.com/a-h/templ/cosign.pub new file mode 100644 index 0000000..9d7967b --- /dev/null +++ b/vendor/github.com/a-h/templ/cosign.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqHp75uAj8XqKrLO2YvY0M2EddckH +evQnNAj+0GmBptqdf3NJcUCjL6w4z2Ikh/Zb8lh6b13akAwO/dJQaMLoMA== +-----END PUBLIC KEY----- diff --git a/vendor/github.com/a-h/templ/flake.lock b/vendor/github.com/a-h/templ/flake.lock new file mode 100644 index 0000000..af4e370 --- /dev/null +++ b/vendor/github.com/a-h/templ/flake.lock @@ -0,0 +1,140 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "gomod2nix": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1717050755, + "narHash": "sha256-C9IEHABulv2zEDFA+Bf0E1nmfN4y6MIUe5eM2RCrDC0=", + "owner": "nix-community", + "repo": "gomod2nix", + "rev": "31b6d2e40b36456e792cd6cf50d5a8ddd2fa59a1", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "gomod2nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1720096762, + "narHash": "sha256-KvpJIWxTNuaSpN2L/9TmTlEhlwxEnzJ1vCpEcfK/4mQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "638369f687471823770f6d3093f1721dc7b8c897", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "gitignore": "gitignore", + "gomod2nix": "gomod2nix", + "nixpkgs": "nixpkgs", + "xc": "xc" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "xc": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1717601811, + "narHash": "sha256-+XQvDRXpzjBdZI3JGKP6SAOYXM+JSEbWL5kqtCwRJXE=", + "owner": "joerdav", + "repo": "xc", + "rev": "f8e8e658978d6c9fe49c27b684ca7375a74deef1", + "type": "github" + }, + "original": { + "owner": "joerdav", + "repo": "xc", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/vendor/github.com/a-h/templ/flake.nix b/vendor/github.com/a-h/templ/flake.nix new file mode 100644 index 0000000..fd8a238 --- /dev/null +++ b/vendor/github.com/a-h/templ/flake.nix @@ -0,0 +1,93 @@ +{ + description = "templ"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/release-24.05"; + gomod2nix = { + url = "github:nix-community/gomod2nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + gitignore = { + url = "github:hercules-ci/gitignore.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + xc = { + url = "github:joerdav/xc"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, gomod2nix, gitignore, xc }: + let + allSystems = [ + "x86_64-linux" # 64-bit Intel/AMD Linux + "aarch64-linux" # 64-bit ARM Linux + "x86_64-darwin" # 64-bit Intel macOS + "aarch64-darwin" # 64-bit ARM macOS + ]; + forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f { + inherit system; + pkgs = import nixpkgs { inherit system; }; + }); + in + { + packages = forAllSystems ({ system, pkgs, ... }: + let + buildGoApplication = gomod2nix.legacyPackages.${system}.buildGoApplication; + in + rec { + default = templ; + + templ = buildGoApplication { + name = "templ"; + src = gitignore.lib.gitignoreSource ./.; + # Update to latest Go version when https://nixpk.gs/pr-tracker.html?pr=324123 is backported to release-24.05. + go = pkgs.go; + # Must be added due to bug https://github.com/nix-community/gomod2nix/issues/120 + pwd = ./.; + subPackages = [ "cmd/templ" ]; + CGO_ENABLED = 0; + flags = [ + "-trimpath" + ]; + ldflags = [ + "-s" + "-w" + "-extldflags -static" + ]; + }; + }); + + # `nix develop` provides a shell containing development tools. + devShell = forAllSystems ({ system, pkgs }: + pkgs.mkShell { + buildInputs = with pkgs; [ + (golangci-lint.override { buildGoModule = buildGo121Module; }) + cosign # Used to sign container images. + esbuild # Used to package JS examples. + go_1_21 + gomod2nix.legacyPackages.${system}.gomod2nix + gopls + goreleaser + gotestsum + ko # Used to build Docker images. + nodejs # Used to build templ-docs. + xc.packages.${system}.xc + ]; + }); + + # This flake outputs an overlay that can be used to add templ and + # templ-docs to nixpkgs as per https://templ.guide/quick-start/installation/#nix + # + # Example usage: + # + # nixpkgs.overlays = [ + # inputs.templ.overlays.default + # ]; + overlays.default = final: prev: { + templ = self.packages.${final.stdenv.system}.templ; + templ-docs = self.packages.${final.stdenv.system}.templ-docs; + }; + }; +} + diff --git a/vendor/github.com/a-h/templ/flush.go b/vendor/github.com/a-h/templ/flush.go new file mode 100644 index 0000000..56d7d3a --- /dev/null +++ b/vendor/github.com/a-h/templ/flush.go @@ -0,0 +1,36 @@ +package templ + +import ( + "context" + "io" +) + +// Flush flushes the output buffer after all its child components have been rendered. +func Flush() FlushComponent { + return FlushComponent{} +} + +type FlushComponent struct { +} + +type flusherError interface { + Flush() error +} + +type flusher interface { + Flush() +} + +func (f FlushComponent) Render(ctx context.Context, w io.Writer) (err error) { + if err = GetChildren(ctx).Render(ctx, w); err != nil { + return err + } + switch w := w.(type) { + case flusher: + w.Flush() + return nil + case flusherError: + return w.Flush() + } + return nil +} diff --git a/vendor/github.com/a-h/templ/gomod2nix.toml b/vendor/github.com/a-h/templ/gomod2nix.toml new file mode 100644 index 0000000..d572a5f --- /dev/null +++ b/vendor/github.com/a-h/templ/gomod2nix.toml @@ -0,0 +1,90 @@ +schema = 3 + +[mod] + [mod."github.com/PuerkitoBio/goquery"] + version = "v1.8.1" + hash = "sha256-z2RaB8PVPEzSJdMUfkfNjT616yXWTjW2gkhNOh989ZU=" + [mod."github.com/a-h/htmlformat"] + version = "v0.0.0-20231108124658-5bd994fe268e" + hash = "sha256-YSl9GsXhc0L2oKGZLwwjUtpe5W6ra6kk74zvQdsDCMU=" + [mod."github.com/a-h/parse"] + version = "v0.0.0-20240121214402-3caf7543159a" + hash = "sha256-ee/g6xwwhtF7vVt3griUSh96Kz4z0hM5/tpXxHW6PZk=" + [mod."github.com/a-h/pathvars"] + version = "v0.0.14" + hash = "sha256-2NytUpcO0zbzE5XunCLcK3jDqxYzmyb3WqtYDEudAYg=" + [mod."github.com/a-h/protocol"] + version = "v0.0.0-20240704131721-1e461c188041" + hash = "sha256-KSw8m+kVIubEi+nuS3dMdBw2ZZTlmcKD/hGbVRFaE5Q=" + [mod."github.com/andybalholm/brotli"] + version = "v1.1.0" + hash = "sha256-njLViV4v++ZdgOWGWzlvkefuFvA/nkugl3Ta/h1nu/0=" + [mod."github.com/andybalholm/cascadia"] + version = "v1.3.1" + hash = "sha256-M0u22DXSeXUaYtl1KoW1qWL46niFpycFkraCEQ/luYA=" + [mod."github.com/cenkalti/backoff/v4"] + version = "v4.3.0" + hash = "sha256-wfVjNZsGG1WoNC5aL+kdcy6QXPgZo4THAevZ1787md8=" + [mod."github.com/cli/browser"] + version = "v1.3.0" + hash = "sha256-06hcvQeOEm31clxkTuZ8ts8ZtdNKY575EsM1osRVpLg=" + [mod."github.com/fatih/color"] + version = "v1.16.0" + hash = "sha256-Aq/SM28aPJVzvapllQ64R/DM4aZ5CHPewcm/AUJPyJQ=" + [mod."github.com/fsnotify/fsnotify"] + version = "v1.7.0" + hash = "sha256-MdT2rQyQHspPJcx6n9ozkLbsktIOJutOqDuKpNAtoZY=" + [mod."github.com/google/go-cmp"] + version = "v0.6.0" + hash = "sha256-qgra5jze4iPGP0JSTVeY5qV5AvEnEu39LYAuUCIkMtg=" + [mod."github.com/mattn/go-colorable"] + version = "v0.1.13" + hash = "sha256-qb3Qbo0CELGRIzvw7NVM1g/aayaz4Tguppk9MD2/OI8=" + [mod."github.com/mattn/go-isatty"] + version = "v0.0.20" + hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ=" + [mod."github.com/natefinch/atomic"] + version = "v1.0.1" + hash = "sha256-fbOVHCwRNI8PFjC4o0YXpKZO0JU2aWTfH5c7WXXKMHg=" + [mod."github.com/rs/cors"] + version = "v1.11.0" + hash = "sha256-hF25bVehtWCQsxiOfLuL4Hv8NKVunEqLPk/Vcuheha0=" + [mod."github.com/segmentio/asm"] + version = "v1.2.0" + hash = "sha256-zbNuKxNrUDUc6IlmRQNuJQzVe5Ol/mqp7srDg9IMMqs=" + [mod."github.com/segmentio/encoding"] + version = "v0.4.0" + hash = "sha256-4pWI9eTZRRDP9kO8rG6vbLCtBVVRLtbCJKd0Z2+8JoU=" + [mod."github.com/stretchr/testify"] + version = "v1.8.4" + hash = "sha256-MoOmRzbz9QgiJ+OOBo5h5/LbilhJfRUryvzHJmXAWjo=" + [mod."go.lsp.dev/jsonrpc2"] + version = "v0.10.0" + hash = "sha256-RbRsMYVBLR7ZDHHGMooycrkdbIauMXkQjVOGP7ggSgM=" + [mod."go.lsp.dev/pkg"] + version = "v0.0.0-20210717090340-384b27a52fb2" + hash = "sha256-TxS0Iqe1wbIaFe7MWZJRQdgqhKE8i8CggaGSV9zU1Vg=" + [mod."go.lsp.dev/uri"] + version = "v0.3.0" + hash = "sha256-jGP0N7Gf+bql5oJraUo33sXqWg7AKOTj0D8b4paV4dc=" + [mod."go.uber.org/multierr"] + version = "v1.11.0" + hash = "sha256-Lb6rHHfR62Ozg2j2JZy3MKOMKdsfzd1IYTR57r3Mhp0=" + [mod."go.uber.org/zap"] + version = "v1.27.0" + hash = "sha256-8655KDrulc4Das3VRduO9MjCn8ZYD5WkULjCvruaYsU=" + [mod."golang.org/x/mod"] + version = "v0.17.0" + hash = "sha256-CLaPeF6uTFuRDv4oHwOQE6MCMvrzkUjWN3NuyywZjKU=" + [mod."golang.org/x/net"] + version = "v0.24.0" + hash = "sha256-w1c21ljta5wNIyel9CSIn/crPzwOCRofNKhqmfs4aEQ=" + [mod."golang.org/x/sync"] + version = "v0.3.0" + hash = "sha256-bCJKLvwExhYacH2ZrWlZ38lr1d6oNenNt2m1QqDCs0o=" + [mod."golang.org/x/sys"] + version = "v0.21.0" + hash = "sha256-gapzPWuEqY36V6W2YhIDYR49sEvjJRd7bSuf9K1f4JY=" + [mod."golang.org/x/tools"] + version = "v0.13.0" + hash = "sha256-OCgLOwia8fNHxfdogXVApf0/qK6jE2ukegOx7lkOzfo=" diff --git a/vendor/github.com/a-h/templ/handler.go b/vendor/github.com/a-h/templ/handler.go new file mode 100644 index 0000000..a28d561 --- /dev/null +++ b/vendor/github.com/a-h/templ/handler.go @@ -0,0 +1,102 @@ +package templ + +import "net/http" + +// ComponentHandler is a http.Handler that renders components. +type ComponentHandler struct { + Component Component + Status int + ContentType string + ErrorHandler func(r *http.Request, err error) http.Handler + StreamResponse bool +} + +const componentHandlerErrorMessage = "templ: failed to render template" + +func (ch *ComponentHandler) ServeHTTPBuffered(w http.ResponseWriter, r *http.Request) { + // Since the component may error, write to a buffer first. + // This prevents partial responses from being written to the client. + buf := GetBuffer() + defer ReleaseBuffer(buf) + err := ch.Component.Render(r.Context(), buf) + if err != nil { + if ch.ErrorHandler != nil { + w.Header().Set("Content-Type", ch.ContentType) + ch.ErrorHandler(r, err).ServeHTTP(w, r) + return + } + http.Error(w, componentHandlerErrorMessage, http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", ch.ContentType) + if ch.Status != 0 { + w.WriteHeader(ch.Status) + } + // Ignore write error like http.Error() does, because there is + // no way to recover at this point. + _, _ = w.Write(buf.Bytes()) +} + +func (ch *ComponentHandler) ServeHTTPStreamed(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", ch.ContentType) + if ch.Status != 0 { + w.WriteHeader(ch.Status) + } + if err := ch.Component.Render(r.Context(), w); err != nil { + if ch.ErrorHandler != nil { + w.Header().Set("Content-Type", ch.ContentType) + ch.ErrorHandler(r, err).ServeHTTP(w, r) + return + } + http.Error(w, componentHandlerErrorMessage, http.StatusInternalServerError) + } +} + +// ServeHTTP implements the http.Handler interface. +func (ch ComponentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if ch.StreamResponse { + ch.ServeHTTPStreamed(w, r) + return + } + ch.ServeHTTPBuffered(w, r) +} + +// Handler creates a http.Handler that renders the template. +func Handler(c Component, options ...func(*ComponentHandler)) *ComponentHandler { + ch := &ComponentHandler{ + Component: c, + ContentType: "text/html; charset=utf-8", + } + for _, o := range options { + o(ch) + } + return ch +} + +// WithStatus sets the HTTP status code returned by the ComponentHandler. +func WithStatus(status int) func(*ComponentHandler) { + return func(ch *ComponentHandler) { + ch.Status = status + } +} + +// WithContentType sets the Content-Type header returned by the ComponentHandler. +func WithContentType(contentType string) func(*ComponentHandler) { + return func(ch *ComponentHandler) { + ch.ContentType = contentType + } +} + +// WithErrorHandler sets the error handler used if rendering fails. +func WithErrorHandler(eh func(r *http.Request, err error) http.Handler) func(*ComponentHandler) { + return func(ch *ComponentHandler) { + ch.ErrorHandler = eh + } +} + +// WithStreaming sets the ComponentHandler to stream the response instead of buffering it. +func WithStreaming() func(*ComponentHandler) { + return func(ch *ComponentHandler) { + ch.StreamResponse = true + } +} diff --git a/vendor/github.com/a-h/templ/ide-demo.gif b/vendor/github.com/a-h/templ/ide-demo.gif Binary files differnew file mode 100644 index 0000000..e35fd68 --- /dev/null +++ b/vendor/github.com/a-h/templ/ide-demo.gif diff --git a/vendor/github.com/a-h/templ/jsonscript.go b/vendor/github.com/a-h/templ/jsonscript.go new file mode 100644 index 0000000..6e88174 --- /dev/null +++ b/vendor/github.com/a-h/templ/jsonscript.go @@ -0,0 +1,85 @@ +package templ + +import ( + "context" + "encoding/json" + "fmt" + "io" +) + +var _ Component = JSONScriptElement{} + +// JSONScript renders a JSON object inside a script element. +// e.g. <script type="application/json">{"foo":"bar"}</script> +func JSONScript(id string, data any) JSONScriptElement { + return JSONScriptElement{ + ID: id, + Type: "application/json", + Data: data, + Nonce: GetNonce, + } +} + +// WithType sets the value of the type attribute of the script element. +func (j JSONScriptElement) WithType(t string) JSONScriptElement { + j.Type = t + return j +} + +// WithNonceFromString sets the value of the nonce attribute of the script element to the given string. +func (j JSONScriptElement) WithNonceFromString(nonce string) JSONScriptElement { + j.Nonce = func(context.Context) string { + return nonce + } + return j +} + +// WithNonceFrom sets the value of the nonce attribute of the script element to the value returned by the given function. +func (j JSONScriptElement) WithNonceFrom(f func(context.Context) string) JSONScriptElement { + j.Nonce = f + return j +} + +type JSONScriptElement struct { + // ID of the element in the DOM. + ID string + // Type of the script element, defaults to "application/json". + Type string + // Data that will be encoded as JSON. + Data any + // Nonce is a function that returns a CSP nonce. + // Defaults to CSPNonceFromContext. + // See https://content-security-policy.com/nonce for more information. + Nonce func(ctx context.Context) string +} + +func (j JSONScriptElement) Render(ctx context.Context, w io.Writer) (err error) { + if _, err = io.WriteString(w, "<script"); err != nil { + return err + } + if j.ID != "" { + if _, err = fmt.Fprintf(w, " id=\"%s\"", EscapeString(j.ID)); err != nil { + return err + } + } + if j.Type != "" { + if _, err = fmt.Fprintf(w, " type=\"%s\"", EscapeString(j.Type)); err != nil { + return err + } + } + if nonce := j.Nonce(ctx); nonce != "" { + if _, err = fmt.Fprintf(w, " nonce=\"%s\"", EscapeString(nonce)); err != nil { + return err + } + } + if _, err = io.WriteString(w, ">"); err != nil { + return err + } + if err = json.NewEncoder(w).Encode(j.Data); err != nil { + return err + } + if _, err = io.WriteString(w, "</script>"); err != nil { + return err + } + return nil +} diff --git a/vendor/github.com/a-h/templ/jsonstring.go b/vendor/github.com/a-h/templ/jsonstring.go new file mode 100644 index 0000000..425e4e8 --- /dev/null +++ b/vendor/github.com/a-h/templ/jsonstring.go @@ -0,0 +1,14 @@ +package templ + +import ( + "encoding/json" +) + +// JSONString returns a JSON encoded string of v. +func JSONString(v any) (string, error) { + b, err := json.Marshal(v) + if err != nil { + return "", err + } + return string(b), nil +} diff --git a/vendor/github.com/a-h/templ/once.go b/vendor/github.com/a-h/templ/once.go new file mode 100644 index 0000000..7860ab8 --- /dev/null +++ b/vendor/github.com/a-h/templ/once.go @@ -0,0 +1,64 @@ +package templ + +import ( + "context" + "io" + "sync/atomic" +) + +// onceHandleIndex is used to identify unique once handles in a program run. +var onceHandleIndex int64 + +type OnceOpt func(*OnceHandle) + +// WithOnceComponent sets the component to be rendered once per context. +// This can be used instead of setting the children of the `Once` method, +// for example, if creating a code component outside of a templ HTML template. +func WithComponent(c Component) OnceOpt { + return func(o *OnceHandle) { + o.c = c + } +} + +// NewOnceHandle creates a OnceHandle used to ensure that the children of its +// `Once` method are only rendered once per context. +func NewOnceHandle(opts ...OnceOpt) *OnceHandle { + oh := &OnceHandle{ + id: atomic.AddInt64(&onceHandleIndex, 1), + } + for _, opt := range opts { + opt(oh) + } + return oh +} + +// OnceHandle is used to ensure that the children of its `Once` method are are only +// rendered once per context. +type OnceHandle struct { + // id is used to identify which instance of the OnceHandle is being used. + // The OnceHandle can't be an empty struct, because: + // + // | Two distinct zero-size variables may + // | have the same address in memory + // + // https://go.dev/ref/spec#Size_and_alignment_guarantees + id int64 + // c is the component to be rendered once per context. + // if c is nil, the children of the `Once` method are rendered. + c Component +} + +// Once returns a component that renders its children once per context. +func (o *OnceHandle) Once() Component { + return ComponentFunc(func(ctx context.Context, w io.Writer) (err error) { + _, v := getContext(ctx) + if v.getHasBeenRendered(o) { + return nil + } + v.setHasBeenRendered(o) + if o.c != nil { + return o.c.Render(ctx, w) + } + return GetChildren(ctx).Render(ctx, w) + }) +} diff --git a/vendor/github.com/a-h/templ/push-tag.sh b/vendor/github.com/a-h/templ/push-tag.sh new file mode 100644 index 0000000..9eedeed --- /dev/null +++ b/vendor/github.com/a-h/templ/push-tag.sh @@ -0,0 +1,14 @@ +#!/bin/sh +if [ `git rev-parse --abbrev-ref HEAD` != "main" ]; then + echo "Error: Not on main branch. Please switch to main branch."; + exit 1; +fi +git pull +if ! git diff --quiet; then + echo "Error: Working directory is not clean. Please commit the changes first."; + exit 1; +fi +export VERSION=`cat .version` +echo Adding git tag with version v${VERSION}; +git tag v${VERSION}; +git push origin v${VERSION}; diff --git a/vendor/github.com/a-h/templ/runtime.go b/vendor/github.com/a-h/templ/runtime.go new file mode 100644 index 0000000..d4d5aa0 --- /dev/null +++ b/vendor/github.com/a-h/templ/runtime.go @@ -0,0 +1,855 @@ +package templ + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "html" + "html/template" + "io" + "net/http" + "os" + "reflect" + "runtime" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/a-h/templ/safehtml" +) + +// Types exposed by all components. + +// Component is the interface that all templates implement. +type Component interface { + // Render the template. + Render(ctx context.Context, w io.Writer) error +} + +// ComponentFunc converts a function that matches the Component interface's +// Render method into a Component. +type ComponentFunc func(ctx context.Context, w io.Writer) error + +// Render the template. +func (cf ComponentFunc) Render(ctx context.Context, w io.Writer) error { + return cf(ctx, w) +} + +// WithNonce sets a CSP nonce on the context and returns it. +func WithNonce(ctx context.Context, nonce string) context.Context { + ctx, v := getContext(ctx) + v.nonce = nonce + return ctx +} + +// GetNonce returns the CSP nonce value set with WithNonce, or an +// empty string if none has been set. +func GetNonce(ctx context.Context) (nonce string) { + if ctx == nil { + return "" + } + _, v := getContext(ctx) + return v.nonce +} + +func WithChildren(ctx context.Context, children Component) context.Context { + ctx, v := getContext(ctx) + v.children = &children + return ctx +} + +func ClearChildren(ctx context.Context) context.Context { + _, v := getContext(ctx) + v.children = nil + return ctx +} + +// NopComponent is a component that doesn't render anything. +var NopComponent = ComponentFunc(func(ctx context.Context, w io.Writer) error { return nil }) + +// GetChildren from the context. +func GetChildren(ctx context.Context) Component { + _, v := getContext(ctx) + if v.children == nil { + return NopComponent + } + return *v.children +} + +// EscapeString escapes HTML text within templates. +func EscapeString(s string) string { + return html.EscapeString(s) +} + +// Bool attribute value. +func Bool(value bool) bool { + return value +} + +// Classes for CSS. +// Supported types are string, ConstantCSSClass, ComponentCSSClass, map[string]bool. +func Classes(classes ...any) CSSClasses { + return CSSClasses(classes) +} + +// CSSClasses is a slice of CSS classes. +type CSSClasses []any + +// String returns the names of all CSS classes. +func (classes CSSClasses) String() string { + if len(classes) == 0 { + return "" + } + cp := newCSSProcessor() + for _, v := range classes { + cp.Add(v) + } + return cp.String() +} + +func newCSSProcessor() *cssProcessor { + return &cssProcessor{ + classNameToEnabled: make(map[string]bool), + } +} + +type cssProcessor struct { + classNameToEnabled map[string]bool + orderedNames []string +} + +func (cp *cssProcessor) Add(item any) { + switch c := item.(type) { + case []string: + for _, className := range c { + cp.AddClassName(className, true) + } + case string: + cp.AddClassName(c, true) + case ConstantCSSClass: + cp.AddClassName(c.ClassName(), true) + case ComponentCSSClass: + cp.AddClassName(c.ClassName(), true) + case map[string]bool: + // In Go, map keys are iterated in a randomized order. + // So the keys in the map must be sorted to produce consistent output. + keys := make([]string, len(c)) + var i int + for key := range c { + keys[i] = key + i++ + } + sort.Strings(keys) + for _, className := range keys { + cp.AddClassName(className, c[className]) + } + case []KeyValue[string, bool]: + for _, kv := range c { + cp.AddClassName(kv.Key, kv.Value) + } + case KeyValue[string, bool]: + cp.AddClassName(c.Key, c.Value) + case []KeyValue[CSSClass, bool]: + for _, kv := range c { + cp.AddClassName(kv.Key.ClassName(), kv.Value) + } + case KeyValue[CSSClass, bool]: + cp.AddClassName(c.Key.ClassName(), c.Value) + case CSSClasses: + for _, item := range c { + cp.Add(item) + } + case []CSSClass: + for _, item := range c { + cp.Add(item) + } + case func() CSSClass: + cp.AddClassName(c().ClassName(), true) + default: + cp.AddClassName(unknownTypeClassName, true) + } +} + +func (cp *cssProcessor) AddClassName(className string, enabled bool) { + cp.classNameToEnabled[className] = enabled + cp.orderedNames = append(cp.orderedNames, className) +} + +func (cp *cssProcessor) String() string { + // Order the outputs according to how they were input, and remove disabled names. + rendered := make(map[string]any, len(cp.classNameToEnabled)) + var names []string + for _, name := range cp.orderedNames { + if enabled := cp.classNameToEnabled[name]; !enabled { + continue + } + if _, hasBeenRendered := rendered[name]; hasBeenRendered { + continue + } + names = append(names, name) + rendered[name] = struct{}{} + } + + return strings.Join(names, " ") +} + +// KeyValue is a key and value pair. +type KeyValue[TKey comparable, TValue any] struct { + Key TKey `json:"name"` + Value TValue `json:"value"` +} + +// KV creates a new key/value pair from the input key and value. +func KV[TKey comparable, TValue any](key TKey, value TValue) KeyValue[TKey, TValue] { + return KeyValue[TKey, TValue]{ + Key: key, + Value: value, + } +} + +const unknownTypeClassName = "--templ-css-class-unknown-type" + +// Class returns a CSS class name. +// Deprecated: use a string instead. +func Class(name string) CSSClass { + return SafeClass(name) +} + +// SafeClass bypasses CSS class name validation. +// Deprecated: use a string instead. +func SafeClass(name string) CSSClass { + return ConstantCSSClass(name) +} + +// CSSClass provides a class name. +type CSSClass interface { + ClassName() string +} + +// ConstantCSSClass is a string constant of a CSS class name. +// Deprecated: use a string instead. +type ConstantCSSClass string + +// ClassName of the CSS class. +func (css ConstantCSSClass) ClassName() string { + return string(css) +} + +// ComponentCSSClass is a templ.CSS +type ComponentCSSClass struct { + // ID of the class, will be autogenerated. + ID string + // Definition of the CSS. + Class SafeCSS +} + +// ClassName of the CSS class. +func (css ComponentCSSClass) ClassName() string { + return css.ID +} + +// CSSID calculates an ID. +func CSSID(name string, css string) string { + sum := sha256.Sum256([]byte(css)) + hp := hex.EncodeToString(sum[:])[0:4] + // Benchmarking showed this was fastest, and with fewest allocations (1). + // Using strings.Builder (2 allocs). + // Using fmt.Sprintf (3 allocs). + return name + "_" + hp +} + +// NewCSSMiddleware creates HTTP middleware that renders a global stylesheet of ComponentCSSClass +// CSS if the request path matches, or updates the HTTP context to ensure that any handlers that +// use templ.Components skip rendering <style> elements for classes that are included in the global +// stylesheet. By default, the stylesheet path is /styles/templ.css +func NewCSSMiddleware(next http.Handler, classes ...CSSClass) CSSMiddleware { + return CSSMiddleware{ + Path: "/styles/templ.css", + CSSHandler: NewCSSHandler(classes...), + Next: next, + } +} + +// CSSMiddleware renders a global stylesheet. +type CSSMiddleware struct { + Path string + CSSHandler CSSHandler + Next http.Handler +} + +func (cssm CSSMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == cssm.Path { + cssm.CSSHandler.ServeHTTP(w, r) + return + } + // Add registered classes to the context. + ctx, v := getContext(r.Context()) + for _, c := range cssm.CSSHandler.Classes { + v.addClass(c.ID) + } + // Serve the request. Templ components will use the updated context + // to know to skip rendering <style> elements for any component CSS + // classes that have been included in the global stylesheet. + cssm.Next.ServeHTTP(w, r.WithContext(ctx)) +} + +// NewCSSHandler creates a handler that serves a stylesheet containing the CSS of the +// classes passed in. This is used by the CSSMiddleware to provide global stylesheets +// for templ components. +func NewCSSHandler(classes ...CSSClass) CSSHandler { + ccssc := make([]ComponentCSSClass, 0, len(classes)) + for _, c := range classes { + ccss, ok := c.(ComponentCSSClass) + if !ok { + continue + } + ccssc = append(ccssc, ccss) + } + return CSSHandler{ + Classes: ccssc, + } +} + +// CSSHandler is a HTTP handler that serves CSS. +type CSSHandler struct { + Logger func(err error) + Classes []ComponentCSSClass +} + +func (cssh CSSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/css") + for _, c := range cssh.Classes { + _, err := w.Write([]byte(c.Class)) + if err != nil && cssh.Logger != nil { + cssh.Logger(err) + } + } +} + +// RenderCSSItems renders the CSS to the writer, if the items haven't already been rendered. +func RenderCSSItems(ctx context.Context, w io.Writer, classes ...any) (err error) { + if len(classes) == 0 { + return nil + } + _, v := getContext(ctx) + sb := new(strings.Builder) + renderCSSItemsToBuilder(sb, v, classes...) + if sb.Len() > 0 { + if _, err = io.WriteString(w, `<style type="text/css">`); err != nil { + return err + } + if _, err = io.WriteString(w, sb.String()); err != nil { + return err + } + if _, err = io.WriteString(w, `</style>`); err != nil { + return err + } + } + return nil +} + +func renderCSSItemsToBuilder(sb *strings.Builder, v *contextValue, classes ...any) { + for _, c := range classes { + switch ccc := c.(type) { + case ComponentCSSClass: + if !v.hasClassBeenRendered(ccc.ID) { + sb.WriteString(string(ccc.Class)) + v.addClass(ccc.ID) + } + case KeyValue[ComponentCSSClass, bool]: + if !ccc.Value { + continue + } + renderCSSItemsToBuilder(sb, v, ccc.Key) + case KeyValue[CSSClass, bool]: + if !ccc.Value { + continue + } + renderCSSItemsToBuilder(sb, v, ccc.Key) + case CSSClasses: + renderCSSItemsToBuilder(sb, v, ccc...) + case []CSSClass: + for _, item := range ccc { + renderCSSItemsToBuilder(sb, v, item) + } + case func() CSSClass: + renderCSSItemsToBuilder(sb, v, ccc()) + case []string: + // Skip. These are class names, not CSS classes. + case string: + // Skip. This is a class name, not a CSS class. + case ConstantCSSClass: + // Skip. This is a class name, not a CSS class. + case CSSClass: + // Skip. This is a class name, not a CSS class. + case map[string]bool: + // Skip. These are class names, not CSS classes. + case KeyValue[string, bool]: + // Skip. These are class names, not CSS classes. + case []KeyValue[string, bool]: + // Skip. These are class names, not CSS classes. + case KeyValue[ConstantCSSClass, bool]: + // Skip. These are class names, not CSS classes. + case []KeyValue[ConstantCSSClass, bool]: + // Skip. These are class names, not CSS classes. + } + } +} + +// SafeCSS is CSS that has been sanitized. +type SafeCSS string + +type SafeCSSProperty string + +var safeCSSPropertyType = reflect.TypeOf(SafeCSSProperty("")) + +// SanitizeCSS sanitizes CSS properties to ensure that they are safe. +func SanitizeCSS[T ~string](property string, value T) SafeCSS { + if reflect.TypeOf(value) == safeCSSPropertyType { + return SafeCSS(safehtml.SanitizeCSSProperty(property) + ":" + string(value) + ";") + } + p, v := safehtml.SanitizeCSS(property, string(value)) + return SafeCSS(p + ":" + v + ";") +} + +// Attributes is an alias to map[string]any made for spread attributes. +type Attributes map[string]any + +// sortedKeys returns the keys of a map in sorted order. +func sortedKeys(m map[string]any) (keys []string) { + keys = make([]string, len(m)) + var i int + for k := range m { + keys[i] = k + i++ + } + sort.Strings(keys) + return keys +} + +func writeStrings(w io.Writer, ss ...string) (err error) { + for _, s := range ss { + if _, err = io.WriteString(w, s); err != nil { + return err + } + } + return nil +} + +func RenderAttributes(ctx context.Context, w io.Writer, attributes Attributes) (err error) { + for _, key := range sortedKeys(attributes) { + value := attributes[key] + switch value := value.(type) { + case string: + if err = writeStrings(w, ` `, EscapeString(key), `="`, EscapeString(value), `"`); err != nil { + return err + } + case *string: + if value != nil { + if err = writeStrings(w, ` `, EscapeString(key), `="`, EscapeString(*value), `"`); err != nil { + return err + } + } + case bool: + if value { + if err = writeStrings(w, ` `, EscapeString(key)); err != nil { + return err + } + } + case *bool: + if value != nil && *value { + if err = writeStrings(w, ` `, EscapeString(key)); err != nil { + return err + } + } + case KeyValue[string, bool]: + if value.Value { + if err = writeStrings(w, ` `, EscapeString(key), `="`, EscapeString(value.Key), `"`); err != nil { + return err + } + } + case KeyValue[bool, bool]: + if value.Value && value.Key { + if err = writeStrings(w, ` `, EscapeString(key)); err != nil { + return err + } + } + case func() bool: + if value() { + if err = writeStrings(w, ` `, EscapeString(key)); err != nil { + return err + } + } + } + } + return nil +} + +// Script handling. + +func safeEncodeScriptParams(escapeHTML bool, params []any) []string { + encodedParams := make([]string, len(params)) + for i := 0; i < len(encodedParams); i++ { + enc, _ := json.Marshal(params[i]) + if !escapeHTML { + encodedParams[i] = string(enc) + continue + } + encodedParams[i] = EscapeString(string(enc)) + } + return encodedParams +} + +// SafeScript encodes unknown parameters for safety for inside HTML attributes. +func SafeScript(functionName string, params ...any) string { + encodedParams := safeEncodeScriptParams(true, params) + sb := new(strings.Builder) + sb.WriteString(functionName) + sb.WriteRune('(') + sb.WriteString(strings.Join(encodedParams, ",")) + sb.WriteRune(')') + return sb.String() +} + +// SafeScript encodes unknown parameters for safety for inline scripts. +func SafeScriptInline(functionName string, params ...any) string { + encodedParams := safeEncodeScriptParams(false, params) + sb := new(strings.Builder) + sb.WriteString(functionName) + sb.WriteRune('(') + sb.WriteString(strings.Join(encodedParams, ",")) + sb.WriteRune(')') + return sb.String() +} + +type contextKeyType int + +const contextKey = contextKeyType(0) + +type contextValue struct { + ss map[string]struct{} + onceHandles map[*OnceHandle]struct{} + children *Component + nonce string +} + +func (v *contextValue) setHasBeenRendered(h *OnceHandle) { + if v.onceHandles == nil { + v.onceHandles = map[*OnceHandle]struct{}{} + } + v.onceHandles[h] = struct{}{} +} + +func (v *contextValue) getHasBeenRendered(h *OnceHandle) (ok bool) { + if v.onceHandles == nil { + v.onceHandles = map[*OnceHandle]struct{}{} + } + _, ok = v.onceHandles[h] + return +} + +func (v *contextValue) addScript(s string) { + if v.ss == nil { + v.ss = map[string]struct{}{} + } + v.ss["script_"+s] = struct{}{} +} + +func (v *contextValue) hasScriptBeenRendered(s string) (ok bool) { + if v.ss == nil { + v.ss = map[string]struct{}{} + } + _, ok = v.ss["script_"+s] + return +} + +func (v *contextValue) addClass(s string) { + if v.ss == nil { + v.ss = map[string]struct{}{} + } + v.ss["class_"+s] = struct{}{} +} + +func (v *contextValue) hasClassBeenRendered(s string) (ok bool) { + if v.ss == nil { + v.ss = map[string]struct{}{} + } + _, ok = v.ss["class_"+s] + return +} + +// InitializeContext initializes context used to store internal state used during rendering. +func InitializeContext(ctx context.Context) context.Context { + if _, ok := ctx.Value(contextKey).(*contextValue); ok { + return ctx + } + v := &contextValue{} + ctx = context.WithValue(ctx, contextKey, v) + return ctx +} + +func getContext(ctx context.Context) (context.Context, *contextValue) { + v, ok := ctx.Value(contextKey).(*contextValue) + if !ok { + ctx = InitializeContext(ctx) + v = ctx.Value(contextKey).(*contextValue) + } + return ctx, v +} + +// ComponentScript is a templ Script template. +type ComponentScript struct { + // Name of the script, e.g. print. + Name string + // Function to render. + Function string + // Call of the function in JavaScript syntax, including parameters, and + // ensures parameters are HTML escaped; useful for injecting into HTML + // attributes like onclick, onhover, etc. + // + // Given: + // functionName("some string",12345) + // It would render: + // __templ_functionName_sha("some string",12345)) + // + // This is can be injected into HTML attributes: + // <button onClick="__templ_functionName_sha("some string",12345))">Click Me</button> + Call string + // Call of the function in JavaScript syntax, including parameters. It + // does not HTML escape parameters; useful for directly calling in script + // elements. + // + // Given: + // functionName("some string",12345) + // It would render: + // __templ_functionName_sha("some string",12345)) + // + // This is can be used to call the function inside a script tag: + // <script>__templ_functionName_sha("some string",12345))</script> + CallInline string +} + +var _ Component = ComponentScript{} + +func writeScriptHeader(ctx context.Context, w io.Writer) (err error) { + var nonceAttr string + if nonce := GetNonce(ctx); nonce != "" { + nonceAttr = " nonce=\"" + EscapeString(nonce) + "\"" + } + _, err = fmt.Fprintf(w, `<script type="text/javascript"%s>`, nonceAttr) + return err +} + +func (c ComponentScript) Render(ctx context.Context, w io.Writer) error { + err := RenderScriptItems(ctx, w, c) + if err != nil { + return err + } + if len(c.Call) > 0 { + if err = writeScriptHeader(ctx, w); err != nil { + return err + } + if _, err = io.WriteString(w, c.CallInline); err != nil { + return err + } + if _, err = io.WriteString(w, `</script>`); err != nil { + return err + } + } + return nil +} + +// RenderScriptItems renders a <script> element, if the script has not already been rendered. +func RenderScriptItems(ctx context.Context, w io.Writer, scripts ...ComponentScript) (err error) { + if len(scripts) == 0 { + return nil + } + _, v := getContext(ctx) + sb := new(strings.Builder) + for _, s := range scripts { + if !v.hasScriptBeenRendered(s.Name) { + sb.WriteString(s.Function) + v.addScript(s.Name) + } + } + if sb.Len() > 0 { + if err = writeScriptHeader(ctx, w); err != nil { + return err + } + if _, err = io.WriteString(w, sb.String()); err != nil { + return err + } + if _, err = io.WriteString(w, `</script>`); err != nil { + return err + } + } + return nil +} + +var bufferPool = sync.Pool{ + New: func() any { + return new(bytes.Buffer) + }, +} + +func GetBuffer() *bytes.Buffer { + return bufferPool.Get().(*bytes.Buffer) +} + +func ReleaseBuffer(b *bytes.Buffer) { + b.Reset() + bufferPool.Put(b) +} + +// JoinStringErrs joins an optional list of errors. +func JoinStringErrs(s string, errs ...error) (string, error) { + return s, errors.Join(errs...) +} + +// Error returned during template rendering. +type Error struct { + Err error + // FileName of the template file. + FileName string + // Line index of the error. + Line int + // Col index of the error. + Col int +} + +func (e Error) Error() string { + if e.FileName == "" { + e.FileName = "templ" + } + return fmt.Sprintf("%s: error at line %d, col %d: %v", e.FileName, e.Line, e.Col, e.Err) +} + +func (e Error) Unwrap() error { + return e.Err +} + +// Raw renders the input HTML to the output without applying HTML escaping. +// +// Use of this component presents a security risk - the HTML should come from +// a trusted source, because it will be included as-is in the output. +func Raw[T ~string](html T, errs ...error) Component { + return ComponentFunc(func(ctx context.Context, w io.Writer) (err error) { + if err = errors.Join(errs...); err != nil { + return err + } + _, err = io.WriteString(w, string(html)) + return err + }) +} + +// FromGoHTML creates a templ Component from a Go html/template template. +func FromGoHTML(t *template.Template, data any) Component { + return ComponentFunc(func(ctx context.Context, w io.Writer) (err error) { + return t.Execute(w, data) + }) +} + +// ToGoHTML renders the component to a Go html/template template.HTML string. +func ToGoHTML(ctx context.Context, c Component) (s template.HTML, err error) { + b := GetBuffer() + defer ReleaseBuffer(b) + if err = c.Render(ctx, b); err != nil { + return + } + s = template.HTML(b.String()) + return +} + +// WriteWatchModeString is used when rendering templates in development mode. +// the generator would have written non-go code to the _templ.txt file, which +// is then read by this function and written to the output. +func WriteWatchModeString(w io.Writer, lineNum int) error { + _, path, _, _ := runtime.Caller(1) + if !strings.HasSuffix(path, "_templ.go") { + return errors.New("templ: WriteWatchModeString can only be called from _templ.go") + } + txtFilePath := strings.Replace(path, "_templ.go", "_templ.txt", 1) + + literals, err := getWatchedStrings(txtFilePath) + if err != nil { + return fmt.Errorf("templ: failed to cache strings: %w", err) + } + + if lineNum > len(literals) { + return errors.New("templ: failed to find line " + strconv.Itoa(lineNum) + " in " + txtFilePath) + } + + unquoted, err := strconv.Unquote(`"` + literals[lineNum-1] + `"`) + if err != nil { + return err + } + _, err = io.WriteString(w, unquoted) + return err +} + +var ( + watchModeCache = map[string]watchState{} + watchStateMutex sync.Mutex +) + +type watchState struct { + modTime time.Time + strings []string +} + +func getWatchedStrings(txtFilePath string) ([]string, error) { + watchStateMutex.Lock() + defer watchStateMutex.Unlock() + + state, cached := watchModeCache[txtFilePath] + if !cached { + return cacheStrings(txtFilePath) + } + + if time.Since(state.modTime) < time.Millisecond*100 { + return state.strings, nil + } + + info, err := os.Stat(txtFilePath) + if err != nil { + return nil, fmt.Errorf("templ: failed to stat %s: %w", txtFilePath, err) + } + + if !info.ModTime().After(state.modTime) { + return state.strings, nil + } + + return cacheStrings(txtFilePath) +} + +func cacheStrings(txtFilePath string) ([]string, error) { + txtFile, err := os.Open(txtFilePath) + if err != nil { + return nil, fmt.Errorf("templ: failed to open %s: %w", txtFilePath, err) + } + defer txtFile.Close() + + info, err := txtFile.Stat() + if err != nil { + return nil, fmt.Errorf("templ: failed to stat %s: %w", txtFilePath, err) + } + + all, err := io.ReadAll(txtFile) + if err != nil { + return nil, fmt.Errorf("templ: failed to read %s: %w", txtFilePath, err) + } + + literals := strings.Split(string(all), "\n") + watchModeCache[txtFilePath] = watchState{ + modTime: info.ModTime(), + strings: literals, + } + + return literals, nil +} diff --git a/vendor/github.com/a-h/templ/runtime/buffer.go b/vendor/github.com/a-h/templ/runtime/buffer.go new file mode 100644 index 0000000..63e4acd --- /dev/null +++ b/vendor/github.com/a-h/templ/runtime/buffer.go @@ -0,0 +1,62 @@ +package runtime + +import ( + "bufio" + "io" + "net/http" +) + +// DefaultBufferSize is the default size of buffers. It is set to 4KB by default, which is the +// same as the default buffer size of bufio.Writer. +var DefaultBufferSize = 4 * 1024 // 4KB + +// Buffer is a wrapper around bufio.Writer that enables flushing and closing of +// the underlying writer. +type Buffer struct { + Underlying io.Writer + b *bufio.Writer +} + +// Write the contents of p into the buffer. +func (b *Buffer) Write(p []byte) (n int, err error) { + return b.b.Write(p) +} + +// Flush writes any buffered data to the underlying io.Writer and +// calls the Flush method of the underlying http.Flusher if it implements it. +func (b *Buffer) Flush() error { + if err := b.b.Flush(); err != nil { + return err + } + if f, ok := b.Underlying.(http.Flusher); ok { + f.Flush() + } + return nil +} + +// Close closes the buffer and the underlying io.Writer if it implements io.Closer. +func (b *Buffer) Close() error { + if c, ok := b.Underlying.(io.Closer); ok { + return c.Close() + } + return nil +} + +// Reset sets the underlying io.Writer to w and resets the buffer. +func (b *Buffer) Reset(w io.Writer) { + if b.b == nil { + b.b = bufio.NewWriterSize(b, DefaultBufferSize) + } + b.Underlying = w + b.b.Reset(w) +} + +// Size returns the size of the underlying buffer in bytes. +func (b *Buffer) Size() int { + return b.b.Size() +} + +// WriteString writes the contents of s into the buffer. +func (b *Buffer) WriteString(s string) (n int, err error) { + return b.b.WriteString(s) +} diff --git a/vendor/github.com/a-h/templ/runtime/bufferpool.go b/vendor/github.com/a-h/templ/runtime/bufferpool.go new file mode 100644 index 0000000..ca2a131 --- /dev/null +++ b/vendor/github.com/a-h/templ/runtime/bufferpool.go @@ -0,0 +1,38 @@ +package runtime + +import ( + "io" + "sync" +) + +var bufferPool = sync.Pool{ + New: func() any { + return new(Buffer) + }, +} + +// GetBuffer creates and returns a new buffer if the writer is not already a buffer, +// or returns the existing buffer if it is. +func GetBuffer(w io.Writer) (b *Buffer, existing bool) { + if w == nil { + return nil, false + } + b, ok := w.(*Buffer) + if ok { + return b, true + } + b = bufferPool.Get().(*Buffer) + b.Reset(w) + return b, false +} + +// ReleaseBuffer flushes the buffer and returns it to the pool. +func ReleaseBuffer(w io.Writer) (err error) { + b, ok := w.(*Buffer) + if !ok { + return nil + } + err = b.Flush() + bufferPool.Put(b) + return err +} diff --git a/vendor/github.com/a-h/templ/runtime/builder.go b/vendor/github.com/a-h/templ/runtime/builder.go new file mode 100644 index 0000000..0f4c9d4 --- /dev/null +++ b/vendor/github.com/a-h/templ/runtime/builder.go @@ -0,0 +1,8 @@ +package runtime + +import "strings" + +// GetBuilder returns a strings.Builder. +func GetBuilder() (sb strings.Builder) { + return sb +} diff --git a/vendor/github.com/a-h/templ/runtime/runtime.go b/vendor/github.com/a-h/templ/runtime/runtime.go new file mode 100644 index 0000000..aaa4a2c --- /dev/null +++ b/vendor/github.com/a-h/templ/runtime/runtime.go @@ -0,0 +1,21 @@ +package runtime + +import ( + "context" + "io" + + "github.com/a-h/templ" +) + +// GeneratedComponentInput is used to avoid generated code needing to import the `context` and `io` packages. +type GeneratedComponentInput struct { + Context context.Context + Writer io.Writer +} + +// GeneratedTemplate is used to avoid generated code needing to import the `context` and `io` packages. +func GeneratedTemplate(f func(GeneratedComponentInput) error) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { + return f(GeneratedComponentInput{ctx, w}) + }) +} diff --git a/vendor/github.com/a-h/templ/safehtml/style.go b/vendor/github.com/a-h/templ/safehtml/style.go new file mode 100644 index 0000000..486df7c --- /dev/null +++ b/vendor/github.com/a-h/templ/safehtml/style.go @@ -0,0 +1,168 @@ +// Adapted from https://raw.githubusercontent.com/google/safehtml/3c4cd5b5d8c9a6c5882fba099979e9f50b65c876/style.go + +// Copyright (c) 2017 The Go Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +package safehtml + +import ( + "net/url" + "regexp" + "strings" +) + +// SanitizeCSS attempts to sanitize CSS properties. +func SanitizeCSS(property, value string) (string, string) { + property = SanitizeCSSProperty(property) + if property == InnocuousPropertyName { + return InnocuousPropertyName, InnocuousPropertyValue + } + return property, SanitizeCSSValue(property, value) +} + +func SanitizeCSSValue(property, value string) string { + if sanitizer, ok := cssPropertyNameToValueSanitizer[property]; ok { + return sanitizer(value) + } + return sanitizeRegular(value) +} + +func SanitizeCSSProperty(property string) string { + if !identifierPattern.MatchString(property) { + return InnocuousPropertyName + } + return strings.ToLower(property) +} + +// identifierPattern matches a subset of valid <ident-token> values defined in +// https://www.w3.org/TR/css-syntax-3/#ident-token-diagram. This pattern matches all generic family name +// keywords defined in https://drafts.csswg.org/css-fonts-3/#family-name-value. +var identifierPattern = regexp.MustCompile(`^[-a-zA-Z]+$`) + +var cssPropertyNameToValueSanitizer = map[string]func(string) string{ + "background-image": sanitizeBackgroundImage, + "font-family": sanitizeFontFamily, + "display": sanitizeEnum, + "background-color": sanitizeRegular, + "background-position": sanitizeRegular, + "background-repeat": sanitizeRegular, + "background-size": sanitizeRegular, + "color": sanitizeRegular, + "height": sanitizeRegular, + "width": sanitizeRegular, + "left": sanitizeRegular, + "right": sanitizeRegular, + "top": sanitizeRegular, + "bottom": sanitizeRegular, + "font-weight": sanitizeRegular, + "padding": sanitizeRegular, + "z-index": sanitizeRegular, +} + +var validURLPrefixes = []string{ + `url("`, + `url('`, + `url(`, +} + +var validURLSuffixes = []string{ + `")`, + `')`, + `)`, +} + +func sanitizeBackgroundImage(v string) string { + // Check for <> as per https://github.com/google/safehtml/blob/be23134998433fcf0135dda53593fc8f8bf4df7c/style.go#L87C2-L89C3 + if strings.ContainsAny(v, "<>") { + return InnocuousPropertyValue + } + for _, u := range strings.Split(v, ",") { + u = strings.TrimSpace(u) + var found bool + for i, prefix := range validURLPrefixes { + if strings.HasPrefix(u, prefix) && strings.HasSuffix(u, validURLSuffixes[i]) { + found = true + u = strings.TrimPrefix(u, validURLPrefixes[i]) + u = strings.TrimSuffix(u, validURLSuffixes[i]) + break + } + } + if !found || !urlIsSafe(u) { + return InnocuousPropertyValue + } + } + return v +} + +func urlIsSafe(s string) bool { + u, err := url.Parse(s) + if err != nil { + return false + } + if u.IsAbs() { + if strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https") || strings.EqualFold(u.Scheme, "mailto") { + return true + } + return false + } + return true +} + +var genericFontFamilyName = regexp.MustCompile(`^[a-zA-Z][- a-zA-Z]+$`) + +func sanitizeFontFamily(s string) string { + for _, f := range strings.Split(s, ",") { + f = strings.TrimSpace(f) + if strings.HasPrefix(f, `"`) { + if !strings.HasSuffix(f, `"`) { + return InnocuousPropertyValue + } + continue + } + if !genericFontFamilyName.MatchString(f) { + return InnocuousPropertyValue + } + } + return s +} + +func sanitizeEnum(s string) string { + if !safeEnumPropertyValuePattern.MatchString(s) { + return InnocuousPropertyValue + } + return s +} + +func sanitizeRegular(s string) string { + if !safeRegularPropertyValuePattern.MatchString(s) { + return InnocuousPropertyValue + } + return s +} + +// InnocuousPropertyName is an innocuous property generated by a sanitizer when its input is unsafe. +const InnocuousPropertyName = "zTemplUnsafeCSSPropertyName" + +// InnocuousPropertyValue is an innocuous property generated by a sanitizer when its input is unsafe. +const InnocuousPropertyValue = "zTemplUnsafeCSSPropertyValue" + +// safeRegularPropertyValuePattern matches strings that are safe to use as property values. +// Specifically, it matches string where every '*' or '/' is followed by end-of-text or a safe rune +// (i.e. alphanumerics or runes in the set [+-.!#%_ \t]). This regex ensures that the following +// are disallowed: +// - "/*" and "*/", which are CSS comment markers. +// - "//", even though this is not a comment marker in the CSS specification. Disallowing +// this string minimizes the chance that browser peculiarities or parsing bugs will allow +// sanitization to be bypassed. +// - '(' and ')', which can be used to call functions. +// - ',', since it can be used to inject extra values into a property. +// - Runes which could be matched on CSS error recovery of a previously malformed token, such as '@' +// and ':'. See http://www.w3.org/TR/css3-syntax/#error-handling. +var safeRegularPropertyValuePattern = regexp.MustCompile(`^(?:[*/]?(?:[0-9a-zA-Z+-.!#%_ \t]|$))*$`) + +// safeEnumPropertyValuePattern matches strings that are safe to use as enumerated property values. +// Specifically, it matches strings that contain only alphabetic and '-' runes. +var safeEnumPropertyValuePattern = regexp.MustCompile(`^[a-zA-Z-]*$`) diff --git a/vendor/github.com/a-h/templ/templ.png b/vendor/github.com/a-h/templ/templ.png Binary files differnew file mode 100644 index 0000000..1c4bc2f --- /dev/null +++ b/vendor/github.com/a-h/templ/templ.png diff --git a/vendor/github.com/a-h/templ/url.go b/vendor/github.com/a-h/templ/url.go new file mode 100644 index 0000000..bf912e1 --- /dev/null +++ b/vendor/github.com/a-h/templ/url.go @@ -0,0 +1,20 @@ +package templ + +import "strings" + +// FailedSanitizationURL is returned if a URL fails sanitization checks. +const FailedSanitizationURL = SafeURL("about:invalid#TemplFailedSanitizationURL") + +// URL sanitizes the input string s and returns a SafeURL. +func URL(s string) SafeURL { + if i := strings.IndexRune(s, ':'); i >= 0 && !strings.ContainsRune(s[:i], '/') { + protocol := s[:i] + if !strings.EqualFold(protocol, "http") && !strings.EqualFold(protocol, "https") && !strings.EqualFold(protocol, "mailto") && !strings.EqualFold(protocol, "tel") && !strings.EqualFold(protocol, "ftp") && !strings.EqualFold(protocol, "ftps") { + return FailedSanitizationURL + } + } + return SafeURL(s) +} + +// SafeURL is a URL that has been sanitized. +type SafeURL string diff --git a/vendor/github.com/a-h/templ/version.go b/vendor/github.com/a-h/templ/version.go new file mode 100644 index 0000000..b7fbb6f --- /dev/null +++ b/vendor/github.com/a-h/templ/version.go @@ -0,0 +1,10 @@ +package templ + +import _ "embed" + +//go:embed .version +var version string + +func Version() string { + return "v" + version +} |