57 Commits

Author SHA1 Message Date
30b7681234 Update index.md
All checks were successful
Lint / shellcheck (push) Successful in 19s
2026-03-22 07:24:36 +01:00
13b6106efd docs: bpkg
All checks were successful
Lint / shellcheck (push) Successful in 20s
2026-03-22 07:23:24 +01:00
831b081fc7 docs: new contribution instructions
All checks were successful
Lint / shellcheck (push) Successful in 19s
2026-03-21 16:10:33 +01:00
fde423a32b docs: brew
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-20 09:36:53 +01:00
55a82f75a9 fix: link in homebrew
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-20 09:33:45 +01:00
f85abd43c4 fix: brew
Some checks failed
Lint / shellcheck (push) Has been cancelled
Release Standalone Builder / build (release) Successful in 30s
Release Standalone Builder / publish-aur (release) Successful in 32s
Release Standalone Builder / publish-homebrew (release) Successful in 6s
2026-03-20 09:26:20 +01:00
0f66ebf52a dist: brew
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-20 09:23:43 +01:00
55a515ccd5 dist: bpkg preparation
All checks were successful
Lint / shellcheck (push) Successful in 18s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Successful in 34s
2026-03-20 08:46:13 +01:00
de8cbefb8e feat: task lists
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-20 08:32:28 +01:00
cc7fee573f feat: custom admonitions 2026-03-20 08:31:58 +01:00
137be9579a feat: incremental rebuilds 2026-03-20 08:30:25 +01:00
5afd0170e5 fix: less tmp file spam 2026-03-20 08:29:05 +01:00
5a2053cfb4 fix: publish-aur-git shoudln't run so often now
All checks were successful
Lint / shellcheck (push) Successful in 19s
2026-03-19 22:34:31 +01:00
2fc3d6fc6f fix: AUR links
All checks were successful
Lint / shellcheck (push) Successful in 18s
Publish kewt-git to AUR / publish-aur-git (push) Successful in 29s
Release Standalone Builder / build (release) Successful in 30s
Release Standalone Builder / publish-aur (release) Successful in 44s
2026-03-19 21:50:34 +01:00
c5a9355871 fix: config slashes
All checks were successful
Lint / shellcheck (push) Successful in 18s
Publish kewt-git to AUR / publish-aur-git (push) Successful in 30s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Successful in 34s
2026-03-19 21:45:38 +01:00
b8cd129c47 fix: rename to later in the alphabet, just in case
All checks were successful
Lint / shellcheck (push) Successful in 17s
Publish kewt-git to AUR / publish-aur-git (push) Successful in 33s
2026-03-19 21:29:22 +01:00
5e033a65e7 dist: AUR updates and upgrades
All checks were successful
Publish kewt-git to AUR / publish-aur-git (push) Successful in 34s
Lint / shellcheck (push) Successful in 20s
2026-03-19 21:26:17 +01:00
5bf2e2abe5 Update LICENSE
All checks were successful
Lint / shellcheck (push) Successful in 20s
2026-03-19 16:27:56 +01:00
3a2056ff8f Update site/site.conf
All checks were successful
Lint / shellcheck (push) Successful in 20s
2026-03-19 16:20:52 +01:00
fd829a3f22 branding: icon
All checks were successful
Lint / shellcheck (push) Successful in 19s
2026-03-19 16:12:21 +01:00
bad02decba docs: add contributing instructions
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-19 15:58:41 +01:00
2aef6ec4a1 Update site/site.conf
All checks were successful
Lint / shellcheck (push) Successful in 19s
2026-03-19 15:40:39 +01:00
78eac182dc dist: github release descriptions
All checks were successful
Lint / shellcheck (push) Successful in 19s
Release Standalone Builder / build (release) Successful in 30s
Release Standalone Builder / publish-aur (release) Successful in 33s
2026-03-19 15:37:55 +01:00
b7382a20ab feat: new default style and more
All checks were successful
Lint / shellcheck (push) Successful in 20s
2026-03-19 15:35:14 +01:00
d8cf07ee2a docs: move to only be on the website
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-19 15:25:01 +01:00
76e2ae0117 make automatic filesnames not use : for better windows support ig
All checks were successful
Lint / shellcheck (push) Successful in 20s
2026-03-19 09:35:14 +01:00
2e331b5d9a fix: to the fix, clap your hands together, caramelldanse o-o-or whatever
All checks were successful
Lint / shellcheck (push) Successful in 17s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Successful in 35s
2026-03-18 22:26:32 +01:00
9f5d1089a2 feat: --version, fix: --post fixes
All checks were successful
Lint / shellcheck (push) Successful in 19s
2026-03-18 22:18:13 +01:00
9ccba8fd4e feat/fix: Site.conf now uses escaped " so it's more conventional
All checks were successful
Lint / shellcheck (push) Successful in 19s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Successful in 36s
2026-03-18 08:49:41 +01:00
95679abd85 docs: update bianry download url
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-17 13:54:13 +01:00
8b1e793510 docs: new features
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-17 12:56:15 +01:00
19f96553d9 feat: RSS!!!!!!!!!!
All checks were successful
Lint / shellcheck (push) Successful in 17s
Release Standalone Builder / build (release) Successful in 32s
Release Standalone Builder / publish-aur (release) Successful in 33s
2026-03-17 12:46:29 +01:00
1a7525a857 feat: config updating
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-17 11:53:15 +01:00
e7d90d18e8 feat: sitemap 2026-03-17 11:37:17 +01:00
4019d2721d Header links 2026-03-17 11:28:47 +01:00
b58604a4cf feat/docs: LICENSE
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-17 11:17:26 +01:00
99e805b180 fix: Title formatting parsing fix
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-17 02:18:42 +01:00
62075dea4a docs: forgot
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-17 02:07:52 +01:00
7afd041e53 functionality: Dynamic page titles, versioning, automatic 404 page
All checks were successful
Lint / shellcheck (push) Successful in 17s
Release Standalone Builder / build (release) Successful in 32s
Release Standalone Builder / publish-aur (release) Successful in 33s
2026-03-17 01:55:32 +01:00
64d08a0de3 functionality: BetterHelp
All checks were successful
Lint / shellcheck (push) Successful in 19s
Release Standalone Builder / build (release) Successful in 32s
Release Standalone Builder / publish-aur (release) Successful in 35s
2026-03-16 10:33:21 +01:00
dd18bc3367 docs: update readme
All checks were successful
Lint / shellcheck (push) Successful in 39s
2026-03-16 09:33:45 +01:00
f89661c1a5 standalone version tmp spam fix
All checks were successful
Lint / shellcheck (push) Successful in 19s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Successful in 33s
2026-03-11 13:57:10 +01:00
e0a3b66fa9 docs: AUR
All checks were successful
Lint / shellcheck (push) Successful in 16s
2026-03-11 08:44:37 +01:00
4e6c9dbeb5 kewt-git AUR 2026-03-11 08:43:19 +01:00
af453ca2ec aur now workie pretty please
All checks were successful
Lint / shellcheck (push) Successful in 16s
Release Standalone Builder / build (release) Successful in 33s
Release Standalone Builder / publish-aur (release) Successful in 1m7s
2026-03-11 08:36:22 +01:00
cea84de242 Hopefully AUR fixed
Some checks failed
Lint / shellcheck (push) Successful in 17s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Failing after 23s
2026-03-11 08:31:00 +01:00
ad1ec9c2e3 Auto GitHub
Some checks failed
Lint / shellcheck (push) Successful in 18s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Failing after 43s
2026-03-11 08:26:36 +01:00
fa3b5592da AUR
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-11 08:21:12 +01:00
722d687afe docs: add standalone download instructions
All checks were successful
Lint / shellcheck (push) Successful in 15s
2026-03-11 08:07:17 +01:00
77c0b29b4c fix gitea actions missing go
All checks were successful
Lint / shellcheck (push) Successful in 16s
Release Standalone Builder / build (release) Successful in 1m8s
2026-03-11 08:01:26 +01:00
9ae965662c Actions! Work!
Some checks failed
Lint / shellcheck (push) Successful in 48s
Release Standalone Builder / build (release) Failing after 6s
2026-03-11 07:51:31 +01:00
9ced2af562 Actions
Some checks failed
Lint / shellcheck (push) Has been cancelled
2026-03-11 07:42:45 +01:00
f407b1c4af extra navbar links 2026-03-09 20:05:42 +01:00
bee12ce8c1 Update markdown_inline.awk 2026-03-09 10:40:30 +01:00
696dc1c142 Fix building from ./ 2026-03-08 19:28:06 +01:00
c2416e731b Turn off antialiasing in the default style 2026-03-08 17:54:10 +01:00
01fd55001a html tag fix 2026-03-08 17:44:28 +01:00
35 changed files with 1394 additions and 264 deletions

16
.gitea/workflows/lint.yml Normal file
View File

@@ -0,0 +1,16 @@
name: Lint
on:
push:
branches: [main, master]
pull_request:
jobs:
shellcheck:
runs-on: local
steps:
- uses: actions/checkout@v4
- name: Install Shellcheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Run Shellcheck
run: shellcheck -s sh kewt.sh markdown.sh tools/build-standalone.sh || true

View File

@@ -0,0 +1,38 @@
name: Publish kewt-git to AUR
on:
push:
branches:
- main
paths:
- 'packaging/AUR/PKGBUILD.git'
- 'packaging/AUR/.SRCINFO.git'
workflow_dispatch:
jobs:
publish-aur-git:
runs-on: local
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Arch Linux environment
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager curl jq || true
- name: Prepare AUR files
run: |
mkdir -p aur-work
cp packaging/AUR/PKGBUILD.git aur-work/PKGBUILD
cp packaging/AUR/.SRCINFO.git aur-work/.SRCINFO
- name: Publish to AUR
uses: KSXGitHub/github-actions-deploy-aur@v3.0.1
with:
pkgname: kewt-git
pkgbuild: ./aur-work/PKGBUILD
commit_username: ${{ github.actor }}
commit_email: ${{ github.actor }}@users.noreply.github.com
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
commit_message: "Update kewt-git to ${{ github.sha }}"

View File

