├── .commitlog.release ├── .github └── workflows │ └── docs.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── docs ├── hooks │ ├── 00-copy-readme.lua │ └── 01-add-navigation.lua ├── lib │ └── utils.lua ├── pages │ ├── 00-readme.md │ ├── 01-basics.md │ ├── 03-concepts.md │ ├── 04-dev-server.md │ ├── 05-CLI.md │ ├── 06-recipes.md │ ├── 404.html │ ├── _layout.html │ ├── concepts │ │ ├── scripting.md │ │ └── writers.md │ └── index.md └── public │ └── styles.css ├── go.mod ├── go.sum ├── lua └── alvu │ └── alvu.go ├── main.go ├── readme.md └── scripts └── cross-compile.sh /.commitlog.release: -------------------------------------------------------------------------------- 1 | v0.2.16 -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | on: [push] 3 | 4 | permissions: 5 | contents: read 6 | pages: write 7 | id-token: write 8 | 9 | concurrency: 10 | group: "pages" 11 | cancel-in-progress: false 12 | 13 | jobs: 14 | build-docs: 15 | concurrency: ci-${{ github.ref }} 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Pages 23 | id: pages 24 | uses: actions/configure-pages@v5 25 | 26 | - name: Install and Build 27 | run: make docs 28 | 29 | - name: Upload artifact 30 | uses: actions/upload-pages-artifact@v3 31 | with: 32 | path: ./dist 33 | 34 | deploy: 35 | environment: 36 | name: github-pages 37 | url: ${{ steps.deployment.outputs.page_url }} 38 | needs: build-docs 39 | runs-on: ubuntu-latest 40 | name: Deploy 41 | steps: 42 | - name: Deploy to GitHub Pages 43 | id: deployment 44 | uses: actions/deploy-pages@v4 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lab 2 | /dist 3 | /alvu 4 | /.tmp 5 | .DS_Store 6 | /bin 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.2.13 2 | ee5f992a504fa929e4d780ac0073d8b26db6127f Merge pull request #3 from barelyhuman/feat/fs-poller 3 | 4 | ## v0.2.12 5 | 3a4ab4b1384ad0f90945cddc59e97cfe49762917 fix: socket reset 6 | 7 | ## v0.2.11 8 | bfdb93636c3682163eacffbbb9acbf65214e22c6 fix: watcher cleanup, sub process for watcher 9 | 10 | ## v0.2.10 11 | bd4e055e401f2516e0260c3eb9b24c758eb5ac61 fix: handle nested directory watching 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-Present reaper a.k.a Siddharth Gelera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY.: all 3 | 4 | all: clean build 5 | 6 | clean: 7 | rm -rf alvu 8 | 9 | build: 10 | go build -ldflags '-s -w' 11 | 12 | demo: 13 | go run . --path lab 14 | 15 | docs: build 16 | ./alvu --path="docs" --baseurl="/alvu/" --highlight --hard-wrap=false 17 | 18 | docs_dev: build 19 | ./alvu --highlight --hard-wrap=false --serve --path='./docs' 20 | 21 | pages: docs 22 | rm -rf alvu 23 | rm -rf .tmp 24 | mkdir .tmp 25 | mv dist/* .tmp 26 | git checkout pages 27 | rm -rf ./* 28 | mv .tmp/* . 29 | git add -A; git commit -m "update pages"; git push origin pages; 30 | git checkout main 31 | 32 | cross: 33 | ./scripts/cross-compile.sh 34 | -------------------------------------------------------------------------------- /docs/hooks/00-copy-readme.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable-next-line: undefined-global 2 | local wdir = workingdir 3 | 4 | local json = require("json") 5 | 6 | ForFile = "00-readme.md" 7 | 8 | function Writer(filedata) 9 | local sourcedata = json.decode(filedata) 10 | if sourcedata.name == "00-readme.html" 11 | then 12 | local f = assert(io.open(wdir.."/../readme.md", "rb")) 13 | local content = f:read("*all") 14 | f:close() 15 | sourcedata.content = content 16 | end 17 | return json.encode(sourcedata) 18 | end -------------------------------------------------------------------------------- /docs/hooks/01-add-navigation.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable-next-line: undefined-global 2 | local wdir = workingdir 3 | package.path = package.path .. ";" .. wdir .. "/lib/?.lua" 4 | 5 | local json = require("json") 6 | local alvu = require("alvu") 7 | local utils = require(wdir .. ".lib.utils") 8 | 9 | function Writer(filedata) 10 | local pagesPath = wdir .. "/pages" 11 | local index = {} 12 | local files = alvu.files(pagesPath) 13 | 14 | for fileIndex = 1, #files do 15 | local file_name = files[fileIndex] 16 | if not (file_name == "_layout.html" or file_name == "index.md" or utils.starts_with(file_name,"concepts/")) 17 | then 18 | local name = string.gsub(file_name, ".md", "") 19 | name = string.gsub(name, ".html", "") 20 | local title, _ = utils.normalize(name):lower() 21 | 22 | table.insert(index, { 23 | name = title, 24 | slug = name 25 | }) 26 | end 27 | end 28 | 29 | table.insert(index, 1, { 30 | name = "..", 31 | slug = "index" 32 | }) 33 | 34 | local source_data = json.decode(filedata) 35 | 36 | local template = [[ 37 |
38 | 44 |
45 |
46 | ]] 47 | 48 | source_data.content = template .. "\n" .. source_data.content .. "
" 49 | source_data.data = { 50 | index = index 51 | } 52 | 53 | return json.encode(source_data) 54 | end 55 | -------------------------------------------------------------------------------- /docs/lib/utils.lua: -------------------------------------------------------------------------------- 1 | local Lib = {} 2 | 3 | function Lib.normalize(name) 4 | return name 5 | :gsub("-", " ") 6 | :gsub("^%d+", "") 7 | end 8 | 9 | function Lib.totitlecase(name) 10 | return string 11 | .gsub(name, "(%l)(%w*)", function(a, b) 12 | return string.upper(a) .. b 13 | end) 14 | end 15 | 16 | function Lib.starts_with(str,start) 17 | return string.sub(str,1,string.len(start))==start 18 | end 19 | 20 | 21 | return Lib 22 | -------------------------------------------------------------------------------- /docs/pages/00-readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | [readme](../../readme.md) 5 | -------------------------------------------------------------------------------- /docs/pages/01-basics.md: -------------------------------------------------------------------------------- 1 | # Basics 2 | 3 | Yeah, so to use `alvu`, you just run it. 4 | 5 | ```sh 6 | $ alvu 7 | ``` 8 | 9 | Yep, that's it, that's all you do. 10 | 11 | Mainly because the tool comes with it's preferred defaults and they are as 12 | follows. 13 | 14 | - a directory named `pages` 15 | - a directory named `hooks` 16 | - a directory named `public` 17 | 18 | and each one of them represent exactly what they are called. 19 | 20 | ## Pages 21 | 22 | This is simply a collection of files that you're website would be made up of, 23 | these can be of the following formats. 24 | 25 | - `.md` - Markdown - Will be converted to HTML 26 | - `.html` - HTML - Will be converted to nothing. 27 | - `.xml` - XML - Will be converted to nothing. 28 | 29 | **So, just a markdown processor huh?** 30 | 31 | Yeah... and no. 32 | 33 | ### Special Files 34 | 35 | - `_head.html` - will add the header section to the final HTML (deprecated in v0.2.7) 36 | - `_tail.html` - will add the footer section to the final HTML (deprecated in v0.2.7) 37 | - `_layout.html` - defines a common layout for all files that'll be rendered. 38 | - `404.html` - alvu will serve this file whenever the requested page is not found (Nested within `_layout.html`, if exists). This is only true for the development mode, for built dist, if the deployed platform needs special handling for the 404 static file, then that'll need to be configured by you accordingly 39 | 40 | The `_head.html` and `_tail.html` files were used as placeholders for 41 | repeated layout across your markdown files, this has now been replaced 42 | by the `_layout.html` file which wraps around your markdown content and 43 | can be defined as shown below 44 | 45 | ```go-html-template 46 | 47 | 48 | 49 | 50 | { { .Content } } 51 | 52 | 53 | ``` 54 | 55 | `.Content` can be used as a slot or placeholder to be replaced by the content of each markdown file. 56 | 57 | > **Note**: Make sure to remove the spaces between the `{` and `}` in the above code snippet, these were added to avoid getting replaced by the template code 58 | 59 | We deprecated `_head.html` and `_tail.html` because they would cause abnormalities in the HTML output causing certain element tags to be duplicated. Which isn't semantically correct, also the template execution for these would end up creating arbitrary string nodes at the end of the HTML, which isn't intentional. 60 | 61 | The fix for this would include writing an HTML dedupe handler, which might be a project in itself considering all the edge cases. It was easier to just let golang templates get what they want, hence the introduction of the `_layout.html` file. 62 | 63 | ## Hooks 64 | 65 | The other reason for writing `alvu` was to be able to extend simple functionalities when 66 | needed, and this is where the concept of hooks comes in. 67 | 68 | Hooks are a collection of `.lua` files that can each have a `Writer` 69 | function, this function receives the data of the file that's currently being processed. 70 | 71 | So, if `alvu` is processing `index.md` then you will get the name, path, its current content, which you can return as is or change it and that'll override the content for the compiled version of the file. 72 | 73 | For example, you can check this very documentation site's source code to check how the navigation is added 74 | to each file. 75 | 76 | ## Public 77 | 78 | Pretty self-explanatory but the `public` folder will copy everything 79 | put into it to the `dist` folder. This can be used for assets, styles, etc. 80 | 81 | Let's move forward to [scripting →]({{.Meta.BaseURL}}concepts/scripting) 82 | -------------------------------------------------------------------------------- /docs/pages/03-concepts.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | Explanation of primary blocks of functionality when working with 4 | alvu 5 | 6 | - [Scripting]({{.Meta.BaseURL}}concepts/scripting) 7 | - [Writers and Hooks]({{.Meta.BaseURL}}concepts/writers) -------------------------------------------------------------------------------- /docs/pages/04-dev-server.md: -------------------------------------------------------------------------------- 1 | # Development Server 2 | 3 | The tool comes with it's own dev server (since v0.2.4), and can be used to run a 4 | simple file server for the output folder. 5 | 6 | It's as simple as running the following command 7 | 8 | ```sh 9 | $ alvu --serve 10 | # short for 11 | $ alvu --serve --port=3000 12 | ``` 13 | 14 | ## Live Reload 15 | 16 | Added in `v0.2.9` 17 | 18 | There's no changes that you need to do, just upgrade your installation to 19 | `v0.2.9` 20 | 21 | #### What's to be expected 22 | 23 | - Will reload on changes from the directories `pages`, `public` , or if you 24 | changed them with flags then the respective paths will be watched instead 25 | 26 | - The rebuilding process is atomic and will recompile a singular file if that's 27 | all that's changed instead of compiling the whole folder. This is only true 28 | for files in the `pages` directory, if any changes were made in `public` 29 | directory then the whole alvu setup will rebuild itself again. 30 | 31 | #### Caveats 32 | 33 | - `./hooks` are not watched, this is because hooks have their own state and 34 | involve a VM, the current structure of alvu stops us from doing this but 35 | should be done soon without increasing complexity or size of alvu itself. 36 | -------------------------------------------------------------------------------- /docs/pages/05-CLI.md: -------------------------------------------------------------------------------- 1 | # CLI Reference 2 | 3 | This can also be accessed by using the `-h` flag on the binary. 4 | 5 | ```sh 6 | $ alvu -h 7 | ``` 8 | 9 | ``` 10 | Usage of alvu: 11 | -baseurl URL 12 | URL to be used as the root of the project (default "/") 13 | -hard-wrap
14 | enable hard wrapping of elements with
(default true) 15 | -highlight 16 | enable highlighting for markdown files 17 | -highlight-theme THEME 18 | THEME to use for highlighting (supports most themes from pygments) (default "bw") 19 | -hooks DIR 20 | DIR that contains hooks for the content (default "./hooks") 21 | -out DIR 22 | DIR to output the compiled files to (default "./dist") 23 | -path DIR 24 | DIR to search for the needed folders in (default ".") 25 | -port PORT 26 | PORT to start the server on (default "3000") 27 | -serve 28 | start a local server 29 | ``` 30 | 31 | [Check out Recipes →]({{.Meta.BaseURL}}06-recipes) 32 | -------------------------------------------------------------------------------- /docs/pages/06-recipes.md: -------------------------------------------------------------------------------- 1 | # Recipes 2 | 3 | ### TOC 4 | 5 | - [Watching for Changes](#watching-for-changes) 6 | - [Importing Other lua files](#importing-other-lua-files) 7 | - [String Interpolation](#string-interpolation) 8 | - [String Functions](#string-functions) 9 | - [Get Files from a Dir](#get-files-from-a-directory) 10 | - [Reading Writing Files](#reading--writing-files) 11 | - [Getting network Data](#getting-network-data) 12 | - [Templates](#templates) 13 | 14 | Methods and ways to be able to do basic tasks while working with alvu 15 | 16 | ## Watching for changes 17 | 18 | I use [entr](https://github.com/eradman/entr) as a file notifier which can run 19 | arbitrary commands on file changes, which can be done like so to have alvu 20 | monitor for files 21 | 22 | ```sh 23 | ls docs/**/* | entr -cr alvu --path='./docs' 24 | ``` 25 | 26 | > **Note**: since v0.2.9, alvu comes with it's own file watcher and live-reload 27 | > but is limited to the `public` and `pages` directory and it is still 28 | > recommended to use `entr` if you wish to handle watching custom paths 29 | 30 | This will list all files in the `docs` folder (root of an alvu project) and then 31 | run the alvu command while specifying the base path to be `./docs` 32 | 33 | ## Importing other lua files 34 | 35 | You'll need to work with lua files that are in a sibling directory in the 36 | project and you can do so by adding them to the scripts `package.path` like so 37 | 38 | ```lua 39 | -- specify that you wish to taken in any `.lua` file in the `lib` folder 40 | package.path = package.path .. ";../lib/?.lua" 41 | 42 | -- require lib/utils.lua to use utilities from it 43 | local lib = require("lib.utils") 44 | ``` 45 | 46 | ## String Interpolation 47 | 48 | There's no way to directly do string interpolation in lua but is almost always 49 | needed so here's how you can implement a small helper for it 50 | 51 | ```lua 52 | local function interpolate(s, tab) 53 | return (s:gsub('($%b{})', function(w) return tab[w:sub(3, -2)] or w end)) 54 | end 55 | 56 | -- usage 57 | interpolate("this is an ${message} string", { message = "interpolated" }) 58 | ``` 59 | 60 | ## String Functions 61 | 62 | A helper library is injected into all alvu hook files which can be required into 63 | the script to help with basic string manipulation and querying 64 | 65 | ```lua 66 | local strings = require("strings") 67 | if strings.contains("hello world", "hello") 68 | then 69 | print("found hello") 70 | end 71 | ``` 72 | 73 | You can read more about these from the 74 | [gopher-lua-libs](https://github.com/vadv/gopher-lua-libs/tree/master/strings) 75 | repo. 76 | 77 | ## Get files from a directory 78 | 79 | If working with blogs and nested data you might wanna get files in a directory 80 | and you can use the following function 81 | 82 | There's 2 methods, you can either use the `alvu` helper library or you can add 83 | the `scandir` function shown below to your `libs` folder if you wish to maintain 84 | control over the file reading functionality 85 | 86 | - Using `alvu` helpers 87 | 88 | ```lua 89 | local alvu = require("alvu") 90 | local all_files = alvu.files(path) 91 | ``` 92 | 93 | - Custom function 94 | 95 | ```lua 96 | function scandir(directory) 97 | local i, t, popen = 0, {}, io.popen 98 | 99 | local pfile = popen('ls -a "' .. directory .. '"') 100 | if pfile then 101 | for filename in pfile:lines() do 102 | i = i + 1 103 | t[i] = filename 104 | end 105 | pfile:close() 106 | end 107 | return t 108 | end 109 | ``` 110 | 111 | ## Reading / Writing files 112 | 113 | This can be done with native lua functions but here's a snippet of the 114 | `onFinish` hook from [reaper.is](https://github.com/barelyhuman/reaper.is)' RSS 115 | Feed hook 116 | 117 | ```lua 118 | function OnFinish() 119 | -- attempt to open the template file in read mode 120 | local rss_temp_fd = io.open("dist/rss_tmpl.xml", "r") 121 | -- attempt to open the final file in write mode 122 | local rss_fd = io.open("dist/rss.xml", "w") 123 | 124 | -- check if the file descriptors are available and usable 125 | if rss_temp_fd and rss_fd 126 | then 127 | -- read the entire template file's body 128 | -- which contains the rss tags 129 | local body = "" 130 | for c in rss_temp_fd:lines() do 131 | body = body .. "\n" .. c 132 | end 133 | 134 | -- generate a rss file template for the following 135 | -- site data 136 | local rss_data = rss_template({ 137 | site_name = "reaper", 138 | site_link = "https://reaper.is", 139 | site_description = "reaper's rants,notes and stuff", 140 | itembody = body 141 | }) 142 | 143 | -- write the whole thing to the final rss.xml file 144 | rss_fd:write(rss_data) 145 | end 146 | end 147 | ``` 148 | 149 | ## Getting Network Data 150 | 151 | Getting data at build time for dynamic data is a very common usecase and this is 152 | something that alvu supports. 153 | 154 | The network data needs to be fetched from a hook, let's take a simple example. 155 | 156 | I need to get downloads for a certain set of packages that've been published to 157 | the NPM Registry. 158 | 159 | ```lua 160 | -- require the needed packages. 161 | -- both http and json are injected packages by alvu and 162 | -- dont need any dependencies from lua to be added 163 | local http = require("http") 164 | local json = require("json") 165 | 166 | -- we only want this hook to run for the file 167 | -- packages.md 168 | ForFile = "packages.md" 169 | 170 | local npm_url = "https://api.npmjs.org/downloads/point/last-month/" 171 | 172 | -- tiny utility function that takes in 173 | -- a pkg_name and returns the number of downloads from 174 | -- the request. 175 | -- we use the `json` utility to parse the text response 176 | -- into a json 177 | local function get_downloads_for_pkg(pkg_name) 178 | local response,error_message = http.get(npm_url..pkg_name) 179 | local body_json = json.decode(response.body) 180 | return body_json.downloads 181 | end 182 | 183 | local packages = { 184 | "@barelyhuman/tocolor", 185 | "@barelyhuman/pipe", 186 | "@barelyhuman/preact-island-plugins", 187 | "@barelyreaper/themer", 188 | "jotai-form" 189 | } 190 | 191 | function Writer(source_data) 192 | local source = json.decode(source_data) 193 | 194 | -- create a table with the download count for each package, 195 | -- mentioned in the `packages` variable 196 | local downloads_table = {} 197 | 198 | for k,pkg_name in ipairs(packages) do 199 | local download_count = get_downloads_for_pkg(pkg_name) 200 | table.insert(downloads_table,{ 201 | title = pkg_name, 202 | downloads = download_count 203 | }) 204 | end 205 | 206 | -- Pass down the data to file that's being rendered, in this case `packages.md` 207 | return json.encode({ 208 | data = { 209 | packages = downloads_table, 210 | }, 211 | }) 212 | end 213 | ``` 214 | 215 | Once you have the hook ready, you can now add in the file this is for. 216 | `pages/packages.md` 217 | 218 | Which would look, something like this. 219 | 220 | ```md 221 | # Packages 222 | 223 | | name | downloads(last 30 days) | 224 | | -------------------------- | ----------------------- | 225 | | { {range .Data.packages} } | | 226 | | { {.title} } | { {.downloads} } | 227 | | { {end} } | | 228 | ``` 229 | 230 | Here the `range .Data.packages` loops through the elements that were returned 231 | from the hook on the `data` parameter, and you can now point to the variables 232 | you need to get the required data into the template. 233 | 234 | ## Templates 235 | 236 | The most preferred way of using alvu is to avoid having to construct hooks and 237 | use existing example repositories as the source, this gives us the advantage of 238 | not having to spend time writing similar static site generation logic while 239 | keeping it easy to extend. 240 | 241 | ### Official Templates 242 | 243 | [alvu-foam-template](https://github.com/barelyhuman/alvu-foam-template) - Foam 244 | plugin template for writing a wiki styled website 245 | 246 | ### Community Templates 247 | 248 | Feel free to add in your templates here via PR's on the 249 | [source repo](http://github.com/barelyhuman/alvu) or mailing 250 | [me](mailto:ahoy@barelyhuman.dev) 251 | -------------------------------------------------------------------------------- /docs/pages/404.html: -------------------------------------------------------------------------------- 1 |
2 |

