commit 5cfaddf479cce627c9a7ef38d4b14e8f5334de2c
parent 7d70a4a88423f5e2f53c8f8d9528e209db94b39b
Author: vx-clutch <[email protected]>
Date: Wed, 19 Nov 2025 20:29:00 -0500
alpha
Diffstat:
9 files changed, 583 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+fes
diff --git a/README.md b/README.md
@@ -0,0 +1,127 @@
+# Fes
+
+A lightweight static site generator built with Go and Lua. Write your websites in Lua and generate beautiful HTML with a modern dark theme.
+
+## Features
+
+- 🚀 **Simple & Fast**: Minimal setup, fast development workflow
+- 🎨 **Beautiful Default Theme**: Modern dark theme with clean typography
+- 📝 **Lua-Powered**: Write your site logic in Lua for flexibility
+- ⚙️ **TOML Configuration**: Simple configuration via `Fes.toml`
+- 🔧 **Easy CLI**: Two simple commands to get started
+
+## Installation
+
+### Prerequisites
+
+- Go 1.25.4 or later
+- Git (for author name detection)
+
+### Build from Source
+
+```bash
+git clone <repository-url>
+cd fes
+go build -o fes
+```
+
+## Usage
+
+### Create a New Project
+
+```bash
+fes new <project_dir>
+```
+
+This creates a new project directory with:
+- `www/index.lua` - Your main Lua file
+- `Fes.toml` - Project configuration
+
+### Run Development Server
+
+```bash
+fes run <project_dir> [-p <port>]
+```
+
+Starts a local development server (default port: 3000).
+
+Example:
+```bash
+fes run my-site -p 8080
+```
+
+## Project Structure
+
+```
+my-site/
+├── Fes.toml # Project configuration
+└── www/
+ └── index.lua # Main Lua file
+```
+
+## Configuration
+
+Edit `Fes.toml` to configure your site:
+
+```toml
+[site]
+name = "My Site"
+version = "0.0.1"
+authors = ["Your Name"]
+
+[fes]
+version = "1.0.0"
+CUSTOM_CSS = "" # Optional custom CSS
+```
+
+## Writing Your Site
+
+Your `www/index.lua` file should use the Fes API to build your site:
+
+```lua
+local fes = require("fes")
+local site = fes.site_builder()
+
+site:h1("Hello, World!")
+site:h2("Welcome to Fes")
+site:custom("<p>This is a custom HTML paragraph.</p>")
+
+return site
+```
+
+### Available Methods
+
+- `site:h1(text)` - Heading 1
+- `site:h2(text)` - Heading 2
+- `site:h3(text)` - Heading 3
+- `site:h4(text)` - Heading 4
+- `site:h5(text)` - Heading 5
+- `site:h6(text)` - Heading 6
+- `site:custom(html)` - Insert custom HTML
+- `site:version()` - Get site version from config
+
+### Standard Library
+
+Access version information via `fes.std`:
+
+```lua
+local fes = require("fes")
+local site = fes.site_builder()
+
+site:h2("Fes version: " .. fes.std.fes_version())
+site:h2("Site version: " .. fes.std.site_version())
+
+return site
+```
+
+## Example
+
+See the `test/` directory for a complete example project.
+
+## License
+
+ISC License
+
+Copyright (c) 2025 fSD
+
+See `COPYING` for full license text.
diff --git a/core/builtin.lua b/core/builtin.lua
@@ -0,0 +1,172 @@
+local M = {}
+M.__index = M
+
+function M.site_builder(header, footer)
+ local config = {}
+ local site_config = {}
+
+ local fes_mod = package.loaded.fes
+ if fes_mod and fes_mod.config then
+ config = fes_mod.config
+ if config.site then
+ site_config = config.site
+ end
+ end
+
+ local self = {
+ version = site_config.version or "",
+ header = header or [[
+<!DOCTYPE html>
+<html lang="en">
+<style>
+ html,
+ body {
+ height: 100%
+ }
+
+ body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ background: #0f1113;
+ color: #e6eef3;
+ line-height: 1.45;
+ padding: 36px;
+ }
+
+ .container {
+ max-width: 1100px;
+ margin: 0 auto
+ }
+
+ h1 {
+ font-size: 40px;
+ margin: 0 0 18px 0;
+ font-weight: 700;
+ }
+
+ .note {
+ background: #17191b;
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ padding: 18px;
+ border-radius: 4px;
+ margin: 12px 0 26px 0;
+ color: #dfe9ee;
+ }
+
+ .note strong {
+ color: #f0f6f8
+ }
+
+ .callout {
+ display: block;
+ margin: 10px 0
+ }
+
+ a {
+ color: #68a6ff;
+ text-decoration: none
+ }
+
+ a:hover {
+ text-decoration: underline
+ }
+
+ p {
+ margin: 12px 0
+ }
+
+ .muted {
+ color: #9aa6b1
+ }
+
+ .lead {
+ font-size: 15px;
+ margin-top: 8px
+ }
+
+ .highlight {
+ font-weight: 700;
+ color: #cde7ff
+ }
+
+ .small {
+ font-size: 13px;
+ color: #9aa6b1;
+ margin-top: 6px
+ }
+
+ .links {
+ margin: 10px 0
+ }
+
+ .section {
+ margin-top: 18px
+ }
+ </style>
+<body>
+<div class="container">
+]],
+ footer = footer or [[
+</div>
+</body>
+</html>
+]],
+ parts = {}
+ }
+ return setmetatable(self, M)
+end
+
+function M:custom(str)
+ table.insert(self.parts, str)
+ return self
+end
+
+function M:h1(str)
+ str = str or ""
+ table.insert(self.parts, "<h1>" .. str .. "</h1>")
+ return self
+end
+
+function M:h2(str)
+ str = str or ""
+ table.insert(self.parts, "<h2>" .. str .. "</h2>")
+ return self
+end
+
+function M:h3(str)
+ str = str or ""
+ table.insert(self.parts, "<h3>" .. str .. "</h3>")
+ return self
+end
+
+function M:h4(str)
+ str = str or ""
+ table.insert(self.parts, "<h4>" .. str .. "</h4>")
+ return self
+end
+
+function M:h5(str)
+ str = str or ""
+ table.insert(self.parts, "<h5>" .. str .. "</h5>")
+ return self
+end
+
+function M:h6(str)
+ str = str or ""
+ table.insert(self.parts, "<h6>" .. str .. "</h6>")
+ return self
+end
+
+function M:version()
+ return self.version
+end
+
+function M:build()
+ return self.header .. table.concat(self.parts) .. self.footer
+end
+
+M.__tostring = function(self)
+ return self:build()
+end
+
+return M
diff --git a/core/std.lua b/core/std.lua
@@ -0,0 +1,19 @@
+local M = {}
+
+function M.fes_version()
+ local fes_mod = package.loaded.fes
+ if fes_mod and fes_mod.config and fes_mod.config.fes and fes_mod.config.fes.version then
+ return fes_mod.config.fes.version
+ end
+ return ""
+end
+
+function M.site_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
+
+return M
diff --git a/go.mod b/go.mod
@@ -0,0 +1,10 @@
+module fes
+
+go 1.25.4
+
+require (
+ github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 // indirect
+ github.com/gomarkdown/mdtohtml v0.0.0-20240124153210-d773061d1585 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/yuin/gopher-lua v1.1.1 // indirect
+)
diff --git a/go.sum b/go.sum
@@ -0,0 +1,6 @@
+github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
+github.com/gomarkdown/mdtohtml v0.0.0-20240124153210-d773061d1585/go.mod h1:6grYm5/uY15CwgBBqwA3+o/cAzaxssckznJ0B35ouBY=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
+github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
diff --git a/main.go b/main.go
@@ -0,0 +1,231 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "net/http"
+ "os"
+ "os/exec"
+ "os/user"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/pelletier/go-toml/v2"
+ lua "github.com/yuin/gopher-lua"
+)
+
+const version = "1.0.0"
+
+type MyConfig struct {
+ Site struct {
+ Name string `toml:"name"`
+ Version string `toml:"version"`
+ Authors []string `toml:"authors"`
+ } `toml:"site"`
+ Fes struct {
+ Version string `toml:"version"`
+ CUSTOM_CSS string `toml:"CUSTOM_CSS,omitempty"`
+ } `toml:"fes"`
+}
+
+var port = flag.Int("p", 3000, "set the server port")
+
+func loadLua(luaDir string, entry string, cfg *MyConfig) (string, error) {
+ L := lua.NewState()
+ defer L.Close()
+
+ L.PreloadModule("fes", func(L *lua.LState) int {
+ mod := L.NewTable()
+ wd, _ := os.Getwd()
+ corePath := filepath.Join(wd, "core")
+ files, _ := os.ReadDir(corePath)
+ for _, f := range files {
+ if f.IsDir() || filepath.Ext(f.Name()) != ".lua" {
+ continue
+ }
+ modName := f.Name()[:len(f.Name())-len(".lua")]
+ if err := L.DoFile(filepath.Join(corePath, f.Name())); err != nil {
+ fmt.Println("error loading", f.Name(), ":", err)
+ continue
+ }
+ val := L.Get(-1)
+ L.Pop(1)
+ tbl, ok := val.(*lua.LTable)
+ if !ok {
+ t := L.NewTable()
+ t.RawSetString("value", val)
+ tbl = t
+ }
+ if modName == "builtin" {
+ tbl.ForEach(func(key, value lua.LValue) {
+ mod.RawSet(key, value)
+ })
+ } else {
+ mod.RawSetString(modName, tbl)
+ }
+ }
+ // Pass config to Lua
+ if cfg != nil {
+ configTable := L.NewTable()
+ siteTable := L.NewTable()
+ siteTable.RawSetString("version", lua.LString(cfg.Site.Version))
+ siteTable.RawSetString("name", lua.LString(cfg.Site.Name))
+ authorsTable := L.NewTable()
+ for i, author := range cfg.Site.Authors {
+ authorsTable.RawSetInt(i+1, lua.LString(author))
+ }
+ siteTable.RawSetString("authors", authorsTable)
+ configTable.RawSetString("site", siteTable)
+ fesTable := L.NewTable()
+ fesTable.RawSetString("version", lua.LString(cfg.Fes.Version))
+ configTable.RawSetString("fes", fesTable)
+ mod.RawSetString("config", configTable)
+ }
+ L.Push(mod)
+ return 1
+ })
+
+ if err := L.DoFile(entry); err != nil {
+ return "", err
+ }
+
+ top := L.GetTop()
+ if top == 0 {
+ fmt.Println("warning: no return value from Lua file")
+ return "", nil
+ }
+
+ resultVal := L.Get(-1)
+ L.SetGlobal("__fes_result", resultVal)
+ if err := L.DoString("return tostring(__fes_result)"); err != nil {
+ L.GetGlobal("__fes_result")
+ if s := L.ToString(-1); s != "" {
+ return s, nil
+ }
+ return "", nil
+ }
+ if s := L.ToString(-1); s != "" {
+ return s, nil
+ }
+ return "", nil
+}
+
+func getName() string {
+ out, err := exec.Command("git", "config", "user.name").Output()
+ if err == nil {
+ s := strings.TrimSpace(string(out))
+ if s != "" {
+ return s
+ }
+ }
+ u, err := user.Current()
+ if err == nil && u.Username != "" {
+ return u.Username
+ }
+ return ""
+}
+
+func newProject(dir string) error {
+ if err := os.MkdirAll(filepath.Join(dir, "www"), 0755); err != nil {
+ return err
+ }
+ indexLua := filepath.Join(dir, "www", "index.lua")
+ if _, err := os.Stat(indexLua); os.IsNotExist(err) {
+ content := `local fes = require("fes")
+local site = fes.site_builder()
+
+site:h1("Hello, World!")
+
+return site
+`
+ if err := os.WriteFile(indexLua, []byte(content), 0644); err != nil {
+ return err
+ }
+ }
+ indexFes := filepath.Join(dir, "Fes.toml")
+ if _, err := os.Stat(indexFes); os.IsNotExist(err) {
+ content := fmt.Sprintf(`[site]
+
+name = "%s"
+version = "0.0.1"
+authors = ["%s"]
+
+[fes]
+version = "%s"
+CUSTOM_CSS =
+`, dir, getName(), version)
+ if err := os.WriteFile(indexFes, []byte(content), 0644); err != nil {
+ return err
+ }
+ }
+ fmt.Println("Created new project at", dir)
+ return nil
+}
+
+func fixMalformedToml(content string) string {
+ // Fix lines like "CUSTOM_CSS =" (with no value) to "CUSTOM_CSS = \"\""
+ re := regexp.MustCompile(`(?m)^(\s*\w+\s*=\s*)$`)
+ return re.ReplaceAllStringFunc(content, func(match string) string {
+ // Extract the key name
+ parts := strings.Split(strings.TrimSpace(match), "=")
+ if len(parts) == 2 && strings.TrimSpace(parts[1]) == "" {
+ key := strings.TrimSpace(parts[0])
+ return key + " = \"\""
+ }
+ return match
+ })
+}
+
+func startServer(dir string) error {
+ doc, err := os.ReadFile(filepath.Join(dir, "Fes.toml"))
+ if err != nil {
+ return err
+ }
+
+ // Fix malformed TOML before parsing
+ docStr := fixMalformedToml(string(doc))
+
+ var cfg MyConfig
+ err = toml.Unmarshal([]byte(docStr), &cfg)
+ if err != nil {
+ return fmt.Errorf("failed to parse Fes.toml: %w", err)
+ }
+
+ luaPath := filepath.Join(dir, "www", "index.lua")
+ data, err := loadLua(dir, luaPath, &cfg)
+ if err != nil {
+ return err
+ }
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte(data))
+ })
+ fmt.Printf("App running at:\n - Local: http://localhost:%d/\n", *port)
+ return http.ListenAndServe(fmt.Sprintf(":%d", *port), nil)
+}
+
+func main() {
+ flag.Parse()
+ if len(os.Args) < 3 {
+ fmt.Println("Usage: fes <command> <project_dir>")
+ fmt.Println("Commands: new, serve")
+ os.Exit(1)
+ }
+
+ cmd := os.Args[1]
+ dir := os.Args[2]
+
+ switch cmd {
+ case "new":
+ if err := newProject(dir); err != nil {
+ panic(err)
+ }
+ case "run":
+ if err := startServer(dir); err != nil {
+ panic(err)
+ }
+ default:
+ fmt.Println("Unknown command:", cmd)
+ os.Exit(1)
+ }
+}
diff --git a/test/Fes.toml b/test/Fes.toml
@@ -0,0 +1,9 @@
+[site]
+
+name = "test"
+version = "0.0.1"
+authors = ["vx-clutch"]
+
+[fes]
+version = "1.0.0"
+CUSTOM_CSS =
diff --git a/test/www/index.lua b/test/www/index.lua
@@ -0,0 +1,8 @@
+local fes = require("fes")
+local site = fes.site_builder()
+
+site:h1("Hello, Sam!")
+site:h2(fes.std.fes_version())
+site:h2(fes.std.site_version())
+
+return site