diff --git a/commands/commands.go b/commands/commands.go index 739df27..726fce4 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -7,42 +7,60 @@ import ( "io/fs" "os" "path/filepath" + "regexp" "strings" + "time" "embed" "github.com/facundoolano/jorge/config" "github.com/facundoolano/jorge/site" + "golang.org/x/text/unicode/norm" ) //go:embed all:initfiles var initfiles embed.FS -var initConfig string = `name: "%s" + +var INIT_CONFIG string = `name: "%s" author: "%s" url: "%s" ` -var initReadme string = ` +var INIT_README string = ` # %s A jorge blog by %s. ` +var DEFAULT_FRONTMATTER string = `--- +title: %s +date: %s +layout: post +lang: %s +tags: [] +--- +` + +var DEFAULT_ORG_DIRECTIVES string = `#+OPTIONS: toc:nil num:nil +#+LANGUAGE: %s +` const FILE_RW_MODE = 0777 +// Initialize a new jorge project in the given directory, +// prompting for basic site config and creating default files. func Init(projectDir string) error { if err := ensureEmptyProjectDir(projectDir); err != nil { return err } - siteName := prompt("site name") - siteUrl := prompt("site url") - siteAuthor := prompt("author") + siteName := Prompt("site name") + siteUrl := Prompt("site url") + siteAuthor := Prompt("author") // creating config and readme files manually, since I want to use the supplied config values in their // contents. (I don't want to render liquid templates in the WalkDir below since some of the initfiles // are actual templates that should be left as is). - configFile := fmt.Sprintf(initConfig, siteName, siteAuthor, siteUrl) - readmeFile := fmt.Sprintf(initReadme, siteName, siteAuthor) + configFile := fmt.Sprintf(INIT_CONFIG, siteName, siteAuthor, siteUrl) + readmeFile := fmt.Sprintf(INIT_README, siteName, siteAuthor) os.WriteFile(filepath.Join(projectDir, "config.yml"), []byte(configFile), site.FILE_RW_MODE) os.WriteFile(filepath.Join(projectDir, "README.md"), []byte(readmeFile), site.FILE_RW_MODE) @@ -80,10 +98,84 @@ func Init(projectDir string) error { if err != nil { return err } + fmt.Println("added", path) return targetFile.Sync() }) } +// Create a new post template in the given site, with the given title, +// with pre-filled front matter. +func Post(root string, title string) error { + config, err := config.Load(root) + if err != nil { + return err + } + + now := time.Now() + slug := slugify(title) + filename := strings.ReplaceAll(config.PostFormat, ":title", slug) + filename = strings.ReplaceAll(filename, ":year", fmt.Sprint(now.Year())) + filename = strings.ReplaceAll(filename, ":month", fmt.Sprint(int(now.Month()))) + filename = strings.ReplaceAll(filename, ":day", fmt.Sprint(now.Day())) + path := filepath.Join(config.SrcDir, filename) + + // ensure the dir already exists + if err := os.MkdirAll(filepath.Dir(path), FILE_RW_MODE); err != nil { + return err + } + + // if file already exists, prompt user for a different one + if _, err := os.Stat(path); os.IsExist(err) { + fmt.Printf("%s already exists\n", path) + filename = Prompt("filename") + path = filepath.Join(config.SrcDir, filename) + } + + // initialize the post front matter + content := fmt.Sprintf(DEFAULT_FRONTMATTER, title, now.Format(time.DateTime), config.Lang) + + // org files need some extra boilerplate + if filepath.Ext(path) == ".org" { + content += fmt.Sprintf(DEFAULT_ORG_DIRECTIVES, config.Lang) + } + + if err := os.WriteFile(path, []byte(content), FILE_RW_MODE); err != nil { + return err + } + fmt.Println("added", path) + return nil +} + +// Read the files in src/ render them and copy the result to target/ +func Build(root string) error { + config, err := config.Load(root) + if err != nil { + return err + } + + site, err := site.Load(*config) + if err != nil { + return err + } + + return site.Build() +} + +// Prompt the user for a string value +func Prompt(label string) string { + // https://dev.to/tidalcloud/interactive-cli-prompts-in-go-3bj9 + var s string + r := bufio.NewReader(os.Stdin) + for { + fmt.Fprint(os.Stderr, label+": ") + s, _ = r.ReadString('\n') + if s != "" { + break + } + } + return strings.TrimSpace(s) +} + func ensureEmptyProjectDir(projectDir string) error { if err := os.Mkdir(projectDir, 0777); err != nil { // if it fails with dir already exist, check if it's empty @@ -107,42 +199,15 @@ func ensureEmptyProjectDir(projectDir string) error { return nil } -// Prompt the user for a string value -func prompt(label string) string { - // https://dev.to/tidalcloud/interactive-cli-prompts-in-go-3bj9 - var s string - r := bufio.NewReader(os.Stdin) - for { - fmt.Fprint(os.Stderr, label+": ") - s, _ = r.ReadString('\n') - if s != "" { - break - } - } - return strings.TrimSpace(s) -} - -func New() error { - // prompt for title - // slugify - // fail if file already exist - // create a new .org file with the slug - // add front matter and org options - fmt.Println("not implemented yet") - return nil -} - -// Read the files in src/ render them and copy the result to target/ -func Build(root string) error { - config, err := config.Load(root) - if err != nil { - return err - } - - site, err := site.Load(*config) - if err != nil { - return err - } - - return site.Build() +var nonWordRegex = regexp.MustCompile(`[^\w-]`) +var whitespaceRegex = regexp.MustCompile(`\s+`) + +func slugify(title string) string { + slug := strings.ToLower(title) + slug = strings.TrimSpace(slug) + slug = norm.NFD.String(slug) + slug = whitespaceRegex.ReplaceAllString(slug, "-") + slug = nonWordRegex.ReplaceAllString(slug, "") + + return slug } diff --git a/config/config.go b/config/config.go index 192f6c4..69af09d 100644 --- a/config/config.go +++ b/config/config.go @@ -27,7 +27,8 @@ type Config struct { DataDir string SiteUrl string - SlugFormat string + PostFormat string + Lang string Minify bool LiveReload bool @@ -53,7 +54,8 @@ func Load(rootDir string) (*Config, error) { LayoutsDir: filepath.Join(rootDir, "layouts"), IncludesDir: filepath.Join(rootDir, "includes"), DataDir: filepath.Join(rootDir, "data"), - SlugFormat: ":title", + PostFormat: "blog/:title.org", + Lang: "en", Minify: true, LiveReload: false, LinkStatic: false, @@ -80,8 +82,11 @@ func Load(rootDir string) (*Config, error) { if url, found := config.overrides["url"]; found { config.SiteUrl = url.(string) } - if slug, found := config.overrides["url"]; found { - config.SlugFormat = slug.(string) + if format, found := config.overrides["post_format"]; found { + config.PostFormat = format.(string) + } + if format, found := config.overrides["lang"]; found { + config.Lang = format.(string) } return config, nil diff --git a/go.mod b/go.mod index 3e445a9..544faed 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/tdewolff/minify/v2 v2.20.16 github.com/yuin/goldmark v1.7.0 golang.org/x/net v0.0.0-20201224014010-6772e930b67b + golang.org/x/text v0.3.3 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) diff --git a/go.sum b/go.sum index 8aa6335..c85cb3c 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/main.go b/main.go index 158266e..0ac514d 100644 --- a/main.go +++ b/main.go @@ -39,8 +39,15 @@ func run(args []string) error { rootDir = os.Args[2] } return commands.Build(rootDir) - case "new": - return commands.New() + case "post": + var title string + if len(os.Args) >= 3 { + title = os.Args[2] + } else { + title = commands.Prompt("title") + } + rootDir := "." + return commands.Post(rootDir, title) case "serve": rootDir := "." if len(os.Args) > 2 { diff --git a/site/site.go b/site/site.go index f119fdf..255d480 100644 --- a/site/site.go +++ b/site/site.go @@ -338,7 +338,7 @@ func writeToFile(targetPath string, source io.Reader) error { return err } - fmt.Println("added", targetPath) + fmt.Println("wrote", targetPath) return targetFile.Sync() }