404

3 |

You seem lost...

4 |

5 | ← Go Back? 6 |

7 |
8 | -------------------------------------------------------------------------------- /docs/pages/_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | alvu | documentation 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | {{.Content}} 19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/pages/concepts/scripting.md: -------------------------------------------------------------------------------- 1 | # Scripting 2 | 3 | The whole point of writing this as a CLI instead of just a simple script to 4 | convert folders of markdown was to be able to add and extend the basic 5 | functionality as needed. 6 | 7 | Here's where the `hooks` folder is being used and this part of the documentation 8 | is a quick reference on how this works. 9 | 10 | ## Language 11 | 12 | [lua](https://www.lua.org) is the language of choice, primarily because it's 13 | very easy to add more functionality by just bringing in `.lua` files from the 14 | web and adding them to your project. 15 | 16 | Here's what a simple writer function would look like, this function adds a 17 | `Hello World` heading to every html/markdown file that alvu would process. 18 | 19 | ```lua 20 | -- add-hello-world.lua 21 | 22 | -- a special json library injected by alvu 23 | local json = require("json") 24 | 25 | function Writer(filedata) 26 | -- convert the file's information into 27 | -- a lua accessible table 28 | local source_data = json.decode(filedata) 29 | 30 | -- concatenate the heading with the rest of the content 31 | source_data.content = "

