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("\n
Index of ")
+ b.WriteString(template.HTMLEscapeString(urlPath))
+ b.WriteString("\n\nIndex 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\nIndex 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