2024-02-10 15:37:38 +01:00
|
|
|
package commands
|
|
|
|
|
|
|
|
import (
|
2024-02-17 21:09:19 +01:00
|
|
|
"bufio"
|
2024-02-10 15:37:38 +01:00
|
|
|
"fmt"
|
2024-02-17 21:09:19 +01:00
|
|
|
"io"
|
|
|
|
"io/fs"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2024-02-21 16:56:22 +01:00
|
|
|
"regexp"
|
2024-02-17 21:09:19 +01:00
|
|
|
"strings"
|
2024-02-21 16:56:22 +01:00
|
|
|
"time"
|
2024-02-17 21:09:19 +01:00
|
|
|
|
|
|
|
"embed"
|
2024-02-14 17:16:41 +01:00
|
|
|
|
2024-02-16 19:29:43 +01:00
|
|
|
"github.com/facundoolano/jorge/config"
|
|
|
|
"github.com/facundoolano/jorge/site"
|
2024-02-21 16:56:22 +01:00
|
|
|
"golang.org/x/text/unicode/norm"
|
2024-02-10 15:37:38 +01:00
|
|
|
)
|
|
|
|
|
2024-02-17 21:09:19 +01:00
|
|
|
//go:embed all:initfiles
|
|
|
|
var initfiles embed.FS
|
2024-02-21 16:56:22 +01:00
|
|
|
|
|
|
|
var INIT_CONFIG string = `name: "%s"
|
2024-02-17 21:09:19 +01:00
|
|
|
author: "%s"
|
|
|
|
url: "%s"
|
|
|
|
`
|
2024-02-21 16:56:22 +01:00
|
|
|
var INIT_README string = `
|
2024-02-17 21:09:19 +01:00
|
|
|
# %s
|
|
|
|
|
|
|
|
A jorge blog by %s.
|
|
|
|
`
|
2024-02-21 16:56:22 +01:00
|
|
|
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
|
|
|
|
`
|
2024-02-17 21:09:19 +01:00
|
|
|
|
|
|
|
const FILE_RW_MODE = 0777
|
|
|
|
|
2024-02-21 16:56:22 +01:00
|
|
|
// Initialize a new jorge project in the given directory,
|
|
|
|
// prompting for basic site config and creating default files.
|
2024-02-17 21:09:19 +01:00
|
|
|
func Init(projectDir string) error {
|
|
|
|
if err := ensureEmptyProjectDir(projectDir); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-02-21 16:56:22 +01:00
|
|
|
siteName := Prompt("site name")
|
|
|
|
siteUrl := Prompt("site url")
|
|
|
|
siteAuthor := Prompt("author")
|
2024-02-21 18:32:03 +01:00
|
|
|
fmt.Println()
|
2024-02-17 21:09:19 +01:00
|
|
|
|
|
|
|
// 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).
|
2024-02-21 16:56:22 +01:00
|
|
|
configFile := fmt.Sprintf(INIT_CONFIG, siteName, siteAuthor, siteUrl)
|
|
|
|
readmeFile := fmt.Sprintf(INIT_README, siteName, siteAuthor)
|
2024-02-17 21:09:19 +01:00
|
|
|
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)
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2024-02-21 18:32:03 +01:00
|
|
|
fmt.Println("added", targetPath)
|
2024-02-17 21:09:19 +01:00
|
|
|
return targetFile.Sync()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-02-21 16:56:22 +01:00
|
|
|
// 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
|
|
|
|
}
|
2024-02-21 22:41:03 +01:00
|
|
|
fmt.Println("added", path)
|
2024-02-21 16:56:22 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2024-02-17 21:09:19 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2024-02-10 15:37:38 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-02-21 16:56:22 +01:00
|
|
|
var nonWordRegex = regexp.MustCompile(`[^\w-]`)
|
|
|
|
var whitespaceRegex = regexp.MustCompile(`\s+`)
|
2024-02-15 16:38:17 +01:00
|
|
|
|
2024-02-21 16:56:22 +01:00
|
|
|
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, "")
|
2024-02-15 03:54:46 +01:00
|
|
|
|
2024-02-21 16:56:22 +01:00
|
|
|
return slug
|
2024-02-15 03:54:46 +01:00
|
|
|
}
|