8000 parser: improve template hierarchy and error messages by abiosoft · Pull Request #6 · abiosoft/mold · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

parser: improve template hierarchy and error messages #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 86 additions & 44 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ const (
)

type (
templateSet map[string]*template.Template
moldEngine templateSet
templateSet map[string]*templateFile
moldEngine map[string]*template.Template
)

func newEngine(fsys fs.FS, options ...Option) (Engine, error) {
Expand All @@ -40,29 +40,25 @@ func newEngine(fsys fs.FS, options ...Option) (Engine, error) {

m := moldEngine{}

// traverse to fetch all templates and populate the root template.
root, ts, err := walk(c.fs, c.exts.val, c.funcMap.val)
// traverse to fetch all templates
set, err := walk(c.fs, c.exts.val, c.funcMap.val)
if err != nil {
return nil, fmt.Errorf("error creating new engine: %w", err)
}

// process layout
layout, err := parseLayout(root, c.layoutFile, c.funcMap.val)
layout, err := parseLayout(set, c.layoutRaw, c.funcMap.val)
if err != nil {
return nil, fmt.Errorf("error parsing layout: %w", err)
}

// process views
for _, t := range ts {
// ignore layout file
if t.name == c.layoutFile.name {
continue
}
view, err := parseView(root, layout, t.name, t.body)
for name := range set {
view, err := parseView(set, layout, name)
if err != nil {
return nil, err
}
m[t.name] = view
m[name] = view
}

return m, nil
Expand All @@ -82,8 +78,8 @@ func (m moldEngine) Render(w io.Writer, view string, data any) error {
return nil
}

func walk(fsys fs.FS, exts []string, funcMap template.FuncMap) (root templateSet, ts []templateFile, err error) {
root = templateSet{}
func walk(fsys fs.FS, exts []string, funcMap template.FuncMap) (set templateSet, err error) {
set = templateSet{}
err = fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
Expand All @@ -106,6 +102,11 @@ func walk(fsys fs.FS, exts []string, funcMap template.FuncMap) (root templateSet
return nil
}

// skip layout files
if err := validateLayoutFile(exts, path); err == nil {
return nil
}

f, err := readFile(fsys, path)
if err != nil {
return err
Expand All @@ -114,8 +115,7 @@ func walk(fsys fs.FS, exts []string, funcMap template.FuncMap) (root templateSet
if t, err := template.New(path).Funcs(funcMap).Parse(f); err != nil {
return fmt.Errorf("error parsing template '%s': %w", path, err)
} else {
root[path] = t
ts = append(ts, templateFile{name: path, body: f})
set[path] = &templateFile{Template: t, body: f}
}

return nil
Expand All @@ -139,22 +139,24 @@ func setup(c *Config, options ...Option) error {
c.fs = sub
}

// extensions
if !c.exts.set {
c.exts.update(defaultExts)
}

// layout
if c.layout.set {
if err := validateLayoutFile(c.exts.val, c.layout.val); err != nil {
return fmt.Errorf("invalid layout file: %w", err)
}
f, err := readFile(c.fs, c.layout.val)
if err != nil {
return fmt.Errorf("error reading layout file '%s': %w", c.layout.val, err)
}
c.layoutFile.body = f
c.layoutFile.name = c.layout.val
c.layoutRaw = f
} else {
c.layoutFile.body = defaultLayout
c.layoutFile.name = "default_layout"
}

// extensions
if !c.exts.set {
c.exts.update(defaultExts)
c.layout.update("default_layout")
c.layoutRaw = defaultLayout
}

// funcMap
Expand All @@ -169,14 +171,20 @@ func setup(c *Config, options ...Option) error {
return nil
}

func parseLayout(root templateSet, t templateFile, funcMap template.FuncMap) (*template.Template, error) {
layout, err := template.New("layout").Funcs(funcMap).Parse(t.body)
func parseLayout(root templateSet, layoutRaw string, funcMap template.FuncMap) (*templateFile, error) {
t, err := template.New("layout").Funcs(funcMap).Parse(layoutRaw)
if err != nil {
return nil, err
}

layout := &templateFile{
Template: t,
typ: layoutType,
body: layoutRaw,
}

// process template tree for layout
refs, err := processTree(layout, t.body)
refs, err := processTree(layout)
if err != nil {
return nil, fmt.Errorf("error processing layout: %w", err)
}
Expand All @@ -186,35 +194,43 @@ func parseLayout(root templateSet, t templateFile, funcMap template.FuncMap) (*t
if ref.typ == partialFunc {
return nil, fmt.Errorf("error parsing template '%s': %w", ref.name, ErrNotFound)
}
t, _ = template.New(ref.name).Parse("")
tpl, _ := template.New(ref.name).Parse("") // safe to ignore the err
t = &templateFile{Template: tpl}
}

t.typ = partialType
if err := parsePartial(t); err != nil {
return nil, fmt.Errorf("error parsing partial: '%s': %w", ref.name, err)
}

layout.AddParseTree(ref.name, t.Tree)
}

return layout, nil
}

func parseView(root templateSet, layout *template.Template, name, raw string) (*template.Template, error) {
view, err := layout.Clone()
if err != nil {
return nil, fmt.Errorf("error creating layout for view '%s': %w", name, err)
}
func parseView(set templateSet, layout *templateFile, name string) (*template.Template, error) {
view := template.Must(layout.Clone()) // safe

body := root[name]
if body == nil {
return nil, ErrNotFound
}
body := set[name]
body.typ = viewType

// process template tree for body
refs, err := processTree(body, raw)
refs, err := processTree(body)
if err != nil {
return nil, fmt.Errorf("error parsing view '%s': %w", name, err)
}
for _, ref := range refs {
t := root[ref.name]
t := set[ref.name]
if t == nil {
return nil, fmt.Errorf("error parsing template '%s': %w", ref.name, ErrNotFound)
}

t.typ = partialType
if err := parsePartial(t); err != nil {
return nil, fmt.Errorf("error parsing partial: '%s': %w", ref.name, err)
}

view.AddParseTree(ref.name, t.Tree)
}

Expand All @@ -230,6 +246,11 @@ func parseView(root templateSet, layout *template.Template, name, raw string) (*
return view, nil
}

func parsePartial(partial *templateFile) error {
_, err := processTree(partial)
return err
}

func readFile(fsys fs.FS, name string) (string, error) {
f, err := fs.ReadFile(fsys, name)
if err != nil {
Expand All @@ -243,29 +264,50 @@ func validExt(exts []string, ext string) bool {
if ext == "" {
return false
}

sanitize := func(ext string) string {
return strings.ToLower(strings.TrimPrefix(ext, "."))
}

for _, e := range exts {
if sanitize(e) == sanitize(ext) {
return true
}
}

return false
}

func validateLayoutFile(exts []string, name string) error {
ext := filepath.Ext(name)
if !validExt(exts, ext) {
return fmt.Errorf("unsupported filename extension '%s'", ext)
}

nameOnly := strings.TrimSuffix(name, ext)
if !strings.HasSuffix(strings.ToLower(nameOnly), "layout") {
return fmt.Errorf("invalid file name, must be suffixed with 'layout'. e.g. layout%s", ext)
}

return nil
}

func placeholderFuncs() template.FuncMap {
return map[string]any{
renderFunc.String(): func(...string) string { return "" },
partialFunc.String(): func(string, ...any) string { return "" },
}
}

type templateType string

// template types
const (
layoutType templateType = "layout"
viewType templateType = "view"
partialType templateType = "partial"
)

type templateFile struct {
name string
*template.Template
typ templateType
body string
}

Expand Down
13 changes: 11 additions & 2 deletions mold.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ type Engine interface {

// Config is the configuration for a new [Engine].
type Config struct {
fs fs.FS
layoutFile templateFile
fs fs.FS
layoutRaw string

// options
root optionVal[string]
Expand All @@ -48,6 +48,15 @@ var ErrNotFound = errors.New("template not found")

// New creates a new [Engine] with fs as the underlying filesystem.
//
// The directory will be traversed and all files matching the configured filename extensions would be parsed.
// The filename extensions can be configured with [WithExt].
//
// At most one layout file would be parsed, if set with [WithLayout]. Others would be skipped.
//
// Layout files are files suffixed (case insensitively) with "layout" before the filename extension.
// e.g. "layout.html", "Layout.html", "AppLayout.html", "app_layout.html" would all be regarded as layout files
// and skipped.
//
// Example:
//
// //go:embed web
Expand Down
52 changes: 50 additions & 2 deletions mold_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func createTestFS(extraFiles ...testFile) fs.FS {
func TestNew(t *testing.T) {
defer func() {
if err := recover(); err != nil {
t.Errorf("Must() expected no panic, got panic")
t.Errorf("Must() expected no panic, got panic: %v", err)
}
}()

Expand Down Expand Up @@ -79,6 +79,14 @@ func TestNew_LayoutParseError(t *testing.T) {
}
}

func TestNew_LayoutInvalidExt(t *testing.T) {
testFS := createTestFS(testFile{"layout.txt", "{{partial}}"})

if _, err := New(testFS, WithLayout("layout.txt")); err == nil {
t.Errorf("New() expected error, got nil")
}
}

func TestNew_ViewParseError(t *testing.T) {
testFS := createTestFS(testFile{"parse.html", "{{partial}}"})

Expand Down Expand Up @@ -196,7 +204,15 @@ func TestRender_ViewNotFound(t *testing.T) {
}
}

func TestRender_PartialNotFound(t *testing.T) {
func TestRender_LayoutPartialNotFound(t *testing.T) {
testFS := createTestFS(testFile{"layout.html", `{{partial "invalid.html"}}`})

if _, err := New(testFS, WithLayout("layout.html")); err == nil {
t.Errorf("New() expected error, got nil")
}
}

func TestRender_ViewPartialNotFound(t *testing.T) {
testFS := createTestFS(testFile{"view.html", `{{partial "invalid.html"}}`})

if _, err := New(testFS); err == nil {
Expand All @@ -215,6 +231,38 @@ func TestRender_InvalidData(t *testing.T) {
}
}

func TestRender_ViewInvalidRender(t *testing.T) {
testFS := createTestFS(
testFile{"view.html", `{{render}}`},
)

if _, err := New(testFS); err == nil {
t.Errorf("New() expected error, got nil")
}
}

func TestRender_PartialInvalidRender(t *testing.T) {
testFS := createTestFS(
testFile{"view.html", `{{partial "partial.html"}}`},
testFile{"view_partial.html", `{{render}}`},
)

if _, err := New(testFS); err == nil {
t.Errorf("New() expected error, got nil")
}
}

func TestRender_PartialInvalidPartial(t *testing.T) {
testFS := createTestFS(
testFile{"view.html", `{{partial "partial.html"}}`},
testFile{"partial.html", `{{partial "view.html"}}`},
)

if _, err := New(testFS); err == nil {
t.Errorf("New() expected error, got nil")
}
}

func TestRender_TemplateIf(t *testing.T) {
testFS := createTestFS(testFile{"index.html", `{{if .Name}}{{.Name}}{{end}}`})

Expand Down
Loading
Loading
0