This commit is contained in:
2025-11-19 20:29:00 -05:00
parent 7d70a4a884
commit 5cfaddf479
9 changed files with 583 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
fes

127
README.md Normal file
View File

@@ -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.

172
core/builtin.lua Normal file
View File

@@ -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

19
core/std.lua Normal file
View File

@@ -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

10
go.mod Normal file
View File

@@ -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
)

6
go.sum Normal file
View File

@@ -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=

231
main.go Normal file
View File

@@ -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)
}
}

9
test/Fes.toml Normal file
View File

@@ -0,0 +1,9 @@
[site]
name = "test"
version = "0.0.1"
authors = ["vx-clutch"]
[fes]
version = "1.0.0"
CUSTOM_CSS =

8
test/www/index.lua Normal file
View File

@@ -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