package main import ( "io/ioutil" "os" "path/filepath" "regexp" "strings" "text/template" "time" ) // Site represents a kiln site. type Site struct { Static map[string][]byte // Static files Directory *Directory // Site directory Templates *template.Template // Templates } // LoadSite loads and returns a Site. // It reads site content from the specified source directory. func LoadSite(srcDir string) (*Site, error) { tmpl, err := template.New("templates").ParseGlob("templates/*.gmi") if err != nil { return nil, err } site := &Site{ Static: map[string][]byte{}, Directory: NewDirectory(""), Templates: tmpl, } if err := site.Directory.Read(site, srcDir, ""); err != nil { return nil, err } return site, nil } // Write writes the contents of the Index to the provided destination directory. func (s *Site) Write(dstDir string) error { // Empty the destination directory if err := os.RemoveAll(dstDir); err != nil { return err } // Create the destination directory if err := os.MkdirAll(dstDir, 0755); err != nil { return err } // Write the static files for path := range s.Static { // Create any parent directories if dir := filepath.Dir(path); dir != "." { dstPath := filepath.Join(dstDir, dir) if err := os.MkdirAll(dstPath, 0755); err != nil { return err } } // Write the file dstPath := filepath.Join(dstDir, path) f, err := os.Create(dstPath) if err != nil { return err } data := s.Static[path] if _, err := f.Write(data); err != nil { return err } } // Write the directory return s.Directory.Write(dstDir) } // Manipulate processes and manipulates the site's content. func (s *Site) Manipulate(dir *Directory) error { // Write the directory index file, if it doesn't exist if dir.Index == nil { path := filepath.Join(dir.Path, "index.gmi") builder := &strings.Builder{} tmpl := s.Templates.Lookup("directory.gmi") if err := tmpl.Execute(builder, dir); err != nil { return err } content := builder.String() permalink := filepath.Dir(path) if permalink == "." { permalink = "" } page := &Page{ Path: path, Permalink: "/" + permalink, Content: content, } dir.Index = page } // Manipulate pages for i := range dir.Pages { builder := &strings.Builder{} tmpl := s.Templates.Lookup("page.gmi") if err := tmpl.Execute(builder, dir.Pages[i]); err != nil { return err } dir.Pages[i].Content = builder.String() } return nil } // Page represents a page. type Page struct { // The path to this page. Path string // The permalink to this page. Permalink string // The title of this page, parsed from the Gemini contents. Title string // The date of the page. Dates are specified in the filename. // Ex: 2020-09-22-hello-world.gmi Date time.Time // The content of this page. Content string } var titleRE = regexp.MustCompile("^# ?([^#\r\n]+)\r?\n?\r?\n?") // NewPage returns a new Page with the given path and content. func NewPage(path string, content string) *Page { // Try to parse the date from the page filename var date time.Time const layout = "2006-01-02" base := filepath.Base(path) if len(base) >= len(layout) { dateStr := base[:len(layout)] if time, err := time.Parse(layout, dateStr); err == nil { date = time } // Remove the date from the path base = base[len(layout):] if len(base) > 0 { // Remove a leading dash if base[0] == '-' { base = base[1:] } if len(base) > 0 { dir := filepath.Dir(path) if dir == "." { dir = "" } path = filepath.Join(dir, base) } } } // Try to parse the title from the contents var title string if submatches := titleRE.FindStringSubmatch(content); submatches != nil { title = submatches[1] // Remove the title from the contents content = content[len(submatches[0]):] } return &Page{ Path: path, Permalink: "/" + path, Title: title, Date: date, Content: content, } } // Directory represents a directory of pages. type Directory struct { // The path to this directory. Path string // The permalink to this directory. Permalink string // The pages in this directory. Pages []*Page // The subdirectories of this directory. Directories []*Directory // The index file (index.gmi). Index *Page } // NewDirectory returns a new Directory with the given path. func NewDirectory(path string) *Directory { var permalink string if path == "" { permalink = "/" } else { permalink = "/" + path + "/" } return &Directory{ Path: path, Permalink: permalink, } } // Read reads from a directory and indexes the files and directories within it. func (d *Directory) Read(site *Site, srcDir string, path string) error { entries, err := ioutil.ReadDir(srcDir) if err != nil { return err } for _, entry := range entries { name := entry.Name() path := filepath.Join(path, name) srcPath := filepath.Join(srcDir, path) if entry.IsDir() { // Gather directory data dir := NewDirectory(path) dir.Read(site, srcPath, path) d.Directories = append(d.Directories, dir) } else { content, err := ioutil.ReadFile(srcPath) if err != nil { return err } switch filepath.Ext(name) { case ".gmi", ".gemini": // Gather page data content := string(content) page := NewPage(path, content) if name == "index.gmi" { d.Index = page } else { d.Pages = append(d.Pages, page) } default: // Static file site.Static[path] = content } } } return nil } // Write writes the Directory to the provided destination path. func (d *Directory) Write(dstDir string) error { // Create the directory dirPath := filepath.Join(dstDir, d.Path) if err := os.MkdirAll(dirPath, 0755); err != nil { return err } // Write the files for _, page := range d.Pages { dstPath := filepath.Join(dstDir, page.Path) f, err := os.Create(dstPath) if err != nil { return err } data := []byte(page.Content) if _, err := f.Write(data); err != nil { return err } } // Write the index file if d.Index != nil { dstPath := filepath.Join(dstDir, d.Index.Path) f, err := os.Create(dstPath) if err != nil { return err } data := []byte(d.Index.Content) if _, err := f.Write(data); err != nil { return err } } // Write subdirectories for _, dir := range d.Directories { dir.Write(dstDir) } return nil }