diff --git a/cmd/snips/generatecmd/eventhandler.go b/cmd/snips/generatecmd/eventhandler.go index 62fbb13..b1e0c52 100644 --- a/cmd/snips/generatecmd/eventhandler.go +++ b/cmd/snips/generatecmd/eventhandler.go @@ -5,7 +5,6 @@ import ( "context" "crypto/sha256" "fmt" - "go/format" "io" "log/slog" "os" @@ -14,6 +13,10 @@ import ( "strings" "sync" "time" + "unicode" + + "github.com/a-h/templ/cmd/templ/imports" + parser "github.com/a-h/templ/parser/v2" "github.com/alecthomas/chroma/v2/formatters/html" "github.com/fsnotify/fsnotify" @@ -175,54 +178,104 @@ func (h *FSEventHandler) UpsertHash(fileName string, hash [sha256.Size]byte) (up // generate Go code for a single template. // If a basePath is provided, the filename included in error messages is relative to it. func (h *FSEventHandler) generate(fileName string) (goUpdated, textUpdated bool, err error) { - fmt.Println(fileName, snips.PackageName(fileName)) - - // remove .code. from the filename - targetFileName := fileName + ".templ" + og := fileName + fileName = stripCode(fileName) + targetFileName := fileName + "_templ.go" - // Only use relative filenames to the basepath for filenames in runtime error messages. - absFilePath, err := filepath.Abs(fileName) - if err != nil { - return false, false, fmt.Errorf("failed to get absolute path for %q: %w", fileName, err) - } - relFilePath, err := filepath.Rel(h.dir, absFilePath) - if err != nil { - return false, false, fmt.Errorf("failed to get relative path for %q: %w", fileName, err) + parts := strings.Split(filepath.ToSlash(fileName), "/") + if len(parts) == 0 { + return false, false, fmt.Errorf("unexpected file name %q", fileName) } - // Convert Windows file paths to Unix-style for consistency. - relFilePath = filepath.ToSlash(relFilePath) + + fileName = sanitzeFileName(parts[len(parts)-1]) + dir := strings.Join(parts[:len(parts)-1], "/") var b bytes.Buffer - literals, err := generator.Generate(&b, h.genOpts, generator.WithFileName(relFilePath)) + _, err = generator.Generate(&b, h.genOpts, og, snips.PackageName(dir), fileName) if err != nil { return false, false, fmt.Errorf("%s generation error: %w", fileName, err) } + formattedCode := b.Bytes() + // formattedCode, _, err := format(b.String(), false) + // if err != nil { + // return false, false, fmt.Errorf("% source formatting error %w", fileName, err) + // } - formattedGoCode, err := format.Source(b.Bytes()) - if err != nil { - return false, false, fmt.Errorf("% source formatting error %w", fileName, err) - } - - // Hash output, and write out the file if the goCodeHash has changed. - goCodeHash := sha256.Sum256(formattedGoCode) - if h.UpsertHash(targetFileName, goCodeHash) { + // Hash output, and write out the file if the codeHash has changed. + codeHash := sha256.Sum256(formattedCode) + if h.UpsertHash(targetFileName, codeHash) { goUpdated = true - if err = h.writer(targetFileName, formattedGoCode); err != nil { + if err = h.writer(targetFileName, formattedCode); err != nil { return false, false, fmt.Errorf("failed to write target file %q: %w", targetFileName, err) } } - // Add the txt file if it has changed. - if len(literals) > 0 { - txtFileName := "_code.txt" - txtHash := sha256.Sum256([]byte(literals)) - if h.UpsertHash(txtFileName, txtHash) { - textUpdated = true - if err = os.WriteFile(txtFileName, []byte(literals), 0o644); err != nil { - return false, false, fmt.Errorf("failed to write string literal file %q: %w", txtFileName, err) + // // Add the txt file if it has changed. + // if len(literals) > 0 { + // txtFileName := "_code.txt" + // txtHash := sha256.Sum256([]byte(literals)) + // if h.UpsertHash(txtFileName, txtHash) { + // textUpdated = true + // if err = os.WriteFile(txtFileName, []byte(literals), 0o644); err != nil { + // return false, false, fmt.Errorf("failed to write string literal file %q: %w", txtFileName, err) + // } + // } + // } + + return goUpdated, textUpdated, err +} + +func stripCode(fileName string) string { + parts := strings.Split(fileName, ".code") + if len(parts) != 2 { + return fileName + } + return parts[0] + parts[1] +} + +func sanitzeFileName(fileName string) string { + var result []rune + firstLetter := true + for _, char := range fileName { + if char == ' ' { + firstLetter = true + continue + } + + if unicode.IsLetter(char) || unicode.IsDigit(char) { + if firstLetter { + result = append(result, unicode.ToUpper(char)) + firstLetter = false + } else { + result = append(result, char) } + } else { + firstLetter = true } } + return string(result) +} - return goUpdated, textUpdated, err +func format(src string, writeIfUnchanged bool) (res []byte, fileChanged bool, err error) { + t, err := parser.ParseString(src) + if err != nil { + return nil, false, err + } + t.Filepath = "" + t, err = imports.Process(t) + if err != nil { + return nil, false, err + } + w := new(bytes.Buffer) + if err = t.Write(w); err != nil { + return nil, false, fmt.Errorf("formatting error: %w", err) + } + + fileChanged = (src != w.String()) + + if !writeIfUnchanged && !fileChanged { + return nil, fileChanged, nil + } + + return w.Bytes(), fileChanged, nil } diff --git a/generator/generator.go b/generator/generator.go index 91b348e..f53b5ca 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -1,13 +1,16 @@ package generator import ( + "bytes" "io" - "path/filepath" + "os" "strings" "time" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/alecthomas/chroma/v2/lexers" + "github.com/alecthomas/chroma/v2/styles" ) type GenerateOpt func(g *generator) error @@ -28,18 +31,6 @@ func WithTimestamp(d time.Time) GenerateOpt { } } -// WithFileName sets the filename of the templ file in template rendering error messages. -func WithFileName(name string) GenerateOpt { - return func(g *generator) error { - if filepath.IsAbs(name) { - _, g.fileName = filepath.Split(name) - return nil - } - g.fileName = name - return nil - } -} - func WithExtractStrings() GenerateOpt { return func(g *generator) error { g.w.literalWriter = &watchLiteralWriter{ @@ -57,14 +48,20 @@ type generator struct { version string // generatedDate to include as a comment. generatedDate string - // fileName to include as a comment. - fileName string + // packageName to use in the generated code. + packageName string + // componentName to use in the generated code. + componentName string + og string } -func Generate(w io.Writer, htmlOpts []html.Option, opts ...GenerateOpt) (literals string, err error) { +func Generate(w io.Writer, htmlOpts []html.Option, path string, packageName string, componentName string, opts ...GenerateOpt) (literals string, err error) { g := generator{ - f: html.New(htmlOpts...), - w: NewRangeWriter(w), + f: html.New(htmlOpts...), + w: NewRangeWriter(w), + packageName: packageName, + componentName: componentName, + og: path, } for _, opt := range opts { @@ -91,6 +88,15 @@ func (g *generator) generate() (err error) { if err = g.writePackage(); err != nil { return } + if err = g.writeImports(); err != nil { + return + } + if err = g.writFoo(); err != nil { + return + } + if err = g.writeBlankAssignmentForRuntimeImport(); err != nil { + return + } return err } @@ -117,8 +123,187 @@ func (g *generator) writeGeneratedDateComment() (err error) { return err } -func (g *generator) writePackage() error { - // package ... - _, err := g.w.Write("package snips") +func (g *generator) writePackage() (err error) { + if _, err := g.w.Write("package " + g.packageName + "\n\n"); err != nil { + return err + } + if _, err = g.w.Write("//lint:file-ignore SA4006 This context is only used if a nested component is present.\n\n"); err != nil { + return err + } return err } + +func (g *generator) writeImports() error { + var err error + // Always import templ because it's the interface type of all templates. + if _, err = g.w.Write("import \"github.com/a-h/templ\"\n"); err != nil { + return err + } + if _, err = g.w.Write("import templruntime \"github.com/a-h/templ/runtime\"\n"); err != nil { + return err + } + if _, err = g.w.Write("\n"); err != nil { + return err + } + return nil +} + +func (g *generator) writFoo() (err error) { + if _, err = g.w.Write("func " + g.componentName + "() templ.Component {\n"); err != nil { + return + } + if _, err = g.w.Write("\treturn templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {\n"); err != nil { + return + } + if _, err = g.w.Write("\t\ttempl_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context\n"); err != nil { + return + } + if _, err = g.w.Write("\t\tif templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {\n"); err != nil { + return + } + if _, err = g.w.Write("\t\t\treturn templ_7745c5c3_CtxErr\n"); err != nil { + return + } + if _, err = g.w.Write("\t\t}\n"); err != nil { + return + } + if _, err = g.w.Write("\t\ttempl_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)\n"); err != nil { + return + } + if _, err = g.w.Write("\t\tif !templ_7745c5c3_IsBuffer {\n"); err != nil { + return + } + if _, err = g.w.Write("\t\t\tdefer func() {\n"); err != nil { + return + } + if _, err = g.w.Write("\t\t\t\ttempl_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)\n"); err != nil { + return + } + if _, err = g.w.Write("\t\t\t\tif templ_7745c5c3_Err == nil {\n"); err != nil { + return + } + if _, err = g.w.Write("\t\t\t\t\ttempl_7745c5c3_Err = templ_7745c5c3_BufErr\n"); err != nil { + return + } + if _, err = g.w.Write("\t\t\t\t}\n"); err != nil { + return + } + if _, err = g.w.Write("\t\t\t}()\n"); err != nil { + return + } + if _, err = g.w.Write("\t\t}\n"); err != nil { + return + } + if _, err = g.w.Write("\t\tctx = templ.InitializeContext(ctx)\n"); err != nil { + return + } + if _, err = g.w.Write("\t\ttempl_7745c5c3_Var1 := templ.GetChildren(ctx)\n"); err != nil { + return + } + if _, err = g.w.Write("\t\tif templ_7745c5c3_Var1 == nil {\n"); err != nil { + return + } + if _, err = g.w.Write("\t\t\ttempl_7745c5c3_Var1 = templ.NopComponent\n"); err != nil { + return + } + if _, err = g.w.Write("\t\t}\n"); err != nil { + return + } + if _, err = g.w.Write("\t\tctx = templ.ClearChildren(ctx)\n"); err != nil { + return + } + // MARK + f, err := os.ReadFile(g.og) + contents, err := io.ReadAll(bytes.NewReader(f)) + lexer := lexers.Analyse(string(contents)) + if lexer == nil { + lexer = lexers.Fallback + } + lexer = chroma.Coalesce(lexer) + formatter := html.New() + style := styles.Get("monkokailight") + if style == nil { + style = styles.Fallback + } + iterator, err := lexer.Tokenise(nil, string(contents)) + var b bytes.Buffer + err = formatter.Format(&b, style, iterator) + if err != nil { + return err + } + + formattedHTML := strings.ReplaceAll(b.String(), `"`, `\"`) + formattedHTML = strings.ReplaceAll(formattedHTML, "\n", "\\n") + + if _, err = g.w.Write("\t\t_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(\"" + formattedHTML + "\")\n"); err != nil { + return + } + if _, err = g.w.Write("\t\tif templ_7745c5c3_Err != nil {\n"); err != nil { + return + } + if _, err = g.w.Write("\t\t\treturn templ_7745c5c3_Err\n"); err != nil { + return + } + if _, err = g.w.Write("\t\t}\n"); err != nil { + return + } + if _, err = g.w.Write("\t\treturn templ_7745c5c3_Err\n"); err != nil { + return + } + if _, err = g.w.Write("\t})\n"); err != nil { + return + } + if _, err = g.w.Write("}\n"); err != nil { + return + } + return nil +} + +func (g *generator) writeComponent() (err error) { + if _, err = g.w.Write("templ " + g.componentName + "() {\n"); err != nil { + return + } + + f, err := os.ReadFile(g.og) + contents, err := io.ReadAll(bytes.NewReader(f)) + lexer := lexers.Analyse(string(contents)) + if lexer == nil { + lexer = lexers.Fallback + } + lexer = chroma.Coalesce(lexer) + formatter := html.New(html.PreventSurroundingPre(true), html.InlineCode(true)) + style := styles.Get("monkokailight") + if style == nil { + style = styles.Fallback + } + iterator, err := lexer.Tokenise(nil, string(contents)) + var b bytes.Buffer + err = formatter.Format(&b, style, iterator) + if err != nil { + return err + } + // if _, err = g.w.Write("
 {`\n" + b.String() + "\n`}
"); err != nil { + + if _, err = g.w.Write(b.String()); err != nil { + return err + } + if _, err = g.w.Write("\n"); err != nil { + return err + } + + if _, err := g.w.Write("}\n"); err != nil { + return err + } + return err +} + +// writeBlankAssignmentForRuntimeImport writes out a blank identifier assignment. +// This ensures that even if the github.com/a-h/templ/runtime package is not used in the generated code, +// the Go compiler will not complain about the unused import. +func (g *generator) writeBlankAssignmentForRuntimeImport() error { + var err error + if _, err = g.w.Write("var _ = templruntime.GeneratedTemplate"); err != nil { + return err + } + return nil +} diff --git a/go.mod b/go.mod index 6e182a8..0ec182a 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,18 @@ module github.com/garrettladley/snips go 1.23.2 require ( - github.com/a-h/templ v0.2.778 + github.com/a-h/templ v0.2.793 github.com/fatih/color v1.17.0 github.com/fsnotify/fsnotify v1.7.0 golang.org/x/mod v0.20.0 ) -require github.com/dlclark/regexp2 v1.11.0 // indirect +require ( + github.com/a-h/parse v0.0.0-20240121214402-3caf7543159a // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/tools v0.24.0 // indirect +) require ( github.com/alecthomas/chroma/v2 v2.14.0 diff --git a/go.sum b/go.sum index b1e55c7..5f418b2 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ -github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM= -github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w= +github.com/a-h/parse v0.0.0-20240121214402-3caf7543159a h1:vlmAfVwFK9sRpDlJyuHY8htP+KfGHB2VH02u0SoIufk= +github.com/a-h/parse v0.0.0-20240121214402-3caf7543159a/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= +github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY= +github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= @@ -23,7 +25,11 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= diff --git a/package.go b/package.go index dd4d47d..61ed1ad 100644 --- a/package.go +++ b/package.go @@ -44,5 +44,5 @@ func fallback(dir string) (name string) { return parts[n-1] } - return "" + return "main" } diff --git a/package_test.go b/package_test.go index 432cdd7..fec0e6b 100644 --- a/package_test.go +++ b/package_test.go @@ -32,7 +32,7 @@ func TestPackageNameDifferentFromDirectory(t *testing.T) { func TestPackageNameFallback(t *testing.T) { dir := createTempDir(t) - filePath := filepath.Join(dir, "views", "foo", "ex.rs") + filePath := filepath.Join(dir, "views", "foo", "ex.code.rs") createTempFile(t, filePath, "fn main() {\n println!(\"Hello World!\");\n}") pkg := snips.PackageName(filepath.Join(dir, "views", "foo"))