fix style, implement config load/save

This commit is contained in:
Iris Lightshard 2022-06-07 23:44:54 -06:00
parent 8fdc9ddb46
commit 37d184b3e2
Signed by: Iris Lightshard
GPG key ID: 3B7FBC22144E6398
9 changed files with 333 additions and 54 deletions

View file

@ -5,13 +5,18 @@ type BuildStatus struct {
Message string Message string
} }
type ConfigOption struct {
Name string
Type string
}
type Adapter interface { type Adapter interface {
Init(cfg *Config) Init(cfg *Config)
Name() string Name() string
EditableSlugs() bool EditableSlugs() bool
BuildOptions() []string BuildOptions() []string
GetConfig(key string) (interface{}, error) GetConfig() map[ConfigOption]string
SetConfig(key string, value interface{}) error SetConfig(map[ConfigOption]string) error
ListPages() map[string]string ListPages() map[string]string
GetPage(string) (Page, error) GetPage(string) (Page, error)
FormatPage(string) string FormatPage(string) string

View file

@ -6,12 +6,13 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
) )
type EurekaAdapter struct { type EurekaAdapter struct {
Root string Root string
Config map[string]interface{} Config map[ConfigOption]string
} }
func (self *EurekaAdapter) Init(cfg *Config) { func (self *EurekaAdapter) Init(cfg *Config) {
@ -23,8 +24,12 @@ func (self *EurekaAdapter) Init(cfg *Config) {
} }
self.Root = cfg.Root self.Root = cfg.Root
self.Config = make(map[ConfigOption]string)
// TODO: read config.h and build self.Config // TODO: read config.h and build self.Config
err = self.readCfg()
if err != nil {
panic("config.h is malformed!")
}
} }
func (self *EurekaAdapter) Name() string { func (self *EurekaAdapter) Name() string {
@ -39,12 +44,13 @@ func (self *EurekaAdapter) BuildOptions() []string {
return []string{"twtxt"} return []string{"twtxt"}
} }
func (self *EurekaAdapter) GetConfig(key string) (interface{}, error) { func (self *EurekaAdapter) GetConfig() map[ConfigOption]string {
return nil, nil return self.Config
} }
func (self *EurekaAdapter) SetConfig(key string, value interface{}) error { func (self *EurekaAdapter) SetConfig(cfg map[ConfigOption]string) error {
return nil self.Config = cfg
return self.writeCfg()
} }
func (self *EurekaAdapter) ListPages() map[string]string { func (self *EurekaAdapter) ListPages() map[string]string {
@ -85,7 +91,6 @@ func (self *EurekaAdapter) GetPage(filename string) (Page, error) {
return Page{ return Page{
Title: title, Title: title,
Slug: filename,
Content: content, Content: content,
Edited: fileInfo.ModTime(), Edited: fileInfo.ModTime(),
}, nil }, nil
@ -129,7 +134,15 @@ func (self *EurekaAdapter) SavePage(oldSlug, newSlug, title, content string) err
defer f.Close() defer f.Close()
if oldSlug != newSlug { if oldSlug != newSlug {
// TODO: delete old html as well siteRoot := self.Config[ConfigOption{
Name: "SITEROOT",
Type: "string",
}]
htmlFile := filepath.Join(self.Root, siteRoot, oldSlug+"l")
_, err := os.Stat(htmlFile)
if !os.IsNotExist(err) {
os.Remove(htmlFile)
}
os.Remove(filepath.Join(self.Root, "inc", oldSlug)) os.Remove(filepath.Join(self.Root, "inc", oldSlug))
} }
@ -138,7 +151,15 @@ func (self *EurekaAdapter) SavePage(oldSlug, newSlug, title, content string) err
} }
func (self *EurekaAdapter) DeletePage(slug string) error { func (self *EurekaAdapter) DeletePage(slug string) error {
// TODO: delete old html as well siteRoot := self.Config[ConfigOption{
Name: "SITEROOT",
Type: "string",
}]
htmlFile := filepath.Join(self.Root, siteRoot, slug+"l")
_, err := os.Stat(htmlFile)
if !os.IsNotExist(err) {
os.Remove(htmlFile)
}
return os.Remove(filepath.Join(self.Root, "inc", slug)) return os.Remove(filepath.Join(self.Root, "inc", slug))
} }
@ -147,7 +168,7 @@ func (self *EurekaAdapter) Build(buildOptions map[string][]string) BuildStatus {
twtxt := buildOptions["twtxt"][0] twtxt := buildOptions["twtxt"][0]
cmdArgs := "" cmdArgs := ""
if twtxt != "" { if twtxt != "" {
cmdArgs += " -t " + twtxt cmdArgs += "-t " + twtxt
} }
cmd := exec.Command("./build.sh", cmdArgs) cmd := exec.Command("./build.sh", cmdArgs)
@ -159,3 +180,111 @@ func (self *EurekaAdapter) Build(buildOptions map[string][]string) BuildStatus {
Message: string(out), Message: string(out),
} }
} }
func (self *EurekaAdapter) readCfg() error {
configPath := filepath.Join(self.Root, "config.h")
_, err := os.Stat(filepath.Join(self.Root, "config.h"))
if os.IsNotExist(err) {
configPath = filepath.Join(self.Root, "config.def.h")
}
f, err := os.ReadFile(configPath)
if err != nil {
return err
}
fileData := string(f[:])
macros := strings.Split(fileData, "#define ")[1:]
for _, macro := range macros {
tokens := strings.Split(strings.TrimSpace(macro), " ")
k := tokens[0]
v := strings.TrimSpace(strings.Join(tokens[1:], " "))
if strings.Contains(v, "\"") {
if strings.Contains(v, "\\\r\n") || strings.Contains(v, "\\\n") {
// process multiline string
lines := strings.Split(v, "\n")
cleanedString := ""
for _, l := range lines {
l = strings.TrimSuffix(l, "\r")
l = strings.TrimSuffix(l, "\\")
l = strings.TrimSpace(l)
l = strings.TrimPrefix(l, "\"")
l = strings.TrimSuffix(l, "\"")
l = strings.ReplaceAll(l, "\\\"", "\"")
l = strings.ReplaceAll(l, "\\n", "\n")
cleanedString += l
}
self.Config[ConfigOption{
Name: k,
Type: "multilinestring",
}] = cleanedString
} else {
cleanedString := strings.TrimSuffix(strings.TrimPrefix(v, "\""), "\"")
cleanedString = strings.ReplaceAll(cleanedString, "\\n", "\n")
cleanedString = strings.ReplaceAll(cleanedString, "\r", "")
cleanedString = strings.ReplaceAll(cleanedString, "\\\"", "\"")
self.Config[ConfigOption{
Name: k,
Type: "string",
}] = cleanedString
}
} else if strings.Contains(v, ".") {
_, err := strconv.ParseFloat(v, 64)
if err != nil {
return err
}
self.Config[ConfigOption{
Name: k,
Type: "float",
}] = v
} else {
_, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return err
}
self.Config[ConfigOption{
Name: k,
Type: "int",
}] = v
}
}
return nil
}
func (self *EurekaAdapter) writeCfg() error {
f, err := os.Create(filepath.Join(self.Root, "config.h"))
if err != nil {
return err
}
defer f.Close()
for k, v := range self.Config {
switch k.Type {
case "int":
_, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return err
}
f.WriteString("#define " + k.Name + " " + v + "\n")
case "float":
_, err := strconv.ParseFloat(v, 64)
if err != nil {
return err
}
f.WriteString("#define " + k.Name + " " + v + "\n")
case "string":
fallthrough
case "multilinestring":
v = strings.ReplaceAll(v, "\"", "\\\"")
v = strings.ReplaceAll(v, "\n", "\\n\" \\\n\"")
v = strings.ReplaceAll(v, "\r", "")
f.WriteString("#define " + k.Name + " \"" + v + "\"\n")
default:
return errors.New("Unsupported config value type: " + k.Type)
}
}
return nil
}