Hello World

" .. source_data.content 32 | 33 | -- send back serialized json data to the tool 34 | return json.encode(source_data) 35 | end 36 | ``` 37 | 38 | While the comments might already help you out, let's just lay them down as 39 | simple points 40 | 41 | 1. You get data from alvu in the `Writer` function as a serialized/stringified JSON. 42 | 2. You can manipulate the data in lua using the helper library `json` 43 | 3. Return the data as a serialized JSON, so the modifications can be processed. 44 | 45 | You'll learn more about helper libraries as we move ahead. 46 | 47 | ## Data Injection 48 | 49 | There are going to be cases where you might wanna pass back data from the lua 50 | function to the templates and if you wish to build an automatic 51 | `index.html` file that lists all the posts of your blog from a certain folder. 52 | 53 | This can be done by passing one of two keys in the returned JSON. `data` or 54 | `extras` 55 | 56 | Something like this, 57 | 58 | ```lua 59 | local json = require("json") 60 | 61 | function Writer() 62 | return json.encode({ 63 | data = { 64 | name = "reaper" 65 | } 66 | -- or 67 | extras = { 68 | name = "reaper" 69 | } 70 | }) 71 | end 72 | ``` 73 | 74 | You don't have to send back the whole `source_content` for each file since it's 75 | only merged when you send something back, else the original is kept as is and 76 | not all keys that you send back are taken into consideration. 77 | 78 | If you did send the above data `name = "reaper"` and would like to access it in 79 | all templates, you could just do the following 80 | 81 | ```go-html-template 82 |

83 | { { .Data.name } } 84 |

85 | 86 |

87 | { { .Extras.name } } 88 |

