]> Nishi Git Mirror - aya.git/commitdiff
Refactor a bunch of stuff adding zs serve, CI/CD workflows, fixing docs and license...
authorJames Mills <james@mills.io>
Sun, 12 Mar 2023 04:13:53 +0000 (04:13 +0000)
committerJames Mills <james@mills.io>
Sun, 12 Mar 2023 04:13:53 +0000 (04:13 +0000)
Fixes #5
Fixes #8
Fixes #9

Co-authored-by: James Mills <1290234+prologic@users.noreply.github.com>
Reviewed-on: https://git.mills.io/prologic/zs/pulls/10

12 files changed:
.dockerfiles/entrypoint.sh [new file with mode: 0755]
.drone.yml [new file with mode: 0644]
.gitignore
Dockerfile [new file with mode: 0644]
LICENSE
LICENSE.old [new file with mode: 0644]
Makefile
README.md
go.mod
go.sum
main.go
preflight.sh [new file with mode: 0755]

diff --git a/.dockerfiles/entrypoint.sh b/.dockerfiles/entrypoint.sh
new file mode 100755 (executable)
index 0000000..d4da611
--- /dev/null
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+[ -n "${PUID}" ] && usermod -u "${PUID}" zs
+[ -n "${PGID}" ] && groupmod -g "${PGID}" zs
+
+printf "Configuring zs...\n"
+
+printf "Switching UID=%s and GID=%s\n" "${PUID}" "${PGID}"
+exec su-exec zs:zs "$@"
diff --git a/.drone.yml b/.drone.yml
new file mode 100644 (file)
index 0000000..8c43c0e
--- /dev/null
@@ -0,0 +1,81 @@
+---
+kind: pipeline
+type: exec
+name: ๐Ÿš€ CI
+
+platform:
+  os: linux
+  arch: amd64
+
+steps:
+  - name: ๐Ÿ› ๏ธ Build
+    commands:
+      - make build
+
+  - name: ๐Ÿงช Test
+    commands:
+      - make test
+
+trigger:
+  branch:
+    - main
+  event:
+    - tag
+    - push
+    - pull_request
+
+---
+kind: pipeline
+name: ๐Ÿณ Docker
+
+steps:
+  - name: ๐Ÿ“ฆ Image
+    image: plugins/kaniko
+    settings:
+      repo: prologic/saltyim
+      tags: latest
+      build_args:
+        - VERSION=latest
+        - COMMIT=${DRONE_COMMIT_SHA:0:8}
+      username:
+        from_secret: dockerhub_username
+      password:
+        from_secret: dockerhub_password
+    when:
+      branch:
+        - main
+      event:
+        - push
+
+depends_on:
+  - ๐Ÿš€ CI
+
+trigger:
+  branch:
+    - main
+  event:
+    - push
+
+---
+kind: pipeline
+name: ๐Ÿฅณ Done
+
+steps:
+  - name: ๐Ÿ”” Notify
+    image: plugins/webhook
+    settings:
+      urls:
+        - https://msgbus.mills.io/ci.mills.io
+
+depends_on:
+  - ๐Ÿš€ CI
+  - ๐Ÿณ Docker
+
+trigger:
+  branch:
+    - main
+  event:
+    - tag
+    - push
+    - pull_request
+
index ec76799ed63578248a955108b299404da89cdfd0..9638f0d1512ad1b5bfc8f9c5b6203dbee810862f 100644 (file)
@@ -4,4 +4,4 @@
 
 /zs
 /dist
