├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── examples ├── README.md └── single-tag-page │ ├── Makefile │ ├── config │ ├── site.tmpl │ └── src │ ├── blog.atom │ ├── blog │ └── first.md │ ├── index.html │ ├── static │ └── style.css │ └── tag.html ├── go.mod ├── go.sum ├── gostatic.go ├── hotreload ├── assets.go ├── assets │ ├── hotreload.js │ └── morphdom.js ├── core.go └── watch.go ├── lib ├── config.go ├── example.go ├── header.go ├── page.go ├── processors.go ├── render.go ├── site.go ├── template_funcs.go ├── template_funcs_test.go ├── utils.go └── version.go ├── processors ├── chroma.go ├── config.go ├── config_test.go ├── datefilename.go ├── default.go ├── directorify.go ├── ext.go ├── external.go ├── ignore.go ├── ignorefuture.go ├── jekyllify.go ├── markdown.go ├── pagination.go ├── relativize.go ├── rename.go ├── tags.go ├── template.go └── yaml.go ├── test ├── config ├── site │ ├── about.md │ ├── blog │ │ ├── index.md │ │ ├── one.md │ │ ├── three.md │ │ └── two.md │ ├── index.md │ ├── inner │ │ └── inside.src │ ├── test.js │ └── test.less ├── template.tmpl └── templates │ └── test.tmpl └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | /gostatic 2 | /gostatic-* 3 | /test/out/ 4 | /build/ 5 | /examples/*/site/ 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Gostatic changelog 2 | 3 | - SSE now works in a way to prevent Firefox spitting errors in console 4 | 5 | ## 2.36 6 | 7 | - Switch to using [SSE][] from websockets - simpler and less deps 8 | - Using hotreload would trim responses to at most 32kb length, fixed now 9 | - Updated all deps, some of them were few years old 10 | - Markdown is now parsed with [some attributes](https://github.com/yuin/goldmark#attributes) 11 | 12 | [SSE]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events 13 | 14 | ## 2.35 15 | 16 | - New template function `abcsort` to sort pages alphabetically 17 | 18 | ## 2.34 19 | 20 | - New template processor `chroma` to highlight code in HTML files 21 | 22 | ## 2.33 23 | 24 | - New template function `absurl` to join urls sanely 25 | 26 | ## 2.32 27 | 28 | - Template function `markdown` broke backward compatibility, fixed now (always wanted more than one argument) 29 | 30 | ## 2.31 31 | 32 | - Built-in support for [chroma](https://github.com/alecthomas/chroma) highlighter (see README) 33 | - Hot reloading reconnects websocket if it closes, so it persists between runs of gostatic 34 | 35 | ## 2.30 36 | 37 | - Hot reloading will now dispatch JS event `hotreload` on window 38 | - Mac binary is now universal (x64 and arm64 simultaneously) 39 | 40 | ## 2.29 41 | 42 | - added `trim` template function 43 | 44 | ## 2.28 45 | 46 | Hot reloading now uses morphdom and because of that screen doesn't flicker upon change 47 | 48 | ## 2.27 49 | 50 | - added `datefilename` processor 51 | 52 | ## 2.26 53 | 54 | - added ability to parse Github-like frontmatter, i.e. `---\nvar: content\n---` 55 | 56 | ## 2.25 57 | 58 | - all glob matches (like in config) support double stars to descend recursively, like `blog/**/*.md` 59 | - `dir` and `base` functions to manipulate paths in templates 60 | - `refind` function to find strings inside strings 61 | 62 | ## 2.24 63 | 64 | - `cut` now will return empty string if one of regexes did not match 65 | - now invalid processor names from config are reported in a better way - their name is included in error message 66 | 67 | ## 2.23 68 | 69 | Enabled unsafe HTML for goldmark - it won't omit raw HTML when rendering 70 | 71 | ## 2.22 72 | 73 | Switch markdown library from [blackfriday](github.com/russross/blackfriday) to [goldmark](https://github.com/yuin/goldmark/), adding support of Commonmark, and, notably, smartypants-like features. 74 | 75 | ## 2.21 76 | 77 | New template function - `some`: returns first non-nil value, intended to use instead of lengthy ifs. 78 | 79 | ## 2.20 80 | 81 | Increase number in `version.go`, because it still was 2.17. :) 82 | 83 | ## 2.19 84 | 85 | - new template functions: `count` and `reading_time` 86 | - hot reloading has an exponential timeout up to a second to reduce flickering 87 | 88 | ## 2.18 89 | 90 | Hot HTML code reload when in dev mode (using `gostatic -w/--watch`). 91 | 92 | ## 2.17 93 | 94 | `.Has` for pages, `.Where` and `.WhereNot` for page lists. 95 | 96 | ## 2.16 97 | 98 | `exectext` function. 99 | 100 | ## 2.15 101 | 102 | `.Reverse` is now available as a method on page lists. 103 | 104 | ## 2.14 105 | 106 | - new template function: `matches`, checks for regexp in a string. 107 | - fixed parsing tags in YAML header 108 | - inner templates report their errors better now 109 | - support for BOM (easier to use with files created on Windows) 110 | - CRLF support 111 | - also, gomod - we have pinned versions of dependencies 112 | 113 | ## 2.13 114 | 115 | New processors for people switching from Jekyll: `jekyllify` to convert posts to 116 | a familiar path, and `yaml` to process headers as YAML (rather than whatever 117 | custom stuff gostatic uses by default). 118 | 119 | ## 2.12 120 | 121 | Now `cut` searches for the `end` *after* end of `begin` match. 122 | 123 | ## 2.11 124 | 125 | New template function: `replacere`. 126 | 127 | ## 2.10 128 | 129 | Two new template functions: `even` and `odd`. 130 | 131 | ## 2.9 132 | 133 | `gostatic -w` now waits 10 ms before doing anything to prevent problems with 134 | emacs-style file changes, when it first creates empty file in place of an old 135 | one and then moves changes over to it. 136 | 137 | ## 2.8 138 | 139 | Two new template functions: `starts` and `ends`. 140 | 141 | ## 2.7 142 | 143 | Ability to have multiple configurations for a single path (so you can have 144 | multiple outputs from one file). 145 | 146 | ## 2.6 147 | 148 | Sort pages with same date alphabetically. 149 | 150 | ## 2.5 151 | 152 | Get `exec` template function back. 153 | 154 | ## 2.4 155 | 156 | - Fixed handling \r\n in `config` processor 157 | - Now errors of `external` processor are propagated and you'll see them 158 | 159 | ## 2.3 160 | 161 | gostatic is now a [library](https://github.com/piranha/gostatic#extensibility) (thanks @zhuharev)! Plus: 162 | 163 | - `exec` function in templates 164 | - `exceprt` function in templates (thanks @krpors) 165 | - gostatic no longer fails on vim's temp files (thanks @krpors) 166 | 167 | ## 2.2 168 | 169 | Make example site (gostatic -i) work with current gostatic. 170 | 171 | ## 2.1 172 | 173 | Fix `rename` processor for Windows. 174 | 175 | ## 2.0 176 | 177 | Major version - **breaking changes**. 178 | 179 | - **Backward incompatible** - [template functions](https://github.com/piranha/gostatic#global-functions) `cut` and `split` now have different order of arguments to better support [template pipelining](https://golang.org/pkg/text/template/#hdr-Pipelines). 180 | - Pagination is now supported, see `paginate` [processor](https://github.com/piranha/gostatic#processors) and `paginator` [template function](https://github.com/piranha/gostatic#global-functions). 181 | - Template and config changes are now tracked and will result in full re-render. 182 | - [Page](https://github.com/piranha/gostatic#page-interface) now has `.Raw` property, containing unprocessed data (but after `config` being consumed). 183 | - `strip_newlines`, `replace`, `replacen`, `contains`, `markdown` [template functions](https://github.com/piranha/gostatic#global-functions). 184 | - [Page list](https://github.com/piranha/gostatic#page-list-interface) new methods: `.Slice` and `.GlobSource`. 185 | 186 | ## 1.17 187 | 188 | More fsnotify stuff. 189 | 190 | ## 1.16 191 | 192 | Updated fsnotify; potentially better watcher behavior. 193 | 194 | ## 1.15 195 | 196 | Ability to specify folders with templates. 197 | 198 | ## 1.14 199 | 200 | "split" function in templates to generate array from string. 201 | 202 | ## 1.13 203 | 204 | Ability to hide pages with `hide: true` in page header. 205 | 206 | ## 1.12 207 | 208 | More functions for templates: `truncate` and `split_html`. 209 | 210 | ## 1.11 211 | 212 | Make errors when processing template at least a bit better. 213 | 214 | ## 1.10 215 | 216 | More strict split in ProcessConfig. 217 | 218 | ## 1.9 219 | 220 | Enable header ids in markdown processing. 221 | 222 | ## 1.8 223 | 224 | Somewhat simplified watch code and it started working. 225 | 226 | ## 1.7 227 | 228 | - Enable footnotes 229 | - Smaller binaries (by skipping debug info) 230 | - Fix directory walking error handling 231 | - Watch source directory instead of destination 232 | - Watch templates for changes 233 | 234 | ## 1.6 235 | 236 | Fixed crash on `PageSlice.Prev` when no previous pages exist. 237 | 238 | ## 1.5 239 | 240 | Add `PageSlice.Next` and `PageSlice.Prev` 241 | 242 | ## 1.4 243 | 244 | Ability to print page metadata as json (`gostatic --dump src/path/to/url config`). 245 | 246 | ## 1.3 247 | 248 | - Fixed bug with empty bodies 249 | - Ability to have comments in page header (with `#`) 250 | 251 | ## 1.2 252 | 253 | - Fix `PageSlice.Slice` crashes 254 | - Fix `cut` to not fail when search returns no results 255 | - `Page.UrlMatches` 256 | - Compare tag pages by path (not by title) 257 | 258 | ## 1.1 259 | 260 | - Fix example site to escape entities 261 | - Fix symlink handling 262 | 263 | ## 1.0 264 | 265 | First tagged release, lots of good stuff. :) 266 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2012-2013, Alexander Solovyov 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCE = $(shell find . -name '*.go') 2 | TAG ?= $(shell git describe --tags) 3 | GOBUILD = go build -ldflags '-s -w' 4 | 5 | ALL = \ 6 | $(foreach suffix,linux mac win.exe,\ 7 | build/gostatic-64-$(suffix)) 8 | 9 | all: $(ALL) 10 | 11 | run: 12 | go run *.go test/config --summary 13 | 14 | render: 15 | go run *.go test/config 16 | 17 | config: 18 | go run *.go test/config --show-config 19 | 20 | 21 | ### Utils 22 | 23 | fmt: 24 | gofmt -w=true *.go 25 | 26 | morphdom: 27 | curl -Lo hotreload/assets/morphdom.js https://github.com/patrick-steele-idem/morphdom/raw/master/dist/morphdom-umd.js 28 | 29 | 30 | ### Releases 31 | 32 | # os is determined as thus: if variable of suffix exists, it's taken, if not, then 33 | # suffix itself is taken 34 | win.exe = GOOS=windows GOARCH=amd64 35 | linux = GOOS=linux GOARCH=amd64 36 | mac-amd64 = GOOS=darwin GOARCH=amd64 37 | mac-arm64 = GOOS=darwin GOARCH=arm64 38 | build/gostatic-64-%: $(SOURCE) 39 | @mkdir -p $(@D) 40 | CGO_ENABLED=0 $($*) $(GOBUILD) -o $@ 41 | 42 | build/gostatic-64-mac: %: %-amd64 %-arm64 43 | @mkdir -p $(@D) 44 | lipo -create -output $@ $^ 45 | 46 | # NOTE: first push a tag, then make release! 47 | ifndef desc 48 | release: 49 | @echo "You forgot description! Run it as 'make release desc=tralala'" 50 | else 51 | release: $(ALL) 52 | github-release release -u piranha -r gostatic -t "$(TAG)" -n "$(TAG)" --description '$(desc)' 53 | @sleep 1 54 | @for x in $(ALL); do \ 55 | github-release upload -u piranha \ 56 | -r gostatic \ 57 | -t "$(TAG)" \ 58 | -f "$$x" \ 59 | -n "$$(basename $$x)" \ 60 | && echo "Uploaded $$x"; \ 61 | done 62 | endif 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gostatic 2 | 3 | Gostatic is a static site generator. It tracks file changes during compilation, 4 | which is why it works reasonably [fast](#speed). Also it provides framework for 5 | [configuration](#configuration) akin to Make, which makes it easy to understand 6 | and to write custom configurations. 7 | 8 | Features include: 9 | 10 | - No run-time dependencies, just a single binary - [download](https://github.com/piranha/gostatic/releases/) it and run 11 | - Dependency tracking and re-rendering only changed pages 12 | - Markdown support - [Commonmark](https://commonmark.org/) via [goldmark](https://github.com/yuin/goldmark/) 13 | - Simple [config syntax](#configuration) 14 | - Flexible [filter system](#processors) 15 | - Support for pagination 16 | - Plays well with external commands and scripts 17 | - HTTP server and watcher (instant rendering on changes) 18 | - Suitable for automation (ability to query state with `gostatic --dump`) 19 | 20 | And all in all, it works nicely for me, so it may work for you! 21 | 22 | ## Installation 23 | 24 | Just download a binary from [releases page](https://github.com/piranha/gostatic/releases). 25 | 26 | If you need to automate downloading latest release, I use this script (change 27 | `64-linux` to the type you need): 28 | 29 | ``` 30 | URL=$(curl -s https://api.github.com/repos/piranha/gostatic/releases | awk '/download_url.*64-linux/ { print $2; exit }') 31 | curl -Lso gostatic $(URL) 32 | chmod +x gostatic 33 | ``` 34 | 35 | When downloading in Macos, your file will be quarantined and to use it you'll 36 | have to assure Macos you know what you're doing: 37 | 38 | ``` 39 | xattr -r -d com.apple.quarantine gostatic 40 | ``` 41 | 42 | Obviously, `go get https://github.com/piranha/gostatic` also works, if you want 43 | to compile from source. 44 | 45 | 46 | ## Quick Start 47 | 48 | Run `gostatic -i my-site` to generate basic site in directory `my-site`. It will 49 | have a basic `config` file, which you should edit to put relevant variables at 50 | the top - it also contains description of how files in your `src` directory are 51 | treated. 52 | 53 | `src` directory obviously contains sources of your site (name of this directory 54 | can be changed in `config`). You can follow general idea of this directory to 55 | create new blog posts or new pages. All files, which are not mentioned in 56 | `config`, are just copied over. Run `gostatic -fv config` to see how your `src` 57 | is processed. 58 | 59 | `site.tmpl` is a file that defines templates your are able to use for your 60 | pages. You can see those templates mentioned in `config`. 61 | 62 | And, finally, there is a `Makefile`, just for convenience. Run `make` to build 63 | your site once or `make w` to run watcher and server, to see your site changes 64 | in real time. 65 | 66 | Also, you could look at [my site](https://github.com/piranha/solovyov.net) for 67 | an example of advanced usage. 68 | 69 | Good luck! And remember, your contributions either to gostatic or to 70 | documentation (even if it's just this `README.md`) are always very welcome! 71 | 72 | ## Documentation index: 73 | 74 | - [Approach](#approach) 75 | - [Speed](#speed) 76 | - [External Resources](#external-resources) 77 | - [Configuration](#configuration) 78 | - [Constants](#constants) 79 | - [Page Config](#page-config) 80 | - [Processors](#processors) 81 | - [Template API Reference](#template-api-reference) 82 | - [Global Functions](#global-functions) 83 | - [Page interface](#page-interface) 84 | - [Page list interface](#page-list-interface) 85 | - [Site interface](#site-interface) 86 | - [Extensibility](#extensibility) 87 | 88 | Also, see [wiki](https://github.com/piranha/gostatic/wiki) - and feel free to 89 | add more information there! 90 | 91 | ## Approach 92 | 93 | Each given file is processed through a pipeline of filters, which modify the 94 | file state and then rendered on disk. Single input file corresponds to a single 95 | output file, but filters can generate virtual input files (like tag files). 96 | 97 | File is rendering in those cases: 98 | 99 | - output file does not exists 100 | - file source is newer than it's output 101 | - one of those is the case for one of file's dependencies 102 | 103 | All files are sorted by date. This date is taken in their [config](#page-config) 104 | or, in case if date in config is absent or dates there are equal, by file 105 | modification time. 106 | 107 | ## Speed 108 | 109 | | Box | Pages | Full | Noop | Single post | 110 | |:--------------------------|------:|------:|------:|-----------------:| 111 | | Macbook Air '15 (i7) | 630 | 450ms | 100ms | 160ms (93 pages) | 112 | | Macbook Air '15 (i7) | 250 | 180ms | n/a | n/a | 113 | | Macbook Air '20 (M1) | 486 | 185ms | 39ms | 140ms (97 pages) | 114 | | Hetzner CX21 (2 vCPU/4GB) | 486 | 502ms | 89ms | 414ms (97 pages) | 115 | 116 | This are results of forced full site rebuild, then checking there are no 117 | modification, and then re-rendering a single changed page (along with pages 118 | which depend on this page). 119 | 120 | Also note that if you're using various external post-processors (like uglifyjs 121 | or sassc) they tend to slow down things a bit (for my specific use case both 122 | uglifyjs and sassc add another `0.25s` when files they process change). 123 | 124 | To reproduce numbers, [download hyperfine][], [download gostatic][], clone 125 | [solovyov.net][], comment out `:google-closure-compiler` in `config` and then run: 126 | 127 | - `hyperfine 'gostatic -f config'` 128 | - `hyperfine 'gostatic config'` 129 | - `hyperfine 'touch src/blog/2017/fuji-raw.md && gostatic config'` 130 | 131 | [download hyperfine]: https://github.com/sharkdp/hyperfine/releases 132 | [download gostatic]: https://github.com/piranha/gostatic/releases 133 | [solovyov.net]: https://github.com/piranha/solovyov.net 134 | 135 | ## External resources 136 | 137 | - Jack Pearkes made a [Heroku buildpack][] for gostatic. 138 | 139 | [Heroku buildpack]: https://github.com/pearkes/heroku-buildpack-gostatic 140 | 141 | ## Configuration 142 | 143 | Config syntax is Makefile-inspired with some simplifications, look at the 144 | example: 145 | 146 | ```Makefile 147 | TEMPLATES = site.tmpl templates-folder 148 | SOURCE = src 149 | OUTPUT = site 150 | 151 | # this is a comment 152 | *.md: 153 | config 154 | ext .html 155 | directorify 156 | tags tags/*.tag 157 | markdown 158 | template page # yeah, this is a comment as well 159 | 160 | index.md: blog/*.md 161 | config 162 | ext .html 163 | inner-template 164 | markdown 165 | template page 166 | 167 | *.tag: blog/*.md 168 | ext .html 169 | directorify 170 | template tag 171 | markdown 172 | template page 173 | ``` 174 | 175 | Here we have constants declaration (first three lines), a comment and then three 176 | rules. One for any markdown file, one specifically for index.md and one for 177 | generated tags. 178 | 179 | Specific rules override generic matching rules, but logic is not exactly very 180 | smart, and there is no real precedence defined, so if you have several matches 181 | for a single file you could end up with any of them. Note that there is some 182 | order: exact path match, exact name match, glob path match, glob name 183 | match. NOTE: this may change in future. 184 | 185 | Rules consist of path/match, list of dependencies (also paths and matches, the 186 | ones listed after colon) and commands. 187 | 188 | Each command consists of a name of processor and (possibly) some 189 | arguments. Arguments are separated by spaces. 190 | 191 | Note: if a file has no rules whatsoever, it will be copied to exactly same 192 | location at destination as it was in source without being read into memory. So 193 | heavy images etc shouldn't be a problem. 194 | 195 | ### Constants 196 | 197 | There are three configuration constants: 198 | 199 | - `SOURCE` - sources to read (relative to location of config) 200 | - `OUTPUT` - directory for output (relative to location of config) 201 | - `TEMPLATES` - list of files and/or directories (containing `*.tmpl` files), 202 | which will be parsed as Go templates. Each file can contain more than one 203 | template (see [docs](https://golang.org/pkg/text/template/#hdr-Nested_template_definitions) 204 | on that). 205 | 206 | You can also use arbitrary names for constants to 207 | [access later](#site-interface) from templates - just use any other name 208 | (`AUTHOR` could be one). 209 | 210 | All constants can also be accessed from the config itself, using 211 | `$(CONSTANT_NAME)` syntax, just like in `Makefile`. 212 | 213 | ## Page Config 214 | 215 | Page config is only processed if you specify `config` processor for a page. It's 216 | format is `name: value`, for example: 217 | 218 | ``` 219 | title: This is a page 220 | tags: test 221 | date: 2013-01-05 222 | ``` 223 | 224 | Parsed properties: 225 | 226 | - `title` - page title. 227 | - `tags` - list of tags, separated by `,`. 228 | - `date` - page date, could be used for blog. Accepts formats from bigger to 229 | smaller (from `"2006-01-02 15:04:05 -07"` to `"2006-01-02"`) 230 | - `hide` - false if not specified or is one of `f`, `false`, `False`, 231 | `FALSE`. True in other cases. Hides page from children and tag lists when true. 232 | 233 | You can also define any other property you like, it's value will be treated as a 234 | string and it's key is capitalized and put on the `.Other` 235 | [page property](#page-interface). 236 | 237 | ## Processors 238 | 239 | You can always check list of available processors with `gostatic --processors`. 240 | 241 | - `config` - reads config from content. Config should be in format "name: value" 242 | and separated by four dashes on empty line (`----`) from content. 243 | 244 | - `ignore` - ignore file. 245 | 246 | - `rename ` - rename a file to `new-name`. Note this does not change 247 | path to a file (you can use `..`, though, but be careful about platform 248 | differences). If `new-name` contains `*`, then it'll be replaced with content 249 | of `*` from path match. For example, with `blog/*.md: rename ../blog-*.html` 250 | this will rename `blog/one.html` to `blog-one.html`. 251 | 252 | - `ext <.ext>` - change file extension to a given one (which should be prefixed 253 | with a dot). 254 | 255 | - `datefilename` - rename a file from `whatever/2021-02-08-name.html` to 256 | `whatever/name.html` and set the `page.Date` to `2021-02-08`. 257 | 258 | - `directorify` - rename a file from `whatever/name.html` to 259 | `whatever/name/index.html`. 260 | 261 | - `markdown` - process content as Markdown. 262 | `markdown` without any arguments will not do any code-block highlighting. 263 | `markdown chroma=monokai` will use the [Chroma][chroma] highlighter to highlight code blocks, using the Monokai style, with inline CSS styles. (No .css file needed). 264 | You can see the styles at the [Chroma style previewer][chromaStyles1]. 265 | The official list of styles is in the [Chroma repo here][chromaStyles2]. 266 | 267 | [chroma]: https://github.com/alecthomas/chroma 268 | [chromaStyles1]: https://xyproto.github.io/splash/docs/ 269 | [chromaStyles2]: https://github.com/alecthomas/chroma/tree/master/styles 270 | 271 | - `inner-template` - process content as Go template. 272 | 273 | - `template ` - pass page to a template named ``. 274 | 275 | - `tags ` - create a virtual page for all tags of a current 276 | page. This tag page has path formed by replacing `*` in `` with 277 | a tag name and has a tag as its `.Title` (use `{{ range .Site.Pages.WithTag 278 | .Title }}...{{end}}` to get a list of tagged pages. 279 | 280 | - `relativize` - change all urls archored at `/` to be relative (i.e. add 281 | appropriate amount of `../`) so that generated content can be deployed in a 282 | subfolder of a site. 283 | 284 | - `external ` - call external command with content of a page 285 | as stdin and using stdout as a new content of a page. Has a shortcut: 286 | `: ` (`:` is replaced with `external `). 287 | 288 | - `paginate ` - create a virtual page for each `n` of pages 289 | (grouped by `path-pattern`, so you can paginate few groups of pages as a 290 | single one). `path-pattern` has `*` replaced by an index of this virtual page 291 | (1-based), and you can get a list of pages with `{{ range paginator 292 | .}}...{{end}}` (see [paginator](#global-functions) function). Using `paginate` 293 | with the same `path-pattern` on different types of pages will group them in 294 | same paginated list (*request*: please open an 295 | [issue](https://github.com/piranha/gostatic/issues/new) if you have an idea 296 | how to phrase this better). 297 | 298 | - `jekyllify` - creating pages in jekyll style, for example, the page 299 | `2018-02-02-name.md` will be converted to `/2018/02/02/name.md`. 300 | 301 | - `yaml` - read the configuration for the page using yaml format (like jekyll). 302 | 303 | ## Template API Reference 304 | 305 | Templating is provided using 306 | [Go templates](https://golang.org/pkg/text/template/). See link for documentation 307 | on syntax. 308 | 309 | Each template is executed in context of a [page](#page-interface). This means it 310 | has certain properties and methods it can output or call to generate content, 311 | i.e. `{{ .Content }}` will output page content in place. 312 | 313 | ### Global functions 314 | 315 | Go template system provides some convenient 316 | [functions](https://golang.org/pkg/text/template/#hdr-Functions), and gostatic 317 | expands on that a bit: 318 | 319 | - `absurl ` - given an url and a base will join them to something 320 | sane: leave the `` in place if it's absolute, or resolve it within 321 | `` if it's not. 322 | 323 | - `changed ` - checks if `` has changed since previous call 324 | with the same name. Storage used for checking is global over the whole run of 325 | gostatic, so choose unique names for different places. 326 | 327 | - `cut ` - cut partial content from ``, delimited 328 | by regular expressions `` and ``. 329 | 330 | - `hash ` - return 32-bit hash of a given value. 331 | 332 | - `version ` - return relative URL to a page with resulting path 333 | `` with `?v=<32-bit hash>` appended (use to override cache settings for 334 | static files). 335 | 336 | - `truncate ` - truncate string to given length (if it's 337 | longer). 338 | 339 | - `strip_html ` - remove all HTML tags from string. 340 | 341 | - `strip_newlines ` - remove all line breaks and newlines from string. 342 | 343 | - `trim ` - trim all whitespace ([strings.TrimSpace](https://golang.org/pkg/strings/#TrimSpace)). 344 | 345 | - `replace ` - replace all occurrences of `old` with `new` in 346 | `value`. 347 | 348 | - `replacen ` - same as above, but only `n` times. 349 | 350 | - `replacere ` - replace text in `value` 351 | according to [regexp](https://golang.org/pkg/regexp/syntax/) `pattern` and 352 | `replacement`. 353 | 354 | - `split ` - split string by separator, generating an array 355 | (you can use `range` with result of this function). 356 | 357 | - `contains ` - check if a string `value` contains `needle`. 358 | 359 | - `starts ` - check if a string `value` starts with `needle`. 360 | 361 | - `ends ` - check if a string `value` ends with `needle`. 362 | 363 | - `matches ` - check if a 364 | [regexp](https://golang.org/pkg/regexp/syntax/) `pattern` matches string `value`. 365 | 366 | - `refind ` - apply regexp `pattern` to a string `value` and 367 | return first submatch (the thing in parentheses), if any, or a whole matched 368 | string. 369 | 370 | - `markdown ` - convert a string (`value`) from Markdown to HTML. 371 | 372 | - `paginator ` - get a [paginator](#paginator-interface) object for 373 | current page (only works on pages created by `paginate` processor). 374 | 375 | - `exec [ ....]` - exec a command with (optional) arguments. 376 | 377 | - `exectext [ ....] ` - exec a command with (optional) 378 | arguments and last argument (presumably some text) bound to command's 379 | stdin. If you need to do something hard, use it like `{{ exectext "sh" "-c" 380 | "pipe | line" .Content }}`. 381 | 382 | - `excerpt ` - Gets an excerpt from the given text, to a 383 | maximum of `maxWordCount` words. When the text is shortened, it will produce 384 | an `[...]` string, denoting there's more. For example, `The quick brown fox` 385 | with `maxWordCount` of 2 will result in `The quick [...]`. 386 | 387 | - `even ` - tests if `n` is divisible by 2. 388 | 389 | - `odd ` - tests if `n` is not divisible by 2. 390 | 391 | - `count ` - returns a number of words in text. 392 | 393 | - `reading_time ` - returns reading time based on [average reading speed 394 | being 200](https://help.medium.com/hc/en-us/articles/214991667-Read-time). 395 | 396 | - `some ....` - returns first non-nil value as a string 397 | 398 | - `dir ` - returns all but the last element of a path (same as [filepath.Dir](https://golang.org/pkg/path/filepath/#Dir)) 399 | 400 | - `base ` - returns the last element of a path (same as [filepath.Base](https://golang.org/pkg/path/filepath/#Base)) 401 | 402 | - `abcsort ` - returns the pages sorted in alphabetical order of their .Name 403 | 404 | ### Page interface 405 | 406 | - `.Site` - global [site object](#site-interface). 407 | - `.Rule` - rule object, matched by page. 408 | - `.Pattern` - pattern, which matched this page. 409 | - `.Deps` - list of pages, which are dependencies for this page. 410 | - `.Next` - next page in a list of all site pages (use specific PageSlice's 411 | `.Next` method if you need more precise matching). 412 | - `.Prev` - previous page in a list of all site pages (use specific PageSlice's 413 | `.Prev` method if you need more precise matching). 414 | 415 | ---- 416 | 417 | - `.Source` - relative path to page source. 418 | - `.FullPath` - full path to page source. 419 | - `.Path` - relative path to page destination. 420 | - `.OutputPath` - full path to page destination. 421 | - `.ModTime` - page last modification time. 422 | 423 | ---- 424 | 425 | - `.Title` - page title. 426 | - `.Tags` - list of page tags. 427 | - `.Date` - page date, as defined in [page config](#page-config). 428 | - `.Hide` - boolean if page is going to be absent from `{{ .Children }}` or `{{ 429 | .WithTag }}` lists. 430 | - `.Other` - map of all other properties (capitalized) from 431 | [page config](#page-config), like `{{ .Other.Author }}`. 432 | 433 | ---- 434 | 435 | - `.Raw` - page content after preprocessors (i.e. after `config` has stripped it 436 | part), that was originally read from the disk. 437 | - `.Content` - page content. 438 | - `.Url` - page url (i.e. `.Path`, but with `index.html` stripped from the end). 439 | - `.Name` - page name (i.e. last part of `.Url`). 440 | - `.UrlTo ` - relative url from current to some other page. 441 | - `.Rel ` - relative url to given absolute (anchored at `/`) url. 442 | - `.Is ` - checks if page is at passed url (or path) - use it for marking 443 | active elements in menu, for example. 444 | - `.UrlMatches ` - checks if page url matches regular expression 445 | ``. 446 | - `.Has ` - backend for `.Where` and `.WhereNot`, checks if field equals to value, or: 447 | - `"Url"` - calls `UrlMatches` 448 | - `"Tag"` - checks tag is present in `.Tags` 449 | - `"Source"` - [matches](https://golang.org/pkg/path/#Match) source path for `value`. 450 | 451 | ### Paginator interface 452 | 453 | - `.Number` - number of paginator page, first is 1 454 | - `.PathPattern` - whatever was passed as `path-pattern` to `paginate` 455 | (processor)[#processors] 456 | - `.Page` - paginator's own [page](#page-interface) 457 | - `.Pages` - [list of pages](#page-list-interface) 458 | - `.Prev` - previous paginator object (if current is first, then `nil`) 459 | - `.Next` - next paginator object (if current is last, then `nil`) 460 | 461 | ### Page list interface 462 | 463 | - `.Get ` - [page](#page-interface) number ``. 464 | - `.First` - first page. 465 | - `.Last` - last page. 466 | - `.Len` - length of page list. 467 | - `.Prev ` - return page with earlier date than given. Returns nil if no 468 | earlier pages exist or page is not in page list. 469 | - `.Next ` - return page with later date than given. Returns nil if no 470 | later pages exist or page is not in page list. 471 | - `.Slice ` - return pages from `from` to `to` (i.e. from 0 to 10). 472 | 473 | ---- 474 | 475 | - `.Children ` - list of pages, nested under ``. 476 | - `.WithTag ` - list of pages, tagged with ``. 477 | - `.Reverse` - list of pages, sorted in reverse order. 478 | 479 | ---- 480 | 481 | - `.BySource ` - finds a page with source path ``. 482 | - `.ByPath ` - finds a page with resulting path ``. 483 | - `.GlobSource ` - list of pages, [matching](https://golang.org/pkg/path/#Match) source path ``. 484 | - `.Where ` - list of pages, which return `true` for `.Has ` 485 | - `.WhereNot ` - list of pages, which return `false` for `.Has ` 486 | 487 | ### Site interface 488 | 489 | - `.Pages` - [list of all pages](#page-list-interface). 490 | - `.Source` - path to site source. 491 | - `.Output` - path to site destination. 492 | - `.Templates` - list of template files used for the site. 493 | - `.Other` - any other properties (capitalized) defined in site config. 494 | 495 | ## Extensibility 496 | 497 | Obviously, the easiest way to extend gostatic's functionality is to use 498 | `external` [processor](#processors). It makes you able to process files in the 499 | way you want, but is more or less limited to that. There is no API right now to 500 | create pages on the fly (like `tags` processor does) using this method, for 501 | example. 502 | 503 | But `gostatic` itself is a 504 | [library](https://github.com/piranha/gostatic/tree/master/lib), and you can 505 | write your own static site generator using this library. See 506 | [gostatic.go](https://github.com/piranha/gostatic/blob/master/gostatic.go) for 507 | an example of one. 508 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Gostatic Examples 2 | 3 | Those dirs contain solutions to a question "How do I do..." people ask 4 | sometimes. I should've started collecting them earlier, but better later than 5 | never I guess. :) 6 | 7 | ## `single-tag-page` 8 | 9 | If you want to render a single page containing all tags. Idea is that tags are 10 | links to a regular pages, gostatic core has almost no idea about them except for 11 | `Site.WithTag` method. 12 | 13 | So what we do is `ignore` tag pages, and instead create a template which 14 | iterates over `.Site.Pages.Children "tags/*"` pages and then uses their `.Title` 15 | as a tag id. 16 | -------------------------------------------------------------------------------- /examples/single-tag-page/Makefile: -------------------------------------------------------------------------------- 1 | GS ?= gostatic 2 | 3 | compile: 4 | $(GS) config 5 | 6 | w: 7 | $(GS) -w config 8 | -------------------------------------------------------------------------------- /examples/single-tag-page/config: -------------------------------------------------------------------------------- 1 | TEMPLATES = site.tmpl 2 | SOURCE = src 3 | OUTPUT = site 4 | TITLE = Example Site 5 | URL = https://example.com/ 6 | AUTHOR = Your Name 7 | 8 | blog/*.md: 9 | config 10 | ext .html 11 | directorify 12 | tags tags/*.tag 13 | markdown 14 | template post 15 | template page 16 | 17 | tags/*.tag: blog/*.md 18 | ignore 19 | 20 | blog.atom: blog/*.md 21 | inner-template 22 | 23 | *.html: blog/*.md 24 | config 25 | inner-template 26 | template page 27 | -------------------------------------------------------------------------------- /examples/single-tag-page/site.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "header" }} 2 | 3 | 4 | 5 | 6 | 7 | {{ .Site.Other.Title }}{{ if .Title }}: {{ .Title }}{{ end }} 8 | 9 | 10 | 11 | 12 | {{ end }} 13 | 14 | {{ define "footer" }} 15 | 16 | 17 | {{ end }} 18 | 19 | {{define "date"}} 20 | 23 | {{end}} 24 | 25 | {{ define "page" }}{{ template "header" . }} 26 | {{ .Content }} 27 | {{ template "footer" . }}{{ end }} 28 | 29 | {{ define "post" }} 30 |
31 |
32 |

{{ .Title }}

33 |
34 | {{ template "date" .Date }} — 35 | {{ range $i, $t := .Tags }}{{if $i}},{{end}} 36 | {{ $t }}{{ end }} 37 |
38 |
39 |
40 | {{ .Content }} 41 |
42 |
43 | {{ end }} 44 | 45 | {{define "tag"}} 46 | # Pages tagged with {{ .Title }} 47 | {{ range .Site.Pages.WithTag .Title }} 48 | - [{{ .Title }}](../../{{ .Url }}) 49 | {{ end }} 50 | {{ end }} 51 | -------------------------------------------------------------------------------- /examples/single-tag-page/src/blog.atom: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ .Site.Other.Url }} 4 | {{ .Site.Other.Title }} 5 | {{ with .Site.Pages.Children "blog/" }} 6 | {{ .First.Date.Format "2006-01-02T15:04:05Z07:00" }} 7 | {{ end }} 8 | {{ .Site.Other.Author }} 9 | 10 | gostatic 11 | 12 | {{ with .Site.Pages.Children "blog/" }} 13 | {{ range .Slice 0 5 }} 14 | 15 | {{ .Url }} 16 | {{ or .Other.Author .Site.Other.Author }} 17 | {{ html .Title }} 18 | {{ .Date.Format "2006-01-02T15:04:05Z07:00" }} 19 | {{ range .Tags }} 20 | 21 | {{ end }} 22 | 23 | 24 | {{/* .Process runs here in case only feed changed */}} 25 | {{ with cut "
" "
" .Process.Content }} 26 | {{ html . }} 27 | {{ end }} 28 |
29 |
30 | {{ end }} 31 | {{ end }} 32 |
33 | -------------------------------------------------------------------------------- /examples/single-tag-page/src/blog/first.md: -------------------------------------------------------------------------------- 1 | title: First Post 2 | date: 2012-12-12 3 | tags: blog, other 4 | ---- 5 | My first post with [gostatic](https://github.com/piranha/gostatic). 6 | -------------------------------------------------------------------------------- /examples/single-tag-page/src/index.html: -------------------------------------------------------------------------------- 1 | title: Main Page 2 | ---- 3 |
    4 | {{ range .Site.Pages.Children "blog/" }} 5 |
  • 6 | {{ template "date" .Date }} - {{ .Title }} 7 |
  • 8 | {{ end }} 9 |
10 | -------------------------------------------------------------------------------- /examples/single-tag-page/src/static/style.css: -------------------------------------------------------------------------------- 1 | /* put your style rules here */ 2 | -------------------------------------------------------------------------------- /examples/single-tag-page/src/tag.html: -------------------------------------------------------------------------------- 1 | title: All Tags 2 | ---- 3 | 4 |

{{ .Title }}

5 | 6 | {{ range .Site.Pages.GlobSource "tags/*.tag" }} 7 |

{{ .Title }}

8 |
    9 | {{ range .Site.Pages.WithTag .Title }} 10 |
  • 11 | {{ template "date" .Date }} - {{ .Title }} 12 |
  • 13 | {{ end }} 14 |
15 |
16 | {{ end }} 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/piranha/gostatic 2 | 3 | require ( 4 | github.com/alecthomas/chroma/v2 v2.5.0 5 | github.com/bmatcuk/doublestar/v4 v4.6.0 6 | github.com/dlclark/regexp2 v1.8.1 // indirect 7 | github.com/fsnotify/fsnotify v1.6.0 8 | github.com/jessevdk/go-flags v1.5.0 9 | github.com/yuin/goldmark v1.5.4 10 | github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87 11 | golang.org/x/sys v0.5.0 // indirect 12 | gopkg.in/yaml.v3 v3.0.1 13 | ) 14 | 15 | go 1.16 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= 2 | github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= 3 | github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= 4 | github.com/alecthomas/chroma/v2 v2.5.0 h1:CQCdj1BiBV17sD4Bd32b/Bzuiq/EqoNTrnIhyQAZ+Rk= 5 | github.com/alecthomas/chroma/v2 v2.5.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= 6 | github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= 7 | github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= 8 | github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 9 | github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc= 10 | github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 14 | github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 15 | github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= 16 | github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 17 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 18 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 19 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 20 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 21 | github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= 22 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 24 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 25 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 26 | github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 27 | github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= 28 | github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 29 | github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87 h1:Py16JEzkSdKAtEFJjiaYLYBOWGXc1r/xHj/Q/5lA37k= 30 | github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= 31 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 34 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 36 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 37 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 38 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 39 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 40 | -------------------------------------------------------------------------------- /gostatic.go: -------------------------------------------------------------------------------- 1 | // (c) 2012 Alexander Solovyov 2 | // under terms of ISC license 3 | 4 | package main 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | flags "github.com/jessevdk/go-flags" 14 | hotreload "github.com/piranha/gostatic/hotreload" 15 | gostatic "github.com/piranha/gostatic/lib" 16 | "github.com/piranha/gostatic/processors" 17 | ) 18 | 19 | const ( 20 | // ExitCodeOk is used when the application exits without error. 21 | ExitCodeOk = 0 22 | // ExitCodeInvalidFlags is used when invalid flags are passed. 23 | ExitCodeInvalidFlags = 1 24 | // ExitCodeInvalidConfig is used when an invalid configuration file is given. 25 | ExitCodeInvalidConfig = 2 26 | // ExitCodeOther is used in all other situations. 27 | ExitCodeOther = 127 28 | ) 29 | 30 | // Opts contains the flags which have been parsed by go-flags. 31 | type Opts struct { 32 | ShowProcessors bool `long:"processors" description:"show page processors"` 33 | ShowConfig bool `long:"show-config" description:"print config as JSON"` 34 | ShowSummary bool `long:"summary" description:"print all pages on stdout"` 35 | InitExample *string `short:"i" long:"init" description:"create example site"` 36 | DumpPage string `short:"d" long:"dump" description:"print page metadata as JSON (pass path to source or target file)"` 37 | 38 | // checked in Page.Changed() 39 | Force bool `short:"f" long:"force" description:"force building all pages"` 40 | 41 | Watch bool `short:"w" long:"watch" description:"serve site on HTTP, rebuild on changes and hot reload HTML in browser"` 42 | NoHotreload bool `long:"no-hotreload" description:"disable hot reload during --watch"` 43 | Port string `short:"p" long:"port" default:"8000" description:"port to serve on"` 44 | 45 | Verbose bool `short:"v" long:"verbose" description:"enable verbose output"` 46 | Version bool `short:"V" long:"version" description:"show version and exit"` 47 | } 48 | 49 | var opts Opts 50 | 51 | func main() { 52 | argparser := flags.NewParser(&opts, 53 | flags.PrintErrors|flags.PassDoubleDash|flags.HelpFlag) 54 | argparser.Usage = "[OPTIONS] path/to/config\n\nBuild a site." 55 | 56 | args, err := argparser.Parse() 57 | 58 | if err != nil { 59 | if _, ok := err.(*flags.Error); ok { 60 | return 61 | } 62 | 63 | errhandle(fmt.Errorf("unknown error: %v", err)) 64 | os.Exit(ExitCodeOther) 65 | } 66 | 67 | if opts.ShowSummary && opts.Watch { 68 | errhandle(fmt.Errorf("--summary and --watch do not mix together well")) 69 | os.Exit(ExitCodeOther) 70 | } 71 | 72 | if opts.Verbose { 73 | gostatic.DEBUG = true 74 | } 75 | 76 | if opts.Version { 77 | out("gostatic %s\n", gostatic.VERSION) 78 | return 79 | } 80 | 81 | if opts.InitExample != nil { 82 | target, _ := os.Getwd() 83 | if len(*opts.InitExample) > 0 { 84 | // If an absolute path was given, use verbatim. Otherwise rebase path 85 | // on top of current working directory. 86 | if strings.HasPrefix(*opts.InitExample, "/") { 87 | target = *opts.InitExample 88 | } else { 89 | target = filepath.Join(target, *opts.InitExample) 90 | } 91 | } 92 | gostatic.WriteExample(target) 93 | return 94 | } 95 | 96 | if opts.ShowProcessors { 97 | processors.DefaultProcessors.ProcessorSummary() 98 | return 99 | } 100 | 101 | if len(args) == 0 { 102 | argparser.WriteHelp(os.Stderr) 103 | os.Exit(ExitCodeInvalidFlags) 104 | return 105 | } 106 | 107 | // config, err := gostatic.NewSiteConfig(args[0]) 108 | // if err != nil { 109 | // errhandle(fmt.Errorf("invalid config file '%s': %v", args[0], err)) 110 | // os.Exit(ExitCodeInvalidConfig) 111 | // } 112 | 113 | site := gostatic.NewSite(args[0], processors.DefaultProcessors) 114 | 115 | if opts.Force { 116 | site.ForceRefresh = true 117 | } 118 | 119 | if opts.ShowConfig { 120 | x, err := json.MarshalIndent(site.SiteConfig, "", " ") 121 | errhandle(err) 122 | fmt.Fprintln(os.Stderr, string(x)) 123 | return 124 | } 125 | 126 | if len(opts.DumpPage) > 0 { 127 | page := site.PageBySomePath(opts.DumpPage) 128 | if page == nil { 129 | out("Page '%s' not found (supply source or destination path)\n", 130 | opts.DumpPage) 131 | return 132 | } 133 | dump, err := json.MarshalIndent(page, "", " ") 134 | errhandle(err) 135 | out("%s\n", dump) 136 | return 137 | } 138 | 139 | if opts.ShowSummary { 140 | site.Summary() 141 | } else { 142 | site.Render() 143 | } 144 | 145 | if opts.Watch { 146 | err := hotreload.Watch([]string{site.SiteConfig.Source}, site.SiteConfig.Templates, 147 | func() { 148 | site.Reconfig() 149 | site.Render() 150 | }) 151 | errhandle(err) 152 | 153 | out("Starting server at *:%s...\n", opts.Port) 154 | 155 | err = hotreload.ServeHTTP(site.SiteConfig.Output, opts.Port, !opts.NoHotreload) 156 | errhandle(err) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /hotreload/assets.go: -------------------------------------------------------------------------------- 1 | package hotreload 2 | 3 | import _ "embed" 4 | 5 | //go:embed assets/morphdom.js 6 | var Morphdom []byte 7 | 8 | //go:embed assets/hotreload.js 9 | var Script []byte 10 | -------------------------------------------------------------------------------- /hotreload/assets/hotreload.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 6 */ 2 | 3 | (function() { 4 | function esconnect() { 5 | var es = new EventSource('/.gostatic.hotreload'); 6 | es.onmessage = function(e) { 7 | // console.log(e); 8 | localStorage.hotreloaddebug && console.log(e.data); 9 | enqueue(e.data); 10 | } 11 | window.addEventListener('beforeunload', _ => es.close()); 12 | } 13 | esconnect(); 14 | 15 | var MESSAGES = new Set(); 16 | var timeout, timeoutMs; 17 | 18 | function enqueue(msg) { 19 | MESSAGES.add(msg); 20 | // start with 32ms and double every message up to 1000 21 | timeoutMs = timeout ? Math.min(timeoutMs * 2, 1000) : 32; 22 | clearTimeout(timeout); 23 | timeout = setTimeout(execute, timeoutMs); 24 | } 25 | 26 | function execute() { 27 | localStorage.hotreloaddebug && console.log('reload', MESSAGES); 28 | MESSAGES.forEach(mode => RELOADERS[mode]()); 29 | MESSAGES.clear(); 30 | timeout = null; 31 | } 32 | 33 | 34 | var RELOADERS = { 35 | start() { 36 | console.log('hotreload connection established'); 37 | }, 38 | page() { 39 | fetch(window.location.href, {mode: 'same-origin', 40 | headers: {'X-With': 'hotreload'}}) 41 | .then(res => res.text()) 42 | .then(text => { 43 | morphdom(document.documentElement, text); 44 | // document.documentElement.innerHTML = text; 45 | var e = new Event('hotreload', {'bubbles': true}); 46 | window.dispatchEvent(e); 47 | }) 48 | .catch(e => { 49 | if (e.message != "The operation was aborted. ") { 50 | console.log(e); 51 | } 52 | }); 53 | }, 54 | css() { 55 | // This snippet pinched from quickreload, under the MIT license: 56 | // https://github.com/bjoerge/quickreload/blob/master/client.js 57 | var killcache = '__gostatic=' + new Date().getTime(); 58 | var stylesheets = Array.prototype.slice.call( 59 | document.querySelectorAll('link[rel="stylesheet"]') 60 | ); 61 | stylesheets.forEach(function (el) { 62 | var href = el.href.replace(/(&|\?)__gostatic\=\d+/, ''); 63 | el.href = ''; 64 | el.href = href + (href.indexOf("?") == -1 ? '?' : '&') + killcache; 65 | }); 66 | }}; 67 | })(); 68 | -------------------------------------------------------------------------------- /hotreload/assets/morphdom.js: -------------------------------------------------------------------------------- 1 | // morphdom 2.7.0 2 | (function (global, factory) { 3 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 4 | typeof define === 'function' && define.amd ? define(factory) : 5 | (global = global || self, global.morphdom = factory()); 6 | }(this, function () { 'use strict'; 7 | 8 | var DOCUMENT_FRAGMENT_NODE = 11; 9 | 10 | function morphAttrs(fromNode, toNode) { 11 | var toNodeAttrs = toNode.attributes; 12 | var attr; 13 | var attrName; 14 | var attrNamespaceURI; 15 | var attrValue; 16 | var fromValue; 17 | 18 | // document-fragments dont have attributes so lets not do anything 19 | if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) { 20 | return; 21 | } 22 | 23 | // update attributes on original DOM element 24 | for (var i = toNodeAttrs.length - 1; i >= 0; i--) { 25 | attr = toNodeAttrs[i]; 26 | attrName = attr.name; 27 | attrNamespaceURI = attr.namespaceURI; 28 | attrValue = attr.value; 29 | 30 | if (attrNamespaceURI) { 31 | attrName = attr.localName || attrName; 32 | fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName); 33 | 34 | if (fromValue !== attrValue) { 35 | if (attr.prefix === 'xmlns'){ 36 | attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix 37 | } 38 | fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue); 39 | } 40 | } else { 41 | fromValue = fromNode.getAttribute(attrName); 42 | 43 | if (fromValue !== attrValue) { 44 | fromNode.setAttribute(attrName, attrValue); 45 | } 46 | } 47 | } 48 | 49 | // Remove any extra attributes found on the original DOM element that 50 | // weren't found on the target element. 51 | var fromNodeAttrs = fromNode.attributes; 52 | 53 | for (var d = fromNodeAttrs.length - 1; d >= 0; d--) { 54 | attr = fromNodeAttrs[d]; 55 | attrName = attr.name; 56 | attrNamespaceURI = attr.namespaceURI; 57 | 58 | if (attrNamespaceURI) { 59 | attrName = attr.localName || attrName; 60 | 61 | if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) { 62 | fromNode.removeAttributeNS(attrNamespaceURI, attrName); 63 | } 64 | } else { 65 | if (!toNode.hasAttribute(attrName)) { 66 | fromNode.removeAttribute(attrName); 67 | } 68 | } 69 | } 70 | } 71 | 72 | var range; // Create a range object for efficently rendering strings to elements. 73 | var NS_XHTML = 'http://www.w3.org/1999/xhtml'; 74 | 75 | var doc = typeof document === 'undefined' ? undefined : document; 76 | var HAS_TEMPLATE_SUPPORT = !!doc && 'content' in doc.createElement('template'); 77 | var HAS_RANGE_SUPPORT = !!doc && doc.createRange && 'createContextualFragment' in doc.createRange(); 78 | 79 | function createFragmentFromTemplate(str) { 80 | var template = doc.createElement('template'); 81 | template.innerHTML = str; 82 | return template.content.childNodes[0]; 83 | } 84 | 85 | function createFragmentFromRange(str) { 86 | if (!range) { 87 | range = doc.createRange(); 88 | range.selectNode(doc.body); 89 | } 90 | 91 | var fragment = range.createContextualFragment(str); 92 | return fragment.childNodes[0]; 93 | } 94 | 95 | function createFragmentFromWrap(str) { 96 | var fragment = doc.createElement('body'); 97 | fragment.innerHTML = str; 98 | return fragment.childNodes[0]; 99 | } 100 | 101 | /** 102 | * This is about the same 103 | * var html = new DOMParser().parseFromString(str, 'text/html'); 104 | * return html.body.firstChild; 105 | * 106 | * @method toElement 107 | * @param {String} str 108 | */ 109 | function toElement(str) { 110 | str = str.trim(); 111 | if (HAS_TEMPLATE_SUPPORT) { 112 | // avoid restrictions on content for things like `Hi` which 113 | // createContextualFragment doesn't support 114 | //