--- /dev/null
+{{ define "main" }}
+<!DOCTYPE html>
+<html>
+ <head>
+ </head>
+ <body>
+ {{ range .Items }}
+ {{ template "item" . }}
+ {{ end }}
+ </body>
+</html>
+{{ end }}
--- /dev/null
+{{ define "item" }}
+<div>
+ {{ $color := "black" }}
+ {{ if .completed }}
+ {{ $color = "gray" }}
+ {{ end }}
+ <span style="color: {{ $color }}"> {{ .name }} </span>
+</div>
+{{ end }}
--- /dev/null
+{{ define "item" }}
+<!DOCTYPE html>
+<html>
+ <head>
+ <link rel="stylesheet" href="/static/style.css">
+ </head>
+ <body>
+ <a href="/" class="link"> <span> > back </span> </a>
+ <div class="column">
+ {{/* Choose an emoji */}}
+ {{ $symbol := "" }}
+ {{ $gray := false }}
+ {{ if eq .type "task" }}
+ {{ if .completed }}
+ {{ $symbol = "✅" }}
+ {{ else }}
+ {{ $symbol = "❎" }}
+ {{ end}}
+ {{ else if eq .type "bug" }}
+ {{ $symbol = "🐞" }}
+ {{ $gray = .completed }}
+ {{ else if eq .type "milestone" }}
+ {{ $symbol = "🏆" }}
+ {{ $gray = not .completed }}
+ {{ end }}
+
+ {{/* Emoji tooltip */}}
+ {{ $completed := "" }}
+ {{ if .completed }}
+ {{ $completed = "completed" }}
+ {{ end }}
+ {{ $tooltip := printf "%s %s" $completed .type }}
+
+ <h1> <span title="{{ $tooltip }}" {{ if $gray }} class="grayscale" {{ end }}> {{ $symbol }} <span/> {{ .name }} </h1>
+ <p> {{ .description }} </p>
+ </div>
+ </body>
+</html>
+{{ end }}
--- /dev/null
+{{ define "listitem" }}
+<div>
+ {{ $color := "black" }}
+ {{ if .completed }}
+ {{ $color = "gray" }}
+ {{ end }}
+ <a href="{{ ._url }}" class="link"> <span class="bg{{.type}}" style="color: {{ $color }}"> {{ .name }} </span> </a>
+</div>
+{{ end }}
--- /dev/null
+{{ define "main" }}
+<!DOCTYPE html>
+<html>
+ <head>
+ <link rel="stylesheet" href="/static/style.css">
+ </head>
+ <body>
+ <div class="column">
+ {{ with .Project }}
+ <h1> {{ .projectname }} </h1>
+ <p> {{ .projectdescription }} </p>
+ {{ end }}
+ <h2 class="centered"> backlog </h2>
+ <hr/>
+ <div class="items">
+ {{ range .Items }}
+ {{ template "listitem" . }}
+ {{ end }}
+ </div>
+ </div>
+ </body>
+</html>
+{{ end }}
--- /dev/null
+projectName = "void"
+
+items = []
--- /dev/null
+[project]
+projectname = "yampl"
+projectdescription = '''
+A toml-to-html site generator for project management.
+'''
+
+[[items]]
+name = "hello world"
+description = '''
+Have an program.
+'''
+type = "milestone"
+completed = true
+
+[[items]]
+name = "command line utility"
+description = '''
+Run the program once as command line utility.
+It acts as a conversion script.
+It turns the project file and template into a html file.
+'''
+type = "task"
+completed = true
+
+[[items]]
+name = "http server"
+description = '''
+Run the program as a http service.
+On request it reads the project file and runs the detemplating.
+'''
+type = "task"
+completed = true
+
+[[items]]
+name = "project repo"
+description = '''
+set up a git page for yampl.
+'''
+type = "task"
+completed = false
+
+[[items]]
+name = "static links to details"
+description = '''
+Links are based on index of the item, but this index can change.
+Figure out static routing, perhaps based on name.
+'''
+type = "bug"
+completed = false
+
+[[items]]
+name = "hosted"
+description = '''
+Host yampl for yampl on project.openfl.eu/yamlp
+'''
+type = "milestone"
+completed = false
+
+[[items]]
+name = "git hook"
+description = '''
+On push to main branch of yampl, the toml file must be checked out into the working directory.
+The yampl service should pick up that new file.
+'''
+type = "task"
+completed = false
+
+[[items]]
+name = "watch project file"
+description = '''
+Run the program as a http service, with websocket connection to the frontend.
+On project file change, reload the file, and update clients with new content.
+'''
+type = "task"
+completed = false
+
+[[items]]
+name = "auto hosted"
+description = '''
+Have the hosting be completely automatic, with automatic updates of the project file
+and the container.
+'''
+type = "milestone"
+completed = false
+
+[[items]]
+name = "heat death of the universe"
+description = '''
+survive.
+'''
+type = "milestone"
+completed = false
--- /dev/null
+package main
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path"
+)
+
+var InvalidFilepathError = errors.New("invalid file.")
+
+func getFile(filepath string) (io.ReadCloser, error) {
+ dirname, filename := path.Split(filepath)
+ if dirname == "" {
+ return nil, fmt.Errorf("%w: the file '%s' could not be read", InvalidFilepathError, filepath)
+ }
+ dirFS := os.DirFS(dirname)
+ fileInfo, err := fs.Stat(dirFS, filename)
+ if err != nil {
+ return nil, fmt.Errorf("%w: could not check the file %s", InvalidFilepathError, filepath)
+ }
+ if !fileInfo.Mode().IsRegular() {
+ return nil, fmt.Errorf("%w: the file '%s' is not a text file", InvalidFilepathError, filepath)
+ }
+
+ return os.Open(filepath)
+}
--- /dev/null
+module go.openfl.eu/yampl
+
+go 1.23.5
+
+require github.com/BurntSushi/toml v1.4.0 // indirect
--- /dev/null
+github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
+github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
--- /dev/null
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "strconv"
+
+ "github.com/BurntSushi/toml"
+)
+
+type ProjectData struct {
+ Project Project
+ Items []ProjectItems
+}
+
+type Project any
+type ProjectItems map[string]any
+
+var tomlfilepath = flag.String("toml", "", "path from current working directory to project file")
+var tempdirpath = flag.String("templ", "", "path from current working directory to a directory containing template files")
+var staticdirpath = flag.String("static", "", "path from current working direcotry to a directory containing static files to serve over http")
+
+func listRespond(w http.ResponseWriter, r *http.Request) {
+ templ, err := getTemplate(*tempdirpath)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ log.Println(err)
+ return
+ }
+
+ tomlfile, err := getFile(*tomlfilepath)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ log.Println(err)
+ return
+ }
+
+ var data ProjectData
+ _, err = toml.NewDecoder(tomlfile).Decode(&data)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ log.Println(err)
+ return
+ }
+
+ for i, it := range data.Items {
+ it["_index"] = i
+ it["_url"] = fmt.Sprintf("/items/%d", i)
+ }
+
+ templ.ExecuteTemplate(w, "main", data)
+}
+
+func itemRespond(w http.ResponseWriter, r *http.Request) {
+ templ, err := getTemplate(*tempdirpath)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ log.Println(err)
+ return
+ }
+
+ tomlfile, err := getFile(*tomlfilepath)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ log.Println(err)
+ return
+ }
+
+ var data ProjectData
+ _, err = toml.NewDecoder(tomlfile).Decode(&data)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ log.Println(err)
+ return
+ }
+
+ index, err := strconv.Atoi(r.PathValue("index"))
+ if err != nil || index < 0 || len(data.Items) < index {
+ w.WriteHeader(http.StatusInternalServerError)
+ log.Println(err)
+ return
+ }
+
+ templ.ExecuteTemplate(w, "item", data.Items[index])
+}
+
+func main() {
+ flag.Parse()
+ if *tomlfilepath == "" || *tempdirpath == "" {
+ flag.Usage()
+ os.Exit(-1)
+ }
+
+ http.HandleFunc("GET /{$}", listRespond)
+ http.HandleFunc("GET /items/{index}", itemRespond)
+ if *staticdirpath != "" {
+ log.Println("serving static content")
+ http.Handle("GET /static/", http.StripPrefix("/static", http.FileServerFS(os.DirFS(*staticdirpath))))
+ }
+
+ fmt.Println("listening on localhost:8081")
+ panic(http.ListenAndServe(":8081", nil))
+}
--- /dev/null
+
+<!DOCTYPE html>
+<html>
+ <head>
+ </head>
+ <body>
+
+
+<div>
+
+
+
+
+ <span style="color: gray"> command line utility </span>
+</div>
+
+
+
+<div>
+
+
+ <span style="color: black"> http server </span>
+</div>
+
+
+
+<div>
+
+
+ <span style="color: black"> watch project file </span>
+</div>
+
+
+
+<div>
+
+
+ <span style="color: black"> heat death of the universe </span>
+</div>
+
+
+ </body>
+</html>
--- /dev/null
+body {
+ background-color: white;
+ display: flex;
+ justify-content: center;
+ font-family: monospace;
+ background-color: blanchedalmond;
+}
+.column {
+ max-width: 50%;
+}
+
+.items {
+}
+
+.link {
+ color: black;
+ text-decoration: none;;
+}
+
+.link :hover {
+ text-decoration: underline;
+}
+
+h2.centered {
+ text-align: center;
+}
+
+.bgtask {
+ background-color: lightblue;
+}
+.bgbug {
+ background-color: crimson;
+}
+
+.bgmilestone {
+ background-color: goldenrod;
+}
+
+.grayscale {
+ filter: grayscale(100%);
+}
\ No newline at end of file
--- /dev/null
+package main
+
+import (
+ "html/template"
+ "os"
+)
+
+func getTemplate(templpath string) (*template.Template, error) {
+ templfs := os.DirFS(templpath)
+ return template.ParseFS(templfs, "*.html")
+}