-/test.md
+**/.DS_Store
diff --git a/Dockerfile b/Dockerfile
new file mode 100644 (file)
index 0000000..c78862d
--- /dev/null
@@ -0,0 +1,60 @@
+# Build
+FROM golang:alpine AS build
+
+RUN apk add --no-cache -U build-base git
+
+RUN mkdir -p /src
+
+WORKDIR /src
+
+# Copy Makefile
+COPY Makefile ./
+
+# Install deps
+RUN make deps
+
+# Copy go.mod and go.sum and install and cache dependencies
+COPY go.mod .
+COPY go.sum .
+
+# Download dependencies
+RUN go mod download
+
+# Copy sources
+COPY *.go ./
+
+# Version/Commit (there there is no .git in Docker build context)
+# NOTE: This is fairly low down in the Dockerfile instructions so
+#       we don't break the Docker build cache just be changing
+#       unrelated files that actually haven't changed but caused the
+#       COMMIT value to change.
+ARG VERSION="0.0.0"
+ARG COMMIT="HEAD"
+ARG BUILD=""
+
+# Build cli binary
+RUN make cli VERSION=$VERSION COMMIT=$COMMIT BUILD=$BUILD
+
+# Runtime
+FROM alpine:latest
+
+RUN apk --no-cache -U add su-exec shadow
+
+ENV PUID=1000
+ENV PGID=1000
+
+RUN addgroup -g "${PGID}" zs && \
+    adduser -D -H -G zs -h /var/empty -u "${PUID}" zs && \
+    mkdir -p /data && chown -R zs:zs /data
+
+VOLUME /data
+
+WORKDIR /
+
+COPY --from=build /src/zs /usr/local/bin/zs
+
+COPY .dockerfiles/entrypoint.sh /init
+
+ENTRYPOINT ["/init"]
+
+CMD ["zs""]
diff --git a/LICENSE b/LICENSE
index c68b526a8c2f60ea4eebb53be51d44b928ff1a0d..bfe2bdd142f8a20d42b812b9004af7265365837c 100644 (file)
--- a/LICENSE
+++ b/LICENSE
@@ -1,22 +1,22 @@
-The MIT License (MIT)
+Copyright (C) 2021-present James Mills
 
-Copyright (c) 2014 zserge
+zs is covered by the MIT license::
 
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of the Software,
+and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
 
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
 
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
+OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/LICENSE.old b/LICENSE.old
new file mode 100644 (file)
index 0000000..c68b526
--- /dev/null
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 zserge
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
index b1b1fe862359c130ef02f8163e5e995b2acf1d60..2d3cd73fac4816df8f2c875e8e476f8290e3e4ec 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,13 +1,92 @@
-destdir ?= 
-prefix ?= /usr/local
-
-build:
-       go build -v
-clean:
-       rm -f zs
-install:
-       install -m0755 zs ${destdir}${prefix}/bin/zs
-       install -m0644 zs.1 ${destdir}${prefix}/share/man/man1/zs.1
-uninstall:
-       rm -f ${prefix}/bin/zs
-       rm -f ${prefix}/share/man/man1/zs.1
+-include environ.inc
+.PHONY: help deps dev build install image release test clean
+
+export CGO_ENABLED=0
+VERSION=$(shell git describe --abbrev=0 --tags 2>/dev/null || echo "$VERSION")
+COMMIT=$(shell git rev-parse --short HEAD || echo "$COMMIT")
+BRANCH=$(shell git rev-parse --abbrev-ref HEAD)
+BUILD=$(shell git show -s --pretty=format:%cI)
+GOCMD=go
+
+DESTDIR=/usr/local/bin
+
+ifeq ($(LOCAL), 1)
+IMAGE := r.mills.io/prologic/zs
+TAG := dev
+else
+IMAGE := prologic/zs
+TAG := latest
+endif
+
+all: help
+
+help: ## Show this help message
+       @echo "zs - Zen Static site generator"
+       @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m\033[0m\n"} /^[$$()% a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
+
+preflight: ## Run preflight checks to ensure you have the right build tools
+       @./preflight.sh
+
+deps: ## Install any required dependencies
+
+dev : DEBUG=1
+dev : build ## Build debug version of zs (cli)
+       @./zs -v
+
+cli: ## Build the zs command-line tool
+ifeq ($(DEBUG), 1)
+       @echo "Building in debug mode..."
+       @$(GOCMD) build -tags "netgo static_build" -installsuffix netgo \
+               -ldflags "\
+               -X $(shell go list).Version=$(VERSION) \
+               -X $(shell go list).Commit=$(COMMIT) \
+               -X $(shell go list).Build=$(BUILD)" \
+               .
+else
+       @$(GOCMD) build -tags "netgo static_build" -installsuffix netgo \
+               -ldflags "-w \
+               -X $(shell go list).Version=$(VERSION) \
+               -X $(shell go list).Commit=$(COMMIT) \
+               -X $(shell go list).Build=$(BUILD)" \
+               .
+endif 
+
+build: cli ## Build the cli
+
+install: build ## Install zs (cli) to $DESTDIR
+       @install -D -m 755 zs $(DESTDIR)/zs
+
+ifeq ($(PUBLISH), 1)
+image: generate ## Build the Docker image
+       @docker buildx build \
+               --build-arg VERSION="$(VERSION)" \
+               --build-arg COMMIT="$(COMMIT)" \
+               --build-arg BUILD="$(BUILD)" \
+               --platform linux/amd64,linux/arm64 --push -t $(IMAGE):$(TAG) .
+else
+image: generate
+       @docker build  \
+               --build-arg VERSION="$(VERSION)" \
+               --build-arg COMMIT="$(COMMIT)" \
+               --build-arg BUILD="$(BUILD)" \
+               -t $(IMAGE):$(TAG) .
+endif
+
+release: generate ## Release a new version to Gitea
+       @./tools/release.sh
+
+fmt: ## Format sources files
+       @$(GOCMD) fmt ./...
+
+test: ## Run test suite
+       @CGO_ENABLED=1 $(GOCMD) test -v -cover -race ./...
+
+coverage: ## Get test coverage report
+       @CGO_ENABLED=1 $(GOCMD) test -v -cover -race -cover -coverprofile=coverage.out  ./...
+       @$(GOCMD) tool cover -html=coverage.out
+
+clean: ## Remove untracked files
+       @git clean -f -d -x
+
+clean-all:  ## Remove untracked and Git ignored files
+       @git clean -f -d -X
index dda5588e55cfadcf557ddd0908f6d876ac223c9a..c36b04af4b9ebb5cdea6bb2383b53b326f8d21f5 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,10 +1,8 @@
-# zs
+# zs - Zen Static site generator
 
 zs is an extremely minimal static site generator written in Go.
 
-It's inspired by `zas` generator, but is even more minimal.
-
-The name stands for 'zen static' as well as it's my initials.
+[![Build Status](https://ci.mills.io/api/badges/prologic/zs/status.svg)](https://ci.mills.io/prologic/zs)
 
 ## Features
 
@@ -17,9 +15,19 @@ The name stands for 'zen static' as well as it's my initials.
 
 ## Installation
 
-Download the binaries from Github or build it manually:
+Download the binaries from [go.mills.io/prologic/zs](https://git.mills.io/prologic/zs):
+
+```console
+go get go.mills.io/zs@latest
+```
 
-       $ go get git.mills.io/prologic/zs
+Or build from source manaully:
+
+```console
+git clone https://git.mills.io/prologic/zs
+cd zs
+make install
+```
 
 ## Ideology
 
@@ -29,13 +37,16 @@ of your blog/site.
 Keep all service files (extensions, layout pages, deployment scripts etc)
 in the `.zs` subdirectory.
 
-Define variables in the header of the content files using [YAML]:
+Define variables in the header of the content files using [YAML front matter](https://assemble.io/docs/YAML-front-matter.html):
 
-       title: My web site
-       keywords: best website, hello, world
-       ---
+```markdown
+---
+title: My web site
+keywords: best website, hello, world
+---
 
-       Markdown text goes after a header *separator*
+Markdown text goes after a header *separator*
+```
 
 Use placeholders for variables and plugins in your markdown or html
 files, e.g. `{{ title }}` or `{{ command arg1 arg2 }}.
@@ -48,16 +59,16 @@ placeholder.
 
 Every variable from the content header will be passed via environment variables like `title` becomes `$ZS_TITLE` and so on. There are some special variables:
 
-* `$ZS` - a path to the `zs` executable
-* `$ZS_OUTDIR` - a path to the directory with generated files
-* `$ZS_FILE` - a path to the currently processed markdown file
-* `$ZS_URL` - a URL for the currently generated page
+- `$ZS` - a path to the `zs` executable
+- `$ZS_OUTDIR` - a path to the directory with generated files
+- `$ZS_FILE` - a path to the currently processed markdown file
+- `$ZS_URL` - a URL for the currently generated page
 
 ## Example of RSS generation
 
 Extensions can be written in any language you know (Bash, Python, Lua, JavaScript, Go, even Assembler). Here's an example of how to scan all markdown blog posts and create RSS items:
 
-``` bash
+```bash
 for f in ./blog/*.md ; do
        d=$($ZS var $f date)
        if [ ! -z $d ] ; then
@@ -81,26 +92,29 @@ done | sort -r -n | cut -d' ' -f2-
 
 There are two special plugin names that are executed every time the build
 happens - `prehook` and `posthook`. You can define some global actions here like
-content generation, or additional commands, like LESS to CSS conversion:
-
-       # .zs/post
+content generation, or additional commands, like to minify CSS or Javascript files.
 
-       #!/bin/sh
-       lessc < $ZS_OUTDIR/styles.less > $ZS_OUTDIR/styles.css
-       rm -f $ZS_OUTDIR/styles.css
+```bash
+#!/bin/sh
 
-## Command line usage
+set -e
 
-`zs build` re-builds your site.
+minify -o "$ZS_OUTDIR/css/fa.min.css" "$ZS_OUTDIR/css/fa.css"
+minify -o "$ZS_OUTDIR/css/site.min.css" "$ZS_OUTDIR/css/site.css"
 
-`zs build <file>` re-builds one file and prints resulting content to stdout.
+rm -rf "$ZS_OUTDIR/css/fa.css"
+rm -rf "$ZS_OUTDIR/css/screen.css"
+```
 
-`zs watch` rebuilds your site every time you modify any file.
+## Command line usage
 
-`zs var <filename> [var1 var2...]` prints a list of variables defined in the
+- `zs build` re-builds your site.
+- `zs build <file>` re-builds one file and prints resulting content to stdout.
+- `zs watch` rebuilds your site every time you modify any file.
+- `zs var <filename> [var1 var2...]` prints a list of variables defined in the
 header of a given markdown file, or the values of certain variables (even if
 it's an empty string).
 
 ## License
 
-The software is distributed under the MIT license.
+`zs` is licensed under the terms of the [MIT License](/LICENSE) and was orignally forked from [zserge/zs](https://github.com/zserge/zs) also licensed under the terms of the [MIT License](/LICENSE.old).
diff --git a/go.mod b/go.mod
index 636544d895ef24e4e27352a6b3621287a8d47ab8..c5e34599062641bd5adc6a2c7e102cfb8c32ae1a 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,18 @@
-module git.mills.io/prologic/zs
+module go.mills.io/zs
 
 go 1.17
 
 require (
        github.com/russross/blackfriday/v2 v2.1.0
+       go.mills.io/static v0.0.0-20230312034046-6dff09caed3b
        gopkg.in/yaml.v2 v2.4.0
 )
+
+require (
+       github.com/NYTimes/gziphandler v1.1.1 // indirect
+       github.com/cyphar/filepath-securejoin v0.2.3 // indirect
+       github.com/julienschmidt/httprouter v1.3.0 // indirect
+       github.com/sirupsen/logrus v1.9.0 // indirect
+       github.com/unrolled/logger v0.0.0-20201216141554-31a3694fe979 // indirect
+       golang.org/x/sys v0.0.0-20221010170243-090e33056c14 // indirect
+)
diff --git a/go.sum b/go.sum
index 6fc90070d83fe904734e35fb90ecd13c7f51040f..3ad723225e8f8b0ef1c173a59ac40244fcf00c1d 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,33 @@
+github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
+github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
+github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
+github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
+github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/unrolled/logger v0.0.0-20201216141554-31a3694fe979 h1:47+K4wN0S8L3fUwgZtPEBIfNqtAE3tUvBfvHVZJAXfg=
+github.com/unrolled/logger v0.0.0-20201216141554-31a3694fe979/go.mod h1:X5DBNY1yIVkuLwJP3BXlCoQCa5mGg7hPJPIA0Blwc44=
+go.mills.io/static v0.0.0-20230312034046-6dff09caed3b h1:9mSSHQJztO83b4939B31Z8bCOlvQUei6bRhnJq8eRC0=
+go.mills.io/static v0.0.0-20230312034046-6dff09caed3b/go.mod h1:TmaEDwM+IgrCRyMxtVWtmSdoxLP3N6ehBa7AiOZj2Mk=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20221010170243-090e33056c14 h1:k5II8e6QD8mITdi+okbbmR/cIyEbeXLBhy5Ha4nevyc=
+golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/main.go b/main.go
index 5335dccd0410cadc1c18bc32fae3c159280c7f37..a281d39ee9c862346ca75ed5f5d211e5cc247b56 100644 (file)
--- a/main.go
+++ b/main.go
@@ -1,29 +1,53 @@
+// Package main is a command-lint tool `zs` called Zen Static for generating static websites
 package main
 
 import (
        "bytes"
+       "context"
        "fmt"
        "io"
        "io/ioutil"
        "log"
        "os"
        "os/exec"
+       "os/signal"
        "path/filepath"
        "strings"
+       "syscall"
        "text/template"
        "time"
 
        "github.com/russross/blackfriday/v2"
+       "go.mills.io/static"
        "gopkg.in/yaml.v2"
 )
 
 const (
-       ZSDIR  = ".zs"
+       // ZSDIR is the default directory for storing layouts and extensions
+       ZSDIR = ".zs"
+
+       // PUBDIR is the default directory for publishing final built content
        PUBDIR = ".pub"
 )
 
+// Vars holds a map of global variables
 type Vars map[string]string
 
+// NewTicker is a function that wraps a time.Ticker and ticks immediately instead of waiting for the first interval
+func NewTicker(d time.Duration) *time.Ticker {
+       ticker := time.NewTicker(d)
+       oc := ticker.C
+       nc := make(chan time.Time, 1)
+       go func() {
+               nc <- time.Now()
+               for tm := range oc {
+                       nc <- tm
+               }
+       }()
+       ticker.C = nc
+       return ticker
+}
+
 // renameExt renames extension (if any) from oldext to newext
 // If oldext is an empty string - extension is extracted automatically.
 // If path has no extension - new extension is appended
@@ -113,60 +137,62 @@ func getVars(path string, globals Vars) (Vars, string, error) {
        }
 
        delim := "\n---\n"
-       if sep := strings.Index(s, delim); sep == -1 {
+       sep := strings.Index(s, delim)
+       if sep == -1 {
                return v, s, nil
-       } else {
-               header := s[:sep]
-               body := s[sep+len(delim):]
+       }
 
-               vars := Vars{}
-               if err := yaml.Unmarshal([]byte(header), &vars); err != nil {
-                       fmt.Println("ERROR: failed to parse header", err)
-                       return nil, "", err
-               } else {
-                       // Override default values + globals with the ones defines in the file
-                       for key, value := range vars {
-                               v[key] = value
-                       }
-               }
-               if strings.HasPrefix(v["url"], "./") {
-                       v["url"] = v["url"][2:]
-               }
-               return v, body, nil
+       header := s[:sep]
+       body := s[sep+len(delim):]
+
+       vars := Vars{}
+       if err := yaml.Unmarshal([]byte(header), &vars); err != nil {
+               fmt.Println("WARN: failed to parse header", err)
+               return v, s, nil
+       }
+       // Override default values + globals with the ones defines in the file
+       for key, value := range vars {
+               v[key] = value
+       }
+       if strings.HasPrefix(v["url"], "./") {
+               v["url"] = v["url"][2:]
        }
+       return v, body, nil
 }
 
 // Render expanding zs plugins and variables
 func render(s string, vars Vars) (string, error) {
-       delim_open := "{{"
-       delim_close := "}}"
+       openingDelimiter := "{{"
+       closingDelimiter := "}}"
 
        out := &bytes.Buffer{}
        for {
-               if from := strings.Index(s, delim_open); from == -1 {
+               from := strings.Index(s, openingDelimiter)
+               if from == -1 {
                        out.WriteString(s)
                        return out.String(), nil
-               } else {
-                       if to := strings.Index(s, delim_close); to == -1 {
-                               return "", fmt.Errorf("Close delim not found")
-                       } else {
-                               out.WriteString(s[:from])
-                               cmd := s[from+len(delim_open) : to]
-                               s = s[to+len(delim_close):]
-                               m := strings.Fields(cmd)
-                               if len(m) == 1 {
-                                       if v, ok := vars[m[0]]; ok {
-                                               out.WriteString(v)
-                                               continue
-                                       }
-                               }
-                               if res, err := run(vars, m[0], m[1:]...); err == nil {
-                                       out.WriteString(res)
-                               } else {
-                                       fmt.Println(err)
-                               }
+               }
+
+               to := strings.Index(s, closingDelimiter)
+               if to == -1 {
+                       return "", fmt.Errorf("Close delim not found")
+               }
+
+               out.WriteString(s[:from])
+               cmd := s[from+len(openingDelimiter) : to]
+               s = s[to+len(closingDelimiter):]
+               m := strings.Fields(cmd)
+               if len(m) == 1 {
+                       if v, ok := vars[m[0]]; ok {
+                               out.WriteString(v)
+                               continue
                        }
                }
+               if res, err := run(vars, m[0], m[1:]...); err == nil {
+                       out.WriteString(res)
+               } else {
+                       fmt.Println(err)
+               }
        }
 
 }
@@ -228,12 +254,12 @@ func buildRaw(path string, w io.Writer) error {
        }
        defer in.Close()
        if w == nil {
-               if out, err := os.Create(filepath.Join(PUBDIR, path)); err != nil {
+               out, err := os.Create(filepath.Join(PUBDIR, path))
+               if err != nil {
                        return err
-               } else {
-                       defer out.Close()
-                       w = out
                }
+               defer out.Close()
+               w = out
        }
        _, err = io.Copy(w, in)
        return err
@@ -250,51 +276,80 @@ func build(path string, w io.Writer, vars Vars) error {
        }
 }
 
-func buildAll(watch bool) {
+func buildAll(ctx context.Context, watch bool) {
+       ticker := NewTicker(time.Second)
+       defer ticker.Stop()
+
        lastModified := time.Unix(0, 0)
        modified := false
 
        vars := globals()
        for {
-               os.Mkdir(PUBDIR, 0755)
-               filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
-                       // ignore hidden files and directories
-                       if filepath.Base(path)[0] == '.' || strings.HasPrefix(path, ".") {
-                               return nil
-                       }
-                       // inform user about fs walk errors, but continue iteration
-                       if err != nil {
-                               fmt.Println("error:", err)
-                               return nil
-                       }
+               select {
+               case <-ctx.Done():
+                       return
+               case <-ticker.C:
+                       os.Mkdir(PUBDIR, 0755)
+                       filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
+                               // ignore hidden files and directories
+                               if filepath.Base(path)[0] == '.' || strings.HasPrefix(path, ".") {
+                                       return nil
+                               }
+                               // inform user about fs walk errors, but continue iteration
+                               if err != nil {
+                                       fmt.Println("error:", err)
+                                       return nil
+                               }
 
-                       if info.IsDir() {
-                               os.Mkdir(filepath.Join(PUBDIR, path), 0755)
-                               return nil
-                       } else if info.ModTime().After(lastModified) {
-                               if !modified {
-                                       // First file in this build cycle is about to be modified
-                                       run(vars, "prehook")
-                                       modified = true
+                               if info.IsDir() {
+                                       os.Mkdir(filepath.Join(PUBDIR, path), 0755)
+                                       return nil
+                               } else if info.ModTime().After(lastModified) {
+                                       if !modified {
+                                               // First file in this build cycle is about to be modified
+                                               run(vars, "prehook")
+                                               modified = true
+                                       }
+                                       log.Println("build:", path)
+                                       return build(path, nil, vars)
                                }
-                               log.Println("build:", path)
-                               return build(path, nil, vars)
+                               return nil
+                       })
+                       if modified {
+                               // At least one file in this build cycle has been modified
+                               run(vars, "posthook")
+                               modified = false
                        }
-                       return nil
-               })
-               if modified {
-                       // At least one file in this build cycle has been modified
-                       run(vars, "posthook")
-                       modified = false
-               }
-               if !watch {
-                       break
+                       if !watch {
+                               return
+                       }
+                       lastModified = time.Now()
                }
-               lastModified = time.Now()
-               time.Sleep(1 * time.Second)
        }
 }
 
+// serve runs a static web server and builds and continuously watches for changes to rebuild
+func serve(ctx context.Context, bind string) error {
+       os.Mkdir(PUBDIR, 0755)
+
+       svr, err := static.NewServer(
+               static.WithBind(bind),
+               static.WithDir(true),
+               static.WithRoot(PUBDIR),
+               static.WithSPA(true),
+       )
+       if err != nil {
+               return err
+       }
+
+       go svr.Run(ctx)
+       go buildAll(ctx, true)
+
+       <-ctx.Done()
+
+       return nil
+}
+
 func init() {
        // prepend .zs to $PATH, so plugins will be found before OS commands
        p := os.Getenv("PATH")
@@ -305,15 +360,21 @@ func init() {
 
 func main() {
        if len(os.Args) == 1 {
-               fmt.Println(os.Args[0], "<command> [args]")
+               fmt.Printf("%s <command> [args]\n", filepath.Base(os.Args[0]))
+               os.Exit(1)
                return
        }
+
        cmd := os.Args[1]
        args := os.Args[2:]
+
+       ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
+       defer cancel()
+
        switch cmd {
        case "build":
                if len(args) == 0 {
-                       buildAll(false)
+                       buildAll(ctx, false)
                } else if len(args) == 1 {
                        if err := build(args[0], os.Stdout, globals()); err != nil {
                                fmt.Println("ERROR: " + err.Error())
@@ -322,7 +383,15 @@ func main() {
                        fmt.Println("ERROR: too many arguments")
                }
        case "watch":
-               buildAll(true)
+               buildAll(ctx, true)
+       case "serve":
+               bind := ":8000"
+               if len(args) > 1 {
+                       bind = args[0]
+               }
+               if err := serve(ctx, bind); err != nil {
+                       fmt.Println("ERROR: " + err.Error())
+               }
        case "var":
                if len(args) == 0 {
                        fmt.Println("var: filename expected")
diff --git a/preflight.sh b/preflight.sh
new file mode 100755 (executable)
index 0000000..164f4e1
--- /dev/null
@@ -0,0 +1,145 @@
+#!/usr/bin/env sh
+
+set -e
+
+color() {
+  fg="$1"
+  bg="${2}"
+  ft="${3:-0}"
+
+  printf "\33[%s;%s;%s" "$ft" "$fg" "$bg"
+}
+
+color_reset() {
+  printf "\033[0m"
+}
+
+ok() {
+  if [ -t 1 ]; then
+    printf "%s[ OK ]%s\n" "$(color 37 42m 1)" "$(color_reset)"
+  else
+    printf "%s\n" "[ OK ]"
+  fi
+}
+
+err() {
+  if [ -t 1 ]; then
+    printf "%s[ ERR ]%s\n" "$(color 37 41m 1)" "$(color_reset)"
+  else
+    printf "%s\n" "[ ERR ]"
+  fi
+}
+
+run() {
+  retval=0
+  logfile="$(mktemp -t "run-XXXXXX")"
+  if "$@" 2> "$logfile"; then
+    ok
+  else
+    retval=$?
+    err
+    cat "$logfile" || true
+  fi
+  rm -rf "$logfile"
+  return $retval
+}
+
+progress() {
+  printf "%-40s" "$(printf "%s ... " "$1")"
+}
+
+log() {
+  printf "%s\n" "$1"
+}
+
+log2() {
+  printf "%s\n" "$1" 1>&2
+}
+
+error() {
+  log "ERROR: ${1}"
+}
+
+fail() {
+  log "FATAL: ${1}"
+  exit 1
+}
+
+check_goversion() {
+  progress "Checking Go version"
+
+  if ! command -v go > /dev/null 2>&1; then
+    log2 "Cannot find the Go compiler"
+    return 1
+  fi
+
+  gover="$(go version | grep -o -E 'go[0-9]+\.[0-9]+(\.[0-9]+)?')"
+
+  if ! go version | grep -E 'go1\.1[6789](\.[0-9]+)?' > /dev/null; then
+    log2 "Go 1.16+ is required, found ${gover}"
+    return 1
+  fi
+
+  return 0
+}
+
+check_path() {
+  progress "Checking \$PATH"
+
+  gobin="$(eval "$(go env | grep GOBIN)")"
+  gopath="$(eval "$(go env | grep GOPATH)")"
+
+  if [ -n "$gobin" ] && ! echo "$PATH" | grep "$gobin" > /dev/null; then
+    log2 "\$GOBIN '$gobin' is not in your \$PATH"
+    return 1
+  fi
+
+  if [ -n "$gopath" ] && ! echo "$PATH" | grep "$gopath/bin" > /dev/null; then
+    log2 "\$GOPATH/bin '$gopath/bin' is not in your \$PATH"
+    return 1
+  fi
+
+  if ! echo "$PATH" | grep "$HOME/go/bin" > /dev/null; then
+    log2 "\$HOME/go/bin is not in your \$PATH"
+    return 1
+  fi
+
+  return 0
+}
+
+check_deps() {
+  progress "Checking deps"
+
+  if ! command -v minify > /dev/null 2>&1; then
+    log2 "minify not found, Try running: make deps"
+    return 1
+  fi
+
+  if ! minify --help 2>&1 | grep '\-b, \-\-bundle' > /dev/null; then
+    log2 "wrong version of minify found, Try running: make deps"
+    return 1
+  fi
+
+  if ! command -v goi18n > /dev/null 2>&1; then
+    log2 "goi18n not found, Try running: make deps"
+    return 1
+  fi
+
+  return 0
+}
+
+steps="check_goversion check_path check_deps"
+
+_main() {
+  for step in $steps; do
+    if ! run "$step"; then
+      fail "๐Ÿ™ preflight failed"
+    fi
+  done
+
+  log "๐Ÿฅณ All Done! Ready to build, run: make build"
+}
+
+if [ -n "$0" ] && [ x"$0" != x"-bash" ]; then
+  _main "$@"
+fi