Compare commits
5 Commits
3a66b7a77d
...
4651e8a477
Author | SHA1 | Date |
---|---|---|
Ben Visness | 4651e8a477 | |
Ben Visness | 44e055155e | |
Ben Visness | 2cb367ba18 | |
Ben Visness | fc6b979a46 | |
Ben Visness | 524cf8e27b |
|
@ -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.
|
||||
|
|
|
@ -82,4 +82,7 @@ var Config = HMNConfig{
|
|||
CineraOutputPath: "./annotations/",
|
||||
Projects: map[string]string{"hero": "code", "riscy": "riscy", "bitwise": "bitwise"},
|
||||
},
|
||||
DevConfig: DevConfig{
|
||||
LiveTemplates: true,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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) {
|
||||
|
|
Reference in New Issue