diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..510bb9a --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/ChaoticByte/plaintext-encyclopedia + +go 1.22.4 diff --git a/main.go b/main.go new file mode 100644 index 0000000..93b8ff2 --- /dev/null +++ b/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "html/template" + "io/fs" + "log" + "net/http" + "os" + "strings" +) + +var logger *log.Logger +var appTemplate *template.Template = template.New("app") + +type TemplateData struct { + TOC []string + Entry string + Title string + EntryTitle string +} + +func loadTemplate() { + data, err := os.ReadFile(TemplateFile) + if err != nil { logger.Panic(err) } + appTemplate.Parse(string(data)) +} + +func getTOC() []string { + entriesDirFs := os.DirFS(EntriesDirectory) + m, err := fs.Glob(entriesDirFs, "*") + if err != nil { logger.Panic(err) } + return m +} + +func getEntry(name string) (string, error) { + data, err := os.ReadFile(strings.TrimRight(EntriesDirectory, "/") + "/" + name) + if err != nil { return "", err } + return string(data), err +} + +func handleApplication(w http.ResponseWriter, req *http.Request) { + var entry string + var err error + entryName := strings.Trim(req.URL.Path, "/") + if entryName != "" { + if strings.Contains(entryName, "/") || strings.Contains(entryName, ".") { + // path traversal + logger.Println("Possible path traversal attempt from", req.RemoteAddr, "to", entryName) + w.WriteHeader(http.StatusForbidden) + return + } + // load entry + entry, err = getEntry(entryName) + if err != nil { + logger.Println("Couldn't open entry", entryName) + w.WriteHeader(http.StatusNotFound) + } + } + err = appTemplate.ExecuteTemplate(w, "app", TemplateData{TOC: getTOC(), Entry: entry, Title: MainTitle, EntryTitle: entryName}) + if err != nil { logger.Println(err) } +} + +func main() { + // get logger + logger = log.Default() + // load template + loadTemplate() + // handle static files + staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir(StaticDirectory))) + http.Handle("/static/", staticHandler) + // handle application + http.HandleFunc("/", handleApplication) + // start server + logger.Println("Starting server on", ServerListen) + err := http.ListenAndServe(ServerListen, nil) + if err != nil { logger.Panic(err) } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..b5ff3aa --- /dev/null +++ b/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + {{ if .EntryTitle }}{{ .EntryTitle }} - {{ end }}{{ .Title }} + + + +
+

{{ .EntryTitle }}

+
+ {{ .Entry }} +
+
+ + diff --git a/public/static/style.css b/public/static/style.css new file mode 100644 index 0000000..ef5c4de --- /dev/null +++ b/public/static/style.css @@ -0,0 +1,84 @@ +body { + margin: 0; + padding: 0; + display: flex; + flex-direction: row; + justify-content: left; + align-items: top; + width: 100vw; + font-family: sans-serif; +} +.sidebar-symbol { + margin-left: .5rem; + margin-top: .5rem; + width: 2rem; + height: 2rem; + font-size: 1.8rem; + font-family: monospace; + text-align: center; + user-select: none; + cursor: pointer; + pointer-events: all; +} +.sidebar-switch { pointer-events: none; } +.sidebar-switch > input { display: none; } +.sidebar-switch > .sidebar { display: none; } +.sidebar-switch > input:checked + .sidebar { display: flex; } +.sidebar { + flex-direction: column; + pointer-events: all; + width: max-content; + padding: 1rem; + gap: .25rem; +} +a { + color: black; + text-decoration: none; +} +a:hover { text-decoration: underline; } +.main { + padding-right: 2rem; + margin: 2rem 0; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +} +.main > h1, .content { + width: 70vw; + max-width: 50rem; +} +.main > h1 { + font-size: 1.4rem; + margin-bottom: 0; +} +.content { white-space: pre-line; } + +@media only screen and (max-width: 750px) { + body { + flex-direction: column; + align-items: center; + } + .sidebar-symbol { + margin-left: unset; + text-align: left; + } + .sidebar-switch { + width: 90vw; + background-color: white; + } + .sidebar { + width: 100%; + height: max-content; + padding: 1rem 0; + gap: .4rem; + } + .main { + margin-top: .5rem; + padding-right: 0; + } + .main > h1, .content { + width: 90vw; + max-width: unset; + } +} diff --git a/settings.go b/settings.go new file mode 100644 index 0000000..85979cd --- /dev/null +++ b/settings.go @@ -0,0 +1,7 @@ +package main + +var ServerListen = ":7000" +var EntriesDirectory = "./entries" +var TemplateFile = "./public/index.html" +var StaticDirectory = "./public/static" +var MainTitle = "Encyclopedia"