diff --git a/commands/commands.go b/commands/commands.go index 14c7688..739df27 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -1,21 +1,127 @@ package commands import ( + "bufio" "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + + "embed" "github.com/facundoolano/jorge/config" "github.com/facundoolano/jorge/site" ) -func Init() error { - // get working directory - // default to . - // if not exist, create directory - // copy over default files - fmt.Println("not implemented yet") +//go:embed all:initfiles +var initfiles embed.FS +var initConfig string = `name: "%s" +author: "%s" +url: "%s" +` +var initReadme string = ` +# %s + +A jorge blog by %s. +` + +const FILE_RW_MODE = 0777 + +func Init(projectDir string) error { + if err := ensureEmptyProjectDir(projectDir); err != nil { + return err + } + + 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) + 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 + } + 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 } +// 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 diff --git a/commands/initfiles/.gitignore b/commands/initfiles/.gitignore new file mode 100644 index 0000000..228c813 --- /dev/null +++ b/commands/initfiles/.gitignore @@ -0,0 +1,3 @@ +target +.DS_Store + diff --git a/commands/initfiles/includes/post_preview.html b/commands/initfiles/includes/post_preview.html new file mode 100644 index 0000000..7ea5ad1 --- /dev/null +++ b/commands/initfiles/includes/post_preview.html @@ -0,0 +1,14 @@ +
+ {{ post.date | date: "%Y-%m-%d" }} +
+ {{ post.title }} + + {% if post.favorite %} {% endif %} + + {% for tag in post.tags %} + #{{tag}} + {% endfor %} + + +
+
diff --git a/commands/initfiles/layouts/base.html b/commands/initfiles/layouts/base.html new file mode 100644 index 0000000..f9192f2 --- /dev/null +++ b/commands/initfiles/layouts/base.html @@ -0,0 +1,51 @@ +--- +--- + + + + + + {% if page.title %} + {{page.head_title|default: page.title }} | {{ site.config.name }} + {% else %} + {{ site.config.name }} + {% endif %} + + + + + + + + {% if page.title %} + + + {% endif %} + + {% if page.excerpt %} + + + + {% endif %} + + {% if page.layout == "post" %} + + + + + {% else %} + + + + {% endif %} + + {% if page.image != "" %} + + + {% endif %} + + + + {{ content }} + + diff --git a/commands/initfiles/layouts/default.html b/commands/initfiles/layouts/default.html new file mode 100644 index 0000000..7b10544 --- /dev/null +++ b/commands/initfiles/layouts/default.html @@ -0,0 +1,21 @@ +--- +layout: base +--- + + +
+ {% if page.title %}

{{page.title}}

{% endif %} + {{ content }} +
+
+ diff --git a/commands/initfiles/layouts/post.html b/commands/initfiles/layouts/post.html new file mode 100644 index 0000000..81cc4bb --- /dev/null +++ b/commands/initfiles/layouts/post.html @@ -0,0 +1,32 @@ +--- +layout: base +--- + + +
+
+

{{ page.title }}

+ {% if page.subtitle %}

{{ page.subtitle }}

{% endif %} + {% if page.cover-img %} + + {% endif %} +
+ {{ content }} +
+ +
+
+ + diff --git a/commands/initfiles/src/assets/css/main.css b/commands/initfiles/src/assets/css/main.css new file mode 100644 index 0000000..2c1ec94 --- /dev/null +++ b/commands/initfiles/src/assets/css/main.css @@ -0,0 +1,223 @@ +html { + color-scheme: light dark; + overflow-y: scroll; +} + +body { + max-width: 45em; + margin: 0 auto; + padding: 0 1rem; + width: auto; + font-family: Tahoma, Verdana, Arial, sans-serif; + line-height: 1.5; + + height: 100vh; + display: flex; + flex-direction: column; +} + +.footer { + padding-top: 2rem; + margin-top: auto; +} + +@media screen and (max-width: 480px) { + .hidden-mobile { + display: none + } +} + +nav { + margin: 1rem 0; + line-height: 1.8; + border-bottom: 1px solid; + display: flex; +} +.nav-right { + margin-left: auto; +} +nav a:not(:last-child) { + margin-right: 1rem +} +nav a:hover { + text-decoration: none; +} + +li { + line-height: 1.6; +} + +li:not(:last-child) { + padding-bottom: .75rem; +} + +a, a:visited { + color: LinkText; + text-decoration: none; +} +a:hover { + text-decoration: underline +} + +a.title { + color: unset!important; + padding-right: .25rem; +} + +.footer, .footer a, .date, .tags, .tags a { + color: silver; +} +.date { + padding-right: .5rem; + white-space: nowrap; +} +.tags { + display: inline-block; +} + +article.post { + display: flex; + padding-bottom: .5rem; +} + +.center-block { + text-align: center; +} + +hr { + border: 0; + border-top:1px solid; +} + +/* tweaks for post content style */ +.layout-post img { + max-width: 75%; + max-height: 400px; +} + +img.cover-img { + width: 100%; + max-width: 100%; + max-height: 200px; + object-fit: cover; + margin-bottom: -2rem; +} + +.layout-post hr { + border: 1px solid silver; +} + +.layout-post { + hyphens: auto; + text-align: justify; + font-size: 1.15rem; + line-height: 1.6; + + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased !important; + -moz-font-smoothing: antialiased !important; + text-rendering: optimizelegibility !important; + letter-spacing: .03em; +} + +.layout-post .title { + hyphens:none; +} + +.layout-post header { + margin: 3rem 0 5rem; + text-align: left; +} + +.layout-post header.with-cover { + margin-top: -0.5rem; +} + +.src pre { + font-size: 1rem; + overflow-x: auto; + line-height: 1.5; + padding-left: 1rem +} + +blockquote { + border-left: 2px solid whitesmoke; + padding-left: 1rem; +} + +.layout-post p.verse { + text-align: right; +} + +.layout-post p { + line-height: 1.8; + margin-bottom: 1.5rem; +} + +.layout-post p + h2 { + padding-top: 1.5rem; +} + +.layout-post .center-block { + margin: 2rem 0; +} + +/* override in mobile devices for more compact text */ +@media screen and (max-width: 768px) { + .layout-post { + font-size: 1rem; + line-height: 1.2; + letter-spacing: unset; + hyphens: none; + text-align: left; + } + + .layout-post p { + margin: 0 0 1rem 0; + line-height: 1.7; + } + + .layout-post p + p { + text-indent: 0; + } + + .layout-post img { + max-width: 100%; + } +} + +hr.footnotes-separatator { + display:none; +} + +/* makes footnote number and text display in the same line */ +.footnote-definition { + display: block; + vertical-align: top; + margin-bottom: .4rem; +} +.footnote-body, .footnote-body p { + display: inline; +} + +/* These control the expand/collapse behavior of the tags page */ +details summary { + list-style: none; + cursor: pointer; +} + +details summary h3::after { + content: "[+]"; + font-size: small; + font-weight: normal; + font-family: monospace; + vertical-align: middle; +} + +details[open] summary h3::after { + content: "[-]" +} + +details > summary::-webkit-details-marker { + display: none; +} diff --git a/commands/initfiles/src/blog/goodbye-markdown.md b/commands/initfiles/src/blog/goodbye-markdown.md new file mode 100644 index 0000000..60509d3 --- /dev/null +++ b/commands/initfiles/src/blog/goodbye-markdown.md @@ -0,0 +1,13 @@ +--- +title: Goodbye Markdown... +tags: [blog] +date: 2024-02-16 +layout: post +--- + +## For the record + +For the record, even though it has *org* in the name, jorge can also render markdown, +thanks to [goldmark](https://github.com/yuin/goldmark/). + +[Next time](./hello-org), I'll talk about org-mode posts. diff --git a/commands/initfiles/src/blog/hello-org.org b/commands/initfiles/src/blog/hello-org.org new file mode 100644 index 0000000..456bca2 --- /dev/null +++ b/commands/initfiles/src/blog/hello-org.org @@ -0,0 +1,22 @@ +--- +title: Hello Org! +subtitle: Writing posts with org-mode syntax +tags: [blog, emacs] +date: 2024-02-17 +layout: post +--- +#+OPTIONS: toc:nil num:nil + +** Indeed + +This post was originally written with org-mode syntax, instead of [[file:goodbye-markdown][markdown]]. + +As you can see, /italics/ and *bold* render as expected, and you can even use footnotes[fn:1]. + +All of this is powered by [[https://github.com/niklasfasching/go-org][go-org]], btw[fn:2]. + +** Notes + +[fn:1] See? + +[fn:2] And another one footnote, to stay on the safe side. diff --git a/commands/initfiles/src/blog/index.html b/commands/initfiles/src/blog/index.html new file mode 100644 index 0000000..e54dce3 --- /dev/null +++ b/commands/initfiles/src/blog/index.html @@ -0,0 +1,15 @@ +--- +layout: default +submenu: [["/feed", "/feed.xml"], ["/tags", "/blog/tags"]] +title: Blog +--- + +{% assign posts_by_year = site.posts | group_by_exp:"post", "post.date | date: '%Y'" %} +{% for year in posts_by_year %} + {% unless forloop.first%} +
+ {% endunless %} + {% for post in year.items %} + {% include post_preview.html %} + {% endfor %} +{% endfor %} diff --git a/commands/initfiles/src/blog/tags.html b/commands/initfiles/src/blog/tags.html new file mode 100644 index 0000000..c967018 --- /dev/null +++ b/commands/initfiles/src/blog/tags.html @@ -0,0 +1,16 @@ +--- +layout: default +title: Tags +--- + +{% for tag in site.tags %} +
+ +

#{{tag[0]}}

+
+ + {% for post in tag[1] %} + {% include post_preview.html %} + {% endfor %} +
+{% endfor %} diff --git a/commands/initfiles/src/feed.xml b/commands/initfiles/src/feed.xml new file mode 100644 index 0000000..d61631d --- /dev/null +++ b/commands/initfiles/src/feed.xml @@ -0,0 +1,35 @@ +--- +--- + + + jorge + + + {{ "now" | date: "%Y-%m-%d %H:%M" }} + {{ page.url | absolute_url}} + {{ site.config.name }} + + {{ site.config.author }} + + {% for post in site.posts limit:10 %} + + {% assign post_title = post.title | strip_html | normalize_whitespace | xml_escape %} + {{ post.title }} + + {{ post.date | date: "%Y-%m-%d %H:%M" }} + {{ post.date | date: "%Y-%m-%d %H:%M" }} + {{ post.url | absolute_url }} + + {{ post.author | default:site.config.author }} + + {% for tag in post.tags %} + + {% endfor %} + + {% if post.image %} + + + {% endif %} + + {% endfor %} + diff --git a/commands/initfiles/src/index.html b/commands/initfiles/src/index.html new file mode 100644 index 0000000..76394f1 --- /dev/null +++ b/commands/initfiles/src/index.html @@ -0,0 +1,21 @@ +--- +layout: default +--- +

About

+

Welcome to {{ site.config.name }} by {{ site.config.author }}.

+
+ +

Latest posts

+ +{% for post in site.posts limit:3 %} +{% include post_preview.html %} +{% endfor %} + +

See the full blog archive or subscribe to the feed.

+
+ +

Contact

+ diff --git a/commands/serve.go b/commands/serve.go index f3fcffe..cfe491b 100644 --- a/commands/serve.go +++ b/commands/serve.go @@ -32,12 +32,12 @@ func Serve(rootDir string) error { defer watcher.Close() // serve the target dir with a file server - fs := http.FileServer(HTMLDir{http.Dir("target/")}) + fs := http.FileServer(HTMLDir{http.Dir(config.TargetDir)}) http.Handle("/", http.StripPrefix("/", fs)) - fmt.Println("server listening at http://localhost:4001/") - http.ListenAndServe(":4001", nil) - return nil + addr := fmt.Sprintf("%s:%d", config.ServerHost, config.ServerPort) + fmt.Printf("server listening at http://%s\n", addr) + return http.ListenAndServe(addr, nil) } func rebuild(config *config.Config) error { diff --git a/main.go b/main.go index e77d191..158266e 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "errors" - "flag" "fmt" "os" @@ -21,8 +20,6 @@ func main() { 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 - initCmd := flag.NewFlagSet("init", flag.ExitOnError) - newCmd := flag.NewFlagSet("new", flag.ExitOnError) if len(os.Args) < 2 { // TODO print usage @@ -31,8 +28,11 @@ func run(args []string) error { switch os.Args[1] { case "init": - initCmd.Parse(os.Args[2:]) - return commands.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 { @@ -40,7 +40,6 @@ func run(args []string) error { } return commands.Build(rootDir) case "new": - newCmd.Parse(os.Args[2:]) return commands.New() case "serve": rootDir := "."