Use templ instead of Go template for HTML sources
Also add Nix.
This commit is contained in:
parent
1397196e4e
commit
2ace4b61bf
|
@ -8,5 +8,5 @@ indent_size = 2
|
|||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{Makefile,*.go}]
|
||||
[{*.templ,*.go}]
|
||||
indent_style = tab
|
||||
|
|
|
@ -1 +1,7 @@
|
|||
/out
|
||||
# Nix
|
||||
/result
|
||||
|
||||
# Built artifacts
|
||||
*_templ.go
|
||||
/src/**/*.html
|
||||
/out/
|
||||
|
|
|
@ -5,34 +5,43 @@ default:
|
|||
- docker
|
||||
- linux
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
|
||||
stages:
|
||||
- build
|
||||
- deploy
|
||||
|
||||
build:generate_html:
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
|
||||
variables:
|
||||
SRC_DIR: out
|
||||
|
||||
build:site:
|
||||
stage: build
|
||||
image: golang:1.21-alpine
|
||||
before_script:
|
||||
- echo "https://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories
|
||||
- apk add --no-cache minify templ
|
||||
- mkdir "$SRC_DIR"
|
||||
- cp -r src/* "$SRC_DIR"/
|
||||
script:
|
||||
- templ generate
|
||||
- go run ./*.go -gen "$SRC_DIR"
|
||||
- minify -r "$SRC_DIR" -o .
|
||||
artifacts:
|
||||
expire_in: 1 hour
|
||||
paths:
|
||||
- out/
|
||||
image: golang:1.20-alpine
|
||||
script:
|
||||
- apk --no-cache add minify make
|
||||
- make
|
||||
|
||||
deploy:cloudflare_pages:
|
||||
stage: deploy
|
||||
image:
|
||||
name: node:20-alpine
|
||||
name: node:21-alpine
|
||||
entrypoint: ['']
|
||||
script: |
|
||||
npx wrangler pages deploy \
|
||||
--project-name=folliehiyuki-cdn \
|
||||
--branch="$CI_COMMIT_REF_NAME" \
|
||||
out/
|
||||
"$SRC_DIR"/
|
||||
dependencies:
|
||||
- build:generate_html
|
||||
- build:site
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
[linters]
|
||||
disable-all = true
|
||||
enable = [
|
||||
"errcheck",
|
||||
"goconst",
|
||||
"gofumpt",
|
||||
"gosec",
|
||||
"gosimple",
|
||||
"govet",
|
||||
"ineffassign",
|
||||
"lll",
|
||||
"misspell",
|
||||
"nakedret",
|
||||
"revive",
|
||||
"staticcheck",
|
||||
"typecheck",
|
||||
"unconvert",
|
||||
"unused"
|
||||
]
|
||||
|
||||
[linters-settings]
|
||||
[linters-settings.errcheck]
|
||||
check-type-assertions = true
|
||||
[linters-settings.gofumpt]
|
||||
extra-rules = true
|
||||
[linters-settings.govet]
|
||||
check-shadowing = true
|
||||
|
||||
[issues]
|
||||
fix = false
|
||||
|
||||
[output]
|
||||
sort-results = true
|
||||
|
||||
[run]
|
||||
skip-dirs-use-default = true
|
||||
skip-files = [ ".*_templ\\.go$" ]
|
||||
go = "1.20"
|
||||
timeout = "15m"
|
20
LICENSE
20
LICENSE
|
@ -1,7 +1,19 @@
|
|||
Copyright © 2023 Hoang Nguyen
|
||||
Copyright © 2023 Hoang Nguyen <folliekazetani@protonmail.com>
|
||||
|
||||
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:
|
||||
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 above copyright notice and this permission notice (including the next
|
||||
paragraph) 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.
|
||||
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.
|
||||
|
|
28
Makefile
28
Makefile
|
@ -1,28 +0,0 @@
|
|||
.DEFAULT_GOAL := bundle
|
||||
|
||||
OUTPUT_DIR ?= out
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@rm -rf $(OUTPUT_DIR)
|
||||
|
||||
.PHONY: gen
|
||||
gen:
|
||||
@mkdir -p $(OUTPUT_DIR)
|
||||
@cp -rf src/* $(OUTPUT_DIR)
|
||||
@find ./$(OUTPUT_DIR) -mindepth 1 -type d -print -exec go run ./tools/update_html.go -root=$(OUTPUT_DIR) {} \;
|
||||
@go run ./tools/update_html.go -root=$(OUTPUT_DIR) -home=true "$(OUTPUT_DIR)"
|
||||
|
||||
.PHONY: minify
|
||||
minify:
|
||||
@minify -r ./$(OUTPUT_DIR) -o .
|
||||
|
||||
.PHONY: serve
|
||||
serve: gen minify
|
||||
@podman run --rm -it \
|
||||
-p 8080:80 \
|
||||
-v ./$(OUTPUT_DIR):/usr/share/nginx/html:ro \
|
||||
docker.io/library/nginx:stable-alpine
|
||||
|
||||
.PHONY: bundle
|
||||
bundle: clean gen minify
|
|
@ -0,0 +1,144 @@
|
|||
{
|
||||
"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": 1694102001,
|
||||
"narHash": "sha256-vky6VPK1n1od6vXbqzOXnekrQpTL4hbPAwUhT5J9c9E=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "9e21c80adf67ebcb077d75bd5e7d724d21eeafd6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1700204040,
|
||||
"narHash": "sha256-xSVcS5HBYnD3LTer7Y2K8ZQCDCXMa3QUD1MzRjHzuhI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c757e9bd77b16ca2e03c89bf8bc9ecb28e0c06ad",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"templ-src": "templ-src"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"templ-src": {
|
||||
"inputs": {
|
||||
"gitignore": [
|
||||
"gitignore"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"xc": "xc"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1700298481,
|
||||
"narHash": "sha256-eFDvyf1EGfHpFQn7XLG9CRba4BleYRlTM64Oyty6rpA=",
|
||||
"owner": "a-h",
|
||||
"repo": "templ",
|
||||
"rev": "6c411018048a7aa35a275caddc5c3c1222283293",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "a-h",
|
||||
"repo": "templ",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"xc": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"templ-src",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1696495449,
|
||||
"narHash": "sha256-dthZiJ2FX/eIC0l1mdfefJZXDTVLfwp7L7Arq5rsCWA=",
|
||||
"owner": "joerdav",
|
||||
"repo": "xc",
|
||||
"rev": "c8baab14d679fb276f11c576607010283be21220",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "joerdav",
|
||||
"repo": "xc",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
{
|
||||
description = "FollieHiyuki's stupid CDN";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
templ-src = {
|
||||
url = "github:a-h/templ";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs.gitignore.follows = "gitignore";
|
||||
};
|
||||
|
||||
gitignore = {
|
||||
url = "github:hercules-ci/gitignore.nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { nixpkgs, templ-src, flake-utils, gitignore, ... }:
|
||||
flake-utils.lib.eachSystem [
|
||||
"x86_64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-linux"
|
||||
"aarch64-darwin"
|
||||
]
|
||||
(system:
|
||||
let
|
||||
inherit (nixpkgs) lib;
|
||||
pkgs = nixpkgs.legacyPackages."${system}" // templ-src.packages."${system}";
|
||||
|
||||
buildInputs = with pkgs; [ go minify templ ];
|
||||
shellHook = ''
|
||||
export GOTOOLCHAIN=local
|
||||
export CGO_ENABLED=0
|
||||
'';
|
||||
|
||||
tasks = with pkgs; {
|
||||
gen = {
|
||||
runtimeInputs = [ templ ];
|
||||
script = "templ generate";
|
||||
};
|
||||
preview = {
|
||||
runtimeInputs = [ go ];
|
||||
script = "go run ./*.go -preview result/";
|
||||
};
|
||||
publish = {
|
||||
runtimeInputs = [ nodePackages.wrangler ];
|
||||
|
||||
# NOTE: need to login beforehand, or export CLOUDFLARE_API_TOKEN first
|
||||
script = ''
|
||||
wrangler pages deploy \
|
||||
--project-name=folliehiyuki-cdn \
|
||||
--branch=main \
|
||||
result/
|
||||
'';
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
apps = (lib.attrsets.mapAttrs
|
||||
(k: v: flake-utils.lib.mkApp {
|
||||
drv = pkgs.writeShellApplication {
|
||||
name = k;
|
||||
runtimeInputs = v.runtimeInputs;
|
||||
text = v.script;
|
||||
};
|
||||
})
|
||||
tasks);
|
||||
|
||||
packages = rec {
|
||||
default = bundle;
|
||||
bundle = with pkgs; stdenv.mkDerivation {
|
||||
inherit buildInputs;
|
||||
name = "cdn.folliehiyuki.com";
|
||||
src = lib.cleanSource (gitignore.lib.gitignoreSource ./.);
|
||||
configurePhase = ''
|
||||
${shellHook}
|
||||
export HOME="$TMPDIR"
|
||||
export GOCACHE="$PWD/go-cache"
|
||||
export GOTMPDIR="$PWD"
|
||||
export GOMODCACHE="$PWD/go"
|
||||
|
||||
templ generate
|
||||
'';
|
||||
buildPhase = ''
|
||||
go run *.go -gen src
|
||||
minify -r ./src -o .
|
||||
'';
|
||||
installPhase = ''
|
||||
mkdir -p "$out"
|
||||
cp -r ./src/* "$out"/
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default = with pkgs; mkShell {
|
||||
inherit shellHook;
|
||||
nativeBuildInputs = buildInputs ++ [ nodePackages.wrangler ];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
module cdn
|
||||
|
||||
go 1.21.3
|
||||
|
||||
require (
|
||||
github.com/a-h/templ v0.2.476
|
||||
golang.org/x/sync v0.5.0
|
||||
)
|
|
@ -0,0 +1,6 @@
|
|||
github.com/a-h/templ v0.2.476 h1:+H4hP4CwK4kfJwXsE6kHeFWMGtcVOVoOm/I64uzARBk=
|
||||
github.com/a-h/templ v0.2.476/go.mod h1:zQ95mSyadNTGHv6k5Fm+wQU8zkBMMbHCHg7eAvUZKNM=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
@ -0,0 +1,118 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
previewPort = "8080"
|
||||
|
||||
// Specify the starting directory, for consistency
|
||||
_, f, _, _ = runtime.Caller(0)
|
||||
)
|
||||
|
||||
// genListingPages generates listing pages recursively inside provided directory
|
||||
func genListingPages(path string) error {
|
||||
entries, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
indexFile, err := os.Create(filepath.Join(path, "index.html"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := listingPage(path, entries).Render(ctx, indexFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Goroutines are cheap, so just spawn them needlessly \^-^/
|
||||
errGroup := new(errgroup.Group)
|
||||
for _, entry := range entries {
|
||||
entry := entry
|
||||
if entry.IsDir() {
|
||||
errGroup.Go(func() error {
|
||||
return genListingPages(filepath.Join(path, entry.Name()))
|
||||
})
|
||||
}
|
||||
}
|
||||
return errGroup.Wait()
|
||||
}
|
||||
|
||||
// preview Previews the website at specified root directory on localhost:<port>
|
||||
func preview(root, port string) error {
|
||||
http.Handle("/", http.FileServer(
|
||||
http.Dir(
|
||||
filepath.Join(filepath.Dir(f), root),
|
||||
),
|
||||
))
|
||||
fmt.Println("Serving the preview webpage at http://localhost:" + port)
|
||||
srv := &http.Server{
|
||||
Addr: ":" + port,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
return srv.ListenAndServe()
|
||||
}
|
||||
|
||||
// generate generates index.html files for the root directory
|
||||
func generate(root string) error {
|
||||
// NOTE: it also generate one for the root directory. We'll override this file later
|
||||
if err := genListingPages(root); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Render the home page to override the written listing page at root
|
||||
homePage, err := os.Create(filepath.Join(root, "index.html"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return indexPage().Render(ctx, homePage)
|
||||
}
|
||||
|
||||
func main() {
|
||||
genFlag := flag.Bool("gen", false, "Generate index files")
|
||||
previewFlag := flag.Bool("preview", false, "Preview the built website")
|
||||
flag.Usage = func() {
|
||||
fmt.Printf(`Usage: go run *.go [OPTIONS] DIR
|
||||
|
||||
Arguments:
|
||||
DIR
|
||||
Path to the directory containing the source files
|
||||
|
||||
Options:
|
||||
`)
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
// The root directory argument is required
|
||||
if len(flag.Args()) != 1 {
|
||||
log.Fatal("[ERROR] exactly 1 argument DIR is required")
|
||||
}
|
||||
rootDir := flag.Arg(0)
|
||||
|
||||
if *genFlag {
|
||||
if err := generate(rootDir); err != nil {
|
||||
log.Fatalf("[ERROR] %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Only preview after things are generated
|
||||
if *previewFlag {
|
||||
if err := preview(rootDir, previewPort); err != nil {
|
||||
log.Fatalf("[ERROR] %v", err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package main
|
||||
|
||||
import "io/fs"
|
||||
|
||||
var (
|
||||
siteName = "FollieCDN"
|
||||
siteURL = "https://cdn.folliehiyuki.com/"
|
||||
)
|
||||
|
||||
templ pageTemplate(path string) {
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title>
|
||||
if path != "" {
|
||||
{ path + " | " }
|
||||
}
|
||||
{ siteName }
|
||||
</title>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
if path != "" {
|
||||
<meta name="description" content={ path + " | " + siteName }/>
|
||||
} else {
|
||||
<meta name="description" content={ siteName }/>
|
||||
}
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico"/>
|
||||
<link rel="canonical" href={ siteURL + path }/>
|
||||
<link rel="stylesheet" href="/styles/normalize.css" integrity="sha256-Atknw9eu6T9FV6v//wFp8skKdJ5JcAqANQ1bPykR4go=" crossorigin="anonymous"/>
|
||||
<link rel="stylesheet" href="/styles/self.css" integrity="sha256-KReR+W3bFJ+Eu2X+F0lR+bULlbmaLS8SXUhIQJ/p/p4=" crossorigin="anonymous"/>
|
||||
<link rel="stylesheet" href="/fonts/iosevka/iosevka-aile.css" integrity="sha256-20HRMpRlRW2+dk9R7asoOl5/z8Xyc2BbjeLZsButUww=" crossorigin="anonymous"/>
|
||||
<link rel="stylesheet" href="/fonts/font-awesome/solid.css" integrity="sha256-xuw8YIIudFiLKxnOSDufxt0N2CFAJQXsK2lnTUPbofE=" crossorigin="anonymous"/>
|
||||
<link rel="preload" href="/fonts/iosevka/iosevka-aile-bold.woff2" as="font" type="font/woff2" integrity="sha256-Hu4sqJ4m0rjWqqKYxhGKw1Lqz/VpftqKe0CeHACkSto=" crossorigin="anonymous"/>
|
||||
<link rel="preload" href="/fonts/iosevka/iosevka-aile-bolditalic.woff2" as="font" type="font/woff2" integrity="sha256-fRJP2kQFy+xRyveNgFHkYWfHUG/DSkoUrXdfx7RGJMk=" crossorigin="anonymous"/>
|
||||
<link rel="preload" href="/fonts/iosevka/iosevka-aile-italic.woff2" as="font" type="font/woff2" integrity="sha256-gH5Tq4ZWU0MJJQjXsdC00d++xrM842dN8pCVRmBnf0k=" crossorigin="anonymous"/>
|
||||
<link rel="preload" href="/fonts/iosevka/iosevka-aile-regular.woff2" as="font" type="font/woff2" integrity="sha256-y6fQ+gvBFZWjeNgxiWQrYYgni3T5H+TLAwHCsSFSjKE=" crossorigin="anonymous"/>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
{ children... }
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
templ indexPage() {
|
||||
@pageTemplate("") {
|
||||
<h1>FollieHiyuki's personal web assets</h1>
|
||||
<p>
|
||||
Hi! Welcome to my <em>"Content Delivery Network"</em> <s>freeloading</s> running on Cloudflare. The files served here are used to power my websites, including my
|
||||
<a href="https://www.folliehiyuki.com" target="_blank" rel="author noopener external">personal blog</a> and
|
||||
<a href="https://docs.folliehiyuki.com" target="_blank" rel="noopener external">handbook</a>. The source code is avaliable to see on
|
||||
<a href="https://gitlab.com/FollieHiyuki/cdn" target="_blank" rel="noreferrer nofollow external">GitLab</a>.
|
||||
</p>
|
||||
<h2>Packages</h2>
|
||||
<p>
|
||||
<span class="fa-solid"></span> <a href="/fonts/">fonts</a> - Various 3rd-party fonts I use everywhere
|
||||
<br/>
|
||||
<span class="fa-solid"></span> <a href="/styles/">styles</a> - Some minimal, opinionated CSS settings
|
||||
<br/>
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
templ listingPage(path string, entries []fs.DirEntry) {
|
||||
@pageTemplate(path) {
|
||||
<h1>{ path }</h1>
|
||||
<p>
|
||||
<span class="fa-solid"></span> <a href="../">{ "../" }</a>
|
||||
<br/>
|
||||
for _, entry := range entries {
|
||||
// Obviously we need to discard the index file from the UI
|
||||
if entry.Name() != "index.html" {
|
||||
<span class="fa-solid">
|
||||
if entry.IsDir() {
|
||||

|
||||
} else {
|
||||

|
||||
}
|
||||
</span>
|
||||
<a href={ templ.URL("./" + entry.Name()) }>{ entry.Name() }</a>
|
||||
<br/>
|
||||
}
|
||||
}
|
||||
</p>
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title>{{ if not .IsHomePage }}{{ .Path }} | {{ end }}FollieCDN</title>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="{{ if not .IsHomePage }}{{ .Path }} | {{ end }}FollieCDN">
|
||||
|
||||
<link rel="stylesheet" href="/css/normalize.css" integrity="sha256-Atknw9eu6T9FV6v//wFp8skKdJ5JcAqANQ1bPykR4go=" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/css/self.css" integrity="sha256-KReR+W3bFJ+Eu2X+F0lR+bULlbmaLS8SXUhIQJ/p/p4=" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/fonts/iosevka/iosevka-aile.css" integrity="sha256-20HRMpRlRW2+dk9R7asoOl5/z8Xyc2BbjeLZsButUww=" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/fonts/font-awesome/solid.css" integrity="sha256-xuw8YIIudFiLKxnOSDufxt0N2CFAJQXsK2lnTUPbofE=" crossorigin="anonymous">
|
||||
|
||||
<link rel="canonical" href="https://cdn.folliehiyuki.com{{ if not .IsHomePage }}/{{ .Path }}{{ end }}">
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
|
||||
<link rel="preload" href="/fonts/iosevka/iosevka-aile-bold.woff2" as="font" type="font/woff2" integrity="sha256-Hu4sqJ4m0rjWqqKYxhGKw1Lqz/VpftqKe0CeHACkSto=" crossorigin="anonymous">
|
||||
<link rel="preload" href="/fonts/iosevka/iosevka-aile-bolditalic.woff2" as="font" type="font/woff2" integrity="sha256-fRJP2kQFy+xRyveNgFHkYWfHUG/DSkoUrXdfx7RGJMk=" crossorigin="anonymous">
|
||||
<link rel="preload" href="/fonts/iosevka/iosevka-aile-italic.woff2" as="font" type="font/woff2" integrity="sha256-gH5Tq4ZWU0MJJQjXsdC00d++xrM842dN8pCVRmBnf0k=" crossorigin="anonymous">
|
||||
<link rel="preload" href="/fonts/iosevka/iosevka-aile-regular.woff2" as="font" type="font/woff2" integrity="sha256-y6fQ+gvBFZWjeNgxiWQrYYgni3T5H+TLAwHCsSFSjKE=" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>
|
||||
{{ if .IsHomePage }}
|
||||
FollieHiyuki's personal web assets
|
||||
{{ else }}
|
||||
{{ .Path }}
|
||||
{{ end }}
|
||||
</h1>
|
||||
{{ if .IsHomePage }}
|
||||
<p>
|
||||
Hi! Welcome to my <em>"Content Delivery Network"</em> <s>freeloading</s> running on Cloudflare. The files served here are used to power my websites, including my
|
||||
<a href="https://www.folliehiyuki.com" target="_blank" rel="author noopener external">personal blog</a> and
|
||||
<a href="https://docs.folliehiyuki.com" target="_blank" rel="noopener external">handbook</a>. The source code is avaliable to see on
|
||||
<a href="https://gitlab.com/FollieHiyuki/cdn" target="_blank" rel="noreferrer nofollow external">GitLab</a>.
|
||||
</p>
|
||||
<h2>Packages</h2>
|
||||
<p>
|
||||
<span class="fa-solid"></span> <a href="./fonts/">fonts</a> - Various 3rd-party fonts I use everywhere<br>
|
||||
<span class="fa-solid"></span> <a href="./css/">css</a> - Some minimal, opinionated CSS settings<br>
|
||||
</p>
|
||||
{{ else }}
|
||||
<p>
|
||||
<span class="fa-solid"></span> <a href="../">../</a><br>
|
||||
{{ range .Entries }}
|
||||
<span class="fa-solid">{{ if .IsDir }}{{ else }} {{ end }}</span>
|
||||
<a href="./{{ .Name }}">{{ .Name }}</a><br>
|
||||
{{ end }}
|
||||
</p>
|
||||
{{ end }}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
|
@ -1,88 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"flag"
|
||||
"html/template"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// A struct based on fs.DirEntry
|
||||
type Entry struct {
|
||||
Name string
|
||||
IsDir bool
|
||||
}
|
||||
|
||||
//go:embed index.html.tmpl
|
||||
var indexTemplate string
|
||||
|
||||
// Return the list of entries inside a specified directory
|
||||
func getEntries(dirPath string) ([]Entry, error) {
|
||||
dirEntries, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entryList := []Entry{}
|
||||
for _, entry := range dirEntries {
|
||||
// Of course we want to ignore index.html file in the directory listing
|
||||
if entry.Name() != "index.html" {
|
||||
entryName := entry.Name()
|
||||
if entry.IsDir() {
|
||||
entryName = entryName + "/"
|
||||
}
|
||||
entryList = append(entryList, Entry{Name: entryName, IsDir: entry.IsDir()})
|
||||
}
|
||||
}
|
||||
|
||||
return entryList, nil
|
||||
}
|
||||
|
||||
// Output an index.html file listing entries to a specified destination
|
||||
func renderIndexHTML(entries []Entry, root string, dest string, isHomePage bool) error {
|
||||
// Get rid of ./out/ prefix from find(1) result
|
||||
re := regexp.MustCompile(`^\.\/` + root + `\/`)
|
||||
dirPath := re.ReplaceAllString(dest, "")
|
||||
|
||||
tmpl, err := template.New(dirPath).Parse(indexTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Path string
|
||||
Entries []Entry
|
||||
IsHomePage bool
|
||||
}{Path: dirPath, Entries: entries, IsHomePage: isHomePage}
|
||||
|
||||
destFile, err := os.Create(path.Join(dest, "index.html"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tmpl.Execute(destFile, data)
|
||||
}
|
||||
|
||||
func main() {
|
||||
rootDir := flag.String("root", "out", "Directory indicating the root of the website")
|
||||
isHomePage := flag.Bool("home", false, "Whether the directory is the top-level website's index")
|
||||
flag.Parse()
|
||||
|
||||
dirPath := flag.Arg(0)
|
||||
if dirPath == "" {
|
||||
log.Fatalln("Missing argument: directory to output the index.html file")
|
||||
}
|
||||
|
||||
entries, err := getEntries(dirPath)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
err = renderIndexHTML(entries, *rootDir, dirPath, *isHomePage)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.git
|
||||
Dockerfile
|
||||
.dockerignore
|
|
@ -0,0 +1,25 @@
|
|||
# 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*
|
|
@ -0,0 +1,36 @@
|
|||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
dir: cmd/templ
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
|
||||
signs:
|
||||
- artifacts: checksum
|
||||
|
||||
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 }}
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
|
@ -0,0 +1 @@
|
|||
0.2.476
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
@ -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.
|
|
@ -0,0 +1,143 @@
|
|||
![templ](https://github.com/a-h/templ/raw/main/templ.png)
|
||||
|
||||
## A 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
|
||||
```
|
||||
|
||||
### install-snapshot
|
||||
|
||||
Build and install to ~/bin
|
||||
|
||||
```sh
|
||||
rm cmd/templ/lspcmd/*.txt || true
|
||||
go run ./get-version > .version
|
||||
cd cmd/templ && go build -o ~/bin/templ
|
||||
```
|
||||
|
||||
### 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-cover
|
||||
|
||||
Run Go tests.
|
||||
|
||||
```sh
|
||||
# Create test profile directories.
|
||||
mkdir -p coverage/fmt
|
||||
mkdir -p coverage/generate
|
||||
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
|
||||
# Run the unit tests.
|
||||
go test -cover ./... -args -test.gocoverdir="$PWD/coverage/unit"
|
||||
# Display the combined percentage.
|
||||
go tool covdata percent -i=./coverage/fmt,./coverage/generate,./coverage/unit
|
||||
# Generate a text coverage profile for tooling to use.
|
||||
go tool covdata textfmt -i=./coverage/fmt,./coverage/generate,./coverage/unit -o coverage.out
|
||||
# Print total
|
||||
go tool cover -func coverage.out | grep total
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
### release
|
||||
|
||||
Create production build with goreleaser.
|
||||
|
||||
```sh
|
||||
if [ "${GITHUB_TOKEN}" == "" ]; then echo "No github token, run:"; echo "export GITHUB_TOKEN=`pass github.com/goreleaser_access_token`"; exit 1; fi
|
||||
./push-tag.sh
|
||||
goreleaser --clean
|
||||
```
|
||||
|
||||
### docs-run
|
||||
|
||||
Run the development server.
|
||||
|
||||
Directory: docs
|
||||
|
||||
```sh
|
||||
npm run start
|
||||
```
|
||||
|
||||
### docs-build
|
||||
|
||||
Build production docs site.
|
||||
|
||||
Directory: docs
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
|
@ -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.
|
|
@ -0,0 +1,85 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"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": 1694102001,
|
||||
"narHash": "sha256-vky6VPK1n1od6vXbqzOXnekrQpTL4hbPAwUhT5J9c9E=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "9e21c80adf67ebcb077d75bd5e7d724d21eeafd6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1694422566,
|
||||
"narHash": "sha256-lHJ+A9esOz9vln/3CJG23FV6Wd2OoOFbDeEs4cMGMqc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3a2786eea085f040a66ecde1bc3ddc7099f6dbeb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"xc": "xc"
|
||||
}
|
||||
},
|
||||
"xc": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1696495449,
|
||||
"narHash": "sha256-dthZiJ2FX/eIC0l1mdfefJZXDTVLfwp7L7Arq5rsCWA=",
|
||||
"owner": "joerdav",
|
||||
"repo": "xc",
|
||||
"rev": "c8baab14d679fb276f11c576607010283be21220",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "joerdav",
|
||||
"repo": "xc",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
{
|
||||
description = "templ";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
|
||||
gitignore = {
|
||||
url = "github:hercules-ci/gitignore.nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
xc = {
|
||||
url = "github:joerdav/xc";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, gitignore, xc }:
|
||||
let
|
||||
# Systems supported
|
||||
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
|
||||
];
|
||||
|
||||
# Helper to provide system-specific attributes
|
||||
forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f {
|
||||
inherit system;
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
});
|
||||
in
|
||||
rec {
|
||||
packages = forAllSystems ({ pkgs, ... }: rec {
|
||||
default = templ;
|
||||
|
||||
templ = pkgs.buildGo121Module {
|
||||
name = "templ";
|
||||
src = gitignore.lib.gitignoreSource ./.;
|
||||
subPackages = [ "cmd/templ" ];
|
||||
vendorSha256 = "sha256-hbXKWWwrlv0w3SxMgPtDBpluvrbjDRGiJ/9QnRKlwCE=";
|
||||
CGO_ENALBED = 0;
|
||||
|
||||
flags = [
|
||||
"-trimpath"
|
||||
];
|
||||
|
||||
ldflags = [
|
||||
"-s"
|
||||
"-w"
|
||||
"-extldflags -static"
|
||||
];
|
||||
};
|
||||
|
||||
templ-docs = pkgs.buildNpmPackage {
|
||||
name = "templ-docs";
|
||||
src = gitignore.lib.gitignoreSource ./docs;
|
||||
npmDepsHash = "sha256-i6clvSyHtQEGl2C/wcCXonl1W/Kxq7WPTYH46AhUvDM=";
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/share
|
||||
cp -r build/ $out/share/docs
|
||||
'';
|
||||
};
|
||||
});
|
||||
|
||||
# `nix develop` provides a shell containing required tools for development
|
||||
devShell = forAllSystems ({ system, pkgs }:
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
(golangci-lint.override { buildGoModule = buildGo121Module; })
|
||||
go_1_21
|
||||
goreleaser
|
||||
nodejs
|
||||
xc.packages.${system}.xc
|
||||
];
|
||||
});
|
||||
|
||||
# Allows users to install the package on their system in an easy way
|
||||
overlays.default = final: prev:
|
||||
forAllSystems ({ system, ... }: {
|
||||
templ = packages.${system}.templ;
|
||||
templ-docs = packages.${system}.templ-docs;
|
||||
});
|
||||
};
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 531 KiB |
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
export VERSION=`cat .version`
|
||||
echo Adding git tag with version v${VERSION};
|
||||
git tag v${VERSION};
|
||||
git push origin v${VERSION};
|
|
@ -0,0 +1,645 @@
|
|||
package templ
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
const componentHandlerErrorMessage = "templ: failed to render template"
|
||||
|
||||
// ServeHTTP implements the http.Handler interface.
|
||||
func (ch ComponentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if ch.Status != 0 {
|
||||
w.WriteHeader(ch.Status)
|
||||
}
|
||||
w.Header().Add("Content-Type", ch.ContentType)
|
||||
err := ch.Component.Render(r.Context(), w)
|
||||
if err != nil {
|
||||
if ch.ErrorHandler != nil {
|
||||
ch.ErrorHandler(r, err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
http.Error(w, componentHandlerErrorMessage, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler creates a http.Handler that renders the template.
|
||||
func Handler(c Component, options ...func(*ComponentHandler)) *ComponentHandler {
|
||||
ch := &ComponentHandler{
|
||||
Component: c,
|
||||
ContentType: "text/html",
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// WithConentType 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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 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]
|
||||
return fmt.Sprintf("%s_%s", 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 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
|
||||
|
||||
// SanitizeCSS sanitizes CSS properties to ensure that they are safe.
|
||||
func SanitizeCSS(property, value string) SafeCSS {
|
||||
p, v := safehtml.SanitizeCSS(property, value)
|
||||
return SafeCSS(p + ":" + v + ";")
|
||||
}
|
||||
|
||||
// Hyperlink sanitization.
|
||||
|
||||
// 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") {
|
||||
return FailedSanitizationURL
|
||||
}
|
||||
}
|
||||
return SafeURL(s)
|
||||
}
|
||||
|
||||
// SafeURL is a URL that has been sanitized.
|
||||
type SafeURL string
|
||||
|
||||
// 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{}
|
||||
children *Component
|
||||
}
|
||||
|
||||
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 (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 = io.WriteString(w, `<script type="text/javascript">`); 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 = io.WriteString(w, `<script type="text/javascript">`); 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)
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
// 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) {
|
||||
if !identifierPattern.MatchString(property) {
|
||||
return InnocuousPropertyName, InnocuousPropertyValue
|
||||
}
|
||||
property = strings.ToLower(property)
|
||||
if sanitizer, ok := cssPropertyNameToValueSanitizer[property]; ok {
|
||||
return property, sanitizer(value)
|
||||
}
|
||||
return property, sanitizeRegular(value)
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
func sanitizeBackgroundImage(v string) string {
|
||||
for _, u := range strings.Split(v, ",") {
|
||||
u = strings.TrimSpace(u)
|
||||
if !strings.HasPrefix(u, `url("`) {
|
||||
return InnocuousPropertyValue
|
||||
}
|
||||
if !strings.HasSuffix(u, `")`) {
|
||||
return InnocuousPropertyValue
|
||||
}
|
||||
u := u[5 : len(u)-2]
|
||||
if !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-]*$`)
|
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,6 @@
|
|||
package templ
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed .version
|
||||
var Version string
|
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,22 @@
|
|||
Additional IP Rights Grant (Patents)
|
||||
|
||||
"This implementation" means the copyrightable works distributed by
|
||||
Google as part of the Go project.
|
||||
|
||||
Google hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||
no-charge, royalty-free, irrevocable (except as stated in this section)
|
||||
patent license to make, have made, use, offer to sell, sell, import,
|
||||
transfer and otherwise run, modify and propagate the contents of this
|
||||
implementation of Go, where such license applies only to those patent
|
||||
claims, both currently owned or controlled by Google and acquired in
|
||||
the future, licensable by Google that are necessarily infringed by this
|
||||
implementation of Go. This grant does not include claims that would be
|
||||
infringed only as a consequence of further modification of this
|
||||
implementation. If you or your agent or exclusive licensee institute or
|
||||
order or agree to the institution of patent litigation against any
|
||||
entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||
that this implementation of Go or any code incorporated within this
|
||||
implementation of Go constitutes direct or contributory patent
|
||||
infringement, or inducement of patent infringement, then any patent
|
||||
rights granted to you under this License for this implementation of Go
|
||||
shall terminate as of the date such litigation is filed.
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright 2016 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.
|
||||
|
||||
// Package errgroup provides synchronization, error propagation, and Context
|
||||
// cancelation for groups of goroutines working on subtasks of a common task.
|
||||
package errgroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type token struct{}
|
||||
|
||||
// A Group is a collection of goroutines working on subtasks that are part of
|
||||
// the same overall task.
|
||||
//
|
||||
// A zero Group is valid, has no limit on the number of active goroutines,
|
||||
// and does not cancel on error.
|
||||
type Group struct {
|
||||
cancel func(error)
|
||||
|
||||
wg sync.WaitGroup
|
||||
|
||||
sem chan token
|
||||
|
||||
errOnce sync.Once
|
||||
err error
|
||||
}
|
||||
|
||||
func (g *Group) done() {
|
||||
if g.sem != nil {
|
||||
<-g.sem
|
||||
}
|
||||
g.wg.Done()
|
||||
}
|
||||
|
||||
// WithContext returns a new Group and an associated Context derived from ctx.
|
||||
//
|
||||
// The derived Context is canceled the first time a function passed to Go
|
||||
// returns a non-nil error or the first time Wait returns, whichever occurs
|
||||
// first.
|
||||
func WithContext(ctx context.Context) (*Group, context.Context) {
|
||||
ctx, cancel := withCancelCause(ctx)
|
||||
return &Group{cancel: cancel}, ctx
|
||||
}
|
||||
|
||||
// Wait blocks until all function calls from the Go method have returned, then
|
||||
// returns the first non-nil error (if any) from them.
|
||||
func (g *Group) Wait() error {
|
||||
g.wg.Wait()
|
||||
if g.cancel != nil {
|
||||
g.cancel(g.err)
|
||||
}
|
||||
return g.err
|
||||
}
|
||||
|
||||
// Go calls the given function in a new goroutine.
|
||||
// It blocks until the new goroutine can be added without the number of
|
||||
// active goroutines in the group exceeding the configured limit.
|
||||
//
|
||||
// The first call to return a non-nil error cancels the group's context, if the
|
||||
// group was created by calling WithContext. The error will be returned by Wait.
|
||||
func (g *Group) Go(f func() error) {
|
||||
if g.sem != nil {
|
||||
g.sem <- token{}
|
||||
}
|
||||
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.done()
|
||||
|
||||
if err := f(); err != nil {
|
||||
g.errOnce.Do(func() {
|
||||
g.err = err
|
||||
if g.cancel != nil {
|
||||
g.cancel(g.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// TryGo calls the given function in a new goroutine only if the number of
|
||||
// active goroutines in the group is currently below the configured limit.
|
||||
//
|
||||
// The return value reports whether the goroutine was started.
|
||||
func (g *Group) TryGo(f func() error) bool {
|
||||
if g.sem != nil {
|
||||
select {
|
||||
case g.sem <- token{}:
|
||||
// Note: this allows barging iff channels in general allow barging.
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.done()
|
||||
|
||||
if err := f(); err != nil {
|
||||
g.errOnce.Do(func() {
|
||||
g.err = err
|
||||
if g.cancel != nil {
|
||||
g.cancel(g.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}()
|
||||
return true
|
||||
}
|
||||
|
||||
// SetLimit limits the number of active goroutines in this group to at most n.
|
||||
// A negative value indicates no limit.
|
||||
//
|
||||
// Any subsequent call to the Go method will block until it can add an active
|
||||
// goroutine without exceeding the configured limit.
|
||||
//
|
||||
// The limit must not be modified while any goroutines in the group are active.
|
||||
func (g *Group) SetLimit(n int) {
|
||||
if n < 0 {
|
||||
g.sem = nil
|
||||
return
|
||||
}
|
||||
if len(g.sem) != 0 {
|
||||
panic(fmt.Errorf("errgroup: modify limit while %v goroutines in the group are still active", len(g.sem)))
|
||||
}
|
||||
g.sem = make(chan token, n)
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright 2023 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.
|
||||
|
||||
//go:build go1.20
|
||||
|
||||
package errgroup
|
||||
|
||||
import "context"
|
||||
|
||||
func withCancelCause(parent context.Context) (context.Context, func(error)) {
|
||||
return context.WithCancelCause(parent)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright 2023 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.
|
||||
|
||||
//go:build !go1.20
|
||||
|
||||
package errgroup
|
||||
|
||||
import "context"
|
||||
|
||||
func withCancelCause(parent context.Context) (context.Context, func(error)) {
|
||||
ctx, cancel := context.WithCancel(parent)
|
||||
return ctx, func(error) { cancel() }
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
# github.com/a-h/templ v0.2.476
|
||||
## explicit; go 1.20
|
||||
github.com/a-h/templ
|
||||
github.com/a-h/templ/safehtml
|
||||
# golang.org/x/sync v0.5.0
|
||||
## explicit; go 1.18
|
||||
golang.org/x/sync/errgroup
|
Loading…
Reference in New Issue