Introduced templating and views
9
.gitignore
vendored
@@ -1,2 +1,11 @@
|
||||
pb_data/
|
||||
musenalm
|
||||
node_modules
|
||||
tmp/
|
||||
bin/
|
||||
cache/
|
||||
data_bilder/
|
||||
config.json
|
||||
out.log
|
||||
*.log
|
||||
*.out
|
||||
|
||||
BIN
Static-Bilder/1-vorschau.jpg
Normal file
|
After Width: | Height: | Size: 845 KiB |
BIN
Static-Bilder/1.jpg
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
Static-Bilder/1770ff_freigestellt_JB-vorschau.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
Static-Bilder/1770ff_freigestellt_JB.png
Normal file
|
After Width: | Height: | Size: 5.5 MiB |
BIN
Static-Bilder/2-vorschau.jpg
Normal file
|
After Width: | Height: | Size: 855 KiB |
BIN
Static-Bilder/2.jpg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
Static-Bilder/3-vorschau.jpg
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
Static-Bilder/3.jpg
Normal file
|
After Width: | Height: | Size: 949 KiB |
BIN
Static-Bilder/Alm_des_Muses_frgst_JB-hintergrund.png
Normal file
|
After Width: | Height: | Size: 9.8 MiB |
BIN
Static-Bilder/Alm_des_Muses_frgst_JB-vorschau.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
Static-Bilder/Alm_des_Muses_frgst_JB.png
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
BIN
Static-Bilder/Alm_des_Pros_frgst_JB-hintergrund.png
Normal file
|
After Width: | Height: | Size: 12 MiB |
BIN
Static-Bilder/Alm_des_Pros_frgst_JB-vorschau.png
Normal file
|
After Width: | Height: | Size: 4.1 MiB |
BIN
Static-Bilder/Alm_des_Pros_frgst_JB.png
Normal file
|
After Width: | Height: | Size: 7.9 MiB |
BIN
Static-Bilder/Apo_und_Schk_frgst_JB-hintergrund.png
Normal file
|
After Width: | Height: | Size: 9.8 MiB |
BIN
Static-Bilder/Apo_und_Schk_frgst_JB-vorschau.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
Static-Bilder/Apo_und_Schk_frgst_JB.png
Normal file
|
After Width: | Height: | Size: 5.4 MiB |
BIN
Static-Bilder/Forst-, Jagd-, Garten- TBs MS-hintergrund.png
Normal file
|
After Width: | Height: | Size: 14 MiB |
BIN
Static-Bilder/Forst-, Jagd-, Garten- TBs MS-vorschau.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
Static-Bilder/Forst-, Jagd-, Garten- TBs MS.png
Normal file
|
After Width: | Height: | Size: 8.9 MiB |
BIN
Static-Bilder/Gött. MA, Alm d d M 1770 MS-hintergrund.png
Normal file
|
After Width: | Height: | Size: 10 MiB |
BIN
Static-Bilder/Gött. MA, Alm d d M 1770 MS-vorschau.png
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
BIN
Static-Bilder/Gött. MA, Alm d d M 1770 MS.png
Normal file
|
After Width: | Height: | Size: 5.8 MiB |
BIN
Static-Bilder/Kotzebue_Thea_frgst_JB-hintergrund.png
Normal file
|
After Width: | Height: | Size: 10 MiB |
BIN
Static-Bilder/Kotzebue_Thea_frgst_JB-vorschau.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
Static-Bilder/Kotzebue_Thea_frgst_JB.png
Normal file
|
After Width: | Height: | Size: 5.0 MiB |
BIN
Static-Bilder/Miniaturformat1-vorschau.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
Static-Bilder/Miniaturformat1.png
Normal file
|
After Width: | Height: | Size: 5.9 MiB |
BIN
Static-Bilder/TB z ges. Vergn-hintergrund.png
Normal file
|
After Width: | Height: | Size: 13 MiB |
BIN
Static-Bilder/TB z ges. Vergn-vorschau.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
Static-Bilder/TB z ges. Vergn.png
Normal file
|
After Width: | Height: | Size: 9.5 MiB |
BIN
Static-Bilder/Wiener Almanache-hintergrund.png
Normal file
|
After Width: | Height: | Size: 15 MiB |
BIN
Static-Bilder/Wiener Almanache-vorschau.png
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
BIN
Static-Bilder/Wiener Almanache.png
Normal file
|
After Width: | Height: | Size: 8.7 MiB |
51
Static-Bilder/beschreibungen.txt
Normal file
@@ -0,0 +1,51 @@
|
||||
# Alm_des_Muses_frgst_JB.png
|
||||
## Almanach des Muses
|
||||
### Trotz wechselhafter Zeiten erschien der <em>Almanach des Muses ou Choix des Poésies fugitives</em> von 1765–1833 beinahe ohne Unterbrechung – mit dem Konzept, ein Panorama der zeitgenössischen Dichtung zu liefern, wurde er er zum Vorbild für die ersten Musenalmanache in Deutschland.
|
||||
|
||||
# Alm_des_Pros_frgst_JB.png
|
||||
## Almanach des Prosateurs
|
||||
### In Abrenzung vom <em>Almanach des Muses</em>, der sich auf Lyrik konzentrierte, versammelte der <em>Almanach des Prosateurs</em> auschließlich Prosastücke – auch um den publizistischen Rang dieser Gattung neben der Poesie zu behaupten. Die abgebildeten Exemplare stammen aus der Privatbibliothek Georg Augusts von Mecklenburg, der als General in russischen Diensten stand.
|
||||
|
||||
# Kotzebue_Thea_frgst_JB.png
|
||||
## Almanach dramatischer Spiele
|
||||
### Bis zu seiner Ermordung war Kotzebue Herausgeber und einziger Beiträger dieses Almanachs, der sich mit seinen dramatischen Stücken explizit an Dilettanten- und Liebhabertheater wandte. Die kolorierten Rollenkupfer geben Fingerzeige für Schauspieler und Requisite.
|
||||
|
||||
# Apo_und_Schk_frgst_JB.png
|
||||
## TB für Scheidekünstler und Apotheker
|
||||
### Taschenbücher waren nicht ausschließlich dem Schöngeistigen gewidmet. Das <em>Taschenbuch für Scheidekünstler und Apotheker</em> gab u. a. Anleitung zur Herstellung von Berliner Blau und zum feuerfesten Ausbessern von gesprungenem Porzellan und Glas.
|
||||
|
||||
# Gött. MA, Alm d d M 1770 MS.png
|
||||
## Göttinger MA und Alm d. dt. Musen
|
||||
### Als erster deutscher Musenalmanach erschien der Göttinger Musenalmanach zur Leipziger Michaelismesse. Zeitgleich erschien der Almanach der deutschen Musen in Leipzig unter Verwendung von 18 Gedichten aus dem Göttinger Musenalmanach: Schon vor Drucklegung waren Bögen entwendet und samt Druckfehlern übernommen worden.
|
||||
|
||||
# 1770ff_freigestellt_JB.png
|
||||
## 1770ff
|
||||
### Die ersten Jahrgänge der beiden ersten deutschen Musenalmanache. Der Göttinger Musenalmanach erschien in 35 Jahrgängen 1770-1804. Der Almanach der deutschen Musen (Leipzig) erschien 1770-1881.
|
||||
|
||||
# Miniaturformat1.png
|
||||
## Miniaturformate
|
||||
### In Karlsruhe und Wien erschienen, als Ausweis drucktechnischer und buchmacherrischer Finesse, einige Almanache im Kleinstformat
|
||||
|
||||
# Forst-, Jagd-, Garten- TBs MS.png
|
||||
## Forst, Jagd, Garten
|
||||
### Sprachlich reizvoll und mit kolorierten Illustrationen und Plänen reich ausgestattet richten sich die Jagd-, Forst und Gartenalmanache an Fachleute und ein interessiertes Lesepublikum.
|
||||
|
||||
# TB z ges. Vergn.png
|
||||
## Taschenbuch der Liebe und Freundschaft
|
||||
### Das <em>Taschenbuch der Liebe und Freundschaft</em> erschien bei Wilmans in Bremen 1800-1802 dann in Frankfurt 1803-1841 und gehört zu den langlebigsten Reihen. Es konnte, vor allem unter dem Herausgeber Stephan Schütze 1811-1839 eine reizvolle Gestaltung und literarisches Niveau halten.
|
||||
|
||||
# urania-freigestellt.png
|
||||
## Urania
|
||||
### 1810-1848 erschien bei Brockhaus in Amsterdam, dann in Leipzig die Urania. Die ersten Jahrgänge gab Johanne Caroline Wilhelmine Spazier heraus, es folgte 1812 bis 1824 Johann Arnold Brockhaus. Über Jahre hielt das Taschenbuch ein hohes Niveau, fiel in den 30er Jahren ab und wurde 1848 eingestellt. Das Taschenbuch in seiner jährlichen Erscheinungsweise klang als Medium aus.
|
||||
|
||||
# Wiener Almanache.png
|
||||
## Wiener Almanache
|
||||
### Sorgfältige, liebevolle Gestaltung bei wechselhaftem Niveau der literarischen Beiträge zeichnen die Wiener Almanache und Taschenbücher aus.
|
||||
|
||||
# 1.jpg
|
||||
## Taschenbuch zum geselligen Vergnügen
|
||||
### Eine häufige Beigabe im Taschenbuch zum geselligen Vergnügen waren Tänze (für Klavier gesetzt) mit Tanzanleitungen. Hier: Tanz (Quadrille), Tanzschritte und Tanzanleitung aus dem Jg. 1795
|
||||
|
||||
# 3.jpg
|
||||
## Königl. Großbritannischen Genealogischen Kalender 1784
|
||||
### Modekupfer, häufig koloriert, waren beliebte Beigaben. Hier ein Beispiel aus dem Königl. Großbritannischen Genealogischen Kalender 1784
|
||||
BIN
Static-Bilder/musen.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
Static-Bilder/urania-freigestellt-vorschau.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
Static-Bilder/urania-freigestellt.png
Normal file
|
After Width: | Height: | Size: 5.8 MiB |
BIN
Static-Bilder/urania-hintergrund.png
Normal file
|
After Width: | Height: | Size: 11 MiB |
43
helpers/errors.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/kgpz_web/helpers/logging"
|
||||
)
|
||||
|
||||
func Assert(err error, msg ...string) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
logging.Error(err, msg...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func AssertNonNil(obj interface{}, msg ...string) {
|
||||
if obj != nil {
|
||||
return
|
||||
}
|
||||
|
||||
logging.Error(nil, msg...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func AssertNil(obj interface{}, msg ...string) {
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
logging.Error(nil, msg...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func AssertStr(str string, msg ...string) {
|
||||
if str != "" {
|
||||
return
|
||||
}
|
||||
|
||||
logging.Error(nil, msg...)
|
||||
os.Exit(1)
|
||||
}
|
||||
14
templating/consts.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package templating
|
||||
|
||||
var TEMPLATE_FORMATS = []string{".html", ".tmpl", ".gotmpl", ".gotemplate", ".gohtml", ".gohtmltemplate"}
|
||||
|
||||
const TEMPLATE_GLOBAL_CONTEXT_NAME = "globals"
|
||||
const TEMPLATE_ROOT_NAME = "root"
|
||||
const TEMPLATE_LOCAL_CONTEXT_NAME = "locals"
|
||||
const TEMPLATE_GLOBAL_PREFIX = "_"
|
||||
const TEMPLATE_COMPONENT_DIRECTORY = "components"
|
||||
const TEMPLATE_HEAD = "head"
|
||||
const TEMPLATE_BODY = "body"
|
||||
const TEMPLATE_HEADERS = "headers"
|
||||
const ROOT_LAYOUT_NAME = "root"
|
||||
const DEFAULT_LAYOUT_NAME = "default"
|
||||
148
templating/context.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package templating
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type TemplateContext struct {
|
||||
// WARNING: Path is a URL path, NOT a filesystem path
|
||||
Path string
|
||||
// WARNING: The keys of these maps are template names, NOT filesystem paths
|
||||
// The values are FS paths absolute from the root directory of the templates FS
|
||||
locals map[string]string
|
||||
globals map[string]string
|
||||
}
|
||||
|
||||
func NewTemplateContext(path string) TemplateContext {
|
||||
return TemplateContext{
|
||||
Path: path,
|
||||
locals: make(map[string]string),
|
||||
globals: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TemplateContext) Parse(fsys fs.FS) error {
|
||||
fspath := PathToFSPath(c.Path)
|
||||
entries, err := fs.ReadDir(fsys, fspath)
|
||||
|
||||
if err != nil {
|
||||
return NewError(InvalidPathError, c.Path)
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
// INFO: components in the components directory can be overwritten
|
||||
// by components in the base directory down below
|
||||
// TODO: Maybe allow for subdirectories in the components directory?
|
||||
if e.Name() == TEMPLATE_COMPONENT_DIRECTORY {
|
||||
entries, err := fs.ReadDir(fsys, filepath.Join(fspath, e.Name()))
|
||||
if err != nil {
|
||||
return NewError(FileAccessError, filepath.Join(fspath, e.Name()))
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
ext := filepath.Ext(e.Name())
|
||||
|
||||
if !slices.Contains(TEMPLATE_FORMATS, ext) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.TrimSuffix(e.Name(), ext)
|
||||
if strings.HasPrefix(e.Name(), TEMPLATE_GLOBAL_PREFIX) {
|
||||
c.globals[name] = filepath.Join(fspath, TEMPLATE_COMPONENT_DIRECTORY, e.Name())
|
||||
} else {
|
||||
c.locals[name] = filepath.Join(fspath, TEMPLATE_COMPONENT_DIRECTORY, e.Name())
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
ext := filepath.Ext(e.Name())
|
||||
|
||||
if !slices.Contains(TEMPLATE_FORMATS, ext) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.TrimSuffix(e.Name(), ext)
|
||||
if strings.HasPrefix(e.Name(), TEMPLATE_GLOBAL_PREFIX) {
|
||||
c.globals[name] = filepath.Join(fspath, e.Name())
|
||||
} else {
|
||||
c.locals[name] = filepath.Join(fspath, e.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *TemplateContext) SetGlobals(globals map[string]string) error {
|
||||
// INFO: this allows for overwriting of existing global keys.
|
||||
// Make sure to call this appopriately before or after Parse(), depending on your use case
|
||||
for k, v := range globals {
|
||||
c.globals[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *TemplateContext) Globals() map[string]string {
|
||||
return c.globals
|
||||
}
|
||||
|
||||
func (c *TemplateContext) Template(fsys fs.FS, funcmap *template.FuncMap) (*template.Template, error) {
|
||||
// TODO: locals need to be in order: root, head, body
|
||||
t, err := readTemplates(fsys, nil, c.locals, funcmap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err = readTemplates(fsys, t, c.globals, funcmap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func readTemplates(fsys fs.FS, t *template.Template, paths map[string]string, funcmap *template.FuncMap) (*template.Template, error) {
|
||||
for k, v := range paths {
|
||||
text, err := fs.ReadFile(fsys, v)
|
||||
if err != nil {
|
||||
return nil, NewError(FileAccessError, v)
|
||||
}
|
||||
|
||||
temp := template.New(k)
|
||||
|
||||
if funcmap != nil {
|
||||
temp.Funcs(*funcmap)
|
||||
}
|
||||
|
||||
temp, err = temp.Parse(string(text))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if t == nil {
|
||||
t = temp
|
||||
continue
|
||||
}
|
||||
|
||||
for _, template := range temp.Templates() {
|
||||
_, err = t.AddParseTree(template.Name(), template.Tree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = t.AddParseTree(temp.Name(), temp.Tree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
148
templating/engine.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package templating
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
ASSETS_URL_PREFIX = "/assets"
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
// NOTE: LayoutRegistry and TemplateRegistry have their own syncronization & cache and do not require a mutex here
|
||||
LayoutRegistry *LayoutRegistry
|
||||
TemplateRegistry *TemplateRegistry
|
||||
|
||||
mu *sync.Mutex
|
||||
FuncMap template.FuncMap
|
||||
GlobalData map[string]interface{}
|
||||
}
|
||||
|
||||
// INFO: We pass the app here to be able to access the config and other data for functions
|
||||
// which also means we must reload the engine if the app changes
|
||||
func NewEngine(layouts, templates *fs.FS) *Engine {
|
||||
e := Engine{
|
||||
mu: &sync.Mutex{},
|
||||
LayoutRegistry: NewLayoutRegistry(*layouts),
|
||||
TemplateRegistry: NewTemplateRegistry(*templates),
|
||||
FuncMap: make(template.FuncMap),
|
||||
}
|
||||
e.funcs()
|
||||
return &e
|
||||
}
|
||||
|
||||
func (e *Engine) funcs() error {
|
||||
e.mu.Lock()
|
||||
e.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) Globals(data map[string]interface{}) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.GlobalData == nil {
|
||||
e.GlobalData = data
|
||||
} else {
|
||||
for k, v := range data {
|
||||
(e.GlobalData)[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) Load() error {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
e.LayoutRegistry.Load()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
e.TemplateRegistry.Load()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) Reload() error {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
e.LayoutRegistry.Reset()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
e.TemplateRegistry.Reset()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// INFO: fn is a function that returns either one value or two values, the second one being an error
|
||||
func (e *Engine) AddFunc(name string, fn interface{}) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
e.FuncMap[name] = fn
|
||||
}
|
||||
|
||||
func (e *Engine) AddFuncs(funcs map[string]interface{}) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
for k, v := range funcs {
|
||||
e.FuncMap[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) Render(out io.Writer, path string, ld map[string]interface{}, layout ...string) error {
|
||||
// TODO: check if a reload is needed if files on disk have changed
|
||||
gd := e.GlobalData
|
||||
if e.GlobalData != nil {
|
||||
for k, v := range ld {
|
||||
gd[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
var l *template.Template
|
||||
if layout == nil || len(layout) == 0 {
|
||||
lay, err := e.LayoutRegistry.Default(&e.FuncMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l = lay
|
||||
} else {
|
||||
lay, err := e.LayoutRegistry.Layout(layout[0], &e.FuncMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l = lay
|
||||
}
|
||||
|
||||
lay, err := l.Clone()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = e.TemplateRegistry.Add(path, lay, &e.FuncMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = lay.Execute(out, gd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
21
templating/errors.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package templating
|
||||
|
||||
import "errors"
|
||||
|
||||
var InvalidPathError = errors.New("Invalid path. Must be a directory.")
|
||||
var NoTemplateError = errors.New("No template found for this name")
|
||||
var InvalidTemplateError = errors.New("invalid template")
|
||||
var FileAccessError = errors.New("could not stat file or directory")
|
||||
|
||||
type FSError[T error] struct {
|
||||
File string
|
||||
Err T
|
||||
}
|
||||
|
||||
func NewError[T error](t T, file string) FSError[T] {
|
||||
return FSError[T]{File: file, Err: t}
|
||||
}
|
||||
|
||||
func (e FSError[T]) Error() string {
|
||||
return e.Err.Error() + ": " + e.File
|
||||
}
|
||||
32
templating/helpers.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package templating
|
||||
|
||||
import "strings"
|
||||
|
||||
func PathToFSPath(p string) string {
|
||||
if p == "/" {
|
||||
return "."
|
||||
}
|
||||
|
||||
p = strings.TrimPrefix(p, "/")
|
||||
p = strings.TrimSuffix(p, "/")
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func FSPathToPath(p string) string {
|
||||
if p == "." {
|
||||
return "/"
|
||||
}
|
||||
|
||||
p = strings.TrimPrefix(p, ".")
|
||||
|
||||
if !strings.HasPrefix(p, "/") {
|
||||
p = "/" + p
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(p, "/") {
|
||||
p = p + "/"
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
108
templating/layout_registry.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package templating
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"sync"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers"
|
||||
"github.com/yalue/merged_fs"
|
||||
)
|
||||
|
||||
// TODO: Implement Handler interface, maybe in template? But then template would need to know about the layout registry
|
||||
// Function signature: func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
// A static Handler could incoporate both the layout registry and the template registry and serve templates that dont need any data
|
||||
// INFO: this ist thread-safe and safe to call in a handler or middleware
|
||||
type LayoutRegistry struct {
|
||||
layoutsFS fs.FS
|
||||
once sync.Once
|
||||
// INFO: Layout & cache keys are template directory names
|
||||
layouts map[string]TemplateContext
|
||||
// WARNING: maybe this is too early for caching?
|
||||
cache sync.Map
|
||||
}
|
||||
|
||||
func NewLayoutRegistry(routes fs.FS) *LayoutRegistry {
|
||||
return &LayoutRegistry{
|
||||
layoutsFS: routes,
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Upon registering a new layout dir, we return a new LayoutRegistry
|
||||
func (r *LayoutRegistry) Register(fs fs.FS) *LayoutRegistry {
|
||||
return NewLayoutRegistry(merged_fs.MergeMultiple(fs, r.layoutsFS))
|
||||
}
|
||||
|
||||
func (r *LayoutRegistry) Reset() error {
|
||||
r.cache.Clear()
|
||||
r.once = sync.Once{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LayoutRegistry) Load() error {
|
||||
r.once.Do(func() {
|
||||
err := r.load()
|
||||
helpers.Assert(err, "Error loading layouts. Exiting.")
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LayoutRegistry) load() error {
|
||||
layouts := make(map[string]TemplateContext)
|
||||
rootcontext := NewTemplateContext(".")
|
||||
err := rootcontext.Parse(r.layoutsFS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
globals := rootcontext.Globals()
|
||||
|
||||
entries, err := fs.ReadDir(r.layoutsFS, ".")
|
||||
if err != nil {
|
||||
return NewError(FileAccessError, ".")
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() || e.Name() == TEMPLATE_COMPONENT_DIRECTORY {
|
||||
continue
|
||||
}
|
||||
|
||||
url := FSPathToPath(e.Name())
|
||||
context := NewTemplateContext(url)
|
||||
context.SetGlobals(globals)
|
||||
context.Parse(r.layoutsFS)
|
||||
|
||||
layouts[e.Name()] = context
|
||||
}
|
||||
|
||||
r.layouts = layouts
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LayoutRegistry) Layout(name string, funcmap *template.FuncMap) (*template.Template, error) {
|
||||
cached, ok := r.cache.Load(name)
|
||||
if ok {
|
||||
return cached.(*template.Template), nil
|
||||
}
|
||||
|
||||
// TODO: What todo on errors?
|
||||
r.Load()
|
||||
context, ok := r.layouts[name]
|
||||
if !ok {
|
||||
return nil, NewError(NoTemplateError, name)
|
||||
}
|
||||
|
||||
t, err := context.Template(r.layoutsFS, funcmap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.cache.Store(name, t)
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (r *LayoutRegistry) Default(funcmap *template.FuncMap) (*template.Template, error) {
|
||||
return r.Layout(DEFAULT_LAYOUT_NAME, funcmap)
|
||||
}
|
||||
120
templating/template_registry.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package templating
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers"
|
||||
"github.com/yalue/merged_fs"
|
||||
)
|
||||
|
||||
// INFO: this ist thread-safe and safe to call in a handler or middleware
|
||||
type TemplateRegistry struct {
|
||||
routesFS fs.FS
|
||||
once sync.Once
|
||||
// INFO: Template & cache keys are directory routing paths, with '/' as root
|
||||
// INFO: we don't need a mutex here since this is set in Load() protected by Once().
|
||||
templates map[string]TemplateContext
|
||||
cache sync.Map
|
||||
}
|
||||
|
||||
func NewTemplateRegistry(routes fs.FS) *TemplateRegistry {
|
||||
return &TemplateRegistry{
|
||||
routesFS: routes,
|
||||
}
|
||||
}
|
||||
|
||||
// INFO: This returns a new TemplateRegistry with the new fs added to the existing fs,
|
||||
// merging with the existing FS, possibly overwriting existing files.
|
||||
func (r *TemplateRegistry) Register(path string, fs fs.FS) *TemplateRegistry {
|
||||
return NewTemplateRegistry(merged_fs.MergeMultiple(fs, r.routesFS))
|
||||
}
|
||||
|
||||
func (r *TemplateRegistry) Reset() error {
|
||||
r.cache.Clear()
|
||||
r.once = sync.Once{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *TemplateRegistry) Load() error {
|
||||
r.once.Do(func() {
|
||||
err := r.load()
|
||||
helpers.Assert(err, "Error loading templates. Exiting.")
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Throw errors
|
||||
// TODO: what if there is no template in the directory above?
|
||||
// What if a certain path is or should uncallable since it has no index or body?
|
||||
func (r *TemplateRegistry) load() error {
|
||||
templates := make(map[string]TemplateContext)
|
||||
fs.WalkDir(r.routesFS, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if !d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
url := FSPathToPath(path)
|
||||
tc := NewTemplateContext(url)
|
||||
|
||||
if path != "." {
|
||||
pathelem := strings.Split(path, string(os.PathSeparator))
|
||||
pathabove := strings.Join(pathelem[:len(pathelem)-1], string(os.PathSeparator))
|
||||
pathabove = FSPathToPath(pathabove)
|
||||
|
||||
global, ok := templates[pathabove]
|
||||
if ok {
|
||||
tc.SetGlobals(global.Globals())
|
||||
}
|
||||
}
|
||||
|
||||
tc.Parse(r.routesFS)
|
||||
|
||||
templates[url] = tc
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
r.templates = templates
|
||||
return nil
|
||||
}
|
||||
|
||||
// This function takes a template (typically a layout) and adds all the templates of
|
||||
// a given directory path to it. This is useful for adding a layout to a template.
|
||||
func (r *TemplateRegistry) Add(path string, t *template.Template, funcmap *template.FuncMap) error {
|
||||
temp, ok := r.cache.Load(path)
|
||||
if !ok {
|
||||
r.Load()
|
||||
tc, ok := r.templates[path]
|
||||
if !ok {
|
||||
return NewError(NoTemplateError, path)
|
||||
}
|
||||
|
||||
template, err := tc.Template(r.routesFS, funcmap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.cache.Store(path, template)
|
||||
|
||||
return r.Add(path, t, funcmap)
|
||||
}
|
||||
|
||||
casted := temp.(*template.Template)
|
||||
for _, st := range casted.Templates() {
|
||||
_, err := t.AddParseTree(st.Name(), st.Tree)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: get for a specific component
|
||||
func (r *TemplateRegistry) Get(path string) error {
|
||||
return nil
|
||||
}
|
||||
8
views/.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
||||
[*]
|
||||
trim_trailing_whitespace = true
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
indent_style = tab
|
||||
indent_size = 2
|
||||
max_line_length = 100
|
||||
24
views/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
10
views/.prettierrc
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
bracketSameLine: true,
|
||||
bracketSpacing: true,
|
||||
whitespaceSensitivity: "ignore",
|
||||
proseWrap: "always",
|
||||
bracketLine: true,
|
||||
useTabs: true,
|
||||
tabWidth: 2,
|
||||
"plugins": ["prettier-plugin-go-template"]
|
||||
}
|
||||
2952
views/assets/Diagram.svg
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
views/assets/GND.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
71
views/assets/css/fonts.css
Normal file
@@ -0,0 +1,71 @@
|
||||
@font-face {
|
||||
font-family: "Rancho";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/Rancho-Regular.ttf) format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Merriweather";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/Merriweather-Regular.ttf) format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Merriweather";
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/Merriweather-Italic.ttf) format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Merriweather";
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/Merriweather-Bold.ttf) format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Merriweather";
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/SourceSans3-BoldItalic.ttf) format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Sans 3";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/SourceSans3-Medium.ttf) format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Sans 3";
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/SourceSans3-MediumItalic.ttf) format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Sans 3";
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/SourceSans3-Bold.ttf) format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Sans 3";
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/SourceSans3-BoldItalic.ttf) format("truetype");
|
||||
}
|
||||
3104
views/assets/css/remixicon.css
Normal file
BIN
views/assets/favicon.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |