Introduced templating and views

This commit is contained in:
Simon Martens
2025-02-09 14:51:04 +01:00
parent 159fa1a7bb
commit a250d1b18e
209 changed files with 74559 additions and 0 deletions

9
.gitignore vendored
View File

@@ -1,2 +1,11 @@
pb_data/
musenalm
node_modules
tmp/
bin/
cache/
data_bilder/
config.json
out.log
*.log
*.out

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 KiB

BIN
Static-Bilder/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 KiB

BIN
Static-Bilder/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

BIN
Static-Bilder/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 MiB

View 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 17651833 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

43
helpers/errors.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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)
}

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 117 KiB

BIN
views/assets/GND.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View 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");
}

File diff suppressed because it is too large Load Diff

BIN
views/assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More