Skip to content

Commit 1d381dd

Browse files
committed
Add templates support
1 parent 0f14543 commit 1d381dd

File tree

11 files changed

+326
-9
lines changed

11 files changed

+326
-9
lines changed

examples/templates/templates.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// An example that locally serves files from github.com/golang/go/doc
2+
package main
3+
4+
import (
5+
"context"
6+
"log"
7+
"os"
8+
9+
"github.com/posener/gitfs"
10+
"github.com/posener/gitfs/fsutil"
11+
)
12+
13+
// Add debug mode environment variable. When running with `LOCAL_DEBUG=.`, the
14+
// local git repository will be used instead of the remote github.
15+
var localDebug = os.Getenv("LOCAL_DEBUG")
16+
17+
func main() {
18+
ctx := context.Background()
19+
fs, err := gitfs.New(ctx, "github.com/posener/gitfs/examples/templates", gitfs.OptLocal(localDebug))
20+
if err != nil {
21+
log.Fatalf("Failed initializing git filesystem: %s.", err)
22+
}
23+
24+
tmpls, err := fsutil.TmplParseGlob(fs, nil, "*.gotmpl")
25+
if err != nil {
26+
log.Fatalf("Failed parsing templates.")
27+
}
28+
tmpls.ExecuteTemplate(os.Stdout, "tmpl1.gotmpl", "Foo")
29+
}

examples/templates/tmpl1.gotmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello, {{.}}

fsutil/fs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net/http"
66
"os"
77
"path/filepath"
8+
89
"github.com/kr/fs"
910
)
1011

fsutil/fs_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import (
99

1010
func TestFileSystem(t *testing.T) {
1111
t.Parallel()
12-
walker := Walk(http.Dir("../internal"), "testdata")
12+
1313
var got []string
14-
for walker.Step() {
14+
for walker := Walk(http.Dir("../internal"), "testdata"); walker.Step(); {
1515
got = append(got, walker.Path())
1616
}
1717
want := []string{
@@ -24,4 +24,4 @@ func TestFileSystem(t *testing.T) {
2424
"testdata/d1/d11/f111",
2525
}
2626
assert.ElementsMatch(t, want, got)
27-
}
27+
}

fsutil/testdata/tmpl1.gotmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello, {{.}}

fsutil/testdata/tmpl2.gotmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hi, {{.}}

fsutil/tmpl.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package fsutil
2+
3+
import (
4+
"bytes"
5+
htmltmpl "html/template"
6+
"net/http"
7+
"path/filepath"
8+
"strings"
9+
txttmpl "text/template"
10+
11+
"github.com/pkg/errors"
12+
)
13+
14+
// TmplParse Parse templates from the given filesystem according to the
15+
// given paths. If tmpl is not nil, the templates will be added to it.
16+
// paths must contain at least one path. All paths must exist in the
17+
// given filesystem.
18+
func TmplParse(fs http.FileSystem, tmpl *txttmpl.Template, paths ...string) (*txttmpl.Template, error) {
19+
t := tmplParser{Template: tmpl}
20+
err := parseFiles(fs, t.parse, paths...)
21+
return t.Template, err
22+
}
23+
24+
// TmplParseGlob parses templates from the given filesystem according to
25+
// the provided glob pattern. If tmpl is not nil, the templates will be
26+
// added to it.
27+
func TmplParseGlob(fs http.FileSystem, tmpl *txttmpl.Template, pattern string) (*txttmpl.Template, error) {
28+
t := tmplParser{Template: tmpl}
29+
err := parseGlob(fs, t.parse, pattern)
30+
return t.Template, err
31+
}
32+
33+
// TmplParseHTML Parse HTML templates from the given filesystem according
34+
// to the given paths. If tmpl is not nil, the templates will be added to
35+
// it. paths must contain at least one path. All paths must exist in the
36+
// given filesystem.
37+
func TmplParseHTML(fs http.FileSystem, tmpl *htmltmpl.Template, paths ...string) (*htmltmpl.Template, error) {
38+
t := tmplParserHTML{Template: tmpl}
39+
err := parseFiles(fs, t.parse, paths...)
40+
return t.Template, err
41+
}
42+
43+
// TmplParseGlobHTML parses HTML templates from the given filesystem
44+
// according to the provided glob pattern. If tmpl is not nil, the
45+
// templates will be added to it.
46+
func TmplParseGlobHTML(fs http.FileSystem, tmpl *htmltmpl.Template, pattern string) (*htmltmpl.Template, error) {
47+
t := tmplParserHTML{Template: tmpl}
48+
err := parseGlob(fs, t.parse, pattern)
49+
return t.Template, err
50+
}
51+
52+
type tmplParser struct {
53+
*txttmpl.Template
54+
}
55+
56+
func (t *tmplParser) parse(name, content string) error {
57+
var err error
58+
if t.Template == nil {
59+
t.Template = txttmpl.New(name)
60+
} else {
61+
t.Template = t.New(name)
62+
}
63+
t.Template, err = t.Parse(content)
64+
return err
65+
}
66+
67+
type tmplParserHTML struct {
68+
*htmltmpl.Template
69+
}
70+
71+
func (t *tmplParserHTML) parse(name, content string) error {
72+
var err error
73+
if t.Template == nil {
74+
t.Template = htmltmpl.New(name)
75+
} else {
76+
t.Template = t.New(name)
77+
}
78+
t.Template, err = t.Parse(content)
79+
return err
80+
}
81+
82+
func parseFiles(fs http.FileSystem, parse func(name string, content string) error, paths ...string) error {
83+
if len(paths) == 0 {
84+
return errors.New("no paths provided")
85+
}
86+
buf := bytes.NewBuffer(nil)
87+
for _, path := range paths {
88+
f, err := fs.Open(strings.Trim(path, "/"))
89+
if err != nil {
90+
return errors.Wrapf(err, "opening template %s", path)
91+
}
92+
name := filepath.Base(path)
93+
buf.Reset()
94+
buf.ReadFrom(f)
95+
err = parse(name, buf.String())
96+
if err != nil {
97+
return errors.Wrapf(err, "parsing template %s", path)
98+
}
99+
}
100+
return nil
101+
}
102+
103+
func parseGlob(fs http.FileSystem, parse func(name string, content string) error, pattern string) error {
104+
buf := bytes.NewBuffer(nil)
105+
walker := Walk(fs, "")
106+
for walker.Step() {
107+
matched, err := filepath.Match(pattern, walker.Path())
108+
if err != nil {
109+
return err
110+
}
111+
if !matched {
112+
continue
113+
}
114+
115+
f, err := fs.Open(walker.Path())
116+
if err != nil {
117+
return errors.Wrapf(err, "opening template %s", walker.Path())
118+
}
119+
120+
buf.Reset()
121+
buf.ReadFrom(f)
122+
err = parse(walker.Stat().Name(), buf.String())
123+
if err != nil {
124+
return errors.Wrapf(err, "parsing template %s", walker.Path())
125+
}
126+
}
127+
if err := walker.Err(); err != nil {
128+
return errors.Wrap(err, "failed walking filesystem")
129+
}
130+
return nil
131+
}

fsutil/tmpl_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package fsutil
2+
3+
import (
4+
"bytes"
5+
"net/http"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestTmplParse(t *testing.T) {
13+
t.Parallel()
14+
fs := http.Dir(".")
15+
buf := bytes.NewBuffer(nil)
16+
17+
tmpl, err := TmplParse(fs, nil, "testdata/tmpl1.gotmpl", "testdata/tmpl2.gotmpl")
18+
require.NoError(t, err)
19+
20+
buf.Reset()
21+
require.NoError(t, tmpl.ExecuteTemplate(buf, "tmpl1.gotmpl", "foo"))
22+
assert.Equal(t, "hello, foo", buf.String())
23+
24+
buf.Reset()
25+
require.NoError(t, tmpl.ExecuteTemplate(buf, "tmpl2.gotmpl", "foo"))
26+
assert.Equal(t, "hi, foo", buf.String())
27+
}
28+
29+
func TestTmplParse_noSuchFile(t *testing.T) {
30+
t.Parallel()
31+
fs := http.Dir(".")
32+
_, err := TmplParse(fs, nil, "testdata/tmpl1.gotmpl", "testdata/nosuchfile")
33+
assert.Error(t, err)
34+
_, err = TmplParse(fs, nil, "testdata/nosuchfile", "testdata/tmpl1.gotmpl")
35+
assert.Error(t, err)
36+
}
37+
38+
func TestTmplParse_emptyFileNames(t *testing.T) {
39+
t.Parallel()
40+
fs := http.Dir(".")
41+
_, err := TmplParse(fs, nil)
42+
assert.Error(t, err)
43+
}
44+
45+
func TestTmplParseGlob(t *testing.T) {
46+
t.Parallel()
47+
buf := bytes.NewBuffer(nil)
48+
fs := http.Dir(".")
49+
50+
// Match all files in the directory.
51+
tmpl, err := TmplParseGlob(fs, nil, "testdata/*.gotmpl")
52+
require.NoError(t, err)
53+
54+
buf.Reset()
55+
require.NoError(t, tmpl.ExecuteTemplate(buf, "tmpl1.gotmpl", "foo"))
56+
assert.Equal(t, "hello, foo", buf.String())
57+
58+
buf.Reset()
59+
require.NoError(t, tmpl.ExecuteTemplate(buf, "tmpl2.gotmpl", "foo"))
60+
assert.Equal(t, "hi, foo", buf.String())
61+
62+
// Match only one file.
63+
tmpl, err = TmplParseGlob(fs, nil, "testdata/tmpl1.*")
64+
require.NoError(t, err)
65+
66+
buf.Reset()
67+
require.NoError(t, tmpl.ExecuteTemplate(buf, "tmpl1.gotmpl", "foo"))
68+
assert.Equal(t, "hello, foo", buf.String())
69+
70+
buf.Reset()
71+
assert.Error(t, tmpl.ExecuteTemplate(buf, "tmpl2.gotmpl", "foo"))
72+
}
73+
74+
func TestTmplParseHTML(t *testing.T) {
75+
t.Parallel()
76+
fs := http.Dir(".")
77+
tmpl, err := TmplParseHTML(fs, nil, "testdata/tmpl1.gotmpl", "testdata/tmpl2.gotmpl")
78+
require.NoError(t, err)
79+
buf := bytes.NewBuffer(nil)
80+
require.NoError(t, tmpl.ExecuteTemplate(buf, "tmpl1.gotmpl", "foo"))
81+
assert.Equal(t, "hello, foo", buf.String())
82+
83+
buf.Reset()
84+
require.NoError(t, tmpl.ExecuteTemplate(buf, "tmpl2.gotmpl", "foo"))
85+
assert.Equal(t, "hi, foo", buf.String())
86+
}
87+
88+
func TestTmplParseGlobHTML(t *testing.T) {
89+
t.Parallel()
90+
buf := bytes.NewBuffer(nil)
91+
fs := http.Dir(".")
92+
93+
// Match all files in the directory.
94+
tmpl, err := TmplParseGlobHTML(fs, nil, "testdata/*.gotmpl")
95+
require.NoError(t, err)
96+
97+
buf.Reset()
98+
require.NoError(t, tmpl.ExecuteTemplate(buf, "tmpl1.gotmpl", "foo"))
99+
assert.Equal(t, "hello, foo", buf.String())
100+
101+
buf.Reset()
102+
require.NoError(t, tmpl.ExecuteTemplate(buf, "tmpl2.gotmpl", "foo"))
103+
assert.Equal(t, "hi, foo", buf.String())
104+
105+
// Match only one file.
106+
tmpl, err = TmplParseGlobHTML(fs, nil, "testdata/tmpl1.*")
107+
require.NoError(t, err)
108+
109+
buf.Reset()
110+
require.NoError(t, tmpl.ExecuteTemplate(buf, "tmpl1.gotmpl", "foo"))
111+
assert.Equal(t, "hello, foo", buf.String())
112+
113+
buf.Reset()
114+
assert.Error(t, tmpl.ExecuteTemplate(buf, "tmpl2.gotmpl", "foo"))
115+
}
116+
117+
func TestTmplParseHTML_noSuchFile(t *testing.T) {
118+
t.Parallel()
119+
fs := http.Dir(".")
120+
_, err := TmplParseHTML(fs, nil, "testdata/tmpl1.gotmpl", "testdata/nosuchfile")
121+
assert.Error(t, err)
122+
_, err = TmplParseHTML(fs, nil, "testdata/nosuchfile", "testdata/tmpl1.gotmpl")
123+
assert.Error(t, err)
124+
}
125+
126+
func TestTmplParseHTML_emptyFileNames(t *testing.T) {
127+
t.Parallel()
128+
fs := http.Dir(".")
129+
_, err := TmplParseHTML(fs, nil)
130+
assert.Error(t, err)
131+
}

gitfs_test.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,43 @@ package gitfs
33
import (
44
"context"
55
"io"
6+
"log"
67
"os"
78
"testing"
89

10+
"github.com/posener/gitfs/fsutil"
911
"github.com/stretchr/testify/require"
1012
)
1113

1214
func ExampleTest() {
1315
ctx := context.Background()
1416
fs, err := New(ctx, "github.com/kelseyhightower/helloworld@tags/3.0.0")
1517
if err != nil {
16-
panic(err)
18+
log.Fatalf("Failed initialize filesystem: %s", err)
1719
}
1820
f, err := fs.Open("README.md")
1921
if err != nil {
20-
panic(err)
22+
log.Fatalf("Failed opening file: %s", err)
2123
}
2224
io.Copy(os.Stdout, f)
2325
// Output: # helloworld
2426
}
2527

28+
func ExampleTest_templates() {
29+
ctx := context.Background()
30+
fs, err := New(ctx, "github.com/posener/gitfs/examples/templates")
31+
if err != nil {
32+
log.Fatalf("Failed initialize filesystem: %s", err)
33+
}
34+
35+
tmpls, err := fsutil.TmplParseGlob(fs, nil, "*.gotmpl")
36+
if err != nil {
37+
log.Fatalf("Failed parsing templates: %s", err)
38+
}
39+
tmpls.ExecuteTemplate(os.Stdout, "tmpl1.gotmpl", "Foo")
40+
// Output: Hello, Foo
41+
}
42+
2643
func TestNew_notSupported(t *testing.T) {
2744
t.Parallel()
2845
ctx := context.Background()

internal/testfs/testfs.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,6 @@ func TestFS(t *testing.T, fsFactory func(*testing.T, string) (http.FileSystem, e
4343
_, err := fsFactory(t, "git.com/posener/gitfs")
4444
assert.Error(t, err)
4545
})
46-
// t.Run("BadSubdirectory", func(t *testing.T) {
47-
// _, err := fsFactory(t, "github.com/posener/gitfs/nosuchdir")
48-
// assert.Error(t, err)
49-
// })
5046
}
5147

5248
type fsTest struct {

0 commit comments

Comments
 (0)