View file

@ -6,7 +6,6 @@ import (
type Page struct { type Page struct {
Title string Title string
Slug string
Content string Content string
Edited time.Time Edited time.Time
} }

View file

@ -36,3 +36,32 @@ func EnsurePageData(next http.Handler, adapter core.Adapter) http.Handler {
return http.HandlerFunc(handlerFunc) return http.HandlerFunc(handlerFunc)
} }
func SanitizeFormMap(next http.Handler) http.Handler {
handlerFunc := func(w http.ResponseWriter, req *http.Request) {
delete(req.PostForm, "csrfToken")
next.ServeHTTP(w, req)
}
return http.HandlerFunc(handlerFunc)
}
func FormMapToAdapterConfig(next http.Handler, adapter core.Adapter) http.Handler {
handlerFunc := func(w http.ResponseWriter, req *http.Request) {
cfg := make(map[core.ConfigOption]string)
for k, arr := range req.PostForm {
v := strings.Join(arr, "")
optNameAndType := strings.Split(k, ":")
optName := optNameAndType[0]
optType := optNameAndType[1]
cfg[core.ConfigOption{
Name: optName,
Type: optType,
}] = v
}
*req = *req.WithContext(context.WithValue(req.Context(), "config", cfg))
next.ServeHTTP(w, req)
}
return http.HandlerFunc(handlerFunc)
}

