diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
new file mode 100644
index 0000000..7614095
--- /dev/null
+++ b/.github/workflows/go.yml
@@ -0,0 +1,75 @@
+# This workflow will build a golang project
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
+
+name: Go
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+permissions:
+ contents: read
+ pull-requests: read
+
+jobs:
+
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: '1.21'
+
+ - name: Build
+ run: go build -v ./...
+
+
+
+ golangci:
+ name: lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v4
+ with:
+ go-version: '1.21'
+ cache: false
+ - name: Test
+ run: go test -v ./...
+ - name: golangci-lint
+ uses: golangci/golangci-lint-action@v3
+ with:
+ # Require: The version of golangci-lint to use.
+ # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version.
+ # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit.
+ version: v1.54
+
+ # Optional: working directory, useful for monorepos
+ # working-directory: somedir
+
+ # Optional: golangci-lint command line arguments.
+ #
+ # Note: By default, the `.golangci.yml` file should be at the root of the repository.
+ # The location of the configuration file can be changed by using `--config=`
+ # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0
+
+ # Optional: show only new issues if it's a pull request. The default value is `false`.
+ # only-new-issues: true
+
+ # Optional: if set to true, then all caching functionality will be completely disabled,
+ # takes precedence over all other caching options.
+ # skip-cache: true
+
+ # Optional: if set to true, then the action won't cache or restore ~/go/pkg.
+ # skip-pkg-cache: true
+
+ # Optional: if set to true, then the action won't cache or restore ~/.cache/go-build.
+ # skip-build-cache: true
+
+ # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'.
+ # install-mode: "goinstall"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..2734d12
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2020 euforic
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..daa9cf4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,293 @@
+# templit
+[![Go Report Card](https://goreportcard.com/badge/github.com/euforic/templit)](https://goreportcard.com/report/github.com/euforic/templit)
+[![GoDoc](https://godoc.org/github.com/euforic/templit?status.svg)](https://godoc.org/github.com/euforic/templit)
+![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/euforic/templit)
+![Build Status](https://github.com/euforic/templit/workflows/Go/badge.svg)
+![GitHub](https://img.shields.io/github/license/euforic/templit)
+
+```go
+import "github.com/euforic/templit"
+```
+
+## Index
+
+- [Variables](<#variables>)
+- [func EmbedFunc\(client GitClient\) func\(remotePath string, data interface\{\}, funcMap template.FuncMap\) \(string, error\)](<#EmbedFunc>)
+- [func ImportFunc\(client GitClient\) func\(repoAndTag, destPath string, data interface\{\}, funcMap template.FuncMap\) \(string, error\)](<#ImportFunc>)
+- [func RenderTemplate\(tmpl string, data interface\{\}, funcMap template.FuncMap\) \(string, error\)](<#RenderTemplate>)
+- [func ToCamelCase\(s string\) string](<#ToCamelCase>)
+- [func ToKebabCase\(s string\) string](<#ToKebabCase>)
+- [func ToPascalCase\(s string\) string](<#ToPascalCase>)
+- [func ToSnakeCase\(s string\) string](<#ToSnakeCase>)
+- [func WalkAndProcessDir\(inputDir, outputDir string, funcMap template.FuncMap, data interface\{\}\) error](<#WalkAndProcessDir>)
+- [type DefaultGitClient](<#DefaultGitClient>)
+ - [func NewDefaultGitClient\(token string\) \*DefaultGitClient](<#NewDefaultGitClient>)
+ - [func \(d \*DefaultGitClient\) Checkout\(path, branch string\) error](<#DefaultGitClient.Checkout>)
+ - [func \(d \*DefaultGitClient\) Clone\(host, owner, repo, dest string\) error](<#DefaultGitClient.Clone>)
+- [type DepInfo](<#DepInfo>)
+ - [func ParseDepURL\(rawURL string\) \(\*DepInfo, error\)](<#ParseDepURL>)
+- [type Executor](<#Executor>)
+ - [func NewExecutor\(inputPath string, funcMap template.FuncMap\) \(\*Executor, error\)](<#NewExecutor>)
+ - [func \(e Executor\) Render\(name string, data interface\{\}\) \(string, error\)](<#Executor.Render>)
+- [type GitClient](<#GitClient>)
+- [type WalkAndProcessDirFunc](<#WalkAndProcessDirFunc>)
+
+
+## Variables
+
+DefaultFuncMap is the default function map for templates.
+
+```go
+var DefaultFuncMap = template.FuncMap{
+ "lower": strings.ToLower,
+ "upper": strings.ToUpper,
+ "trim": strings.TrimSpace,
+ "split": strings.Split,
+ "join": strings.Join,
+ "replace": strings.ReplaceAll,
+ "contains": strings.Contains,
+ "hasPrefix": strings.HasPrefix,
+ "hasSuffix": strings.HasSuffix,
+ "trimPrefix": strings.TrimPrefix,
+ "trimSuffix": strings.TrimSuffix,
+ "trimSpace": strings.TrimSpace,
+ "trimLeft": strings.TrimLeft,
+ "trimRight": strings.TrimRight,
+ "count": strings.Count,
+ "repeat": strings.Repeat,
+ "equalFold": strings.EqualFold,
+ "splitN": strings.SplitN,
+ "splitAfter": strings.SplitAfter,
+ "splitAfterN": strings.SplitAfterN,
+ "fields": strings.Fields,
+ "toTitle": strings.ToTitle,
+ "toSnakeCase": ToSnakeCase,
+ "toCamelCase": ToCamelCase,
+ "toKebabCase": ToKebabCase,
+ "toPascalCase": ToPascalCase,
+ "default": DefaultVal,
+}
+```
+
+
+## func EmbedFunc
+
+```go
+func EmbedFunc(client GitClient) func(remotePath string, data interface{}, funcMap template.FuncMap) (string, error)
+```
+
+EmbedFunc returns a template function that can be used to process and embed a template from a remote git repository. EmbedFunc allows embedding content from a remote repository directly into a Go template.
+
+Steps to use:
+
+1. Add the function to the FuncMap.
+2. Use the following syntax within your template:
+ ```go
+ {{ embed "///@" . }}
+ {{ embed "//#@" . }}
+ ```
+
+Placeholders:
+
+- ``: Repository hosting service (e.g., "github.com").
+- ``: Repository owner or organization.
+- ``: Repository name.
+- ``: Path to the desired template file within the repository.
+- ``: Specific template block name.
+- ``: Specific Git reference (tag, commit hash, or branch name).
+
+
+## func ImportFunc
+
+```go
+func ImportFunc(client GitClient) func(repoAndTag, destPath string, data interface{}, funcMap template.FuncMap) (string, error)
+```
+
+ImportFunc returns a function that can be used as a template function to import and process a template from a remote git repository. ImportFunc allows embedding content from a remote repository into a Go template.
+
+Steps to use:
+
+1. Add the function to the FuncMap.
+2. Use the following syntax within your template:
+ ```go
+ {{ import "///@" "" . }}
+ ```
+
+Placeholders:
+
+- ``: Repository hosting service (e.g., "github.com").
+- ``: Repository owner or organization.
+- ``: Repository name.
+- ``: Path to the desired file or directory within the repository.
+- ``: Specific Git reference (tag, commit hash, or branch name).
+
+
+
+## func RenderTemplate
+
+```go
+func RenderTemplate(tmpl string, data interface{}, funcMap template.FuncMap) (string, error)
+```
+
+RenderTemplate renders a template with provided data.
+
+
+## func ToCamelCase
+
+```go
+func ToCamelCase(s string) string
+```
+
+ToCamelCase converts a string to CamelCase.
+
+
+## func ToKebabCase
+
+```go
+func ToKebabCase(s string) string
+```
+
+ToKebabCase converts a string to kebab\-case.
+
+
+## func ToPascalCase
+
+```go
+func ToPascalCase(s string) string
+```
+
+ToPascalCase converts a string to PascalCase.
+
+
+## func ToSnakeCase
+
+```go
+func ToSnakeCase(s string) string
+```
+
+ToSnakeCase converts a string to snake\_case.
+
+
+## func WalkAndProcessDir
+
+```go
+func WalkAndProcessDir(inputDir, outputDir string, funcMap template.FuncMap, data interface{}) error
+```
+
+WalkAndProcessDir processes all files in a directory with the given data. If walkFunc is provided, it's called for each file and directory without writing the file to disk.
+
+
+## type DefaultGitClient
+
+DefaultGitClient provides a default implementation for the GitClient interface.
+
+```go
+type DefaultGitClient struct {
+ Token string
+ BaseURL string
+}
+```
+
+
+### func NewDefaultGitClient
+
+```go
+func NewDefaultGitClient(token string) *DefaultGitClient
+```
+
+NewDefaultGitClient creates a new DefaultGitClient with the given token.
+
+
+### func \(\*DefaultGitClient\) Checkout
+
+```go
+func (d *DefaultGitClient) Checkout(path, branch string) error
+```
+
+Checkout checks out a branch in a Git repository.
+
+
+### func \(\*DefaultGitClient\) Clone
+
+```go
+func (d *DefaultGitClient) Clone(host, owner, repo, dest string) error
+```
+
+Clone clones a Git repository to the given destination.
+
+
+## type DepInfo
+
+DepInfo contains information about an embed URL.
+
+```go
+type DepInfo struct {
+ Host string
+ Owner string
+ Repo string
+ Path string
+ Block string
+ Tag string
+}
+```
+
+
+### func ParseDepURL
+
+```go
+func ParseDepURL(rawURL string) (*DepInfo, error)
+```
+
+ParseDepURL is a parsed embed URL.
+
+
+## type Executor
+
+Executor is a wrapper around the template.Template type
+
+```go
+type Executor struct {
+ *template.Template
+}
+```
+
+
+### func NewExecutor
+
+```go
+func NewExecutor(inputPath string, funcMap template.FuncMap) (*Executor, error)
+```
+
+NewExecutor creates a new Executor with the given template and funcMap
+
+
+### func \(Executor\) Render
+
+```go
+func (e Executor) Render(name string, data interface{}) (string, error)
+```
+
+Render executes the template with the given data
+
+
+## type GitClient
+
+GitClient is an interface that abstracts Git operations.
+
+```go
+type GitClient interface {
+ Clone(host, owner, repo, dest string) error
+ Checkout(path, branch string) error
+}
+```
+
+
+## type WalkAndProcessDirFunc
+
+WalkAndProcessDirFunc is called for each file and directory when walking a directory.
+
+```go
+type WalkAndProcessDirFunc func(path string, isDir bool, content string) error
+```
+
+
diff --git a/dep.go b/dep.go
new file mode 100644
index 0000000..c7bad7e
--- /dev/null
+++ b/dep.go
@@ -0,0 +1,142 @@
+package templit
+
+import (
+ "fmt"
+ "net/url"
+ "strings"
+
+ git "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/transport/http"
+)
+
+// DepInfo contains information about an embed URL.
+type DepInfo struct {
+ Host string
+ Owner string
+ Repo string
+ Path string
+ Block string
+ Tag string
+}
+
+// ParseDepURL is a parsed embed URL.
+func ParseDepURL(rawURL string) (*DepInfo, error) {
+ if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
+ rawURL = "https://" + rawURL
+ }
+
+ u, err := url.Parse(rawURL)
+ if err != nil {
+ return nil, err
+ }
+
+ // Split path into components
+ pathParts := strings.Split(strings.Trim(u.Path, "/"), "/")
+ if len(pathParts) < 2 {
+ return nil, fmt.Errorf("invalid path format in embed URL")
+ }
+
+ owner := pathParts[0]
+ repo := pathParts[1]
+ path := ""
+ if len(pathParts) > 2 {
+ path = strings.Join(pathParts[2:], "/")
+ }
+
+ // Extract block and version
+ block, tag := extractBlockAndTag(u.Fragment)
+
+ if tag == "" {
+ if strings.Contains(path, "@") {
+ path, tag = splitAtSign(path)
+ } else if strings.Contains(repo, "@") {
+ repo, tag = splitAtSign(repo)
+ }
+ }
+
+ return &DepInfo{
+ Host: u.Host,
+ Owner: owner,
+ Repo: repo,
+ Path: path,
+ Block: block,
+ Tag: tag,
+ }, nil
+}
+
+// splitAtSign splits the given string at the '@' sign and returns both parts.
+func splitAtSign(s string) (string, string) {
+ parts := strings.Split(s, "@")
+ if len(parts) > 1 {
+ return parts[0], parts[1]
+ }
+ return parts[0], ""
+}
+
+// extractBlockAndTag extracts the block and tag from a fragment.
+func extractBlockAndTag(fragment string) (string, string) {
+ parts := strings.Split(fragment, "@")
+ if len(parts) > 1 {
+ return parts[0], parts[1]
+ } else if len(parts) == 1 {
+ return parts[0], ""
+ }
+ return "", ""
+}
+
+// GitClient is an interface that abstracts Git operations.
+type GitClient interface {
+ Clone(host, owner, repo, dest string) error
+ Checkout(path, branch string) error
+}
+
+// DefaultGitClient provides a default implementation for the GitClient interface.
+type DefaultGitClient struct {
+ Token string
+ BaseURL string
+}
+
+// NewDefaultGitClient creates a new DefaultGitClient with the given token.
+func NewDefaultGitClient(token string) *DefaultGitClient {
+ return &DefaultGitClient{
+ Token: token,
+ }
+}
+
+// Clone clones a Git repository to the given destination.
+func (d *DefaultGitClient) Clone(host, owner, repo, dest string) error {
+ repoURL := fmt.Sprintf("%s/%s/%s.git", host, owner, repo)
+
+ var auth *http.BasicAuth
+ if d.Token != "" {
+ auth = &http.BasicAuth{
+ Username: "username", // this can be anything except an empty string
+ Password: d.Token,
+ }
+ }
+
+ _, err := git.PlainClone(dest, false, &git.CloneOptions{
+ URL: repoURL,
+ Auth: auth,
+ })
+
+ return err
+}
+
+// Checkout checks out a branch in a Git repository.
+func (d *DefaultGitClient) Checkout(path, branch string) error {
+ r, err := git.PlainOpen(path)
+ if err != nil {
+ return err
+ }
+
+ w, err := r.Worktree()
+ if err != nil {
+ return err
+ }
+
+ return w.Checkout(&git.CheckoutOptions{
+ Branch: plumbing.NewBranchReferenceName(branch),
+ })
+}
diff --git a/dep_test.go b/dep_test.go
new file mode 100644
index 0000000..d03d364
--- /dev/null
+++ b/dep_test.go
@@ -0,0 +1,103 @@
+package templit
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+// TestParseDepURL tests the ParseDepURL function.
+func TestParseDepURL(t *testing.T) {
+ tests := []struct {
+ name string
+ rawURL string
+ expected *DepInfo
+ wantErr bool
+ }{
+ {
+ name: "Basic URL with path and fragment",
+ rawURL: "https://github.com/owner/repo/path/to/file#block@v1.2.3",
+ expected: &DepInfo{
+ Host: "github.com",
+ Owner: "owner",
+ Repo: "repo",
+ Path: "path/to/file",
+ Block: "block",
+ Tag: "v1.2.3",
+ },
+ wantErr: false,
+ },
+ {
+ name: "URL without path",
+ rawURL: "https://github.com/owner/repo",
+ expected: &DepInfo{
+ Host: "github.com",
+ Owner: "owner",
+ Repo: "repo",
+ Path: "",
+ Block: "",
+ Tag: "",
+ },
+ wantErr: false,
+ },
+ {
+ name: "URL with tag in path",
+ rawURL: "https://github.com/owner/repo/path/to/file@v1.2.3",
+ expected: &DepInfo{
+ Host: "github.com",
+ Owner: "owner",
+ Repo: "repo",
+ Path: "path/to/file",
+ Block: "",
+ Tag: "v1.2.3",
+ },
+ wantErr: false,
+ },
+ {
+ name: "URL with tag in repo",
+ rawURL: "https://github.com/owner/repo@v1.2.3",
+ expected: &DepInfo{
+ Host: "github.com",
+ Owner: "owner",
+ Repo: "repo",
+ Path: "",
+ Block: "",
+ Tag: "v1.2.3",
+ },
+ wantErr: false,
+ },
+ {
+ name: "URL without protocol",
+ rawURL: "https://github.com/owner/repo/some/path#test_block@v1.2.3",
+ expected: &DepInfo{
+ Host: "github.com",
+ Owner: "owner",
+ Repo: "repo",
+ Path: "some/path",
+ Block: "test_block",
+ Tag: "v1.2.3",
+ },
+ wantErr: false,
+ },
+ {
+ name: "Invalid URL missing repo name",
+ rawURL: "https://github.com/owner",
+ expected: nil,
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := ParseDepURL(tt.rawURL)
+ if (err != nil) != tt.wantErr {
+ t.Fatalf("expected error %v, got %v", tt.wantErr, err)
+ }
+ if err == nil {
+ if diff := cmp.Diff(tt.expected, result); diff != "" {
+ t.Errorf("result mismatch (-want +got):\n%s", diff)
+ }
+ }
+ })
+ }
+}
diff --git a/embed_func.go b/embed_func.go
new file mode 100644
index 0000000..d54c655
--- /dev/null
+++ b/embed_func.go
@@ -0,0 +1,62 @@
+package templit
+
+import (
+ "fmt"
+ "os"
+ "path"
+ "text/template"
+)
+
+// EmbedFunc returns a template function that can be used to process and embed a template from a remote git repository.
+// EmbedFunc allows embedding content from a remote repository directly into a Go template.
+//
+// Steps to use:
+// 1. Add the function to the FuncMap.
+// 2. Use the following syntax within your template:
+// `{{ embed "///@" . }}`
+// or
+// `{{ embed "//#@" . }}`
+//
+// Placeholders:
+// - ``: Repository hosting service (e.g., "github.com").
+// - ``: Repository owner or organization.
+// - ``: Repository name.
+// - ``: Path to the desired file or directory within the repository.
+// - ``: Specific template block name.
+// - ``: Specific Git reference (tag, commit hash, or branch name).
+func EmbedFunc(client GitClient) func(remotePath string, data interface{}, funcMap template.FuncMap) (string, error) {
+ return func(remotePath string, data interface{}, funcMap template.FuncMap) (string, error) {
+ embedInfo, err := ParseDepURL(remotePath)
+ if err != nil {
+ return "", err
+ }
+
+ const tempDirPrefix = "temp_clone_"
+ tempDir, err := os.MkdirTemp("", tempDirPrefix)
+ if err != nil {
+ return "", fmt.Errorf("failed to create temp dir: %w", err)
+ }
+ defer os.RemoveAll(tempDir) // Cleanup
+
+ err = client.Clone(embedInfo.Host, embedInfo.Owner, embedInfo.Repo, tempDir)
+ if err != nil {
+ return "", fmt.Errorf("failed to clone repo: %w", err)
+ }
+
+ err = client.Checkout(tempDir, embedInfo.Tag)
+ if err != nil {
+ return "", fmt.Errorf("failed to checkout branch: %w", err)
+ }
+
+ executor, err := NewExecutor(tempDir, funcMap)
+ if err != nil {
+ return "", fmt.Errorf("failed to create executor: %w", err)
+ }
+
+ if embedInfo.Block != "" {
+ return executor.Render(embedInfo.Block, data)
+ }
+
+ return executor.Render(path.Join(tempDir, embedInfo.Path), data)
+ }
+}
diff --git a/embed_func_test.go b/embed_func_test.go
new file mode 100644
index 0000000..866641c
--- /dev/null
+++ b/embed_func_test.go
@@ -0,0 +1,75 @@
+package templit
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+// TestEmbedFunc tests the EmbedFunc function.
+func TestEmbedFunc(t *testing.T) {
+ tests := []struct {
+ name string
+ repoAndPath string
+ ctx interface{}
+ expectedFile string
+ expectedText string
+ expectedError error
+ }{
+ {
+ name: "Valid template fetch and execute",
+ repoAndPath: "https://test_data/templates/basic_test/greeting.txt@main",
+ ctx: map[string]string{"Name": "John"},
+ expectedFile: "test_data/outputs/basic_test/greeting.txt",
+ },
+ {
+ name: "Valid template fetch and execute block",
+ repoAndPath: "https://test_data/templates/basic_test/-block.txt#example_block@main",
+ ctx: map[string]string{"Greeting": "Hey"},
+ expectedText: "Hey, this is an example block.",
+ },
+ {
+ name: "Invalid repo path format",
+ repoAndPath: "invalidpath",
+ ctx: nil,
+ expectedText: "",
+ expectedError: fmt.Errorf("invalid path format in embed URL"),
+ },
+ {
+ name: "Invalid repo",
+ repoAndPath: "https://localhost/owner/invalidrepo/greeting.txt@main",
+ ctx: nil,
+ expectedFile: "",
+ expectedError: fmt.Errorf("failed to clone repo: open localhost/owner/invalidrepo: no such file or directory"),
+ },
+ }
+
+ client := &MockGitClient{}
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ fn := EmbedFunc(client)
+ result, err := fn(tt.repoAndPath, tt.ctx, nil)
+ if err != nil {
+ if tt.expectedError == nil || err.Error() != tt.expectedError.Error() {
+ t.Fatalf("expected error %v, got %v", tt.expectedError, err)
+ }
+ return
+ }
+
+ var expected string
+ if tt.expectedFile != "" {
+ expectedBytes, _ := os.ReadFile(tt.expectedFile)
+ expected = string(expectedBytes)
+ } else {
+ expected = tt.expectedText
+ }
+
+ if diff := cmp.Diff(string(expected), result); diff != "" {
+ t.Errorf("result mismatch (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
diff --git a/executor.go b/executor.go
new file mode 100644
index 0000000..3e40770
--- /dev/null
+++ b/executor.go
@@ -0,0 +1,80 @@
+package templit
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "text/template"
+)
+
+// Executor is a wrapper around the template.Template type
+type Executor struct {
+ *template.Template
+}
+
+// NewExecutor creates a new Executor with the given template and funcMap
+func NewExecutor(inputPath string, funcMap template.FuncMap) (*Executor, error) {
+ if funcMap == nil {
+ funcMap = DefaultFuncMap
+ }
+
+ mainTmpl := template.New("main").Funcs(funcMap)
+
+ // check if input is a directory
+ info, err := os.Stat(inputPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to stat input: %w", err)
+ }
+
+ if !info.IsDir() {
+ content, err := os.ReadFile(inputPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read file: %w", err)
+ }
+ tmpl, err := mainTmpl.New(inputPath).Parse(string(content))
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse template: %w", err)
+ }
+ return &Executor{Template: tmpl}, nil
+ }
+
+ err = filepath.Walk(inputPath, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return fmt.Errorf("failed to walk directory: %w", err)
+ }
+
+ if info.IsDir() {
+ return nil
+ }
+
+ // Read, parse, and execute template only if it's a file
+ content, err := os.ReadFile(path)
+ if err != nil {
+ return fmt.Errorf("failed to read file: %w", err)
+ }
+
+ if _, err := mainTmpl.New(path).Parse(string(content)); err != nil {
+ return fmt.Errorf("failed to parse template: %w", err)
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse templates: %w", err)
+ }
+
+ return &Executor{
+ Template: mainTmpl,
+ }, nil
+}
+
+// Render executes the template with the given data
+func (e Executor) Render(name string, data interface{}) (string, error) {
+ var buf strings.Builder
+ if err := e.ExecuteTemplate(&buf, name, data); err != nil {
+ return "", fmt.Errorf("failed to execute template %s: %w", name, err)
+ }
+ return buf.String(), nil
+}
diff --git a/executor_test.go b/executor_test.go
new file mode 100644
index 0000000..4f416f4
--- /dev/null
+++ b/executor_test.go
@@ -0,0 +1,86 @@
+package templit
+
+import (
+ "testing"
+ "text/template"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+// TestNewExecutor tests the NewExecutor function.
+func TestNewExecutor(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ funcMap template.FuncMap
+ expected string
+ err bool
+ }{
+ {
+ name: "valid template file",
+ input: "test_data/templates/basic_test/greeting.txt",
+ funcMap: nil,
+ err: false,
+ },
+ // ... add more test cases here
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ executor, err := NewExecutor(tt.input, tt.funcMap)
+ if (err != nil) != tt.err {
+ t.Fatalf("expected error: %v, got: %v", tt.err, err)
+ }
+ if executor == nil {
+ t.Fatalf("expected executor to not be nil")
+ }
+ })
+ }
+}
+
+// TestRender tests the Render function.
+func TestRender(t *testing.T) {
+ tests := []struct {
+ name string
+ inputPath string
+ templateName string
+ data interface{}
+ expected string
+ err bool
+ }{
+ {
+ name: "valid template",
+ inputPath: "test_data/templates/basic_test",
+ templateName: "test_data/templates/basic_test/greeting.txt",
+ data: map[string]string{"Name": "John"},
+ expected: "Hello, John!\n",
+ err: false,
+ },
+ {
+ name: "valid template block",
+ inputPath: "test_data/templates/basic_test",
+ templateName: "example_block",
+ data: map[string]string{"greeting": "Hey"},
+ expected: "Hey, this is an example block.",
+ err: false,
+ },
+
+ // ... add more test cases here
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ executor, err := NewExecutor(tt.inputPath, nil)
+ if err != nil {
+ t.Fatalf("failed to create executor: %v", err)
+ }
+ result, err := executor.Render(tt.templateName, tt.data)
+ if (err != nil) != tt.err {
+ t.Fatalf("expected error: %v, got: %v", tt.err, err)
+ }
+ if diff := cmp.Diff(tt.expected, result); diff != "" {
+ t.Fatalf("unexpected result (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
diff --git a/funcs.go b/funcs.go
new file mode 100644
index 0000000..64efd8f
--- /dev/null
+++ b/funcs.go
@@ -0,0 +1,45 @@
+package templit
+
+import (
+ "strings"
+ "text/template"
+)
+
+// DefaultFuncMap is the default function map for templates.
+var DefaultFuncMap = template.FuncMap{
+ "lower": strings.ToLower,
+ "upper": strings.ToUpper,
+ "trim": strings.TrimSpace,
+ "split": strings.Split,
+ "join": strings.Join,
+ "replace": strings.ReplaceAll,
+ "contains": strings.Contains,
+ "hasPrefix": strings.HasPrefix,
+ "hasSuffix": strings.HasSuffix,
+ "trimPrefix": strings.TrimPrefix,
+ "trimSuffix": strings.TrimSuffix,
+ "trimSpace": strings.TrimSpace,
+ "trimLeft": strings.TrimLeft,
+ "trimRight": strings.TrimRight,
+ "count": strings.Count,
+ "repeat": strings.Repeat,
+ "equalFold": strings.EqualFold,
+ "splitN": strings.SplitN,
+ "splitAfter": strings.SplitAfter,
+ "splitAfterN": strings.SplitAfterN,
+ "fields": strings.Fields,
+ "toTitle": strings.ToTitle,
+ "toSnakeCase": ToSnakeCase,
+ "toCamelCase": ToCamelCase,
+ "toKebabCase": ToKebabCase,
+ "toPascalCase": ToPascalCase,
+ "default": defaultVal,
+}
+
+// defaultVal returns defaultValue if value is nil, otherwise value.
+func defaultVal(value, defaultValue interface{}) interface{} {
+ if value == nil {
+ return defaultValue
+ }
+ return value
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..59d9486
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,32 @@
+module github.com/euforic/templit
+
+go 1.21.0
+
+require (
+ github.com/go-git/go-git/v5 v5.8.1
+ github.com/google/go-cmp v0.5.9
+)
+
+require (
+ dario.cat/mergo v1.0.0 // indirect
+ github.com/Microsoft/go-winio v0.6.1 // indirect
+ github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
+ github.com/acomagu/bufpipe v1.0.4 // indirect
+ github.com/cloudflare/circl v1.3.3 // indirect
+ github.com/emirpasic/gods v1.18.1 // indirect
+ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
+ github.com/go-git/go-billy/v5 v5.4.1 // indirect
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+ github.com/kevinburke/ssh_config v1.2.0 // indirect
+ github.com/pjbgf/sha1cd v0.3.0 // indirect
+ github.com/sergi/go-diff v1.1.0 // indirect
+ github.com/skeema/knownhosts v1.2.0 // indirect
+ github.com/xanzy/ssh-agent v0.3.3 // indirect
+ golang.org/x/crypto v0.11.0 // indirect
+ golang.org/x/mod v0.8.0 // indirect
+ golang.org/x/net v0.12.0 // indirect
+ golang.org/x/sys v0.10.0 // indirect
+ golang.org/x/tools v0.6.0 // indirect
+ gopkg.in/warnings.v0 v0.1.2 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..8cc10aa
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,144 @@
+dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
+dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
+github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
+github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs=
+github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
+github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
+github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
+github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
+github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
+github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
+github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
+github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
+github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
+github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
+github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A=
+github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
+github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
+github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
+github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM=
+github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
+golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
+golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
+golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
+golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/import_func.go b/import_func.go
new file mode 100644
index 0000000..f8424a1
--- /dev/null
+++ b/import_func.go
@@ -0,0 +1,60 @@
+package templit
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "text/template"
+)
+
+// ImportFunc returns a function that can be used as a template function to import and process a template from a remote git repository.
+// ImportFunc allows embedding content from a remote repository into a Go template.
+//
+// Steps to use:
+// 1. Add the function to the FuncMap.
+// 2. Use the following syntax within your template:
+// `{{ import "///@" "" . }}`
+//
+// Placeholders:
+// - ``: Repository hosting service (e.g., "github.com").
+// - ``: Repository owner or organization.
+// - ``: Repository name.
+// - ``: Path to the desired file or directory within the repository.
+// - ``: Specific Git reference (tag, commit hash, or branch name).
+func ImportFunc(client GitClient) func(repoAndTag, destPath string, data interface{}, funcMap template.FuncMap) (string, error) {
+ return func(repoAndTag, destPath string, data interface{}, funcMap template.FuncMap) (string, error) {
+ const tempDirPrefix = "temp_clone_"
+
+ depInfo, err := ParseDepURL(repoAndTag)
+ if err != nil {
+ return "", fmt.Errorf("failed to parse embed URL: %w", err)
+ }
+
+ if depInfo.Tag == "" {
+ depInfo.Tag = "main"
+ }
+
+ tempDir, err := os.MkdirTemp("", tempDirPrefix)
+ if err != nil {
+ return "", fmt.Errorf("failed to create temp dir: %w", err)
+ }
+ defer os.RemoveAll(tempDir) // Cleanup
+
+ if err := client.Clone(depInfo.Host, depInfo.Owner, depInfo.Repo, tempDir); err != nil {
+ return "", fmt.Errorf("failed to clone repo: %w", err)
+ }
+
+ err = client.Checkout(tempDir, depInfo.Tag)
+ if err != nil {
+ return "", fmt.Errorf("failed to checkout branch: %w", err)
+ }
+
+ sourcePath := filepath.Join(tempDir, depInfo.Path)
+ err = WalkAndProcessDir(sourcePath, destPath, funcMap, data)
+ if err != nil {
+ return "", fmt.Errorf("failed to process template: %w", err)
+ }
+
+ return "", nil
+ }
+}
diff --git a/import_func_test.go b/import_func_test.go
new file mode 100644
index 0000000..cbdbbcb
--- /dev/null
+++ b/import_func_test.go
@@ -0,0 +1,59 @@
+package templit
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+)
+
+// TestImportFunc tests the ImportFunc function.
+func TestImportFunc(t *testing.T) {
+ client := &MockGitClient{}
+
+ tests := []struct {
+ name string
+ repoAndTag string
+ data interface{}
+ expectedError error
+ }{
+ {
+ // Add your test cases here
+ name: "Valid template processing",
+ repoAndTag: "https://test_data/templates/basic_test@main",
+ data: map[string]interface{}{
+ "Name": "John",
+ "Title": "Project",
+ "Description": "This is a test project.",
+ "Detail": "more info here.",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ destPath, err := os.MkdirTemp("", "test_output_"+tt.name)
+ if err != nil {
+ t.Fatal(fmt.Errorf("failed to create temp dir: %w", err))
+ }
+ defer os.RemoveAll(destPath) // Cleanup
+
+ fn := ImportFunc(client)
+ if _, err := fn(tt.repoAndTag, destPath, tt.data, nil); err != nil {
+ if tt.expectedError == nil || err.Error() != tt.expectedError.Error() {
+ t.Fatalf("expected error %v, got %v", tt.expectedError, err)
+ }
+ return
+ }
+
+ // Compare generated files with expected outputs
+ repo := strings.Split(tt.repoAndTag, "@")[0]
+ expectedOutputPath := strings.Replace(repo, "templates", "outputs", 1)
+
+ err = compareDirs(destPath, strings.TrimPrefix(expectedOutputPath, "https://"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ })
+ }
+}
diff --git a/string_funcs.go b/string_funcs.go
new file mode 100644
index 0000000..f2eb27e
--- /dev/null
+++ b/string_funcs.go
@@ -0,0 +1,110 @@
+package templit
+
+import (
+ "strings"
+ "unicode"
+)
+
+// ToCamelCase converts a string to CamelCase.
+func ToCamelCase(s string) string {
+ parts := splitAndFilter(s)
+ for i, part := range parts {
+ if i == 0 {
+ parts[i] = strings.ToLower(part)
+ } else {
+ parts[i] = capitalizeFirstLetter(part)
+ }
+ }
+ return strings.Join(parts, "")
+}
+
+// ToSnakeCase converts a string to snake_case.
+func ToSnakeCase(s string) string {
+ var result strings.Builder
+ previousIsLower := false
+
+ for _, r := range s {
+ switch {
+ case unicode.IsUpper(r) && previousIsLower:
+ result.WriteRune('_')
+ result.WriteRune(unicode.ToLower(r))
+ previousIsLower = false
+ case r == ' ', r == '-', r == '_':
+ result.WriteRune('_')
+ previousIsLower = false
+ case unicode.IsLower(r) || unicode.IsDigit(r):
+ result.WriteRune(r)
+ previousIsLower = true
+ default:
+ result.WriteRune(unicode.ToLower(r))
+ previousIsLower = false
+ }
+ }
+ return result.String()
+}
+
+// ToKebabCase converts a string to kebab-case.
+func ToKebabCase(s string) string {
+ var result strings.Builder
+ previousIsLower := false
+
+ for _, r := range s {
+ switch {
+ case unicode.IsUpper(r) && previousIsLower:
+ result.WriteRune('-')
+ result.WriteRune(unicode.ToLower(r))
+ previousIsLower = false
+ case r == ' ', r == '_', r == '-':
+ result.WriteRune('-')
+ previousIsLower = false
+ case unicode.IsLower(r) || unicode.IsDigit(r):
+ result.WriteRune(r)
+ previousIsLower = true
+ default:
+ result.WriteRune(unicode.ToLower(r))
+ previousIsLower = false
+ }
+ }
+ return result.String()
+}
+
+// ToPascalCase converts a string to PascalCase.
+func ToPascalCase(s string) string {
+ parts := splitAndFilter(s)
+ for i, part := range parts {
+ parts[i] = capitalizeFirstLetter(part)
+ }
+ return strings.Join(parts, "")
+}
+
+// splitAndFilter splits a string by multiple delimiters and filters out non-alphanumeric characters.
+func splitAndFilter(s string) []string {
+ parts := splitByMultipleDelimiters(s, []string{" ", "_", "-"})
+ for i, part := range parts {
+ runes := []rune(part)
+ filtered := make([]rune, 0, len(runes))
+ for _, r := range runes {
+ if unicode.IsLetter(r) || unicode.IsDigit(r) {
+ filtered = append(filtered, r)
+ }
+ }
+ parts[i] = string(filtered)
+ }
+ return parts
+}
+
+// splitByMultipleDelimiters splits a string based on multiple delimiters.
+func splitByMultipleDelimiters(s string, delimiters []string) []string {
+ for _, delimiter := range delimiters {
+ s = strings.ReplaceAll(s, delimiter, " ")
+ }
+ return strings.Fields(s)
+}
+
+// capitalizeFirstLetter capitalizes the first letter of a string.
+func capitalizeFirstLetter(s string) string {
+ if s == "" {
+ return s
+ }
+ return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:])
+}
diff --git a/string_funcs_test.go b/string_funcs_test.go
new file mode 100644
index 0000000..dcfdc77
--- /dev/null
+++ b/string_funcs_test.go
@@ -0,0 +1,93 @@
+package templit
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+// TestToCamelCase tests the ToCamelCase function.
+func TestToCamelCase(t *testing.T) {
+ tests := []struct {
+ input string
+ output string
+ }{
+ {"hello world", "helloWorld"},
+ {"Hello World", "helloWorld"},
+ {"HELLO_WORLD", "helloWorld"},
+ {"XML HTTP_request2_a-b", "xmlHttpRequest2AB"},
+ }
+
+ for _, test := range tests {
+ result := ToCamelCase(test.input)
+ if diff := cmp.Diff(test.output, result); diff != "" {
+ t.Errorf("toCamelCase(%s) mismatch (-want +got):\n%s", test.input, diff)
+ }
+ }
+}
+
+// TestToSnakeCase tests the ToSnakeCase function.
+func TestToSnakeCase(t *testing.T) {
+ tests := []struct {
+ input string
+ output string
+ }{
+ {"hello world", "hello_world"},
+ {"Hello World", "hello_world"},
+ {"HELLO_WORLD", "hello_world"},
+ {"HelloWorld", "hello_world"},
+ {"HelloWorld Today", "hello_world_today"},
+ {"HelloWorld-today", "hello_world_today"},
+ {"XML HTTP_request2_a-b", "xml_http_request2_a_b"},
+ }
+
+ for _, test := range tests {
+ result := ToSnakeCase(test.input)
+ if diff := cmp.Diff(test.output, result); diff != "" {
+ t.Errorf("ToSnakeCase(%s) mismatch (-want +got):\n%s", test.input, diff)
+ }
+ }
+}
+
+// TestToKebabCase tests the ToKebabCase function.
+func TestToKebabCase(t *testing.T) {
+ tests := []struct {
+ input string
+ output string
+ }{
+ {"hello world", "hello-world"},
+ {"Hello World", "hello-world"},
+ {"HELLO_WORLD", "hello-world"},
+ {"HelloWorld", "hello-world"},
+ {"HelloWorld Today", "hello-world-today"},
+ {"HelloWorld-today", "hello-world-today"},
+ {"XML HTTP_request2_a-b", "xml-http-request2-a-b"},
+ }
+
+ for _, test := range tests {
+ result := ToKebabCase(test.input)
+ if diff := cmp.Diff(test.output, result); diff != "" {
+ t.Errorf("ToKebabCase(%s) mismatch (-want +got):\n%s", test.input, diff)
+ }
+ }
+}
+
+// TestToPascalCase tests the ToPascalCase function.
+func TestToPascalCase(t *testing.T) {
+ tests := []struct {
+ input string
+ output string
+ }{
+ {"hello world", "HelloWorld"},
+ {"Hello World", "HelloWorld"},
+ {"HELLO_WORLD", "HelloWorld"},
+ {"XML HTTP_request2_a-b", "XmlHttpRequest2AB"},
+ }
+
+ for _, test := range tests {
+ result := ToPascalCase(test.input)
+ if diff := cmp.Diff(test.output, result); diff != "" {
+ t.Errorf("ToPascalCase(%s) mismatch (-want +got):\n%s", test.input, diff)
+ }
+ }
+}
diff --git a/templit.go b/templit.go
new file mode 100644
index 0000000..7a6901a
--- /dev/null
+++ b/templit.go
@@ -0,0 +1,111 @@
+package templit
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "text/template"
+)
+
+// WalkAndProcessDirFunc is called for each file and directory when walking a directory.
+type WalkAndProcessDirFunc func(path string, isDir bool, content string) error
+
+// WalkAndProcessDir processes all files in a directory with the given data.
+// If walkFunc is provided, it's called for each file and directory without writing the file to disk.
+func WalkAndProcessDir(inputDir, outputDir string, funcMap template.FuncMap, data interface{}) error {
+ // Create output directory
+ if err := os.MkdirAll(outputDir, 0755); err != nil {
+ return fmt.Errorf("failed to create output directory: %w", err)
+ }
+
+ // Master template for file processing
+ masterTmpl := template.New("main").Funcs(funcMap)
+
+ err := filepath.Walk(inputDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return fmt.Errorf("error walking through directory: %w", err)
+ }
+
+ parsedName, err := RenderTemplate(filepath.Base(path), data, funcMap)
+ if err != nil {
+ return fmt.Errorf("error rendering path template: %w", err)
+ }
+
+ relPath, err := filepath.Rel(inputDir, filepath.Dir(path))
+ if err != nil {
+ return fmt.Errorf("error getting relative path: %w", err)
+ }
+
+ outPath := filepath.Join(outputDir, relPath, parsedName)
+ parsedOutPath, err := RenderTemplate(outPath, data, funcMap)
+ if err != nil {
+ return fmt.Errorf("error rendering path template: %w", err)
+ }
+
+ if info.IsDir() {
+ // Skip directories with empty or "-" prefixed names
+ if parsedName == "" || strings.HasPrefix(parsedName, "-") {
+ return filepath.SkipDir
+ }
+
+ // Skip root directory
+ if filepath.Base(outPath) == filepath.Base(inputDir) {
+ return nil
+ }
+
+ if err := os.MkdirAll(parsedOutPath, info.Mode()); err != nil {
+ return fmt.Errorf("error creating directory: %w", err)
+ }
+
+ return nil
+ }
+
+ // Skip files with empty names or "-" prefixed
+ if parsedName == "" || strings.HasPrefix(parsedName, "-") {
+ return nil
+ }
+
+ content, err := os.ReadFile(path)
+ if err != nil {
+ return fmt.Errorf("error reading file from templates: %w", err)
+ }
+
+ tmpl, err := masterTmpl.New(path).Parse(string(content))
+ if err != nil {
+ return fmt.Errorf("error parsing template: %w", err)
+ }
+
+ var buf strings.Builder
+ if err := tmpl.Execute(&buf, data); err != nil {
+ return fmt.Errorf("error executing template: %w", err)
+ }
+
+ if err := os.WriteFile(parsedOutPath, []byte(buf.String()), info.Mode()); err != nil {
+ return fmt.Errorf("error writing file to output: %w", err)
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return fmt.Errorf("error walking through directory: %w", err)
+ }
+
+ return nil
+}
+
+// RenderTemplate renders a template with provided data.
+func RenderTemplate(tmpl string, data interface{}, funcMap template.FuncMap) (string, error) {
+ t, err := template.New("main").Funcs(funcMap).Parse(tmpl)
+ if err != nil {
+ return "", fmt.Errorf("error parsing template: %w", err)
+ }
+
+ var buf strings.Builder
+ if err := t.Execute(&buf, data); err != nil {
+ return "", fmt.Errorf("error executing template: %w", err)
+ }
+
+ return buf.String(), nil
+}
diff --git a/templit_test.go b/templit_test.go
new file mode 100644
index 0000000..b16a168
--- /dev/null
+++ b/templit_test.go
@@ -0,0 +1,111 @@
+package templit
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+ "text/template"
+)
+
+// TestRenderTemplate tests the RenderTemplate function.
+func TestRenderTemplate(t *testing.T) {
+ var tests = []struct {
+ name string
+ tmpl string
+ data interface{}
+ funcMap template.FuncMap
+ expected string
+ wantErr bool
+ }{
+ {
+ name: "Simple template without data",
+ tmpl: "Hello, World!",
+ expected: "Hello, World!",
+ },
+ {
+ name: "Template with data",
+ tmpl: "Hello, {{.Name}}!",
+ data: map[string]string{"Name": "John"},
+ expected: "Hello, John!",
+ },
+ {
+ name: "Template with function",
+ tmpl: "Hello, {{lower .Name}}!",
+ data: map[string]string{"Name": "JOHN"},
+ funcMap: template.FuncMap{"lower": func(s string) string { return strings.ToLower(s) }},
+ expected: "Hello, john!",
+ },
+ {
+ name: "Malformed template",
+ tmpl: "Hello, {{.Name!",
+ data: map[string]string{"Name": "John"},
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := RenderTemplate(tt.tmpl, tt.data, tt.funcMap)
+ if (err != nil) != tt.wantErr {
+ t.Fatalf("RenderTemplate() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.expected {
+ t.Errorf("RenderTemplate() = %v, expected %v", got, tt.expected)
+ }
+ })
+ }
+}
+
+// TestWalkAndProcessDir tests the WalkAndProcessDir function.
+func TestWalkAndProcessDir(t *testing.T) {
+ tests := []struct {
+ name string
+ inputDir string
+ data interface{}
+ funcMap template.FuncMap
+ expectedOutput string
+ expectedError error
+ }{
+ {
+ name: "Simple directory processing",
+ inputDir: "test_data/templates/basic_test",
+ data: map[string]interface{}{
+ "Name": "John",
+ "Title": "Project",
+ "Description": "This is a test project.",
+ "Detail": "more info here.",
+ },
+ funcMap: DefaultFuncMap,
+ expectedOutput: "test_data/outputs/basic_test/",
+ },
+ // ... (other test cases)
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Create temp directory for output
+ tempOutputDir, err := os.MkdirTemp("", "test_output_"+strings.ReplaceAll(strings.ToLower(tt.name), " ", "_"))
+ if err != nil {
+ t.Fatalf("failed to create temp dir: %v", err)
+ }
+ //defer os.RemoveAll(tempOutputDir) // Cleanup
+
+ err = WalkAndProcessDir(tt.inputDir, tempOutputDir, tt.funcMap, tt.data)
+ if err != nil {
+ if tt.expectedError == nil || err.Error() != tt.expectedError.Error() {
+ t.Fatalf("expected error %v, got %v", tt.expectedError, err)
+ }
+ return
+ }
+
+ // Compare generated output directory with expected directory
+ fmt.Println(tt.expectedOutput, tempOutputDir)
+ err = compareDirs(tt.expectedOutput, tempOutputDir)
+ if err != nil {
+ t.Fatalf("output directory mismatch: %v", err)
+ }
+ })
+ }
+}
diff --git a/test_data/outputs/basic_test/docs/details/nested.txt b/test_data/outputs/basic_test/docs/details/nested.txt
new file mode 100644
index 0000000..7616507
--- /dev/null
+++ b/test_data/outputs/basic_test/docs/details/nested.txt
@@ -0,0 +1 @@
+Nested more info here.
diff --git a/test_data/outputs/basic_test/greeting.txt b/test_data/outputs/basic_test/greeting.txt
new file mode 100644
index 0000000..1590ee0
--- /dev/null
+++ b/test_data/outputs/basic_test/greeting.txt
@@ -0,0 +1 @@
+Hello, John!
diff --git a/test_data/outputs/basic_test/info.txt b/test_data/outputs/basic_test/info.txt
new file mode 100644
index 0000000..8b649c1
--- /dev/null
+++ b/test_data/outputs/basic_test/info.txt
@@ -0,0 +1 @@
+Project: This is a test project.
diff --git a/test_data/templates/basic_test/-block.txt b/test_data/templates/basic_test/-block.txt
new file mode 100644
index 0000000..adeac3f
--- /dev/null
+++ b/test_data/templates/basic_test/-block.txt
@@ -0,0 +1,8 @@
+{{ define "example_block" -}}
+Hey, this is an example block.
+{{- end -}}
+
+{{- define "other_block" -}}
+Hey, this is an example block.
+{{- end -}}
+
diff --git a/test_data/templates/basic_test/docs/details/nested.txt b/test_data/templates/basic_test/docs/details/nested.txt
new file mode 100644
index 0000000..9f1642d
--- /dev/null
+++ b/test_data/templates/basic_test/docs/details/nested.txt
@@ -0,0 +1 @@
+Nested {{.Detail}}
diff --git a/test_data/templates/basic_test/greeting.txt b/test_data/templates/basic_test/greeting.txt
new file mode 100644
index 0000000..89dce33
--- /dev/null
+++ b/test_data/templates/basic_test/greeting.txt
@@ -0,0 +1 @@
+Hello, {{.Name}}!
diff --git a/test_data/templates/basic_test/info.txt b/test_data/templates/basic_test/info.txt
new file mode 100644
index 0000000..3514fee
--- /dev/null
+++ b/test_data/templates/basic_test/info.txt
@@ -0,0 +1 @@
+{{.Title}}: {{.Description}}
diff --git a/test_data/templates/basic_test/{{if .templatefile}}templatefile.txt{{end}} b/test_data/templates/basic_test/{{if .templatefile}}templatefile.txt{{end}}
new file mode 100644
index 0000000..e69de29
diff --git a/test_test.go b/test_test.go
new file mode 100644
index 0000000..7a8a603
--- /dev/null
+++ b/test_test.go
@@ -0,0 +1,137 @@
+package templit
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+type MockGitClient struct{}
+
+func (m *MockGitClient) Clone(host, owner, repo, dest string) error {
+ src := filepath.Join(host, owner, repo)
+ return copyDir(src, dest)
+}
+
+func (m *MockGitClient) Checkout(path, branch string) error {
+ // Mocked function, doesn't need to do anything for this test.
+ return nil
+}
+
+// copyDir copies a directory recursively
+func copyDir(src string, dst string) error {
+ entries, err := os.ReadDir(src)
+ if err != nil {
+ return err
+ }
+
+ for _, entry := range entries {
+ sourcePath := filepath.Join(src, entry.Name())
+ destPath := filepath.Join(dst, entry.Name())
+
+ if entry.IsDir() {
+ err = os.MkdirAll(destPath, os.ModePerm)
+ if err != nil {
+ return err
+ }
+ err = copyDir(sourcePath, destPath)
+ if err != nil {
+ return err
+ }
+ continue
+ }
+
+ if err := copyFile(sourcePath, destPath); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// copyFile copies a file
+func copyFile(src, dst string) error {
+ bytes, err := os.ReadFile(src)
+ if err != nil {
+ return err
+ }
+ return os.WriteFile(dst, bytes, 0644)
+}
+
+// compareDirs recursively compares the contents of two directories.
+// dir1 is the expected directory, while dir2 is the actual directory.
+func compareDirs(dir1, dir2 string) error {
+ entries1, err := os.ReadDir(dir1)
+ if err != nil {
+ return err
+ }
+
+ entries2, err := os.ReadDir(dir2)
+ if err != nil {
+ return err
+ }
+
+ entryMap2 := make(map[string]os.DirEntry)
+ for _, entry := range entries2 {
+ entryMap2[entry.Name()] = entry
+ }
+
+ for _, entry1 := range entries1 {
+ entry2, exists := entryMap2[entry1.Name()]
+
+ if !exists && strings.HasPrefix(entry1.Name(), "-") {
+ continue
+ }
+
+ if !exists {
+ entryType := "file"
+ if entry1.IsDir() {
+ entryType = "directory"
+ }
+
+ return fmt.Errorf("missing %s in %s: %s", entryType, dir2, entry1.Name())
+ }
+
+ if entry1.IsDir() {
+ if !entry2.IsDir() {
+ return fmt.Errorf("expected file, but found directory in %s: %s", dir2, entry1.Name())
+ }
+
+ if err := compareDirs(filepath.Join(dir1, entry1.Name()), filepath.Join(dir2, entry2.Name())); err != nil {
+ return err
+ }
+ }
+
+ if entry1.Type().IsRegular() && entry2.Type().IsRegular() {
+ bytes1, err := os.ReadFile(filepath.Join(dir1, entry1.Name()))
+ if err != nil {
+ return err
+ }
+
+ bytes2, err := os.ReadFile(filepath.Join(dir2, entry2.Name()))
+ if err != nil {
+ return err
+ }
+
+ if diff := cmp.Diff(string(bytes1), string(bytes2)); diff != "" {
+ return fmt.Errorf("mismatch in files %s vs %s\n\t(-want +got):\n%s ", filepath.Join(dir1, entry1.Name()), filepath.Join(dir2, entry2.Name()), diff)
+ }
+ continue
+ }
+
+ if entry1.Type().IsRegular() || entry2.Type().IsRegular() {
+ return fmt.Errorf("file type mismatch for %s in %s vs %s", entry1.Name(), dir1, dir2)
+ }
+ }
+
+ // Check for any extra entries in dir2 that are not present in dir1
+ for _, entry2 := range entries2 {
+ if _, exists := entryMap2[entry2.Name()]; !exists {
+ return fmt.Errorf("unexpected entry in %s: %s", dir2, entry2.Name())
+ }
+ }
+
+ return nil
+}