## Installation and Requirements
`go get git.earlybird.gay/today@latest`
+
+## Known Issues/Limitations
+
+| Issue | Documented | Fix Planned |
+| ------------------------------------------------------------------------------------------------- | ----------- | ----------- |
+| Conditionally filling a template slot still removes the default slot if condition isn't fulfilled | Dec 20 2024 | Yes |
"path"
"path/filepath"
"runtime"
+ "runtime/debug"
"slices"
"strings"
"sync"
for _, middleware := range app.preHandler {
handler = middleware(handler)
}
+ app.Logger.Debug("handling today page", "path", translatedExpr, "page", todayPage.Name(), "language", lang)
app.ServeMux.Handle(translatedExpr, handler)
}
} else {
for _, middleware := range app.preHandler {
handler = middleware(handler)
}
+ app.Logger.Debug("handling", "path", expr)
app.ServeMux.Handle(expr, handler)
}
}
return
}
app.setDefaults()
+ app.Logger.Debug("handling static files")
app.registerStatic()
app.ready.Store(true)
func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
app.initOnce()
+ defer func() {
+ if x := recover(); x != nil {
+ fmt.Println(x)
+ fmt.Println(string(debug.Stack()))
+ w.WriteHeader(http.StatusInternalServerError)
+ }
+ }()
app.Logger.Debug("serving request", "host", r.Host, "path", r.URL.Path)
app.handler.ServeHTTP(w, r)
}
--- /dev/null
+<!DOCTYPE html>
+<head>
+ <title>Today Test Page</title>
+</head>
+<body>
+ <main>
+ <my-text :value=".header" id="my-text-header">
+ <h1 slot="value">{{ .header.Value }}</h1>
+ <p>{{ . }}</p>
+ </my-text>
+ {{ range $i, $text := .texts }}
+ <my-text :value=".">
+ {{ if lt $i 3 }}<h2 slot="value">{{ .Value }}</h2>{{ end }}
+ <p>This is a my-text element with value {{ .Value }}!</p>
+ </my-text>
+ {{ end }}
+ </main>
+</body>
\ No newline at end of file
--- /dev/null
+package main
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "os"
+ "text/template"
+
+ tapp "git.earlybird.gay/today/app"
+ "git.earlybird.gay/today/web/page"
+ "git.earlybird.gay/today/web/render"
+)
+
+var Index = page.New("Index", "index.html",
+ page.OnLoad(func(ctx context.Context, data render.Data) error {
+ data.Set("header", MyTextType{Value: "Hello, World!"})
+ texts := make([]MyTextType, 0)
+ for i := range 5 {
+ texts = append(texts, MyTextType{Value: fmt.Sprintf("%d", i)})
+ }
+ data.Set("texts", texts)
+ return nil
+ }),
+ page.Includes(MyText),
+ page.Funcs(template.FuncMap{
+ "log": fmt.Println,
+ }),
+ page.Pretty(" "),
+)
+
+func init() {
+ fmt.Println(Index.Raw(Index.DefaultLanguage()))
+ if Index.Error() != nil {
+ panic(Index.Error())
+ }
+}
+
+func main() {
+ app := tapp.New()
+ app.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
+ Level: slog.LevelDebug,
+ }))
+ app.ShutdownOnSignal(os.Interrupt)
+
+ app.Handle("/{$}", Index)
+
+ app.ListenAndServe()
+}
--- /dev/null
+package main
+
+import (
+ "context"
+ "fmt"
+
+ "git.earlybird.gay/today/web/part"
+ "git.earlybird.gay/today/web/render"
+)
+
+type MyTextType struct {
+ Value string
+}
+
+var MyText = part.New("my-text", "my-text.html",
+ part.OnLoad(func(ctx context.Context, data render.Data) error {
+ fmt.Println(data.Get("value"))
+ return nil
+ }),
+)
+
+func init() {
+ if MyText.Error() != nil {
+ panic(MyText.Error())
+ }
+}
--- /dev/null
+<template>
+ <slot name="value"><p class="default-text">{{ .value.Value }}</p></slot>
+</template>
\ No newline at end of file
import (
"database/sql"
"errors"
- "fmt"
"io"
"io/fs"
"os"
}
}
-func New(name string, confs ...Config) *Database {
+// New creates a new Database.
+// If rootDir is given and isn't absolute, it's a relative reference to
+// the current file. If it's given and absolute, it's used as the root directory
+// for the database and version. If it's not given, the root directory
+// is the directory containing the current file.
+func New(name, rootDir string, confs ...Config) *Database {
db := &Database{
name: name,
}
- db.rootDir, _ = include.Dir("git.earlybird.gay/today/database")
+ if rootDir == "" {
+ db.rootDir, _ = include.Dir("git.earlybird.gay/today/database")
+ } else if !filepath.IsAbs(rootDir) {
+ db.rootDir, _ = include.Dir("git.earlybird.gay/today/database")
+ db.rootDir = filepath.Join(db.rootDir, rootDir)
+ } else {
+ db.rootDir = rootDir
+ }
for _, conf := range confs {
db.err = errors.Join(db.err, conf(db))
if !entry.IsDir() {
continue
}
- fmt.Println("entry", entry.Name())
if submatches := versionRegexp.FindAllStringSubmatch(entry.Name(), -1); len(submatches) == 1 {
dbFolder := submatches[0]
dbf, dbfName := dbFolder[0], dbFolder[1]
)
func TestRawInit(t *testing.T) {
- db := New("raw-init",
- Subdirectory("test/raw-init"),
+ db := New("raw-init", "test/raw-init",
Init(`
CREATE TABLE messages (
user TEXT,
}
func TestFileInit(t *testing.T) {
- db := New("file-init",
- Subdirectory("test/file-init"),
+ db := New("file-init", "test/file-init",
Init(`init.sql`),
)
if db.Error() != nil {
}
func TestVersionsInit(t *testing.T) {
- db := New("versions-init",
- Subdirectory("test/versions-init"),
- )
+ db := New("versions-init", "test/versions-init")
if db.Error() != nil {
t.Fatal(db.Error())
}
}
func TestNamedVersionsInit(t *testing.T) {
- db := New("named-versions-init",
- Subdirectory("test"),
- )
+ db := New("named-versions-init", "test/named-versions-init")
if db.Error() != nil {
t.Fatal(db.Error())
}
--- /dev/null
+ALTER TABLE messages ADD likes INTEGER;
+UPDATE messages SET likes = 5;
\ No newline at end of file
--- /dev/null
+CREATE TABLE messages (
+ user TEXT,
+ msg TEXT
+);
\ No newline at end of file
--- /dev/null
+INSERT INTO messages (user, msg) SELECT 'test-user', msg FROM old.messages;
\ No newline at end of file
--- /dev/null
+CREATE TABLE messages (
+ msg TEXT
+);
+INSERT INTO messages (msg) VALUES ('hello, world');
\ No newline at end of file
--- /dev/null
+named-versions-init is nested so that the test can
+reference the first named-versions-init folder, AND
+the database can be named named-versions-init.
+++ /dev/null
-ALTER TABLE messages ADD likes INTEGER;
-UPDATE messages SET likes = 5;
\ No newline at end of file
+++ /dev/null
-CREATE TABLE messages (
- user TEXT,
- msg TEXT
-);
\ No newline at end of file
+++ /dev/null
-INSERT INTO messages (user, msg) SELECT 'test-user', msg FROM old.messages;
\ No newline at end of file
+++ /dev/null
-CREATE TABLE messages (
- msg TEXT
-);
-INSERT INTO messages (msg) VALUES ('hello, world');
\ No newline at end of file
}
return map[language.Tag]Opener{
DefaultLanguage: file,
- }, nil, err
+ }, nil, nil
}
}
dir = dir[:strings.LastIndexByte(dir, filepath.Separator)]
// Copyright (C) 2024 early (LGPL)
package htmltree
-import "git.earlybird.gay/today/web/internal/html"
+import (
+ "git.earlybird.gay/today/web/internal/html"
+)
func GetAttr(n *html.Node, name string) string {
for _, attr := range n.Attr {
for fname, f := range templateSource.TemplateFuncs() {
templateFuncs[snakeCase(n.Data)+"_"+fname] = f
}
+ // Add $today_parent before template sources
+ n.Parent.InsertBefore(&html.Node{
+ Type: html.TextNode,
+ Data: "{{ $today_parent := . }}",
+ }, n)
// Parse HTML fragment and replace the content of the node with it
computeSubSource, err := insertTemplateSource(lang, templateSource, n)
if err != nil {
htmlId string
compute render.OnLoadFunc
asDataFromParent map[string]string
- raiseFromParent []string
children []*computeNode
}
func (root *computeNode) Compute(r *http.Request) (render.Data, func(any) error, error) {
- impose := func(cData, pData render.Data, raise []string) {
- for _, key := range raise {
- cData.Set(key, pData.Get(key))
- }
- }
var dot any
setDot := func(val any) error {
dot = val
}
childData.Set(dataKey, val)
}
- // Raise parent variables to the "parent" var
- childParentData := make(render.Data)
- impose(childParentData, data, child.raiseFromParent)
- childData.Set("parent", childParentData)
f(child, childData)
return childData, child.compute(ctx, childData)
})
)
var funcRegexp = regexp.MustCompile(`^\(?([a-zA-Z]\w*)\)?$`)
-var fieldRegexp = regexp.MustCompile(`^(?:\.[a-zA-Z]\w*)+$`)
+var fieldRegexp = regexp.MustCompile(`^(?:\.[a-zA-Z]\w*)+$|^\.$`)
var scopesDownRegexp = regexp.MustCompile(`[\{\s]end[\}\s]`)
var setsDotRegexp = regexp.MustCompile(`^\{\{-?\s*(?:with|range).*?\}\}$`)
var templateRegexp = regexp.MustCompile(`\{\{.*\}\}`)
if funcName == "" {
goto done
}
- // fmt.Println("func", token)
- // fmt.Println(source.TemplateFuncs())
// Skip anything that the target source doesn't have as a templatefunc
if _, ok := source.TemplateFuncs()[funcName]; ok {
- // fmt.Println("found", token)
namespaced := snakeCase(source.Name()) + "_" + funcName
token = strings.Replace(token, funcName, namespaced, -1)
}
}
// Sets template fields (.field) in any template strings in this node to
-// .parent.field.
+// $today_parent.field.
// Returns a list of raised fields.
-func replaceTemplateFields(n *html.Node) []string {
- var done, inTemplate, inAttrs bool
+func replaceTemplateFields(n *html.Node) {
+ var inAttrs bool
var datas []string
var isDataAttr []bool
var sets []func(string)
- var raised []string
switch n.Type {
case html.TextNode:
}
}
for i, data := range datas {
+ var done, inTemplate bool
var output []string
for {
endToken := strings.Index(data, " ")
// with a namespaced version
if inTemplate && isField(token) ||
inAttrs && isDataAttr[i] {
- raised = append(raised, token[1:])
- token = ".parent" + token
+ // special case; . is parentable but shouldn't be appended
+ fmt.Println("replacing token", token)
+ if token == "." {
+ token = "$today_parent"
+ } else {
+ token = "$today_parent" + token
+ }
}
-
output = append(output, token)
if done {
break
}
sets[i](strings.Join(output, " "))
}
-
- return raised
}
// Removes data attributes :name.
func insertTemplateSource(lang language.Tag, subSource TemplateSource, context *html.Node) (*computeNode, error) {
computedName := snakeCase(subSource.Name())
// ===== CONTEXT PREPROCESSING =====
- // Raise any fields pre-existing in the context to .parent.field.
- raiseFields := make([]string, 0)
+ // Raise any fields pre-existing in the context to $today_parent.field.
raiseDepth := 0
+ raiseStack := []string{}
htmltree.Walk(context.FirstChild, func(n *html.Node) (bool, error) {
if raiseDepth == 0 {
- raiseFields = append(raiseFields, replaceTemplateFields(n)...)
+ fmt.Println("replace", n.Data)
+ replaceTemplateFields(n)
+ fmt.Println("result", n.Data)
}
if n.Type == html.TextNode {
+ // Scope up/down only when setting dot
+ // Make a better way to do this
if scopesUp(n.Data) {
- raiseDepth++
+ if setsDot(n.Data) {
+ raiseDepth++
+ }
+ raiseStack = append(raiseStack, n.Data)
} else if scopesDown(n.Data) {
- raiseDepth--
+ if setsDot(raiseStack[len(raiseStack)-1]) {
+ raiseDepth--
+ }
+ raiseStack = raiseStack[:len(raiseStack)-1]
}
}
return false, nil
htmlId: htmlId,
compute: subSource.OnLoad(),
asDataFromParent: asData,
- raiseFromParent: raiseFields,
}
// Insert scope up/down template pipeline
templateFuncs template.FuncMap
- raw string
+ raws map[language.Tag]string
chooseRenderable func(r *http.Request) *renderable
err error
p.err = err
return p
}
- p.languages = localeMeta.Languages
- p.translations = localeMeta.Translations
- p.defaultLanguage = localeMeta.DefaultLanguage
+ if localeMeta != nil {
+ p.languages = localeMeta.Languages
+ p.translations = localeMeta.Translations
+ p.defaultLanguage = localeMeta.DefaultLanguage
+ } else {
+ p.languages = []language.Tag{language.English}
+ p.defaultLanguage = language.English
+ }
p.sources = sources
p.onLoad = func(ctx context.Context, d render.Data) error {
return nil
return p
}
+ p.raws = make(map[language.Tag]string)
renderables := make(map[language.Tag]*renderable)
for _, result := range results {
+ p.raws[result.Language] = result.TemplateRaw
// templatize
tmpl, err := template.New(p.name).
Funcs(result.TemplateFuncs).
templateLoad: result.TemplateDataLoader,
}
}
- // result := results[0]
- // p.raw = result.TemplateRaw
- // p.templateLoad = result.TemplateDataLoader
-
- // selector := language.NewMatcher(localeMeta.Languages)
p.chooseRenderable = func(r *http.Request) *renderable {
lang, ok := r.Context().Value(localization.Locale).(language.Tag)
if !ok {
- return renderables[localeMeta.DefaultLanguage]
+ return renderables[p.defaultLanguage]
}
if chosen, ok := renderables[lang]; ok {
return chosen
} else {
- return renderables[localeMeta.DefaultLanguage]
+ return renderables[p.defaultLanguage]
}
}
return p
renderables := make(map[language.Tag]*renderable)
for _, result := range results {
+ p.raws[result.Language] = result.TemplateRaw
// templatize
tmpl, err := template.New(p.name).
Funcs(result.TemplateFuncs).
return p.includes
}
-func (p *Page) Raw() string {
- return p.raw
+func (p *Page) Raw(lang language.Tag) string {
+ return p.raws[lang]
}
func (p *Page) Error() error {
p.err = err
return p
}
- p.languages = localeMeta.Languages
- p.translations = localeMeta.Translations
- p.defaultLanguage = localeMeta.DefaultLanguage
+ if localeMeta != nil {
+ p.languages = localeMeta.Languages
+ p.translations = localeMeta.Translations
+ p.defaultLanguage = localeMeta.DefaultLanguage
+ } else {
+ p.languages = []language.Tag{language.English}
+ p.defaultLanguage = language.English
+ }
p.sources = sources
if p.err != nil {
return p