Add macro tag

This commit is contained in:
Elara 2023-10-30 20:07:57 -07:00
parent 3eca240b56
commit 17906cf329
8 changed files with 238 additions and 8 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
/readme /readme
/replybot /replybot
/include /include
/macro

93
examples/macro/include.go Normal file
View File

@ -0,0 +1,93 @@
/*
* Salix - Go templating engine
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"embed"
"io/fs"
"log"
"net/http"
"time"
"go.elara.ws/salix"
)
//go:embed tmpls
var tmpls embed.FS
func main() {
tmplsFS, err := fs.Sub(tmpls, "tmpls")
if err != nil {
log.Fatalln(err)
}
ns := salix.New().WithVarMap(map[string]any{"now": time.Now})
err = ns.ParseFSGlob(tmplsFS, "*.html")
if err != nil {
log.Fatalln(err)
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmpl, ok := ns.GetTemplate("home.html")
if !ok {
w.WriteHeader(http.StatusInternalServerError)
return
}
name := r.URL.Query().Get("name")
vars := map[string]any{"title": "Home"}
if name != "" {
vars["name"] = name
}
err = tmpl.
WithVarMap(vars).
Execute(w)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
})
http.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
tmpl, ok := ns.GetTemplate("about.html")
if !ok {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = tmpl.
WithVarMap(map[string]any{"title": "About"}).
Execute(w)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
})
log.Println("Starting HTTP server on port 8080")
err = http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatalln(err)
}
}

View File

@ -0,0 +1,16 @@
#macro("content"):
<section class="hero is-fullheight-with-navbar">
<div class="hero-head">
<div class="container">
<p class="title has-text-centered mt-2">About Salix</p>
<p>
Salix (pronounced <i>say-lix</i>) is a Go templating engine inspired by <a href="https://github.com/vapor/leaf">Leaf</a>.
<br><br>
Salix's syntax is similar to Leaf and (in my opinion at least), it's much more fun to write than the Go template syntax. If you like this project, please star its repo. I hope you enjoy! :)
</p>
</div>
</div>
</section>
#!macro
#include("base.html")

View File

@ -0,0 +1,55 @@
<html>
<head>
<title>#(title)</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
</head>
<body>
<nav class="navbar is-dark">
<div class="navbar-brand">
<a class="navbar-item" href="/">Salix</a>
<a class="navbar-burger" id="navbarMenuIcon" onclick="toggleNavMenu()">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu" id="navbarMenu">
<div class="navbar-end">
<a class='navbar-item #(title == "Home" ? "is-active" : "")' href="/">
Home
</a>
<a class='navbar-item #(title == "About" ? "is-active" : "")' href="/about">
About
</a>
</div>
</div>
</nav>
#macro("content")
<section class="hero is-small is-dark">
<div class="hero-body">
<div class="container">
<p class="has-text-centered">Copyright &copy; #(now().Year()) Salix Contributors. Licensed under the GPLv3.</p>
</div>
</div>
</section>
<script>
function toggleNavMenu() {
let navbarMenuIcon = document.getElementById("navbarMenuIcon");
let navbarMenu = document.getElementById("navbarMenu");
if (navbarMenu.classList.contains('is-active')) {
navbarMenuIcon.classList.remove('is-active')
navbarMenu.classList.remove('is-active')
} else {
navbarMenuIcon.classList.add('is-active')
navbarMenu.classList.add('is-active')
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,13 @@
#macro("content"):
<section class="hero is-fullheight-with-navbar">
<div class="hero-body">
<div class="container">
<p class="title">Hello, #(name | "World")!</p>
<p class="subtitle">This is a demo of the Salix template engine.</p>
<a class="button is-link is-rounded" href="/about">About &rarr;</a>
</div>
</div>
</section>
#!macro
#include("base.html")

47
macro_tag.go Normal file
View File

@ -0,0 +1,47 @@
package salix
import (
"errors"
"go.elara.ws/salix/ast"
)
var (
ErrMacroInvalidArgs = errors.New("macro expects one string argument")
ErrNoSuchMacro = errors.New("no such template")
)
// macroTag represents an #macro tag within a Salix template
type macroTag struct{}
func (mt macroTag) Run(tc *TagContext, block, args []ast.Node) error {
if len(args) != 1 {
return ErrMacroInvalidArgs
}
nameVal, err := tc.GetValue(args[0], nil)
if err != nil {
return err
}
name, ok := nameVal.(string)
if !ok {
return ErrMacroInvalidArgs
}
if len(block) == 0 {
tc.t.ns.mu.Lock()
macro, ok := tc.t.ns.macros[name]
if !ok {
return ErrNoSuchMacro
}
tc.t.ns.mu.Unlock()
return tc.Execute(macro, nil)
} else {
tc.t.ns.mu.Lock()
tc.t.ns.macros[name] = block
tc.t.ns.mu.Unlock()
}
return nil
}

View File

@ -21,22 +21,26 @@ package salix
import ( import (
"reflect" "reflect"
"sync" "sync"
"go.elara.ws/salix/ast"
) )
// Namespace represents a collection of templates that can include each other // Namespace represents a collection of templates that can include each other
type Namespace struct { type Namespace struct {
mu sync.Mutex mu sync.Mutex
tmpls map[string]*Template tmpls map[string]*Template
vars map[string]reflect.Value vars map[string]reflect.Value
tags map[string]Tag tags map[string]Tag
macros map[string][]ast.Node
} }
// New returns a new template namespace // New returns a new template namespace
func New() *Namespace { func New() *Namespace {
return &Namespace{ return &Namespace{
tmpls: map[string]*Template{}, tmpls: map[string]*Template{},
vars: map[string]reflect.Value{}, vars: map[string]reflect.Value{},
tags: map[string]Tag{}, tags: map[string]Tag{},
macros: map[string][]ast.Node{},
} }
} }

View File

@ -33,6 +33,7 @@ var globalTags = map[string]Tag{
"if": ifTag{}, "if": ifTag{},
"for": forTag{}, "for": forTag{},
"include": includeTag{}, "include": includeTag{},
"macro": macroTag{},
} }
// TagContext is passed to Tag implementations to allow them to control the interpreter // TagContext is passed to Tag implementations to allow them to control the interpreter