View file

@ -27,14 +27,18 @@ func main() {
cfg.Adapter.Init(cfg) cfg.Adapter.Init(cfg)
pathConcat := filepath.Join
rtr := &router.Router{ rtr := &router.Router{
StaticPaths: map[string]string{ StaticPaths: map[string]string{
"/static": cfg.AssetRoot, "/static": filepath.Join(cfg.AssetRoot, "static"),
}, },
} }
templateRoot := pathConcat(cfg.AssetRoot, "templates")
rtr.Get("/login", renderer.Template( rtr.Get("/login", renderer.Template(
"templates/login.html")) pathConcat(templateRoot, "login.html")))
rtr.Post("/login", middleware.Authorize("/", udb, "/login?tryagain=1")) rtr.Post("/login", middleware.Authorize("/", udb, "/login?tryagain=1"))
@ -45,9 +49,9 @@ func main() {
middleware.Protected( middleware.Protected(
shell.WithAdapter( shell.WithAdapter(
renderer.Template( renderer.Template(
"templates/cms_list.html", pathConcat(templateRoot, "cms_list.html"),
"templates/header.html", pathConcat(templateRoot, "header.html"),
"templates/footer.html"), pathConcat(templateRoot, "footer.html")),
cfg.Adapter), cfg.Adapter),
http.MethodGet, http.MethodGet,
udb, udb,
@ -59,9 +63,9 @@ func main() {
middleware.Protected( middleware.Protected(
shell.WithAdapter( shell.WithAdapter(
renderer.Template( renderer.Template(
"templates/cms_edit.html", pathConcat(templateRoot, "cms_edit.html"),
"templates/header.html", pathConcat(templateRoot, "header.html"),
"templates/footer.html"), pathConcat(templateRoot, "footer.html")),
cfg.Adapter), cfg.Adapter),
http.MethodGet, http.MethodGet,
udb, udb,
@ -74,9 +78,9 @@ func main() {
shell.WithAdapter( shell.WithAdapter(
shell.EnsurePageData( shell.EnsurePageData(
renderer.Template( renderer.Template(
"templates/cms_save.html", pathConcat(templateRoot, "cms_save.html"),
"templates/header.html", pathConcat(templateRoot, "header.html"),
"templates/footer.html"), pathConcat(templateRoot, "footer.html")),
cfg.Adapter), cfg.Adapter),
cfg.Adapter), cfg.Adapter),
http.MethodGet, http.MethodGet,
@ -91,9 +95,9 @@ func main() {
middleware.Protected( middleware.Protected(
shell.WithAdapter( shell.WithAdapter(
renderer.Template( renderer.Template(
"templates/cms_new.html", pathConcat(templateRoot, "cms_new.html"),
"templates/header.html", pathConcat(templateRoot, "header.html"),
"templates/footer.html"), pathConcat(templateRoot, "footer.html")),
cfg.Adapter), cfg.Adapter),
http.MethodGet, http.MethodGet,
udb, udb,
@ -106,9 +110,9 @@ func main() {
shell.WithAdapter( shell.WithAdapter(
shell.EnsurePageData( shell.EnsurePageData(
renderer.Template( renderer.Template(
"templates/cms_create.html", pathConcat(templateRoot, "cms_create.html"),
"templates/header.html", pathConcat(templateRoot, "header.html"),
"templates/footer.html"), pathConcat(templateRoot, "footer.html")),
cfg.Adapter), cfg.Adapter),
cfg.Adapter), cfg.Adapter),
http.MethodGet, http.MethodGet,
@ -123,9 +127,9 @@ func main() {
middleware.Protected( middleware.Protected(
shell.WithAdapter( shell.WithAdapter(
renderer.Template( renderer.Template(
"templates/build.html", pathConcat(templateRoot, "build.html"),
"templates/header.html", pathConcat(templateRoot, "header.html"),
"templates/footer.html"), pathConcat(templateRoot, "footer.html")),
cfg.Adapter), cfg.Adapter),
http.MethodGet, http.MethodGet,
udb, udb,
@ -135,12 +139,13 @@ func main() {
`/build-run`, `/build-run`,
middleware.Defend( middleware.Defend(
middleware.Protected( middleware.Protected(
shell.SanitizeFormMap(
shell.WithAdapter( shell.WithAdapter(
renderer.Template( renderer.Template(
"templates/build_run.html", pathConcat(templateRoot, "build_run.html"),
"templates/header.html", pathConcat(templateRoot, "header.html"),
"templates/footer.html"), pathConcat(templateRoot, "footer.html")),
cfg.Adapter), cfg.Adapter)),
http.MethodGet, http.MethodGet,
udb, udb,
"/login"), "/login"),
@ -153,14 +158,48 @@ func main() {
middleware.Protected( middleware.Protected(
shell.WithAdapter( shell.WithAdapter(
renderer.Template( renderer.Template(
"templates/delete.html", pathConcat(templateRoot, "delete.html"),
"templates/header.html", pathConcat(templateRoot, "header.html"),
"templates/footer.html"), pathConcat(templateRoot, "footer.html")),
cfg.Adapter), cfg.Adapter),
http.MethodGet, http.MethodGet,
udb, udb,
"/login"), "/login"),
udb, udb,
"/")) "/"))
rtr.Get(
`/config`,
middleware.Fortify(
middleware.Protected(
shell.WithAdapter(
renderer.Template(
pathConcat(templateRoot, "config.html"),
pathConcat(templateRoot, "header.html"),
pathConcat(templateRoot, "footer.html")),
cfg.Adapter),
http.MethodGet,
udb,
"/login")))
rtr.Post(
`/config-set`,
middleware.Defend(
middleware.Protected(
shell.SanitizeFormMap(
shell.FormMapToAdapterConfig(
shell.WithAdapter(
renderer.Template(
pathConcat(templateRoot, "config_set.html"),
pathConcat(templateRoot, "header.html"),
pathConcat(templateRoot, "footer.html")),
cfg.Adapter),
cfg.Adapter)),
http.MethodGet,
udb,
"/login"),
udb,
"/"))
http.ListenAndServe(":8080", rtr) http.ListenAndServe(":8080", rtr)
} }

View file

@ -1,7 +1,7 @@
body { body {
padding: 0; padding: 0;
margin: 0; margin: 0;
font-family: sans; font-family: sans-serif;
font-size: 16px; font-size: 16px;
height: 100vh; height: 100vh;
background: black; background: black;
@ -37,7 +37,18 @@ body {
color: lightgray; color: lightgray;
} }
.login form input { .login label {
padding: 0;
margin: 0;
height: 0;
width: 0;
font-size: 0;
display: inline;
position: absolute;
}
.login form input, .login form input:-internal-autofill-selected {
display: block; display: block;
margin: 1em; margin: 1em;
margin-left: auto; margin-left: auto;
@ -52,6 +63,7 @@ body {
.login form input[type="text"], login form input[type="password"] { .login form input[type="text"], login form input[type="password"] {
transition: border 1s; transition: border 1s;
outline: none; outline: none;
margin-bottom: -17px;
} }
.login form input:focus { .login form input:focus {
@ -61,6 +73,7 @@ body {
.login form input[type="submit"] { .login form input[type="submit"] {
margin-top: -17px;
text-transform: uppercase; text-transform: uppercase;
transition: background 1s, color 1s; transition: background 1s, color 1s;
} }
@ -155,7 +168,9 @@ h2 {
top: 3em; top: 3em;
} }
.page-list, form.editor, form.build, span.adapter-error, span.adapter-success, .danger-zone { .page-list, form.editor, form.build, form.configurator, span.adapter-error, span.adapter-success, .danger-zone {
display: block;
overflow-x: hidden;
width: 80%; width: 80%;
max-width: 500px; max-width: 500px;
background: rgba(0,0,0,0.8); background: rgba(0,0,0,0.8);
@ -166,13 +181,17 @@ h2 {
overflow-y: auto; overflow-y: auto;
} }
form.editor label, form.build label { span.adapter-error {
border-bottom: 2px solid crimson;
}
form.editor label, form.build label, .danger-zone label, form.configurator label {
font-size: 80%; font-size: 80%;
color: lightgray; color: lightgray;
text-transform: uppercase; text-transform: uppercase;
} }
form.editor input, form.build input, form.editor textarea { form.editor input, form.build input, form.editor textarea, form.configurator input, form.configurator textarea, .danger-zone input[type="submit"] {
display: block; display: block;
margin: 0; margin: 0;
margin-top: 0.2em; margin-top: 0.2em;
@ -185,24 +204,42 @@ form.editor input, form.build input, form.editor textarea {
outline: none; outline: none;
} }
form.editor input.title-input { form.editor input[type="text"], form.configurator input[type="text"], form.configurator input[type="number"] {
margin: 0; margin: 0;
width: 100%; width: 100%;
}
form.editor input.title-input {
font-size: 150%; font-size: 150%;
} }
form input:focus, form textarea:focus {
border: 2px solid cyan;
}
form.editor, .danger-zone { form.editor, .danger-zone {
max-width: 80em; max-width: 80em;
} }
form.editor textarea { form.editor textarea {
margin: 0; margin: 0;
width: 80em; width: 100%;
font-size: 16px; font-size: 16px;
height: 25em; height: 25em;
} }
form.editor input[type="submit"], form.build input[type="submit"] { form.configurator textarea {
marign: 0;
width: 100%;
height: 5em;
}
form.configurator input, form.configurator textarea {
font-size: 125%;
}
form.editor input[type="submit"], form.build input[type="submit"], .danger-zone input[type="submit"], form.configurator input[type="submit"] {
margin-left: auto; margin-left: auto;
margin-right: 0; margin-right: 0;
font-size: 150%; font-size: 150%;
@ -210,7 +247,7 @@ form.editor input[type="submit"], form.build input[type="submit"] {
transition: background 1s, color 1s; transition: background 1s, color 1s;
} }
form.editor input[type="submit"]:hover { form.editor input[type="submit"]:hover,form.build input[type="submit"]:hover, .danger-zone input[type="submit"]:hover, form.configurator input[type="submit"]:hover {
background: lightgray; background: lightgray;
color: black; color: black;
} }

25
templates/config.html Normal file
View file

@ -0,0 +1,25 @@
{{ $config := ((.Context).Value "adapter").GetConfig }}
{{ $csrfToken := (.Context).Value "csrfToken" }}
{{ template "header" . }}
<h2>Configuration</h2>
<form class="configurator" method="POST" action="/config-set">
<input hidden type="text" name="csrfToken" value="{{$csrfToken}}"/>
{{ range $opt, $val := $config }}
{{ if eq ($opt).Type "int" }}
<label>{{($opt).Name}} <input type="number" step="1" name="{{($opt).Name}}:{{($opt).Type}}" value="{{$val}}"/></label><br/>
{{ else if eq ($opt).Type "float" }}
<label>{{($opt).Name}} <input type="number" step="0.00000001" name="{{($opt).Name}}:{{($opt).Type}}" value="{{$val}}"/></label><br/>
{{ else if eq ($opt).Type "string" }}
<label>{{($opt).Name}} <input type="text" name="{{($opt).Name}}:{{($opt).Type}}" value="{{$val}}"/></label><br/>
{{ else if eq ($opt).Type "multilinestring" }}
<label>{{($opt).Name}} <textarea name="{{($opt).Name}}:{{($opt).Type}}">{{$val}}</textarea></label><br/>
{{ end}}
{{ end }}
<input type="submit" value="Save"/>
</form>
{{ template "footer" . }}

14
templates/config_set.html Normal file
View file

@ -0,0 +1,14 @@
{{ $config := (.Context).Value "config" }}
{{ $cfgError := ((.Context).Value "adapter").SetConfig $config }}
{{ template "header" . }}
{{ if $cfgError }}
<h2>Configuration Error</h2>
<span class="adapter-error">{{($cfgError).Error}}</span>
{{ else }}
<h2>Configuration Saved</h2>
<span class="adapter-success">The adapter configuration has been saved</span>
{{ end }}
{{ template "footer" . }}

View file

@ -17,8 +17,10 @@
<span class="error">Incorrect credentials; please try again.</span> <span class="error">Incorrect credentials; please try again.</span>
{{ end }} {{ end }}
<form action='/login' method='post'> <form action='/login' method='post'>
<input type="text" name="user" placeholder="user"> <label for="user-input">Username</label>
<input type="password" name="password" placeholder="password"> <input type="text" id="user-input" name="user" placeholder="user"><br/>
<label for="password-input">Password</label>
<input type="password" id="password-input" name="password" placeholder="password"><br/>
<input type="submit" value="Login"> <input type="submit" value="Login">
</form> </form>
</div> </div>