Compare commits

...

5 Commits

Author SHA1 Message Date
Ben Visness 4651e8a477   2023-04-08 13:09:28 -05:00
Ben Visness 44e055155e Redo the home page banner copy 2023-04-08 13:07:40 -05:00
Ben Visness 2cb367ba18 Update README to warn about wellington problems 2023-04-08 11:49:48 -05:00
Ben Visness fc6b979a46 Update implementation of utils.DirFS
It is now patterned after the Go 1.20 implementation, in order to better represent the current state of things in a proposal I am making.
2023-04-08 11:41:02 -05:00
Ben Visness 524cf8e27b Add ability to load templates live from the filesystem
See config.go.example.
2023-04-08 11:14:44 -05:00
8 changed files with 157 additions and 22 deletions

View File

@ -10,10 +10,12 @@ We want the website to be a great example of Handmade software on the web. We en
You will need the following software installed:
- Go 1.18 or higher: https://go.dev/
- Go 1.18 or 1.19: https://go.dev/
You can download Go directly from the website, or install it through major package managers. If you already have Go installed, but are unsure of the version, you can check by running `go version`.
**PLEASE NOTE:** Go 1.20 currently does not work due to a bug in a third-party library. See [this issue](https://git.handmade.network/hmn/hmn/issues/59#issuecomment-1335).
- Postgres: https://www.postgresql.org/
Any Postgres installation should work fine, although less common distributions may not work as nicely with our scripts out of the box. On Mac, [Postgres.app](https://postgresapp.com/) is recommended.

View File

@ -82,4 +82,7 @@ var Config = HMNConfig{
CineraOutputPath: "./annotations/",
Projects: map[string]string{"hero": "code", "riscy": "riscy", "bitwise": "bitwise"},
},
DevConfig: DevConfig{
LiveTemplates: true,
},
}

View File

@ -29,6 +29,7 @@ type HMNConfig struct {
Discord DiscordConfig
Twitch TwitchConfig
EpisodeGuide EpisodeGuide
DevConfig DevConfig
}
type PostgresConfig struct {
@ -101,6 +102,10 @@ type AdminConfig struct {
AtomPassword string
}
type DevConfig struct {
LiveTemplates bool // load templates live from the filesystem instead of embedding them
}
func init() {
if Config.EpisodeGuide.Projects == nil {
Config.EpisodeGuide.Projects = make(map[string]string)

View File

@ -99,10 +99,7 @@ func IsEmail(address string) bool {
func renderTemplate(name string, data interface{}) (string, error) {
var buffer bytes.Buffer
template, hasTemplate := templates.Templates[name]
if !hasTemplate {
return "", oops.New(nil, "Template not found: %s", name)
}
template := templates.GetTemplate(name)
err := template.Execute(&buffer, data)
if err != nil {
return "", oops.New(err, "Failed to render template for email")

View File

@ -348,9 +348,9 @@
{{ svg "hmn_circuit" }}
</div>
<div id="welcome-content" class="center-layout" style="max-width: 51rem">
<p class="b">We are a community of programmers producing quality software through deeper understanding.</p>
<p>Originally inspired by Casey Muratori's <a href="https://handmadehero.org/" target="_blank">Handmade Hero</a>, we have grown into a thriving community focused on building truly high-quality software. We're not low-level in the typical sense. Instead we realize that to write great software, you need to understand things on a deeper level.</p>
<p>Modern software is a mess. The status quo needs to change. But we're optimistic that we can change it.</p>
<p class="b">We are working to correct the course of the software industry.</p>
<p>We are a community of low-level programmers with high-level goals. Originally inspired by Casey Muratori's <a href="https://handmadehero.org/" target="_blank">Handmade Hero</a>, we dig deep into our systems and learn how to do things from scratch. We're not satisfied by the latest popular language or the framework of the month. Instead we care about how computers <b>actually&nbsp;work.</b></p>
<p>Software quality is declining, and modern development practices are making it worse. We need to change course. <b>Help us get the software industry back on track.</b></p>
</div>
<div id="welcome-actions" class="flex flex-column flex-row-ns justify-center">
<a class="ba b--white br2 pa3 ph4-ns" href="{{ .ManifestoUrl }}">Read our manifesto</a>

View File

@ -4,13 +4,16 @@ import (
"embed"
"fmt"
"html/template"
"io/fs"
"regexp"
"strings"
"time"
"git.handmade.network/hmn/hmn/src/auth"
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/logging"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/utils"
"github.com/Masterminds/sprig"
"github.com/google/uuid"
@ -25,22 +28,22 @@ const (
)
//go:embed src
var templateFs embed.FS
var Templates map[string]*template.Template
var embeddedTemplateFs embed.FS
var embeddedTemplates map[string]*template.Template
//go:embed src/fishbowls
var FishbowlFS embed.FS
func Init() {
Templates = make(map[string]*template.Template)
func getTemplatesFromFS(templateFS fs.ReadDirFS) map[string]*template.Template {
templates := make(map[string]*template.Template)
files := utils.Must1(templateFs.ReadDir("src"))
files := utils.Must1(templateFS.ReadDir("src"))
for _, f := range files {
if hasSuffix(f.Name(), ".html") {
t := template.New(f.Name())
t = t.Funcs(sprig.FuncMap())
t = t.Funcs(HMNTemplateFuncs)
t, err := t.ParseFS(templateFs,
t, err := t.ParseFS(templateFS,
"src/layouts/*",
"src/include/*",
"src/"+f.Name(),
@ -49,19 +52,40 @@ func Init() {
logging.Fatal().Str("filename", f.Name()).Err(err).Msg("failed to parse template")
}
Templates[f.Name()] = t
templates[f.Name()] = t
} else if hasSuffix(f.Name(), ".css", ".js", ".xml") {
t := template.New(f.Name())
t = t.Funcs(sprig.FuncMap())
t = t.Funcs(HMNTemplateFuncs)
t, err := t.ParseFS(templateFs, "src/"+f.Name())
t, err := t.ParseFS(templateFS, "src/"+f.Name())
if err != nil {
logging.Fatal().Str("filename", f.Name()).Err(err).Msg("failed to parse template")
}
Templates[f.Name()] = t
templates[f.Name()] = t
}
}
return templates
}
func Init() {
embeddedTemplates = getTemplatesFromFS(embeddedTemplateFs)
}
func GetTemplate(name string) *template.Template {
var templates map[string]*template.Template
if config.Config.DevConfig.LiveTemplates {
templates = getTemplatesFromFS(utils.DirFS("src/templates").(fs.ReadDirFS))
} else {
templates = embeddedTemplates
}
template, hasTemplate := templates[name]
if !hasTemplate {
panic(oops.New(nil, "Template not found: %s", name))
}
return template
}
func hasSuffix(s string, suffixes ...string) bool {

108
src/utils/dirfs.go Normal file
View File

@ -0,0 +1,108 @@
package utils
import (
"errors"
"io/fs"
"os"
"runtime"
)
// DirFS returns a file system (an fs.FS) for the tree of files rooted at the directory dir.
//
// Note that DirFS("/prefix") only guarantees that the Open calls it makes to the
// operating system will begin with "/prefix": DirFS("/prefix").Open("file") is the
// same as os.Open("/prefix/file"). So if /prefix/file is a symbolic link pointing outside
// the /prefix tree, then using DirFS does not stop the access any more than using
// os.Open does. Additionally, the root of the fs.FS returned for a relative path,
// DirFS("prefix"), will be affected by later calls to Chdir. DirFS is therefore not
// a general substitute for a chroot-style security mechanism when the directory tree
// contains arbitrary content.
//
// The result implements fs.StatFS AND fs.ReadDirFS because god dammit why not.
//
// Implementation copy-pasted from Go 1.20.2.
func DirFS(dir string) fs.FS {
return dirFS(dir)
}
type dirFS string
var _ fs.StatFS = dirFS("")
var _ fs.ReadDirFS = dirFS("")
func (dir dirFS) Open(name string) (fs.File, error) {
fullname, err := dir.join(name)
if err != nil {
return nil, &os.PathError{Op: "stat", Path: name, Err: err}
}
f, err := os.Open(fullname)
if err != nil {
// DirFS takes a string appropriate for GOOS,
// while the name argument here is always slash separated.
// dir.join will have mixed the two; undo that for
// error reporting.
err.(*os.PathError).Path = name
return nil, err
}
return f, nil
}
func (dir dirFS) Stat(name string) (fs.FileInfo, error) {
fullname, err := dir.join(name)
if err != nil {
return nil, &os.PathError{Op: "stat", Path: name, Err: err}
}
f, err := os.Stat(fullname)
if err != nil {
// See comment in dirFS.Open.
err.(*os.PathError).Path = name
return nil, err
}
return f, nil
}
func (dir dirFS) ReadDir(name string) ([]fs.DirEntry, error) {
fullname, err := dir.join(name)
if err != nil {
return nil, &os.PathError{Op: "stat", Path: name, Err: err}
}
d, err := os.ReadDir(fullname)
if err != nil {
// See comment in dirFS.Open.
err.(*os.PathError).Path = name
return nil, err
}
return d, nil
}
func fromFS(path string) (string, error) {
if runtime.GOOS == "plan9" {
if len(path) > 0 && path[0] == '#' {
return "", os.ErrInvalid
}
}
for i := range path {
if path[i] == 0 {
return "", os.ErrInvalid
}
}
return path, nil
}
// join returns the path for name in dir.
func (dir dirFS) join(name string) (string, error) {
if dir == "" {
return "", errors.New("os: DirFS with empty root")
}
if !fs.ValidPath(name) {
return "", os.ErrInvalid
}
name, err := fromFS(name)
if err != nil {
return "", os.ErrInvalid
}
if os.IsPathSeparator(dir[len(dir)-1]) {
return string(dir) + name, nil
}
return string(dir) + string(os.PathSeparator) + name, nil
}

View File

@ -444,11 +444,7 @@ func (rd *ResponseData) WriteTemplate(name string, data interface{}, rp *perf.Re
rp.StartBlock("TEMPLATE", name)
defer rp.EndBlock()
}
template, hasTemplate := templates.Templates[name]
if !hasTemplate {
panic(oops.New(nil, "Template not found: %s", name))
}
return template.Execute(rd, data)
return templates.GetTemplate(name).Execute(rd, data)
}
func (rd *ResponseData) MustWriteTemplate(name string, data interface{}, rp *perf.RequestPerf) {