diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000..85826cd --- /dev/null +++ b/Brewfile @@ -0,0 +1,2 @@ +# build deps +brew "go@1.25" diff --git a/COPYING b/LICENSE similarity index 100% rename from COPYING rename to LICENSE diff --git a/examples/07_extentions/Fes.toml b/examples/07_extentions/Fes.toml deleted file mode 100644 index 7f3be7f..0000000 --- a/examples/07_extentions/Fes.toml +++ /dev/null @@ -1,5 +0,0 @@ -[app] - -name = "extentions" -version = "0.0.1" -authors = ["vx-clutch"] \ No newline at end of file diff --git a/examples/07_extentions/README.md b/examples/07_extentions/README.md deleted file mode 100644 index 47477b6..0000000 --- a/examples/07_extentions/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# extentions - -``` -fes new extentions -``` - -> **Know what you are doing?** Delete this file. Have fun! - -## Project Structure - -Inside your Fes project, you'll see the following directories and files: - -``` -. -├── Fes.toml -├── README.md -└── www - └── index.lua -``` - -Fes looks for `.lua` files in the `www/` directory. Each file is exposed as a route based on its file name. - -## Commands - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :------------------------ | :----------------------------------------------- | -| `fes run .` | Runs the project at `.` | - -## What to learn more? - -Check out [Fes's docs](https://docs.vxserver.dev/static/fes.html). \ No newline at end of file diff --git a/examples/07_extentions/www/index.lua b/examples/07_extentions/www/index.lua deleted file mode 100644 index a24c17b..0000000 --- a/examples/07_extentions/www/index.lua +++ /dev/null @@ -1,12 +0,0 @@ -local fes = require("fes") -local site = fes.fes() - -site.copyright = fes.util.copyright("https://fsd.vxserver.dev/", "fSD") - -site:extend("myext", { - shout = function(self, str) return self:g(str:upper()) end -}) - -site.myext:shout("hello world") - -return site diff --git a/lib/site.lua b/lib/site.lua deleted file mode 100644 index 0be7eeb..0000000 --- a/lib/site.lua +++ /dev/null @@ -1,27 +0,0 @@ -local M = {} - -function M.name() - local fes_mod = package.loaded.fes - if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.name then - return fes_mod.config.site.name - end - return "" -end - -function M.version() - local fes_mod = package.loaded.fes - if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.version then - return fes_mod.config.site.version - end - return "" -end - -function M.authors() - local fes_mod = package.loaded.fes - if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.authors then - return fes_mod.config.site.authors - end - return {} -end - -return M diff --git a/main.go b/main.go index 30941bf..ce47324 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "embed" - "errors" "flag" "fmt" "os" @@ -103,16 +102,7 @@ func main() { runtime.ReadMemStats(&m) ui.Log("FRE memory usage when created %v Mb", m.TotalAlloc/1024/1024) - if err := server.Start(dir); err != nil { - if errors.Is(err, os.ErrNotExist) { - fmt.Fprintf(os.Stderr, "%s does not exist\n", dir) - fmt.Fprintf(os.Stderr, "Try: fes new %s\n", dir) - os.Exit(1) - } else { - fmt.Fprintln(os.Stderr, "Error:", err) - os.Exit(1) - } - } + server.Start(dir) default: fmt.Fprintf(os.Stderr, "Unknown command: %s\n", cmd) flag.Usage() diff --git a/modules/server/archive.go b/modules/server/archive.go new file mode 100644 index 0000000..4d48fc0 --- /dev/null +++ b/modules/server/archive.go @@ -0,0 +1,109 @@ +package server + +import ( + "fmt" + "net/http" + "os" + "path" + "path/filepath" + "sort" + "strings" + "text/template" + "time" +) + +/* this indexes and generate the page for viewing the archive directory */ +func generateArchiveIndex(fsPath string, urlPath string) (string, error) { + info, err := os.Stat(fsPath) + if err != nil { + return "", err + } + if !info.IsDir() { + return "", fmt.Errorf("not a directory") + } + ents, err := os.ReadDir(fsPath) + if err != nil { + return "", err + } + type entryInfo struct { + name string + isDir bool + href string + size int64 + mod time.Time + } + var list []entryInfo + for _, e := range ents { + n := e.Name() + full := filepath.Join(fsPath, n) + st, err := os.Stat(full) + if err != nil { + continue + } + isd := st.IsDir() + displayName := n + if isd { + displayName = n + "/" + } + href := path.Join(urlPath, n) + if isd && !strings.HasSuffix(href, "/") { + href = href + "/" + } + size := int64(-1) + if !isd { + size = st.Size() + } + list = append(list, entryInfo{name: displayName, isDir: isd, href: href, size: size, mod: st.ModTime()}) + } + sort.Slice(list, func(i, j int) bool { + if list[i].isDir != list[j].isDir { + return list[i].isDir + } + return strings.ToLower(list[i].name) < strings.ToLower(list[j].name) + }) + + urlPath = strings.TrimPrefix(urlPath, "/archive") + + var b strings.Builder + + b.WriteString("\nIndex of ") + b.WriteString(template.HTMLEscapeString(urlPath)) + b.WriteString("\n\n

