Add post command (#9)

* rename command to post

* initial jorge post implementation

* fixes

* update comment

* move funs around

* move default front matter to constants

* more docs

* tweak defaults line breaks
This commit is contained in:
Facundo Olano 2024-02-21 12:56:22 -03:00 committed by GitHub
parent 2d52e13932
commit 20150cf7a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 131 additions and 52 deletions

View file

@ -7,42 +7,60 @@ import (
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"time"
"embed" "embed"
"github.com/facundoolano/jorge/config" "github.com/facundoolano/jorge/config"
"github.com/facundoolano/jorge/site" "github.com/facundoolano/jorge/site"
"golang.org/x/text/unicode/norm"
) )
//go:embed all:initfiles //go:embed all:initfiles
var initfiles embed.FS var initfiles embed.FS
var initConfig string = `name: "%s"
var INIT_CONFIG string = `name: "%s"
author: "%s" author: "%s"
url: "%s" url: "%s"
` `
var initReadme string = ` var INIT_README string = `
# %s # %s
A jorge blog by %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 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 { func Init(projectDir string) error {
if err := ensureEmptyProjectDir(projectDir); err != nil { if err := ensureEmptyProjectDir(projectDir); err != nil {
return err return err
} }
siteName := prompt("site name") siteName := Prompt("site name")
siteUrl := prompt("site url") siteUrl := Prompt("site url")
siteAuthor := prompt("author") siteAuthor := Prompt("author")
// creating config and readme files manually, since I want to use the supplied config values in their // 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 // 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). // are actual templates that should be left as is).
configFile := fmt.Sprintf(initConfig, siteName, siteAuthor, siteUrl) configFile := fmt.Sprintf(INIT_CONFIG, siteName, siteAuthor, siteUrl)
readmeFile := fmt.Sprintf(initReadme, siteName, siteAuthor) 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, "config.yml"), []byte(configFile), site.FILE_RW_MODE)
os.WriteFile(filepath.Join(projectDir, "README.md"), []byte(readmeFile), 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 { if err != nil {
return err return err
} }
fmt.Println("added", path)
return targetFile.Sync() 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 { func ensureEmptyProjectDir(projectDir string) error {
if err := os.Mkdir(projectDir, 0777); err != nil { if err := os.Mkdir(projectDir, 0777); err != nil {
// if it fails with dir already exist, check if it's empty // if it fails with dir already exist, check if it's empty
@ -107,42 +199,15 @@ func ensureEmptyProjectDir(projectDir string) error {
return nil return nil
} }
// Prompt the user for a string value var nonWordRegex = regexp.MustCompile(`[^\w-]`)
func prompt(label string) string { var whitespaceRegex = regexp.MustCompile(`\s+`)
// 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 { func slugify(title string) string {
// prompt for title slug := strings.ToLower(title)
// slugify slug = strings.TrimSpace(slug)
// fail if file already exist slug = norm.NFD.String(slug)
// create a new .org file with the slug slug = whitespaceRegex.ReplaceAllString(slug, "-")
// add front matter and org options slug = nonWordRegex.ReplaceAllString(slug, "")
fmt.Println("not implemented yet")
return nil
}
// Read the files in src/ render them and copy the result to target/ return slug
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()
} }

View file

@ -27,7 +27,8 @@ type Config struct {
DataDir string DataDir string
SiteUrl string SiteUrl string
SlugFormat string PostFormat string
Lang string
Minify bool Minify bool
LiveReload bool LiveReload bool
@ -53,7 +54,8 @@ func Load(rootDir string) (*Config, error) {
LayoutsDir: filepath.Join(rootDir, "layouts"), LayoutsDir: filepath.Join(rootDir, "layouts"),
IncludesDir: filepath.Join(rootDir, "includes"), IncludesDir: filepath.Join(rootDir, "includes"),
DataDir: filepath.Join(rootDir, "data"), DataDir: filepath.Join(rootDir, "data"),
SlugFormat: ":title", PostFormat: "blog/:title.org",
Lang: "en",
Minify: true, Minify: true,
LiveReload: false, LiveReload: false,
LinkStatic: false, LinkStatic: false,
@ -80,8 +82,11 @@ func Load(rootDir string) (*Config, error) {
if url, found := config.overrides["url"]; found { if url, found := config.overrides["url"]; found {
config.SiteUrl = url.(string) config.SiteUrl = url.(string)
} }
if slug, found := config.overrides["url"]; found { if format, found := config.overrides["post_format"]; found {
config.SlugFormat = slug.(string) config.PostFormat = format.(string)
}
if format, found := config.overrides["lang"]; found {
config.Lang = format.(string)
} }
return config, nil return config, nil

1
go.mod
View file

@ -10,6 +10,7 @@ require (
github.com/tdewolff/minify/v2 v2.20.16 github.com/tdewolff/minify/v2 v2.20.16
github.com/yuin/goldmark v1.7.0 github.com/yuin/goldmark v1.7.0
golang.org/x/net v0.0.0-20201224014010-6772e930b67b 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.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
) )

1
go.sum
View file

@ -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 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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/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/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

11
main.go
View file

@ -39,8 +39,15 @@ func run(args []string) error {
rootDir = os.Args[2] rootDir = os.Args[2]
} }
return commands.Build(rootDir) return commands.Build(rootDir)
case "new": case "post":
return commands.New() 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": case "serve":
rootDir := "." rootDir := "."
if len(os.Args) > 2 { if len(os.Args) > 2 {

View file

@ -338,7 +338,7 @@ func writeToFile(targetPath string, source io.Reader) error {
return err return err
} }
fmt.Println("added", targetPath) fmt.Println("wrote", targetPath)
return targetFile.Sync() return targetFile.Sync()
} }