89 | ``` 90 | 91 | The lua table could nest as much as needed and the same could be accessed in the 92 | templates, but try to avoid it. 93 | 94 | and yes, **all templates**, each hook runs on each template and every 95 | manipulation cascades on the other so if you are running a lot of hooks, name 96 | them with numbers (eg: `01-add-navigation.lua` ) if you wish for them to follow a 97 | certain sequence 98 | 99 | ## Single File Hooks 100 | 101 | In most cases you use hooks for common functionality but there's definitely cases 102 | where just one file needs to be processed with hook and that's easily doable with 103 | alvu. 104 | 105 | You use the `ForFile` global value in the lua hook and the hook will only run for that 106 | file. An example of this would look something like this. 107 | 108 | ```lua 109 | ---@diagnostic disable-next-line: undefined-global 110 | local wdir = workingdir 111 | 112 | local json = require("json") 113 | 114 | ForFile = "00-readme.md" 115 | 116 | function Writer(filedata) 117 | local sourcedata = json.decode(filedata) 118 | if sourcedata.name == "00-readme.html" 119 | then 120 | local f = assert(io.open(wdir.."/../readme.md", "rb")) 121 | local content = f:read("*all") 122 | f:close() 123 | sourcedata.content = content 124 | end 125 | return json.encode(sourcedata) 126 | end 127 | ``` 128 | 129 | The above only runs for the file `00-readme.md` and is responsible for copying the contents 130 | of the `readme.md` and overwriting the `00-readme.md` file's content with it at **build time** 131 | 132 | [More about Writers → ]({{.Meta.BaseURL}}concepts/writers) 133 | -------------------------------------------------------------------------------- /docs/pages/concepts/writers.md: -------------------------------------------------------------------------------- 1 | # Writer and Hooks 2 | 3 | The tool comes with 3 basic hooks 4 | 5 | 1. OnStart 6 | 2. Writer 7 | 3. OnFinish 8 | 9 | Each of them is a simple lua function and might later move to go plugins if the 10 | need arises or if people would like to be able to talk to alvu in various 11 | languages. 12 | 13 | > **Note**: The choice of lua was made because it's easy to pass down new 14 | > utilities to the language but then there's obvious cases where the language 15 | > falls behind. (regex, string manipulations, etc etc) 16 | 17 | ## `OnStart` 18 | 19 | This hook is triggered right before processing the files and it's going to get 20 | called just once per hook file, and as applies with other hook rules, these will 21 | be cascaded, so if you are working with writing and deleting files, please make 22 | sure you order the hooks with file names 23 | 24 | ## `Writer` 25 | 26 | The [Scripting]({{.Meta.BaseURL}}concepts/scripting) section, covers most of what this writer does but 27 | to reiterate, the `Writer` hooks are called for everyfile in the `pages` 28 | directory and allow you to manipulate the content of the file before it gets 29 | compiled 30 | 31 | ## `OnFinish` 32 | 33 | This hook is triggered right after all the processing as completed and the files 34 | have been compiled. This is primarily for you to be able to run cleanup tasks 35 | but is not limited to that. 36 | 37 | [Read the CLI reference →]({{.Meta.BaseURL}}05-CLI) 38 | -------------------------------------------------------------------------------- /docs/pages/index.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | You can skip to individual sections above. I know a search would be 4 | helpful (patience, will add it some day) 5 | 6 | Well alvu is _something_, I don't know what to call it but it was built with the 7 | purpose of being able to generate static websites. 8 | 9 | Yes, I wrote another one of these, pretty amazing right? (_\*sigh\*_) 10 | 11 | **Why build another one?** 12 | 13 | I wanted to, that's it. Also, the idea of being able to add scripting to a 14 | rather tight flow made me think this'd work well. 15 | 16 | As always, a tiny little tool built for me and hopefully someday someone else 17 | might like it. 18 | 19 | Well, let's head to [the basics →]({{.Meta.BaseURL}}01-basics) 20 | -------------------------------------------------------------------------------- /docs/public/styles.css: -------------------------------------------------------------------------------- 1 | /* base style from https://oaklang.org/ */ 2 | /* modified for usecase accordingly */ 3 | 4 | :root { 5 | --font-sans: "IBM Plex Sans", system-ui, sans-serif; 6 | --font-mono: "IBM Plex Mono", "Menlo", monospace; 7 | --primary-bg: #fdfeff; 8 | --primary-text: #111111; 9 | --secondary-bg: #f1f1f8; 10 | --secondary-text: #9b9b9b; 11 | --underline-bg: #aaafb5; 12 | } 13 | 14 | /* @media (prefers-color-scheme: dark) { 15 | :root { 16 | --primary-bg: #191815; 17 | --primary-text: rgba(166, 166, 166, 1); 18 | --secondary-bg: #333; 19 | --secondary-text: #a4a7a9; 20 | --hover-bg: #474c50; 21 | --active-bg: #626569; 22 | --underline-bg: #6d7176; 23 | } 24 | } */ 25 | 26 | html, 27 | body { 28 | margin: 0; 29 | padding: 0; 30 | box-sizing: border-box; 31 | font-size: 16px; 32 | } 33 | 34 | body { 35 | font-family: var(--font-sans); 36 | color: var(--primary-text); 37 | background: var(--primary-bg); 38 | 39 | border-bottom: 8px solid #333; 40 | 41 | display: flex; 42 | flex-direction: column; 43 | min-height: 100vh; 44 | } 45 | 46 | header, 47 | footer { 48 | flex-grow: 0; 49 | flex-shrink: 0; 50 | } 51 | 52 | main { 53 | flex-grow: 1; 54 | flex-shrink: 0; 55 | } 56 | 57 | a { 58 | color: var(--primary-text); 59 | } 60 | 61 | .container { 62 | max-width: 96ch; 63 | width: calc(100% - 2rem); 64 | margin-left: auto; 65 | margin-right: auto; 66 | } 67 | 68 | h1, 69 | h2, 70 | h3 { 71 | line-height: 1.5em; 72 | color: var(--primary-text); 73 | margin-top: 1.5em; 74 | margin-bottom: 0.75em; 75 | font-weight: normal; 76 | } 77 | 78 | h1 { 79 | margin-top: 1em; 80 | font-size: 2em; 81 | } 82 | 83 | h2 { 84 | font-size: 1.5em; 85 | } 86 | 87 | h3 { 88 | font-size: 1.2em; 89 | } 90 | 91 | p, 92 | li { 93 | line-height: 1.5em; 94 | margin: 1em 0; 95 | max-width: 64ch; 96 | } 97 | 98 | p img.blend-multiply { 99 | mix-blend-mode: multiply; 100 | } 101 | 102 | /* HEADER */ 103 | 104 | header a { 105 | text-decoration: none; 106 | font-size: 1.125em; 107 | } 108 | 109 | header a:hover { 110 | text-decoration: underline; 111 | } 112 | 113 | header .overlay, 114 | nav { 115 | display: flex; 116 | flex-direction: row; 117 | align-items: center; 118 | justify-content: flex-start; 119 | height: 100%; 120 | } 121 | 122 | header { 123 | background: var(--primary-bg); 124 | height: 100px; 125 | } 126 | 127 | nav { 128 | gap: 1.5ch; 129 | } 130 | 131 | nav a { 132 | font-weight: normal; 133 | white-space: nowrap; 134 | } 135 | 136 | /* ARTICLE */ 137 | 138 | main { 139 | overflow: hidden; 140 | } 141 | 142 | details { 143 | margin: 1em 0; 144 | } 145 | 146 | summary { 147 | cursor: pointer; 148 | } 149 | 150 | summary > h2, 151 | summary > h3, 152 | summary > h4 { 153 | display: inline; 154 | margin-left: 8px; 155 | } 156 | 157 | table a, 158 | article p a, 159 | article li a { 160 | text-decoration-color: var(--underline-bg); 161 | text-underline-offset: 1px; 162 | text-decoration-thickness: 2px; 163 | transition: text-decoration-color 0.2s; 164 | } 165 | 166 | table a:hover, 167 | article p a:hover, 168 | article li a:hover { 169 | text-decoration-color: var(--secondary-text); 170 | } 171 | 172 | p img { 173 | width: 100vw; 174 | max-width: unset; 175 | position: relative; 176 | top: 0; 177 | left: -16px; 178 | } 179 | 180 | pre, 181 | code, 182 | kbd { 183 | font-family: var(--font-mono); 184 | tab-size: 4; 185 | } 186 | 187 | pre { 188 | display: block; 189 | margin: 1.5em 0; 190 | border-radius: 6px; 191 | background: var(--secondary-bg); 192 | overflow-x: auto; 193 | overflow-y: hidden; 194 | -webkit-overflow-scrolling: touch; 195 | width: 100%; 196 | display: flex; 197 | flex-direction: row; 198 | } 199 | 200 | pre > code { 201 | flex-shrink: 0; 202 | display: block; 203 | padding: 1em; 204 | font-size: calc(1em - 2px); 205 | -webkit-text-size-adjust: none; 206 | } 207 | 208 | pre > code { 209 | width: 100%; 210 | } 211 | 212 | code, 213 | kbd { 214 | background: var(--secondary-bg); 215 | } 216 | 217 | h1 code, 218 | h2 code, 219 | h3 code, 220 | h4 code, 221 | p code, 222 | p kbd, 223 | li code, 224 | li kbd { 225 | padding: 3px 5px; 226 | border-radius: 4px; 227 | } 228 | 229 | blockquote { 230 | font-style: italic; 231 | } 232 | 233 | blockquote em, 234 | blockquote pre { 235 | font-style: normal; 236 | } 237 | 238 | pre, 239 | code { 240 | font-size: 1.09em; 241 | line-height: 2em; 242 | } 243 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/barelyhuman/alvu 2 | 3 | go 1.22 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/barelyhuman/go v0.2.2-0.20230713173609-2ee88bb52634 9 | github.com/cjoudrey/gluahttp v0.0.0-20201111170219-25003d9adfa9 10 | github.com/joho/godotenv v1.5.1 11 | github.com/vadv/gopher-lua-libs v0.4.1 12 | github.com/yuin/goldmark v1.7.10 13 | github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 14 | github.com/yuin/gopher-lua v1.1.0 15 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2 16 | gopkg.in/yaml.v3 v3.0.1 17 | layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf 18 | ) 19 | 20 | require ( 21 | github.com/alecthomas/chroma v0.10.0 // indirect 22 | github.com/dlclark/regexp2 v1.4.0 // indirect 23 | gopkg.in/yaml.v2 v2.3.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= 2 | github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= 3 | github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= 4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 8 | github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= 9 | github.com/aws/aws-sdk-go v1.33.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= 10 | github.com/barelyhuman/go v0.2.2-0.20230713173609-2ee88bb52634 h1:a53Bc1LuSAB9rGbQkBopsYFJNVTgeoUSgnd0do7PDxw= 11 | github.com/barelyhuman/go v0.2.2-0.20230713173609-2ee88bb52634/go.mod h1:hox2iDYZAarjpS7jKQeYIi2F+qMA8KLMtCws++L2sSY= 12 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 13 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 14 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 15 | github.com/cbroglie/mustache v1.0.1/go.mod h1:R/RUa+SobQ14qkP4jtx5Vke5sDytONDQXNLPY/PO69g= 16 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 17 | github.com/cheggaaa/pb/v3 v3.0.5/go.mod h1:X1L61/+36nz9bjIsrDU52qHKOQukUQe2Ge+YvGuquCw= 18 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 19 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 20 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 21 | github.com/cjoudrey/gluahttp v0.0.0-20201111170219-25003d9adfa9 h1:rdWOzitWlNYeUsXmz+IQfa9NkGEq3gA/qQ3mOEqBU6o= 22 | github.com/cjoudrey/gluahttp v0.0.0-20201111170219-25003d9adfa9/go.mod h1:X97UjDTXp+7bayQSFZk2hPvCTmTZIicUjZQRtkwgAKY= 23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= 27 | github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 28 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 29 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 30 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 31 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 32 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 33 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 34 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 35 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 36 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 37 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 38 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 40 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 41 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 42 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 43 | github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= 44 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 45 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 46 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 47 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 48 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 49 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 50 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 51 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 52 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 53 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 54 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 55 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 56 | github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 57 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 58 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 59 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 60 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 61 | github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= 62 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 63 | github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 64 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 65 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 66 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 67 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 68 | github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 69 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 70 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 71 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 72 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 73 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 74 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 75 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 76 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 77 | github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 78 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 79 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 80 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 81 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 82 | github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 83 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 84 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 85 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 86 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 87 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 88 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 89 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 90 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 91 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 92 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 93 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 94 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 95 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 96 | github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= 97 | github.com/vadv/gopher-lua-libs v0.4.1 h1:NgxYEQ0C027X1U348GnFBxf6S8nqYtgHUEuZnA6w2bU= 98 | github.com/vadv/gopher-lua-libs v0.4.1/go.mod h1:j16bcBLqJUwpQT75QztdmfOa8J7CXMmf8BLbtvAR9NY= 99 | github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7/go.mod h1:bbMEM6aU1WDF1ErA5YJ0p91652pGv140gGw4Ww3RGp8= 100 | github.com/yuin/goldmark v1.4.5/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= 101 | github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI= 102 | github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 103 | github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 h1:yHfZyN55+5dp1wG7wDKv8HQ044moxkyGq12KFFMFDxg= 104 | github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594/go.mod h1:U9ihbh+1ZN7fR5Se3daSPoz1CGF9IYtSvWwVQtnzGHU= 105 | github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= 106 | github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= 107 | github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= 108 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 109 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 110 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 111 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 112 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= 113 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 114 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 116 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 119 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 120 | golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 121 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 122 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 123 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 127 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 128 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 129 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 130 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 131 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 132 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 133 | gopkg.in/xmlpath.v2 v2.0.0-20150820204837-860cbeca3ebc/go.mod h1:N8UOSI6/c2yOpa/XDz3KVUiegocTziPiqNkeNTMiG1k= 134 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 135 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 136 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 137 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 138 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 139 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 140 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 141 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 142 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 143 | layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf h1:rRz0YsF7VXj9fXRF6yQgFI7DzST+hsI3TeFSGupntu0= 144 | layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf/go.mod h1:ivKkcY8Zxw5ba0jldhZCYYQfGdb2K6u9tbYK1AwMIBc= 145 | -------------------------------------------------------------------------------- /lua/alvu/alvu.go: -------------------------------------------------------------------------------- 1 | package alvu 2 | 3 | import ( 4 | "os" 5 | "path" 6 | 7 | dotenv "github.com/joho/godotenv" 8 | lua "github.com/yuin/gopher-lua" 9 | ) 10 | 11 | var api = map[string]lua.LGFunction{ 12 | "files": GetFilesIndex, 13 | "get_env": GetEnv, 14 | } 15 | 16 | // Preload adds json to the given Lua state's package.preload table. After it 17 | // has been preloaded, it can be loaded using require: 18 | // 19 | // local json = require("json") 20 | func Preload(L *lua.LState) { 21 | L.PreloadModule("alvu", Loader) 22 | } 23 | 24 | // Loader is the module loader function. 25 | func Loader(L *lua.LState) int { 26 | t := L.NewTable() 27 | L.SetFuncs(t, api) 28 | L.Push(t) 29 | return 1 30 | } 31 | 32 | // Decode lua json.decode(string) returns (table, err) 33 | func GetFilesIndex(L *lua.LState) int { 34 | // path to get the index for 35 | str := L.CheckString(1) 36 | 37 | value, err := LGetFilesIndex(L, str) 38 | if err != nil { 39 | L.Push(lua.LNil) 40 | L.Push(lua.LString(err.Error())) 41 | return 2 42 | } 43 | 44 | L.Push(value) 45 | return 1 46 | } 47 | 48 | func LGetFilesIndex(L *lua.LState, pathToIndex string) (*lua.LTable, error) { 49 | indexedPaths, err := getFilesIndex(pathToIndex) 50 | if err != nil { 51 | return nil, err 52 | } 53 | arr := L.CreateTable(len(indexedPaths), 0) 54 | for _, item := range indexedPaths { 55 | arr.Append(lua.LString(item)) 56 | } 57 | 58 | return arr, nil 59 | } 60 | 61 | func getFilesIndex(pathToIndex string) (paths []string, err error) { 62 | files, err := os.ReadDir(pathToIndex) 63 | if err != nil { 64 | return 65 | } 66 | 67 | for _, f := range files { 68 | if f.IsDir() { 69 | nestPath := path.Join(pathToIndex, f.Name()) 70 | nestPaths, err := getFilesIndex(nestPath) 71 | if err != nil { 72 | return paths, err 73 | } 74 | withPathPrefix := []string{} 75 | for _, _path := range nestPaths { 76 | withPathPrefix = append(withPathPrefix, path.Join(f.Name(), _path)) 77 | } 78 | paths = append(paths, withPathPrefix...) 79 | } else { 80 | paths = append(paths, f.Name()) 81 | } 82 | } 83 | 84 | return 85 | } 86 | 87 | func GetEnv(L *lua.LState) int { 88 | // path to get the env from 89 | str := L.CheckString(1) 90 | // key to get from index 91 | key := L.CheckString(2) 92 | value := LGetEnv(L, str, key) 93 | L.Push(value) 94 | return 1 95 | } 96 | 97 | func LGetEnv(L *lua.LState, fromFile string, str string) lua.LString { 98 | dotenv.Load(fromFile) 99 | val := os.Getenv(str) 100 | return lua.LString(val) 101 | } 102 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "html/template" 10 | textTmpl "text/template" 11 | 12 | "io" 13 | "io/fs" 14 | "log" 15 | "net/http" 16 | "os" 17 | "path/filepath" 18 | "regexp" 19 | "runtime" 20 | "strings" 21 | "sync" 22 | 23 | _ "embed" 24 | 25 | "github.com/barelyhuman/go/env" 26 | "github.com/barelyhuman/go/poller" 27 | ghttp "github.com/cjoudrey/gluahttp" 28 | 29 | "github.com/barelyhuman/go/color" 30 | 31 | stringsLib "github.com/vadv/gopher-lua-libs/strings" 32 | 33 | yamlLib "github.com/vadv/gopher-lua-libs/yaml" 34 | "github.com/yuin/goldmark" 35 | "github.com/yuin/goldmark/extension" 36 | "github.com/yuin/goldmark/parser" 37 | "github.com/yuin/goldmark/renderer" 38 | "github.com/yuin/goldmark/renderer/html" 39 | 40 | highlighting "github.com/yuin/goldmark-highlighting" 41 | 42 | lua "github.com/yuin/gopher-lua" 43 | "gopkg.in/yaml.v3" 44 | 45 | luaAlvu "github.com/barelyhuman/alvu/lua/alvu" 46 | "golang.org/x/net/websocket" 47 | luajson "layeh.com/gopher-json" 48 | ) 49 | 50 | const logPrefix = "[alvu] " 51 | 52 | var mdProcessor goldmark.Markdown 53 | var baseurl string 54 | var basePath string 55 | var outPath string 56 | var hardWraps bool 57 | var hookCollection HookCollection 58 | var reloadCh = []chan bool{} 59 | var serveFlag *bool 60 | var notFoundPageExists bool 61 | 62 | //go:embed .commitlog.release 63 | var release string 64 | 65 | var layoutFiles []string = []string{"_head.html", "_tail.html", "_layout.html"} 66 | 67 | type SiteMeta struct { 68 | BaseURL string 69 | } 70 | 71 | type PageRenderData struct { 72 | Meta SiteMeta 73 | Data map[string]interface{} 74 | Extras map[string]interface{} 75 | } 76 | 77 | type LayoutRenderData struct { 78 | PageRenderData 79 | Content template.HTML 80 | } 81 | 82 | // TODO: move stuff into the alvu struct type 83 | // on each newly added feature or during improving 84 | // older features. 85 | type Alvu struct { 86 | publicPath string 87 | files []*AlvuFile 88 | filesIndex []string 89 | } 90 | 91 | func (al *Alvu) AddFile(file *AlvuFile) { 92 | al.files = append(al.files, file) 93 | al.filesIndex = append(al.filesIndex, file.sourcePath) 94 | } 95 | 96 | func (al *Alvu) IsAlvuFile(filePath string) bool { 97 | for _, af := range al.filesIndex { 98 | if af == filePath { 99 | return true 100 | } 101 | } 102 | return false 103 | } 104 | 105 | func (al *Alvu) Build() { 106 | for ind := range al.files { 107 | alvuFile := al.files[ind] 108 | alvuFile.Build() 109 | } 110 | 111 | onDebug(func() { 112 | debugInfo("Run all OnFinish Hooks") 113 | memuse() 114 | }) 115 | 116 | // right before completion run all hooks again but for the onFinish 117 | hookCollection.RunAll("OnFinish") 118 | } 119 | 120 | func (al *Alvu) CopyPublic() { 121 | onDebug(func() { 122 | debugInfo("Before copying files") 123 | memuse() 124 | }) 125 | // copy public to out 126 | _, err := os.Stat(al.publicPath) 127 | if err == nil { 128 | err = copyDir(al.publicPath, outPath) 129 | if err != nil { 130 | bail(err) 131 | } 132 | } 133 | onDebug(func() { 134 | debugInfo("After copying files") 135 | memuse() 136 | }) 137 | } 138 | 139 | func main() { 140 | onDebug(func() { 141 | debugInfo("Before Exec") 142 | memuse() 143 | }) 144 | 145 | var versionFlag bool 146 | 147 | flag.BoolVar(&versionFlag, "version", false, "version info") 148 | flag.BoolVar(&versionFlag, "v", false, "version info") 149 | basePathFlag := flag.String("path", ".", "`DIR` to search for the needed folders in") 150 | outPathFlag := flag.String("out", "./dist", "`DIR` to output the compiled files to") 151 | baseurlFlag := flag.String("baseurl", "/", "`URL` to be used as the root of the project") 152 | hooksPathFlag := flag.String("hooks", "./hooks", "`DIR` that contains hooks for the content") 153 | enableHighlightingFlag := flag.Bool("highlight", false, "enable highlighting for markdown files") 154 | highlightThemeFlag := flag.String("highlight-theme", "bw", "`THEME` to use for highlighting (supports most themes from pygments)") 155 | serveFlag = flag.Bool("serve", false, "start a local server") 156 | hardWrapsFlag := flag.Bool("hard-wrap", true, "enable hard wrapping of elements with `
`") 157 | portFlag := flag.String("port", "3000", "`PORT` to start the server on") 158 | pollDurationFlag := flag.Int("poll", 350, "Polling duration for file changes in milliseconds") 159 | 160 | flag.Parse() 161 | 162 | // Show version and exit 163 | if versionFlag { 164 | println(release) 165 | os.Exit(0) 166 | } 167 | 168 | baseurl = *baseurlFlag 169 | basePath = filepath.Join(*basePathFlag) 170 | pagesPath := filepath.Join(*basePathFlag, "pages") 171 | publicPath := filepath.Join(*basePathFlag, "public") 172 | headFilePath := filepath.Join(pagesPath, "_head.html") 173 | baseFilePath := filepath.Join(pagesPath, "_layout.html") 174 | tailFilePath := filepath.Join(pagesPath, "_tail.html") 175 | notFoundFilePath := filepath.Join(pagesPath, "404.html") 176 | outPath = filepath.Join(*outPathFlag) 177 | hooksPath := filepath.Join(*basePathFlag, *hooksPathFlag) 178 | hardWraps = *hardWrapsFlag 179 | 180 | headTailDeprecationWarning := color.ColorString{} 181 | headTailDeprecationWarning.Yellow(logPrefix).Yellow("[WARN] use of _tail.html and _head.html is deprecated, please use _layout.html instead") 182 | 183 | os.MkdirAll(publicPath, os.ModePerm) 184 | 185 | alvuApp := &Alvu{ 186 | publicPath: publicPath, 187 | } 188 | 189 | watcher := NewWatcher(alvuApp, *pollDurationFlag) 190 | 191 | if *serveFlag { 192 | watcher.AddDir(pagesPath) 193 | watcher.AddDir(publicPath) 194 | } 195 | 196 | onDebug(func() { 197 | debugInfo("Opening _head") 198 | memuse() 199 | }) 200 | headFileFd, err := os.Open(headFilePath) 201 | if err != nil { 202 | if err == fs.ErrNotExist { 203 | log.Println("no _head.html found,skipping") 204 | } 205 | } else { 206 | fmt.Println(headTailDeprecationWarning.String()) 207 | } 208 | 209 | onDebug(func() { 210 | debugInfo("Opening _layout") 211 | memuse() 212 | }) 213 | baseFileFd, err := os.Open(baseFilePath) 214 | if err != nil { 215 | if err == fs.ErrNotExist { 216 | log.Println("no _layout.html found,skipping") 217 | } 218 | } 219 | 220 | onDebug(func() { 221 | debugInfo("Opening _tail") 222 | memuse() 223 | }) 224 | tailFileFd, err := os.Open(tailFilePath) 225 | if err != nil { 226 | if err == fs.ErrNotExist { 227 | log.Println("no _tail.html found, skipping") 228 | } 229 | } else { 230 | fmt.Println(headTailDeprecationWarning.String()) 231 | } 232 | 233 | onDebug(func() { 234 | debugInfo("Checking if 404.html exists") 235 | memuse() 236 | }) 237 | if _, err := os.Stat(notFoundFilePath); errors.Is(err, os.ErrNotExist) { 238 | notFoundPageExists = false 239 | log.Println("no 404.html found, skipping") 240 | } else { 241 | notFoundPageExists = true 242 | } 243 | 244 | alvuApp.CopyPublic() 245 | 246 | onDebug(func() { 247 | debugInfo("Reading hook and to process files") 248 | memuse() 249 | }) 250 | CollectHooks(basePath, hooksPath) 251 | toProcess := CollectFilesToProcess(pagesPath) 252 | onDebug(func() { 253 | log.Println("printing files to process") 254 | log.Println(toProcess) 255 | }) 256 | 257 | initMDProcessor(*enableHighlightingFlag, *highlightThemeFlag) 258 | 259 | onDebug(func() { 260 | debugInfo("Running all OnStart hooks") 261 | memuse() 262 | }) 263 | 264 | hookCollection.RunAll("OnStart") 265 | 266 | prefixSlashPath := regexp.MustCompile(`^\/`) 267 | 268 | onDebug(func() { 269 | debugInfo("Creating Alvu Files") 270 | memuse() 271 | }) 272 | for _, toProcessItem := range toProcess { 273 | fileName := strings.Replace(toProcessItem, pagesPath, "", 1) 274 | fileName = prefixSlashPath.ReplaceAllString(fileName, "") 275 | destFilePath := strings.Replace(toProcessItem, pagesPath, outPath, 1) 276 | isHTML := strings.HasSuffix(fileName, ".html") 277 | 278 | alvuFile := &AlvuFile{ 279 | lock: &sync.Mutex{}, 280 | sourcePath: toProcessItem, 281 | hooks: hookCollection, 282 | destPath: destFilePath, 283 | name: fileName, 284 | isHTML: isHTML, 285 | headFile: headFileFd, 286 | tailFile: tailFileFd, 287 | baseTemplate: baseFileFd, 288 | data: map[string]interface{}{}, 289 | extras: map[string]interface{}{}, 290 | } 291 | 292 | alvuApp.AddFile(alvuFile) 293 | 294 | // If serving, also add the nested path into it 295 | if *serveFlag { 296 | watcher.AddDir(filepath.Dir(alvuFile.sourcePath)) 297 | } 298 | } 299 | 300 | alvuApp.Build() 301 | 302 | onDebug(func() { 303 | runtime.GC() 304 | debugInfo("On Completions") 305 | memuse() 306 | }) 307 | 308 | cs := &color.ColorString{} 309 | fmt.Println(cs.Blue(logPrefix).Green("Compiled ").Cyan("\"" + basePath + "\"").Green(" to ").Cyan("\"" + outPath + "\"").String()) 310 | 311 | if *serveFlag { 312 | watcher.StartWatching() 313 | runServer(*portFlag) 314 | } 315 | 316 | hookCollection.Shutdown() 317 | } 318 | 319 | func runServer(port string) { 320 | normalizedPort := port 321 | 322 | if !strings.HasPrefix(normalizedPort, ":") { 323 | normalizedPort = ":" + normalizedPort 324 | } 325 | 326 | cs := &color.ColorString{} 327 | cs.Blue(logPrefix).Green("Serving on").Reset(" ").Cyan(normalizedPort) 328 | fmt.Println(cs.String()) 329 | 330 | http.Handle("/", http.HandlerFunc(ServeHandler)) 331 | AddWebsocketHandler() 332 | 333 | err := http.ListenAndServe(normalizedPort, nil) 334 | 335 | if strings.Contains(err.Error(), "address already in use") { 336 | bail(errors.New("port already in use, use another port with the `-port` flag instead")) 337 | } 338 | } 339 | 340 | func CollectFilesToProcess(basepath string) []string { 341 | files := []string{} 342 | 343 | pathstoprocess, err := os.ReadDir(basepath) 344 | if err != nil { 345 | panic(err) 346 | } 347 | 348 | for _, pathInfo := range pathstoprocess { 349 | _path := filepath.Join(basepath, pathInfo.Name()) 350 | 351 | if Contains(layoutFiles, pathInfo.Name()) { 352 | continue 353 | } 354 | 355 | if pathInfo.IsDir() { 356 | files = append(files, CollectFilesToProcess(_path)...) 357 | } else { 358 | files = append(files, _path) 359 | } 360 | 361 | } 362 | 363 | return files 364 | } 365 | 366 | func CollectHooks(basePath, hooksBasePath string) { 367 | if _, err := os.Stat(hooksBasePath); err != nil { 368 | return 369 | } 370 | pathsToProcess, err := os.ReadDir(hooksBasePath) 371 | if err != nil { 372 | panic(err) 373 | } 374 | 375 | for _, pathInfo := range pathsToProcess { 376 | if !strings.HasSuffix(pathInfo.Name(), ".lua") { 377 | continue 378 | } 379 | hook := NewHook() 380 | hookPath := filepath.Join(hooksBasePath, pathInfo.Name()) 381 | if err := hook.DoFile(hookPath); err != nil { 382 | panic(err) 383 | } 384 | hookCollection = append(hookCollection, &Hook{ 385 | path: hookPath, 386 | state: hook, 387 | }) 388 | } 389 | 390 | } 391 | 392 | func initMDProcessor(highlight bool, theme string) { 393 | 394 | rendererOptions := []renderer.Option{ 395 | html.WithXHTML(), 396 | html.WithUnsafe(), 397 | } 398 | 399 | if hardWraps { 400 | rendererOptions = append(rendererOptions, html.WithHardWraps()) 401 | } 402 | gmPlugins := []goldmark.Option{ 403 | goldmark.WithExtensions(extension.GFM, extension.Footnote), 404 | goldmark.WithParserOptions( 405 | parser.WithAutoHeadingID(), 406 | ), 407 | goldmark.WithRendererOptions( 408 | rendererOptions..., 409 | ), 410 | } 411 | 412 | if highlight { 413 | gmPlugins = append(gmPlugins, goldmark.WithExtensions( 414 | highlighting.NewHighlighting( 415 | highlighting.WithStyle(theme), 416 | ), 417 | )) 418 | } 419 | 420 | mdProcessor = goldmark.New(gmPlugins...) 421 | } 422 | 423 | type Hook struct { 424 | path string 425 | state *lua.LState 426 | } 427 | 428 | type HookCollection []*Hook 429 | 430 | func (hc HookCollection) Shutdown() { 431 | for _, hook := range hc { 432 | hook.state.Close() 433 | } 434 | } 435 | 436 | func (hc HookCollection) RunAll(funcName string) { 437 | for _, hook := range hc { 438 | hookFunc := hook.state.GetGlobal(funcName) 439 | 440 | if hookFunc == lua.LNil { 441 | continue 442 | } 443 | 444 | if err := hook.state.CallByParam(lua.P{ 445 | Fn: hookFunc, 446 | NRet: 0, 447 | Protect: true, 448 | }); err != nil { 449 | bail(err) 450 | } 451 | } 452 | } 453 | 454 | type AlvuFile struct { 455 | lock *sync.Mutex 456 | hooks HookCollection 457 | name string 458 | sourcePath string 459 | isHTML bool 460 | destPath string 461 | meta map[string]interface{} 462 | content []byte 463 | writeableContent []byte 464 | headFile *os.File 465 | tailFile *os.File 466 | baseTemplate *os.File 467 | targetName []byte 468 | data map[string]interface{} 469 | extras map[string]interface{} 470 | } 471 | 472 | func (alvuFile *AlvuFile) Build() { 473 | bail(alvuFile.ReadFile()) 474 | bail(alvuFile.ParseMeta()) 475 | 476 | if len(alvuFile.hooks) == 0 { 477 | alvuFile.ProcessFile(nil) 478 | } 479 | 480 | for _, hook := range hookCollection { 481 | 482 | isForSpecificFile := hook.state.GetGlobal("ForFile") 483 | 484 | if isForSpecificFile != lua.LNil { 485 | if alvuFile.name == isForSpecificFile.String() { 486 | alvuFile.ProcessFile(hook.state) 487 | } else { 488 | bail(alvuFile.ProcessFile(nil)) 489 | } 490 | } else { 491 | bail(alvuFile.ProcessFile(hook.state)) 492 | } 493 | } 494 | 495 | alvuFile.FlushFile() 496 | } 497 | 498 | func (af *AlvuFile) ReadFile() error { 499 | filecontent, err := os.ReadFile(af.sourcePath) 500 | if err != nil { 501 | return fmt.Errorf("error reading file, error: %v", err) 502 | } 503 | af.content = filecontent 504 | return nil 505 | } 506 | 507 | func (af *AlvuFile) ParseMeta() error { 508 | sep := []byte("---") 509 | if !bytes.HasPrefix(af.content, sep) { 510 | af.writeableContent = af.content 511 | return nil 512 | } 513 | 514 | metaParts := bytes.SplitN(af.content, sep, 3) 515 | 516 | var meta map[string]interface{} 517 | err := yaml.Unmarshal([]byte(metaParts[1]), &meta) 518 | if err != nil { 519 | return err 520 | } 521 | 522 | af.meta = meta 523 | af.writeableContent = []byte(metaParts[2]) 524 | 525 | return nil 526 | } 527 | 528 | func (af *AlvuFile) ProcessFile(hook *lua.LState) error { 529 | // pre process hook => should return back json with `content` and `data` 530 | af.lock.Lock() 531 | defer af.lock.Unlock() 532 | 533 | af.targetName = regexp.MustCompile(`\.md$`).ReplaceAll([]byte(af.name), []byte(".html")) 534 | onDebug(func() { 535 | debugInfo(af.name + " will be changed to " + string(af.targetName)) 536 | }) 537 | 538 | buf := bytes.NewBuffer([]byte("")) 539 | mdToHTML := "" 540 | 541 | if filepath.Ext(af.name) == ".md" { 542 | newName := strings.Replace(af.name, filepath.Ext(af.name), ".html", 1) 543 | af.targetName = []byte(newName) 544 | mdProcessor.Convert(af.writeableContent, buf) 545 | mdToHTML = buf.String() 546 | } 547 | 548 | if hook == nil { 549 | return nil 550 | } 551 | 552 | hookInput := struct { 553 | Name string `json:"name"` 554 | SourcePath string `json:"source_path"` 555 | DestPath string `json:"dest_path"` 556 | Meta map[string]interface{} `json:"meta"` 557 | WriteableContent string `json:"content"` 558 | HTMLContent string `json:"html"` 559 | }{ 560 | Name: string(af.targetName), 561 | SourcePath: af.sourcePath, 562 | DestPath: af.destPath, 563 | Meta: af.meta, 564 | WriteableContent: string(af.writeableContent), 565 | HTMLContent: mdToHTML, 566 | } 567 | 568 | hookJsonInput, err := json.Marshal(hookInput) 569 | bail(err) 570 | 571 | if err := hook.CallByParam(lua.P{ 572 | Fn: hook.GetGlobal("Writer"), 573 | NRet: 1, 574 | Protect: true, 575 | }, lua.LString(hookJsonInput)); err != nil { 576 | panic(err) 577 | } 578 | 579 | ret := hook.Get(-1) 580 | 581 | var fromPlug map[string]interface{} 582 | 583 | err = json.Unmarshal([]byte(ret.String()), &fromPlug) 584 | bail(err) 585 | 586 | if fromPlug["content"] != nil { 587 | stringVal := fmt.Sprintf("%s", fromPlug["content"]) 588 | af.writeableContent = []byte(stringVal) 589 | } 590 | 591 | if fromPlug["name"] != nil { 592 | af.targetName = []byte(fmt.Sprintf("%v", fromPlug["name"])) 593 | } 594 | 595 | if fromPlug["data"] != nil { 596 | af.data = mergeMapWithCheck(af.data, fromPlug["data"]) 597 | } 598 | 599 | if fromPlug["extras"] != nil { 600 | af.extras = mergeMapWithCheck(af.extras, fromPlug["extras"]) 601 | } 602 | 603 | hook.Pop(1) 604 | return nil 605 | } 606 | 607 | func (af *AlvuFile) FlushFile() { 608 | destFolder := filepath.Dir(af.destPath) 609 | os.MkdirAll(destFolder, os.ModePerm) 610 | 611 | justFileName := strings.TrimSuffix( 612 | filepath.Base(af.destPath), 613 | filepath.Ext(af.destPath), 614 | ) 615 | 616 | targetFile := strings.Replace(filepath.Join(af.destPath), af.name, string(af.targetName), 1) 617 | if justFileName != "index" && justFileName != "404" { 618 | targetFile = filepath.Join(filepath.Dir(af.destPath), justFileName, "index.html") 619 | os.MkdirAll(filepath.Dir(targetFile), os.ModePerm) 620 | } 621 | 622 | onDebug(func() { 623 | debugInfo("flushing for file: " + af.name + string(af.targetName)) 624 | debugInfo("flusing file: " + targetFile) 625 | }) 626 | 627 | f, err := os.Create(targetFile) 628 | bail(err) 629 | defer f.Sync() 630 | 631 | writeHeadTail := false 632 | 633 | if af.baseTemplate == nil && (filepath.Ext(af.sourcePath) == ".md" || filepath.Ext(af.sourcePath) == "html") { 634 | writeHeadTail = true 635 | } 636 | 637 | if writeHeadTail && af.headFile != nil { 638 | shouldCopyContentsWithReset(af.headFile, f) 639 | } 640 | 641 | renderData := PageRenderData{ 642 | Meta: SiteMeta{ 643 | BaseURL: baseurl, 644 | }, 645 | Data: af.data, 646 | Extras: af.extras, 647 | } 648 | 649 | // Run the Markdown file through the conversion 650 | // process to be able to use template variables in 651 | // the markdown instead of writing them in 652 | // raw HTML 653 | var preConvertHTML bytes.Buffer 654 | preConvertTmpl := textTmpl.New("temporary_pre_template") 655 | preConvertTmpl.Parse(string(af.writeableContent)) 656 | err = preConvertTmpl.Execute(&preConvertHTML, renderData) 657 | bail(err) 658 | 659 | var toHtml bytes.Buffer 660 | if !af.isHTML { 661 | err = mdProcessor.Convert(preConvertHTML.Bytes(), &toHtml) 662 | bail(err) 663 | } else { 664 | toHtml = preConvertHTML 665 | } 666 | 667 | layoutData := LayoutRenderData{ 668 | PageRenderData: renderData, 669 | Content: template.HTML(toHtml.Bytes()), 670 | } 671 | 672 | // If a layout file was found 673 | // write the converted html content into the 674 | // layout template file 675 | 676 | layout := template.New("layout") 677 | var layoutTemplateData string 678 | if af.baseTemplate != nil { 679 | layoutTemplateData = string(readFileToBytes(af.baseTemplate)) 680 | } else { 681 | layoutTemplateData = `{{.Content}}` 682 | } 683 | 684 | layoutTemplateData = _injectLiveReload(&layoutTemplateData) 685 | toHtml.Reset() 686 | layout.Parse(layoutTemplateData) 687 | layout.Execute(&toHtml, layoutData) 688 | 689 | io.Copy( 690 | f, &toHtml, 691 | ) 692 | 693 | if writeHeadTail && af.tailFile != nil && af.baseTemplate == nil { 694 | shouldCopyContentsWithReset(af.tailFile, f) 695 | } 696 | 697 | data, err := os.ReadFile(targetFile) 698 | bail(err) 699 | 700 | onDebug(func() { 701 | debugInfo("template path: %v", af.sourcePath) 702 | }) 703 | 704 | t := template.New(filepath.Join(af.sourcePath)) 705 | t.Parse(string(data)) 706 | 707 | f.Seek(0, 0) 708 | 709 | err = t.Execute(f, renderData) 710 | bail(err) 711 | } 712 | 713 | func NewHook() *lua.LState { 714 | lState := lua.NewState() 715 | luaAlvu.Preload(lState) 716 | luajson.Preload(lState) 717 | yamlLib.Preload(lState) 718 | stringsLib.Preload(lState) 719 | lState.PreloadModule("http", ghttp.NewHttpModule(&http.Client{}).Loader) 720 | if basePath == "." { 721 | lState.SetGlobal("workingdir", lua.LString("")) 722 | } else { 723 | lState.SetGlobal("workingdir", lua.LString(basePath)) 724 | } 725 | return lState 726 | } 727 | 728 | // UTILS 729 | func memuse() { 730 | var m runtime.MemStats 731 | runtime.ReadMemStats(&m) 732 | fmt.Printf("heap: %v MiB\n", bytesToMB(m.HeapAlloc)) 733 | } 734 | 735 | func bytesToMB(inBytes uint64) uint64 { 736 | return inBytes / 1024 / 1024 737 | } 738 | 739 | func bail(err error) { 740 | if err == nil { 741 | return 742 | } 743 | cs := &color.ColorString{} 744 | fmt.Fprintln(os.Stderr, cs.Red(logPrefix).Red(": "+err.Error()).String()) 745 | panic("") 746 | } 747 | 748 | func debugInfo(msg string, a ...any) { 749 | cs := &color.ColorString{} 750 | prefix := logPrefix 751 | baseMessage := cs.Reset("").Yellow(prefix).Reset(" ").Gray(msg).String() 752 | fmt.Fprintf(os.Stdout, baseMessage+" \n", a...) 753 | } 754 | 755 | func showDebug() bool { 756 | showInfo := env.Get("DEBUG_ALVU", "") 757 | return len(showInfo) != 0 758 | } 759 | 760 | func onDebug(fn func()) { 761 | if !showDebug() { 762 | return 763 | } 764 | 765 | fn() 766 | } 767 | 768 | func mergeMapWithCheck(maps ...any) (source map[string]interface{}) { 769 | source = map[string]interface{}{} 770 | for _, toCheck := range maps { 771 | if pairs, ok := toCheck.(map[string]interface{}); ok { 772 | for k, v := range pairs { 773 | source[k] = v 774 | } 775 | } 776 | } 777 | return source 778 | } 779 | 780 | func readFileToBytes(fd *os.File) []byte { 781 | buf := &bytes.Buffer{} 782 | fd.Seek(0, 0) 783 | _, err := io.Copy(buf, fd) 784 | bail(err) 785 | return buf.Bytes() 786 | } 787 | 788 | func shouldCopyContentsWithReset(src *os.File, target *os.File) { 789 | src.Seek(0, 0) 790 | _, err := io.Copy(target, src) 791 | bail(err) 792 | } 793 | 794 | func ServeHandler(rw http.ResponseWriter, req *http.Request) { 795 | path := req.URL.Path 796 | 797 | if path == "/" { 798 | path = filepath.Join(outPath, "index.html") 799 | http.ServeFile(rw, req, path) 800 | return 801 | } 802 | 803 | // check if the requested file already exists 804 | file := filepath.Join(outPath, path) 805 | info, err := os.Stat(file) 806 | 807 | // if not, check if it's a directory 808 | // and if it's a directory, we look for 809 | // a index.html inside the directory to return instead 810 | if err == nil { 811 | if info.Mode().IsDir() { 812 | file = filepath.Join(outPath, path, "index.html") 813 | _, err := os.Stat(file) 814 | if err != nil { 815 | notFoundHandler(rw, req) 816 | return 817 | } 818 | } 819 | 820 | http.ServeFile(rw, req, file) 821 | return 822 | } 823 | 824 | // if neither a directory or file was found 825 | // try a secondary case where the file might be missing 826 | // a `.html` extension for cleaner url so append a .html 827 | // to look for the file. 828 | if err != nil { 829 | file := filepath.Join(outPath, normalizeFilePath(path)) 830 | _, err := os.Stat(file) 831 | 832 | if err != nil { 833 | notFoundHandler(rw, req) 834 | return 835 | } 836 | 837 | http.ServeFile(rw, req, file) 838 | return 839 | } 840 | 841 | notFoundHandler(rw, req) 842 | } 843 | 844 | // _webSocketHandler Internal function to setup a listener loop 845 | // for the live reload setup 846 | func _webSocketHandler(ws *websocket.Conn) { 847 | reloadCh = append(reloadCh, make(chan bool, 1)) 848 | currIndex := len(reloadCh) - 1 849 | 850 | defer ws.Close() 851 | 852 | for range reloadCh[currIndex] { 853 | err := websocket.Message.Send(ws, "reload") 854 | if err != nil { 855 | // For debug only 856 | // log.Printf("Error sending message: %s", err.Error()) 857 | break 858 | } 859 | onDebug(func() { 860 | debugInfo("Reload message sent") 861 | }) 862 | } 863 | 864 | } 865 | 866 | func AddWebsocketHandler() { 867 | wsHandler := websocket.Handler(_webSocketHandler) 868 | 869 | // Use a custom HTTP handler function to upgrade the HTTP request to WebSocket 870 | http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { 871 | // Check the request's 'Upgrade' header to see if it's a WebSocket request 872 | if r.Header.Get("Upgrade") != "websocket" { 873 | http.Error(w, "Not a WebSocket handshake request", http.StatusBadRequest) 874 | return 875 | } 876 | 877 | // Upgrade the HTTP connection to a WebSocket connection 878 | wsHandler.ServeHTTP(w, r) 879 | }) 880 | 881 | } 882 | 883 | // _clientNotifyReload Internal function to 884 | // report changes to all possible reload channels 885 | func _clientNotifyReload() { 886 | for ind := range reloadCh { 887 | reloadCh[ind] <- true 888 | } 889 | reloadCh = []chan bool{} 890 | } 891 | 892 | func normalizeFilePath(path string) string { 893 | if strings.HasSuffix(path, ".html") { 894 | return path 895 | } 896 | return path + ".html" 897 | } 898 | 899 | func notFoundHandler(w http.ResponseWriter, _ *http.Request) { 900 | if notFoundPageExists { 901 | compiledNotFoundFile := filepath.Join(outPath, "404.html") 902 | notFoundFile, err := os.ReadFile(compiledNotFoundFile) 903 | if err != nil { 904 | http.Error(w, "404, Page not found....", http.StatusNotFound) 905 | return 906 | } 907 | w.WriteHeader(http.StatusNotFound) 908 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 909 | w.Write(notFoundFile) 910 | return 911 | } 912 | http.Error(w, "404, Page not found....", http.StatusNotFound) 913 | } 914 | 915 | func Contains(collection []string, item string) bool { 916 | for _, x := range collection { 917 | if item == x { 918 | return true 919 | } 920 | } 921 | return false 922 | } 923 | 924 | // Watcher , create an interface over the fsnotify watcher 925 | // to be able to run alvu compile processes again 926 | // FIXME: redundant compile process for the files 927 | type Watcher struct { 928 | alvu *Alvu 929 | poller *poller.Poller 930 | dirs []string 931 | } 932 | 933 | func NewWatcher(alvu *Alvu, interval int) *Watcher { 934 | watcher := &Watcher{ 935 | alvu: alvu, 936 | poller: poller.NewPollWatcher(interval), 937 | } 938 | 939 | return watcher 940 | } 941 | 942 | func (w *Watcher) AddDir(dirPath string) { 943 | 944 | for _, pth := range w.dirs { 945 | if pth == dirPath { 946 | return 947 | } 948 | } 949 | 950 | w.dirs = append(w.dirs, dirPath) 951 | w.poller.Add(dirPath) 952 | } 953 | 954 | func (w *Watcher) RebuildAlvu() { 955 | onDebug(func() { 956 | debugInfo("Rebuild Started") 957 | }) 958 | w.alvu.CopyPublic() 959 | w.alvu.Build() 960 | onDebug(func() { 961 | debugInfo("Build Completed") 962 | }) 963 | } 964 | 965 | func (w *Watcher) RebuildFile(filePath string) { 966 | onDebug(func() { 967 | debugInfo("RebuildFile Started") 968 | }) 969 | for i, af := range w.alvu.files { 970 | if af.sourcePath != filePath { 971 | continue 972 | } 973 | 974 | w.alvu.files[i].Build() 975 | break 976 | } 977 | onDebug(func() { 978 | debugInfo("RebuildFile Completed") 979 | }) 980 | } 981 | 982 | func (w *Watcher) StartWatching() { 983 | go w.poller.StartPoller() 984 | go func() { 985 | for { 986 | select { 987 | case evt := <-w.poller.Events: 988 | onDebug(func() { 989 | debugInfo("Events registered") 990 | }) 991 | 992 | recompiledText := &color.ColorString{} 993 | recompiledText.Blue(logPrefix).Green("Recompiled!").Reset(" ") 994 | 995 | _, err := os.Stat(evt.Path) 996 | 997 | // Do nothing if the file doesn't exit, just continue 998 | if err != nil { 999 | continue 1000 | } 1001 | 1002 | // If alvu file then just build the file, else 1003 | // just rebuilt the whole folder since it could 1004 | // be a file from the public folder or the _layout file 1005 | if w.alvu.IsAlvuFile(evt.Path) { 1006 | recompilingText := &color.ColorString{} 1007 | recompilingText.Blue(logPrefix).Cyan("Recompiling: ").Gray(evt.Path).Reset(" ") 1008 | fmt.Println(recompilingText.String()) 1009 | w.RebuildFile(evt.Path) 1010 | } else { 1011 | recompilingText := &color.ColorString{} 1012 | recompilingText.Blue(logPrefix).Cyan("Recompiling: ").Gray("All").Reset(" ") 1013 | fmt.Println(recompilingText.String()) 1014 | w.RebuildAlvu() 1015 | } 1016 | 1017 | _clientNotifyReload() 1018 | fmt.Println(recompiledText.String()) 1019 | continue 1020 | 1021 | case err := <-w.poller.Errors: 1022 | // If the poller has an error, just crash, 1023 | // digesting polling issues without killing the program would make it complicated 1024 | // to handle cleanup of all the kind of files that are being maintained by alvu 1025 | bail(err) 1026 | } 1027 | } 1028 | }() 1029 | } 1030 | 1031 | func _injectLiveReload(layoutHTML *string) string { 1032 | if !*serveFlag { 1033 | return *layoutHTML 1034 | } 1035 | return *layoutHTML + `` 1051 | } 1052 | 1053 | // Recursively copy files from a directory to 1054 | // another directory. 1055 | // The files copied are overwritten on the dest 1056 | func copyDir(src string, dest string) error { 1057 | err := os.MkdirAll(dest, os.ModePerm) 1058 | if err != nil { 1059 | return err 1060 | } 1061 | 1062 | dirEntries, err := os.ReadDir(src) 1063 | if err != nil { 1064 | return err 1065 | } 1066 | 1067 | for _, s := range dirEntries { 1068 | if s.IsDir() { 1069 | if err := os.MkdirAll(filepath.Join(dest, s.Name()), os.ModePerm); err != nil { 1070 | return err 1071 | } 1072 | err := copyDir(filepath.Join(src, s.Name()), filepath.Join(dest, s.Name())) 1073 | if err != nil { 1074 | return err 1075 | } 1076 | } else { 1077 | dest, err := os.OpenFile(filepath.Join(dest, s.Name()), os.O_CREATE|os.O_WRONLY, os.ModePerm) 1078 | if err != nil { 1079 | return err 1080 | } 1081 | src, err := os.OpenFile(filepath.Join(src, s.Name()), os.O_RDONLY, os.ModePerm) 1082 | if err != nil { 1083 | return err 1084 | } 1085 | _, err = io.Copy(dest, src) 1086 | if err != nil { 1087 | return err 1088 | } 1089 | } 1090 | } 1091 | return nil 1092 | } 1093 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # alvu 2 | 3 | > another tiny static site generator 4 | 5 | ## Installation 6 | 7 | #### Quick 8 | 9 | - You'll need `curl` installed on your system 10 | 11 | ```sh 12 | curl -sf https://goblin.run/github.com/barelyhuman/alvu | sh 13 | ``` 14 | 15 | #### From Source / Language tools 16 | 17 | - You'll need go lang installed and its binary path added to your `PATH` env 18 | variable 19 | 20 | ```sh 21 | # if go >= 1.8 22 | go install github.com/barelyhuman/alvu 23 | ``` 24 | 25 | **or** 26 | 27 | ```sh 28 | git clone https://github.com/barelyhuman/alvu 29 | cd alvu 30 | go mod tidy 31 | go build 32 | go install 33 | ``` 34 | 35 | ## Documentation 36 | You can read the [docs](docs/pages/index) folder in the source code or visit the hosted version of it [here→](https://barelyhuman.github.io/alvu/) 37 | 38 | ## License 39 | [MIT](license) 2022-Present [Reaper](https://github.com/barelyhuman) 40 | -------------------------------------------------------------------------------- /scripts/cross-compile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euxo pipefail 4 | 5 | rm -rf ./bin 6 | 7 | build_commands=(' 8 | apk add make curl git \ 9 | ; GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bin/linux-arm64/alvu \ 10 | ; GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/linux-amd64/alvu \ 11 | ; GOOS=linux GOARCH=arm go build -ldflags="-s -w" -o bin/linux-arm/alvu \ 12 | ; GOOS=windows GOARCH=386 go build -ldflags="-s -w" -o bin/windows-386/alvu \ 13 | ; GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o bin/windows-amd64/alvu \ 14 | ; GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o bin/darwin-amd64/alvu \ 15 | ; GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o bin/darwin-arm64/alvu 16 | ') 17 | 18 | # run a docker container with osxcross and cross compile everything 19 | docker run -it --rm -v $(pwd):/usr/local/src -w /usr/local/src \ 20 | golang:alpine3.16 \ 21 | sh -c "$build_commands" 22 | 23 | # create archives 24 | cd bin 25 | for dir in $(ls -d *); 26 | do 27 | cp ../readme.md $dir 28 | cp ../license $dir 29 | mkdir -p $dir/docs 30 | cp -r ../docs/pages/* $dir/docs 31 | tar cfzv "$dir".tgz $dir 32 | rm -rf $dir 33 | done 34 | cd .. 35 | --------------------------------------------------------------------------------