├── .gitignore
├── atom
├── feed.go
└── uuid.go
├── commands
├── build.go
├── init.go
├── new.go
└── serve.go
├── config
└── config.go
├── contrib
└── style.go
├── default.nix
├── fileutil
├── copy.go
└── rmall.go
├── flake.lock
├── flake.nix
├── formats
├── anything.go
├── markdown
│ └── markdown.go
└── yaml
│ └── yaml.go
├── go.mod
├── go.sum
├── license
├── main.go
├── makefile
├── readme
├── template
└── template.go
└── types
└── types.go
/.gitignore:
--------------------------------------------------------------------------------
1 | vite
2 | .env
3 | .direnv
4 | result
5 |
--------------------------------------------------------------------------------
/atom/feed.go:
--------------------------------------------------------------------------------
1 | package atom
2 |
3 | import (
4 | "encoding/xml"
5 | "fmt"
6 | "net/url"
7 | "path/filepath"
8 | "time"
9 |
10 | "tangled.sh/icyphox.sh/vite/config"
11 | "tangled.sh/icyphox.sh/vite/types"
12 | )
13 |
14 | type AtomLink struct {
15 | XMLName xml.Name `xml:"link"`
16 | Href string `xml:"href,attr"`
17 | Rel string `xml:"rel,attr,omitempty"`
18 | }
19 |
20 | type AtomSummary struct {
21 | XMLName xml.Name `xml:"summary"`
22 | Content string `xml:",chardata"`
23 | Type string `xml:"type,attr"`
24 | }
25 |
26 | type AtomAuthor struct {
27 | XMLName xml.Name `xml:"author"`
28 | Name string `xml:"name"`
29 | Email string `xml:"email"`
30 | }
31 |
32 | type AtomEntry struct {
33 | XMLName xml.Name `xml:"entry"`
34 | Title string `xml:"title"`
35 | Updated string `xml:"updated"`
36 | ID string `xml:"id"`
37 | Link *AtomLink
38 | Summary *AtomSummary
39 | }
40 |
41 | type AtomFeed struct {
42 | XMLName xml.Name `xml:"feed"`
43 | Xmlns string `xml:"xmlns,attr"`
44 | Title string `xml:"title"`
45 | Subtitle string `xml:"subtitle"`
46 | ID string `xml:"id"`
47 | Updated string `xml:"updated"`
48 | Link *AtomLink
49 | Author *AtomAuthor `xml:"author"`
50 | Entries []AtomEntry
51 | }
52 |
53 | // Creates a new Atom feed.
54 | func NewAtomFeed(srcDir string, posts []types.Post) ([]byte, error) {
55 | entries := []AtomEntry{}
56 |
57 | for _, p := range posts {
58 | dateStr := p.Meta["date"].(string)
59 | date, err := time.Parse("2006-01-02", dateStr)
60 | if err != nil {
61 | return nil, err
62 | }
63 | rfc3339 := date.Format(time.RFC3339)
64 |
65 | var summaryContent string
66 | if subtitle, ok := p.Meta["subtitle"]; ok {
67 | summaryContent = fmt.Sprintf("
%s
\n%s",
68 | subtitle.(string),
69 | string(p.Body))
70 | } else {
71 | summaryContent = string(p.Body)
72 | }
73 |
74 | entry := AtomEntry{
75 | Title: p.Meta["title"].(string),
76 | Updated: rfc3339,
77 | // tag:icyphox.sh,2019-10-23:blog/some-post/
78 | ID: fmt.Sprintf(
79 | "tag:%s,%s:%s",
80 | config.Config.URL[8:], // strip https://
81 | dateStr,
82 | filepath.Join(srcDir, p.Meta["slug"].(string)),
83 | ),
84 | Link: newAtomLink(config.Config.URL, srcDir, p.Meta["slug"].(string)),
85 | Summary: &AtomSummary{
86 | Content: summaryContent,
87 | Type: "html",
88 | },
89 | }
90 | entries = append(entries, entry)
91 | }
92 |
93 | // 2021-07-14T00:00:00Z
94 | now := time.Now().Format(time.RFC3339)
95 | feed := &AtomFeed{
96 | Xmlns: "http://www.w3.org/2005/Atom",
97 | Title: config.Config.Title,
98 | ID: config.Config.URL,
99 | Subtitle: config.Config.Desc,
100 | Link: &AtomLink{Href: config.Config.URL},
101 | Author: &AtomAuthor{
102 | Name: config.Config.Author.Name,
103 | Email: config.Config.Author.Email,
104 | },
105 | Updated: now,
106 | Entries: entries,
107 | }
108 |
109 | feedXML, err := xml.MarshalIndent(feed, " ", " ")
110 | if err != nil {
111 | return nil, err
112 | }
113 | // Add the header.
114 | return []byte(xml.Header + string(feedXML)), nil
115 | }
116 |
117 | // Creates a new Atom link.
118 | //
119 | // Example:
120 | //
121 | // newAtomLink("https://example.com", "blog", "some-post")
122 | // // →
123 | func newAtomLink(base string, subdomain string, slug string) *AtomLink {
124 | baseURL, err := url.Parse(base)
125 | if err != nil {
126 | return nil
127 | }
128 |
129 | baseURL.Host = subdomain + "." + baseURL.Host
130 | baseURL.Path = slug
131 |
132 | return &AtomLink{Href: baseURL.String()}
133 | }
134 |
--------------------------------------------------------------------------------
/atom/uuid.go:
--------------------------------------------------------------------------------
1 | package atom
2 |
3 | import (
4 | "crypto/rand"
5 | "fmt"
6 | )
7 |
8 | type UUID [16]byte
9 |
10 | // Create a new uuid v4
11 | func NewUUID() *UUID {
12 | u := &UUID{}
13 | _, err := rand.Read(u[:16])
14 | if err != nil {
15 | panic(err)
16 | }
17 |
18 | u[8] = (u[8] | 0x80) & 0xBf
19 | u[6] = (u[6] | 0x40) & 0x4f
20 | return u
21 | }
22 |
23 | func (u *UUID) String() string {
24 | return fmt.Sprintf("%x-%x-%x-%x-%x", u[:4], u[4:6], u[6:8], u[8:10], u[10:])
25 | }
26 |
--------------------------------------------------------------------------------
/commands/build.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/exec"
7 | "path/filepath"
8 | "sort"
9 | "strings"
10 | "time"
11 |
12 | "tangled.sh/icyphox.sh/vite/atom"
13 | "tangled.sh/icyphox.sh/vite/config"
14 | util "tangled.sh/icyphox.sh/vite/fileutil"
15 | "tangled.sh/icyphox.sh/vite/formats"
16 | "tangled.sh/icyphox.sh/vite/formats/markdown"
17 | "tangled.sh/icyphox.sh/vite/formats/yaml"
18 | "tangled.sh/icyphox.sh/vite/types"
19 | )
20 |
21 | type Dir struct {
22 | Name string
23 | HasIndex bool
24 | Files []types.File
25 | }
26 |
27 | type Pages struct {
28 | Dirs []Dir
29 | Files []types.File
30 |
31 | // A map of directories to their respective posts.
32 | AllPosts map[string][]types.Post
33 | }
34 |
35 | func NewPages() (*Pages, error) {
36 | pages := &Pages{}
37 |
38 | entries, err := os.ReadDir(types.PagesDir)
39 | if err != nil {
40 | return nil, err
41 | }
42 |
43 | pages.AllPosts = make(map[string][]types.Post)
44 |
45 | for _, entry := range entries {
46 | if entry.IsDir() {
47 | thingsDir := filepath.Join(types.PagesDir, entry.Name())
48 | dir := Dir{Name: entry.Name()}
49 | things, err := os.ReadDir(thingsDir)
50 | if err != nil {
51 | return nil, err
52 | }
53 |
54 | for _, thing := range things {
55 | if thing.Name() == "_index.md" {
56 | dir.HasIndex = true
57 | continue
58 | }
59 | switch filepath.Ext(thing.Name()) {
60 | case ".md":
61 | path := filepath.Join(thingsDir, thing.Name())
62 | dir.Files = append(dir.Files, &markdown.Markdown{Path: path})
63 | case ".yaml":
64 | path := filepath.Join(thingsDir, thing.Name())
65 | dir.Files = append(dir.Files, &yaml.YAML{Path: path})
66 | default:
67 | fmt.Printf("warn: unrecognized filetype for file: %s\n", thing.Name())
68 | }
69 | }
70 |
71 | pages.Dirs = append(pages.Dirs, dir)
72 | } else {
73 | path := filepath.Join(types.PagesDir, entry.Name())
74 | switch filepath.Ext(entry.Name()) {
75 | case ".md":
76 | pages.Files = append(pages.Files, &markdown.Markdown{Path: path})
77 | case ".yaml":
78 | pages.Files = append(pages.Files, &yaml.YAML{Path: path})
79 | default:
80 | pages.Files = append(pages.Files, formats.Anything{Path: path})
81 | }
82 | }
83 | }
84 |
85 | return pages, nil
86 | }
87 |
88 | // Build is the core builder function. Converts markdown/yaml
89 | // to html, copies over non-.md/.yaml files, etc.
90 | func Build(drafts bool) error {
91 | startTime := time.Now()
92 |
93 | if err := preBuild(); err != nil {
94 | return err
95 | }
96 | fmt.Println("vite: building")
97 |
98 | pages, err := NewPages()
99 | if err != nil {
100 | return fmt.Errorf("error: reading 'pages/' %w", err)
101 | }
102 |
103 | if err := util.Clean(types.BuildDir); err != nil {
104 | return err
105 | }
106 |
107 | if err := pages.ProcessDirectories(drafts); err != nil {
108 | return err
109 | }
110 |
111 | if err := pages.ProcessFiles(drafts); err != nil {
112 | return err
113 | }
114 |
115 | buildStatic := filepath.Join(types.BuildDir, types.StaticDir)
116 | if err := os.MkdirAll(buildStatic, 0755); err != nil {
117 | return err
118 | }
119 | if err := util.CopyDir(types.StaticDir, buildStatic); err != nil {
120 | return err
121 | }
122 |
123 | buildTime := time.Since(startTime)
124 | fmt.Printf("vite: completed in %v\n", buildTime)
125 |
126 | return nil
127 | }
128 |
129 | // ProcessFiles handles root level files under 'pages',
130 | // for example: 'pages/_index.md' or 'pages/about.md'.
131 | func (p *Pages) ProcessFiles(drafts bool) error {
132 | for _, f := range p.Files {
133 | var htmlDir string
134 | if f.Basename() == "_index.md" {
135 | htmlDir = types.BuildDir
136 | } else {
137 | htmlDir = filepath.Join(types.BuildDir, strings.TrimSuffix(f.Basename(), f.Ext()))
138 | }
139 |
140 | destFile := filepath.Join(htmlDir, "index.html")
141 | if f.Ext() == "" {
142 | destFile = filepath.Join(types.BuildDir, f.Basename())
143 | } else {
144 | if err := os.MkdirAll(htmlDir, 0755); err != nil {
145 | return err
146 | }
147 | }
148 | if err := f.Render(destFile, p.AllPosts, drafts); err != nil {
149 | return fmt.Errorf("error: failed to render %s: %w", destFile, err)
150 | }
151 | }
152 | return nil
153 | }
154 |
155 | // ProcessDirectories handles directories of posts under 'pages',
156 | // for example: 'pages/photos/foo.md' or 'pages/blog/bar.md'.
157 | func (p *Pages) ProcessDirectories(drafts bool) error {
158 | for _, dir := range p.Dirs {
159 | dstDir := filepath.Join(types.BuildDir, dir.Name)
160 | if err := os.MkdirAll(dstDir, 0755); err != nil {
161 | return fmt.Errorf("error: failed to create directory: %s: %w", dstDir, err)
162 | }
163 |
164 | posts := []types.Post{}
165 |
166 | for _, file := range dir.Files {
167 | post := types.Post{}
168 | // foo-bar.md -> foo-bar
169 | slug := strings.TrimSuffix(file.Basename(), file.Ext())
170 | dstFile := filepath.Join(dstDir, slug, "index.html")
171 |
172 | // ex: build/blog/foo-bar/
173 | if err := os.MkdirAll(filepath.Join(dstDir, slug), 0755); err != nil {
174 | return fmt.Errorf("error: failed to create directory: %s: %w", dstDir, err)
175 | }
176 |
177 | if err := file.Render(dstFile, nil, drafts); err != nil {
178 | return fmt.Errorf("error: failed to render %s: %w", dstFile, err)
179 | }
180 |
181 | post.Meta = file.Frontmatter()
182 | post.Body = file.Body()
183 | isDraft := post.Meta["draft"] == "true"
184 | if !isDraft || (isDraft && drafts) {
185 | posts = append(posts, post)
186 | }
187 |
188 | // Copy the post to the root if it's marked as such.
189 | // ex: build/blog/foo-bar -> build/foo-bar
190 | atroot, ok := post.Meta["atroot"]
191 | if ok && atroot.(bool) {
192 | os.Mkdir(filepath.Join(types.BuildDir, slug), 0755)
193 | df := filepath.Join(types.BuildDir, slug+".html")
194 | util.CopyFile(filepath.Join(dstDir, slug, "index.html"), df)
195 | }
196 | }
197 |
198 | sort.Slice(posts, func(i, j int) bool {
199 | dateStr1 := posts[j].Meta["date"].(string)
200 | dateStr2 := posts[i].Meta["date"].(string)
201 | date1, _ := time.Parse("2006-01-02", dateStr1)
202 | date2, _ := time.Parse("2006-01-02", dateStr2)
203 | return date1.Before(date2)
204 | })
205 |
206 | if dir.HasIndex {
207 | indexMd := filepath.Join(types.PagesDir, dir.Name, "_index.md")
208 | index := markdown.Markdown{Path: indexMd}
209 | dstFile := filepath.Join(dstDir, "index.html")
210 | if err := index.Render(dstFile, posts, false); err != nil {
211 | return fmt.Errorf("error: failed to render index %s: %w", dstFile, err)
212 | }
213 | }
214 |
215 | xml, err := atom.NewAtomFeed(dir.Name, posts)
216 | if err != nil {
217 | return fmt.Errorf("error: failed to create atom feed for: %s: %w", dir.Name, err)
218 | }
219 | feedFile := filepath.Join(dstDir, "feed.xml")
220 | os.WriteFile(feedFile, xml, 0755)
221 |
222 | p.AllPosts[dir.Name] = posts
223 | }
224 |
225 | return nil
226 | }
227 |
228 | func runCmd(cmd string, args ...string) error {
229 | parts := strings.Fields(cmd)
230 | if len(parts) == 0 {
231 | return fmt.Errorf("error: is there an empty command?")
232 | }
233 |
234 | execCmd := exec.Command(parts[0], parts[1:]...)
235 |
236 | output, err := execCmd.CombinedOutput()
237 | if err != nil {
238 | return fmt.Errorf("error: command %q failed with %v: %s", cmd, err, output)
239 | }
240 | return nil
241 | }
242 |
243 | func postBuild() error {
244 | for _, cmd := range config.Config.PostBuild {
245 | fmt.Println("vite: running post-build command:", cmd)
246 | if err := runCmd(cmd); err != nil {
247 | return err
248 | }
249 | }
250 | return nil
251 | }
252 |
253 | func preBuild() error {
254 | for _, cmd := range config.Config.PreBuild {
255 | fmt.Println("vite: running pre-build command:", cmd)
256 | if err := runCmd(cmd); err != nil {
257 | return err
258 | }
259 | }
260 | return nil
261 | }
262 |
--------------------------------------------------------------------------------
/commands/init.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | )
8 |
9 | func Init(path string) error {
10 | paths := []string{"build", "pages", "static", "templates"}
11 | var dirPath string
12 |
13 | for _, p := range paths {
14 | dirPath = filepath.Join(path, p)
15 | err := os.MkdirAll(dirPath, 0755)
16 | if err != nil {
17 | return err
18 | }
19 | }
20 | fp, _ := filepath.Abs(path)
21 | fmt.Printf("vite: created project at %s\n", fp)
22 | return nil
23 | }
24 |
--------------------------------------------------------------------------------
/commands/new.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 | "time"
10 | )
11 |
12 | func New(path string) error {
13 | _, file := filepath.Split(path)
14 | url := strings.TrimSuffix(file, filepath.Ext(file))
15 |
16 | content := fmt.Sprintf(`---
17 | atroot: true
18 | template:
19 | slug: %s
20 | title:
21 | subtitle:
22 | date: %s
23 | draft: true
24 | ---`, url, time.Now().Format("2006-01-02"))
25 |
26 | if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
27 | _, err := os.Create(path)
28 | if err != nil {
29 | return err
30 | }
31 | os.WriteFile(path, []byte(content), 0755)
32 | fmt.Printf("vite: created new post at %s\n", path)
33 | return nil
34 | }
35 |
36 | fmt.Printf("error: %s already exists\n", path)
37 | return nil
38 | }
39 |
--------------------------------------------------------------------------------
/commands/serve.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | )
7 |
8 | func Serve(addr string) error {
9 | fs := http.FileServer(http.Dir("./build"))
10 | mux := http.NewServeMux()
11 | mux.Handle("/", fs)
12 | fmt.Printf("vite: serving on %s\n", addr)
13 | if err := http.ListenAndServe(addr, mux); err != nil {
14 | return err
15 | }
16 | return nil
17 | }
18 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "gopkg.in/yaml.v3"
8 | )
9 |
10 | var Config ConfigYaml
11 |
12 | func init() {
13 | err := Config.parseConfig()
14 | if err != nil {
15 | fmt.Fprintf(os.Stderr, "error: config: %+v\n", err)
16 | os.Exit(1)
17 | }
18 | }
19 |
20 | type ConfigYaml struct {
21 | Title string `yaml:"title"`
22 | Desc string `yaml:"description"`
23 | DefaultTemplate string `yaml:"-"`
24 | Author struct {
25 | Name string `yaml:"name"`
26 | Email string `yaml:"email"`
27 | } `yaml:"author"`
28 | URL string `yaml:"url"`
29 | PreBuild []string `yaml:"preBuild"`
30 | PostBuild []string `yaml:"postBuild"`
31 | }
32 |
33 | // For backward compat with `default-template`
34 | func (c *ConfigYaml) UnmarshalYAML(value *yaml.Node) error {
35 | type Alias ConfigYaml // Create an alias to avoid recursion
36 |
37 | var aux Alias
38 |
39 | if err := value.Decode(&aux); err != nil {
40 | return err
41 | }
42 |
43 | // Handle the DefaultTemplate field
44 | var temp struct {
45 | DefaultTemplate1 string `yaml:"default-template"`
46 | DefaultTemplate2 string `yaml:"defaultTemplate"`
47 | }
48 | if err := value.Decode(&temp); err != nil {
49 | return err
50 | }
51 |
52 | if temp.DefaultTemplate1 != "" {
53 | aux.DefaultTemplate = temp.DefaultTemplate1
54 | } else {
55 | aux.DefaultTemplate = temp.DefaultTemplate2
56 | }
57 |
58 | *c = ConfigYaml(aux) // Assign the unmarshalled values back to the original struct
59 |
60 | return nil
61 | }
62 |
63 | func (c *ConfigYaml) parseConfig() error {
64 | cf, err := os.ReadFile("config.yaml")
65 | if err != nil {
66 | return err
67 | }
68 | if err = yaml.Unmarshal(cf, c); err != nil {
69 | return err
70 | }
71 | return nil
72 | }
73 |
--------------------------------------------------------------------------------
/contrib/style.go:
--------------------------------------------------------------------------------
1 | // style.go: generate chroma css
2 | // go run contrib/style.go > syntax.css
3 | package main
4 |
5 | import (
6 | "os"
7 |
8 | "github.com/alecthomas/chroma"
9 | "github.com/alecthomas/chroma/formatters/html"
10 | "github.com/alecthomas/chroma/styles"
11 | )
12 |
13 | var syntax = chroma.MustNewStyle("syntax", chroma.StyleEntries{
14 | chroma.CommentMultiline: "italic #999988",
15 | chroma.CommentPreproc: "bold #999999",
16 | chroma.CommentSingle: "italic #999988",
17 | chroma.CommentSpecial: "bold italic #999999",
18 | chroma.Comment: "italic #999988",
19 | chroma.Error: "bg:#e3d2d2 #a61717",
20 | chroma.GenericDeleted: "bg:#ffdddd #000000",
21 | chroma.GenericEmph: "italic #000000",
22 | chroma.GenericError: "#aa0000",
23 | chroma.GenericHeading: "#999999",
24 | chroma.GenericInserted: "bg:#ddffdd #000000",
25 | chroma.GenericOutput: "#888888",
26 | chroma.GenericPrompt: "#555555",
27 | chroma.GenericStrong: "bold",
28 | chroma.GenericSubheading: "#aaaaaa",
29 | chroma.GenericTraceback: "#aa0000",
30 | chroma.GenericUnderline: "underline",
31 | chroma.KeywordType: "bold #222222",
32 | chroma.Keyword: "bold #000000",
33 | chroma.LiteralNumber: "#009999",
34 | chroma.LiteralStringRegex: "#009926",
35 | chroma.LiteralStringSymbol: "#990073",
36 | chroma.LiteralString: "#509c93",
37 | chroma.NameAttribute: "#008080",
38 | chroma.NameBuiltinPseudo: "#999999",
39 | chroma.NameBuiltin: "#509c93",
40 | chroma.NameClass: "bold #666666",
41 | chroma.NameConstant: "#008080",
42 | chroma.NameDecorator: "bold #3c5d5d",
43 | chroma.NameEntity: "#509c93",
44 | chroma.NameException: "bold #444444",
45 | chroma.NameFunction: "bold #444444",
46 | chroma.NameLabel: "bold #444444",
47 | chroma.NameNamespace: "#555555",
48 | chroma.NameTag: "#000080",
49 | chroma.NameVariableClass: "#008080",
50 | chroma.NameVariableGlobal: "#008080",
51 | chroma.NameVariableInstance: "#008080",
52 | chroma.NameVariable: "#008080",
53 | chroma.Operator: "bold #000000",
54 | chroma.TextWhitespace: "#bbbbbb",
55 | chroma.Background: " bg:#ffffff",
56 | })
57 |
58 | func main() {
59 | formatter := html.New(html.WithClasses(true))
60 | formatter.WriteCSS(os.Stdout, styles.Get("catppuccin-latte"))
61 | }
62 |
--------------------------------------------------------------------------------
/default.nix:
--------------------------------------------------------------------------------
1 | { pkgs ? (
2 | let
3 | inherit (builtins) fetchTree fromJSON readFile;
4 | inherit ((fromJSON (readFile ./flake.lock)).nodes) nixpkgs gomod2nix;
5 | in
6 | import (fetchTree nixpkgs.locked) {
7 | overlays = [
8 | (import "${fetchTree gomod2nix.locked}/overlay.nix")
9 | ];
10 | }
11 | )
12 | }:
13 |
14 | pkgs.buildGoApplication {
15 | pname = "vite";
16 | version = "0.1";
17 | pwd = ./.;
18 | src = ./.;
19 | modules = ./gomod2nix.toml;
20 | }
21 |
--------------------------------------------------------------------------------
/fileutil/copy.go:
--------------------------------------------------------------------------------
1 | package fileutil
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "os"
7 | "path/filepath"
8 | )
9 |
10 | // CopyFile copies a file from src to dst.
11 | func CopyFile(src, dst string) error {
12 | in, err := os.Open(src)
13 | if err != nil {
14 | return err
15 | }
16 | defer in.Close()
17 |
18 | out, err := os.Create(dst)
19 | if err != nil {
20 | return err
21 | }
22 | defer out.Close()
23 |
24 | _, err = io.Copy(out, in)
25 | if err != nil {
26 | return err
27 | }
28 |
29 | // Copy modes.
30 | f, err := os.Stat(src)
31 | if err == nil {
32 | err = os.Chmod(dst, f.Mode())
33 | if err != nil {
34 | return err
35 | }
36 | }
37 |
38 | return out.Close()
39 | }
40 |
41 | // CopyDir copies an entire directory tree from
42 | // src to dst.
43 | func CopyDir(src, dst string) error {
44 | fi, err := os.Stat(src)
45 | if err != nil {
46 | return err
47 | }
48 |
49 | if !fi.IsDir() {
50 | return fmt.Errorf("error: %q is not a directory", fi)
51 | }
52 |
53 | if err = os.MkdirAll(dst, 0755); err != nil {
54 | return err
55 | }
56 |
57 | items, _ := os.ReadDir(src)
58 | for _, item := range items {
59 | srcFilename := filepath.Join(src, item.Name())
60 | dstFilename := filepath.Join(dst, item.Name())
61 | if item.IsDir() {
62 | if err := CopyDir(srcFilename, dstFilename); err != nil {
63 | return err
64 | }
65 | } else {
66 | if err := CopyFile(srcFilename, dstFilename); err != nil {
67 | return err
68 | }
69 | }
70 | }
71 |
72 | return nil
73 | }
74 |
--------------------------------------------------------------------------------
/fileutil/rmall.go:
--------------------------------------------------------------------------------
1 | package fileutil
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | )
7 |
8 | // Cleans a given directory, removing all files and subdirs.
9 | func Clean(dir string) error {
10 | files, err := filepath.Glob(filepath.Join(dir, "*"))
11 | if err != nil {
12 | return err
13 | }
14 |
15 | for _, file := range files {
16 | err = os.RemoveAll(file)
17 | if err != nil {
18 | return err
19 | }
20 | }
21 | return nil
22 | }
23 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "nixpkgs": {
4 | "locked": {
5 | "lastModified": 1716807034,
6 | "narHash": "sha256-AQwfMBtC8tiDZHEgLssOU4qUiRhC2nO+YdxAvZXeW7o=",
7 | "owner": "nixos",
8 | "repo": "nixpkgs",
9 | "rev": "e751fce87c5d9a8c9375b59d5b508a34a76b5623",
10 | "type": "github"
11 | },
12 | "original": {
13 | "owner": "nixos",
14 | "repo": "nixpkgs",
15 | "type": "github"
16 | }
17 | },
18 | "root": {
19 | "inputs": {
20 | "nixpkgs": "nixpkgs"
21 | }
22 | }
23 | },
24 | "root": "root",
25 | "version": 7
26 | }
27 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "a fast and minimal static site generator";
3 |
4 | inputs.nixpkgs.url = "github:nixos/nixpkgs";
5 |
6 | outputs =
7 | { self
8 | , nixpkgs
9 | ,
10 | }:
11 | let
12 | supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
13 | forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
14 | nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
15 | in
16 | {
17 | overlay = final: prev: {
18 | vite = self.packages.${prev.system}.vite;
19 | };
20 | nixosModule = import ./module.nix;
21 | packages = forAllSystems (system:
22 | let
23 | pkgs = nixpkgsFor.${system};
24 | in
25 | {
26 | vite = pkgs.buildGoModule {
27 | name = "vite";
28 | rev = "master";
29 | src = ./.;
30 |
31 | vendorHash = "sha256-jZO2ZX5Ik3TxBWMkq4TkA3TZvzGTQsuKRNKZFQt3gac=";
32 | };
33 | });
34 |
35 | defaultPackage = forAllSystems (system: self.packages.${system}.vite);
36 | devShells = forAllSystems (system:
37 | let
38 | pkgs = nixpkgsFor.${system};
39 | in
40 | {
41 | default = pkgs.mkShell {
42 | nativeBuildInputs = with pkgs; [
43 | go
44 | ];
45 | };
46 | });
47 | };
48 | }
49 |
--------------------------------------------------------------------------------
/formats/anything.go:
--------------------------------------------------------------------------------
1 | package formats
2 |
3 | import (
4 | "path/filepath"
5 |
6 | util "tangled.sh/icyphox.sh/vite/fileutil"
7 | )
8 |
9 | // Anything is a stub format for unrecognized files
10 | type Anything struct{ Path string }
11 |
12 | func (Anything) Ext() string { return "" }
13 | func (Anything) Frontmatter() map[string]any { return nil }
14 | func (Anything) Body() string { return "" }
15 | func (a Anything) Basename() string { return filepath.Base(a.Path) }
16 |
17 | func (a Anything) Render(dest string, data interface{}, drafts bool) error {
18 | return util.CopyFile(a.Path, dest)
19 | }
20 |
--------------------------------------------------------------------------------
/formats/markdown/markdown.go:
--------------------------------------------------------------------------------
1 | package markdown
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | gotmpl "text/template"
9 | "time"
10 |
11 | "github.com/adrg/frontmatter"
12 | "tangled.sh/icyphox.sh/vite/config"
13 | "tangled.sh/icyphox.sh/vite/template"
14 | "tangled.sh/icyphox.sh/vite/types"
15 |
16 | bf "git.icyphox.sh/grayfriday"
17 | )
18 |
19 | var (
20 | bfFlags = bf.UseXHTML | bf.Smartypants | bf.SmartypantsFractions |
21 | bf.SmartypantsDashes | bf.NofollowLinks | bf.FootnoteReturnLinks
22 | bfExts = bf.NoIntraEmphasis | bf.Tables | bf.FencedCode | bf.Autolink |
23 | bf.Strikethrough | bf.SpaceHeadings | bf.BackslashLineBreak |
24 | bf.AutoHeadingIDs | bf.HeadingIDs | bf.Footnotes | bf.NoEmptyLineBeforeBlock
25 | )
26 |
27 | type Markdown struct {
28 | body []byte
29 | frontmatter map[string]any
30 | Path string
31 | }
32 |
33 | func (*Markdown) Ext() string { return ".md" }
34 |
35 | func (md *Markdown) Basename() string {
36 | return filepath.Base(md.Path)
37 | }
38 |
39 | // mdToHtml renders source markdown to html
40 | func mdToHtml(source []byte) []byte {
41 | return bf.Run(
42 | source,
43 | bf.WithNoExtensions(),
44 | bf.WithRenderer(bf.NewHTMLRenderer(bf.HTMLRendererParameters{Flags: bfFlags})),
45 | bf.WithExtensions(bfExts),
46 | )
47 | }
48 |
49 | // template checks the frontmatter for a specified template or falls back
50 | // to the default template -- to which it, well, templates whatever is in
51 | // data and writes it to dest.
52 | func (md *Markdown) template(dest, tmplDir string, data any) error {
53 | metaTemplate, ok := md.frontmatter["template"].(string)
54 | if !ok || metaTemplate == "" {
55 | metaTemplate = config.Config.DefaultTemplate
56 | }
57 |
58 | tmpl := template.NewTmpl()
59 | tmpl.SetFuncs(gotmpl.FuncMap{
60 | "parsedate": func(s string) time.Time {
61 | date, _ := time.Parse("2006-01-02", s)
62 | return date
63 | },
64 | })
65 | if err := tmpl.Load(tmplDir); err != nil {
66 | return err
67 | }
68 |
69 | return tmpl.Write(dest, metaTemplate, data)
70 | }
71 |
72 | // extractFrontmatter takes the source markdown page, extracts the frontmatter
73 | // and body. The body is converted from markdown to html here.
74 | func (md *Markdown) extractFrontmatter(source []byte) error {
75 | r := bytes.NewReader(source)
76 | rest, err := frontmatter.Parse(r, &md.frontmatter)
77 | if err != nil {
78 | return err
79 | }
80 | md.body = mdToHtml(rest)
81 | return nil
82 | }
83 |
84 | func (md *Markdown) Frontmatter() map[string]any {
85 | return md.frontmatter
86 | }
87 |
88 | func (md *Markdown) Body() string {
89 | return string(md.body)
90 | }
91 |
92 | type templateData struct {
93 | Cfg config.ConfigYaml
94 | Meta map[string]any
95 | Body string
96 | Extra any
97 | }
98 |
99 | func (md *Markdown) Render(dest string, data any, drafts bool) error {
100 | source, err := os.ReadFile(md.Path)
101 | if err != nil {
102 | return fmt.Errorf("markdown: error reading file: %w", err)
103 | }
104 |
105 | err = md.extractFrontmatter(source)
106 | if err != nil {
107 | return fmt.Errorf("markdown: error extracting frontmatter: %w", err)
108 | }
109 |
110 | isDraft, ok := md.frontmatter["draft"].(bool)
111 | if ok && isDraft {
112 | if !drafts {
113 | fmt.Printf("vite: skipping draft %s\n", md.Path)
114 | return nil
115 | }
116 | fmt.Printf("vite: rendering draft %s\n", md.Path)
117 | }
118 |
119 | err = md.template(dest, types.TemplatesDir, templateData{
120 | config.Config,
121 | md.frontmatter,
122 | string(md.body),
123 | data,
124 | })
125 | if err != nil {
126 | return fmt.Errorf("markdown: failed to render to destination %s: %w", dest, err)
127 | }
128 | return nil
129 | }
130 |
--------------------------------------------------------------------------------
/formats/yaml/yaml.go:
--------------------------------------------------------------------------------
1 | package yaml
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | gotmpl "text/template"
8 | "time"
9 |
10 | "gopkg.in/yaml.v3"
11 | "tangled.sh/icyphox.sh/vite/config"
12 | "tangled.sh/icyphox.sh/vite/template"
13 | "tangled.sh/icyphox.sh/vite/types"
14 | )
15 |
16 | type YAML struct {
17 | Path string
18 |
19 | meta map[string]any
20 | }
21 |
22 | func (*YAML) Ext() string { return ".yaml" }
23 | func (*YAML) Body() string { return "" }
24 | func (y *YAML) Basename() string { return filepath.Base(y.Path) }
25 |
26 | func (y *YAML) Frontmatter() map[string]any {
27 | return y.meta
28 | }
29 |
30 | type templateData struct {
31 | Cfg config.ConfigYaml
32 | Meta map[string]any
33 | Yaml map[string]any
34 | Body string
35 | }
36 |
37 | func (y *YAML) template(dest, tmplDir string, data any) error {
38 | var metaTemplate string
39 | if templateVal, ok := y.meta["template"]; ok {
40 | if strVal, isStr := templateVal.(string); isStr {
41 | metaTemplate = strVal
42 | }
43 | }
44 | if metaTemplate == "" {
45 | metaTemplate = config.Config.DefaultTemplate
46 | }
47 |
48 | tmpl := template.NewTmpl()
49 | tmpl.SetFuncs(gotmpl.FuncMap{
50 | "parsedate": func(s string) time.Time {
51 | date, _ := time.Parse("2006-01-02", s)
52 | return date
53 | },
54 | })
55 | if err := tmpl.Load(tmplDir); err != nil {
56 | return err
57 | }
58 |
59 | return tmpl.Write(dest, metaTemplate, data)
60 | }
61 |
62 | func (y *YAML) Render(dest string, data any, drafts bool) error {
63 | yamlBytes, err := os.ReadFile(y.Path)
64 | if err != nil {
65 | return fmt.Errorf("yaml: failed to read file: %s: %w", y.Path, err)
66 | }
67 |
68 | yamlData := map[string]any{}
69 | err = yaml.Unmarshal(yamlBytes, yamlData)
70 | if err != nil {
71 | return fmt.Errorf("yaml: failed to unmarshal yaml file: %s: %w", y.Path, err)
72 | }
73 |
74 | metaInterface, ok := yamlData["meta"].(map[string]any)
75 | if !ok {
76 | return fmt.Errorf("yaml: meta section is not a map: %s", y.Path)
77 | }
78 |
79 | stringMeta := make(map[string]any)
80 | for k, v := range metaInterface {
81 | stringMeta[k] = convertToString(v)
82 | }
83 |
84 | y.meta = stringMeta
85 |
86 | err = y.template(dest, types.TemplatesDir, templateData{
87 | config.Config,
88 | y.meta,
89 | yamlData,
90 | "",
91 | })
92 | if err != nil {
93 | return fmt.Errorf("yaml: failed to render to destination %s: %w", dest, err)
94 | }
95 |
96 | return nil
97 | }
98 |
99 | func convertToString(value any) string {
100 | // Infer type and convert to string
101 | switch v := value.(type) {
102 | case string:
103 | return v
104 | case time.Time:
105 | return v.Format("2006-01-02")
106 | default:
107 | return fmt.Sprintf("%v", v)
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module tangled.sh/icyphox.sh/vite
2 |
3 | go 1.22
4 |
5 | toolchain go1.24.1
6 |
7 | require (
8 | git.icyphox.sh/grayfriday v0.0.0-20221126034429-23c704183914
9 | github.com/adrg/frontmatter v0.2.0
10 | github.com/alecthomas/chroma v0.10.0
11 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
12 | )
13 |
14 | require (
15 | github.com/BurntSushi/toml v0.3.1 // indirect
16 | github.com/alecthomas/chroma/v2 v2.16.0 // indirect
17 | github.com/dlclark/regexp2 v1.11.5 // indirect
18 | gopkg.in/yaml.v2 v2.3.0 // indirect
19 | )
20 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | git.icyphox.sh/grayfriday v0.0.0-20221126034429-23c704183914 h1:4f0PFapEZUYls6gEjWMA82jOm0s5E4I9p23QpXv1sSg=
2 | git.icyphox.sh/grayfriday v0.0.0-20221126034429-23c704183914/go.mod h1:/wTbXjiiGlIYuqC6rVyD9ml88NWw7tujy3pOqj8kkKc=
3 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
5 | github.com/adrg/frontmatter v0.2.0 h1:/DgnNe82o03riBd1S+ZDjd43wAmC6W35q67NHeLkPd4=
6 | github.com/adrg/frontmatter v0.2.0/go.mod h1:93rQCj3z3ZlwyxxpQioRKC1wDLto4aXHrbqIsnH9wmE=
7 | github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
8 | github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
9 | github.com/alecthomas/chroma/v2 v2.16.0 h1:QC5ZMizk67+HzxFDjQ4ASjni5kWBTGiigRG1u23IGvA=
10 | github.com/alecthomas/chroma/v2 v2.16.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14 | github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
15 | github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
16 | github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
17 | github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
21 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
22 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
25 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
26 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
27 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
28 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
29 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
30 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Anirudh Oppiliappan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a
6 | copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included
14 | in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
24 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "tangled.sh/icyphox.sh/vite/commands"
8 | )
9 |
10 | func main() {
11 | args := os.Args
12 |
13 | helpStr := `usage: vite [options]
14 |
15 | A simple and minimal static site generator.
16 |
17 | options:
18 | init PATH create vite project at PATH
19 | build [--drafts] builds the current project
20 | new PATH create a new markdown post
21 | serve [HOST:PORT] serves the 'build' directory
22 | `
23 |
24 | if len(args) <= 1 {
25 | fmt.Println(helpStr)
26 | return
27 | }
28 |
29 | switch args[1] {
30 | case "init":
31 | if len(args) <= 2 {
32 | fmt.Println(helpStr)
33 | return
34 | }
35 | initPath := args[2]
36 | if err := commands.Init(initPath); err != nil {
37 | fmt.Fprintf(os.Stderr, "error: init: %+v\n", err)
38 | }
39 |
40 | case "build":
41 | var drafts bool
42 | if len(args) > 2 && args[2] == "--drafts" {
43 | drafts = true
44 | }
45 | if err := commands.Build(drafts); err != nil {
46 | fmt.Fprintf(os.Stderr, "error: build: %+v\n", err)
47 | }
48 |
49 | case "new":
50 | if len(args) <= 2 {
51 | fmt.Println(helpStr)
52 | return
53 | }
54 | newPath := args[2]
55 | if err := commands.New(newPath); err != nil {
56 | fmt.Fprintf(os.Stderr, "error: new: %+v\n", err)
57 | }
58 | case "serve":
59 | var addr string
60 | if len(args) == 3 {
61 | addr = args[2]
62 | } else {
63 | addr = ":9191"
64 | }
65 | if err := commands.Serve(addr); err != nil {
66 | fmt.Fprintf(os.Stderr, "error: serve: %+v\n", err)
67 | }
68 | default:
69 | fmt.Println(helpStr)
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | default:
2 | go build -o vite
3 |
4 | install:
5 | install -Dm755 vite $(DESTDIR)$(PREFIX)/bin/vite
6 |
7 | uninstall:
8 | @rm -f $(DESTDIR)$(PREFIX)/bin/vite
9 |
--------------------------------------------------------------------------------
/readme:
--------------------------------------------------------------------------------
1 | vite
2 | ----
3 |
4 | A fast (this time, actually) and minimal static site generator.
5 |
6 | INSTALLING
7 |
8 | go install tangled.sh/icyphox.sh/vite@latest
9 |
10 |
11 | USAGE
12 |
13 | usage: vite [options]
14 |
15 | A simple and minimal static site generator.
16 |
17 | options:
18 | init PATH create vite project at PATH
19 | build builds the current project
20 | new PATH create a new markdown post
21 | serve [HOST:PORT] serves the 'build' directory
22 |
23 |
24 | CONFIG
25 |
26 | The configuration is unmarshalled from a config.yaml file, into the
27 | below struct:
28 |
29 | type ConfigYaml struct {
30 | Title string `yaml:"title"`
31 | Desc string `yaml:"description"`
32 | DefaultTemplate string `yaml:"-"`
33 | Author struct {
34 | Name string `yaml:"name"`
35 | Email string `yaml:"email"`
36 | } `yaml:"author"`
37 | URL string `yaml:"url"`
38 | PreBuild []string `yaml:"preBuild"`
39 | PostBuild []string `yaml:"postBuild"`
40 | }
41 |
42 | Example config: https://tangled.sh/@icyphox.sh/site/blob/master/config.yaml
43 |
44 |
45 | SYNTAX HIGHLIGHTING
46 |
47 | vite uses chroma (https://github.com/alecthomas/chroma) for syntax
48 | highlighting. Note that CSS is not provided, and will have to be
49 | included by the user in the templates. A sample style can be generated
50 | by running:
51 |
52 | go run contrib/style.go > syntax.css
53 |
54 |
55 | SPECIAL META DIRECTIVES
56 |
57 | • draft: sets a post to draft (boolean) and will only be rendered if
58 | the build command is run with the --drafts flag.
59 | • atroot: sets a post to be also rendered at the root of the site.
60 |
61 |
62 | TEMPLATING
63 |
64 | Non-index templates have access to the below objects:
65 | • Cfg: object of ConfigYaml
66 | • Meta: map[string]string of the page's frontmatter metadata
67 | • Body: Contains the HTML
68 |
69 |
70 | Index templates have access to everything above, and an Extra object,
71 | which is a slice of types.Post containing Body and Meta. This is useful
72 | for iterating through to generate an index page.
73 | Example: https://git.icyphox.sh/site/tree/templates/index.html
74 |
75 | Templates are written as standard Go templates (ref:
76 | https://godocs.io/text/template), and can be loaded recursively.
77 | Consider the below template structure:
78 |
79 | templates/
80 | |-- blog.html
81 | |-- index.html
82 | |-- project/
83 | |-- index.html
84 | `-- project.html
85 |
86 | The templates under project/ are referenced as project/index.html.
87 | This deserves mention because Go templates don't recurse into
88 | subdirectories by default (template.ParseGlob uses filepath.Glob, and
89 | doesn't support deep-matching, i.e. **).
90 |
91 | vite also supports templating generic YAML files. Take for instance,
92 | pages/reading.yaml (https://git.icyphox.sh/site/blob/master/pages/reading.yaml):
93 |
94 | meta:
95 | template: reading.html
96 | title: reading
97 | subtitle: Tracking my reading.
98 | description: I use this page to track my reading.
99 |
100 | books:
101 | - 2024:
102 | - name: Dune Messiah
103 | link: https://en.wikipedia.org/wiki/Dune_Messiah
104 | author: Frank Herbert
105 | status: now reading
106 | - 2023:
107 | - name: Dune
108 | link: https://en.wikipedia.org/wiki/Dune_(novel)
109 | author: Frank Herbert
110 | status: finished
111 |
112 | vite will look for a 'meta' key in the YAML file, and use the 'template'
113 | specified to render the page. The rest of the YAML file is available to
114 | you in the template as a map[string]interface{} called Yaml.
115 |
116 |
117 | More templating examples can be found at:
118 | https://git.icyphox.sh/site/tree/templates
119 |
120 |
121 | FEEDS
122 |
123 | Atom feeds are generated for all directories under pages/. So
124 | pages/foo will have a Atom feed at build/foo/feed.xml.
125 |
126 |
127 | FILE TREE
128 |
129 | .
130 | |-- build/
131 | |-- config.yaml
132 | |-- pages/
133 | |-- static/
134 | |-- templates/
135 |
136 | The entire static/ directory gets copied over to build/, and can be
137 | used to reference static assets -- css, images, etc. pages/ supports
138 | only nesting one directory deep; for example: pages/blog/*.md will
139 | render, but pages/blog/foo/*.md will not.
140 |
141 |
142 | BUGS
143 |
144 | Or rather, (undocumented) features. There's probably a couple. If you are
145 | actually using this, feel free to reach out and I can try to help.
146 |
--------------------------------------------------------------------------------
/template/template.go:
--------------------------------------------------------------------------------
1 | package template
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "strings"
7 | "text/template"
8 | )
9 |
10 | type Tmpl struct {
11 | *template.Template
12 | }
13 |
14 | func NewTmpl() *Tmpl {
15 | tmpl := Tmpl{}
16 | tmpl.Template = template.New("")
17 | return &tmpl
18 | }
19 |
20 | func (t *Tmpl) SetFuncs(funcMap template.FuncMap) {
21 | t.Template = t.Template.Funcs(funcMap)
22 | }
23 |
24 | func (t *Tmpl) Load(dir string) (err error) {
25 | if dir, err = filepath.Abs(dir); err != nil {
26 | return err
27 | }
28 |
29 | root := t.Template
30 |
31 | if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) (_ error) {
32 | if err != nil {
33 | return err
34 | }
35 |
36 | if filepath.Ext(path) != ".html" {
37 | return
38 | }
39 |
40 | var rel string
41 | if rel, err = filepath.Rel(dir, path); err != nil {
42 | return err
43 | }
44 |
45 | rel = strings.Join(strings.Split(rel, string(os.PathSeparator)), "/")
46 | newTmpl := root.New(rel)
47 |
48 | var b []byte
49 | if b, err = os.ReadFile(path); err != nil {
50 | return err
51 | }
52 |
53 | _, err = newTmpl.Parse(string(b))
54 | return err
55 | }); err != nil {
56 | return err
57 | }
58 | return nil
59 | }
60 |
61 | func (t *Tmpl) Write(dest string, name string, data interface{}) error {
62 | w, err := os.Create(dest)
63 | if err != nil {
64 | return err
65 | }
66 |
67 | return t.ExecuteTemplate(w, name, data)
68 | }
69 |
--------------------------------------------------------------------------------
/types/types.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | const (
4 | BuildDir = "build"
5 | PagesDir = "pages"
6 | TemplatesDir = "templates"
7 | StaticDir = "static"
8 | )
9 |
10 | type File interface {
11 | Ext() string
12 | // Render takes any arbitrary data and combines that with the global config,
13 | // page frontmatter and the body, as template params. Templates are read
14 | // from types.TemplateDir and the final html is written to dest,
15 | // with necessary directories being created.
16 | Render(dest string, data interface{}, drafts bool) error
17 |
18 | // Frontmatter will not be populated if Render hasn't been called.
19 | Frontmatter() map[string]any
20 | // Body will not be populated if Render hasn't been called.
21 | Body() string
22 | Basename() string
23 | }
24 |
25 | // Only used for building indexes and Atom feeds
26 | type Post struct {
27 | Meta map[string]any
28 | // HTML-formatted body of post
29 | Body string
30 | }
31 |
--------------------------------------------------------------------------------