Index of ") + b.WriteString(template.HTMLEscapeString(urlPath)) + b.WriteString("


")
+	b.WriteString("../\n")
+
+	nameCol := 50
+	for _, ei := range list {
+		escapedName := template.HTMLEscapeString(ei.name)
+		dateStr := ei.mod.Local().Format("02-Jan-2006 15:04")
+		var sizeStr string
+		if ei.isDir {
+			sizeStr = "-"
+		} else {
+			sizeStr = fmt.Sprintf("%d", ei.size)
+		}
+		spaces := 1
+		if len(escapedName) < nameCol {
+			spaces = nameCol - len(escapedName)
+		}
+		line := `` + escapedName + `` + strings.Repeat(" ", spaces) + dateStr + strings.Repeat(" ", 19-len(sizeStr)) + sizeStr + "\n"
+		b.WriteString(line)
+	}
+	b.WriteString("

\n") + return b.String(), nil +} + +/* helper to read the archive files */ +func readArchive(w http.ResponseWriter, route string) error { + fsPath := "." + route + if info, err := os.Stat(fsPath); err == nil && info.IsDir() { + if page, err := generateArchiveIndex(fsPath, route); err == nil { + w.Write([]byte(page)) + return nil + } else { + return err + } + } + return nil +} diff --git a/modules/server/dirs.go b/modules/server/dirs.go new file mode 100644 index 0000000..aefa13e --- /dev/null +++ b/modules/server/dirs.go @@ -0,0 +1,80 @@ +package server + +import ( + "fes/modules/ui" + "fmt" + "os" + "path/filepath" + "strings" +) + +/* performs relavent handling based on the directory passaed + * + * Special directories + * - www/ <= contains lua routes. + * - static/ <= static content accessable at /static/path or /static/dir/path. + * - include/ <= globally accessable lua functions, cannot directly access "fes" right now. + * - archive/ <= contains user facing files such as archives or dists. + * + */ +func handleDir(entries []os.DirEntry, dir string, routes map[string]string, base string, isStatic bool) error { + for _, entry := range entries { + path := filepath.Join(dir, entry.Name()) + if entry.IsDir() { + nextBase := joinBase(base, entry.Name()) + subEntries, err := os.ReadDir(path) + if err != nil { + return fmt.Errorf("failed to read directory %s: %w", path, err) + } + if err := handleDir(subEntries, path, routes, nextBase, isStatic); err != nil { + return err + } + continue + } + route := joinBase(base, entry.Name()) + if !isStatic && strings.HasSuffix(entry.Name(), ".lua") { + name := strings.TrimSuffix(entry.Name(), ".lua") + if name == "index" { + routes[basePath(base)] = path + routes[route] = path + continue + } + route = joinBase(base, name) + } else if !isStatic && strings.HasSuffix(entry.Name(), ".md") { + name := strings.TrimSuffix(entry.Name(), ".md") + if name == "index" { + routes[basePath(base)] = path + routes[route] = path + continue + } + route = joinBase(base, name) + } + routes[route] = path + } + return nil +} + +/* helper to load all special directories */ +func loadDirs() map[string]string { + routes := make(map[string]string) + + if entries, err := os.ReadDir("www"); err == nil { + if err := handleDir(entries, "www", routes, "", false); err != nil { + ui.Warning("failed to handle www directory", err) + } + } + + if entries, err := os.ReadDir("static"); err == nil { + if err := handleDir(entries, "static", routes, "/static", true); err != nil { + ui.Warning("failed to handle static directory", err) + } + } + + if entries, err := os.ReadDir("archive"); err == nil { + if err := handleDir(entries, "archive", routes, "/archive", true); err != nil { + ui.Warning("failed to handle archive directory", err) + } + } + + return routes +} diff --git a/modules/server/render.go b/modules/server/render.go new file mode 100644 index 0000000..c99162f --- /dev/null +++ b/modules/server/render.go @@ -0,0 +1,164 @@ +package server + +import ( + "fes/modules/config" + "io/fs" + "os" + "path/filepath" + "strings" + + lua "github.com/yuin/gopher-lua" +) + +/* this is the request data we pass over the bus to the application, via the fes.bus interface */ +type reqData struct { + path string + params map[string]string +} + +/* returns a string of rendered html */ +func render(luapath string, requestData reqData) ([]byte, error) { + L := lua.NewState() + defer L.Close() + + if lib, err := fs.ReadDir(config.Lib, "lib"); err == nil { + for _, de := range lib { + if de.IsDir() || !strings.HasSuffix(de.Name(), ".lua") { + continue + } + path := filepath.Join("lib", de.Name()) + if data, err := config.Lib.ReadFile(path); err != nil { + continue + } else { + L.DoString(string(data)) + } + } + } + + preloadLuaModule := func(name, path string) { + L.PreloadModule(name, func(L *lua.LState) int { + fileData, err := config.Lib.ReadFile(path) + if err != nil { + panic(err) + } + if err := L.DoString(string(fileData)); err != nil { + panic(err) + } + L.Push(L.Get(-1)) + return 1 + }) + } + + preloadLuaModule("lib.std", "lib/std.lua") + preloadLuaModule("lib.symbol", "lib/symbol.lua") + preloadLuaModule("lib.util", "lib/util.lua") + + L.PreloadModule("fes", func(L *lua.LState) int { + mod := L.NewTable() + libModules := []string{} + if ents, err := fs.ReadDir(config.Lib, "lib"); err == nil { + for _, e := range ents { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".lua") { + continue + } + libModules = append(libModules, strings.TrimSuffix(e.Name(), ".lua")) + } + } + for _, modName := range libModules { + path := filepath.Join("lib", modName+".lua") + fileData, err := config.Lib.ReadFile(path) + if err != nil { + continue + } + if err := L.DoString(string(fileData)); err != nil { + continue + } + val := L.Get(-1) + L.Pop(1) + tbl, ok := val.(*lua.LTable) + if !ok || tbl == nil { + tbl = L.NewTable() + } + if modName == "fes" { + tbl.ForEach(func(k, v lua.LValue) { mod.RawSet(k, v) }) + } else { + mod.RawSetString(modName, tbl) + } + } + + mod.RawSetString("app", func() *lua.LTable { + app := L.NewTable() + includeDir := "include" + + includes, err := os.ReadDir(includeDir) + if err != nil { + return app // load no includes + } + + for _, de := range includes { + if de.IsDir() || !strings.HasSuffix(de.Name(), ".lua") { + continue + } + + base := strings.TrimSuffix(de.Name(), ".lua") + path := filepath.Join(includeDir, de.Name()) + + if _, err := os.Stat(path); err != nil { + continue + } else if err := L.DoFile(path); err != nil { + continue + } + + val := L.Get(-1) + L.Pop(1) + tbl, ok := val.(*lua.LTable) + if !ok || tbl == nil { + tbl = L.NewTable() + } + app.RawSetString(base, tbl) + } + + return app + }()) + + bus := L.NewTable() + bus.RawSetString("url", lua.LString(requestData.path)) + params := L.NewTable() + for k, v := range requestData.params { + params.RawSetString(k, lua.LString(v)) + } + bus.RawSetString("params", params) + mod.RawSetString("bus", bus) + + mod.RawSetString("markdown_to_html", L.NewFunction(func(L *lua.LState) int { + L.Push(lua.LString(markdownToHTML(L.ToString(1)))) + return 1 + })) + + L.Push(mod) + return 1 + }) + + if err := L.DoFile(luapath); err != nil { + return []byte(""), err + } + + if L.GetTop() == 0 { + return []byte(""), nil + } + + L.SetGlobal("__fes_result", L.Get(-1)) + if err := L.DoString("return tostring(__fes_result)"); err != nil { + L.GetGlobal("__fes_result") + if s := L.ToString(-1); s != "" { + return []byte(s), nil + } + return []byte(""), nil + } + + if s := L.ToString(-1); s != "" { + return []byte(s), nil + } + + return []byte(""), nil +} diff --git a/modules/server/server.go b/modules/server/server.go index b253de5..31fc9a8 100644 --- a/modules/server/server.go +++ b/modules/server/server.go @@ -1,435 +1,25 @@ package server import ( - "errors" "fes/modules/config" "fes/modules/ui" "fmt" - "html/template" - "io/fs" + "log" "net/http" "os" - "path" "path/filepath" - "sort" "strings" - "time" - - "github.com/pelletier/go-toml/v2" - lua "github.com/yuin/gopher-lua" ) -/* this is the request data we pass over the bus to the application, via the fes.bus interface */ -type reqData struct { - path string - params map[string]string -} +var routes map[string]string -/* performs relavent handling based on the directory passaed - * - * Special directories - * - www/ <= contains lua routes. - * - static/ <= static content accessable at /static/path or /static/dir/path. - * - include/ <= globally accessable lua functions, cannot directly access "fes" right now. - * - archive/ <= contains user facing files such as archives or dists. - * - */ -func handleDir(entries []os.DirEntry, dir string, routes map[string]string, base string, isStatic bool) error { - for _, entry := range entries { - path := filepath.Join(dir, entry.Name()) - if entry.IsDir() { - nextBase := joinBase(base, entry.Name()) - subEntries, err := os.ReadDir(path) - if err != nil { - return fmt.Errorf("failed to read directory %s: %w", path, err) - } - if err := handleDir(subEntries, path, routes, nextBase, isStatic); err != nil { - return err - } - continue - } - route := joinBase(base, entry.Name()) - if !isStatic && strings.HasSuffix(entry.Name(), ".lua") { - name := strings.TrimSuffix(entry.Name(), ".lua") - if name == "index" { - routes[basePath(base)] = path - routes[route] = path - continue - } - route = joinBase(base, name) - } else if !isStatic && strings.HasSuffix(entry.Name(), ".md") { - name := strings.TrimSuffix(entry.Name(), ".md") - if name == "index" { - routes[basePath(base)] = path - routes[route] = path - continue - } - route = joinBase(base, name) - } - routes[route] = path - } - return nil -} - -// TODO(vx-clutch): this should not be a function -func loadIncludeModules(L *lua.LState, includeDir string) *lua.LTable { - app := L.NewTable() - ents, err := os.ReadDir(includeDir) - if err != nil { - return app - } - for _, e := range ents { - if e.IsDir() || !strings.HasSuffix(e.Name(), ".lua") { - continue - } - base := strings.TrimSuffix(e.Name(), ".lua") - path := filepath.Join(includeDir, e.Name()) - if _, err := os.Stat(path); err != nil { - tbl := L.NewTable() - tbl.RawSetString("error", lua.LString(fmt.Sprintf("file not found: %s", path))) - app.RawSetString(base, tbl) - continue - } - if err := L.DoFile(path); err != nil { - tbl := L.NewTable() - tbl.RawSetString("error", lua.LString(err.Error())) - app.RawSetString(base, tbl) - continue - } - val := L.Get(-1) - L.Pop(1) - tbl, ok := val.(*lua.LTable) - if !ok || tbl == nil { - tbl = L.NewTable() - } - app.RawSetString(base, tbl) - } - return app -} - -/* renders the given lua route */ -func renderRoute(entry string, cfg *config.AppConfig, requestData reqData) ([]byte, error) { - L := lua.NewState() - defer L.Close() - - libFiles, err := fs.ReadDir(config.Lib, "lib") - if err == nil { - for _, de := range libFiles { - if de.IsDir() || !strings.HasSuffix(de.Name(), ".lua") { - continue - } - path := filepath.Join("lib", de.Name()) - fileData, err := config.Lib.ReadFile(path) - if err != nil { - continue - } - L.DoString(string(fileData)) - } - } - - preloadLuaModule := func(name, path string) { - L.PreloadModule(name, func(L *lua.LState) int { - fileData, err := config.Lib.ReadFile(path) - if err != nil { - panic(err) - } - if err := L.DoString(string(fileData)); err != nil { - panic(err) - } - L.Push(L.Get(-1)) - return 1 - }) - } - - preloadLuaModule("lib.std", "lib/std.lua") - preloadLuaModule("lib.symbol", "lib/symbol.lua") - preloadLuaModule("lib.util", "lib/util.lua") - - L.PreloadModule("fes", func(L *lua.LState) int { - mod := L.NewTable() - libModules := []string{} - if ents, err := fs.ReadDir(config.Lib, "lib"); err == nil { - for _, e := range ents { - if e.IsDir() || !strings.HasSuffix(e.Name(), ".lua") { - continue - } - libModules = append(libModules, strings.TrimSuffix(e.Name(), ".lua")) - } - } - for _, modName := range libModules { - path := filepath.Join("lib", modName+".lua") - fileData, err := config.Lib.ReadFile(path) - if err != nil { - continue - } - if err := L.DoString(string(fileData)); err != nil { - continue - } - val := L.Get(-1) - L.Pop(1) - tbl, ok := val.(*lua.LTable) - if !ok || tbl == nil { - tbl = L.NewTable() - } - if modName == "fes" { - tbl.ForEach(func(k, v lua.LValue) { mod.RawSet(k, v) }) - } else { - mod.RawSetString(modName, tbl) - } - } - - mod.RawSetString("app", loadIncludeModules(L, filepath.Join(".", "include"))) - - if cfg != nil { - site := L.NewTable() - site.RawSetString("version", lua.LString(cfg.App.Version)) - site.RawSetString("name", lua.LString(cfg.App.Name)) - authors := L.NewTable() - for i, a := range cfg.App.Authors { - authors.RawSetInt(i+1, lua.LString(a)) - } - site.RawSetString("authors", authors) - mod.RawSetString("site", site) - } - - bus := L.NewTable() - bus.RawSetString("url", lua.LString(requestData.path)) - params := L.NewTable() - for k, v := range requestData.params { - params.RawSetString(k, lua.LString(v)) - } - bus.RawSetString("params", params) - mod.RawSetString("bus", bus) - - mod.RawSetString("markdown_to_html", L.NewFunction(func(L *lua.LState) int { - L.Push(lua.LString(markdownToHTML(L.ToString(1)))) - return 1 - })) - - L.Push(mod) - return 1 - }) - - if err := L.DoFile(entry); err != nil { - return []byte(""), err - } - - if L.GetTop() == 0 { - return []byte(""), nil - } - - L.SetGlobal("__fes_result", L.Get(-1)) - if err := L.DoString("return tostring(__fes_result)"); err != nil { - L.GetGlobal("__fes_result") - if s := L.ToString(-1); s != "" { - return []byte(s), nil - } - return []byte(""), nil - } - - if s := L.ToString(-1); s != "" { - return []byte(s), nil - } - return []byte(""), nil -} - -/* this indexes and generate the page for viewing the archive directory */ -func generateArchiveIndex(fsPath string, urlPath string) (string, error) { - info, err := os.Stat(fsPath) - if err != nil { - return "", err - } - if !info.IsDir() { - return "", fmt.Errorf("not a directory") - } - ents, err := os.ReadDir(fsPath) - if err != nil { - return "", err - } - type entryInfo struct { - name string - isDir bool - href string - size int64 - mod time.Time - } - var list []entryInfo - for _, e := range ents { - n := e.Name() - full := filepath.Join(fsPath, n) - st, err := os.Stat(full) - if err != nil { - continue - } - isd := st.IsDir() - displayName := n - if isd { - displayName = n + "/" - } - href := path.Join(urlPath, n) - if isd && !strings.HasSuffix(href, "/") { - href = href + "/" - } - size := int64(-1) - if !isd { - size = st.Size() - } - list = append(list, entryInfo{name: displayName, isDir: isd, href: href, size: size, mod: st.ModTime()}) - } - sort.Slice(list, func(i, j int) bool { - if list[i].isDir != list[j].isDir { - return list[i].isDir - } - return strings.ToLower(list[i].name) < strings.ToLower(list[j].name) - }) - - urlPath = basePath(strings.TrimPrefix(urlPath, "/archive")) - - var b strings.Builder - - b.WriteString("\nIndex of ") - b.WriteString(template.HTMLEscapeString(urlPath)) - b.WriteString("\n\n

Index of ") - b.WriteString(template.HTMLEscapeString(urlPath)) - b.WriteString("


")
-
-	if urlPath != "/" {
-		b.WriteString(
-			`../` + "\n",
-		)
-	} else {
-		b.WriteString(
-			`../` + "\n",
-		)
-	}
-
-	nameCol := 50
-	for _, ei := range list {
-		escapedName := template.HTMLEscapeString(ei.name)
-		dateStr := ei.mod.Local().Format("02-Jan-2006 15:04")
-		var sizeStr string
-		if ei.isDir {
-			sizeStr = "-"
-		} else {
-			sizeStr = fmt.Sprintf("%d", ei.size)
-		}
-		spaces := 1
-		if len(escapedName) < nameCol {
-			spaces = nameCol - len(escapedName)
-		}
-		line := `` + escapedName + `` + strings.Repeat(" ", spaces) + dateStr + strings.Repeat(" ", 19-len(sizeStr)) + sizeStr + "\n"
-		b.WriteString(line)
-	}
-	b.WriteString("

\n") - return b.String(), nil -} - -/* generates the data for the not found page. Checks for user-defined source in this order - * 404.lua => 404.md => 404.html => default. - */ -func generateNotFoundData(cfg *config.AppConfig) []byte { - notFoundData := []byte(` - -404 Not Found - -

404 Not Found

-
fes
- - -`) - if _, err := os.Stat(filepath.Join("www", "404.lua")); err == nil { - if nf, err := renderRoute("www/404.lua", cfg, reqData{}); err == nil { - notFoundData = nf - } - } else if _, err := os.Stat("www/404.md"); err == nil { - if buf, err := os.ReadFile("www/404.html"); err == nil { - notFoundData = []byte(markdownToHTML(string(buf))) - } - } else if _, err := os.Stat("www/404.html"); err == nil { - if buf, err := os.ReadFile("www/404.html"); err == nil { - notFoundData = buf - } - } - return notFoundData -} - -/* helper to load all special directories */ -func loadDirs() map[string]string { - routes := make(map[string]string) - - if entries, err := os.ReadDir("www"); err == nil { - if err := handleDir(entries, "www", routes, "", false); err != nil { - ui.Warning("failed to handle www directory", err) - } - } - - if entries, err := os.ReadDir("static"); err == nil { - if err := handleDir(entries, "static", routes, "/static", true); err != nil { - ui.Warning("failed to handle static directory", err) - } - } - - if entries, err := os.ReadDir("archive"); err == nil { - if err := handleDir(entries, "archive", routes, "/archive", true); err != nil { - ui.Warning("failed to handle archive directory", err) - } - } - - return routes -} - -/* helper to parse the Fes.toml and generate config */ -func parseConfig() config.AppConfig { - defaultCfg := config.AppConfig{} - defaultCfg.App.Authors = []string{"unknown"} - defaultCfg.App.Name = "unknown" - defaultCfg.App.Version = "unknown" - - tomlDocument, err := os.ReadFile("Fes.toml") - if err != nil { - if errors.Is(err, os.ErrNotExist) { - ui.WARN("no config file found, using the default config. In order to specify a config file write to Fes.toml") - return defaultCfg - } else { - ui.Error("failed to read Fes.toml", err) - os.Exit(1) - } - } - docStr := fixMalformedToml(string(tomlDocument)) - var cfg config.AppConfig - if err := toml.Unmarshal([]byte(docStr), &cfg); err != nil { - ui.Warning("failed to parse Fes.toml", err) - cfg = defaultCfg - } - return cfg -} - -/* helper to read the archive files */ -func readArchive(w http.ResponseWriter, route string) error { - fsPath := "." + route - if info, err := os.Stat(fsPath); err == nil && info.IsDir() { - if page, err := generateArchiveIndex(fsPath, route); err == nil { - w.Write([]byte(page)) - return nil - } else { - return err - } - } - return nil -} - -/* start the Fes server */ -func Start(dir string) error { +func Start(dir string) { if err := os.Chdir(dir); err != nil { - return ui.Error(fmt.Sprintf("failed to change directory to %s", dir), err) + ui.Error(fmt.Sprintf("failed to change directory to %s", dir), err) } - ui.Log("Running root=%s, port=%d.", filepath.Clean(dir), *config.Port) + ui.Log("running root=%s, port=%d.", filepath.Clean(dir), *config.Port) - cfg := parseConfig() - notFoundData := generateNotFoundData(&cfg) routes := loadDirs() http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { @@ -450,7 +40,7 @@ func Start(dir string) error { err = readArchive(w, route) } else { w.WriteHeader(http.StatusNotFound) - w.Write([]byte(notFoundData)) + w.Write([]byte("not found :(")) } return } @@ -464,7 +54,7 @@ func Start(dir string) error { var data []byte if strings.HasSuffix(route, ".lua") { - data, err = renderRoute(route, &cfg, reqData{path: r.URL.Path, params: params}) + data, err = render(route, reqData{path: r.URL.Path, params: params}) } else if strings.HasSuffix(route, ".md") { data, err = os.ReadFile(route) data = []byte(markdownToHTML(string(data))) @@ -481,6 +71,5 @@ func Start(dir string) error { }) ui.Log("Server initialized") - ui.Log("Ready to accept connections tcp") - return http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", *config.Port), nil) + log.Fatal(http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", *config.Port), nil)) } diff --git a/modules/server/util.go b/modules/server/util.go index cb4a30a..57bb01e 100644 --- a/modules/server/util.go +++ b/modules/server/util.go @@ -4,36 +4,8 @@ import ( "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/parser" - "regexp" - "strings" ) -func joinBase(base, name string) string { - if base == "" { - return "/" + name - } - return base + "/" + name -} - -func basePath(base string) string { - if base == "" || base == "." { - return "/" - } - return base -} - -func fixMalformedToml(content string) string { - re := regexp.MustCompile(`(?m)^(\s*\w+\s*=\s*)$`) - return re.ReplaceAllStringFunc(content, func(match string) string { - parts := strings.Split(strings.TrimSpace(match), "=") - if len(parts) == 2 && strings.TrimSpace(parts[1]) == "" { - key := strings.TrimSpace(parts[0]) - return key + " = \"\"" - } - return match - }) -} - func markdownToHTML(mdText string) string { extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock p := parser.NewWithExtensions(extensions) @@ -43,3 +15,17 @@ func markdownToHTML(mdText string) string { renderer := html.NewRenderer(opts) return string(markdown.Render(doc, renderer)) } + +func basePath(base string) string { + if base == "" || base == "." { + return "/" + } + return base +} + +func joinBase(base, name string) string { + if base == "" { + return "/" + name + } + return base + "/" + name +} diff --git a/modules/version/version.go b/modules/version/version.go index 503a018..338b932 100644 --- a/modules/version/version.go +++ b/modules/version/version.go @@ -9,7 +9,7 @@ var gitCommit string = "devel" const PROGRAM_NAME string = "fes" const PROGRAM_NAME_LONG string = "fes/fSD" -const VERSION string = "0.3.1" +const VERSION string = "1.0.1" func Version() { fmt.Printf("%s version %s\n", PROGRAM_NAME_LONG, VERSION) diff --git a/scripts/test_all b/scripts/test_all new file mode 100755 index 0000000..2c9593c --- /dev/null +++ b/scripts/test_all @@ -0,0 +1,88 @@ +#! /bin/sh +# Runs Fes on all projects in test/ + +scriptversion="1" + +# +# +# Copyright (C) 2025-2026 fSD +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +me=$0 +version="$me/fSD v$scriptversion + +Copyright (C) 2025-2026 fSD. +This is free software; you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law." + +usage="Usage: $me [OPTION]... +Runs Fes on all projects in test/ + +Options: + --help print this help and exit + --version output version information" + +say() { + if [ -z "$q" ]; then + echo "$me: $*" + fi +} + +lsay() { + if [ -z "$q" ]; then + echo " => $*" + fi +} + +check_dep() { + printf 'checking for %s... ' "$1" + if command -v "$1" > /dev/null; then + echo "yes" + else + echo "no" + exit 1 + fi +} + +while [ $# -gt 0 ]; do + case $1 in + --help) echo "$usage"; exit 0 ;; + --version) echo "$version"; exit 0 ;; + -*) echo "$me: Unknown option '$1'." >&2; exit 1 ;; + esac +done + +if [ ! -d "modules" ]; then + echo "$me: error: must be run in project root." >&2 + exit 1 +fi + +if [ ! -f "fes" ]; then + echo "$me: error: run 'make'." >&2 + exit 1 +fi + +for dir in test/*; do + ./fes run "$dir" >/dev/null 2>&1 & + pid=$! + + printf "test '%s'" "$dir" + + printf "\nPress [Enter] to move to start the next..." + read -r unused + + if kill -0 $pid 2>/dev/null; then + kill $pid + fi +done + +echo "done" +kill $$ + +# BUGS +# +# doesn't kill the last test diff --git a/examples/06_archive/Fes.toml b/test/archive_test/Fes.toml similarity index 100% rename from examples/06_archive/Fes.toml rename to test/archive_test/Fes.toml diff --git a/examples/06_archive/README.md b/test/archive_test/README.md similarity index 100% rename from examples/06_archive/README.md rename to test/archive_test/README.md diff --git a/examples/06_archive/archive/facts/seals/seals.txt b/test/archive_test/archive/facts/seals/seals.txt similarity index 100% rename from examples/06_archive/archive/facts/seals/seals.txt rename to test/archive_test/archive/facts/seals/seals.txt diff --git a/examples/06_archive/archive/seal.png b/test/archive_test/archive/seal.png similarity index 100% rename from examples/06_archive/archive/seal.png rename to test/archive_test/archive/seal.png diff --git a/examples/06_archive/www/index.lua b/test/archive_test/www/index.lua similarity index 100% rename from examples/06_archive/www/index.lua rename to test/archive_test/www/index.lua diff --git a/examples/01_hello/Fes.toml b/test/basic_hello_world/Fes.toml similarity index 100% rename from examples/01_hello/Fes.toml rename to test/basic_hello_world/Fes.toml diff --git a/examples/01_hello/README.md b/test/basic_hello_world/README.md similarity index 100% rename from examples/01_hello/README.md rename to test/basic_hello_world/README.md diff --git a/examples/01_hello/www/index.lua b/test/basic_hello_world/www/index.lua similarity index 100% rename from examples/01_hello/www/index.lua rename to test/basic_hello_world/www/index.lua diff --git a/examples/05_best/Fes.toml b/test/complex_sample_project/Fes.toml similarity index 100% rename from examples/05_best/Fes.toml rename to test/complex_sample_project/Fes.toml diff --git a/examples/05_best/README.md b/test/complex_sample_project/README.md similarity index 100% rename from examples/05_best/README.md rename to test/complex_sample_project/README.md diff --git a/examples/05_best/include/footer.lua b/test/complex_sample_project/include/footer.lua similarity index 100% rename from examples/05_best/include/footer.lua rename to test/complex_sample_project/include/footer.lua diff --git a/examples/05_best/include/header.lua b/test/complex_sample_project/include/header.lua similarity index 100% rename from examples/05_best/include/header.lua rename to test/complex_sample_project/include/header.lua diff --git a/examples/05_best/static/favicon.ico b/test/complex_sample_project/static/favicon.ico similarity index 100% rename from examples/05_best/static/favicon.ico rename to test/complex_sample_project/static/favicon.ico diff --git a/examples/05_best/www/index.lua b/test/complex_sample_project/www/index.lua similarity index 100% rename from examples/05_best/www/index.lua rename to test/complex_sample_project/www/index.lua diff --git a/examples/02_default/Fes.toml b/test/default_project/Fes.toml similarity index 100% rename from examples/02_default/Fes.toml rename to test/default_project/Fes.toml diff --git a/examples/02_default/README.md b/test/default_project/README.md similarity index 100% rename from examples/02_default/README.md rename to test/default_project/README.md diff --git a/examples/02_default/www/index.lua b/test/default_project/www/index.lua similarity index 100% rename from examples/02_default/www/index.lua rename to test/default_project/www/index.lua diff --git a/examples/03_error/Fes.toml b/test/expected_error/Fes.toml similarity index 100% rename from examples/03_error/Fes.toml rename to test/expected_error/Fes.toml diff --git a/examples/03_error/README.md b/test/expected_error/README.md similarity index 100% rename from examples/03_error/README.md rename to test/expected_error/README.md diff --git a/examples/03_error/www/index.lua b/test/expected_error/www/index.lua similarity index 100% rename from examples/03_error/www/index.lua rename to test/expected_error/www/index.lua diff --git a/examples/04_markdown/Fes.toml b/test/markdown_render/Fes.toml similarity index 100% rename from examples/04_markdown/Fes.toml rename to test/markdown_render/Fes.toml diff --git a/examples/04_markdown/README.md b/test/markdown_render/README.md similarity index 100% rename from examples/04_markdown/README.md rename to test/markdown_render/README.md diff --git a/examples/04_markdown/www/index.md b/test/markdown_render/www/index.md similarity index 100% rename from examples/04_markdown/www/index.md rename to test/markdown_render/www/index.md diff --git a/examples/00_simple/Fes.toml b/test/minimal_return/Fes.toml similarity index 100% rename from examples/00_simple/Fes.toml rename to test/minimal_return/Fes.toml diff --git a/examples/00_simple/README.md b/test/minimal_return/README.md similarity index 100% rename from examples/00_simple/README.md rename to test/minimal_return/README.md diff --git a/examples/00_simple/www/index.lua b/test/minimal_return/www/index.lua similarity index 100% rename from examples/00_simple/www/index.lua rename to test/minimal_return/www/index.lua