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" }}
+
+
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 %}
+
+
+
+
+
+ {% 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
+---
+
+Welcome to {{ site.config.name }} by {{ site.config.author }}.
+
+
+
+
+{% for post in site.posts limit:3 %}
+{% include post_preview.html %}
+{% endfor %}
+
+See the full blog archive or subscribe to the feed.
+
+
+
+
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 := "."