@@ -0,0 +1,140 @@
name: Release Standalone Builder
on:
release:
types: [published]
workflow_dispatch:
jobs:
build:
runs-on: local
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Build standalone executable
run: |
chmod +x tools/build-standalone.sh
./tools/build-standalone.sh
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Upload Release Asset
uses: https://gitea.com/actions/release-action@main
with:
files: |-
kewt
api_key: '${{secrets.GITEA_TOKEN}}'
- name: Push to GitHub Release
run: |
TAG="${GITHUB_REF#refs/tags/}"
# Fetch release body from Gitea
RELEASE_BODY=$(curl -sL \
"https://git.krzak.org/api/v1/repos/N0VA/kewt/releases/tags/${TAG}" \
| jq -r '.body // ""')
# Build JSON payload
PAYLOAD=$(jq -n \
--arg tag "$TAG" \
--arg name "Release $TAG" \
--arg body "$RELEASE_BODY" \
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')
# Create the release on GitHub
curl -sL -X POST \
-H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/n0va-bot/kewt/releases" \
-d "$PAYLOAD" || true
# Get the release ID
RELEASE_ID=$(curl -sL \
-H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/n0va-bot/kewt/releases/tags/${TAG}" | jq -r '.id')
# Upload the asset
curl -sL -X POST \
-H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
"https://uploads.github.com/repos/n0va-bot/kewt/releases/${RELEASE_ID}/assets?name=kewt" \
--data-binary @kewt
publish-aur:
runs-on: local
needs: build
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Arch Linux environment
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager curl jq || true
- name: Render PKGBUILD and SRCINFO
run: |
VERSION=${GITHUB_REF#refs/tags/v}
VERSION=${VERSION#refs/tags/}
curl -sL -o kewt https://git.krzak.org/N0VA/kewt/releases/download/v${VERSION}/kewt
CHECKSUM=$(sha256sum kewt | awk '{print $1}')
mkdir -p aur-work
sed -e "s/VERSION_PLACEHOLDER/${VERSION}/g" \
-e "s/SHA256SUM_PLACEHOLDER/${CHECKSUM}/g" \
packaging/AUR/PKGBUILD.template > aur-work/PKGBUILD
sed -e "s/VERSION_PLACEHOLDER/${VERSION}/g" \
-e "s/SHA256SUM_PLACEHOLDER/${CHECKSUM}/g" \
packaging/AUR/.SRCINFO.template > aur-work/.SRCINFO
- name: Publish to AUR
uses: KSXGitHub/github-actions-deploy-aur@v3.0.1
with:
pkgname: kewt-bin
pkgbuild: ./aur-work/PKGBUILD
commit_username: ${{ github.actor }}
commit_email: ${{ github.actor }}@users.noreply.github.com
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
commit_message: "Update kewt-bin to ${{ github.ref_name }}"
publish-homebrew:
runs-on: local
needs: build
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Render Formula and push to GitHub
run: |
TAG="${GITHUB_REF#refs/tags/}"
VERSION="${TAG#v}"
curl -sL -o kewt-binary \
"https://git.krzak.org/N0VA/kewt/releases/download/${TAG}/kewt"
CHECKSUM=$(sha256sum kewt-binary | awk '{print $1}')
rm -f kewt-binary
mkdir -p brew-work/Formula
sed -e "s/VERSION_PLACEHOLDER/${VERSION}/g" \
-e "s/SHA256SUM_PLACEHOLDER/${CHECKSUM}/g" \
packaging/homebrew/kewt.rb.template > brew-work/Formula/kewt.rb
cd brew-work
git init
git remote add origin https://x-access-token:${{ secrets.GH_RELEASE_TOKEN }}@github.com/n0va-bot/homebrew-tap.git
git fetch origin main || true
git checkout main 2>/dev/null || git checkout --orphan main
git add Formula/kewt.rb
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.github.com"
git commit -m "Update kewt to ${TAG}" || echo "No changes to commit"
git push origin main

3
.gitignore vendored
View File

@@ -1 +1,4 @@
out/
kewt
site.conf
template.html

34
LICENSE Normal file
View File

@@ -0,0 +1,34 @@
ISC License
Copyright 2026 N0\A
Permission to use, copy, modify, and/or distribute this software
for any purpose with or without fee is hereby granted, provided
that the above copyright notice and this permission notice appear
in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
---
This project incorporates code (CSS style) from the 'kew' project,
which is also licensed under the ISC License: Copyright (c) 2026 uint23
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

19
Makefile Normal file
View File

@@ -0,0 +1,19 @@
PREFIX ?= /usr/local
BINDIR = $(PREFIX)/bin
all: kewt
kewt:
./tools/build-standalone.sh
install: kewt
install -d $(DESTDIR)$(BINDIR)
install -m 755 kewt $(DESTDIR)$(BINDIR)/kewt
uninstall:
rm -f $(DESTDIR)$(BINDIR)/kewt
clean:
rm -f kewt
.PHONY: all install uninstall clean

View File

@@ -1,89 +1,21 @@
# _kewt_
# ![kewt](/icon.svg)
### Pronounced "cute"
***
# [Go to the website](https://kewt.krzak.org)
***
_kewt_ is a minimalist ssg inspired by _[werc](http://werc.cat-v.org/)_ and _[kew](https://github.com/uint23/kew)_
It's meant to be a static site generator, like _[kew](https://github.com/uint23/kew)_ but use only default (POSIX) tooling, like _[werc](http://werc.cat-v.org/)_ (and definitely unlike _[kew](https://github.com/uint23/kew)_)
## Features
## [Installation](https://kewt.krzak.org/#Installation)
- No dependencies
- Supports many embed types
- Automatic css variable replacement for older browsers
- Automatic inlining and embedding of many filetypes with `\![link]` or `\![alt](link)`
- Inline html support
- MFM `$font` and `\<plain>` tags
- Admonition support (that's what the blocks like the warning block below are called)
## Contributing
If you want to **force** a file to be inlined, use `\!![]` instead of `\![]`
Either open an issue or pull request on the **home** repository ([N0VA/kewt](https://git.krzak.org/N0VA/kewt)) or message me on my email address ([n0va@krzak.org](mailto:n0va@krzak.org?subject=%5Bkewt%5D%20something)) with the subjectline being `[kewt] something`.
## Usage
## License
```sh
./kewt.sh --help
./kewt.sh --new [title]
./kewt.sh --from <src> --to <out>
./kewt.sh [src] [out]
```
`--new [title]` creates a new site directory with a copied `site.conf` and a default `index.md`.
## site.conf
```conf
title = "kewt"
style = "kewt"
dir_indexes = true
single_file_index = true
flatten = false
order = ""
home_name = "Home"
show_home_in_nav = true
footer = "made with <a href="https://kewt.krzak.org">kewt</a>"
logo = ""
display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
```
- `title` site title
- `style` style file name from `./styles` (without `.css`)
- `dir_indexes` generate directory index pages when missing `index.md`
- `single_file_index` if a directory has one markdown file and no `index.md`, use that file as `index.html`
- `flatten` flatten sidebar directory levels
- `order` comma separated file/directory name list to order the sidebar (alphabetical by default)
- `home_name` text for the home link in navigation (default: "Home")
- `show_home_in_nav` show home link in navigation (default: true)
- `footer` footer html/text shown at the bottom of pages
- `logo` logo image path (used in header if enabled)
- `display_logo` show logo in header
- `display_title` show title text in header
- `logo_as_favicon` use `logo` as favicon
- `favicon` explicit favicon path (used when `logo_as_favicon` is false or no logo is set)
## Ignores
- `.kewtignore`: Files/directories to ignore. If empty, the whole directory gets ignored
- `.kewthide`: Files/directories to hide from navigation but still process. Same empty rules as with ignore
- `.kewtpreserve`: Files/directories to copy but not convert markdown to html. Same empty rules again
## Embeds
- `\![link]`:
- local image/audio/video files are embedded as media tags
- local text/code files are inlined directly
- global image/audio/video links are embedded as media tags
- other global links are embedded as `<iframe>`
- `\![alt](link)` works the same, with `alt` used for images
- `\!![]` and `\!![alt](link)` force inline local file contents
## Credits
- Markdown to html conversion based on [markdown.bash](https://github.com/chadbraunduin/markdown.bash) by [chadbraunduin](https://github.com/chadbraunduin)
- Default css style and html template based on _[kew](https://github.com/uint23/kew)_ by [uint23](https://github.com/uint23)
>[!WARNING]
>Most of this was coded at night, while sleepy and a bit sick, and after walking for about 4 hours around a forest, so...
ISC

View File

@@ -22,7 +22,19 @@ END {
sub(/^\[!/, "", kind)
sub(/\]$/, "", kind)
lkind = tolower(kind)
if (lkind == "note" || lkind == "tip" || lkind == "important" || lkind == "warning" || lkind == "caution") {
is_valid = 0
if (custom_admonitions != "") {
n = split(tolower(custom_admonitions), adms, ",")
for (idx = 1; idx <= n; idx++) {
adm = adms[idx]
sub(/^[ \t]+/, "", adm)
sub(/[ \t]+$/, "", adm)
if (lkind == adm) { is_valid = 1; break }
}
} else if (lkind == "note" || lkind == "tip" || lkind == "important" || lkind == "warning" || lkind == "caution") {
is_valid = 1
}
if (is_valid) {
print "<div class=\"admonition admonition-" lkind "\">"
print "<p class=\"admonition-title\">" cap(lkind) "</p>"
has_body = 0

View File

@@ -1,4 +1,5 @@
BEGIN {
src = ENVIRON["AWK_SRC"]
slen = length(src)
}

View File

@@ -1,11 +1,20 @@
BEGIN { in_fence = 0; first_line = 0 }
BEGIN { in_fence = 0; first_line = 0; code_tag = "<code>" }
{
if (!in_fence && $0 ~ /^```/) {
in_fence = 1
first_line = 1
lang = $0
sub(/^```[[:space:]]*/, "", lang)
sub(/[[:space:]]*$/, "", lang)
if (lang != "") {
code_tag = "<code class=\"language-" lang "\">"
} else {
code_tag = "<code>"
}
next
}
if (in_fence && $0 ~ /^```[[:space:]]*$/) {
if (first_line) printf "%s", "<pre>" code_tag
print "</code></pre>"
in_fence = 0
next
@@ -14,8 +23,12 @@ BEGIN { in_fence = 0; first_line = 0 }
gsub(/&/, "\\&amp;"); gsub(/</, "\\&lt;"); gsub(/>/, "\\&gt;")
if (first_line) {
first_line = 0
if ($0 == "") next
print "<pre><code>" $0
printf "%s", "<pre>" code_tag
if ($0 == "") {
print ""
next
}
print $0
} else {
print
}
@@ -24,5 +37,8 @@ BEGIN { in_fence = 0; first_line = 0 }
}
}
END {
if (in_fence) print "</code></pre>"
if (in_fence) {
if (first_line) printf "%s", "<pre>" code_tag
print "</code></pre>"
}
}

View File

@@ -33,6 +33,13 @@ function compare_paths(p1, p2, parts1, parts2, n1, n2, i, name1, name2, lname
}
BEGIN {
src = ENVIRON["AWK_SRC"]
single_file_index = ENVIRON["AWK_SINGLE_FILE_INDEX"]
flatten = ENVIRON["AWK_FLATTEN"]
order = ENVIRON["AWK_ORDER"]
home_name = ENVIRON["AWK_HOME_NAME"]
show_home_in_nav = ENVIRON["AWK_SHOW_HOME_IN_NAV"]
dinfo = ENVIRON["AWK_DINFO"]
n_dlines = split(dinfo, dlines, "\n")
for (i = 1; i <= n_dlines; i++) {
if (split(dlines[i], dparts, "|") == 3) {

View File

@@ -8,18 +8,21 @@ function strip_markdown(s) {
return s
}
function print_header(line) {
if (line ~ /^# /) {
sub(/^# /, "", line); print "<h1 id=\"" strip_markdown(line) "\">" line "</h1>"
} else if (line ~ /^## /) {
sub(/^## /, "", line); print "<h2 id=\"" strip_markdown(line) "\">" line "</h2>"
} else if (line ~ /^### /) {
sub(/^### /, "", line); print "<h3 id=\"" strip_markdown(line) "\">" line "</h3>"
} else if (line ~ /^#### /) {
sub(/^#### /, "", line); print "<h4 id=\"" strip_markdown(line) "\">" line "</h4>"
} else if (line ~ /^##### /) {
sub(/^##### /, "", line); print "<h5 id=\"" strip_markdown(line) "\">" line "</h5>"
} else if (line ~ /^###### /) {
sub(/^###### /, "", line); print "<h6 id=\"" strip_markdown(line) "\">" line "</h6>"
tag = ""
if (line ~ /^# /) { tag = "h1"; sub(/^# /, "", line) }
else if (line ~ /^## /) { tag = "h2"; sub(/^## /, "", line) }
else if (line ~ /^### /) { tag = "h3"; sub(/^### /, "", line) }
else if (line ~ /^#### /) { tag = "h4"; sub(/^#### /, "", line) }
else if (line ~ /^##### /) { tag = "h5"; sub(/^##### /, "", line) }
else if (line ~ /^###### /) { tag = "h6"; sub(/^###### /, "", line) }
if (tag != "") {
id = strip_markdown(line)
if (enable_header_links == "true") {
print "<" tag " id=\"" id "\"><a href=\"#" id "\" class=\"header-anchor\">" line "</a></" tag ">"
} else {
print "<" tag " id=\"" id "\">" line "</" tag ">"
}
} else {
print line
}

View File

@@ -1,9 +1,20 @@
BEGIN { in_code = 0 }
/^ | / {
if (!in_code) { print "<pre><code>"; in_code = 1 }
sub(/^ | /, "", $0)
BEGIN { in_code = 0; in_html_pre = 0 }
{
if ($0 ~ /<pre>/) in_html_pre = 1
if ($0 ~ /<\/pre>/) { in_html_pre = 0; if (in_code) { print "</code></pre>"; in_code = 0 }; print; next }
if (!in_html_pre && $0 ~ /^(\t| )/) {
if (!in_code) { printf "%s", "<pre><code>"; in_code = 1 }
sub(/^(\t| )/, "", $0)
gsub(/&/, "\\&amp;"); gsub(/</, "\\&lt;"); gsub(/>/, "\\&gt;")
print; next
print
next
}
if (in_code) {
print "</code></pre>"
in_code = 0
}
print
}
{ if (in_code) { print "</code></pre>"; in_code = 0 } print }
END { if (in_code) print "</code></pre>" }

View File

@@ -48,7 +48,26 @@ BEGIN {
}
}
has_checkbox = 0
if (content ~ /^\[[ \t]\] /) {
has_checkbox = 1
is_checked = 0
sub(/^\[[ \t]\] /, "", content)
} else if (content ~ /^\[[xX]\] /) {
has_checkbox = 1
is_checked = 1
sub(/^\[[xX]\] /, "", content)
}
if (has_checkbox) {
if (is_checked) {
print "<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" checked disabled> " content "</li>"
} else {
print "<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" disabled> " content "</li>"
}
} else {
print "<li>" content "</li>"
}
} else {
while (depth > 0) {
print "</" cur_type[depth] ">"

View File

@@ -2,6 +2,31 @@ BEGIN {
in_pre = 0
}
function mask_html_tags(s, out, rest, start, len, tag, token) {
out = ""
rest = s
html_tag_count = 0
while (match(rest, /<[^>]+>/)) {
out = out substr(rest, 1, RSTART - 1)
start = RSTART
len = RLENGTH
tag = substr(rest, start, len)
html_tag_count++
html_tag_token[html_tag_count] = "\034HT" html_tag_count "\034"
html_tag_value[html_tag_count] = tag
out = out html_tag_token[html_tag_count]
rest = substr(rest, start + len)
}
return out rest
}
function restore_html_tags(s, i) {
for (i = 1; i <= html_tag_count; i++) {
gsub(html_tag_token[i], html_tag_value[i], s)
}
return s
}
{
if ($0 ~ /<pre>/) {
in_pre = 1
@@ -114,6 +139,8 @@ BEGIN {
line = substr(line, 1, start - 1) "<span style=\"font-family: sans-serif;\">" content "</span>" substr(line, start + len)
}
line = mask_html_tags(line)
# Bold, Italic, Strikethrough (BRE-like logic in AWK)
# Strong Bold **
while (match(line, /\*\*[^*]+\*\*/)) {
@@ -152,6 +179,8 @@ BEGIN {
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
line = restore_html_tags(line)
# special characters
if (line !~ /&[A-Za-z0-9#]+;/) {
gsub(/&/, "&amp;", line)

View File

@@ -1,9 +1,24 @@
function replace_all(text, token, value, pos, token_len) {
function replace_all(text, token, value, pos, token_len, res) {
token_len = length(token)
res = ""
while ((pos = index(text, token)) > 0) {
text = substr(text, 1, pos - 1) value substr(text, pos + token_len)
res = res substr(text, 1, pos - 1) value
text = substr(text, pos + token_len)
}
return res text
}
BEGIN {
current_url = ENVIRON["AWK_CURRENT_URL"]
nav = ENVIRON["AWK_NAV"]
title = ENVIRON["AWK_TITLE"]
footer = ENVIRON["AWK_FOOTER"]
style_path = ENVIRON["AWK_STYLE_PATH"]
head_extra = ENVIRON["AWK_HEAD_EXTRA"]
header_brand = ENVIRON["AWK_HEADER_BRAND"]
if (current_url != "") {
nav = replace_all(nav, "href=\"" current_url "\"", "href=\"" current_url "\" class=\"current-page\"")
}
return text
}
{

View File

@@ -1,4 +1,7 @@
BEGIN { done = 0 }
BEGIN {
new_title = ENVIRON["AWK_NEW_TITLE"]
done = 0
}
/^title[[:space:]]*=/ {
print "title = \"" new_title "\""
done = 1

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

21
icon.svg Normal file
View File

@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<linearGradient id="bg-grad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#4a3b69"/>
<stop offset="100%" stop-color="#352654"/>
</linearGradient>
</defs>
<rect width="512" height="512" rx="80" ry="80" fill="url(#bg-grad)"/>
<text
x="256"
y="296"
text-anchor="middle"
dominant-baseline="central"
font-family="Georgia, 'Times New Roman', Times, serif"
font-size="220"
font-weight="bold"
font-style="italic"
fill="#debfff"
letter-spacing="-8"
>kewt</text>
</svg>

After

Width:  |  Height:  |  Size: 639 B

654
kewt.sh
View File

@@ -6,26 +6,44 @@ die() {
}
usage() {
invoked_as=$(basename "${KEWT_INVOKED_AS:-$0}")
cat <<EOF
Usage: $0 [--from <src>] [--to <out>]
$0 [src] [out]
$0 --new [title]
$0 --help
Usage: $invoked_as [--from <src>] [--to <out>]
$invoked_as [src] [out]
$invoked_as --new [title]
$invoked_as --update [dir]
$invoked_as --post
$invoked_as --version
$invoked_as --help
Options:
--help Show this help message.
--new [title] Create a new site directory (default: site)
--update [dir] Update site.conf and template.html with latest defaults (defaults to current directory)
--post Create a new empty post file in the configured posts_dir with current date and time as name
--version Show version information.
--from <src> Source directory (default: site)
--to <out> Output directory (default: out)
EOF
}
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
script_dir=$(CDPATH="" cd -- "$(dirname -- "$0")" && pwd)
awk_dir="$script_dir/awk"
ensure_root_defaults() {
if [ ! -f "./site.conf" ]; then
cat > "./site.conf" <<'EOF'
KEWT_TMPDIR=$(mktemp -d "/tmp/kewt_run.XXXXXX")
trap 'rm -rf "$KEWT_TMPDIR"' EXIT HUP INT TERM
create_new_site() {
new_title="$1"
new_dir="site"
[ -n "$new_title" ] && new_dir="$new_title"
[ -e "$new_dir" ] && die "Target '$new_dir' already exists."
mkdir -p "$new_dir"
cat > "$new_dir/site.conf" <<'EOF'
title = "kewt"
style = "kewt"
dir_indexes = true
@@ -34,21 +52,31 @@ flatten = false
order = ""
home_name = "Home"
show_home_in_nav = true
footer = "made with <a href="https://kewt.krzak.org">kewt</a>"
nav_links = ""
nav_extra = ""
footer = "made with <a href=\"https://kewt.krzak.org\">kewt</a>"
logo = ""
display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
enable_header_links = true
base_url = ""
generate_feed = false
feed_file = "rss.xml"
posts_dir = ""
custom_admonitions = ""
EOF
fi
if [ ! -f "./template.html" ]; then
cat > "./template.html" <<'EOF'
cat > "$new_dir/template.html" <<'EOF'
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{TITLE}}</title>
<link rel="stylesheet" href="{{CSS}}" type="text/css" />
@@ -67,39 +95,153 @@ EOF
</body>
</html>
EOF
fi
}
create_new_site() {
new_title="$1"
new_dir="site"
[ -n "$new_title" ] && new_dir="$new_title"
[ -e "$new_dir" ] && die "Target '$new_dir' already exists."
ensure_root_defaults
mkdir -p "$new_dir"
cp "./site.conf" "$new_dir/site.conf"
printf "# _kewt_ website\n" > "$new_dir/index.md"
if [ -n "$new_title" ]; then
awk -v new_title="$new_title" -f "$awk_dir/update_site_conf.awk" "$new_dir/site.conf" > "$new_dir/site.conf.tmp" && mv "$new_dir/site.conf.tmp" "$new_dir/site.conf"
AWK_NEW_TITLE="$new_title" awk -f "$awk_dir/update_site_conf.awk" "$new_dir/site.conf" > "$new_dir/site.conf.tmp" && mv "$new_dir/site.conf.tmp" "$new_dir/site.conf"
fi
echo "Created new site at '$new_dir'."
exit 0
}
generate_nav() {
dinfo=$(find "$1" \( -name ".*" ! -name "." ! -name ".." -prune \) -o -print | sort | awk -v src="$1" -f "$awk_dir/collect_dir_info.awk")
find "$1" \( -name ".*" ! -name "." ! -name ".." -prune \) -o -name "*.md" -print | sort | awk -v src="$1" -v single_file_index="$single_file_index" -v flatten="$flatten" -v order="$order" -v dinfo="$dinfo" -f "$awk_dir/generate_sidebar.awk"
create_new_post() {
post_src_dir="$1"
target_dir="$post_src_dir"
if [ -n "$posts_dir" ]; then
target_dir="$post_src_dir/$posts_dir"
fi
mkdir -p "$target_dir"
base_filename="$(date +%Y-%m-%d-%H-%M)"
filename="${base_filename}.md"
file_path="$target_dir/$filename"
counter=1
while [ -e "$file_path" ]; do
filename="${base_filename}_${counter}.md"
file_path="$target_dir/$filename"
counter=$((counter + 1))
done
touch "$file_path"
echo "Created new post at '$file_path'."
exit 0
}
update_site() {
update_dir="${1:-.}"
[ -d "$update_dir" ] || die "Directory '$update_dir' does not exist."
target_conf="$update_dir/site.conf"
target_tmpl="$update_dir/template.html"
# Generate default site.conf
default_conf="$KEWT_TMPDIR/default_site.conf"
cat > "$default_conf" <<'CONFEOF'
title = "kewt"
style = "kewt"
dir_indexes = true
single_file_index = true
flatten = false
order = ""
home_name = "Home"
show_home_in_nav = true
nav_links = ""
nav_extra = ""
footer = "made with <a href=\"https://kewt.krzak.org\">kewt</a>"
logo = ""
display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
enable_header_links = true
base_url = ""
generate_feed = false
feed_file = "rss.xml"
posts_dir = ""
custom_admonitions = ""
CONFEOF
# Update site.conf
if [ ! -f "$target_conf" ]; then
echo "No site.conf found in '$update_dir'; nothing to update."
else
added=0
while IFS= read -r line; do
case "$line" in
''|'#'*) continue ;;
*=*) ;;
*) continue ;;
esac
key=$(printf '%s' "${line%%=*}" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
if ! grep -q "^[[:space:]]*${key}[[:space:]]*=" "$target_conf"; then
printf '%s\n' "$line" >> "$target_conf"
echo " Added: $key"
added=$((added + 1))
fi
done < "$default_conf"
if [ "$added" -eq 0 ]; then
echo "site.conf is already up to date."
else
echo "Added $added new key(s) to '$target_conf'."
fi
fi
# Update template.html
if [ -f "$target_tmpl" ]; then
default_tmpl="$KEWT_TMPDIR/default_template.html"
cat > "$default_tmpl" <<'TMPLEOF'
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{TITLE}}</title>
<link rel="stylesheet" href="{{CSS}}" type="text/css" />
{{HEAD_EXTRA}}
</head>
<body>
<header>
<h1>{{HEADER_BRAND}}</h1>
</header>
<nav id="side-bar">{{NAV}}</nav>
<article>{{CONTENT}}</article>
<footer>{{FOOTER}}</footer>
</body>
</html>
TMPLEOF
if cmp -s "$default_tmpl" "$target_tmpl" 2>/dev/null; then
echo "template.html is already up to date."
else
cp "$default_tmpl" "${target_tmpl}.default"
echo "template.html has local changes; saved latest default as '${target_tmpl}.default'."
echo ""
diff "$target_tmpl" "${target_tmpl}.default" || true
fi
fi
exit 0
}
src=""
out=""
new_mode="false"
new_title=""
post_mode="false"
post_title=""
positional_count=0
while [ $# -gt 0 ]; do
@@ -115,6 +257,21 @@ while [ $# -gt 0 ]; do
shift
fi
;;
--version|-v)
echo "kewt version git"
exit 0
;;
--post)
post_mode="true"
;;
--update)
update_dir="."
if [ $# -gt 1 ] && [ "${2#-}" = "$2" ]; then
update_dir="$2"
shift
fi
update_site "$update_dir"
;;
--from)
[ $# -lt 2 ] && die "--from requires a value."
src="$2"
@@ -131,9 +288,9 @@ while [ $# -gt 0 ]; do
*)
positional_count=$((positional_count + 1))
if [ "$positional_count" -eq 1 ]; then
[ -z "$src" ] && src="$1" || die "Source already set (use either positional or --from)."
if [ -z "$src" ]; then src="$1"; else die "Source already set (use either positional or --from)."; fi
elif [ "$positional_count" -eq 2 ]; then
[ -z "$out" ] && out="$1" || die "Output already set (use either positional or --to)."
if [ -z "$out" ]; then out="$1"; else die "Output already set (use either positional or --to)."; fi
else
die "Too many positional arguments."
fi
@@ -144,17 +301,30 @@ done
[ "$new_mode" = "true" ] && create_new_site "$new_title"
ensure_root_defaults
[ -z "$src" ] && src="site"
if [ -z "$src" ]; then
if [ "$post_mode" = "true" ] && [ -f "./site.conf" ]; then
src="."
else
src="site"
fi
fi
[ -z "$out" ] && out="out"
src="${src%/}"
out="${out%/}"
[ -d "$src" ] || die "Source directory '$src' does not exist."
if [ ! -d "$src" ]; then
if [ "$src" = "site" ]; then
usage
exit 1
else
die "Source directory '$src' does not exist."
fi
fi
IGNORE_ARGS="-name '.kewtignore' -o -name '.*'"
IGNORE_ARGS="-name '.kewtignore' -o -path '$src/.*'"
if [ -f "$src/.kewtignore" ]; then
while IFS= read -r line || [ -n "$line" ]; do
@@ -175,16 +345,16 @@ if [ -f "$src/.kewtignore" ]; then
done < "$src/.kewtignore"
fi
find "$src" -name .kewtignore > "/tmp/kewt_ignore_$$"
find "$src" -name .kewtignore > "$KEWT_TMPDIR/kewt_ignore"
while read -r ki; do
d="${ki%/.kewtignore}"
if [ "$d" != "$src" ] && [ "$d" != "." ]; then
IGNORE_ARGS="$IGNORE_ARGS -o -path '$d' -o -path '$d/*'"
fi
done < "/tmp/kewt_ignore_$$"
rm -f "/tmp/kewt_ignore_$$"
done < "$KEWT_TMPDIR/kewt_ignore"
rm -f "$KEWT_TMPDIR/kewt_ignore"
HIDE_ARGS="-name '.kewtignore' -o -name '.kewthide' -o -name '.kewtpreserve' -o -name '.*'"
HIDE_ARGS="-name '.kewtignore' -o -name '.kewthide' -o -name '.kewtpreserve' -o -path '$src/.*'"
if [ -f "$src/.kewthide" ]; then
while IFS= read -r line || [ -n "$line" ]; do
@@ -205,14 +375,14 @@ if [ -f "$src/.kewthide" ]; then
done < "$src/.kewthide"
fi
find "$src" -name .kewthide > "/tmp/kewt_hide_$$"
find "$src" -name .kewthide > "$KEWT_TMPDIR/kewt_hide"
while read -r kh; do
d="${kh%/.kewthide}"
if [ "$d" != "$src" ] && [ "$d" != "." ]; then
HIDE_ARGS="$HIDE_ARGS -o -path '$d' -o -path '$d/*'"
fi
done < "/tmp/kewt_hide_$$"
rm -f "/tmp/kewt_hide_$$"
done < "$KEWT_TMPDIR/kewt_hide"
rm -f "$KEWT_TMPDIR/kewt_hide"
PRESERVE_ARGS="-false"
@@ -235,18 +405,22 @@ if [ -f "$src/.kewtpreserve" ]; then
done < "$src/.kewtpreserve"
fi
find "$src" -name .kewtpreserve > "/tmp/kewt_preserve_$$"
find "$src" -name .kewtpreserve > "$KEWT_TMPDIR/kewt_preserve"
while read -r kp; do
d="${kp%/.kewtpreserve}"
if [ "$d" != "$src" ] && [ "$d" != "." ]; then
PRESERVE_ARGS="$PRESERVE_ARGS -o -path '$d' -o -path '$d/*'"
fi
done < "/tmp/kewt_preserve_$$"
rm -f "/tmp/kewt_preserve_$$"
done < "$KEWT_TMPDIR/kewt_preserve"
rm -f "$KEWT_TMPDIR/kewt_preserve"
generate_nav() {
dinfo=$(eval "find \"$1\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -print" | sort | awk -v src="$1" -f "$awk_dir/collect_dir_info.awk")
eval "find \"$1\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -name \"*.md\" -print" | sort | awk -v src="$1" -v single_file_index="$single_file_index" -v flatten="$flatten" -v order="$order" -v home_name="$home_name" -v show_home_in_nav="$show_home_in_nav" -v dinfo="$dinfo" -f "$awk_dir/generate_sidebar.awk"
dinfo=$(eval "find \"$1\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -print" | sort | AWK_SRC="$1" awk -f "$awk_dir/collect_dir_info.awk")
find_cmd="find \"$1\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -name \"*.md\" -print"
if [ -n "$posts_dir" ] && [ -d "$1/$posts_dir" ]; then
find_cmd="$find_cmd && echo \"$1/$posts_dir/index.md\""
fi
eval "$find_cmd" | sort -u | AWK_SRC="$1" AWK_SINGLE_FILE_INDEX="$single_file_index" AWK_FLATTEN="$flatten" AWK_ORDER="$order" AWK_HOME_NAME="$home_name" AWK_SHOW_HOME_IN_NAV="$show_home_in_nav" AWK_DINFO="$dinfo" awk -f "$awk_dir/generate_sidebar.awk"
}
title="kewt"
@@ -258,11 +432,23 @@ flatten="false"
order=""
home_name="Home"
show_home_in_nav="true"
nav_links=""
nav_extra=""
footer="made with <a href=\"https://kewt.krzak.org\">kewt</a>"
logo=""
display_logo="false"
display_title="true"
logo_as_favicon="true"
favicon=""
generate_page_title="true"
error_page="not_found.html"
versioning="false"
enable_header_links="true"
base_url=""
generate_feed="false"
feed_file="rss.xml"
posts_dir=""
custom_admonitions=""
load_config() {
[ -f "$1" ] || return
@@ -279,25 +465,42 @@ load_config() {
key=$(printf '%s' "$key" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
val=$(printf '%s' "$val" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$val" in
\"*\") val=${val#\"}; val=${val%\"} ;;
\'*\') val=${val#\'}; val=${val%\'} ;;
\"*\")
val=${val#\"}; val=${val%\"}
val=$(printf '%s' "$val" | sed 's/\\"/\"/g; s/\\\\/\\/g')
;;
\'*\')
val=${val#\'}; val=${val%\'}
val=$(printf '%s' "$val" | sed "s/\\\\'/'/g; s/\\\\/\\/g")
;;
esac
case "$key" in
title) title="$val" ;;
style) style="$val" ;;
style) style="${val#/}" ;;
dir_indexes) dir_indexes="$val" ;;
single_file_index) single_file_index="$val" ;;
flatten) flatten="$val" ;;
order) order="$val" ;;
home_name) home_name="$val" ;;
show_home_in_nav) show_home_in_nav="$val" ;;
nav_links) nav_links="$val" ;;
nav_extra) nav_extra="$val" ;;
footer) footer="$val" ;;
logo) logo="$val" ;;
logo) logo="${val#/}" ;;
display_logo) display_logo="$val" ;;
display_title) display_title="$val" ;;
logo_as_favicon) logo_as_favicon="$val" ;;
favicon) favicon="$val" ;;
favicon) favicon="${val#/}" ;;
generate_page_title) generate_page_title="$val" ;;
error_page) error_page="${val#/}" ;;
versioning) versioning="$val" ;;
enable_header_links) enable_header_links="$val" ;;
base_url) base_url="$val" ;;
generate_feed) generate_feed="$val" ;;
feed_file) feed_file="${val#/}" ;;
posts_dir) posts_dir="${val#/}" ;;
custom_admonitions) custom_admonitions="$val" ;;
esac
done < "$1"
}
@@ -305,14 +508,119 @@ load_config() {
load_config "./site.conf"
load_config "$src/site.conf"
if [ -n "$posts_dir" ]; then
HIDE_ARGS="$HIDE_ARGS -o -path '$src/$posts_dir/*'"
fi
[ "$post_mode" = "true" ] && create_new_post "$src"
asset_version=""
if [ "$versioning" = "true" ]; then
asset_version="?v=$(date +%s)"
fi
escape_html_text() {
printf '%s' "$1" | sed \
-e 's/&/\&amp;/g' \
-e 's/</\&lt;/g' \
-e 's/>/\&gt;/g'
}
escape_html_attr() {
printf '%s' "$1" | sed \
-e 's/&/\&amp;/g' \
-e 's/"/\&quot;/g' \
-e 's/</\&lt;/g' \
-e 's/>/\&gt;/g'
}
nav_links_html() {
[ -n "$nav_links" ] || return
old_ifs=$IFS
set -f
IFS=','
# shellcheck disable=SC2086
set -- $nav_links
IFS=$old_ifs
set +f
[ $# -gt 0 ] || return
printf '<ul class="nav-extra-links">\n'
for raw_link in "$@"; do
link=$(printf '%s' "$raw_link" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -n "$link" ] || continue
case "$link" in
\[*\]\(*\))
label=${link#\[}
label=${label%%\]*}
link_url=${link#*](}
link_url=${link_url%)}
;;
*)
link_url=$link
label=$(printf '%s' "$link" | sed \
-e 's|^[A-Za-z][A-Za-z0-9+.-]*://||' \
-e 's|/$||')
[ -n "$label" ] || label="$link"
;;
esac
[ -n "$link_url" ] || continue
[ -n "$label" ] || label="$link_url"
link_attr=$(escape_html_attr "$link_url")
label_text=$(escape_html_text "$label")
printf '<li><a href="%s">%s</a></li>\n' "$link_attr" "$label_text"
done
printf '</ul>'
}
template="$src/template.html"
[ -f "$template" ] || template="./template.html"
[ -f "$template" ] || die "Template '$template' not found."
if [ ! -f "$template" ]; then
template="$KEWT_TMPDIR/default_template.html"
cat > "$template" <<'EOF'
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{TITLE}}</title>
<link rel="stylesheet" href="{{CSS}}" type="text/css" />
{{HEAD_EXTRA}}
</head>
<body>
<header>
<h1>{{HEADER_BRAND}}</h1>
</header>
<nav id="side-bar">{{NAV}}</nav>
<article>{{CONTENT}}</article>
<footer>{{FOOTER}}</footer>
</body>
</html>
EOF
fi
[ -d "$out" ] && rm -rf "$out"
mkdir -p "$out"
nav=$(generate_nav "$src")
extra_links=$(nav_links_html)
if [ -n "$extra_links" ]; then
nav="$nav
$extra_links"
fi
if [ -n "$nav_extra" ]; then
nav="$nav
$nav_extra"
fi
find_closest() {
target="$1"
@@ -338,13 +646,37 @@ copy_style_with_resolved_vars() {
render_markdown() {
file="$1"
is_home="$2"
url_override="$3"
if [ -n "$url_override" ]; then
current_url="$url_override"
else
rel_path="${file#"$src"}"
rel_path="${rel_path#/}"
current_url="/${rel_path%.md}.html"
fi
content_file="$file"
if [ -n "$posts_dir" ] && [ "$file" != "$src/$posts_dir/index.md" ]; then
dir_of_file=$(dirname "$file")
rel_dir_of_file="${dir_of_file#"$src"}"
rel_dir_of_file="${rel_dir_of_file#/}"
if [ "$rel_dir_of_file" = "$posts_dir" ]; then
temp_post_with_backlink="$KEWT_TMPDIR/post_with_backlink.md"
printf "[< Back](index.html)\n\n" > "$temp_post_with_backlink"
cat "$file" >> "$temp_post_with_backlink"
content_file="$temp_post_with_backlink"
fi
fi
local_template=$(find_closest "template.html" "$(dirname "$file")")
[ -z "$local_template" ] && local_template="$template"
closest_style_src=$(find_closest "styles.css" "$(dirname "$file")")
[ -z "$closest_style_src" ] && closest_style_src=$(find_closest "style.css" "$(dirname "$file")")
if [ -n "$closest_style_src" ]; then
style_rel_to_src="${closest_style_src#$src/}"
style_rel_to_src="${closest_style_src#"$src"/}"
case "$closest_style_src" in
"$src/styles.css") style_rel_to_src="styles.css" ;;
"$src/style.css") style_rel_to_src="style.css" ;;
@@ -386,42 +718,93 @@ render_markdown() {
head_extra="<link rel=\"icon\" href=\"$favicon_src\" />"
fi
MARKDOWN_SITE_ROOT="$src" MARKDOWN_FALLBACK_FILE="$script_dir/styles/$style.css" sh "$script_dir/markdown.sh" "$file" | awk -v title="$title" -v nav="$nav" -v footer="$footer" -v style_path="$style_path" -v header_brand="$header_brand" -v head_extra="$head_extra" -f "$awk_dir/render_template.awk" "$local_template"
page_title="$title"
if [ "$generate_page_title" = "true" ] && [ -n "$file" ] && [ -f "$file" ]; then
if [ "$is_home" = "true" ] && [ -n "$home_name" ]; then
page_title="$home_name - $title"
else
first_heading=$(grep -m 1 '^# ' "$file" | sed 's/^# *//; s/ *$//')
if [ -n "$first_heading" ]; then
first_heading=$(echo "$first_heading" | sed -e 's/\[//g' -e 's/\]//g' -e 's/!//g' -e 's/\*//g' -e 's/_//g' -e 's/`//g' -e 's/([^)]*)//g' | sed 's/\\//g')
page_title="$first_heading - $title"
else
basename_no_ext=$(basename "$file" .md)
if [ "$basename_no_ext" != "index" ] && [ "$basename_no_ext" != "404_gen" ]; then
cap_basename=$(echo "$basename_no_ext" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')
page_title="$cap_basename - $title"
fi
fi
fi
fi
ENABLE_HEADER_LINKS="$enable_header_links" CUSTOM_ADMONITIONS="$custom_admonitions" MARKDOWN_SITE_ROOT="$src" MARKDOWN_FALLBACK_FILE="$script_dir/styles/$style.css" sh "$script_dir/markdown.sh" "$content_file" | AWK_CURRENT_URL="$current_url" AWK_TITLE="$page_title" AWK_NAV="$nav" AWK_FOOTER="$footer" AWK_STYLE_PATH="${style_path}${asset_version}" AWK_HEADER_BRAND="$header_brand" AWK_HEAD_EXTRA="$head_extra" awk -f "$awk_dir/render_template.awk" "$local_template"
}
needs_rebuild() {
src_file="$1"
out_file="$2"
[ ! -f "$out_file" ] && return 0
[ "$src_file" -nt "$out_file" ] && return 0
[ -f "./site.conf" ] && [ "./site.conf" -nt "$out_file" ] && return 0
[ -f "$src/site.conf" ] && [ "$src/site.conf" -nt "$out_file" ] && return 0
[ -f "$template" ] && [ "$template" -nt "$out_file" ] && return 0
[ -f "$script_dir/styles/$style.css" ] && [ "$script_dir/styles/$style.css" -nt "$out_file" ] && return 0
return 1
}
echo "Building site from '$src' to '$out'..."
eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while read -r dir; do
rel_dir="${dir#$src}"
rel_dir="${dir#"$src"}"
rel_dir="${rel_dir#/}"
[ -z "$rel_dir" ] && rel_dir="."
out_dir="$out/$rel_dir"
mkdir -p "$out_dir"
if [ -f "$dir/styles.css" ]; then
if needs_rebuild "$dir/styles.css" "$out_dir/styles.css"; then
copy_style_with_resolved_vars "$dir/styles.css" "$out_dir/styles.css"
fi
elif [ -f "$dir/style.css" ]; then
if needs_rebuild "$dir/style.css" "$out_dir/styles.css"; then
copy_style_with_resolved_vars "$dir/style.css" "$out_dir/styles.css"
fi
fi
[ "$dir_indexes" != "true" ] && continue
if [ ! -f "$dir/index.md" ]; then
if [ "$single_file_index" = "true" ]; then
is_posts_dir="false"
if [ -n "$posts_dir" ] && { [ "$rel_dir" = "$posts_dir" ] || [ "./$rel_dir" = "$posts_dir" ]; }; then
is_posts_dir="true"
fi
if [ "$single_file_index" = "true" ] && [ "$is_posts_dir" = "false" ]; then
md_count=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md" | wc -l)
if [ "$md_count" -eq 1 ]; then
md_file=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md")
render_markdown "$md_file" > "$out_dir/index.html"
is_home="false"; [ "$dir" = "$src" ] && is_home="true"
target_url="/$rel_dir/index.html"
[ "$rel_dir" = "." ] && target_url="/index.html"
if needs_rebuild "$md_file" "$out_dir/index.html"; then
render_markdown "$md_file" "$is_home" "$target_url" > "$out_dir/index.html"
fi
continue
fi
fi
temp_index="/tmp/kewt_index_$$.md"
temp_index="$KEWT_TMPDIR/index.md"
display_dir="${rel_dir#.}"
[ -z "$display_dir" ] && display_dir="/"
echo "# Index of $display_dir" > "$temp_index"
echo "" >> "$temp_index"
find "$dir" ! -name "$(basename "$dir")" -prune ! -name ".*" -print | sort | while read -r entry; do
sort_args=""
# If this is the posts dir reverse
if [ "$rel_dir" = "$posts_dir" ] || [ "./$rel_dir" = "$posts_dir" ]; then
sort_args="-r"
fi
find "$dir" ! -name "$(basename "$dir")" -prune ! -name ".*" -print | LC_ALL=C sort $sort_args | while read -r entry; do
name="${entry##*/}"
case "$name" in
template.html|site.conf|style.css|index.md) continue ;;
@@ -429,22 +812,54 @@ eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while
if [ -d "$entry" ]; then
echo "- [${name}/](${name}/index.html)" >> "$temp_index"
elif [ "${entry%.md}" != "$entry" ]; then
echo "- [${name%.md}](${name%.md}.html)" >> "$temp_index"
label="${name%.md}"
# Try to get first heading
post_h=$(grep -m 1 '^# ' "$entry" | sed 's/^# *//')
if [ -n "$post_h" ]; then
post_h=$(echo "$post_h" | sed -e 's/\[//g' -e 's/\]//g' -e 's/!//g' -e 's/\*//g' -e 's/_//g' -e 's/`//g' -e 's/([^)]*)//g' | sed 's/\\//g')
if [ "$rel_dir" = "$posts_dir" ] || [ "./$rel_dir" = "$posts_dir" ]; then
# For posts add date and time
p_date=$(echo "${name%.md}" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
p_time="00:00"
if echo "${name%.md}" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}'; then
p_time=$(echo "${name%.md}" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
label="$post_h - $p_date $p_time"
else
label="$post_h"
fi
elif [ "$rel_dir" = "$posts_dir" ] || [ "./$rel_dir" = "$posts_dir" ]; then
# No heading and date and time for posts
p_date=$(echo "${name%.md}" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
p_time="00:00"
if echo "${name%.md}" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}'; then
p_time=$(echo "${name%.md}" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
label="$p_date $p_time"
fi
echo "- [$label](${name%.md}.html)" >> "$temp_index"
else
echo "- [$name]($name)" >> "$temp_index"
fi
done
render_markdown "$temp_index" > "$out_dir/index.html"
is_home="false"; [ "$dir" = "$src" ] && is_home="true"
target_url="/$rel_dir/index.html"
[ "$rel_dir" = "." ] && target_url="/index.html"
if needs_rebuild "$dir" "$out_dir/index.html"; then
render_markdown "$temp_index" "$is_home" "$target_url" > "$out_dir/index.html"
fi
rm "$temp_index"
fi
done
if [ ! -f "$out/styles.css" ] && [ -f "$script_dir/styles/$style.css" ]; then
if [ -f "$script_dir/styles/$style.css" ] && needs_rebuild "$script_dir/styles/$style.css" "$out/styles.css"; then
copy_style_with_resolved_vars "$script_dir/styles/$style.css" "$out/styles.css"
fi
eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type f -print" | sort | while IFS= read -r file; do
rel_path="${file#$src}"
rel_path="${file#"$src"}"
rel_path="${rel_path#/}"
dir_rel=$(dirname "$rel_path")
out_dir="$out/$dir_rel"
@@ -458,18 +873,123 @@ eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type f -print" | sort | while
is_preserved=1
fi
if [ "$single_file_index" = "true" ] && [ "${file%.md}" != "$file" ] && [ "$is_preserved" -eq 0 ] && [ ! -f "$(dirname "$file")/index.md" ]; then
is_posts_dir_2="false"
if [ -n "$posts_dir" ] && { [ "$dir_rel" = "$posts_dir" ] || [ "./$dir_rel" = "$posts_dir" ]; }; then
is_posts_dir_2="true"
fi
if [ "$single_file_index" = "true" ] && [ "${file%.md}" != "$file" ] && [ "$is_preserved" -eq 0 ] && [ ! -f "$(dirname "$file")/index.md" ] && [ "$is_posts_dir_2" = "false" ]; then
md_count=$(find "$(dirname "$file")" ! -name "$(basename "$(dirname "$file")")" -prune -name "*.md" | wc -l)
[ "$md_count" -eq 1 ] && continue
fi
if [ "${file%.md}" != "$file" ] && [ "$is_preserved" -eq 0 ]; then
is_home="false"; [ "$file" = "$src/index.md" ] && is_home="true"
out_file="$out/${rel_path%.md}.html"
render_markdown "$file" > "$out_file"
if needs_rebuild "$file" "$out_file"; then
render_markdown "$file" "$is_home" > "$out_file"
fi
else
if needs_rebuild "$file" "$out/$rel_path"; then
cp "$file" "$out/$rel_path"
fi
fi
done
if [ -n "$error_page" ] && [ ! -f "$out/$error_page" ]; then
temp_404="$KEWT_TMPDIR/404_gen.md"
echo "# 404 - Not Found" > "$temp_404"
echo "" >> "$temp_404"
echo "The requested page could not be found." >> "$temp_404"
render_markdown "$temp_404" "false" "/$error_page" > "$out/$error_page"
rm -f "$temp_404"
fi
if [ -n "$base_url" ]; then
sitemap_file="$out/sitemap.xml"
base_url="${base_url%/}"
today=$(date +%Y-%m-%d)
printf '<?xml version="1.0" encoding="UTF-8"?>\n' > "$sitemap_file"
printf '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' >> "$sitemap_file"
find "$out" -type f -name "*.html" -print | sort | while IFS= read -r html_file; do
rel_url="${html_file#"$out"}"
# Don't include 404 in the sitemap (duh)
[ "${rel_url#/}" = "$error_page" ] && continue
printf ' <url>\n' >> "$sitemap_file"
printf ' <loc>%s%s</loc>\n' "$base_url" "$rel_url" >> "$sitemap_file"
printf ' <lastmod>%s</lastmod>\n' "$today" >> "$sitemap_file"
printf ' </url>\n' >> "$sitemap_file"
done
printf '</urlset>\n' >> "$sitemap_file"
fi
if [ "$generate_feed" = "true" ] && [ -n "$base_url" ]; then
feed_path="$out/$feed_file"
base_url_feed="${base_url%/}"
build_date=$(date -u '+%a, %d %b %Y %H:%M:%S +0000')
printf '<?xml version="1.0" encoding="UTF-8"?>\n' > "$feed_path"
printf '<rss version="2.0">\n' >> "$feed_path"
printf ' <channel>\n' >> "$feed_path"
printf ' <title>%s</title>\n' "$title" >> "$feed_path"
printf ' <link>%s</link>\n' "$base_url_feed" >> "$feed_path"
printf ' <description>%s</description>\n' "$title" >> "$feed_path"
printf ' <lastBuildDate>%s</lastBuildDate>\n' "$build_date" >> "$feed_path"
find "$src" -type f -name '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]*.md' -print | LC_ALL=C sort -r | while IFS= read -r post_file; do
post_basename=$(basename "$post_file" .md)
# Extract YYYY-MM-DD
post_date=$(echo "$post_basename" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
# Extract HH:MM if present (e.g., 2026-03-17-10:30 or 2026-03-17-10:30_1)
post_time="00:00"
if echo "$post_basename" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}'; then
post_time=$(echo "$post_basename" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
post_slug=$(echo "$post_basename" | sed -e 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}//' -e 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}//' -e 's/^[_\-]//')
post_heading=$(grep -m 1 '^# ' "$post_file" | sed 's/^# *//')
if [ -z "$post_heading" ]; then
if [ -n "$post_slug" ] && ! echo "$post_slug" | grep -q '^[0-9]\+$'; then
post_heading=$(echo "$post_slug" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)}1')
else
post_heading="Post"
fi
fi
post_heading=$(echo "$post_heading" | sed -e 's/\[//g' -e 's/\]//g' -e 's/!//g' -e 's/\*//g' -e 's/_//g' -e 's/`//g' -e 's/([^)]*)//g' | sed 's/\\//g')
post_title="$post_heading - $post_date $post_time"
rel_path="${post_file#"$src"}"
rel_path="${rel_path#/}"
post_url="$base_url_feed/${rel_path%.md}.html"
pub_year=$(echo "$post_date" | cut -d- -f1)
pub_month=$(echo "$post_date" | cut -d- -f2)
pub_day=$(echo "$post_date" | cut -d- -f3)
case "$pub_month" in
01) pub_mon="Jan" ;; 02) pub_mon="Feb" ;; 03) pub_mon="Mar" ;;
04) pub_mon="Apr" ;; 05) pub_mon="May" ;; 06) pub_mon="Jun" ;;
07) pub_mon="Jul" ;; 08) pub_mon="Aug" ;; 09) pub_mon="Sep" ;;
10) pub_mon="Oct" ;; 11) pub_mon="Nov" ;; 12) pub_mon="Dec" ;;
esac
pub_date="${pub_day} ${pub_mon} ${pub_year} ${post_time}:00 +0000"
printf ' <item>\n' >> "$feed_path"
printf ' <title>%s</title>\n' "$post_title" >> "$feed_path"
printf ' <link>%s</link>\n' "$post_url" >> "$feed_path"
printf ' <guid>%s</guid>\n' "$post_url" >> "$feed_path"
printf ' <pubDate>%s</pubDate>\n' "$pub_date" >> "$feed_path"
printf ' </item>\n' >> "$feed_path"
done
printf ' </channel>\n' >> "$feed_path"
printf '</rss>\n' >> "$feed_path"
fi
echo "Build complete."

View File

@@ -1,19 +1,21 @@
#!/bin/sh
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
script_dir=$(CDPATH="" cd -- "$(dirname -- "$0")" && pwd)
awk_dir="$script_dir/awk"
sed_inplace() {
script="$1"
file="$2"
tmp="${file}.tmp.$$"
sed "$script" "$file" > "$tmp" && mv "$tmp" "$file" || {
if sed "$script" "$file" > "$tmp" && mv "$tmp" "$file"; then
return 0
else
rm -f "$tmp"
return 1
}
fi
}
temp_file="/tmp/markdown.$$.md"
temp_file="${KEWT_TMPDIR:-/tmp}/markdown.$$.md"
cat "$@" > "$temp_file"
trap 'rm -f "$temp_file" "$temp_file.tmp"' EXIT INT TERM
@@ -38,18 +40,23 @@ done
sed_inplace "/^\[[^\]]*\]: */d" "$temp_file"
# Blocks
sed_inplace "s/^>!\[/> [!/g" "$temp_file"
sed_inplace "s/^>\[!/> [!/g" "$temp_file"
loop_count=0
max_iterations=100
while grep '^>' "$temp_file" >/dev/null; do
awk -f "$awk_dir/blockquote.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
loop_count=$((loop_count + 1))
if [ "$loop_count" -gt "$max_iterations" ]; then
echo "Warning: Blockquote processing exceeded $max_iterations iterations on $1. Breaking to prevent infinite loop." >&2
break
fi
done
awk -f "$awk_dir/blockquote_to_admonition.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -v custom_admonitions="$CUSTOM_ADMONITIONS" -f "$awk_dir/blockquote_to_admonition.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/fenced_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/indented_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/pipe_tables.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/headers.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -v enable_header_links="$ENABLE_HEADER_LINKS" -f "$awk_dir/headers.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/lists.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# Spacing

7
package.json Normal file
View File

@@ -0,0 +1,7 @@
{
"name": "kewt",
"description": "A minimalist static site generator inspired by werc",
"global": "true",
"install": "make install",
"scripts": ["kewt"]
}

View File

@@ -0,0 +1,16 @@
pkgbase = kewt-git
pkgdesc = A minimalist, 100% POSIX, static site generator inspired by werc and kew
pkgver = r0.0000000
pkgrel = 3
url = https://kewt.krzak.org
arch = any
license = ISC
makedepends = git
depends = sh
provides = kewt
conflicts = kewt
conflicts = kewt-bin
source = kewt-git::git+https://git.krzak.org/N0VA/kewt.git
sha256sums = SKIP
pkgname = kewt-git

View File

@@ -0,0 +1,15 @@
pkgbase = kewt-bin
pkgdesc = A minimalist, 100% POSIX, static site generator inspired by werc and kew
pkgver = VERSION_PLACEHOLDER
pkgrel = 2
url = https://kewt.krzak.org
arch = any
license = ISC
depends = sh
provides = kewt
conflicts = kewt
conflicts = kewt-git
source = kewt-bin-VERSION_PLACEHOLDER.sh::https://git.krzak.org/N0VA/kewt/releases/download/vVERSION_PLACEHOLDER/kewt
sha256sums = SHA256SUM_PLACEHOLDER
pkgname = kewt-bin

View File

@@ -0,0 +1,29 @@
# Maintainer: n0va <n0va@krzak.org>
pkgname=kewt-git
pkgver=r0.0000000
pkgrel=2
pkgdesc="A minimalist, 100% POSIX, static site generator inspired by werc and kew"
arch=('any')
url="https://kewt.krzak.org"
license=('ISC')
makedepends=('git')
depends=('sh')
provides=('kewt')
conflicts=('kewt' 'kewt-bin')
source=("${pkgname}::git+https://git.krzak.org/N0VA/kewt.git")
sha256sums=('SKIP')
pkgver() {
cd "${pkgname}"
printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
build() {
cd "${pkgname}"
sh tools/build-standalone.sh
}
package() {
cd "${pkgname}"
install -Dm755 kewt "${pkgdir}/usr/bin/kewt"
}

View File

@@ -0,0 +1,21 @@
# Maintainer: n0va <n0va@krzak.org>
pkgname=kewt-bin
pkgver=VERSION_PLACEHOLDER
pkgrel=1
pkgdesc="A minimalist, 100% POSIX, static site generator inspired by werc and kew"
arch=('any')
url="https://kewt.krzak.org"
license=('ISC')
depends=('sh')
provides=('kewt')
conflicts=('kewt' 'kewt-git')
source=("${pkgname}-${pkgver}.sh::https://git.krzak.org/N0VA/kewt/releases/download/v${pkgver}/kewt")
sha256sums=('SHA256SUM_PLACEHOLDER')
build() {
chmod +x "${srcdir}/${pkgname}-${pkgver}.sh"
}
package() {
install -Dm755 "${srcdir}/${pkgname}-${pkgver}.sh" "${pkgdir}/usr/bin/kewt"
}

View File

@@ -0,0 +1,16 @@
class Kewt < Formula
desc "Minimalist static site generator inspired by werc"
homepage "https://kewt.krzak.org"
url "https://github.com/n0va-bot/kewt/releases/download/vVERSION_PLACEHOLDER/kewt"
sha256 "SHA256SUM_PLACEHOLDER"
license "ISC"
version "VERSION_PLACEHOLDER"
def install
bin.install "kewt"
end
test do
system "#{bin}/kewt", "--version"
end
end

View File

@@ -1,12 +0,0 @@
title = "kewt"
style = "kewt"
dir_indexes = true
single_file_index = true
flatten = false
footer = "made with <a href="https://kewt.krzak.org">kewt</a>"
logo = ""
display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
order = ""

View File

@@ -15,3 +15,5 @@ Sed dictum tortor at interdum dignissim. Nunc hendrerit sollicitudin elementum.
Cras vitae sapien egestas, blandit libero et, volutpat augue. Ut augue quam, sollicitudin quis libero laoreet, bibendum imperdiet massa. Duis sed venenatis risus. Praesent a est mollis, viverra erat quis, faucibus elit. Donec at sagittis est, non posuere nisi. Integer posuere pharetra dui in aliquam. Morbi vehicula eros in hendrerit aliquam. Duis in turpis vel mauris mattis convallis in id tortor. Cras et aliquam augue.
Cras quis consectetur dolor, a sodales tortor. Vestibulum aliquam lacinia metus, sed viverra erat egestas in. Morbi interdum sapien sed bibendum maximus. Aenean accumsan pharetra libero dapibus aliquam. Etiam sodales purus posuere gravida ullamcorper. Vestibulum tincidunt, nibh a pulvinar aliquet, leo tortor pulvinar diam, ut viverra nunc elit bibendum nulla. Praesent vel pulvinar erat, eu efficitur magna. Mauris at consequat purus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce in dui quis nisi elementum aliquam. Proin eget justo sed est commodo accumsan. Suspendisse a feugiat tellus, eget gravida tellus.
![https://www.youtube.com/embed/NvQD9E5Cq8A]

BIN
site/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

View File

@@ -1,8 +1,12 @@
# _kewt_
### Pronounced "cute"
***
# [Go to the repo](https://git.krzak.org/N0VA/kewt)
***
_kewt_ is a minimalist ssg inspired by _[werc](http://werc.cat-v.org/)_ and _[kew](https://github.com/uint23/kew)_
It's meant to be a static site generator, like _[kew](https://github.com/uint23/kew)_ but use only default (POSIX) tooling, like _[werc](http://werc.cat-v.org/)_ (and definitely unlike _[kew](https://github.com/uint23/kew)_)
@@ -15,22 +19,86 @@ It's meant to be a static site generator, like _[kew](https://github.com/uint23/
- Automatic inlining and embedding of many filetypes with `\![link]` or `\![alt](link)`
- Inline html support
- MFM `$font` and `\<plain>` tags
- Admonition support (that's what the blocks like the warning block below are called)
- GFM Admonition support (that's what the blocks like the warning block below are called)
- Task list support (`- [ ]`, `- [x]`)
- RSS/Feed generation and Sitemap support
- Post creation via `--post`
- Automatic 404 page generation
- `?v=n` support for cache busting
- Code block classes for use with external libraries like highlight.js or prism.js (both tested)
- Clickable markdown header anchors
- Mobile responsive layout
If you want to **force** a file to be inlined, use `\!![]` instead of `\![]`
***
## Installation
### Standalone
```sh
curl -L -o kewt https://git.krzak.org/N0VA/kewt/releases/download/latest/kewt
chmod +x kewt
```
### From source
```sh
git clone https://git.krzak.org/N0VA/kewt.git
cd kewt
```
#### Building
```sh
make
```
#### Installing
```sh
sudo make install
```
### Package Managers
#### AUR
- [kewt-bin](https://aur.archlinux.org/packages/kewt-bin) — prebuilt standalone binary from the latest release
- [kewt-git](https://aur.archlinux.org/packages/kewt-git) — built from the latest git source
#### Homebrew
```sh
brew tap n0va-bot/tap
brew install kewt
```
#### bpkg
```sh
bpkg install n0va-bot/kewt
```
***
## Usage
```sh
./kewt.sh --help
./kewt.sh --version
./kewt.sh --new [title]
./kewt.sh --post
./kewt.sh --from <src> --to <out>
./kewt.sh [src] [out]
```
`--new [title]` creates a new site directory with a copied `site.conf` and a default `index.md`.
## site.conf
`--post` creates a new empty markdown file in the configured `posts_dir` with the current date and time as the name.
### site.conf
```conf
title = "kewt"
@@ -41,12 +109,23 @@ flatten = false
order = ""
home_name = "Home"
show_home_in_nav = true
footer = "made with <a href="https://kewt.krzak.org">kewt</a>"
nav_links = ""
nav_extra = ""
footer = "made with <a href=\"https://kewt.krzak.org\">kewt</a>"
logo = ""
display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
base_url = ""
generate_feed = false
feed_file = "rss.xml"
posts_dir = ""
enable_header_links = true
custom_admonitions = ""
```
- `title` site title
@@ -57,20 +136,31 @@ favicon = ""
- `order` comma separated file/directory name list to order the sidebar (alphabetical by default)
- `home_name` text for the home link in navigation (default: "Home")
- `show_home_in_nav` show home link in navigation (default: true)
- `nav_links` comma separated extra nav links, as bare URLs or Markdown links like `[Label](https://example.com)`
- `nav_extra` raw HTML appended inside the `<nav>` after the generated link list
- `footer` footer html/text shown at the bottom of pages
- `logo` logo image path (used in header if enabled)
- `display_logo` show logo in header
- `display_title` show title text in header
- `logo_as_favicon` use `logo` as favicon
- `favicon` explicit favicon path (used when `logo_as_favicon` is false or no logo is set)
- `generate_page_title` automatically generate title text from the first markdown heading or filename (default: true)
- `error_page` filename for the generated 404 error page (default: "not_found.html", empty to disable)
- `versioning` append a version query parameter (`?v=timestamp`) to css asset urls to bypass cache (default: false)
- `base_url` absolute URL of the site, used for sitemap and RSS feed generation
- `generate_feed` enable RSS feed generation (requires `base_url`)
- `feed_file` filename for the generated RSS feed (default: "rss.xml")
- `posts_dir` directory name containing posts (e.g., "posts"). Enables reverse-chronological sorting, title headings in indexes, and automatic backlinks.
- `enable_header_links` turns markdown section headings into clickable anchor links (default: true)
- `custom_admonitions` comma separated list of custom admonitions
## Ignores
### Ignores
- `.kewtignore`: Files/directories to ignore. If empty, the whole directory gets ignored
- `.kewthide`: Files/directories to hide from navigation but still process. Same empty rules as with ignore
- `.kewtpreserve`: Files/directories to copy but not convert markdown to html. Same empty rules again
## Embeds
### Embeds
- `\![link]`:
- local image/audio/video files are embedded as media tags
@@ -80,10 +170,7 @@ favicon = ""
- `\![alt](link)` works the same, with `alt` used for images
- `\!![]` and `\!![alt](link)` force inline local file contents
## Credits
***
- Markdown to html conversion based on [markdown.bash](https://github.com/chadbraunduin/markdown.bash) by [chadbraunduin](https://github.com/chadbraunduin)
- Default css style and html template based on _[kew](https://github.com/uint23/kew)_ by [uint23](https://github.com/uint23)
>![WARNING]
>Most of this was coded at night, while sleepy and a bit sick, and after walking for about 4 hours around a forest, so...
>[!WARNING]
>The base that all of this is built upon was coded at night, while sleepy and a bit sick, and after walking for about 4 hours around a forest, so...

View File

@@ -3,10 +3,20 @@ style = "kewt"
dir_indexes = true
single_file_index = true
flatten = false
footer = "<a href="https://kewt.krzak.org"><img src="/button.gif" /></a>"
footer = "<a href=\"https://kewt.krzak.org\"><img src=\"/button.gif\" /></a>"
logo = ""
display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
logo_as_favicon = false
favicon = "favicon.ico"
order = ""
home_name = "Home"
show_home_in_nav = true
nav_links = ""
nav_extra = ""
generate_page_title = true
error_page = "not_found.html"
versioning = true
enable_header_links = true
base_url = "https://kewt.krzak.org"
custom_admonitions = ""

View File

@@ -1,25 +1,28 @@
:root {
--bg: #646c7f;
--fg: #fffde0;
--fg-link: #fff18f;
--code-bg: #32394a;
--code-border: #8f95a4;
--code-fg: #fffde0;
--code-sel: #fff18f;
--code-prop: #ffd27f;
--code-val: #cde7ff;
--code-var: #b9ffbe;
--code-com: #d0d0d0;
--adm-note-bg: #3f5666;
--adm-note-border: #a8d8ff;
--adm-tip-bg: #3f664c;
--adm-tip-border: #b9ffbe;
--adm-important-bg: #5a4a6c;
--adm-important-border: #e4c7ff;
--adm-warning-bg: #6b5539;
--adm-warning-border: #ffe0a8;
--adm-caution-bg: #6f3f3f;
--adm-caution-border: #ffb4b4;
--bg: #4a3b69;
--bg-deep: #352654;
--fg: #fbf5ff;
--fg-muted: #c8b9df;
--fg-link: #dfaeff;
--fg-heading: #debfff;
--code-bg: #31234c;
--code-border: #8060af;
--code-fg: #fbf5ff;
--code-sel: #ffef99;
--code-prop: #ffdfba;
--code-val: #cae2ff;
--code-var: #caffc2;
--code-com: #b8aac8;
--adm-note-bg: #353866;
--adm-note-border: #b8c5ff;
--adm-tip-bg: #295246;
--adm-tip-border: #aeffda;
--adm-important-bg: #533076;
--adm-important-border: #f4d9ff;
--adm-warning-bg: #634631;
--adm-warning-border: #ffe2bd;
--adm-caution-bg: #662d43;
--adm-caution-border: #ffc4d5;
}
body {
@@ -29,18 +32,22 @@ body {
color: var(--fg);
font-family: serif;
font-size: 16px;
line-height: 1.2;
line-height: 1.5;
}
header {
padding: 20px;
padding-bottom: 0;
border-bottom: 1px solid var(--code-border);
margin-bottom: 20px;
}
header h1 {
margin: 0;
font-size: 35px;
font-weight: normal;
font-weight: bold;
font-style: italic;
color: var(--fg-heading);
}
.site-logo {
@@ -57,18 +64,26 @@ header a {
text-decoration: none;
}
header a:hover {
color: var(--bg-deep);
background: var(--fg);
}
#side-bar {
position: absolute;
top: 80px;
left: 0;
width: 200px;
padding-left: 20px;
margin-right: 14px;
border-right: 1px solid var(--code-border);
padding-right: 7px;
}
.side-title {
font-size: 25px;
margin: 20px 0 8px 0;
color: var(--fg);
color: var(--fg-heading);
}
#side-bar ul {
@@ -87,6 +102,14 @@ a {
padding: 1px 2px;
}
#side-bar a.current-page {
font-weight: bold;
color: var(--fg);
border-left: 3px solid var(--fg-link);
padding-left: 7px;
margin-left: -10px;
}
a:hover {
background: var(--fg);
color: var(--bg);
@@ -100,7 +123,7 @@ article {
h3 {
margin-top: 30px;
font-size: 25px;
color: var(--fg);
color: var(--fg-heading);
font-weight: normal;
}
@@ -197,10 +220,11 @@ pre code {
}
footer {
padding-top: 80px;
padding-top: 60px;
font-style: italic;
font-size: 17px;
margin-bottom: 20px;
color: var(--fg-muted);
}
article,
@@ -208,3 +232,54 @@ footer {
margin-left: 240px;
margin-top: 0px;
}
img {
image-rendering: auto;
image-rendering: crisp-edges;
image-rendering: pixelated;
image-rendering: -webkit-optimize-contrast;
}
footer img {
display: inline-block;
vertical-align: top;
}
hr {
height: 0;
margin: 24px 0;
border: 0;
border-top: 1px solid var(--code-border);
}
@media screen and (max-width: 600px) {
#side-bar {
position: relative;
top: auto;
left: auto;
width: auto;
border-right: none;
border-bottom: 1px solid var(--code-border);
padding: 0 0 20px 0;
margin: 0 20px 20px 20px;
}
article {
margin: 0 20px 0 20px;
}
footer {
margin-left: 20px;
margin-right: 20px;
padding-top: 30px;
}
}
.task-list-item {
list-style-type: none;
}
.task-list-item-checkbox {
margin: 0 0.2em 0.25em -1.6em;
vertical-align: middle;
}

View File

@@ -1,21 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>{{TITLE}}</title>
<link rel="stylesheet" href="{{CSS}}" type="text/css" />
{{HEAD_EXTRA}}
</head>
<body>
<header>
<h1>{{HEADER_BRAND}}</h1>
</header>
<nav id="side-bar">{{NAV}}</nav>
<article>{{CONTENT}}</article>
<footer>{{FOOTER}}</footer>
</body>
</html>

39
tools/build-standalone.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/bin/sh
set -e
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
if [ ! -f "$REPO_ROOT/kewt.sh" ]; then
echo "kewt.sh not found. Run from the repository root or tools/."
exit 1
fi
OUT_FILE="$REPO_ROOT/kewt"
cat << 'EOF' > "$OUT_FILE"
#!/bin/sh
tmpdir=$(mktemp -d "/tmp/kewt.XXXXXX")
trap 'rm -rf "$tmpdir"' EXIT HUP INT TERM
# Extract payload
sed '1,/^#==PAYLOAD==$/d' "$0" | tar -xz -C "$tmpdir"
# Pass control to the extracted script
KEWT_INVOKED_AS="$0" "$tmpdir/kewt.sh" "$@"
exit $?
#==PAYLOAD==
EOF
VERSION=$(git describe --tags 2>/dev/null || echo "standalone")
tmpbuild=$(mktemp -d)
cp -r "$REPO_ROOT/kewt.sh" "$REPO_ROOT/markdown.sh" "$REPO_ROOT/awk" "$REPO_ROOT/styles" "$tmpbuild/"
sed -e "s/kewt version git/kewt version $VERSION/" "$tmpbuild/kewt.sh" > "$tmpbuild/kewt.sh.tmp" && mv "$tmpbuild/kewt.sh.tmp" "$tmpbuild/kewt.sh"
chmod +x "$tmpbuild/kewt.sh" "$tmpbuild/markdown.sh"
tar -cz -C "$tmpbuild" kewt.sh markdown.sh awk styles >> "$OUT_FILE"
rm -rf "$tmpbuild"
chmod +x "$OUT_FILE"
echo "Generated standalone executable at $OUT_FILE"