diff options
Diffstat (limited to 'main.go')
| -rw-r--r-- | main.go | 110 |
1 files changed, 89 insertions, 21 deletions
@@ -30,6 +30,11 @@ import ( const manifestName = ".weft-generated.json" +const ( + gitignoreStart = "# BEGIN weft generated outputs" + gitignoreEnd = "# END weft generated outputs" +) + type Page struct { SourcePath string OutputPath string @@ -54,12 +59,13 @@ type source struct { } type site struct { - root string - pages map[string][]*Page - sources []source - templates *template.Template - fallback string - warnings []string + root string + canonicalRoot string + pages map[string][]*Page + sources []source + templates *template.Template + fallback string + warnings []string } type manifest struct { @@ -69,25 +75,24 @@ type manifest struct { func main() { flags := flag.NewFlagSet("weft", flag.ContinueOnError) fallback := flags.String("fallback", "page", "fallback template for Markdown pages") - flags.Usage = func() { fmt.Fprintln(flags.Output(), "usage: weft [-fallback template] <site-root>") } + manageGitignore := flags.Bool("gitignore", false, "manage generated outputs in the site .gitignore") + flags.Usage = func() { + fmt.Fprintln(flags.Output(), "usage: weft [-gitignore] [-fallback template] <site-root> <canonical-root>") + } if err := flags.Parse(os.Args[1:]); err != nil { os.Exit(2) } - if flags.NArg() != 1 { + if flags.NArg() != 2 { flags.Usage() os.Exit(2) } - if err := buildWithFallback(flags.Arg(0), *fallback); err != nil { + if err := buildWithFallback(flags.Arg(0), flags.Arg(1), *fallback, *manageGitignore); err != nil { fmt.Fprintln(os.Stderr, "weft:", err) os.Exit(1) } } -func build(root string) error { - return buildWithFallback(root, "page") -} - -func buildWithFallback(root, fallback string) error { +func buildWithFallback(root, canonicalRoot, fallback string, manageGitignore bool) error { if strings.TrimSpace(fallback) == "" { return errors.New("fallback template cannot be empty") } @@ -103,7 +108,7 @@ func buildWithFallback(root, fallback string) error { return fmt.Errorf("site root is not a directory: %s", abs) } - s := &site{root: abs, fallback: fallback, pages: map[string][]*Page{}} + s := &site{root: abs, canonicalRoot: canonicalRoot, fallback: fallback, pages: map[string][]*Page{}} if err := s.discover(); err != nil { return err } @@ -117,7 +122,7 @@ func buildWithFallback(root, fallback string) error { if err := s.validate(outputs); err != nil { return err } - if err := s.write(outputs); err != nil { + if err := s.write(outputs, manageGitignore); err != nil { return err } for _, warning := range s.warnings { @@ -248,7 +253,7 @@ func (s *site) parsePage(rel, out string, info fs.FileInfo) (*Page, []string, er } return &Page{ SourcePath: rel, OutputPath: out, URL: urlPath, - CanonicalURL: "https://tjp.lol" + urlPath, Title: title, Meta: meta, + CanonicalURL: s.canonicalRoot + urlPath, Title: title, Meta: meta, ModTime: info.ModTime(), Sections: sections, }, warnings, nil } @@ -392,7 +397,7 @@ func relativePath(from, target string) string { if err != nil { return target } - rel = filepath.ToSlash(rel) + rel = path.Clean(filepath.ToSlash(rel)) if directory { return strings.TrimSuffix(rel, "/") + "/" } @@ -484,7 +489,7 @@ func (s *site) render() (map[string][]byte, error) { current = src.page name = s.markdownTemplate(src.path) } else { - current = &Page{SourcePath: src.path, OutputPath: src.output, URL: "/" + src.output, CanonicalURL: "https://tjp.lol/" + src.output, Meta: map[string]any{}} + current = &Page{SourcePath: src.path, OutputPath: src.output, URL: "/" + src.output, CanonicalURL: s.canonicalRoot + src.output, Meta: map[string]any{}} } if s.templates.Lookup(name) == nil { return nil, fmt.Errorf("%s requires missing template %q", src.path, name) @@ -617,7 +622,7 @@ func (s *site) readManifest() (map[string]bool, error) { return tracked, nil } -func (s *site) write(outputs map[string][]byte) error { +func (s *site) write(outputs map[string][]byte, manageGitignore bool) error { tracked, err := s.readManifest() if err != nil { return err @@ -661,6 +666,15 @@ func (s *site) write(outputs map[string][]byte) error { if err := os.WriteFile(filepath.Join(stage, manifestName), manifestBytes, 0o644); err != nil { return err } + if manageGitignore { + contents, err := s.gitignore(paths) + if err != nil { + return err + } + if err := os.WriteFile(filepath.Join(stage, ".gitignore"), contents, 0o644); err != nil { + return err + } + } var movedOld, installed []string rollback := func() { @@ -699,7 +713,11 @@ func (s *site) write(outputs map[string][]byte) error { } } } - for _, rel := range append(paths, manifestName) { + install := append(slices.Clone(paths), manifestName) + if manageGitignore { + install = append(install, ".gitignore") + } + for _, rel := range install { if err := moveOld(rel); err != nil { rollback() return err @@ -717,3 +735,53 @@ func (s *site) write(outputs map[string][]byte) error { } return nil } + +func (s *site) gitignore(outputs []string) ([]byte, error) { + raw, err := os.ReadFile(filepath.Join(s.root, ".gitignore")) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return nil, err + } + var kept strings.Builder + inBlock, seen := false, false + for _, line := range strings.SplitAfter(string(raw), "\n") { + value := strings.TrimSuffix(strings.TrimSuffix(line, "\n"), "\r") + switch value { + case gitignoreStart: + if inBlock || seen { + return nil, errors.New("invalid managed block in .gitignore") + } + inBlock, seen = true, true + case gitignoreEnd: + if !inBlock { + return nil, errors.New("invalid managed block in .gitignore") + } + inBlock = false + default: + if !inBlock { + kept.WriteString(line) + } + } + } + if inBlock { + return nil, errors.New("unterminated managed block in .gitignore") + } + if kept.Len() > 0 && !strings.HasSuffix(kept.String(), "\n") { + kept.WriteByte('\n') + } + kept.WriteString(gitignoreStart + "\n") + for _, output := range outputs { + if strings.ContainsAny(output, "\r\n") { + return nil, fmt.Errorf("cannot add output to .gitignore: %q", output) + } + kept.WriteString("/" + escapeGitignore(output) + "\n") + } + kept.WriteString(gitignoreEnd + "\n") + return []byte(kept.String()), nil +} + +func escapeGitignore(value string) string { + return strings.NewReplacer( + `\`, `\\`, " ", `\ `, "#", `\#`, "!", `\!`, + "[", `\[`, "]", `\]`, "*", `\*`, "?", `\?`, + ).Replace(value) +} |
