mirror of
https://github.com/facundoolano/jorge.git
synced 2024-12-26 21:58:51 +01:00
Refactor CLI using kong (#13)
* use kong for cli parsing * fix kong usage * remove conditional * don't blow up serve if src dir is missing * load config in main side (boilerplaty) * fix weird names * add versions and aliases * fix version printing * replace command switch with Run methods * move subcommand structs to commands package * distribute commands into files * add usage to docs * add flags to configure server
This commit is contained in:
parent
4bc6867c91
commit
b3594be86c
10 changed files with 279 additions and 244 deletions
|
@ -3,158 +3,23 @@ package commands
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"embed"
|
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
"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
|
|
||||||
var initfiles embed.FS
|
|
||||||
|
|
||||||
var INIT_CONFIG string = `name: "%s"
|
|
||||||
author: "%s"
|
|
||||||
url: "%s"
|
|
||||||
`
|
|
||||||
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
|
const FILE_RW_MODE = 0777
|
||||||
|
|
||||||
// Initialize a new jorge project in the given directory,
|
type Build struct {
|
||||||
// prompting for basic site config and creating default files.
|
ProjectDir string `arg:"" name:"path" optional:"" default:"." help:"Path to the website project to build."`
|
||||||
func Init(projectDir string) error {
|
|
||||||
if err := ensureEmptyProjectDir(projectDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
siteName := Prompt("site name")
|
|
||||||
siteUrl := Prompt("site url")
|
|
||||||
siteAuthor := Prompt("author")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// 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).
|
|
||||||
configDir := filepath.Join(projectDir, "config.yml")
|
|
||||||
configFile := fmt.Sprintf(INIT_CONFIG, siteName, siteAuthor, siteUrl)
|
|
||||||
os.WriteFile(configDir, []byte(configFile), site.FILE_RW_MODE)
|
|
||||||
fmt.Println("added", configDir)
|
|
||||||
|
|
||||||
readmeDir := filepath.Join(projectDir, "README.md")
|
|
||||||
readmeFile := fmt.Sprintf(INIT_README, siteName, siteAuthor)
|
|
||||||
os.WriteFile(readmeDir, []byte(readmeFile), site.FILE_RW_MODE)
|
|
||||||
fmt.Println("added", readmeDir)
|
|
||||||
|
|
||||||
// walk over initfiles fs
|
|
||||||
// copy create directories and copy files at target
|
|
||||||
|
|
||||||
initfilesRoot := "initfiles"
|
|
||||||
return fs.WalkDir(initfiles, initfilesRoot, func(path string, entry fs.DirEntry, err error) error {
|
|
||||||
if path == initfilesRoot {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
subpath, _ := filepath.Rel(initfilesRoot, path)
|
|
||||||
targetPath := filepath.Join(projectDir, subpath)
|
|
||||||
|
|
||||||
// if it's a directory create it at the same location
|
|
||||||
if entry.IsDir() {
|
|
||||||
return os.MkdirAll(targetPath, FILE_RW_MODE)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO duplicated in site, extract to somewhere else
|
|
||||||
// if its a file, copy it over
|
|
||||||
targetFile, err := os.Create(targetPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer targetFile.Close()
|
|
||||||
|
|
||||||
source, err := initfiles.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer source.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(targetFile, source)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Println("added", targetPath)
|
|
||||||
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.Sprintf("%d", now.Year()))
|
|
||||||
filename = strings.ReplaceAll(filename, ":month", fmt.Sprintf("%02d", now.Month()))
|
|
||||||
filename = strings.ReplaceAll(filename, ":day", fmt.Sprintf("%02d", 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/
|
// Read the files in src/ render them and copy the result to target/
|
||||||
func Build(root string) error {
|
func (cmd *Build) Run(ctx *kong.Context) error {
|
||||||
config, err := config.Load(root)
|
config, err := config.Load(cmd.ProjectDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -181,39 +46,3 @@ func Prompt(label string) string {
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(s)
|
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
|
|
||||||
// https://stackoverflow.com/a/30708914/993769
|
|
||||||
if os.IsExist(err) {
|
|
||||||
// check if empty
|
|
||||||
dir, err := os.Open(projectDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer dir.Close()
|
|
||||||
|
|
||||||
// if directory is non empty, fail
|
|
||||||
_, err = dir.Readdirnames(1)
|
|
||||||
if err == nil {
|
|
||||||
return fmt.Errorf("non empty directory %s", projectDir)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
117
commands/init.go
Normal file
117
commands/init.go
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
|
"github.com/facundoolano/jorge/site"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed all:initfiles
|
||||||
|
var initfiles embed.FS
|
||||||
|
|
||||||
|
var INIT_CONFIG string = `name: "%s"
|
||||||
|
author: "%s"
|
||||||
|
url: "%s"
|
||||||
|
`
|
||||||
|
var INIT_README string = `
|
||||||
|
# %s
|
||||||
|
|
||||||
|
A jorge blog by %s.
|
||||||
|
`
|
||||||
|
|
||||||
|
type Init struct {
|
||||||
|
ProjectDir string `arg:"" name:"path" help:"Directory where to initialize the website project."`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a new jorge project in the given directory,
|
||||||
|
// prompting for basic site config and creating default files.
|
||||||
|
func (cmd *Init) Run(ctx *kong.Context) error {
|
||||||
|
if err := ensureEmptyProjectDir(cmd.ProjectDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
siteName := Prompt("site name")
|
||||||
|
siteUrl := Prompt("site url")
|
||||||
|
siteAuthor := Prompt("author")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// 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).
|
||||||
|
configPath := filepath.Join(cmd.ProjectDir, "config.yml")
|
||||||
|
configFile := fmt.Sprintf(INIT_CONFIG, siteName, siteAuthor, siteUrl)
|
||||||
|
os.WriteFile(configPath, []byte(configFile), site.FILE_RW_MODE)
|
||||||
|
fmt.Println("added", configPath)
|
||||||
|
|
||||||
|
readmePath := filepath.Join(cmd.ProjectDir, "README.md")
|
||||||
|
readmeFile := fmt.Sprintf(INIT_README, siteName, siteAuthor)
|
||||||
|
os.WriteFile(readmePath, []byte(readmeFile), site.FILE_RW_MODE)
|
||||||
|
fmt.Println("added", readmePath)
|
||||||
|
|
||||||
|
// walk over initfiles fs
|
||||||
|
// copy create directories and copy files at target
|
||||||
|
|
||||||
|
initfilesRoot := "initfiles"
|
||||||
|
return fs.WalkDir(initfiles, initfilesRoot, func(path string, entry fs.DirEntry, err error) error {
|
||||||
|
if path == initfilesRoot {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
subpath, _ := filepath.Rel(initfilesRoot, path)
|
||||||
|
targetPath := filepath.Join(cmd.ProjectDir, subpath)
|
||||||
|
|
||||||
|
// if it's a directory create it at the same location
|
||||||
|
if entry.IsDir() {
|
||||||
|
return os.MkdirAll(targetPath, FILE_RW_MODE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO duplicated in site, extract to somewhere else
|
||||||
|
// if its a file, copy it over
|
||||||
|
targetFile, err := os.Create(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer targetFile.Close()
|
||||||
|
|
||||||
|
source, err := initfiles.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer source.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(targetFile, source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("added", targetPath)
|
||||||
|
return targetFile.Sync()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// https://stackoverflow.com/a/30708914/993769
|
||||||
|
if os.IsExist(err) {
|
||||||
|
// check if empty
|
||||||
|
dir, err := os.Open(projectDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dir.Close()
|
||||||
|
|
||||||
|
// if directory is non empty, fail
|
||||||
|
_, err = dir.Readdirnames(1)
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("non empty directory %s", projectDir)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
91
commands/post.go
Normal file
91
commands/post.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
|
"github.com/facundoolano/jorge/config"
|
||||||
|
"golang.org/x/text/unicode/norm"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
`
|
||||||
|
|
||||||
|
type Post struct {
|
||||||
|
Title string `arg:"" optional:"" help:"Title of the post"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new post template in the given site, with the given title,
|
||||||
|
// with pre-filled front matter.
|
||||||
|
func (cmd *Post) Run(ctx *kong.Context) error {
|
||||||
|
title := cmd.Title
|
||||||
|
if title == "" {
|
||||||
|
title = Prompt("title")
|
||||||
|
}
|
||||||
|
config, err := config.Load(".")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
slug := slugify(title)
|
||||||
|
filename := strings.ReplaceAll(config.PostFormat, ":title", slug)
|
||||||
|
|
||||||
|
filename = strings.ReplaceAll(filename, ":year", fmt.Sprintf("%d", now.Year()))
|
||||||
|
filename = strings.ReplaceAll(filename, ":month", fmt.Sprintf("%02d", now.Month()))
|
||||||
|
filename = strings.ReplaceAll(filename, ":day", fmt.Sprintf("%02d", 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -9,19 +9,29 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
"github.com/facundoolano/jorge/config"
|
"github.com/facundoolano/jorge/config"
|
||||||
"github.com/facundoolano/jorge/site"
|
"github.com/facundoolano/jorge/site"
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Generate and serve the site, rebuilding when the source files change
|
type Serve struct {
|
||||||
// and triggering a page refresh on clients browsing it.
|
ProjectDir string `arg:"" name:"path" optional:"" default:"." help:"Path to the website project to serve."`
|
||||||
func Serve(rootDir string) error {
|
Host string `short:"h" default:"localhost" help:"Host to run the server on."`
|
||||||
config, err := config.LoadDev(rootDir)
|
Port int `short:"p" default:"4001" help:"Port to run the server on."`
|
||||||
|
NoReload bool `help:"Disable live reloading."`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Serve) Run(ctx *kong.Context) error {
|
||||||
|
config, err := config.LoadDev(cmd.ProjectDir, cmd.Host, cmd.Port, !cmd.NoReload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(config.SrcDir); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("missing src directory")
|
||||||
|
}
|
||||||
|
|
||||||
// watch for changes in src and layouts, and trigger a rebuild
|
// watch for changes in src and layouts, and trigger a rebuild
|
||||||
watcher, broker, err := setupWatcher(config)
|
watcher, broker, err := setupWatcher(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -92,7 +92,7 @@ func Load(rootDir string) (*Config, error) {
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadDev(rootDir string) (*Config, error) {
|
func LoadDev(rootDir string, host string, port int, reload bool) (*Config, error) {
|
||||||
// TODO revisit is this Load vs LoadDevServer is the best way to handle both modes
|
// TODO revisit is this Load vs LoadDevServer is the best way to handle both modes
|
||||||
// TODO some of the options need to be overridable: host, port, live reload at least
|
// TODO some of the options need to be overridable: host, port, live reload at least
|
||||||
|
|
||||||
|
@ -102,10 +102,10 @@ func LoadDev(rootDir string) (*Config, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup serve command specific overrides (these could be eventually tweaked with flags)
|
// setup serve command specific overrides (these could be eventually tweaked with flags)
|
||||||
config.ServerHost = "localhost"
|
config.ServerHost = host
|
||||||
config.ServerPort = 4001
|
config.ServerPort = port
|
||||||
|
config.LiveReload = reload
|
||||||
config.Minify = false
|
config.Minify = false
|
||||||
config.LiveReload = true
|
|
||||||
config.LinkStatic = true
|
config.LinkStatic = true
|
||||||
config.SiteUrl = fmt.Sprintf("http://%s:%d", config.ServerHost, config.ServerPort)
|
config.SiteUrl = fmt.Sprintf("http://%s:%d", config.ServerHost, config.ServerPort)
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,31 @@ $ go install github.com/facundoolano/jorge@latest
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
|
|
||||||
TODO: switch to cli and show usage output
|
Once installed, the help command will provide an overview of what you can do with jorge:
|
||||||
|
|
||||||
|
#+begin_src
|
||||||
|
$ jorge -h
|
||||||
|
Usage: jorge <command>
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
init (i) <path>
|
||||||
|
Initialize a new website project.
|
||||||
|
|
||||||
|
build (b) [<path>]
|
||||||
|
Build a website project.
|
||||||
|
|
||||||
|
post (p) [<title>]
|
||||||
|
Initialize a new post template file.
|
||||||
|
|
||||||
|
serve (s) [<path>]
|
||||||
|
Run a local server for the website.
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h, --help Show context-sensitive help.
|
||||||
|
-v, --version
|
||||||
|
|
||||||
|
Run "jorge <command> --help" for more information on a command.
|
||||||
|
#+end_src
|
||||||
|
|
||||||
#+HTML: <br>
|
#+HTML: <br>
|
||||||
#+ATTR_HTML: :align right
|
#+ATTR_HTML: :align right
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -16,6 +16,7 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/alecthomas/kong v0.8.1 // indirect
|
||||||
github.com/osteele/tuesday v1.0.3 // indirect
|
github.com/osteele/tuesday v1.0.3 // indirect
|
||||||
github.com/tdewolff/parse/v2 v2.7.11 // indirect
|
github.com/tdewolff/parse/v2 v2.7.11 // indirect
|
||||||
golang.org/x/sys v0.16.0 // indirect
|
golang.org/x/sys v0.16.0 // indirect
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -1,3 +1,5 @@
|
||||||
|
github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY=
|
||||||
|
github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/elliotchance/orderedmap/v2 v2.2.0 h1:7/2iwO98kYT4XkOjA9mBEIwvi4KpGB4cyHeOFOnj4Vk=
|
github.com/elliotchance/orderedmap/v2 v2.2.0 h1:7/2iwO98kYT4XkOjA9mBEIwvi4KpGB4cyHeOFOnj4Vk=
|
||||||
|
|
70
main.go
70
main.go
|
@ -1,61 +1,25 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"github.com/alecthomas/kong"
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/facundoolano/jorge/commands"
|
"github.com/facundoolano/jorge/commands"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var cli struct {
|
||||||
|
Init commands.Init `cmd:"" help:"Initialize a new website project." aliases:"i"`
|
||||||
|
Build commands.Build `cmd:"" help:"Build a website project." aliases:"b"`
|
||||||
|
Post commands.Post `cmd:"" help:"Initialize a new post template file." help:"title of the new post." aliases:"p"`
|
||||||
|
Serve commands.Serve `cmd:"" help:"Run a local server for the website." aliases:"s"`
|
||||||
|
Version kong.VersionFlag `short:"v"`
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := run(os.Args)
|
ctx := kong.Parse(
|
||||||
|
&cli,
|
||||||
if err != nil {
|
kong.UsageOnError(),
|
||||||
fmt.Println("error:", err)
|
kong.HelpOptions{FlagsLast: true},
|
||||||
os.Exit(1)
|
kong.Vars{"version": "jorge v.0.1.2"},
|
||||||
}
|
)
|
||||||
}
|
err := ctx.Run()
|
||||||
|
ctx.FatalIfErrorf(err)
|
||||||
func run(args []string) error {
|
|
||||||
// TODO consider using cobra or something else to make cli more declarative
|
|
||||||
// and get a better ux out of the box
|
|
||||||
|
|
||||||
if len(os.Args) < 2 {
|
|
||||||
// TODO print usage
|
|
||||||
return errors.New("expected subcommand")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch os.Args[1] {
|
|
||||||
case "init":
|
|
||||||
if len(os.Args) < 3 {
|
|
||||||
return errors.New("project directory missing")
|
|
||||||
}
|
|
||||||
rootDir := os.Args[2]
|
|
||||||
return commands.Init(rootDir)
|
|
||||||
case "build":
|
|
||||||
rootDir := "."
|
|
||||||
if len(os.Args) > 2 {
|
|
||||||
rootDir = os.Args[2]
|
|
||||||
}
|
|
||||||
return commands.Build(rootDir)
|
|
||||||
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 {
|
|
||||||
rootDir = os.Args[2]
|
|
||||||
}
|
|
||||||
return commands.Serve(rootDir)
|
|
||||||
default:
|
|
||||||
// TODO print usage
|
|
||||||
return errors.New("unknown subcommand")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,14 +120,11 @@ func (site *Site) loadDataFiles() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (site *Site) loadTemplates() error {
|
func (site *Site) loadTemplates() error {
|
||||||
_, err := os.ReadDir(site.Config.SrcDir)
|
if _, err := os.Stat(site.Config.SrcDir); os.IsNotExist(err) {
|
||||||
if os.IsNotExist(err) {
|
return fmt.Errorf("missing src directory")
|
||||||
return fmt.Errorf("missing %s directory", site.Config.SrcDir)
|
|
||||||
} else if err != nil {
|
|
||||||
return fmt.Errorf("couldn't read %s", site.Config.SrcDir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = filepath.WalkDir(site.Config.SrcDir, func(path string, entry fs.DirEntry, err error) error {
|
err := filepath.WalkDir(site.Config.SrcDir, func(path string, entry fs.DirEntry, err error) error {
|
||||||
if !entry.IsDir() {
|
if !entry.IsDir() {
|
||||||
templ, err := templates.Parse(site.templateEngine, path)
|
templ, err := templates.Parse(site.templateEngine, path)
|
||||||
// if something fails or this is not a template, skip
|
// if something fails or this is not a template, skip
|
||||||
|
|
Loading…
Reference in a new issue