41 Commits

Author SHA1 Message Date
930e1ac4dc Fedora packaging (hopefully)
Some checks failed
Deploy Website / deploy-website (push) Successful in 39s
Publish kewt-git to AUR / publish-aur-git (push) Successful in 21s
Release Standalone Builder / build (release) Successful in 27s
Release Standalone Builder / publish-fedora (release) Failing after 15s
Release Standalone Builder / publish-aur (release) Successful in 22s
Release Standalone Builder / publish-homebrew (release) Successful in 7s
2026-05-20 10:30:10 +02:00
cd9550ee8a Add tests and refactor config 2026-05-20 08:30:21 +02:00
8b760b2897 fix: homebrew permissions on macOS (hopefully)
All checks were successful
Deploy Website / deploy-website (push) Successful in 40s
Publish kewt-git to AUR / publish-aur-git (push) Successful in 20s
Release Standalone Builder / build (release) Successful in 26s
Release Standalone Builder / publish-aur (release) Successful in 20s
Release Standalone Builder / publish-homebrew (release) Successful in 7s
2026-05-12 21:00:19 +02:00
e3bf598dd9 packaging: template cleanup 2026-05-12 20:53:13 +02:00
a086565ede fix: header links
All checks were successful
Deploy Website / deploy-website (push) Successful in 21s
Release Standalone Builder / build (release) Successful in 23s
Release Standalone Builder / publish-aur (release) Successful in 20s
Release Standalone Builder / publish-homebrew (release) Successful in 6s
2026-05-06 13:21:25 +02:00
79bc467bf6 fix: styling
Some checks failed
Deploy Website / deploy-website (push) Successful in 28s
Release Standalone Builder / build (release) Successful in 29s
Release Standalone Builder / publish-aur (release) Failing after 4m6s
Release Standalone Builder / publish-homebrew (release) Successful in 7s
2026-05-05 20:50:59 +02:00
8a5fa202de fix: rsync permissions
All checks were successful
Deploy Website / deploy-website (push) Successful in 27s
2026-05-05 20:44:47 +02:00
c8df9a3da9 fix: navbar draft
Some checks failed
Release Standalone Builder / build (release) Successful in 1m50s
Deploy Website / deploy-website (push) Failing after 29s
Release Standalone Builder / publish-homebrew (release) Has been cancelled
Release Standalone Builder / publish-aur (release) Has been cancelled
2026-05-05 20:39:55 +02:00
b8e6c3afa8 feat: manifest/caching 2026-05-05 20:27:35 +02:00
3105c83290 feat: separate emoji file 2026-05-05 20:10:01 +02:00
206d9a650d feat/whatever: cleanup 2026-05-05 19:59:44 +02:00
105dce7d40 theming 2026-05-05 19:48:35 +02:00
21dc56aa6d docs: markdown
All checks were successful
Release Standalone Builder / build (release) Successful in 32s
Release Standalone Builder / publish-aur (release) Successful in 35s
Release Standalone Builder / publish-homebrew (release) Successful in 6s
2026-04-01 15:39:54 +02:00
7df5daaa6c branding: table styles 2026-04-01 15:24:37 +02:00
4f74dd5fe0 fix: !! 2026-04-01 15:09:24 +02:00
0751849492 docs: theming 2026-04-01 15:04:57 +02:00
009877ae76 feat: colour palettes
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-04-01 14:23:12 +02:00
69bd5832e7 Create .gitattributes 2026-04-01 14:22:09 +02:00
b525a5f1c2 branding: new button 2026-04-01 14:21:58 +02:00
2a03859390 Update lint.yml 2026-04-01 13:10:37 +02:00
b65c4c6665 docs: updates for v1.6.0
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-04-01 13:02:16 +02:00
90d8e25b70 feat: --clean and --init
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-04-01 12:53:25 +02:00
8814c12480 feat: include_cw_pages_in_search
All checks were successful
Lint / shellcheck (push) Successful in 19s
Publish kewt-git to AUR / publish-aur-git (push) Successful in 33s
2026-04-01 12:39:06 +02:00
0c0f249226 feat: modular building and search 2026-04-01 12:35:26 +02:00
b29a5274e1 feat: zsh completions 2026-04-01 12:34:54 +02:00
00f4bbb5f0 docs: quickstart.md 2026-04-01 12:34:05 +02:00
7fe204f9f9 feat: previous and next buttons 2026-04-01 09:33:33 +02:00
81d3caff45 feat/fix: sort the blog dir by date
All checks were successful
Lint / shellcheck (push) Successful in 20s
Release Standalone Builder / build (release) Successful in 32s
Release Standalone Builder / publish-aur (release) Successful in 35s
Release Standalone Builder / publish-homebrew (release) Successful in 8s
2026-04-01 09:02:46 +02:00
100979d28a docs: massive doc update
All checks were successful
Lint / shellcheck (push) Successful in 21s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Successful in 35s
Release Standalone Builder / publish-homebrew (release) Successful in 7s
2026-03-31 12:08:52 +02:00
d35ddcf487 feat: {{VERSION}} and content warning pages 2026-03-31 11:44:29 +02:00
aac9198878 feat: draft_by_default and lang
All checks were successful
Lint / shellcheck (push) Successful in 36s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Successful in 34s
Release Standalone Builder / publish-homebrew (release) Successful in 7s
2026-03-30 09:55:26 +02:00
99e1f5dd24 fix: markdown embedding 2026-03-30 09:54:44 +02:00
3970c6eb47 fix: inline html 2026-03-30 09:54:23 +02:00
3075719963 feat: might be actually feature-complete by now
All checks were successful
Lint / shellcheck (push) Successful in 21s
Release Standalone Builder / build (release) Successful in 33s
Release Standalone Builder / publish-aur (release) Successful in 36s
Release Standalone Builder / publish-homebrew (release) Successful in 7s
2026-03-26 13:16:34 +01:00
0379d38234 docs: new docs
All checks were successful
Lint / shellcheck (push) Successful in 22s
Release Standalone Builder / build (release) Successful in 33s
Release Standalone Builder / publish-aur (release) Successful in 34s
Release Standalone Builder / publish-homebrew (release) Successful in 6s
2026-03-25 10:42:24 +01:00
b22897135e feat: --generate-template 2026-03-25 10:20:19 +01:00
185cf769c3 fix: shellcheck style 2026-03-25 10:07:04 +01:00
b2acd26660 feat: custom directory indexes 2026-03-25 10:02:22 +01:00
4069bafd52 fix: typo in codeblock handling that made it so headers appear
All checks were successful
Lint / shellcheck (push) Successful in 19s
Release Standalone Builder / build (release) Successful in 33s
Release Standalone Builder / publish-aur (release) Successful in 36s
Release Standalone Builder / publish-homebrew (release) Successful in 7s
2026-03-24 08:18:11 +01:00
9dbd41392e docs: update installation instructions link in readme
All checks were successful
Lint / shellcheck (push) Successful in 22s
2026-03-24 08:12:48 +01:00
35eac48dcd fix: v1.4.0 hotfix
All checks were successful
Lint / shellcheck (push) Successful in 22s
Release Standalone Builder / build (release) Successful in 32s
Release Standalone Builder / publish-aur (release) Successful in 35s
Release Standalone Builder / publish-homebrew (release) Successful in 6s
2026-03-23 12:09:54 +01:00
88 changed files with 5803 additions and 1309 deletions

14
.gitattributes vendored Normal file
View File

@@ -0,0 +1,14 @@
* text=auto eol=lf
*.sh text eol=lf
*.awk text eol=lf
*.css text eol=lf
*.html text eol=lf
*.js text eol=lf
*.md text eol=lf
*.conf text eol=lf
*.json text eol=lf
*.xml text eol=lf
*.png binary
*.gif binary
*.svg text eol=lf
*.ico binary

View File

@@ -0,0 +1,43 @@
name: Deploy Website
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy-website:
runs-on: local
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Ensure deploy tools are available
run: |
if ! command -v ssh >/dev/null 2>&1 || ! command -v rsync >/dev/null 2>&1 || ! command -v ssh-keyscan >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y openssh-client rsync
fi
- name: Build website
run: |
sh kewt.sh --from site --to out
- name: Setup SSH
run: |
mkdir -p ~/.ssh
printf '%s\n' "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
REMOTE_HOST_VAL="${{ secrets.REMOTE_HOST }}"
REMOTE_ADDR=$(printf '%s\n' "$REMOTE_HOST_VAL" | awk -F@ '{print $NF}')
ssh-keyscan -H "$REMOTE_ADDR" >> ~/.ssh/known_hosts
- name: Deploy website
run: |
rsync -rlz --delete --omit-dir-times \
--exclude '/logs/' \
-e "ssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no" \
out/ "${{ secrets.REMOTE_HOST }}:/var/www/kewt.krzak.org/"

View File

@@ -1,16 +0,0 @@
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

@@ -16,10 +16,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Arch Linux environment
- name: Ensure publish tools are available
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager curl jq || true
sudo apt-get install -y openssh-client git
- name: Prepare AUR files
run: |
@@ -27,12 +27,23 @@ jobs:
cp packaging/AUR/PKGBUILD.git aur-work/PKGBUILD
cp packaging/AUR/.SRCINFO.git aur-work/.SRCINFO
- name: Setup SSH
run: |
mkdir -p ~/.ssh
printf '%s\n' "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
- 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 }}"
run: |
rm -rf aur-repo
git clone "ssh://aur@aur.archlinux.org/kewt-git.git" aur-repo
cp aur-work/PKGBUILD aur-repo/PKGBUILD
cp aur-work/.SRCINFO aur-repo/.SRCINFO
cd aur-repo
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.github.com"
git add PKGBUILD .SRCINFO
git commit -m "Update kewt-git to ${GITHUB_SHA}" || exit 0
GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" git push origin HEAD:master

View File

@@ -20,51 +20,80 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
go-version: "1.21"
- name: Upload Release Asset
uses: https://gitea.com/actions/release-action@main
with:
files: |-
kewt
api_key: '${{secrets.GITEA_TOKEN}}'
packaging/bash/kewt.bash
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-fedora:
runs-on: local
needs: build
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
run: |
sudo apt-get update || true
sudo apt-get install -y rpm || true
- name: Build Fedora Assets
run: |
if command -v rpmbuild >/dev/null; then
make srpm
else
echo "rpmbuild not found, generating spec file only"
TAG="${GITHUB_REF#refs/tags/}"
VERSION="${TAG#v}"
sed -e "s/VERSION_PLACEHOLDER/${VERSION}/g" packaging/fedora/kewt.spec.template > packaging/fedora/kewt.spec
fi
- name: Upload Fedora Assets
uses: https://gitea.com/actions/release-action@main
with:
files: |-
kewt-*.src.rpm
packaging/fedora/kewt.spec
api_key: "${{secrets.GITEA_TOKEN}}"
publish-aur:
runs-on: local
needs: build
@@ -75,7 +104,7 @@ jobs:
- name: Setup Arch Linux environment
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager curl jq || true
sudo apt-get install -y openssh-client curl jq
- name: Render PKGBUILD and SRCINFO
run: |
@@ -95,15 +124,26 @@ jobs:
-e "s/SHA256SUM_PLACEHOLDER/${CHECKSUM}/g" \
packaging/AUR/.SRCINFO.template > aur-work/.SRCINFO
- name: Setup AUR SSH
run: |
mkdir -p ~/.ssh
printf '%s\n' "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
- 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 }}"
run: |
rm -rf aur-repo
git clone "ssh://aur@aur.archlinux.org/kewt-bin.git" aur-repo
cp aur-work/PKGBUILD aur-repo/PKGBUILD
cp aur-work/.SRCINFO aur-repo/.SRCINFO
cd aur-repo
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.github.com"
git add PKGBUILD .SRCINFO
git commit -m "Update kewt-bin to ${GITHUB_REF_NAME}" || exit 0
GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" git push origin HEAD:master
publish-homebrew:
runs-on: local
@@ -122,19 +162,16 @@ jobs:
CHECKSUM=$(sha256sum kewt-binary | awk '{print $1}')
rm -f kewt-binary
git clone https://x-access-token:${{ secrets.GH_RELEASE_TOKEN }}@github.com/n0va-bot/homebrew-tap.git brew-work || true
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
[ -d .git ] || { git init && git checkout --orphan main && git remote add origin https://x-access-token:${{ secrets.GH_RELEASE_TOKEN }}@github.com/n0va-bot/homebrew-tap.git; }
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
git push -u origin main

2
.gitignore vendored
View File

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

1
.shellcheckrc Normal file
View File

@@ -0,0 +1 @@
disable=SC2034,SC2154

View File

@@ -1,5 +1,7 @@
PREFIX ?= /usr/local
BINDIR = $(PREFIX)/bin
ZSHCOMPDIR ?= $(PREFIX)/share/zsh/site-functions
BASHCOMPDIR ?= $(PREFIX)/share/bash-completion/completions
all: kewt
@@ -9,11 +11,32 @@ kewt:
install: kewt
install -d $(DESTDIR)$(BINDIR)
install -m 755 kewt $(DESTDIR)$(BINDIR)/kewt
install -d $(DESTDIR)$(ZSHCOMPDIR)
install -m 644 packaging/zsh/_kewt $(DESTDIR)$(ZSHCOMPDIR)/_kewt
install -d $(DESTDIR)$(BASHCOMPDIR)
install -m 644 packaging/bash/kewt.bash $(DESTDIR)$(BASHCOMPDIR)/kewt
uninstall:
rm -f $(DESTDIR)$(BINDIR)/kewt
rm -f $(DESTDIR)$(ZSHCOMPDIR)/_kewt
rm -f $(DESTDIR)$(BASHCOMPDIR)/kewt
clean:
rm -f kewt
rm -f kewt kewt-*.tar.gz
.PHONY: all install uninstall clean
dist:
$(eval VERSION := $(shell git describe --tags --always | sed 's/^v//;s/-/./g'))
tar -czf kewt-$(VERSION).tar.gz --exclude-vcs --exclude=kewt --exclude=kewt-$(VERSION).tar.gz --transform "s|^|kewt-$(VERSION)/|" *
srpm: dist
$(eval VERSION := $(shell git describe --tags --always | sed 's/^v//;s/-/./g'))
sed -e "s/VERSION_PLACEHOLDER/$(VERSION)/g" packaging/fedora/kewt.spec.template > packaging/fedora/kewt.spec
rpmbuild -bs --define "_sourcedir $(PWD)" --define "_srcrpmdir $(PWD)" packaging/fedora/kewt.spec
test:
sh tests/test_runner.sh
shellcheck:
shellcheck kewt.sh markdown.sh lib/*.sh
.PHONY: all install uninstall clean test shellcheck

View File

@@ -10,7 +10,7 @@
_kewt_ is a minimalist ssg inspired by _[werc](http://werc.cat-v.org/)_ and _[kew](https://github.com/uint23/kew)_
## [Installation](https://kewt.krzak.org/#Installation)
## [Installation](https://kewt.krzak.org/docs/installation)
## Contributing

62
awk/definition_lists.awk Normal file
View File

@@ -0,0 +1,62 @@
BEGIN {
in_dl = 0
has_prev = 0
prev_line = ""
in_pre = 0
}
{
if ($0 ~ /^<pre>/) {
if (!in_pre) in_pre = 1
}
if (!in_pre && $0 ~ /^:[ \t]+[^ \t]/) {
if (!in_dl) {
in_dl = 1
print "<dl>"
print "<dt>" prev_line "</dt>"
has_prev = 0
} else {
if (has_prev && prev_line != "") {
print "<dt>" prev_line "</dt>"
has_prev = 0
}
}
def_text = $0
sub(/^:[ \t]+/, "", def_text)
print "<dd>" def_text "</dd>"
if ($0 ~ /<\/pre>/) {
if (in_pre) in_pre = 0
}
next
} else {
if (in_dl) {
if ($0 == "") {
# End of definition list
print "</dl>"
in_dl = 0
print ""
has_prev = 0
next
}
}
if (has_prev) {
print prev_line
}
prev_line = $0
has_prev = 1
}
if ($0 ~ /<\/pre>/) {
if (in_pre) in_pre = 0
}
}
END {
if (in_dl) {
print "</dl>"
} else {
if (has_prev) {
print prev_line
}
}
}

51
awk/emoji.awk Normal file
View File

@@ -0,0 +1,51 @@
BEGIN {
if (emoji_file == "") {
emoji_file = "emoji.tsv"
}
while ((getline line < emoji_file) > 0) {
split(line, parts, "\t")
if (length(parts[1]) > 0) {
map[parts[1]] = parts[2]
}
}
close(emoji_file)
}
{
if ($0 ~ /<pre>/) in_pre = 1
if (!in_pre) {
code_count = 0
line = $0
out = ""
while (match(line, /<code>[^<]*<\/code>/)) {
code_count++
code_store[code_count] = substr(line, RSTART, RLENGTH)
out = out substr(line, 1, RSTART - 1) "\034EC" code_count "\034"
line = substr(line, RSTART + RLENGTH)
}
out = out line
line = out
out = ""
while (match(line, /:[A-Za-z0-9_+\-]+:/)) {
token = substr(line, RSTART, RLENGTH)
out = out substr(line, 1, RSTART - 1)
if (token in map) {
out = out map[token]
} else {
out = out token
}
line = substr(line, RSTART + RLENGTH)
}
out = out line
for (i = 1; i <= code_count; i++) {
gsub("\034EC" i "\034", code_store[i], out)
delete code_store[i]
}
$0 = out
}
if ($0 ~ /<\/pre>/) in_pre = 0
print
}

827
awk/emoji.tsv Normal file
View File

@@ -0,0 +1,827 @@
:+1: 👍
:100: 💯
:1234: 🔢
:8ball: 🎱
:a: 🅰️
:ab: 🆎
:abc: 🔤
:abcd: 🔡
:accept: 🉑
:aerial_tramway: 🚡
:airplane: ✈️
:alarm_clock: ⏰
:alien: 👽
:ambulance: 🚑
:anchor: ⚓
:angel: 👼
:anger: 💢
:angry: 😠
:anguished: 😧
:ant: 🐜
:apple: 🍎
:aquarius: ♒
:aries: ♈
:arrow_backward: ◀️
:arrow_double_down: ⏬
:arrow_double_up: ⏫
:arrow_down: ⬇️
:arrow_down_small: 🔽
:arrow_forward: ▶️
:arrow_heading_down: ⤵️
:arrow_heading_up: ⤴️
:arrow_left: ⬅️
:arrow_lower_left: ↙️
:arrow_lower_right: ↘️
:arrow_right: ➡️
:arrow_right_hook: ↪️
:arrow_up: ⬆️
:arrow_up_down: ↕️
:arrow_up_small: 🔼
:arrow_upper_left: ↖️
:arrow_upper_right: ↗️
:arrows_clockwise: 🔃
:arrows_counterclockwise: 🔄
:art: 🎨
:articulated_lorry: 🚛
:astonished: 😲
:atm: 🏧
:b: 🅱️
:baby: 👶
:baby_bottle: 🍼
:baby_chick: 🐤
:baby_symbol: 🚼
:baggage_claim: 🛄
:balloon: 🎈
:ballot_box_with_check: ☑️
:bamboo: 🎍
:banana: 🍌
:bangbang: ‼️
:bank: 🏦
:bar_chart: 📊
:barber: 💈
:baseball: ⚾
:basketball: 🏀
:bath: 🛀
:bathtub: 🛁
:battery: 🔋
:bear: 🐻
:beer: 🍺
:beers: 🍻
:beetle: 🪲
:beginner: 🔰
:bell: 🔔
:bento: 🍱
:bicyclist: 🚴
:bike: 🚲
:bikini: 👙
:bird: 🐦
:birthday: 🎂
:black_circle: ⚫
:black_joker: 🃏
:black_nib: ✒️
:black_square_button: 🔲
:blossom: 🌼
:blowfish: 🐡
:blue_book: 📘
:blue_car: 🚙
:blue_heart: 💙
:blush: 😊
:boar: 🐗
:boat: ⛵
:bomb: 💣
:book: 📖
:bookmark: 🔖
:bookmark_tabs: 📑
:books: 📚
:boom: 💥
:boot: 👢
:bouquet: 💐
:bow: 🙇
:bowling: 🎳
:boy: 👦
:bread: 🍞
:bride_with_veil: 👰‍♀️
:bridge_at_night: 🌉
:briefcase: 💼
:broken_heart: 💔
:bug: 🐛
:bulb: 💡
:bullettrain_front: 🚅
:bullettrain_side: 🚄
:bus: 🚌
:busstop: 🚏
:bust_in_silhouette: 👤
:busts_in_silhouette: 👥
:cactus: 🌵
:cake: 🍰
:calendar: 📆
:calling: 📲
:camel: 🐫
:camera: 📷
:cancer: ♋
:candy: 🍬
:capital_abcd: 🔠
:capricorn: ♑
:car: 🚗
:card_index: 📇
:carousel_horse: 🎠
:cat2: 🐈
:cat: 🐱
:cd: 💿
:chart: 💹
:chart_with_downwards_trend: 📉
:chart_with_upwards_trend: 📈
:checkered_flag: 🏁
:cherries: 🍒
:cherry_blossom: 🌸
:chestnut: 🌰
:chicken: 🐔
:children_crossing: 🚸
:chocolate_bar: 🍫
:christmas_tree: 🎄
:church: ⛪
:cinema: 🎦
:circus_tent: 🎪
:city_sunrise: 🌇
:city_sunset: 🌆
:cl: 🆑
:clap: 👏
:clapper: 🎬
:clipboard: 📋
:clock1030: 🕥
:clock10: 🕙
:clock1130: 🕦
:clock11: 🕚
:clock1230: 🕧
:clock12: 🕛
:clock130: 🕜
:clock1: 🕐
:clock230: 🕝
:clock2: 🕑
:clock330: 🕞
:clock3: 🕒
:clock430: 🕟
:clock4: 🕓
:clock530: 🕠
:clock5: 🕔
:clock630: 🕡
:clock6: 🕕
:clock730: 🕢
:clock7: 🕖
:clock830: 🕣
:clock8: 🕗
:clock930: 🕤
:clock9: 🕘
:closed_book: 📕
:closed_lock_with_key: 🔐
:closed_umbrella: 🌂
:cloud: ☁️
:clubs: ♣️
:cn: 🇨🇳
:cocktail: 🍸
:coffee: ☕
:cold_sweat: 😰
:computer: 💻
:confetti_ball: 🎊
:confounded: 😖
:confused: 😕
:congratulations: ㊗️
:construction: 🚧
:construction_worker: 👷
:convenience_store: 🏪
:cookie: 🍪
:cool: 🆒
:cop: 👮
:copyright: ©️
:corn: 🌽
:couple: 👫
:couple_with_heart: 💑
:couplekiss: 💏
:cow2: 🐄
:cow: 🐮
:credit_card: 💳
:crocodile: 🐊
:crossed_flags: 🎌
:crown: 👑
:cry: 😢
:crying_cat_face: 😿
:crystal_ball: 🔮
:cupid: 💘
:curly_loop: ➰
:currency_exchange: 💱
:curry: 🍛
:custard: 🍮
:customs: 🛃
:cyclone: 🌀
:dancer: 💃
:dancers: 👯
:dango: 🍡
:dart: 🎯
:dash: 💨
:date: 📅
:de: 🇩🇪
:deciduous_tree: 🌳
:department_store: 🏬
:diamond_shape_with_a_dot_inside: 💠
:diamonds: ♦️
:disappointed: 😞
:disappointed_relieved: 😥
:dizzy: 💫
:dizzy_face: 😵
:do_not_litter: 🚯
:dog2: 🐕
:dog: 🐶
:dollar: 💵
:dolls: 🎎
:dolphin: 🐬
:door: 🚪
:doughnut: 🍩
:dragon: 🐉
:dragon_face: 🐲
:dress: 👗
:dromedary_camel: 🐪
:droplet: 💧
:dvd: 📀
:ear: 👂
:ear_of_rice: 🌾
:earth_africa: 🌍
:earth_americas: 🌎
:earth_asia: 🌏
:egg: 🥚
:eggplant: 🍆
:eight: 8
:eight_pointed_black_star: ✴️
:eight_spoked_asterisk: ✳️
:electric_plug: 🔌
:elephant: 🐘
:email: 📧
:end: 🔚
:envelope: ✉️
:es: 🇪🇸
:euro: 💶
:european_castle: 🏰
:european_post_office: 🏤
:evergreen_tree: 🌲
:exclamation: ❗
:expressionless: 😑
:eyeglasses: 👓
:eyes: 👀
:factory: 🏭
:fallen_leaf: 🍂
:family: 👪
:fast_forward: ⏩
:fax: 📠
:fearful: 😨
:feet: 🐾
:ferris_wheel: 🎡
:file_folder: 📁
:fire: 🔥
:fire_engine: 🚒
:fireworks: 🎆
:first_quarter_moon: 🌓
:first_quarter_moon_with_face: 🌛
:fish: 🐟
:fish_cake: 🍥
:fishing_pole_and_fish: 🎣
:fist: ✊
:five: 5
:flags: 🎏
:flashlight: 🔦
:floppy_disk: 💾
:flower_playing_cards: 🎴
:flushed: 😳
:foggy: 🌁
:football: 🏈
:fork_and_knife: 🍴
:fountain: ⛲
:four: 4
:four_leaf_clover: 🍀
:fr: 🇫🇷
:free: 🆓
:fried_shrimp: 🍤
:fries: 🍟
:frog: 🐸
:frowning: 😦
:fuelpump: ⛽
:full_moon: 🌕
:full_moon_with_face: 🌝
:game_die: 🎲
:gem: 💎
:gemini: ♊
:ghost: 👻
:gift: 🎁
:gift_heart: 💝
:girl: 👧
:globe_with_meridians: 🌐
:goat: 🐐
:golf: ⛳
:grapes: 🍇
:green_apple: 🍏
:green_book: 📗
:green_heart: 💚
:grey_exclamation: ❕
:grey_question: ❔
:grimacing: 😬
:grin: 😁
:grinning: 😀
:guardsman: 💂‍♂️
:guitar: 🎸
:gun: 🔫
:haircut: 💇
:hamburger: 🍔
:hammer: 🔨
:hamster: 🐹
:handbag: 👜
:hankey: 💩
:hash: #️⃣
:hatched_chick: 🐥
:hatching_chick: 🐣
:headphones: 🎧
:hear_no_evil: 🙉
:heart: ❤️
:heart_decoration: 💟
:heart_eyes: 😍
:heart_eyes_cat: 😻
:heartbeat: 💓
:hearts: ♥️
:heavy_check_mark: ✔️
:heavy_division_sign: ➗
:heavy_dollar_sign: 💲
:heavy_minus_sign:
:heavy_multiplication_x: ✖️
:heavy_plus_sign:
:helicopter: 🚁
:herb: 🌿
:hibiscus: 🌺
:high_brightness: 🔆
:high_heel: 👠
:honey_pot: 🍯
:horse: 🐴
:horse_racing: 🏇
:hospital: 🏥
:hotel: 🏨
:hotsprings: ♨️
:hourglass: ⌛
:hourglass_flowing_sand: ⏳
:house: 🏠
:house_with_garden: 🏡
:hushed: 😯
:ice_cream: 🍨
:icecream: 🍦
:id: 🆔
:ideograph_advantage: 🉐
:imp: 👿
:inbox_tray: 📥
:incoming_envelope: 📨
:information_desk_person: 💁
:information_source:
:innocent: 😇
:interrobang: ⁉️
:iphone: 📱
:it: 🇮🇹
:izakaya_lantern: 🏮
:jack_o_lantern: 🎃
:japan: 🗾
:japanese_castle: 🏯
:japanese_goblin: 👺
:japanese_ogre: 👹
:jeans: 👖
:joy: 😂
:joy_cat: 😹
:jp: 🇯🇵
:key: 🔑
:keycap_ten: 🔟
:kimono: 👘
:kiss: 💋
:kissing: 😗
:kissing_cat: 😽
:kissing_closed_eyes: 😚
:kissing_heart: 😘
:kissing_smiling_eyes: 😙
:koala: 🐨
:koko: 🈁
:kr: 🇰🇷
:large_blue_diamond: 🔷
:large_orange_diamond: 🔶
:last_quarter_moon: 🌗
:last_quarter_moon_with_face: 🌜
:laughing: 😆
:leaves: 🍃
:ledger: 📒
:left_luggage: 🛅
:left_right_arrow: ↔️
:leftwards_arrow_with_hook: ↩️
:lemon: 🍋
:leo: ♌
:leopard: 🐆
:libra: ♎
:light_rail: 🚈
:link: 🔗
:lips: 👄
:lipstick: 💄
:lock: 🔒
:lock_with_ink_pen: 🔏
:lollipop: 🍭
:loop: ➿
:loudspeaker: 📢
:love_hotel: 🏩
:love_letter: 💌
:low_brightness: 🔅
:m: Ⓜ️
:mag: 🔍
:mag_right: 🔎
:mahjong: 🀄
:mailbox: 📫
:mailbox_closed: 📪
:mailbox_with_mail: 📬
:mailbox_with_no_mail: 📭
:man: 👨
:man_with_gua_pi_mao: 👲
:man_with_turban: 👳‍♂️
:mans_shoe: 👞
:maple_leaf: 🍁
:mask: 😷
:massage: 💆
:meat_on_bone: 🍖
:mega: 📣
:melon: 🍈
:memo: 📝
:mens: 🚹
:metal: 🤘
:metro: 🚇
:microphone: 🎤
:microscope: 🔬
:milky_way: 🌌
:minibus: 🚐
:minidisc: 💽
:mobile_phone_off: 📴
:money_with_wings: 💸
:moneybag: 💰
:monkey: 🐒
:monkey_face: 🐵
:monorail: 🚝
:mortar_board: 🎓
:mount_fuji: 🗻
:mountain_bicyclist: 🚵
:mountain_cableway: 🚠
:mountain_railway: 🚞
:mouse2: 🐁
:mouse: 🐭
:movie_camera: 🎥
:moyai: 🗿
:muscle: 💪
:mushroom: 🍄
:musical_keyboard: 🎹
:musical_note: 🎵
:musical_score: 🎼
:mute: 🔇
:nail_care: 💅
:name_badge: 📛
:necktie: 👔
:negative_squared_cross_mark: ❎
:neutral_face: 😐
:new: 🆕
:new_moon: 🌑
:new_moon_with_face: 🌚
:newspaper: 📰
:ng: 🆖
:nine: 9
:no_bell: 🔕
:no_bicycles: 🚳
:no_entry: ⛔
:no_entry_sign: 🚫
:no_good: 🙅
:no_mobile_phones: 📵
:no_mouth: 😶
:no_pedestrians: 🚷
:no_smoking: 🚭
:nose: 👃
:notebook: 📓
:notebook_with_decorative_cover: 📔
:notes: 🎶
:nut_and_bolt: 🔩
:o2: 🅾️
:o: ⭕
:ocean: 🌊
:octopus: 🐙
:oden: 🍢
:office: 🏢
:ok: 🆗
:ok_hand: 👌
:ok_woman: 🙆‍♀️
:older_man: 👴
:older_woman: 👵
:on: 🔛
:oncoming_automobile: 🚘
:oncoming_bus: 🚍
:oncoming_police_car: 🚔
:oncoming_taxi: 🚖
:one: 1
:open_file_folder: 📂
:open_hands: 👐
:open_mouth: 😮
:ophiuchus: ⛎
:orange_book: 📙
:outbox_tray: 📤
:ox: 🐂
:page_facing_up: 📄
:page_with_curl: 📃
:pager: 📟
:palm_tree: 🌴
:panda_face: 🐼
:paperclip: 📎
:parking: 🅿️
:part_alternation_mark: 〽️
:partly_sunny: ⛅
:passport_control: 🛂
:paw_prints: 🐾
:peach: 🍑
:pear: 🍐
:pencil2: ✏️
:pencil: 📝
:penguin: 🐧
:pensive: 😔
:performing_arts: 🎭
:persevere: 😣
:pig2: 🐖
:pig: 🐷
:pig_nose: 🐽
:pill: 💊
:pineapple: 🍍
:pisces: ♓
:pizza: 🍕
:point_down: 👇
:point_left: 👈
:point_right: 👉
:point_up: ☝️
:point_up_2: 👆
:police_car: 🚓
:poodle: 🐩
:poop: 💩
:post_office: 🏣
:postal_horn: 📯
:postbox: 📮
:potable_water: 🚰
:pouch: 👝
:poultry_leg: 🍗
:pound: 💷
:pouting_cat: 😾
:pray: 🙏
:princess: 👸
:punch: 👊
:purple_heart: 💜
:purse: 👛
:pushpin: 📌
:put_litter_in_its_place: 🚮
:question: ❓
:rabbit2: 🐇
:rabbit: 🐰
:racehorse: 🐎
:radio: 📻
:radio_button: 🔘
:rage: 😡
:railway_car: 🚃
:rainbow: 🌈
:raised_hand: ✋
:raised_hands: 🙌
:raising_hand: 🙋
:ram: 🐏
:ramen: 🍜
:rat: 🐀
:recycle: ♻️
:red_car: 🚗
:red_circle: 🔴
:registered: ®️
:relaxed: ☺️
:relieved: 😌
:repeat: 🔁
:repeat_one: 🔂
:restroom: 🚻
:revolving_hearts: 💞
:rewind: ⏪
:ribbon: 🎀
:rice: 🍚
:rice_ball: 🍙
:rice_cracker: 🍘
:rice_scene: 🎑
:ring: 💍
:rocket: 🚀
:roller_coaster: 🎢
:rooster: 🐓
:rose: 🌹
:rotating_light: 🚨
:round_pushpin: 📍
:rowboat: 🚣
:ru: 🇷🇺
:rugby_football: 🏉
:runner: 🏃
:running_shirt_with_sash: 🎽
:sa: 🈂️
:sagittarius: ♐
:sailboat: ⛵
:sake: 🍶
:sandal: 👡
:santa: 🎅
:satellite: 📡
:satisfied: 😆
:saxophone: 🎷
:school: 🏫
:school_satchel: 🎒
:scissors: ✂️
:scorpius: ♏
:scream: 😱
:scream_cat: 🙀
:scroll: 📜
:seat: 💺
:secret: ㊙️
:see_no_evil: 🙈
:seedling: 🌱
:seven: 7
:shaved_ice: 🍧
:sheep: 🐑
:shell: 🐚
:ship: 🚢
:shirt: 👕
:shit: 💩
:shower: 🚿
:signal_strength: 📶
:six: 6
:six_pointed_star: 🔯
:ski: 🎿
:skull: 💀
:sleeping: 😴
:sleepy: 😪
:slot_machine: 🎰
:small_blue_diamond: 🔹
:small_orange_diamond: 🔸
:small_red_triangle: 🔺
:small_red_triangle_down: 🔻
:smile: 😄
:smile_cat: 😸
:smiley: 😃
:smiley_cat: 😺
:smiling_imp: 😈
:smirk: 😏
:smirk_cat: 😼
:smoking: 🚬
:snail: 🐌
:snake: 🐍
:snowboarder: 🏂
:snowflake: ❄️
:snowman: ⛄
:sob: 😭
:soccer: ⚽
:soon: 🔜
:sos: 🆘
:sound: 🔉
:space_invader: 👾
:spades: ♠️
:spaghetti: 🍝
:sparkler: 🎇
:sparkles: ✨
:sparkling_heart: 💖
:speak_no_evil: 🙊
:speaker: 🔈
:speech_balloon: 💬
:speedboat: 🚤
:star2: 🌟
:star: ⭐
:stars: 🌠
:station: 🚉
:statue_of_liberty: 🗽
:steam_locomotive: 🚂
:stew: 🍲
:straight_ruler: 📏
:strawberry: 🍓
:stuck_out_tongue: 😛
:stuck_out_tongue_closed_eyes: 😝
:stuck_out_tongue_winking_eye: 😜
:sun_with_face: 🌞
:sunflower: 🌻
:sunglasses: 😎
:sunny: ☀️
:sunrise: 🌅
:sunrise_over_mountains: 🌄
:surfer: 🏄
:sushi: 🍣
:suspension_railway: 🚟
:sweat: 😓
:sweat_drops: 💦
:sweat_smile: 😅
:sweet_potato: 🍠
:swimmer: 🏊
:symbols: 🔣
:syringe: 💉
:tada: 🎉
:tanabata_tree: 🎋
:tangerine: 🍊
:taurus: ♉
:taxi: 🚕
:tea: 🍵
:telephone: ☎️
:telephone_receiver: 📞
:telescope: 🔭
:tennis: 🎾
:tent: ⛺
:thought_balloon: 💭
:three: 3
:thumbsdown: 👎
:thumbsup: 👍
:ticket: 🎫
:tiger2: 🐅
:tiger: 🐯
:tired_face: 😫
:tm: ™️
:toilet: 🚽
:tokyo_tower: 🗼
:tomato: 🍅
:tongue: 👅
:top: 🔝
:tophat: 🎩
:tractor: 🚜
:traffic_light: 🚥
:train2: 🚆
:train: 🚋
:tram: 🚊
:triangular_flag_on_post: 🚩
:triangular_ruler: 📐
:trident: 🔱
:triumph: 😤
:trolleybus: 🚎
:trophy: 🏆
:tropical_drink: 🍹
:tropical_fish: 🐠
:truck: 🚚
:trumpet: 🎺
:tulip: 🌷
:turtle: 🐢
:tv: 📺
:twisted_rightwards_arrows: 🔀
:two: 2
:two_men_holding_hands: 👬
:two_women_holding_hands: 👭
:u5272: 🈹
:u5408: 🈴
:u55b6: 🈺
:u6307: 🈯
:u6708: 🈷️
:u6709: 🈶
:u6e80: 🈵
:u7121: 🈚
:u7533: 🈸
:u7981: 🈲
:u7a7a: 🈳
:umbrella: ☔
:unamused: 😒
:underage: 🔞
:unlock: 🔓
:up: 🆙
:us: 🇺🇸
:v: ✌️
:vertical_traffic_light: 🚦
:vhs: 📼
:vibration_mode: 📳
:video_camera: 📹
:video_game: 🎮
:violin: 🎻
:virgo: ♍
:volcano: 🌋
:vs: 🆚
:walking: 🚶
:waning_crescent_moon: 🌘
:waning_gibbous_moon: 🌖
:warning: ⚠️
:watch: ⌚
:water_buffalo: 🐃
:watermelon: 🍉
:wave: 👋
:wavy_dash: 〰️
:waxing_crescent_moon: 🌒
:waxing_gibbous_moon: 🌔
:wc: 🚾
:weary: 😩
:wedding: 💒
:whale2: 🐋
:whale: 🐳
:wheelchair: ♿
:white_check_mark: ✅
:white_circle: ⚪
:white_flower: 💮
:white_square_button: 🔳
:wind_chime: 🎐
:wine_glass: 🍷
:wink: 😉
:wolf: 🐺
:woman: 👩
:womans_clothes: 👚
:womans_hat: 👒
:womens: 🚺
:worried: 😟
:wrench: 🔧
:x: ❌
:yellow_heart: 💛
:yen: 💴
:yum: 😋
:zap: ⚡
:zero: 0
:zzz: 💤
1 :+1: 👍
2 :100: 💯
3 :1234: 🔢
4 :8ball: 🎱
5 :a: 🅰️
6 :ab: 🆎
7 :abc: 🔤
8 :abcd: 🔡
9 :accept: 🉑
10 :aerial_tramway: 🚡
11 :airplane: ✈️
12 :alarm_clock:
13 :alien: 👽
14 :ambulance: 🚑
15 :anchor:
16 :angel: 👼
17 :anger: 💢
18 :angry: 😠
19 :anguished: 😧
20 :ant: 🐜
21 :apple: 🍎
22 :aquarius:
23 :aries:
24 :arrow_backward: ◀️
25 :arrow_double_down:
26 :arrow_double_up:
27 :arrow_down: ⬇️
28 :arrow_down_small: 🔽
29 :arrow_forward: ▶️
30 :arrow_heading_down: ⤵️
31 :arrow_heading_up: ⤴️
32 :arrow_left: ⬅️
33 :arrow_lower_left: ↙️
34 :arrow_lower_right: ↘️
35 :arrow_right: ➡️
36 :arrow_right_hook: ↪️
37 :arrow_up: ⬆️
38 :arrow_up_down: ↕️
39 :arrow_up_small: 🔼
40 :arrow_upper_left: ↖️
41 :arrow_upper_right: ↗️
42 :arrows_clockwise: 🔃
43 :arrows_counterclockwise: 🔄
44 :art: 🎨
45 :articulated_lorry: 🚛
46 :astonished: 😲
47 :atm: 🏧
48 :b: 🅱️
49 :baby: 👶
50 :baby_bottle: 🍼
51 :baby_chick: 🐤
52 :baby_symbol: 🚼
53 :baggage_claim: 🛄
54 :balloon: 🎈
55 :ballot_box_with_check: ☑️
56 :bamboo: 🎍
57 :banana: 🍌
58 :bangbang: ‼️
59 :bank: 🏦
60 :bar_chart: 📊
61 :barber: 💈
62 :baseball:
63 :basketball: 🏀
64 :bath: 🛀
65 :bathtub: 🛁
66 :battery: 🔋
67 :bear: 🐻
68 :beer: 🍺
69 :beers: 🍻
70 :beetle: 🪲
71 :beginner: 🔰
72 :bell: 🔔
73 :bento: 🍱
74 :bicyclist: 🚴
75 :bike: 🚲
76 :bikini: 👙
77 :bird: 🐦
78 :birthday: 🎂
79 :black_circle:
80 :black_joker: 🃏
81 :black_nib: ✒️
82 :black_square_button: 🔲
83 :blossom: 🌼
84 :blowfish: 🐡
85 :blue_book: 📘
86 :blue_car: 🚙
87 :blue_heart: 💙
88 :blush: 😊
89 :boar: 🐗
90 :boat:
91 :bomb: 💣
92 :book: 📖
93 :bookmark: 🔖
94 :bookmark_tabs: 📑
95 :books: 📚
96 :boom: 💥
97 :boot: 👢
98 :bouquet: 💐
99 :bow: 🙇
100 :bowling: 🎳
101 :boy: 👦
102 :bread: 🍞
103 :bride_with_veil: 👰‍♀️
104 :bridge_at_night: 🌉
105 :briefcase: 💼
106 :broken_heart: 💔
107 :bug: 🐛
108 :bulb: 💡
109 :bullettrain_front: 🚅
110 :bullettrain_side: 🚄
111 :bus: 🚌
112 :busstop: 🚏
113 :bust_in_silhouette: 👤
114 :busts_in_silhouette: 👥
115 :cactus: 🌵
116 :cake: 🍰
117 :calendar: 📆
118 :calling: 📲
119 :camel: 🐫
120 :camera: 📷
121 :cancer:
122 :candy: 🍬
123 :capital_abcd: 🔠
124 :capricorn:
125 :car: 🚗
126 :card_index: 📇
127 :carousel_horse: 🎠
128 :cat2: 🐈
129 :cat: 🐱
130 :cd: 💿
131 :chart: 💹
132 :chart_with_downwards_trend: 📉
133 :chart_with_upwards_trend: 📈
134 :checkered_flag: 🏁
135 :cherries: 🍒
136 :cherry_blossom: 🌸
137 :chestnut: 🌰
138 :chicken: 🐔
139 :children_crossing: 🚸
140 :chocolate_bar: 🍫
141 :christmas_tree: 🎄
142 :church:
143 :cinema: 🎦
144 :circus_tent: 🎪
145 :city_sunrise: 🌇
146 :city_sunset: 🌆
147 :cl: 🆑
148 :clap: 👏
149 :clapper: 🎬
150 :clipboard: 📋
151 :clock1030: 🕥
152 :clock10: 🕙
153 :clock1130: 🕦
154 :clock11: 🕚
155 :clock1230: 🕧
156 :clock12: 🕛
157 :clock130: 🕜
158 :clock1: 🕐
159 :clock230: 🕝
160 :clock2: 🕑
161 :clock330: 🕞
162 :clock3: 🕒
163 :clock430: 🕟
164 :clock4: 🕓
165 :clock530: 🕠
166 :clock5: 🕔
167 :clock630: 🕡
168 :clock6: 🕕
169 :clock730: 🕢
170 :clock7: 🕖
171 :clock830: 🕣
172 :clock8: 🕗
173 :clock930: 🕤
174 :clock9: 🕘
175 :closed_book: 📕
176 :closed_lock_with_key: 🔐
177 :closed_umbrella: 🌂
178 :cloud: ☁️
179 :clubs: ♣️
180 :cn: 🇨🇳
181 :cocktail: 🍸
182 :coffee:
183 :cold_sweat: 😰
184 :computer: 💻
185 :confetti_ball: 🎊
186 :confounded: 😖
187 :confused: 😕
188 :congratulations: ㊗️
189 :construction: 🚧
190 :construction_worker: 👷
191 :convenience_store: 🏪
192 :cookie: 🍪
193 :cool: 🆒
194 :cop: 👮
195 :copyright: ©️
196 :corn: 🌽
197 :couple: 👫
198 :couple_with_heart: 💑
199 :couplekiss: 💏
200 :cow2: 🐄
201 :cow: 🐮
202 :credit_card: 💳
203 :crocodile: 🐊
204 :crossed_flags: 🎌
205 :crown: 👑
206 :cry: 😢
207 :crying_cat_face: 😿
208 :crystal_ball: 🔮
209 :cupid: 💘
210 :curly_loop:
211 :currency_exchange: 💱
212 :curry: 🍛
213 :custard: 🍮
214 :customs: 🛃
215 :cyclone: 🌀
216 :dancer: 💃
217 :dancers: 👯
218 :dango: 🍡
219 :dart: 🎯
220 :dash: 💨
221 :date: 📅
222 :de: 🇩🇪
223 :deciduous_tree: 🌳
224 :department_store: 🏬
225 :diamond_shape_with_a_dot_inside: 💠
226 :diamonds: ♦️
227 :disappointed: 😞
228 :disappointed_relieved: 😥
229 :dizzy: 💫
230 :dizzy_face: 😵
231 :do_not_litter: 🚯
232 :dog2: 🐕
233 :dog: 🐶
234 :dollar: 💵
235 :dolls: 🎎
236 :dolphin: 🐬
237 :door: 🚪
238 :doughnut: 🍩
239 :dragon: 🐉
240 :dragon_face: 🐲
241 :dress: 👗
242 :dromedary_camel: 🐪
243 :droplet: 💧
244 :dvd: 📀
245 :ear: 👂
246 :ear_of_rice: 🌾
247 :earth_africa: 🌍
248 :earth_americas: 🌎
249 :earth_asia: 🌏
250 :egg: 🥚
251 :eggplant: 🍆
252 :eight: 8️⃣
253 :eight_pointed_black_star: ✴️
254 :eight_spoked_asterisk: ✳️
255 :electric_plug: 🔌
256 :elephant: 🐘
257 :email: 📧
258 :end: 🔚
259 :envelope: ✉️
260 :es: 🇪🇸
261 :euro: 💶
262 :european_castle: 🏰
263 :european_post_office: 🏤
264 :evergreen_tree: 🌲
265 :exclamation:
266 :expressionless: 😑
267 :eyeglasses: 👓
268 :eyes: 👀
269 :factory: 🏭
270 :fallen_leaf: 🍂
271 :family: 👪
272 :fast_forward:
273 :fax: 📠
274 :fearful: 😨
275 :feet: 🐾
276 :ferris_wheel: 🎡
277 :file_folder: 📁
278 :fire: 🔥
279 :fire_engine: 🚒
280 :fireworks: 🎆
281 :first_quarter_moon: 🌓
282 :first_quarter_moon_with_face: 🌛
283 :fish: 🐟
284 :fish_cake: 🍥
285 :fishing_pole_and_fish: 🎣
286 :fist:
287 :five: 5️⃣
288 :flags: 🎏
289 :flashlight: 🔦
290 :floppy_disk: 💾
291 :flower_playing_cards: 🎴
292 :flushed: 😳
293 :foggy: 🌁
294 :football: 🏈
295 :fork_and_knife: 🍴
296 :fountain:
297 :four: 4️⃣
298 :four_leaf_clover: 🍀
299 :fr: 🇫🇷
300 :free: 🆓
301 :fried_shrimp: 🍤
302 :fries: 🍟
303 :frog: 🐸
304 :frowning: 😦
305 :fuelpump:
306 :full_moon: 🌕
307 :full_moon_with_face: 🌝
308 :game_die: 🎲
309 :gem: 💎
310 :gemini:
311 :ghost: 👻
312 :gift: 🎁
313 :gift_heart: 💝
314 :girl: 👧
315 :globe_with_meridians: 🌐
316 :goat: 🐐
317 :golf:
318 :grapes: 🍇
319 :green_apple: 🍏
320 :green_book: 📗
321 :green_heart: 💚
322 :grey_exclamation:
323 :grey_question:
324 :grimacing: 😬
325 :grin: 😁
326 :grinning: 😀
327 :guardsman: 💂‍♂️
328 :guitar: 🎸
329 :gun: 🔫
330 :haircut: 💇
331 :hamburger: 🍔
332 :hammer: 🔨
333 :hamster: 🐹
334 :handbag: 👜
335 :hankey: 💩
336 :hash: #️⃣
337 :hatched_chick: 🐥
338 :hatching_chick: 🐣
339 :headphones: 🎧
340 :hear_no_evil: 🙉
341 :heart: ❤️
342 :heart_decoration: 💟
343 :heart_eyes: 😍
344 :heart_eyes_cat: 😻
345 :heartbeat: 💓
346 :hearts: ♥️
347 :heavy_check_mark: ✔️
348 :heavy_division_sign:
349 :heavy_dollar_sign: 💲
350 :heavy_minus_sign:
351 :heavy_multiplication_x: ✖️
352 :heavy_plus_sign:
353 :helicopter: 🚁
354 :herb: 🌿
355 :hibiscus: 🌺
356 :high_brightness: 🔆
357 :high_heel: 👠
358 :honey_pot: 🍯
359 :horse: 🐴
360 :horse_racing: 🏇
361 :hospital: 🏥
362 :hotel: 🏨
363 :hotsprings: ♨️
364 :hourglass:
365 :hourglass_flowing_sand:
366 :house: 🏠
367 :house_with_garden: 🏡
368 :hushed: 😯
369 :ice_cream: 🍨
370 :icecream: 🍦
371 :id: 🆔
372 :ideograph_advantage: 🉐
373 :imp: 👿
374 :inbox_tray: 📥
375 :incoming_envelope: 📨
376 :information_desk_person: 💁
377 :information_source: ℹ️
378 :innocent: 😇
379 :interrobang: ⁉️
380 :iphone: 📱
381 :it: 🇮🇹
382 :izakaya_lantern: 🏮
383 :jack_o_lantern: 🎃
384 :japan: 🗾
385 :japanese_castle: 🏯
386 :japanese_goblin: 👺
387 :japanese_ogre: 👹
388 :jeans: 👖
389 :joy: 😂
390 :joy_cat: 😹
391 :jp: 🇯🇵
392 :key: 🔑
393 :keycap_ten: 🔟
394 :kimono: 👘
395 :kiss: 💋
396 :kissing: 😗
397 :kissing_cat: 😽
398 :kissing_closed_eyes: 😚
399 :kissing_heart: 😘
400 :kissing_smiling_eyes: 😙
401 :koala: 🐨
402 :koko: 🈁
403 :kr: 🇰🇷
404 :large_blue_diamond: 🔷
405 :large_orange_diamond: 🔶
406 :last_quarter_moon: 🌗
407 :last_quarter_moon_with_face: 🌜
408 :laughing: 😆
409 :leaves: 🍃
410 :ledger: 📒
411 :left_luggage: 🛅
412 :left_right_arrow: ↔️
413 :leftwards_arrow_with_hook: ↩️
414 :lemon: 🍋
415 :leo:
416 :leopard: 🐆
417 :libra:
418 :light_rail: 🚈
419 :link: 🔗
420 :lips: 👄
421 :lipstick: 💄
422 :lock: 🔒
423 :lock_with_ink_pen: 🔏
424 :lollipop: 🍭
425 :loop:
426 :loudspeaker: 📢
427 :love_hotel: 🏩
428 :love_letter: 💌
429 :low_brightness: 🔅
430 :m: Ⓜ️
431 :mag: 🔍
432 :mag_right: 🔎
433 :mahjong: 🀄
434 :mailbox: 📫
435 :mailbox_closed: 📪
436 :mailbox_with_mail: 📬
437 :mailbox_with_no_mail: 📭
438 :man: 👨
439 :man_with_gua_pi_mao: 👲
440 :man_with_turban: 👳‍♂️
441 :mans_shoe: 👞
442 :maple_leaf: 🍁
443 :mask: 😷
444 :massage: 💆
445 :meat_on_bone: 🍖
446 :mega: 📣
447 :melon: 🍈
448 :memo: 📝
449 :mens: 🚹
450 :metal: 🤘
451 :metro: 🚇
452 :microphone: 🎤
453 :microscope: 🔬
454 :milky_way: 🌌
455 :minibus: 🚐
456 :minidisc: 💽
457 :mobile_phone_off: 📴
458 :money_with_wings: 💸
459 :moneybag: 💰
460 :monkey: 🐒
461 :monkey_face: 🐵
462 :monorail: 🚝
463 :mortar_board: 🎓
464 :mount_fuji: 🗻
465 :mountain_bicyclist: 🚵
466 :mountain_cableway: 🚠
467 :mountain_railway: 🚞
468 :mouse2: 🐁
469 :mouse: 🐭
470 :movie_camera: 🎥
471 :moyai: 🗿
472 :muscle: 💪
473 :mushroom: 🍄
474 :musical_keyboard: 🎹
475 :musical_note: 🎵
476 :musical_score: 🎼
477 :mute: 🔇
478 :nail_care: 💅
479 :name_badge: 📛
480 :necktie: 👔
481 :negative_squared_cross_mark:
482 :neutral_face: 😐
483 :new: 🆕
484 :new_moon: 🌑
485 :new_moon_with_face: 🌚
486 :newspaper: 📰
487 :ng: 🆖
488 :nine: 9️⃣
489 :no_bell: 🔕
490 :no_bicycles: 🚳
491 :no_entry:
492 :no_entry_sign: 🚫
493 :no_good: 🙅
494 :no_mobile_phones: 📵
495 :no_mouth: 😶
496 :no_pedestrians: 🚷
497 :no_smoking: 🚭
498 :nose: 👃
499 :notebook: 📓
500 :notebook_with_decorative_cover: 📔
501 :notes: 🎶
502 :nut_and_bolt: 🔩
503 :o2: 🅾️
504 :o:
505 :ocean: 🌊
506 :octopus: 🐙
507 :oden: 🍢
508 :office: 🏢
509 :ok: 🆗
510 :ok_hand: 👌
511 :ok_woman: 🙆‍♀️
512 :older_man: 👴
513 :older_woman: 👵
514 :on: 🔛
515 :oncoming_automobile: 🚘
516 :oncoming_bus: 🚍
517 :oncoming_police_car: 🚔
518 :oncoming_taxi: 🚖
519 :one: 1️⃣
520 :open_file_folder: 📂
521 :open_hands: 👐
522 :open_mouth: 😮
523 :ophiuchus:
524 :orange_book: 📙
525 :outbox_tray: 📤
526 :ox: 🐂
527 :page_facing_up: 📄
528 :page_with_curl: 📃
529 :pager: 📟
530 :palm_tree: 🌴
531 :panda_face: 🐼
532 :paperclip: 📎
533 :parking: 🅿️
534 :part_alternation_mark: 〽️
535 :partly_sunny:
536 :passport_control: 🛂
537 :paw_prints: 🐾
538 :peach: 🍑
539 :pear: 🍐
540 :pencil2: ✏️
541 :pencil: 📝
542 :penguin: 🐧
543 :pensive: 😔
544 :performing_arts: 🎭
545 :persevere: 😣
546 :pig2: 🐖
547 :pig: 🐷
548 :pig_nose: 🐽
549 :pill: 💊
550 :pineapple: 🍍
551 :pisces:
552 :pizza: 🍕
553 :point_down: 👇
554 :point_left: 👈
555 :point_right: 👉
556 :point_up: ☝️
557 :point_up_2: 👆
558 :police_car: 🚓
559 :poodle: 🐩
560 :poop: 💩
561 :post_office: 🏣
562 :postal_horn: 📯
563 :postbox: 📮
564 :potable_water: 🚰
565 :pouch: 👝
566 :poultry_leg: 🍗
567 :pound: 💷
568 :pouting_cat: 😾
569 :pray: 🙏
570 :princess: 👸
571 :punch: 👊
572 :purple_heart: 💜
573 :purse: 👛
574 :pushpin: 📌
575 :put_litter_in_its_place: 🚮
576 :question:
577 :rabbit2: 🐇
578 :rabbit: 🐰
579 :racehorse: 🐎
580 :radio: 📻
581 :radio_button: 🔘
582 :rage: 😡
583 :railway_car: 🚃
584 :rainbow: 🌈
585 :raised_hand:
586 :raised_hands: 🙌
587 :raising_hand: 🙋
588 :ram: 🐏
589 :ramen: 🍜
590 :rat: 🐀
591 :recycle: ♻️
592 :red_car: 🚗
593 :red_circle: 🔴
594 :registered: ®️
595 :relaxed: ☺️
596 :relieved: 😌
597 :repeat: 🔁
598 :repeat_one: 🔂
599 :restroom: 🚻
600 :revolving_hearts: 💞
601 :rewind:
602 :ribbon: 🎀
603 :rice: 🍚
604 :rice_ball: 🍙
605 :rice_cracker: 🍘
606 :rice_scene: 🎑
607 :ring: 💍
608 :rocket: 🚀
609 :roller_coaster: 🎢
610 :rooster: 🐓
611 :rose: 🌹
612 :rotating_light: 🚨
613 :round_pushpin: 📍
614 :rowboat: 🚣
615 :ru: 🇷🇺
616 :rugby_football: 🏉
617 :runner: 🏃
618 :running_shirt_with_sash: 🎽
619 :sa: 🈂️
620 :sagittarius:
621 :sailboat:
622 :sake: 🍶
623 :sandal: 👡
624 :santa: 🎅
625 :satellite: 📡
626 :satisfied: 😆
627 :saxophone: 🎷
628 :school: 🏫
629 :school_satchel: 🎒
630 :scissors: ✂️
631 :scorpius:
632 :scream: 😱
633 :scream_cat: 🙀
634 :scroll: 📜
635 :seat: 💺
636 :secret: ㊙️
637 :see_no_evil: 🙈
638 :seedling: 🌱
639 :seven: 7️⃣
640 :shaved_ice: 🍧
641 :sheep: 🐑
642 :shell: 🐚
643 :ship: 🚢
644 :shirt: 👕
645 :shit: 💩
646 :shower: 🚿
647 :signal_strength: 📶
648 :six: 6️⃣
649 :six_pointed_star: 🔯
650 :ski: 🎿
651 :skull: 💀
652 :sleeping: 😴
653 :sleepy: 😪
654 :slot_machine: 🎰
655 :small_blue_diamond: 🔹
656 :small_orange_diamond: 🔸
657 :small_red_triangle: 🔺
658 :small_red_triangle_down: 🔻
659 :smile: 😄
660 :smile_cat: 😸
661 :smiley: 😃
662 :smiley_cat: 😺
663 :smiling_imp: 😈
664 :smirk: 😏
665 :smirk_cat: 😼
666 :smoking: 🚬
667 :snail: 🐌
668 :snake: 🐍
669 :snowboarder: 🏂
670 :snowflake: ❄️
671 :snowman:
672 :sob: 😭
673 :soccer:
674 :soon: 🔜
675 :sos: 🆘
676 :sound: 🔉
677 :space_invader: 👾
678 :spades: ♠️
679 :spaghetti: 🍝
680 :sparkler: 🎇
681 :sparkles:
682 :sparkling_heart: 💖
683 :speak_no_evil: 🙊
684 :speaker: 🔈
685 :speech_balloon: 💬
686 :speedboat: 🚤
687 :star2: 🌟
688 :star:
689 :stars: 🌠
690 :station: 🚉
691 :statue_of_liberty: 🗽
692 :steam_locomotive: 🚂
693 :stew: 🍲
694 :straight_ruler: 📏
695 :strawberry: 🍓
696 :stuck_out_tongue: 😛
697 :stuck_out_tongue_closed_eyes: 😝
698 :stuck_out_tongue_winking_eye: 😜
699 :sun_with_face: 🌞
700 :sunflower: 🌻
701 :sunglasses: 😎
702 :sunny: ☀️
703 :sunrise: 🌅
704 :sunrise_over_mountains: 🌄
705 :surfer: 🏄
706 :sushi: 🍣
707 :suspension_railway: 🚟
708 :sweat: 😓
709 :sweat_drops: 💦
710 :sweat_smile: 😅
711 :sweet_potato: 🍠
712 :swimmer: 🏊
713 :symbols: 🔣
714 :syringe: 💉
715 :tada: 🎉
716 :tanabata_tree: 🎋
717 :tangerine: 🍊
718 :taurus:
719 :taxi: 🚕
720 :tea: 🍵
721 :telephone: ☎️
722 :telephone_receiver: 📞
723 :telescope: 🔭
724 :tennis: 🎾
725 :tent:
726 :thought_balloon: 💭
727 :three: 3️⃣
728 :thumbsdown: 👎
729 :thumbsup: 👍
730 :ticket: 🎫
731 :tiger2: 🐅
732 :tiger: 🐯
733 :tired_face: 😫
734 :tm: ™️
735 :toilet: 🚽
736 :tokyo_tower: 🗼
737 :tomato: 🍅
738 :tongue: 👅
739 :top: 🔝
740 :tophat: 🎩
741 :tractor: 🚜
742 :traffic_light: 🚥
743 :train2: 🚆
744 :train: 🚋
745 :tram: 🚊
746 :triangular_flag_on_post: 🚩
747 :triangular_ruler: 📐
748 :trident: 🔱
749 :triumph: 😤
750 :trolleybus: 🚎
751 :trophy: 🏆
752 :tropical_drink: 🍹
753 :tropical_fish: 🐠
754 :truck: 🚚
755 :trumpet: 🎺
756 :tulip: 🌷
757 :turtle: 🐢
758 :tv: 📺
759 :twisted_rightwards_arrows: 🔀
760 :two: 2️⃣
761 :two_men_holding_hands: 👬
762 :two_women_holding_hands: 👭
763 :u5272: 🈹
764 :u5408: 🈴
765 :u55b6: 🈺
766 :u6307: 🈯
767 :u6708: 🈷️
768 :u6709: 🈶
769 :u6e80: 🈵
770 :u7121: 🈚
771 :u7533: 🈸
772 :u7981: 🈲
773 :u7a7a: 🈳
774 :umbrella:
775 :unamused: 😒
776 :underage: 🔞
777 :unlock: 🔓
778 :up: 🆙
779 :us: 🇺🇸
780 :v: ✌️
781 :vertical_traffic_light: 🚦
782 :vhs: 📼
783 :vibration_mode: 📳
784 :video_camera: 📹
785 :video_game: 🎮
786 :violin: 🎻
787 :virgo:
788 :volcano: 🌋
789 :vs: 🆚
790 :walking: 🚶
791 :waning_crescent_moon: 🌘
792 :waning_gibbous_moon: 🌖
793 :warning: ⚠️
794 :watch:
795 :water_buffalo: 🐃
796 :watermelon: 🍉
797 :wave: 👋
798 :wavy_dash: 〰️
799 :waxing_crescent_moon: 🌒
800 :waxing_gibbous_moon: 🌔
801 :wc: 🚾
802 :weary: 😩
803 :wedding: 💒
804 :whale2: 🐋
805 :whale: 🐳
806 :wheelchair:
807 :white_check_mark:
808 :white_circle:
809 :white_flower: 💮
810 :white_square_button: 🔳
811 :wind_chime: 🎐
812 :wine_glass: 🍷
813 :wink: 😉
814 :wolf: 🐺
815 :woman: 👩
816 :womans_clothes: 👚
817 :womans_hat: 👒
818 :womens: 🚺
819 :worried: 😟
820 :wrench: 🔧
821 :x:
822 :yellow_heart: 💛
823 :yen: 💴
824 :yum: 😋
825 :zap:
826 :zero: 0️⃣
827 :zzz: 💤

51
awk/footnotes.awk Normal file
View File

@@ -0,0 +1,51 @@
BEGIN { fn_count = 0 }
# Match [^id]: text
/^\[\^[a-zA-Z0-9_-]+\]:/ {
id_start = index($0, "[^") + 2
id_end = index($0, "]:")
id = substr($0, id_start, id_end - id_start)
text = substr($0, id_end + 2)
# Trim leading space
sub(/^[ \t]+/, "", text)
fn_ids[++fn_count] = id
fn_texts[id] = text
next
}
{
lines[++line_count] = $0
}
END {
for (i = 1; i <= line_count; i++) {
line = lines[i]
for (j = 1; j <= fn_count; j++) {
id = fn_ids[j]
marker = "[^" id "]"
repl = "<sup><a href=\"#fn:" id "\" id=\"fnref:" id "\">" id "</a></sup>"
while ((pos = index(line, marker)) > 0) {
line = substr(line, 1, pos - 1) repl substr(line, pos + length(marker))
}
}
print line
}
if (fn_count > 0) {
print "<hr />"
print "<section class=\"footnotes\">"
print "<ol>"
for (j = 1; j <= fn_count; j++) {
id = fn_ids[j]
text = fn_texts[id]
print "<li id=\"fn:" id "\">"
print "<p>" text " <a href=\"#fnref:" id "\" class=\"reversefootnote\">&#8617;</a></p>"
print "</li>"
}
print "</ol>"
print "</section>"
}
}

View File

@@ -3,6 +3,55 @@ function title_from_name(name) {
gsub(/-/, " ", name)
return name
}
function url_encode_path(path) {
gsub(/%/, "%25", path)
gsub(/ /, "%20", path)
gsub(/#/, "%23", path)
gsub(/\?/, "%3F", path)
gsub(/"/, "%22", path)
gsub(/'/, "%27", path)
return path
}
function get_title(path, default_title, full_path, line, title, in_fm) {
full_path = src "/" path
if (path !~ /\.md$/) {
full_path = full_path "/index.md"
}
title = ""
in_fm = 0
while ((getline line < full_path) > 0) {
if (line ~ /^---[[:space:]]*$/) {
if (in_fm == 0) {
in_fm = 1
continue
} else {
break
}
}
if (in_fm) {
if (line ~ /^[[:space:]]*title[[:space:]]*=/) {
sub(/^[[:space:]]*title[[:space:]]*=[[:space:]]*/, "", line)
if (line ~ /^".*"$/) {
title = substr(line, 2, length(line) - 2)
} else if (line ~ /^'.*'$/) {
title = substr(line, 2, length(line) - 2)
} else {
title = line
}
break
}
} else {
break
}
}
close(full_path)
if (title != "") return title
return default_title
}
function compare_paths(p1, p2, parts1, parts2, n1, n2, i, name1, name2, lname1, lname2, w1, w2) {
n1 = split(p1, parts1, "/")
@@ -132,7 +181,7 @@ END {
continue
}
printf "<li><a href=\"/%sindex.html\">%s</a><ul>\n", dir_path, title_from_name(parts[i])
printf "<li><a href=\"/%sindex.html\">%s</a><ul>\n", url_encode_path(dir_path), get_title(this_d, title_from_name(parts[i]))
opened_levels[++depth] = i
}
@@ -143,9 +192,9 @@ END {
is_single = (single_file_index == "true" && md_count[curr_dir] == 1 && !has_index[curr_dir])
if (parts[n] != "index.md" && !is_single) {
path = "/" rel
path = url_encode_path("/" rel)
gsub(/\.md$/, ".html", path)
printf "<li><a href=\"%s\">%s</a></li>\n", path, title_from_name(parts[n])
printf "<li><a href=\"%s\">%s</a></li>\n", path, get_title(rel, title_from_name(parts[n]))
}
prev_n = n

View File

@@ -13,6 +13,31 @@ function strip_markdown(s) {
gsub(/-+$/, "", s)
return s
}
function unique_id(raw_id, candidate) {
candidate = raw_id
if (candidate == "") candidate = "section"
if (!(candidate in seen_ids)) {
seen_ids[candidate] = 1
return candidate
}
seen_ids[candidate]++
return candidate "-" seen_ids[candidate]
}
function has_inline_link(line) {
return (index(line, "<a ") > 0 || index(line, "<a\t") > 0)
}
function print_heading(tag, line, id) {
id = unique_id(strip_markdown(line))
if (enable_header_links == "true") {
if (has_inline_link(line)) {
print "<" tag " id=\"" id "\">" line " <a href=\"#" id "\" class=\"header-anchor\" aria-label=\"Link to this section\">#</a></" tag ">"
} else {
print "<" tag " id=\"" id "\"><a href=\"#" id "\" class=\"header-link\">" line "<span class=\"header-anchor\" aria-hidden=\"true\">#</span></a></" tag ">"
}
} else {
print "<" tag " id=\"" id "\">" line "</" tag ">"
}
}
function print_header(line) {
tag = ""
if (line ~ /^# /) { tag = "h1"; sub(/^# /, "", line) }
@@ -23,12 +48,7 @@ function print_header(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 ">"
}
print_heading(tag, line)
} else {
print line
}
@@ -38,7 +58,7 @@ BEGIN {
in_pre = 0
}
{
if ($0 ~ /^<pre><code>/) {
if ($0 ~ /^<pre><code/) {
in_pre = 1
if (has_prev && prev != "") { print_header(prev); has_prev = 0 }
print
@@ -52,7 +72,7 @@ BEGIN {
if ($0 ~ /^=+$/) {
if (has_prev && prev != "" && prev !~ /^<[a-z]/) {
print "<h1 id=\"" strip_markdown(prev) "\">" prev "</h1>"
print_heading("h1", prev)
has_prev = 0
} else {
if (has_prev) print_header(prev)
@@ -61,7 +81,7 @@ BEGIN {
}
} else if ($0 ~ /^-+$/) {
if (has_prev && prev != "" && prev !~ /^<[a-z]/) {
print "<h2 id=\"" strip_markdown(prev) "\">" prev "</h2>"
print_heading("h2", prev)
has_prev = 0
} else {
if (has_prev) print_header(prev)

View File

@@ -97,6 +97,23 @@ function read_file(path, out, line, rc) {
return out
}
function read_file_or_render_md(path, ext, cmd, content, line, rc) {
content = ""
if (ext == "md") {
cmd = "sh \"" script_dir "/markdown.sh\" \"" path "\""
while ((cmd | getline line) > 0) {
content = content line "\n"
}
close(cmd)
} else {
while ((rc = getline line < path) > 0) {
content = content line "\n"
}
close(path)
}
return content
}
function escape_html(s, t) {
t = s
gsub(/&/, "\\&amp;", t)
@@ -189,7 +206,8 @@ function render_embed(src, alt, has_alt, force_inline, ext, local_path, conte
if (force_inline && !is_global_url(src)) {
local_path = resolve_local_path(src)
if (local_path != "") {
content = read_file(local_path)
ext = ext_of(src)
content = read_file_or_render_md(local_path, ext)
if (content ~ /\n$/) sub(/\n$/, "", content)
return content
}
@@ -217,7 +235,7 @@ function render_embed(src, alt, has_alt, force_inline, ext, local_path, conte
if (is_inline_text_ext(ext)) {
local_path = resolve_local_path(src)
if (local_path != "") {
content = read_file(local_path)
content = read_file_or_render_md(local_path, ext)
if (content ~ /\n$/) sub(/\n$/, "", content)
return content
}
@@ -238,7 +256,7 @@ function render_typed_embed(etype, src, alt, has_alt, local_path, content) {
if (!is_global_url(src)) {
local_path = resolve_local_path(src)
if (local_path != "") {
content = read_file(local_path)
content = read_file_or_render_md(local_path, ext_of(src))
if (content ~ /\n$/) sub(/\n$/, "", content)
return content
}
@@ -357,6 +375,13 @@ function rewrite_img_tags(line, out, rest, tag, src, alt, force_inline_tag, e
} else if (is_image_ext(ext_of(src)) && force_inline_tag == "") {
# Preserve hand-written <img> attributes (style/class/etc) for normal images.
repl = tag
} else if (force_inline_tag != "" && !is_global_url(src) && is_inline_text_ext(ext_of(src))) {
repl = render_code_include(src, 1)
if (repl != "") {
repl = "<pre><code>" repl "</code></pre>"
} else {
repl = render_embed(src, alt, (alt != ""), 1)
}
} else {
repl = render_embed(src, alt, (alt != ""), (force_inline_tag != ""))
}
@@ -381,7 +406,12 @@ function rewrite_double_bang_with_parens(line, out, rest, token, inside, src,
src = substr(inside, sep + 2)
sub(/\)$/, "", src)
repl = render_embed(src, alt, (alt != ""), 1)
repl = render_code_include(src, 1)
if (repl != "") {
repl = "<pre><code>" repl "</code></pre>"
} else {
repl = render_embed(src, alt, (alt != ""), 1)
}
out = out pre repl
rest = post
}
@@ -398,7 +428,12 @@ function rewrite_double_bang_bare(line, out, rest, token, src, pre, post, rep
src = token
sub(/^!!\[/, "", src)
sub(/\]$/, "", src)
repl = render_embed(src, "", 0, 1)
repl = render_code_include(src, 1)
if (repl != "") {
repl = "<pre><code>" repl "</code></pre>"
} else {
repl = render_embed(src, "", 0, 1)
}
out = out pre repl
rest = post
}
@@ -558,6 +593,75 @@ function restore_plain_markers(line) {
return line
}
function break_code_double_bang(line, out, rest, pstart, pend, code_content, token, pre, post, inside, sep, src, alt, repl) {
out = ""
rest = line
while (1) {
pstart = index(rest, "<code>")
if (pstart == 0) {
out = out rest
break
}
out = out substr(rest, 1, pstart - 1)
rest = substr(rest, pstart)
pend = index(substr(rest, 7), "</code>")
if (pend == 0) {
out = out rest
break
}
pend = pend + 6
code_content = substr(rest, 7, pend - 7)
rest = substr(rest, pend + 7)
if (match(code_content, /!!\[[^\]]*\]\([^)]*\)/)) {
token = substr(code_content, RSTART, RLENGTH)
pre = substr(code_content, 1, RSTART - 1)
post = substr(code_content, RSTART + RLENGTH)
inside = token
sub(/^!!\[/, "", inside)
sep = index(inside, "](")
alt = substr(inside, 1, sep - 1)
src = substr(inside, sep + 2)
sub(/\)$/, "", src)
repl = render_code_include(src, 1)
if (repl != "") {
repl = "<pre><code>" repl "</code></pre>"
} else {
repl = render_embed(src, alt, (alt != ""), 1)
}
if (repl == "") {
out = out "<code>" code_content "</code>"
} else {
if (pre != "") out = out "<code>" pre "</code>"
out = out repl
if (post != "") out = out "<code>" post "</code>"
}
} else if (match(code_content, /!!\[[^\]]+\]/)) {
token = substr(code_content, RSTART, RLENGTH)
pre = substr(code_content, 1, RSTART - 1)
post = substr(code_content, RSTART + RLENGTH)
src = token
sub(/^!!\[/, "", src)
sub(/\]$/, "", src)
repl = render_code_include(src, 1)
if (repl != "") {
repl = "<pre><code>" repl "</code></pre>"
} else {
repl = render_embed(src, "", 0, 1)
}
if (repl == "") {
out = out "<code>" code_content "</code>"
} else {
if (pre != "") out = out "<code>" pre "</code>"
out = out repl
if (post != "") out = out "<code>" post "</code>"
}
} else {
out = out "<code>" code_content "</code>"
}
}
return out
}
BEGIN {
input_dir = dirname_of(input_file)
in_pre_code = 0
@@ -583,6 +687,9 @@ BEGIN {
line = apply_td_vertical_align(line)
line = restore_plain_markers(line)
if (!(in_pre_code || start_pre)) {
line = break_code_double_bang(line)
}
print line
if (start_pre && !end_pre) {

View File

@@ -230,5 +230,7 @@ function restore_html_tags(s, i, val) {
}
}
gsub(/<a href="https?:\/\/[^"]*"/, "& rel=\"noopener noreferrer\"", line)
print line
}

View File

@@ -46,14 +46,9 @@ function mask(s, t) {
while (match(substr(line, p), /`+/)) {
pstart = p + RSTART - 1
plen = RLENGTH
if (plen >= 3) {
out = out substr(line, p, pstart - p + plen)
p = pstart + plen
continue
}
# Found 1 or 2 backticks at pstart
# Search for closing marker
# Found backtick sequence at pstart
# Search for closing marker of same length
marker = substr(line, pstart, plen)
tail = substr(line, pstart + plen)
mpos = index(tail, marker)
@@ -69,9 +64,20 @@ function mask(s, t) {
# Found match!
content = substr(tail, 1, mpos - 1)
out = out substr(line, p, pstart - p)
if (plen == 2 && substr(content, 1, 1) == " " && substr(content, length(content), 1) == " ") {
if (plen >= 2 && substr(content, 1, 1) == " " && substr(content, length(content), 1) == " ") {
content = substr(content, 2, length(content) - 2)
}
if (content ~ /!!\[/) {
_rb_test = content
gsub(/!!\[[^\]]*\]\([^)]*\)/, "", _rb_test)
gsub(/!!\[[^\]]+\]/, "", _rb_test)
gsub(/[[:space:]]+/, "", _rb_test)
if (_rb_test == "") {
out = out content
p = pstart + plen + mpos + plen - 1
continue
}
}
out = out "<code>" mask(content) "</code>"
p = pstart + plen + mpos + plen - 1
} else {

View File

@@ -22,7 +22,7 @@ function mask_plain(s, t) {
gsub(/\$/, "\034P8\034", t)
return t
}
BEGIN { in_plain = 0 }
BEGIN { in_plain = 0; in_script_style = 0 }
{
line = $0
out = ""
@@ -48,5 +48,41 @@ BEGIN { in_plain = 0 }
in_plain = 0
}
}
print out
tmp_line = out
out2 = ""
while (1) {
if (!in_script_style) {
pos_script = match(tolower(tmp_line), /<script([ >]|$)/)
script_start = RSTART; script_len = RLENGTH
pos_style = match(tolower(tmp_line), /<style([ >]|$)/)
style_start = RSTART; style_len = RLENGTH
if (pos_script == 0 && pos_style == 0) {
out2 = out2 tmp_line
break
}
if (pos_script > 0 && (pos_style == 0 || pos_script < pos_style)) {
out2 = out2 substr(tmp_line, 1, script_start + script_len - 1)
tmp_line = substr(tmp_line, script_start + script_len)
in_script_style = 1
end_tag = "</script>"
} else {
out2 = out2 substr(tmp_line, 1, style_start + style_len - 1)
tmp_line = substr(tmp_line, style_start + style_len)
in_script_style = 1
end_tag = "</style>"
}
} else {
pos_end = match(tolower(tmp_line), end_tag)
if (pos_end == 0) {
out2 = out2 mask_plain(tmp_line)
tmp_line = ""
break
}
out2 = out2 mask_plain(substr(tmp_line, 1, RSTART - 1)) substr(tmp_line, RSTART, RLENGTH)
tmp_line = substr(tmp_line, RSTART + RLENGTH)
in_script_style = 0
}
}
print out2
}

View File

@@ -4,7 +4,7 @@ BEGIN {
}
{
if ($0 ~ /^<pre>/) in_pre = 1
if ($0 ~ /<pre>/) in_pre = 1
if (in_pre) {
if (in_p) { print "</p>"; in_p = 0 }
@@ -13,7 +13,16 @@ BEGIN {
next
}
if ($0 ~ /^<\/?(div|table|p|[ou]l|h[1-6]|[bh]r|blockquote|li|hr)/) {
if ($0 ~ /^<\/?(div|table|p|[ou]l|h[1-6]|[bh]r|blockquote|li|hr|section|article|nav|aside|header|footer|dl|dt|dd|script|style|iframe|details|summary|figure|figcaption|audio|video|picture)/) {
if (in_p) {
print "</p>"
in_p = 0
}
print
next
}
if ($0 ~ /^[\t ]*!([a-zA-Z])?\[[^\]]*\](\([^)]*\))?[\t ]*$/ || $0 ~ /^[\t ]*!!?\[[^\]]*\](\([^)]*\))?[\t ]*$/) {
if (in_p) {
print "</p>"
in_p = 0

View File

@@ -50,7 +50,7 @@ END {
in_pre = 0
i = 1
while (i <= count) {
if (lines[i] ~ /^<pre><code>/) {
if (lines[i] ~ /^<pre><code/) {
in_pre = 1
print lines[i]
i++

119
awk/reference_links.awk Normal file
View File

@@ -0,0 +1,119 @@
{
lines[NR] = $0
total = NR
if (/^\[[^\]]+\]: */) {
line = $0
sub(/^\[/, "", line)
ref_id = line
sub(/\].*/, "", ref_id)
line = $0
sub(/^\[[^\]]+\]: */, "", line)
ref_url = line
sub(/[ \t].*/, "", ref_url)
ref_title = $0
sub(/^\[[^\]]+\]: *[^\t ]*[ \t]*/, "", ref_title)
sub(/^"/, "", ref_title)
sub(/"$/, "", ref_title)
gsub(/\|/, "!", ref_title)
refs[ref_id] = ref_url
if (ref_title != "") titles[ref_id] = ref_title
is_ref[NR] = 1
}
}
function resolve_image_ref(alt, id, url, title) {
url = refs[id]
title = (id in titles) ? titles[id] : ""
if (url == "") return "![" alt "][" id "]"
return "<img src=\"" url "\" title=\"" title "\" alt=\"" alt "\" />"
}
function resolve_link_ref(text, id, url, title) {
url = refs[id]
title = (id in titles) ? titles[id] : ""
if (url == "") return "[" text "][" id "]"
return "<a href=\"" url "\" title=\"" title "\">" text "</a>"
}
function process_refs(line, result, i, len, ch, j, k, depth, bracket_content, ref_id) {
result = ""
len = length(line)
i = 1
while (i <= len) {
ch = substr(line, i, 1)
if (ch == "!" && i < len && substr(line, i + 1, 1) == "[") {
bracket_content = ""
j = i + 2
while (j <= len && substr(line, j, 1) != "]") {
bracket_content = bracket_content substr(line, j, 1)
j++
}
if (j <= len && j < len && substr(line, j + 1, 1) == "[") {
k = j + 2
ref_id = ""
while (k <= len && substr(line, k, 1) != "]") {
ref_id = ref_id substr(line, k, 1)
k++
}
if (k <= len) {
if (ref_id == "") ref_id = bracket_content
if (ref_id in refs) {
result = result resolve_image_ref(bracket_content, ref_id)
i = k + 1
continue
}
}
}
result = result substr(line, i, 1)
i++
} else if (ch == "[") {
bracket_content = ""
j = i + 1
depth = 1
while (j <= len && depth > 0) {
if (substr(line, j, 1) == "[") depth++
if (substr(line, j, 1) == "]") {
depth--
if (depth == 0) break
}
if (depth > 0) bracket_content = bracket_content substr(line, j, 1)
j++
}
if (j <= len && j < len && substr(line, j + 1, 1) == "[") {
k = j + 2
ref_id = ""
while (k <= len && substr(line, k, 1) != "]") {
ref_id = ref_id substr(line, k, 1)
k++
}
if (k <= len) {
if (ref_id == "") ref_id = bracket_content
if (ref_id in refs) {
result = result resolve_link_ref(bracket_content, ref_id)
i = k + 1
continue
}
}
}
result = result substr(line, i, 1)
i++
} else {
result = result ch
i++
}
}
return result
}
END {
for (n = 1; n <= total; n++) {
if (is_ref[n]) continue
print process_refs(lines[n])
}
}

View File

@@ -16,6 +16,10 @@ BEGIN {
style_path = ENVIRON["AWK_STYLE_PATH"]
head_extra = ENVIRON["AWK_HEAD_EXTRA"]
header_brand = ENVIRON["AWK_HEADER_BRAND"]
header_search = ENVIRON["AWK_HEADER_SEARCH"]
lang = ENVIRON["AWK_LANG"]
version = ENVIRON["AWK_VERSION"]
content_warning = ENVIRON["AWK_CONTENT_WARNING"]
if (current_url != "") {
nav = replace_all(nav, "href=\"" current_url "\"", "href=\"" current_url "\" class=\"current-page\"")
}
@@ -24,11 +28,14 @@ BEGIN {
{
line = $0
line = replace_all(line, "{{TITLE}}", title)
line = replace_all(line, "{{LANG}}", lang)
line = replace_all(line, "{{NAV}}", nav)
line = replace_all(line, "{{FOOTER}}", footer)
line = replace_all(line, "{{CSS}}", style_path)
line = replace_all(line, "{{HEAD_EXTRA}}", head_extra)
line = replace_all(line, "{{HEADER_BRAND}}", header_brand)
line = replace_all(line, "{{HEADER_SEARCH}}", header_search)
line = replace_all(line, "{{VERSION}}", version)
pos = index(line, "{{CONTENT}}")
if (pos > 0) {

50
awk/toc.awk Normal file
View File

@@ -0,0 +1,50 @@
BEGIN {
toc_str = "<ol class=\"toc\">\n"
has_toc = 0
}
{
lines[++n] = $0
if ($0 ~ /<pre>/) in_pre = 1
if (!in_pre && $0 ~ /\{\{TOC\}\}/) {
has_toc = 1
toc_lines[n] = 1
}
if ($0 ~ /<\/pre>/) in_pre = 0
if (match($0, /<h[23][^>]*>/)) {
tag_len = RLENGTH
title_start = RSTART + tag_len
title_str = substr($0, title_start)
title_end = index(title_str, "</h")
if (title_end > 0) {
title = substr(title_str, 1, title_end - 1)
gsub(/<[^>]+>/, "", title)
# extract id
id_start = match($0, /id="[^"]*"/)
if (id_start > 0) {
id_str = substr($0, id_start + 4)
id_end = index(id_str, "\"")
id = substr(id_str, 1, id_end - 1)
# what tag? level
level = substr($0, match($0, /<h[23]/) + 2, 1)
if (level == "2") {
toc_str = toc_str "<li class=\"toc-h2\"><a href=\"#" id "\">" title "</a></li>\n"
} else if (level == "3") {
toc_str = toc_str "<li class=\"toc-h3\"><a href=\"#" id "\">" title "</a></li>\n"
}
}
}
}
}
END {
toc_str = toc_str "</ol>"
for (i = 1; i <= n; i++) {
if (has_toc && toc_lines[i] && lines[i] ~ /^[[:space:]]*\{\{TOC\}\}[[:space:]]*$/) {
toc_lines[i] = 0 # Mark as processed if we want, but not strictly needed
sub(/\{\{TOC\}\}/, toc_str, lines[i])
}
print lines[i]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

49
button.svg Normal file
View File

@@ -0,0 +1,49 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="88"
height="31"
viewBox="0 0 88 31"
>
<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="88" height="31" fill="#000" />
<rect x="1" y="1" width="86" height="29" fill="url(#bg-grad)" />
<text
x="5"
y="12"
font-family="Georgia, 'Times New Roman', Times, serif"
font-size="10"
font-weight="bold"
fill="#debfff"
style="letter-spacing: -0.5px;"
>made</text>
<text
x="5"
y="22"
font-family="Georgia, 'Times New Roman', Times, serif"
font-size="10"
font-weight="bold"
fill="#debfff"
style="letter-spacing: -0.5px;"
>with</text>
<text
x="80"
y="16"
text-anchor="end"
dominant-baseline="central"
font-family="Georgia, 'Times New Roman', Times, serif"
font-size="28"
font-weight="bold"
font-style="italic"
fill="#debfff"
letter-spacing="-2"
>kewt</text>
<rect x="1" y="1" width="86" height="1" fill="#ffffff" fill-opacity="0.2" />
<rect x="1" y="2" width="1" height="27" fill="#ffffff" fill-opacity="0.2" />
<rect x="1" y="29" width="86" height="1" fill="#000000" fill-opacity="0.4" />
<rect x="86" y="2" width="1" height="28" fill="#000000" fill-opacity="0.4" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1131
kewt.sh

File diff suppressed because it is too large Load Diff

662
lib/builder.sh Normal file
View File

@@ -0,0 +1,662 @@
#!/bin/sh
# shellcheck disable=SC2129
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
[ -f "$script_dir/styles/$style.root.css" ] && [ "$script_dir/styles/$style.root.css" -nt "$out_file" ] && return 0
[ -f "$src/styles.root.css" ] && [ "$src/styles.root.css" -nt "$out_file" ] && return 0
return 1
}
write_content_warning_outputs() {
_source_file="$1"
_content_out_file="$2"
_content_rel_url="$3"
_target_url="$4"
_landing_out_file="$5"
_is_home="$6"
is_cw_content_page="true"
render_markdown "$_source_file" "$_is_home" "$_target_url" > "$_content_out_file"
is_cw_content_page="false"
generate_content_warning_page "$fm_title" "$fm_content_warning" "$_content_rel_url" "$_target_url" "$_landing_out_file" "false"
}
build_dir_entries_list() {
_bde_dir="$1"
_bde_rel_dir="$2"
_bde_entries_file="$3"
find "$_bde_dir" ! -name "$(basename "$_bde_dir")" -prune ! -name ".*" -print | while read -r entry; do
name="${entry##*/}"
case "$name" in
template.html|site.conf|style.css|styles.root.css|index.md) continue ;;
esac
if [ -d "$entry" ]; then
entry_rel_dir="${entry#"$src"/}"
manifest_dir_hidden_by_draft_index "$entry_rel_dir" && continue
dir_url="$(encode_url_path "$name")/index.html"
echo "${name}|- [${name}/](${dir_url})" >> "$_bde_entries_file"
elif [ "${entry%.md}" != "$entry" ]; then
entry_rel_path="${entry#"$src"/}"
load_manifest_entry "$entry_rel_path" || continue
if [ "${draft_mode:-false}" != "true" ]; then
[ "$manifest_draft" = "true" ] && continue
fi
label="${name%.md}"
post_h="$manifest_title"
is_post_entry="false"
if is_posts_directory_rel "$_bde_rel_dir"; then
is_post_entry="true"
fi
if [ -n "$post_h" ]; then
if [ "$is_post_entry" = "true" ]; then
if [ -n "$manifest_post_time" ]; then
label="$post_h - $manifest_post_date $manifest_post_time"
else
label="$post_h - $manifest_post_date"
fi
else
label="$post_h"
fi
elif [ "$is_post_entry" = "true" ]; then
if [ -n "$manifest_post_time" ]; then
label="$manifest_post_date $manifest_post_time"
else
label="$manifest_post_date"
fi
fi
if [ "$is_post_entry" = "true" ]; then
sort_key="${manifest_post_date} ${manifest_post_time}"
else
sort_key="$name"
fi
entry_url=$(encode_url_path "${name%.md}.html")
echo "${sort_key}|- [$label](${entry_url})|$name|${entry_url}" >> "$_bde_entries_file"
else
asset_url=$(encode_url_path "$name")
echo "${name}|- [$name]($asset_url)|$name|$asset_url" >> "$_bde_entries_file"
fi
done
}
build_dir_index() {
_bdi_dir="$1"
_bdi_rel_dir="$2"
_bdi_out_dir="$3"
has_custom_index="false"
has_list="false"
if [ -f "$_bdi_dir/index.md" ]; then
has_custom_index="true"
if grep -q '^[[:space:]]*{{LIST}}[[:space:]]*$' "$_bdi_dir/index.md" 2>/dev/null; then
has_list="true"
fi
fi
if [ "$has_custom_index" = "false" ] || [ "$has_list" = "true" ]; then
is_posts_dir="false"
if is_posts_directory_rel "$_bdi_rel_dir"; then
is_posts_dir="true"
fi
if [ "$single_file_index" = "true" ] && [ "$is_posts_dir" = "false" ] && [ "$has_list" = "false" ]; then
if load_manifest_dir_entry "$_bdi_rel_dir" && [ "$dir_md_count" -eq 1 ]; then
md_file="$src/$dir_first_md"
is_home="false"; [ "$_bdi_dir" = "$src" ] && is_home="true"
target_url=$(directory_index_url "$_bdi_rel_dir")
if needs_rebuild "$md_file" "$_bdi_out_dir/index.html"; then
parse_frontmatter "$md_file"
if [ -n "$fm_content_warning" ]; then
content_out_file="$_bdi_out_dir/content.html"
if [ "$_bdi_rel_dir" = "." ]; then
content_rel_url="/content.html"
else
content_rel_url="/$(encode_url_path "$_bdi_rel_dir")/content.html"
fi
write_content_warning_outputs "$md_file" "$content_out_file" "$content_rel_url" "$target_url" "$_bdi_out_dir/index.html" "$is_home"
else
render_markdown "$md_file" "$is_home" "$target_url" > "$_bdi_out_dir/index.html"
fi
fi
return 0
fi
fi
temp_index="$KEWT_TMPDIR/index.md"
temp_list="$KEWT_TMPDIR/list.md"
: > "$temp_list"
if [ "$has_custom_index" = "false" ]; then
display_dir="${_bdi_rel_dir#.}"
[ -z "$display_dir" ] && display_dir="/"
echo "# Index of $display_dir" > "$temp_index"
echo "" >> "$temp_index"
fi
sort_args=""
if is_posts_directory_rel "$_bdi_rel_dir"; then
sort_args="-r"
fi
temp_entries="$KEWT_TMPDIR/entries_$$.txt"
: > "$temp_entries"
build_dir_entries_list "$_bdi_dir" "$_bdi_rel_dir" "$temp_entries"
if [ "$is_posts_dir" = "true" ]; then
LC_ALL=C sort $sort_args "$temp_entries" > "$KEWT_TMPDIR/sorted_entries_$$.txt"
cut -d'|' -f2 "$KEWT_TMPDIR/sorted_entries_$$.txt" >> "$temp_list"
mkdir -p "$KEWT_TMPDIR/prevnext"
awk -F'|' '
{
name[NR] = $3
url[NR] = $4
}
END {
for(i=1; i<=NR; i++) {
prev_str = ""
next_str = ""
if(i > 1) {
next_str = "[Next >](" url[i-1] ")"
}
if(i < NR) {
prev_str = "[< Previous](" url[i+1] ")"
}
if (prev_str != "" || next_str != "") {
out = "'"$KEWT_TMPDIR"'/prevnext/" name[i]
printf "%s|%s\n", prev_str, next_str > out
}
}
}
' "$KEWT_TMPDIR/sorted_entries_$$.txt"
rm -f "$KEWT_TMPDIR/sorted_entries_$$.txt"
else
LC_ALL=C sort $sort_args "$temp_entries" | cut -d'|' -f2 >> "$temp_list"
fi
rm -f "$temp_entries"
is_home="false"; [ "$_bdi_dir" = "$src" ] && is_home="true"
target_url=$(directory_index_url "$_bdi_rel_dir")
render_paginated_index "$_bdi_dir" "$_bdi_rel_dir" "$_bdi_out_dir" "$temp_index" "$temp_list" "$target_url" "$is_home" "$has_custom_index" "$is_posts_dir"
rm -f "$temp_index" "$temp_list"
fi
}
render_paginated_index() {
_rpi_dir="$1"
_rpi_rel_dir="$2"
_rpi_out_dir="$3"
_rpi_temp_index="$4"
_rpi_temp_list="$5"
_rpi_target_url="$6"
_rpi_is_home="$7"
_rpi_has_custom_index="$8"
_rpi_is_posts_dir="$9"
num_items=$(wc -l < "$_rpi_temp_list")
if [ "$_rpi_is_posts_dir" = "true" ] && [ -n "$posts_per_page" ] && [ "$posts_per_page" -gt 0 ] && [ "$num_items" -gt "$posts_per_page" ]; then
num_pages=$(( (num_items + posts_per_page - 1) / posts_per_page ))
p=1
while [ "$p" -le "$num_pages" ]; do
chunk_list="$KEWT_TMPDIR/chunk.md"
start_line=$(( (p - 1) * posts_per_page + 1 ))
tail -n +$start_line "$_rpi_temp_list" | head -n "$posts_per_page" > "$chunk_list"
base_url_dir="$(dirname "$_rpi_target_url")"
[ "$base_url_dir" = "/" ] && base_url_dir=""
nav_html="<div class=\"pagination\">"
if [ "$p" -gt 1 ]; then
if [ "$p" -eq 2 ]; then
nav_html="$nav_html <a href=\"$base_url_dir/index.html\" class=\"prev-page\">&laquo; Prev</a> "
else
nav_html="$nav_html <a href=\"$base_url_dir/page/$((p-1))/index.html\" class=\"prev-page\">&laquo; Prev</a> "
fi
fi
nav_html="$nav_html <span class=\"page-number\">Page $p of $num_pages</span> "
if [ "$p" -lt "$num_pages" ]; then
nav_html="$nav_html <a href=\"$base_url_dir/page/$((p+1))/index.html\" class=\"next-page\">Next &raquo;</a> "
fi
nav_html="$nav_html</div>"
echo "" >> "$chunk_list"
echo "$nav_html" >> "$chunk_list"
temp_index_p="$KEWT_TMPDIR/index_p$p.md"
if [ "$_rpi_has_custom_index" = "false" ]; then
display_dir="${_rpi_rel_dir#.}"
[ -z "$display_dir" ] && display_dir="/"
echo "# Index of $display_dir" > "$temp_index_p"
echo "" >> "$temp_index_p"
else
: > "$temp_index_p"
fi
if [ "$_rpi_has_custom_index" = "true" ]; then
awk '
/^[[:space:]]*\{\{LIST\}\}[[:space:]]*$/ {
while((getline line < "'"$chunk_list"'") > 0) print line
close("'"$chunk_list"'")
next
}
{ print }
' "$_rpi_dir/index.md" >> "$temp_index_p"
else
cat "$chunk_list" >> "$temp_index_p"
fi
if [ "$p" -eq 1 ]; then
out_file="$_rpi_out_dir/index.html"
target_url_p="$_rpi_target_url"
else
out_file="$_rpi_out_dir/page/$p/index.html"
target_url_p="$base_url_dir/page/$p/index.html"
mkdir -p "$(dirname "$out_file")"
fi
render_markdown "$temp_index_p" "$_rpi_is_home" "$target_url_p" > "$out_file"
rm -f "$temp_index_p" "$chunk_list"
p=$((p + 1))
done
else
if [ "$_rpi_has_custom_index" = "true" ]; then
awk '
/^[[:space:]]*\{\{LIST\}\}[[:space:]]*$/ {
while((getline line < "'"$_rpi_temp_list"'") > 0) print line
close("'"$_rpi_temp_list"'")
next
}
{ print }
' "$_rpi_dir/index.md" > "$_rpi_temp_index"
else
cat "$_rpi_temp_list" >> "$_rpi_temp_index"
fi
do_rebuild="false"
needs_rebuild "$_rpi_dir" "$_rpi_out_dir/index.html" && do_rebuild="true"
[ "$_rpi_has_custom_index" = "true" ] && needs_rebuild "$_rpi_dir/index.md" "$_rpi_out_dir/index.html" && do_rebuild="true"
if [ "$do_rebuild" = "false" ] && [ -f "$_rpi_out_dir/index.html" ]; then
for _child in "$_rpi_dir"/*; do
[ -e "$_child" ] || continue
if [ "$_child" -nt "$_rpi_out_dir/index.html" ]; then
do_rebuild="true"
break
fi
done
fi
if [ "$do_rebuild" = "true" ]; then
if [ "$_rpi_has_custom_index" = "true" ]; then
parse_frontmatter "$_rpi_dir/index.md"
else
fm_content_warning=""
fi
if [ -n "$fm_content_warning" ]; then
content_out_file="$_rpi_out_dir/content.html"
if [ "$_rpi_rel_dir" = "." ]; then
content_rel_url="/content.html"
else
content_rel_url="/$(encode_url_path "$_rpi_rel_dir")/content.html"
fi
write_content_warning_outputs "$_rpi_temp_index" "$content_out_file" "$content_rel_url" "$_rpi_target_url" "$_rpi_out_dir/index.html" "$_rpi_is_home"
else
render_markdown "$_rpi_temp_index" "$_rpi_is_home" "$_rpi_target_url" > "$_rpi_out_dir/index.html"
fi
fi
fi
}
build_directories() {
eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while read -r dir; do
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
build_dir_index "$dir" "$rel_dir" "$out_dir"
done
}
build_files() {
eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type f -print" | sort | while IFS= read -r file; do
rel_path="${file#"$src"}"
rel_path="${rel_path#/}"
dir_rel=$(dirname "$rel_path")
out_dir="$out/$dir_rel"
case "${file##*/}" in
template.html|site.conf|style.css|styles.css|styles.root.css) continue ;;
esac
if [ "${file##*/}" = "index.md" ] && grep -q '^[[:space:]]*{{LIST}}[[:space:]]*$' "$file" 2>/dev/null; then
continue
fi
is_preserved=0
if [ -n "$(eval "find \"$file\" \( $PRESERVE_ARGS \) -print")" ]; then
is_preserved=1
fi
is_posts_dir_2="false"
if is_posts_directory_rel "$dir_rel"; 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
load_manifest_dir_entry "$dir_rel" && [ "$dir_md_count" -eq 1 ] && continue
fi
if [ "${file%.md}" != "$file" ] && [ "$is_preserved" -eq 0 ]; then
load_manifest_entry "$rel_path" || continue
if [ "${draft_mode:-false}" != "true" ]; then
[ "$manifest_draft" = "true" ] && continue
fi
is_home="false"; [ "$file" = "$src/index.md" ] && is_home="true"
out_file="$out/${rel_path%.md}.html"
if needs_rebuild "$file" "$out_file"; then
fm_title="$manifest_title"
fm_content_warning="$manifest_content_warning"
if [ -n "$manifest_content_warning" ]; then
content_out_file="$out/${rel_path%.md}-content.html"
content_rel_url="/$(encode_url_path "${rel_path%.md}")-content.html"
orig_rel_url="$manifest_url"
write_content_warning_outputs "$file" "$content_out_file" "$content_rel_url" "$orig_rel_url" "$out_file" "$is_home"
else
render_markdown "$file" "$is_home" > "$out_file"
fi
fi
else
if needs_rebuild "$file" "$out/$rel_path"; then
cp "$file" "$out/$rel_path"
fi
fi
done
}
build_root_style() {
if [ ! -f "$src/styles.css" ] && [ ! -f "$src/style.css" ]; then
if [ -f "$src/styles.root.css" ]; then
_base_css="$script_dir/styles/$style.css"
[ ! -f "$_base_css" ] && _base_css="$script_dir/styles/kewt.css"
if [ ! -f "$out/styles.css" ] || [ "$src/styles.root.css" -nt "$out/styles.css" ] || [ "$_base_css" -nt "$out/styles.css" ]; then
merge_root_style "$src/styles.root.css" "$_base_css" "$out/styles.css"
fi
elif [ -f "$script_dir/styles/$style.css" ]; then
if 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
elif [ -f "$script_dir/styles/$style.root.css" ]; then
_base_css="$script_dir/styles/kewt.css"
if [ ! -f "$out/styles.css" ] || [ "$script_dir/styles/$style.root.css" -nt "$out/styles.css" ] || [ "$_base_css" -nt "$out/styles.css" ]; then
merge_root_style "$script_dir/styles/$style.root.css" "$_base_css" "$out/styles.css"
fi
fi
fi
}
build_sitemap() {
[ -n "$base_url" ] || return
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"}"
[ "${rel_url#/}" = "$error_page" ] && continue
printf ' <url>\n <loc>%s%s</loc>\n <lastmod>%s</lastmod>\n </url>\n' "$base_url" "$rel_url" "$today" >> "$sitemap_file"
done
printf '</urlset>\n' >> "$sitemap_file"
}
build_feed() {
[ "$generate_feed" = "true" ] && [ -n "$base_url" ] || return
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 <channel>\n <title>%s</title>\n <link>%s</link>\n <description>%s</description>\n <lastBuildDate>%s</lastBuildDate>\n' \
"$title" "$base_url_feed" "$title" "$build_date" >> "$feed_path"
temp_feed_files="$KEWT_TMPDIR/feed_files_$$.txt"
: > "$temp_feed_files"
while IFS= read -r manifest_rel_path; do
case "$manifest_rel_path" in
*"${posts_dir:-__no_posts__}"*) ;;
*) continue ;;
esac
load_manifest_entry "$manifest_rel_path" || continue
[ "$manifest_draft" = "true" ] && continue
printf '%s %s|%s\n' "$manifest_post_date" "$manifest_post_time" "$manifest_rel_path" >> "$temp_feed_files"
done < "$manifest_all_list"
LC_ALL=C sort -r "$temp_feed_files" | cut -d'|' -f2- | while IFS= read -r post_rel_path; do
load_manifest_entry "$post_rel_path" || continue
[ "$manifest_draft" = "true" ] && continue
post_date="$manifest_post_date"
post_time="$manifest_post_time"
post_heading="$manifest_title"
post_slug="$manifest_post_slug"
if [ -z "$post_heading" ] && [ -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')
fi
feed_post_title="$post_heading - $post_date $post_time"
post_url="$base_url_feed$manifest_url"
pub_date=$(format_rfc2822_utc "$post_date" "$post_time")
if [ "$feed_full_content" = "true" ]; then
feed_content_file="$src/$post_rel_path"
feed_content_html=$(ENABLE_HEADER_LINKS="false" CUSTOM_ADMONITIONS="" MARKDOWN_SITE_ROOT="$src" MARKDOWN_FALLBACK_FILE="$script_dir/styles/$style.css" sh "$script_dir/markdown.sh" "$feed_content_file" | sed 's/</\&lt;/g; s/>/\&gt;/g')
printf ' <item>\n <title>%s</title>\n <link>%s</link>\n <guid>%s</guid>\n <pubDate>%s</pubDate>\n <description>%s</description>\n </item>\n' \
"$feed_post_title" "$post_url" "$post_url" "$pub_date" "$feed_content_html" >> "$feed_path"
else
printf ' <item>\n <title>%s</title>\n <link>%s</link>\n <guid>%s</guid>\n <pubDate>%s</pubDate>\n </item>\n' \
"$feed_post_title" "$post_url" "$post_url" "$pub_date" >> "$feed_path"
fi
done
printf ' </channel>\n</rss>\n' >> "$feed_path"
}
build_search_index() {
printf '[\n' > "$out/search.json"
first_search_item="true"
while IFS= read -r rel_path; do
load_manifest_entry "$rel_path" || continue
if [ "$manifest_is_index" = "true" ]; then
if [ "$rel_path" = "index.md" ]; then
md_url="/index.html"
else
md_url=$(directory_index_url "${rel_path%/index.md}")
fi
else
md_url="$manifest_url"
if [ "$single_file_index" = "true" ]; then
rel_dir_of_file="$manifest_dir_rel"
[ -z "$rel_dir_of_file" ] && rel_dir_of_file="."
if [ "$rel_dir_of_file" = "." ]; then
dir_of_file="$src"
else
dir_of_file="$src/$rel_dir_of_file"
fi
is_posts_dir_search="false"
if [ -n "$posts_dir" ] && { [ "$rel_dir_of_file" = "$posts_dir" ] || [ "./$rel_dir_of_file" = "$posts_dir" ]; }; then
is_posts_dir_search="true"
fi
if [ "$is_posts_dir_search" = "false" ] && [ ! -f "$dir_of_file/index.md" ]; then
if load_manifest_dir_entry "$rel_dir_of_file" && [ "$dir_md_count" -eq 1 ]; then
if [ "$rel_dir_of_file" = "." ]; then
md_url="/index.html"
else
md_url=$(directory_index_url "$rel_dir_of_file")
fi
fi
fi
fi
fi
[ "$manifest_draft" = "true" ] && continue
md_heading="$manifest_title"
if [ -z "$manifest_content_warning" ] || [ "$include_cw_pages_in_search" = "true" ]; then
md_content="$manifest_search_content"
if [ "$first_search_item" = "false" ]; then
printf ',\n' >> "$out/search.json"
fi
printf ' {"url": "%s", "title": "%s", "content": "%s"}' "$md_url" "$md_heading" "$md_content" >> "$out/search.json"
first_search_item="false"
fi
done < "$manifest_visible_list"
printf '\n]\n' >> "$out/search.json"
cp "$script_dir/lib/search.js" "$out/search.js"
search_md="$KEWT_TMPDIR/search_$$.md"
printf '%s\n' '# Search' '' \
'<form class="kewt-search-page" action="/search.html" method="get">' \
' <input type="text" id="search-box" name="q" placeholder="Search..." required>' \
' <button type="submit">Search</button>' \
'</form>' '' \
'<div id="search-results-list">' \
' <p>Loading...</p>' \
'</div>' '' \
'<script src="/search.js"></script>' > "$search_md"
render_markdown "$search_md" "false" "/search.html" > "$out/search.html"
rm -f "$search_md"
}
build_tags() {
temp_tags="$KEWT_TMPDIR/tags_$$.txt"
: > "$temp_tags"
while IFS= read -r rel_path; do
load_manifest_entry "$rel_path" || continue
[ "$manifest_draft" = "true" ] && continue
md_heading="$manifest_title"
if [ -n "$manifest_tags" ]; then
old_ifs=$IFS
IFS=','
for tag in $manifest_tags; do
tag=$(echo "$tag" | sed 's/^[ \t]*//;s/[ \t]*$//')
[ -z "$tag" ] && continue
printf '%s|%s|%s\n' "$tag" "$manifest_url" "$md_heading" >> "$temp_tags"
done
IFS=$old_ifs
fi
done < "$manifest_visible_list"
tags_out_dir="$out/$tags_dir"
mkdir -p "$tags_out_dir"
tags_index_md="$KEWT_TMPDIR/tags_index_$$.md"
echo "# Tags" > "$tags_index_md"
echo "" >> "$tags_index_md"
cut -d'|' -f1 "$temp_tags" | sort -u | while IFS= read -r tag; do
tag_slug=$(echo "$tag" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g')
echo "- [$tag](/$(echo "$tags_dir" | sed 's|^\/||; s|\/$||')/$tag_slug.html)" >> "$tags_index_md"
tag_page_md="$KEWT_TMPDIR/tag_page_$$.md"
echo "# Tag: $tag" > "$tag_page_md"
echo "" >> "$tag_page_md"
echo "Posts tagged with **$tag**:" >> "$tag_page_md"
echo "" >> "$tag_page_md"
grep "^${tag}|" "$temp_tags" | while IFS='|' read -r _t t_url t_title; do
echo "- [$t_title]($t_url)" >> "$tag_page_md"
done
render_markdown "$tag_page_md" "false" "/$tags_dir/$tag_slug.html" > "$tags_out_dir/$tag_slug.html"
rm -f "$tag_page_md"
done
render_markdown "$tags_index_md" "false" "/$tags_dir/index.html" > "$tags_out_dir/index.html"
rm -f "$tags_index_md" "$temp_tags"
}
build_error_page() {
[ -n "$error_page" ] || return
error_base="${error_page%.html}"
error_md="$src/${error_base}.md"
if [ -f "$error_md" ]; then
if needs_rebuild "$error_md" "$out/$error_page"; then
is_home="false"
current_url="/$error_page"
parse_frontmatter "$error_md"
render_markdown "$error_md" "$is_home" "/$error_page" > "$out/$error_page"
fi
elif [ ! -f "$out/$error_page" ]; then
temp_404="$KEWT_TMPDIR/404_gen.md"
printf '# 404 - Not Found\n\nThe requested page could not be found.\n' > "$temp_404"
render_markdown "$temp_404" "false" "/$error_page" > "$out/$error_page"
rm -f "$temp_404"
fi
}
build_site() {
echo "Building site from '$src' to '$out'..."
build_markdown_manifest
build_full_nav
build_directories
build_root_style
build_files
build_error_page
build_sitemap
build_feed
if [ "$generate_search" = "true" ]; then
build_search_index
fi
if [ "$generate_tags" = "true" ]; then
build_tags
fi
echo "Build complete."
}

144
lib/commands.sh Normal file
View File

@@ -0,0 +1,144 @@
#!/bin/sh
# shellcheck disable=SC2153
usage() {
invoked_as=$(basename "${KEWT_INVOKED_AS:-$0}")
cat <<EOF
Usage: $invoked_as [--from <src>] [--to <out>]
$invoked_as [src] [out]
$invoked_as --new [title]
$invoked_as --update [dir]
$invoked_as --post
$invoked_as --generate-template
$invoked_as --version
$invoked_as --help
Options:
--help Show this help message.
--new, --init [title] Create a new site directory (default: site)
--clean Clean the output directory before building (default).
--no-clean Do not clean the output directory before building.
--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
--generate-template [path] Generate a new template file at <path> (default: template.html)
--version Show version information.
--draft, -d Include draft pages in the build.
--dry-run Show what would be built without writing any files.
--from <src> Source directory (default: site)
--to <out> Output directory (default: out)
--watch, -w Watch for file changes and rebuild automatically.
--serve, -s [port] Start a local HTTP server after building (default port: 8000).
EOF
}
generate_template() {
_gt_path="$1"
[ -e "$_gt_path" ] && die "File '$_gt_path' already exists."
_gt_dir=$(dirname "$_gt_path")
[ -d "$_gt_dir" ] || mkdir -p "$_gt_dir"
printf '%s\n' "$DEFAULT_TMPL" > "$_gt_path"
echo "Generated template at '$_gt_path'."
exit 0
}
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"
printf '%s\n' "$DEFAULT_CONF" > "$new_dir/site.conf"
printf '%s\n' "$DEFAULT_TMPL" > "$new_dir/template.html"
printf "# _kewt_ website\n" > "$new_dir/index.md"
if [ -n "$new_title" ]; then
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
}
create_new_post() {
post_src_dir="$1"
post_user_title="$2"
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
post_date_val="$(date "+%Y-%m-%d %H:%M")"
if [ -n "$post_user_title" ]; then
printf -- '---\ntitle = "%s"\ndate = "%s"\ndraft = %s\n---\n# %s\n' "$post_user_title" "$post_date_val" "$draft_by_default" "$post_user_title" > "$file_path"
else
printf -- '---\ndate = "%s"\ndraft = %s\n---\n' "$post_date_val" "$draft_by_default" > "$file_path"
fi
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"
default_conf="$KEWT_TMPDIR/default_site.conf"
printf '%s\n' "$DEFAULT_CONF" > "$default_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
if [ -f "$target_tmpl" ]; then
default_tmpl="$KEWT_TMPDIR/default_template.html"
printf '%s\n' "$DEFAULT_TMPL" > "$default_tmpl"
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
}

152
lib/config.sh Normal file
View File

@@ -0,0 +1,152 @@
#!/bin/sh
DEFAULT_CONF='title = "kewt"
style = "kewt"
lang = "en"
draft_by_default = false
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 = ""
posts_per_page = 12
custom_admonitions = ""
cw_hide_url = true
generate_tags = false
tags_dir = "tags"
generate_search = false
search_in_footer = false
search_in_header = false
include_cw_pages_in_search = false
feed_full_content = false'
DEFAULT_TMPL='<!doctype html>
<html lang="{{LANG}}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{TITLE}}</title>
<link rel="stylesheet" href="{{CSS}}{{VERSION}}" type="text/css" />
{{HEAD_EXTRA}}
</head>
<body>
<input type="checkbox" id="nav-toggle" class="nav-toggle" aria-hidden="true" />
<header>
<h1>{{HEADER_BRAND}}</h1>
{{HEADER_SEARCH}}
<label for="nav-toggle" class="nav-toggle-label" aria-hidden="true">&#9776;</label>
</header>
<nav id="side-bar">{{NAV}}</nav>
<article>{{CONTENT}}</article>
<footer>{{FOOTER}}</footer>
</body>
</html>'
_parse_conf_val() {
_pv_val="$1"
case "$_pv_val" in
\"*\")
_pv_val=${_pv_val#\"}
_pv_val=${_pv_val%\"}
printf '%s' "$_pv_val" | sed 's/\\"/\"/g; s/\\\\/\\/g'
;;
\'*\')
_pv_val=${_pv_val#\'}
_pv_val=${_pv_val%\'}
printf '%s' "$_pv_val" | sed "s/\\\\'/'/g; s/\\\\/\\/g"
;;
*)
printf '%s' "$_pv_val"
;;
esac
}
_load_conf_line() {
case "$1" in
''|'#'*) return ;;
*=*) ;;
*) return ;;
esac
_lc_key=${1%%=*}
_lc_val=${1#*=}
_lc_key=$(printf '%s' "$_lc_key" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
_lc_val=$(_parse_conf_val "$(printf '%s' "$_lc_val" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')")
case "$_lc_key" in
title) title="$_lc_val" ;;
style) style="${_lc_val#/}" ;;
dir_indexes) dir_indexes="$_lc_val" ;;
single_file_index) single_file_index="$_lc_val" ;;
flatten) flatten="$_lc_val" ;;
order) order="$_lc_val" ;;
home_name) home_name="$_lc_val" ;;
show_home_in_nav) show_home_in_nav="$_lc_val" ;;
nav_links) nav_links="$_lc_val" ;;
nav_extra) nav_extra="$_lc_val" ;;
footer) footer="$_lc_val" ;;
logo) logo="${_lc_val#/}" ;;
display_logo) display_logo="$_lc_val" ;;
display_title) display_title="$_lc_val" ;;
logo_as_favicon) logo_as_favicon="$_lc_val" ;;
favicon) favicon="${_lc_val#/}" ;;
generate_page_title) generate_page_title="$_lc_val" ;;
error_page) error_page="${_lc_val#/}" ;;
versioning) versioning="$_lc_val" ;;
enable_header_links) enable_header_links="$_lc_val" ;;
base_url) base_url="$_lc_val" ;;
generate_feed) generate_feed="$_lc_val" ;;
feed_file) feed_file="${_lc_val#/}" ;;
posts_dir) posts_dir="${_lc_val#/}" ;;
posts_per_page) posts_per_page="$_lc_val" ;;
custom_admonitions) custom_admonitions="$_lc_val" ;;
cw_hide_url) cw_hide_url="$_lc_val" ;;
lang) lang="$_lc_val" ;;
draft_by_default) draft_by_default="$_lc_val" ;;
generate_tags) generate_tags="$_lc_val" ;;
tags_dir) tags_dir="${_lc_val#/}" ;;
generate_search) generate_search="$_lc_val" ;;
search_in_footer) search_in_footer="$_lc_val" ;;
search_in_header) search_in_header="$_lc_val" ;;
include_cw_pages_in_search) include_cw_pages_in_search="$_lc_val" ;;
feed_full_content) feed_full_content="$_lc_val" ;;
esac
}
reset_config() {
while IFS= read -r _rc_line; do
_load_conf_line "$_rc_line"
done <<EOF
$DEFAULT_CONF
EOF
}
load_config() {
[ -f "$1" ] || return
while IFS= read -r _lc_line || [ -n "$_lc_line" ]; do
_load_conf_line "$_lc_line"
done < "$1"
}
reset_config

352
lib/generator.sh Normal file
View File

@@ -0,0 +1,352 @@
#!/bin/sh
# shellcheck disable=SC2086
SEARCH_FORM_FOOTER='<form class="kewt-search-footer" action="/search.html" method="get"><input type="text" name="q" placeholder="Search..." required><button type="submit">Go</button></form>'
SEARCH_FORM_HEADER='<form class="kewt-search-header" action="/search.html" method="get"><input type="text" name="q" placeholder="Search..." required><button type="submit">Go</button></form>'
SEARCH_FORM_NAV='<div class="kewt-search-nav"><form action="/search.html" method="get"><input type="text" name="q" placeholder="Search..." required><button type="submit">Go</button></form></div>'
generate_nav() {
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")
nav_input="$KEWT_TMPDIR/nav_input.lst"
: > "$nav_input"
if [ -f "$manifest_visible_list" ]; then
while IFS= read -r nav_rel_path; do
printf '%s/%s\n' "$1" "$nav_rel_path" >> "$nav_input"
done < "$manifest_visible_list"
if [ -n "$posts_dir" ] && [ -d "$1/$posts_dir" ] && ! manifest_dir_hidden_by_draft_index "$posts_dir"; then
has_posts_nav_entry="false"
has_posts_index_entry="false"
while IFS= read -r nav_rel_path; do
case "$nav_rel_path" in
"$posts_dir"/index.md) has_posts_index_entry="true" ;;
"$posts_dir"/*) has_posts_nav_entry="true" ;;
esac
done < "$manifest_visible_list"
if [ "$has_posts_nav_entry" = "true" ] && [ "$has_posts_index_entry" = "false" ]; then
printf '%s/%s/index.md\n' "$1" "$posts_dir" >> "$nav_input"
fi
fi
else
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 > "$nav_input"
fi
sort -u "$nav_input" | 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"
}
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=','
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>'
}
find_closest() {
target="$1"
start_dir="$2"
curr="$start_dir"
while [ "$curr" != "$src" ] && [ "$curr" != "." ] && [ "$curr" != "/" ]; do
if [ -f "$curr/$target" ]; then
echo "$curr/$target"
return
fi
curr=$(dirname "$curr")
done
if [ -f "$src/$target" ]; then
echo "$src/$target"
fi
}
copy_style_with_resolved_vars() {
src_style="$1"
out_style="$2"
awk -f "$awk_dir/replace_variables.awk" "$src_style" > "$out_style"
}
merge_root_style() {
root_file="$1"
base_css="$2"
out_file="$3"
{
cat "$root_file"
awk '
BEGIN { in_root = 0; brace_depth = 0 }
/^:root[[:space:]]*\{/ { in_root = 1; brace_depth = 1; next }
in_root {
for (i = 1; i <= length($0); i++) {
c = substr($0, i, 1)
if (c == "{") brace_depth++
if (c == "}") { brace_depth--; if (brace_depth == 0) { in_root = 0; next } }
}
next
}
{ print }
' "$base_css"
} | awk -f "$awk_dir/replace_variables.awk" > "$out_file"
}
resolve_render_template() {
local_template=$(find_closest "template.html" "$(dirname "$1")")
[ -z "$local_template" ] && local_template="$template"
}
resolve_render_style_path() {
closest_style_src=$(find_closest "styles.css" "$(dirname "$1")")
[ -z "$closest_style_src" ] && closest_style_src=$(find_closest "style.css" "$(dirname "$1")")
if [ -n "$closest_style_src" ]; then
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" ;;
esac
style_path="/$(encode_url_path "${style_rel_to_src%styles.css}")"
style_path="${style_path%style.css}styles.css"
else
style_path="/styles.css"
fi
}
build_header_brand_html() {
logo_html=""
if [ "$display_logo" = "true" ] && [ -n "$logo" ]; then
logo_html="<img class=\"site-logo\" src=\"$logo\" alt=\"$title\" />"
fi
brand_text=""
if [ "$display_title" = "true" ]; then
brand_text="$title"
fi
if [ -n "$logo_html" ] && [ -n "$brand_text" ]; then
header_brand="<a href=\"/index.html\">$logo_html $brand_text</a>"
elif [ -n "$logo_html" ]; then
header_brand="<a href=\"/index.html\">$logo_html</a>"
elif [ -n "$brand_text" ]; then
header_brand="<a href=\"/index.html\">$brand_text</a>"
else
header_brand="<a href=\"/index.html\">$title</a>"
fi
}
build_favicon_head() {
favicon_src=""
if [ "$logo_as_favicon" = "true" ] && [ -n "$logo" ]; then
favicon_src="$logo"
elif [ -n "$favicon" ]; then
favicon_src="$favicon"
fi
if [ -n "$favicon_src" ]; then
case "$favicon_src" in
http*|/*) head_extra="<link rel=\"icon\" href=\"$favicon_src\" />" ;;
*) head_extra="<link rel=\"icon\" href=\"/$favicon_src\" />" ;;
esac
else
head_extra=""
fi
}
build_page_title() {
page_title="$title"
if [ -n "$fm_title" ]; then
page_title="$fm_title - $title"
elif [ "$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=$(first_heading_from_markdown "$file")
if [ -n "$first_heading" ]; then
first_heading=$(strip_markdown_text "$first_heading")
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
}
build_og_tags() {
head_extra_og="<meta property=\"og:title\" content=\"$(escape_html_attr "$page_title")\" />"
if [ -n "$fm_description" ]; then
head_extra_og="$head_extra_og
<meta property=\"og:description\" content=\"$(escape_html_attr "$fm_description")\" />"
fi
og_url="${base_url%/}${current_url}"
head_extra_og="$head_extra_og
<meta property=\"og:url\" content=\"$(escape_html_attr "$og_url")\" />"
if [ -n "$head_extra" ]; then
head_extra="$head_extra
$head_extra_og"
else
head_extra="$head_extra_og"
fi
}
build_cw_url_hide() {
if [ "$is_cw_content_page" = "true" ] && [ "$cw_hide_url" = "true" ]; then
head_extra="$head_extra
<script>window.history.replaceState(null, '', '$current_url');</script>"
fi
}
build_composed_footer() {
final_footer="$footer"
if [ "$search_in_footer" = "true" ]; then
final_footer="$footer $SEARCH_FORM_FOOTER"
fi
}
build_composed_nav() {
final_nav="$nav"
final_header_brand="$header_brand"
final_header_search=""
if [ "$search_in_header" = "true" ]; then
final_header_search="$SEARCH_FORM_HEADER"
final_nav="$SEARCH_FORM_NAV
$nav"
fi
}
prepare_post_content() {
content_file="$file"
if [ -n "$posts_dir" ] && [ "$file" != "$src/$posts_dir/index.md" ]; then
rel_dir_of_url=$(dirname "$current_url")
rel_dir_of_url="${rel_dir_of_url#/}"
if { [ "$rel_dir_of_url" = "$posts_dir" ] || [ "./$rel_dir_of_url" = "$posts_dir" ]; } && [ "$(basename "$current_url")" != "index.html" ]; then
temp_post_with_backlink="$KEWT_TMPDIR/post_with_backlink_$$.md"
printf "[< Back](index.html)\n\n" > "$temp_post_with_backlink"
awk -f "$awk_dir/frontmatter.awk" "$file" >> "$temp_post_with_backlink"
post_md_name="$(basename "$current_url" .html).md"
prevnext_file="$KEWT_TMPDIR/prevnext/$post_md_name"
if [ -f "$prevnext_file" ]; then
IFS='|' read -r prev_str next_str < "$prevnext_file"
printf "\n\n---\n<div class=\"post-nav\">\n" >> "$temp_post_with_backlink"
if [ -n "$prev_str" ]; then
printf "<span class=\"prev-post\">%s</span>\n" "$prev_str" >> "$temp_post_with_backlink"
fi
if [ -n "$next_str" ]; then
printf "<span class=\"next-post\">%s</span>\n" "$next_str" >> "$temp_post_with_backlink"
fi
printf "</div>\n" >> "$temp_post_with_backlink"
fi
content_file="$temp_post_with_backlink"
fi
fi
}
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=$(markdown_file_url "$rel_path")
fi
prepare_post_content
resolve_render_template "$file"
resolve_render_style_path "$file"
build_header_brand_html
build_favicon_head
parse_frontmatter "$file"
build_page_title
build_og_tags
build_cw_url_hide
build_composed_footer
build_composed_nav
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_LANG="$lang" AWK_CURRENT_URL="$current_url" AWK_TITLE="$page_title" AWK_NAV="$final_nav" AWK_FOOTER="$final_footer" AWK_STYLE_PATH="${style_path}" AWK_HEADER_BRAND="$final_header_brand" AWK_HEADER_SEARCH="$final_header_search" AWK_HEAD_EXTRA="$head_extra" AWK_VERSION="$asset_version" AWK_CONTENT_WARNING="$fm_content_warning" awk -f "$awk_dir/render_template.awk" "$local_template"
}
generate_content_warning_page() {
_fm_title="$1"
_fm_content_warning="$2"
_content_rel_url="$3"
_target_url="$4"
_out_file="$5"
_is_home="$6"
_temp_cw="$KEWT_TMPDIR/cw_$$.md"
_cw_text="${_fm_content_warning}"
[ "$_cw_text" = "true" ] && _cw_text="This content may be sensitive."
cat <<EOF > "$_temp_cw"
---
title = "$_fm_title"
---
> [!CAUTION]
> **Content Warning:** $_cw_text
<a href="$(basename "$_content_rel_url")" class="cw-button">Reveal Content</a>
EOF
render_markdown "$_temp_cw" "$_is_home" "$_target_url" > "$_out_file"
rm -f "$_temp_cw"
}

196
lib/manifest.sh Normal file
View File

@@ -0,0 +1,196 @@
#!/bin/sh
# shellcheck disable=SC2016,SC2030,SC2031
shell_quote() {
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
}
manifest_meta_path() {
printf '%s/manifest/meta/%s.meta\n' "$KEWT_TMPDIR" "$1"
}
manifest_dir_meta_path() {
printf '%s/manifest/dir-meta/%s.meta\n' "$KEWT_TMPDIR" "$1"
}
manifest_dir_hidden_by_draft_index() {
_manifest_hidden_dir="${1:-.}"
[ -f "$manifest_hidden_dirs_list" ] || return 1
while :; do
awk -v dir="$_manifest_hidden_dir" '$0 == dir { found = 1 } END { exit(found ? 0 : 1) }' "$manifest_hidden_dirs_list" >/dev/null 2>&1 && return 0
[ "$_manifest_hidden_dir" = "." ] && return 1
_manifest_hidden_parent=$(dirname "$_manifest_hidden_dir")
[ "$_manifest_hidden_parent" = "$_manifest_hidden_dir" ] && return 1
_manifest_hidden_dir="$_manifest_hidden_parent"
done
}
write_manifest_dir_meta() {
_dir_meta_rel="$1"
_dir_meta_count="$2"
_dir_meta_first="$3"
_dir_meta_has_index="$4"
_dir_meta_path=$(manifest_dir_meta_path "$_dir_meta_rel")
mkdir -p "$(dirname "$_dir_meta_path")"
{
printf 'dir_manifest_rel=%s\n' "$(shell_quote "$_dir_meta_rel")"
printf 'dir_md_count=%s\n' "$(shell_quote "$_dir_meta_count")"
printf 'dir_first_md=%s\n' "$(shell_quote "$_dir_meta_first")"
printf 'dir_has_index=%s\n' "$(shell_quote "$_dir_meta_has_index")"
} > "$_dir_meta_path"
}
load_manifest_dir_entry() {
_dir_manifest_rel="$1"
_dir_manifest_meta_path=$(manifest_dir_meta_path "$_dir_manifest_rel")
[ -f "$_dir_manifest_meta_path" ] || return 1
# shellcheck disable=SC1090
. "$_dir_manifest_meta_path"
}
load_manifest_entry() {
_manifest_rel_path="$1"
_manifest_meta_path=$(manifest_meta_path "$_manifest_rel_path")
[ -f "$_manifest_meta_path" ] || return 1
# shellcheck disable=SC1090
. "$_manifest_meta_path"
}
extract_search_content() {
_search_file="$1"
awk '{
if (NR == 1 && $0 == "---") { in_fm = 1; next }
if (in_fm && $0 == "---") { in_fm = 0; next }
if (in_fm) next
if ($0 ~ /^```/) { in_code = !in_code; next }
if (in_code) next
print
}' "$_search_file" | sed \
-e 's/^#\{1,6\} //' \
-e 's/\*\*\([^*]*\)\*\*/\1/g' \
-e 's/\*\([^*]*\)\*/\1/g' \
-e 's/__\([^_]*\)__/\1/g' \
-e 's/_\([^_]*\)_/\1/g' \
-e 's/`\([^`]*\)`/\1/g' \
-e 's/\[\([^]]*\)](\([^)]*\))/\1/g' \
-e 's/!\[\([^]]*\)](\([^)]*\))//g' \
-e 's/^[[:space:]]*[-*+] //' \
-e 's/^[[:space:]]*[0-9]\{1,\}\. //' \
-e 's/^>[[:space:]]*//' \
-e 's/<[^>]*>//g' \
-e '/^[[:space:]]*$/d' \
-e 's/|//g' \
-e 's/^[[:space:]]*---[[:space:]]*$//' |
tr '\n' ' ' |
sed -e 's/ */ /g' -e 's/\\/\\\\/g' -e 's/"/\\"/g' |
awk '{ print substr($0, 1, 500) }'
}
build_markdown_manifest() {
manifest_root="$KEWT_TMPDIR/manifest"
manifest_meta_root="$manifest_root/meta"
manifest_dir_meta_root="$manifest_root/dir-meta"
manifest_all_list="$manifest_root/all.lst"
manifest_visible_list="$manifest_root/visible.lst"
manifest_hidden_dirs_list="$manifest_root/hidden-dirs.lst"
rm -rf "$manifest_root"
mkdir -p "$manifest_meta_root"
mkdir -p "$manifest_dir_meta_root"
: > "$manifest_all_list"
: > "$manifest_visible_list"
: > "$manifest_hidden_dirs_list"
eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -name \"*.md\" -print" | sort | while IFS= read -r manifest_file; do
manifest_rel_path="${manifest_file#"$src"/}"
manifest_dir_rel=$(dirname "$manifest_rel_path")
manifest_filename=$(basename "$manifest_rel_path")
manifest_is_index="false"
[ "$manifest_filename" = "index.md" ] && manifest_is_index="true"
parse_frontmatter "$manifest_file"
if [ "$manifest_filename" = "index.md" ] && [ "$fm_draft" = "true" ]; then
printf '%s\n' "$manifest_dir_rel" >> "$manifest_hidden_dirs_list"
fi
markdown_title_from_loaded_file "$manifest_file" "$title - Page"
manifest_title="$markdown_title"
set_post_datetime "$fm_date" "$(basename "$manifest_file" .md)"
manifest_post_date="$post_date"
manifest_post_time="$post_time"
manifest_post_slug=$(basename "$manifest_file" .md | 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/^[_\-]//')
if [ "$manifest_is_index" = "true" ]; then
if [ "$manifest_rel_path" = "index.md" ]; then
manifest_url="/index.html"
else
manifest_url=$(directory_index_url "${manifest_rel_path%/index.md}")
fi
else
manifest_url=$(markdown_file_url "$manifest_rel_path")
fi
manifest_search_content=""
if [ "$generate_search" = "true" ]; then
manifest_search_content=$(extract_search_content "$manifest_file")
fi
manifest_meta_file=$(manifest_meta_path "$manifest_rel_path")
mkdir -p "$(dirname "$manifest_meta_file")"
{
printf 'manifest_rel_path=%s\n' "$(shell_quote "$manifest_rel_path")"
printf 'manifest_dir_rel=%s\n' "$(shell_quote "$manifest_dir_rel")"
printf 'manifest_filename=%s\n' "$(shell_quote "$manifest_filename")"
printf 'manifest_is_index=%s\n' "$(shell_quote "$manifest_is_index")"
printf 'manifest_title=%s\n' "$(shell_quote "$manifest_title")"
printf 'manifest_date=%s\n' "$(shell_quote "$fm_date")"
printf 'manifest_draft=%s\n' "$(shell_quote "$fm_draft")"
printf 'manifest_description=%s\n' "$(shell_quote "$fm_description")"
printf 'manifest_content_warning=%s\n' "$(shell_quote "$fm_content_warning")"
printf 'manifest_tags=%s\n' "$(shell_quote "$fm_tags")"
printf 'manifest_url=%s\n' "$(shell_quote "$manifest_url")"
printf 'manifest_search_content=%s\n' "$(shell_quote "$manifest_search_content")"
printf 'manifest_post_date=%s\n' "$(shell_quote "$manifest_post_date")"
printf 'manifest_post_time=%s\n' "$(shell_quote "$manifest_post_time")"
printf 'manifest_post_slug=%s\n' "$(shell_quote "$manifest_post_slug")"
} > "$manifest_meta_file"
if load_manifest_dir_entry "$manifest_dir_rel"; then
:
else
dir_md_count=0
dir_first_md=""
dir_has_index="false"
fi
dir_md_count=$((dir_md_count + 1))
if [ -z "$dir_first_md" ]; then
dir_first_md="$manifest_rel_path"
fi
if [ "$manifest_filename" = "index.md" ]; then
dir_has_index="true"
fi
write_manifest_dir_meta "$manifest_dir_rel" "$dir_md_count" "$dir_first_md" "$dir_has_index"
printf '%s\n' "$manifest_rel_path" >> "$manifest_all_list"
done
if [ -s "$manifest_hidden_dirs_list" ]; then
LC_ALL=C sort -u "$manifest_hidden_dirs_list" > "$manifest_hidden_dirs_list.sorted"
mv "$manifest_hidden_dirs_list.sorted" "$manifest_hidden_dirs_list"
fi
eval "find \"$src\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -name \"*.md\" -print" | sort | while IFS= read -r visible_file; do
visible_rel_path="${visible_file#"$src"/}"
load_manifest_entry "$visible_rel_path" || continue
if [ "${draft_mode:-false}" != "true" ]; then
[ "$manifest_draft" = "true" ] && continue
manifest_dir_hidden_by_draft_index "$manifest_dir_rel" && continue
fi
printf '%s\n' "$visible_rel_path" >> "$manifest_visible_list"
done
}

105
lib/metadata.sh Normal file
View File

@@ -0,0 +1,105 @@
#!/bin/sh
parse_frontmatter() {
_fm_file="$1"
_fm_out="$KEWT_TMPDIR/fm_vals.txt"
: > "$_fm_out"
awk -v fm_out="$_fm_out" -f "$awk_dir/frontmatter.awk" "$_fm_file" > /dev/null
fm_title=""
fm_date=""
fm_draft=""
fm_description=""
fm_content_warning=""
fm_tags=""
while IFS='=' read -r _fk _fv; do
case "$_fk" in
title) fm_title="$_fv" ;;
date) fm_date="$_fv" ;;
draft) fm_draft="$_fv" ;;
description) fm_description="$_fv" ;;
content_warning) fm_content_warning="$_fv" ;;
tags) fm_tags="$_fv" ;;
esac
done < "$_fm_out"
rm -f "$_fm_out"
}
strip_markdown_text() {
printf '%s' "$1" | sed \
-e 's/\[//g' \
-e 's/\]//g' \
-e 's/!//g' \
-e 's/\*//g' \
-e 's/_//g' \
-e 's/`//g' \
-e 's/([^)]*)//g' \
-e 's/\\//g'
}
first_heading_from_markdown() {
grep -m 1 '^# ' "$1" | sed 's/^# *//; s/ *$//'
}
markdown_title_from_loaded_file() {
_title_file="$1"
_title_default="$2"
markdown_title="$fm_title"
if [ -z "$markdown_title" ]; then
markdown_title=$(first_heading_from_markdown "$_title_file")
if [ -n "$markdown_title" ]; then
markdown_title=$(strip_markdown_text "$markdown_title")
fi
fi
if [ -z "$markdown_title" ]; then
basename_no_ext=$(basename "$_title_file" .md)
if [ "$basename_no_ext" != "index" ] && [ "$basename_no_ext" != "404_gen" ]; then
markdown_title=$(echo "$basename_no_ext" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')
else
markdown_title="$_title_default"
fi
fi
}
markdown_title_from_file() {
_title_file="$1"
_title_default="$2"
parse_frontmatter "$_title_file"
markdown_title_from_loaded_file "$_title_file" "$_title_default"
}
set_post_datetime() {
_raw_date="$1"
_fallback_name="$2"
if [ -n "$_raw_date" ]; then
post_date=$(echo "$_raw_date" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
post_time=""
if echo "$_raw_date" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?[0-9]\{2\}[:\-][0-9]\{2\}'; then
post_time=$(echo "$_raw_date" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
return
fi
post_date=$(echo "$_fallback_name" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
post_time="00:00"
if echo "$_fallback_name" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}'; then
post_time=$(echo "$_fallback_name" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
}
set_post_metadata() {
_post_file="$1"
_default_title="$2"
_basename_no_ext=$(basename "$_post_file" .md)
markdown_title_from_file "$_post_file" "$_default_title"
post_heading="$markdown_title"
set_post_datetime "$fm_date" "$_basename_no_ext"
post_slug=$(echo "$_basename_no_ext" | 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/^[_\-]//')
}

193
lib/runtime.sh Normal file
View File

@@ -0,0 +1,193 @@
#!/bin/sh
trim_whitespace() {
printf '%s' "$1" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'
}
encode_url_path() {
printf '%s' "$1" | sed \
-e 's/%/%25/g' \
-e 's/ /%20/g' \
-e 's/#/%23/g' \
-e 's/?/%3F/g' \
-e 's/"/%22/g' \
-e "s/'/%27/g"
}
markdown_file_url() {
_rel_path="$1"
printf '/%s.html\n' "$(encode_url_path "${_rel_path%.md}")"
}
directory_index_url() {
_rel_dir="$1"
if [ -z "$_rel_dir" ] || [ "$_rel_dir" = "." ]; then
printf '/index.html\n'
else
printf '/%s/index.html\n' "$(encode_url_path "$_rel_dir")"
fi
}
format_rfc2822_utc() {
_rfc_date="$1"
_rfc_time="${2:-00:00}"
[ -n "$_rfc_time" ] || _rfc_time="00:00"
awk -v d="$_rfc_date" -v t="$_rfc_time" '
function weekday(y, m, day, k, j, h) {
if (m < 3) {
m += 12
y--
}
k = y % 100
j = int(y / 100)
h = (day + int((13 * (m + 1)) / 5) + k + int(k / 4) + int(j / 4) + 5 * j) % 7
return (h + 6) % 7
}
BEGIN {
split(d, da, "-")
split(t, ti, ":")
year = da[1] + 0
month = da[2] + 0
day = da[3] + 0
hour = (ti[1] == "" ? 0 : ti[1]) + 0
minute = (ti[2] == "" ? 0 : ti[2]) + 0
months[1] = "Jan"; months[2] = "Feb"; months[3] = "Mar"; months[4] = "Apr"
months[5] = "May"; months[6] = "Jun"; months[7] = "Jul"; months[8] = "Aug"
months[9] = "Sep"; months[10] = "Oct"; months[11] = "Nov"; months[12] = "Dec"
days[0] = "Sun"; days[1] = "Mon"; days[2] = "Tue"; days[3] = "Wed"
days[4] = "Thu"; days[5] = "Fri"; days[6] = "Sat"
printf "%s, %02d %s %04d %02d:%02d:00 +0000\n",
days[weekday(year, month, day)], day, months[month], year, hour, minute
}
'
}
append_find_rule() {
_expr="$1"
_rule="$2"
if [ -n "$_expr" ]; then
printf '%s -o %s\n' "$_expr" "$_rule"
else
printf '%s\n' "$_rule"
fi
}
append_pattern_rules_from_file() {
_expr="$1"
_root="$2"
_file="$3"
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|'#'*) continue ;;
esac
pattern=$(trim_whitespace "$line")
[ -z "$pattern" ] && continue
pattern_clean="${pattern#/}"
pattern_clean="${pattern_clean%/}"
if echo "$pattern" | grep -q "/"; then
_expr=$(append_find_rule "$_expr" "-path '$_root/$pattern_clean'")
_expr=$(append_find_rule "$_expr" "-path '$_root/$pattern_clean/*'")
else
_expr=$(append_find_rule "$_expr" "-name '$pattern_clean'")
fi
done < "$_file"
printf '%s\n' "$_expr"
}
append_nested_marker_rules() {
_expr="$1"
_root="$2"
_marker="$3"
_tmp_file="$KEWT_TMPDIR/${_marker#*.}_paths"
find "$_root" -name "$_marker" > "$_tmp_file"
while IFS= read -r marker_path; do
marker_dir="${marker_path%/"$marker"}"
if [ "$marker_dir" != "$_root" ] && [ "$marker_dir" != "." ]; then
_expr=$(append_find_rule "$_expr" "-path '$marker_dir'")
_expr=$(append_find_rule "$_expr" "-path '$marker_dir/*'")
fi
done < "$_tmp_file"
rm -f "$_tmp_file"
printf '%s\n' "$_expr"
}
build_rule_args() {
_root="$1"
_marker="$2"
_base_expr="$3"
_expr="$_base_expr"
if [ -f "$_root/$_marker" ]; then
_expr=$(append_pattern_rules_from_file "$_expr" "$_root" "$_root/$_marker")
fi
_expr=$(append_nested_marker_rules "$_expr" "$_root" "$_marker")
printf '%s\n' "$_expr"
}
resolve_template_path() {
template="$src/template.html"
[ -f "$template" ] || template="./template.html"
if [ ! -f "$template" ]; then
template="$KEWT_TMPDIR/default_template.html"
printf '%s\n' "$DEFAULT_TMPL" > "$template"
fi
}
build_full_nav() {
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
}
refresh_build_context() {
reset_config
load_config "./site.conf"
load_config "$src/site.conf"
HIDE_ARGS="$BASE_HIDE_ARGS"
if [ -n "$posts_dir" ]; then
HIDE_ARGS=$(append_find_rule "$HIDE_ARGS" "-path '$src/$posts_dir/*'")
fi
asset_version=""
if [ "$versioning" = "true" ]; then
asset_version="?v=$(date '+%Y%m%d%H%M%S')"
fi
resolve_template_path
}
watch_for_changes() {
_mark_file="$1"
changed="$(find "$src" -type f -newer "$_mark_file" 2>/dev/null | head -n 1)"
[ -z "$changed" ] && [ -f "site.conf" ] && [ "site.conf" -nt "$_mark_file" ] && changed="site.conf"
[ -z "$changed" ] && [ -f "$src/site.conf" ] && [ "$src/site.conf" -nt "$_mark_file" ] && changed="$src/site.conf"
[ -z "$changed" ] && [ -f "$template" ] && [ "$template" -nt "$_mark_file" ] && changed="$template"
[ -z "$changed" ] && [ -d "$script_dir/styles" ] && changed="$(find "$script_dir/styles" -type f -newer "$_mark_file" 2>/dev/null | head -n 1)"
printf '%s\n' "$changed"
}
is_posts_directory_rel() {
_rel_dir="$1"
[ -n "$posts_dir" ] && { [ "$_rel_dir" = "$posts_dir" ] || [ "./$_rel_dir" = "$posts_dir" ]; }
}

54
lib/search.js Normal file
View File

@@ -0,0 +1,54 @@
document.addEventListener("DOMContentLoaded", function () {
var params = new URLSearchParams(window.location.search);
var query = params.get("q");
var box = document.getElementById("search-box");
var resultsContainer = document.getElementById("search-results-list");
if (box && query) box.value = query;
if (!query) {
resultsContainer.innerHTML = "<p>Enter a search term above.</p>";
return;
}
fetch("/search.json")
.then(function (response) {
return response.json();
})
.then(function (data) {
var q = query.toLowerCase();
var results = data.filter(function (item) {
return (
item.title.toLowerCase().indexOf(q) !== -1 ||
item.content.toLowerCase().indexOf(q) !== -1
);
});
var esc = query.replace(/</g, "&lt;").replace(/>/g, "&gt;");
if (results.length === 0) {
resultsContainer.innerHTML =
'<p>No results found for "<strong>' + esc + '</strong>".</p>';
return;
}
var html =
"<p>Found " +
results.length +
' result(s) for "<strong>' +
esc +
'</strong>":</p>';
results.forEach(function (result) {
var snippet = result.content.substring(0, 200);
if (result.content.length > 200) snippet += "...";
html += '<div class="search-result">';
html += '<a href="' + result.url + '">' + result.title + "</a>";
if (snippet) html += "<p>" + snippet + "</p>";
html += "</div>";
});
resultsContainer.innerHTML = html;
})
.catch(function () {
resultsContainer.innerHTML = "<p>Error loading search index.</p>";
});
});

View File

@@ -3,53 +3,35 @@
script_dir=$(CDPATH="" cd -- "$(dirname -- "$0")" && pwd)
awk_dir="$script_dir/awk"
sed_inplace() {
script="$1"
file="$2"
tmp="${file}.tmp.$$"
if sed "$script" "$file" > "$tmp" && mv "$tmp" "$file"; then
return 0
else
rm -f "$tmp"
run_awk() {
_ra_awk_file="$1"
shift
if ! awk -f "$_ra_awk_file" "$@"; then
echo "Error: AWK failed: $_ra_awk_file" >&2
return 1
fi
}
temp_file="${KEWT_TMPDIR:-/tmp}/markdown.$$.md"
temp_parent="${KEWT_TMPDIR:-${TMPDIR:-/tmp}}"
temp_file="${temp_parent}/markdown.$$.md"
cat "$@" > "$temp_file"
trap 'rm -f "$temp_file" "$temp_file.tmp" "$temp_file.fm"' EXIT INT TERM
# Frontmatter
fm_file="$temp_file.fm"
: > "$fm_file"
awk -v fm_out="$fm_file" -f "$awk_dir/frontmatter.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# Mask
awk -f "$awk_dir/mask_inline_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/mask_plain.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
run_awk "$awk_dir/frontmatter.awk" -v fm_out="$fm_file" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# Reference links
refs=$(cat "$@" | awk '/^\[[^\]]+\]: */')
IFS='
'
for ref in $refs; do
ref_id=$(echo "$ref" | sed 's/^\[\(.*\)\]: .*/\1/')
ref_url=$(echo "$ref" | sed 's/^\[.*\]: \([^ ]*\).*/\1/')
ref_title=$(echo "$ref" | sed -n 's/^\[.*\]: [^ ]* "\(.*\)"/\1/p' | sed 's@|@!@g')
sed_inplace "s|!\[\([^]]*\)\]\[$ref_id\]|<img src=\"$ref_url\" title=\"$ref_title\" alt=\"\1\" />|g" "$temp_file"
sed_inplace "s|\[\([^]]*\)\]\[$ref_id\]|<a href=\"$ref_url\" title=\"$ref_title\">\1</a>|g" "$temp_file"
sed_inplace "s|!\[$ref_id\]\[\]|<img src=\"$ref_url\" title=\"$ref_title\" alt=\"$ref_id\" />|g" "$temp_file"
sed_inplace "s|\[$ref_id\]\[\]|<a href=\"$ref_url\" title=\"$ref_title\">$ref_id</a>|g" "$temp_file"
done
sed_inplace "/^\[[^\]]*\]: */d" "$temp_file"
# Blocks
run_awk "$awk_dir/mask_inline_code.awk" "$temp_file" \
| run_awk "$awk_dir/mask_plain.awk" \
| run_awk "$awk_dir/reference_links.awk" \
> "$temp_file.tmp" && mv "$temp_file.tmp" "$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"
while grep -q '^>' "$temp_file"; do
run_awk "$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
@@ -57,18 +39,19 @@ while grep '^>' "$temp_file" >/dev/null; do
fi
done
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 -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"
run_awk "$awk_dir/blockquote_to_admonition.awk" -v custom_admonitions="$CUSTOM_ADMONITIONS" "$temp_file" \
| run_awk "$awk_dir/fenced_code.awk" \
| run_awk "$awk_dir/indented_code.awk" \
| run_awk "$awk_dir/pipe_tables.awk" \
| run_awk "$awk_dir/definition_lists.awk" \
| run_awk "$awk_dir/lists.awk" \
| run_awk "$awk_dir/toc.awk" \
| run_awk "$awk_dir/footnotes.awk" \
| run_awk "$awk_dir/breaks.awk" \
| run_awk "$awk_dir/paragraphs.awk" \
| run_awk "$awk_dir/emoji.awk" -v emoji_file="$awk_dir/emoji.tsv" \
| run_awk "$awk_dir/markdown_inline.awk" \
| run_awk "$awk_dir/headers.awk" -v enable_header_links="$ENABLE_HEADER_LINKS" \
| run_awk "$awk_dir/markdown_embed.awk" -v input_file="$1" -v site_root="$MARKDOWN_SITE_ROOT" -v fallback_file="$MARKDOWN_FALLBACK_FILE" -v script_dir="$script_dir"
# Spacing
awk -f "$awk_dir/breaks.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/paragraphs.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# Inline styles
awk -f "$awk_dir/markdown_inline.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -v input_file="$1" -v site_root="$MARKDOWN_SITE_ROOT" -v fallback_file="$MARKDOWN_FALLBACK_FILE" -f "$awk_dir/markdown_embed.awk" "$temp_file"
rm "$temp_file"
rm -f "$temp_file"

View File

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

View File

@@ -1,12 +1,13 @@
pkgbase = kewt-git
pkgdesc = A minimalist, 100% POSIX, static site generator inspired by werc and kew
pkgver = r0.0000000
pkgrel = 3
pkgver = r0.0000002
pkgrel = 1
url = https://kewt.krzak.org
arch = any
license = ISC
makedepends = git
depends = sh
optdepends = bash-completion: bash shell completions
provides = kewt
conflicts = kewt
conflicts = kewt-bin

View File

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

View File

@@ -1,7 +1,7 @@
# Maintainer: n0va <n0va@krzak.org>
pkgname=kewt-git
pkgver=r0.0000000
pkgrel=2
pkgver=r0.0000002
pkgrel=1
pkgdesc="A minimalist, 100% POSIX, static site generator inspired by werc and kew"
arch=('any')
url="https://kewt.krzak.org"
@@ -26,4 +26,7 @@ build() {
package() {
cd "${pkgname}"
install -Dm755 kewt "${pkgdir}/usr/bin/kewt"
install -d "${pkgdir}/usr/share/zsh/site-functions"
"${pkgdir}/usr/bin/kewt" --dump-zsh-completions > "${pkgdir}/usr/share/zsh/site-functions/_kewt"
install -Dm644 packaging/bash/kewt.bash "${pkgdir}/usr/share/bash-completion/completions/kewt"
}

View File

@@ -9,8 +9,9 @@ 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')
source=("${pkgname}-${pkgver}.sh::https://git.krzak.org/N0VA/kewt/releases/download/v${pkgver}/kewt"
"${pkgname}-${pkgver}.bash::https://git.krzak.org/N0VA/kewt/releases/download/v${pkgver}/kewt.bash")
sha256sums=('SHA256SUM_PLACEHOLDER' 'SKIP')
build() {
chmod +x "${srcdir}/${pkgname}-${pkgver}.sh"
@@ -18,4 +19,7 @@ build() {
package() {
install -Dm755 "${srcdir}/${pkgname}-${pkgver}.sh" "${pkgdir}/usr/bin/kewt"
install -d "${pkgdir}/usr/share/zsh/site-functions"
"${pkgdir}/usr/bin/kewt" --dump-zsh-completions > "${pkgdir}/usr/share/zsh/site-functions/_kewt"
install -Dm644 "${srcdir}/${pkgname}-${pkgver}.bash" "${pkgdir}/usr/share/bash-completion/completions/kewt"
}

37
packaging/bash/kewt.bash Normal file
View File

@@ -0,0 +1,37 @@
_kewt() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="--help --new --init --clean --no-clean --update --post --generate-template --version --from --to --watch -w --serve -s --draft --dry-run"
case "$prev" in
--from|--to)
COMPREPLY=$(compgen -d -- "$cur")
return 0
;;
--serve|-s)
COMPREPLY=()
return 0
;;
--new|--init|--update)
COMPREPLY=$(compgen -d -- "$cur")
return 0
;;
--generate-template)
COMPREPLY=$(compgen -f -- "$cur")
return 0
;;
esac
if [[ "$cur" == -* ]]; then
COMPREPLY=$(compgen -W "$opts" -- "$cur")
return 0
fi
COMPREPLY=$(compgen -d -- "$cur")
return 0
}
complete -F _kewt kewt

View File

@@ -0,0 +1,41 @@
Name: kewt
Version: VERSION_PLACEHOLDER
Release: 1%{?dist}
Summary: A minimalist, 100% POSIX, static site generator inspired by werc
License: ISC
URL: https://kewt.krzak.org
Source0: https://git.krzak.org/N0VA/kewt/archive/v%{version}.tar.gz
BuildArch: noarch
BuildRequires: make
Requires: sh
Requires: findutils
Requires: grep
Requires: sed
Requires: gawk
Recommends: python3
Recommends: bash-completion
%description
A minimalist, 100% POSIX, static site generator inspired by werc and kew
%prep
%autosetup
%build
%make_build
%install
%make_install PREFIX=%{_prefix} BINDIR=%{_bindir} ZSHCOMPDIR=%{_datadir}/zsh/site-functions BASHCOMPDIR=%{_datadir}/bash-completion/completions
%files
%license LICENSE
%doc README.md
%{_bindir}/kewt
%{_datadir}/zsh/site-functions/_kewt
%{_datadir}/bash-completion/completions/kewt
%changelog
* Mon May 20 2024 n0va <n0va@krzak.org> - VERSION_PLACEHOLDER-1
- Initial package for Fedora

View File

@@ -8,6 +8,8 @@ class Kewt < Formula
def install
bin.install "kewt"
chmod 0755, bin/"kewt"
generate_completions_from_executable(bin/"kewt", "--dump-zsh-completions", shells: [:zsh, :bash])
end
test do

27
packaging/zsh/_kewt Normal file
View File

@@ -0,0 +1,27 @@
#compdef kewt
_kewt() {
local -a args
args=(
'--help[Show help message]'
'(-h)--help[Show help message]'
'(-)--new[Create a new site directory]'
'(-)--init[Create a new site directory (alias for --new)]'
'(-)--clean[Clean the output directory before building]'
'(-)--no-clean[Do not clean the output directory before building]'
'(-)--update[Update site.conf and template.html with latest defaults]'
'(-)--post[Create a new empty post file in the configured posts_dir]'
'(-)--generate-template[Generate a new template file]'
'(-v --version)'{-v,--version}'[Show version information]'
'--from[Source directory]:directory:_directories'
'--to[Output directory]:directory:_directories'
'(-w --watch)'{-w,--watch}'[Watch for file changes and rebuild automatically]'
'(-s --serve)'{-s,--serve}'[Start a local HTTP server after building]::port:'
'(-d --draft)'{-d,--draft}'[Include draft pages in the build]'
'(-)--dry-run[Show what would be built without writing any files]'
)
_arguments -S -C $args '*: :_directories'
}
_kewt "$@"

View File

@@ -1,4 +0,0 @@
# Heaven
| --- | --- |
| ```![/styles.css]``` | <img style="vertical-align: top;" src="catgirl.jpg"> |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -2,6 +2,8 @@
![the greatest drawing of a blobfish in the world](blobfish.bmp)
![mine turtle hello](hello.mp3)
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus malesuada lacus eu ligula semper pharetra. Cras viverra volutpat massa nec sagittis. Aliquam fringilla quam ut tincidunt ultricies. Aliquam erat volutpat. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed id sagittis nisi. Aenean vitae urna justo. Vivamus dictum eros ac mi convallis, blandit hendrerit nunc sagittis. Mauris feugiat neque quis justo molestie, vel pulvinar diam faucibus. Ut tempus magna sit amet ex pharetra mollis. Ut elementum metus metus, id consectetur est tempor eget. Proin sed nibh tincidunt, porttitor elit non, blandit ligula. Ut condimentum accumsan lobortis. Nullam nec tempus leo, sit amet iaculis erat. Donec rutrum, orci in elementum varius, nisl elit rutrum nunc, in lacinia lorem enim non enim.

5
site/depths/index.md Normal file
View File

@@ -0,0 +1,5 @@
# Depths
This is a custom index for a directory
{{LIST}}

View File

@@ -0,0 +1,85 @@
---
title = "Configuration"
---
# Configuration
## site.conf
```conf
title = "kewt"
style = "kewt"
lang = "en"
draft_by_default = false
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 = ""
posts_per_page = 12
custom_admonitions = ""
cw_hide_url = true
generate_tags = false
tags_dir = "tags"
generate_search = false
search_in_footer = false
search_in_header = false
include_cw_pages_in_search = false
```
- `title` - site title
- `style` - style name from the built-in `styles/` directory. See [Theming](theming.md)
- `lang` - document language, used for the `<html lang="...">` attribute (default: "en")
- `draft_by_default` - default value for the `draft` frontmatter field in new posts created (default: false)
- `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)
- `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 build-time version query parameter (for example `?v=20260505193210`) to css asset urls to bypass cache (default: false)
- `enable_header_links` - turns markdown section headings into clickable anchor links (default: true)
- `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.
- `posts_per_page` - number of posts per page in paginated post indexes (default: 12). Set to 0 to disable pagination.
- `custom_admonitions` - comma separated list of custom admonitions
- `cw_hide_url` - embeds non-breaking JS to replace the URL in the browser bar on content warning pages (default: true)
- `generate_tags` - generate tag index pages from post frontmatter (requires `posts_dir`)
- `tags_dir` - directory name for generated tag pages (default: "tags")
- `generate_search` - generate a `search.json` index for client-side search
- `search_in_footer` - include a search box in the page footer (requires `generate_search`)
- `search_in_header` - include a search box in the page header (requires `generate_search` and a template that includes `{{HEADER_SEARCH}}`)
- `include_cw_pages_in_search` - include content warning pages in the search index (default: false)
## Dot Files
- `.kewtignore` - files/directories to ignore completely. If the file is empty, the whole directory gets ignored.
- `.kewthide` - files/directories to hide from navigation but still process. Same empty-file rules as `.kewtignore`.
- `.kewtpreserve` - files/directories to copy as-is without converting markdown to HTML. Same empty-file rules again.

32
site/docs/embeds.md Normal file
View File

@@ -0,0 +1,32 @@
---
title = "Embeds"
---
# 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
- `\!![link]` and `\!![alt](link)` force inline local file contents
If you want to **force** a file to be inlined, use `\!![]` instead of `\![]`
## Reality-Breaking Embeds
`\!![link]` and `\!![alt](link)` work even inside inline code blocks. If the content between backticks consists only of `\!![]` embeds, the embed triggers and the content is inlined instead of being rendered as code.
```
`!![/file.sh]`
```
## Typed Embeds
Force specific output regardless of extension:
- `\!i[link]` or `\!i[alt](link)` - **I**mage
- `\!v[link]` - **V**ideo
- `\!a[link]` - **A**udio
- `\!f[link]` - I**f**rame
- `\!e[link]` - Inline/**e**mbed text/code file directly

22
site/docs/frontmatter.md Normal file
View File

@@ -0,0 +1,22 @@
---
title = "Frontmatter"
---
# Frontmatter
You can set metadata for a page using a `site.conf`-style frontmatter block at the very top of `.md` files:
```conf
---
title = "Custom Page Title"
date = "2026-03-23 11:32"
draft = false
description = "A short page summary"
tags = "example, tutorial"
---
```
- `title` - overrides the page title, post name in index links, and RSS `<title>`.
- `date` - overrides the post date and time. Supports `YYYY-MM-DD` and `YYYY-MM-DD HH:MM` (or `HH-MM`).
- `draft` - if `true`, the file is excluded from HTML generation. If not set, uses the `draft_by_default` config value.
- `description` - page description, used for Open Graph `og:description` meta tag.
- `tags` - comma separated list of tags. Used for tag index generation when `generate_tags` is enabled in `site.conf`.
- `content_warning` - if set, creates an interstitial warning page that the user must click through. If set to `true` uses a generic warning, otherwise uses your string.

6
site/docs/index.md Normal file
View File

@@ -0,0 +1,6 @@
---
title = "Documentation"
---
# Documentation
{{LIST}}

53
site/docs/installation.md Normal file
View File

@@ -0,0 +1,53 @@
---
title = "Installation"
---
# 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
```
### Fedora
```sh
sudo dnf copr enable n0va-bot/kewt
sudo dnf install kewt
```
### bpkg
```sh
bpkg install n0va-bot/kewt
```

120
site/docs/markdown.md Normal file
View File

@@ -0,0 +1,120 @@
---
title = "Markdown Extensions"
---
# Markdown Extensions
## Directory Index Customisation
By default, directories without an `index.md` get an auto-generated index page listing their contents.
If you create your own `index.md` in a directory, you can still include the auto-generated file list by using the `{{LIST}}` placeholder:
```md
# Blog
This is my blog. The posts are below. The top-most one is the most recent.
{{LIST}}
```
The `{{LIST}}` placeholder is replaced with the autogenerated file list.
## Table of Contents
`{{TOC}}` auto-generates a nested heading list with clickable anchors.
## Footnotes
Full support for `[^id]` footnotes and `[^id]: text` definitions. They render as a numbered `<section>` at the bottom of the page.
## Definition Lists
Definition lists use the standard syntax:
```md
Term
: Definition
```
This renders as `<dl><dt>Term</dt><dd>Definition</dd></dl>`. Multiple definitions per term are supported.
## Emoji Shortcodes
Standard GitHub/MkDocs emoji shortcodes like `:smile:`, `:fire:`, `:rocket:` are automatically replaced with their Unicode emoji equivalents. Shortcodes inside codeblocks are left as-is.
## Pipe Tables
Tables use the GitHub-style syntax:
```md
| Header 1 | Header 2 |
|---|---|
| cell 1 | cell 2 |
| cell 3 | cell 4 |
```
Column alignment is set with colons in the separator:
```md
| Left | Center | Right |
|:---|:---:|---:|
| a | b | c |
```
Tables can drop the header row and with a separator:
```md
|---|---|
| a | b |
```
## Blockquotes
Standard Markdown blockquote syntax using `>`:
```md
> This is a blockquote.
> It can span multiple lines.
```
### Admonitions
Blockquotes that start with a type tag become styled admonition blocks. Five built-in types are supported: `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`.
```md
> [!NOTE]
> This is a note admonition.
```
Custom admonition types can be added via the `custom_admonitions` config option in `site.conf`.
## Task Lists
GFM-style task lists are supported inside **both** ordered and unordered lists:
```md
- [ ] Unchecked item
- [x] Checked item
- Normal item
1. [ ] Unchecked item
2. [x] Checked item
3. Normal item
```
## Reference Links
Markdown reference-style links and images are supported:
```md
[link text][ref]
[ref]: https://example.com "Optional title"
![alt text][img-ref]
[img-ref]: /image.png "Optional title"
```
## Plain Text Blocks
Content inside `<plain>...</plain>` tags is rendered without any Markdown processing
## MFM Font Syntax
Misskey-style font syntax is supported for inline font family changes:
- `$[font.serif text]` - serif font
- `$[font.mono text]` - monospace font
- `$[font.sans text]` - sans-serif font

32
site/docs/quickstart.md Normal file
View File

@@ -0,0 +1,32 @@
---
title = "Quickstart"
---
# Quickstart
## Creating a site
```sh
kewt --new mysite
cd mysite
```
This creates a directory with a `site.conf`, `template.html`, and `index.md`.
## Writing content
Edit `index.md` (or any `.md` file) and just write markdown as usual. Files in subdirectories are added to the navigation automatically.
## Building
```sh
kewt src out # Replace with the directories you want
```
Reads from `src` and writes static HTML to `out`.
## Previewing
```sh
kewt --serve
```
Builds the site and starts a local HTTP server. Use `--watch` with `--serve` to rebuild automatically on file changes.
## That's it, if you want to do anything more, look at [the documentation](/docs)

22
site/docs/templates.md Normal file
View File

@@ -0,0 +1,22 @@
---
title = "Templates"
---
# Templates
When customizing `template.html`, the placeholders available are:
- `{{CONTENT}}` - the generated content
- `{{TITLE}}` - the generated title
- `{{NAV}}` - the generated navigation
- `{{FOOTER}}` - the configured footer
- `{{VERSION}}` - the cache-busting string from `versioning = true` (e.g. `?v=12345678`). Safe to use even if versioning is **disabled** (it will be empty).
- `{{CSS}}` - the configured CSS file path
- `{{LANG}}` - the configured document language
- `{{HEAD_EXTRA}}` - meta-tags
- `{{HEADER_BRAND}}` - header rendering the name and/or logo
- `{{HEADER_SEARCH}}` - header search form when `search_in_header = true`
## Search
When `generate_search` is enabled, kewt embeds a search bar into pages based on the `search_in_header` and `search_in_footer` config options. The search uses a `search.json` index generated at build time and a client-side JS script. No external dependencies are required.
If you use a custom template and want header search enabled, make sure your `template.html` includes `{{HEADER_SEARCH}}` somewhere inside `<header>`.

62
site/docs/theming.md Normal file
View File

@@ -0,0 +1,62 @@
---
title = "Theming"
---
# Theming
*kewt* has a few colour palettes built-in. Set the `style` option in `site.conf` to a theme name to apply it.
## Built-in Themes
| Theme | `style` value | Dark/Light |
|---|---|---|
| Kewt (default) | `kewt` | Light |
| Kewt Light | `kewt-light` | Light |
| Nord | `nord` | Dark |
| Nord Light | `nord-light` | Light |
| Monokai | `monokai` | Dark |
| Monokai Light | `monokai-light` | Light |
| Grayscale | `grayscale` | Dark |
| Grayscale Light | `grayscale-light` | Light |
| One Dark | `onedark` | Dark |
| One Light | `onelight` | Light |
| Rose Pine | `rosepine` | Dark |
| Rose Pine Light | `rosepine-light` | Light |
| Solarized | `solarized` | Light |
| Solarized Dark | `solarized-dark` | Dark |
```conf
style = "kewt-light"
```
## How It Works
Each theme is a `.root.css` file containing a `:root` block with CSS custom properties. At build time, *kewt* merges the theme's variables with the base `kewt.css` stylesheet. The base `:root` block is stripped out and replaced with the theme's variables.
## Style Resolution
*kewt* resolves styles in this priority order (highest wins):
1. `site/styles.css` - a full custom stylesheet in your site directory. Overrides everything.
2. `site/styles.root.css` - custom `:root` variables merged with the built-in `kewt.css` base.
3. built-in `<style>.css` - a full stylesheet matching the `style` config value.
4. built-in `<style>.root.css` - `:root` variables merged with `kewt.css`.
If none of these exist, the unmodified `kewt.css` is used
## Custom Themes
To create a custom colour theme, place a `styles.root.css` file in your site directory. The file should contain only a `:root` block with the CSS variables you want to override:
```css
:root {
--bg: #1a1b26;
--fg: #c0caf5;
--fg-link: #7aa2f7;
--fg-heading: #c0caf5;
--code-bg: #24283b;
}
```
Any variables not overridden will fall back to the defaults in `kewt.css`. The `:root` block in the base stylesheet is automatically removed to prevent conflicts.
## Per-Directory Styles
Subdirectories can have their own `styles.css` or `styles.root.css` that apply only to pages in that directory. Per-directory styles follow the same priority.

25
site/docs/usage.md Normal file
View File

@@ -0,0 +1,25 @@
---
title = "Usage"
---
# Usage
```sh
kewt --help
kewt --version
kewt --new [title]
kewt --post [title]
kewt --generate-template [path]
kewt --update [dir]
kewt --from <src> --to <out>
kewt [src] [out]
kewt --watch
kewt --serve [port]
```
- `--new [title]` creates a new site directory with a default `site.conf`, `template.html`, and `index.md`.
- `--post [title]` creates a new markdown file in the configured `posts_dir` with the current date/time as the filename and default frontmatter.
- `--generate-template [path]` writes the default `template.html` to the given path (defaults to `template.html` in the current directory).
- `--update [dir]` adds any missing keys to `site.conf` and checks `template.html` against the latest default.
- `--watch` (`-w`) watches for file changes in the source directory and rebuilds automatically.
- `--clean` cleans the output directory before building (default behavior).
- `--no-clean` does not clean the output directory before building. Useful with `--watch` to avoid clearing output on every rebuild.
- `--serve` (`-s`) starts a local HTTP server (python3 or busybox) in the output directory after building. Use with the port number to specify the port. The default port is `8000`. Composable with `--watch`.

View File

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 149 KiB

9
site/heaven/index.md Normal file
View File

@@ -0,0 +1,9 @@
---
title = "Heaven"
content_warning = "This page may have bad effects on people with an allergy to cats"
---
# Heaven
Told you
![catgirl.jpg]

View File

@@ -14,7 +14,7 @@ It's meant to be a static site generator, like _[kew](https://github.com/uint23/
## Features
- No dependencies
- Frontmatter support (title, date, draft)
- Frontmatter support (title, date, draft, description)
- Supports many embed types
- Automatic css variable replacement for older browsers
- Automatic inlining and embedding of many filetypes with `\![link]` or `\![alt](link)`
@@ -30,171 +30,15 @@ It's meant to be a static site generator, like _[kew](https://github.com/uint23/
- 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 `\![]`
- Customisable directory index pages with `{{LIST}}`
- Open Graph meta tags from frontmatter
- Auto-generated Table of Contents via `{{TOC}}`
- Footnotes (`[^id]`)
- Definition lists
- Emoji shortcodes (`:smile:`, `:fire:`, etc.)
- Post pagination
- `--watch` and `--serve` modes for development
***
## 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`.
`--post [title]` creates a new markdown file in the configured `posts_dir` with the current date/time as the name and creates the default frontmatter.
### 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
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
- `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)
- `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
- `.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
- **Typed Embeds**: Force specific output regardless of extension:
- `\!i[link]` or `\!i[alt](link)`: **I**mage
- `\!v[link]`: **V**ideo
- `\!a[link]`: **A**udio
- `\!f[link]`: I**f**rame
- `\!e[link]`: Inline/**e**mbed text/code file directly
### Frontmatter
You can set metadata for a page using a `site.conf`-style frontmatter block at the very top of `.md` files:
```conf
---
title = "Custom Page Title"
date = "2026-03-23 11:32"
draft = false
---
```
- `title`: Overrides the page title, post name in index links, and RSS `<title>`.
- `date`: Overrides the post date and time. Supports `YYYY-MM-DD` and `YYYY-MM-DD HH:MM` (or `HH-MM`).
- `draft`: If `true`, the file is excluded from HTML generation
***
>[!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...
## [Quickstart guide](/docs/quickstart.html)

View File

@@ -9,7 +9,7 @@ display_logo = false
display_title = true
logo_as_favicon = false
favicon = "favicon.ico"
order = ""
order = "Home, docs, depths, heaven"
home_name = "Home"
show_home_in_nav = true
nav_links = ""
@@ -20,3 +20,11 @@ versioning = true
enable_header_links = true
base_url = "https://kewt.krzak.org"
custom_admonitions = ""
generate_feed = false
feed_file = "rss.xml"
posts_dir = ""
generate_tags = false
generate_search = true
search_in_footer = true
search_in_header = true
include_cw_pages_in_search = false

View File

@@ -0,0 +1,27 @@
:root {
--bg: #f5f5f5;
--bg-deep: #e8e8e8;
--fg: #1a1a1a;
--fg-muted: #808080;
--fg-link: #333333;
--fg-heading: #000000;
--code-bg: #e8e8e8;
--code-border: #c0c0c0;
--code-fg: #1a1a1a;
--code-sel: #555555;
--code-prop: #333333;
--code-val: #666666;
--code-var: #444444;
--code-com: #999999;
--adm-note-bg: #e0e0ee;
--adm-note-border: #8888aa;
--adm-tip-bg: #e0eee0;
--adm-tip-border: #88aa88;
--adm-important-bg: #eee0ee;
--adm-important-border: #aa88aa;
--adm-warning-bg: #eeeed0;
--adm-warning-border: #aaaa78;
--adm-caution-bg: #eee0e0;
--adm-caution-border: #aa8888;
--thead-bg: #e8e8e8;
}

27
styles/grayscale.root.css Normal file
View File

@@ -0,0 +1,27 @@
:root {
--bg: #1a1a1a;
--bg-deep: #111111;
--fg: #d4d4d4;
--fg-muted: #808080;
--fg-link: #e0e0e0;
--fg-heading: #ffffff;
--code-bg: #0d0d0d;
--code-border: #404040;
--code-fg: #d4d4d4;
--code-sel: #b8b8b8;
--code-prop: #e0e0e0;
--code-val: #a0a0a0;
--code-var: #c8c8c8;
--code-com: #585858;
--adm-note-bg: #1a1a2e;
--adm-note-border: #5a5a8a;
--adm-tip-bg: #1a2e1a;
--adm-tip-border: #5a8a5a;
--adm-important-bg: #2e1a2e;
--adm-important-border: #8a5a8a;
--adm-warning-bg: #2e2e1a;
--adm-warning-border: #8a8a5a;
--adm-caution-bg: #2e1a1a;
--adm-caution-border: #8a5a5a;
--thead-bg: #111111;
}

View File

@@ -0,0 +1,27 @@
:root {
--bg: #f5f0ff;
--bg-deep: #e8dffa;
--fg: #2d1b4e;
--fg-muted: #7a6898;
--fg-link: #7b3fba;
--fg-heading: #3d2466;
--code-bg: #e8dffa;
--code-border: #d0c0e8;
--code-fg: #2d1b4e;
--code-sel: #b8860b;
--code-prop: #6f42c1;
--code-val: #0366d6;
--code-var: #22863a;
--code-com: #7a6898;
--adm-note-bg: #e0e0f8;
--adm-note-border: #5b6fc4;
--adm-tip-bg: #ddf0dd;
--adm-tip-border: #46a758;
--adm-important-bg: #eeddf5;
--adm-important-border: #8b5fc7;
--adm-warning-bg: #f5f0dd;
--adm-warning-border: #b8860b;
--adm-caution-bg: #f5dddd;
--adm-caution-border: #c44569;
--thead-bg: #e8dffa;
}

View File

@@ -23,6 +23,7 @@
--adm-warning-border: #ffe2bd;
--adm-caution-bg: #662d43;
--adm-caution-border: #ffc4d5;
--thead-bg: #3d2d5c;
}
body {
@@ -36,6 +37,11 @@ body {
}
header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding: 20px;
padding-bottom: 0;
border-bottom: 1px solid var(--code-border);
@@ -44,10 +50,15 @@ header {
header h1 {
margin: 0;
flex: 1 1 auto;
min-width: 0;
font-size: 35px;
font-weight: bold;
font-style: italic;
color: var(--fg-heading);
display: flex;
align-items: center;
flex-wrap: wrap;
}
.site-logo {
@@ -110,6 +121,12 @@ a {
margin-left: -10px;
}
#side-bar a.current-page:hover,
#side-bar a.current-page:focus {
color: var(--fg);
background: transparent;
}
a:hover {
background: var(--fg);
color: var(--bg);
@@ -127,6 +144,53 @@ h3 {
font-weight: normal;
}
.header-link {
color: var(--fg);
display: inline;
text-decoration: underline;
text-decoration-color: var(--fg-muted);
}
.header-link:hover,
.header-link:focus {
background: var(--fg);
color: var(--bg);
text-decoration-color: var(--fg);
}
.header-anchor {
opacity: 0;
margin-left: 0.25em;
transition: opacity 0.15s ease;
}
h1:hover > .header-anchor,
h2:hover > .header-anchor,
h3:hover > .header-anchor,
h4:hover > .header-anchor,
h5:hover > .header-anchor,
h6:hover > .header-anchor,
h1:focus-within > .header-anchor,
h2:focus-within > .header-anchor,
h3:focus-within > .header-anchor,
h4:focus-within > .header-anchor,
h5:focus-within > .header-anchor,
h6:focus-within > .header-anchor,
h1:hover .header-link .header-anchor,
h2:hover .header-link .header-anchor,
h3:hover .header-link .header-anchor,
h4:hover .header-link .header-anchor,
h5:hover .header-link .header-anchor,
h6:hover .header-link .header-anchor,
h1:focus-within .header-link .header-anchor,
h2:focus-within .header-link .header-anchor,
h3:focus-within .header-link .header-anchor,
h4:focus-within .header-link .header-anchor,
h5:focus-within .header-link .header-anchor,
h6:focus-within .header-link .header-anchor {
opacity: 1;
}
pre {
background: var(--code-bg);
color: var(--code-fg);
@@ -219,6 +283,22 @@ pre code {
border-color: var(--adm-caution-border);
}
.cw-button {
display: inline-block;
padding: 8px 16px;
background: var(--bg-deep);
border: 1px solid var(--code-border);
color: var(--fg);
text-decoration: none;
font-weight: bold;
}
.cw-button:hover {
background: var(--fg);
color: var(--bg);
border-color: var(--fg);
}
footer {
padding-top: 60px;
font-style: italic;
@@ -252,8 +332,54 @@ hr {
border-top: 1px solid var(--code-border);
}
table {
border-collapse: collapse;
margin: 20px 0;
width: 100%;
}
thead {
border-bottom: 2px solid var(--code-border);
background: var(--thead-bg);
}
th {
font-weight: bold;
text-align: left;
padding: 8px 12px;
}
td {
padding: 8px 12px;
border-top: 1px solid var(--code-border);
}
tr:nth-child(even) {
background: var(--bg-deep);
}
.nav-toggle,
.nav-toggle-label {
display: none;
}
@media screen and (max-width: 600px) {
header {
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-toggle-label {
display: block;
font-size: 30px;
cursor: pointer;
color: var(--fg-heading);
user-select: none;
}
#side-bar {
display: none;
position: relative;
top: auto;
left: auto;
@@ -264,6 +390,10 @@ hr {
margin: 0 20px 20px 20px;
}
.nav-toggle:checked ~ #side-bar {
display: block;
}
article {
margin: 0 20px 0 20px;
}
@@ -283,3 +413,215 @@ hr {
margin: 0 0.2em 0.25em -1.6em;
vertical-align: middle;
}
.kewt-search-page {
display: flex;
gap: 8px;
margin: 20px 0;
}
.kewt-search-page input[type="text"] {
flex: 1;
padding: 8px 12px;
font-size: 16px;
font-family: inherit;
background: var(--code-bg);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
outline: none;
}
.kewt-search-page input[type="text"]:focus {
border-color: var(--fg-link);
}
.kewt-search-page button {
padding: 8px 20px;
font-size: 16px;
font-family: inherit;
background: var(--bg-deep);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
cursor: pointer;
}
.kewt-search-page button:hover {
background: var(--fg);
color: var(--bg);
}
.search-result {
margin: 16px 0;
padding: 12px;
background: var(--code-bg);
border: 1px solid var(--code-border);
border-radius: 3px;
}
.search-result a {
font-size: 18px;
font-weight: bold;
color: var(--fg-link);
}
.search-result p {
margin: 6px 0 0 0;
color: var(--fg-muted);
font-size: 14px;
}
/* Footer search */
.kewt-search-footer {
display: inline-flex;
gap: 4px;
margin-left: 12px;
vertical-align: middle;
}
.kewt-search-footer input[type="text"] {
padding: 3px 8px;
font-size: 14px;
font-family: inherit;
background: var(--code-bg);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
outline: none;
width: 120px;
}
.kewt-search-footer input[type="text"]:focus {
border-color: var(--fg-link);
}
.kewt-search-footer button {
padding: 3px 10px;
font-size: 14px;
font-family: inherit;
background: var(--bg-deep);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
cursor: pointer;
}
.kewt-search-footer button:hover {
background: var(--fg);
color: var(--bg);
}
.kewt-search-header {
display: inline-flex;
gap: 4px;
margin-left: 0;
vertical-align: middle;
font-style: normal;
flex: 0 0 auto;
}
.kewt-search-header input[type="text"] {
padding: 4px 8px;
font-size: 14px;
font-family: inherit;
background: var(--code-bg);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
outline: none;
width: 160px;
}
.kewt-search-header input[type="text"]:focus {
border-color: var(--fg-link);
}
.kewt-search-header button {
padding: 4px 10px;
font-size: 14px;
font-family: inherit;
background: var(--bg-deep);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
cursor: pointer;
}
.kewt-search-header button:hover {
background: var(--fg);
color: var(--bg);
}
.kewt-search-nav {
display: none;
padding: 8px 0 12px 0;
margin-bottom: 8px;
border-bottom: 1px solid var(--code-border);
}
.kewt-search-nav form {
display: flex;
gap: 4px;
}
.kewt-search-nav input[type="text"] {
flex: 1;
padding: 6px 8px;
font-size: 14px;
font-family: inherit;
background: var(--code-bg);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
outline: none;
}
.kewt-search-nav input[type="text"]:focus {
border-color: var(--fg-link);
}
.kewt-search-nav button {
padding: 6px 10px;
font-size: 14px;
font-family: inherit;
background: var(--bg-deep);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
cursor: pointer;
}
.kewt-search-nav button:hover {
background: var(--fg);
color: var(--bg);
}
@media screen and (max-width: 600px) {
.kewt-search-header {
display: none;
}
.kewt-search-nav {
display: block;
}
.kewt-search-page {
flex-direction: column;
}
.kewt-search-page button {
align-self: flex-start;
}
.kewt-search-footer {
display: flex;
margin-left: 0;
margin-top: 8px;
}
.kewt-search-footer input[type="text"] {
flex: 1;
width: auto;
}
}

View File

@@ -0,0 +1,27 @@
:root {
--bg: #fafaf8;
--bg-deep: #f0efe8;
--fg: #272822;
--fg-muted: #8f8e84;
--fg-link: #5c8a1e;
--fg-heading: #272822;
--code-bg: #f0efe8;
--code-border: #cccbc2;
--code-fg: #272822;
--code-sel: #5c8a1e;
--code-prop: #c41e6a;
--code-val: #7a3ee0;
--code-var: #2e8faf;
--code-com: #8f8e84;
--adm-note-bg: #edf4f7;
--adm-note-border: #2e8faf;
--adm-tip-bg: #eef7ed;
--adm-tip-border: #5c8a1e;
--adm-important-bg: #f2eef7;
--adm-important-border: #7a3ee0;
--adm-warning-bg: #f7f5ed;
--adm-warning-border: #b0a01e;
--adm-caution-bg: #f7edee;
--adm-caution-border: #c41e6a;
--thead-bg: #f0efe8;
}

27
styles/monokai.root.css Normal file
View File

@@ -0,0 +1,27 @@
:root {
--bg: #272822;
--bg-deep: #1e1f1c;
--fg: #f8f8f2;
--fg-muted: #75715e;
--fg-link: #a6e22e;
--fg-heading: #f8f8f2;
--code-bg: #1e1f1c;
--code-border: #49483e;
--code-fg: #f8f8f2;
--code-sel: #a6e22e;
--code-prop: #f92672;
--code-val: #ae81ff;
--code-var: #66d9ef;
--code-com: #75715e;
--adm-note-bg: #272822;
--adm-note-border: #66d9ef;
--adm-tip-bg: #272822;
--adm-tip-border: #a6e22e;
--adm-important-bg: #272822;
--adm-important-border: #ae81ff;
--adm-warning-bg: #272822;
--adm-warning-border: #e6db74;
--adm-caution-bg: #272822;
--adm-caution-border: #f92672;
--thead-bg: #1e1f1c;
}

View File

@@ -0,0 +1,27 @@
:root {
--bg: #eceff4;
--bg-deep: #d8dee9;
--fg: #2e3440;
--fg-muted: #4c566a;
--fg-link: #5e81ac;
--fg-heading: #3b4252;
--code-bg: #d8dee9;
--code-border: #c5cdd9;
--code-fg: #2e3440;
--code-sel: #d08770;
--code-prop: #5e81ac;
--code-val: #8fbcbb;
--code-var: #a3be8c;
--code-com: #4c566a;
--adm-note-bg: #d8dee9;
--adm-note-border: #5e81ac;
--adm-tip-bg: #e0ebd8;
--adm-tip-border: #a3be8c;
--adm-important-bg: #e5dbe8;
--adm-important-border: #b48ead;
--adm-warning-bg: #ede5d6;
--adm-warning-border: #ebcb8b;
--adm-caution-bg: #eddcdc;
--adm-caution-border: #bf616a;
--thead-bg: #d8dee9;
}

27
styles/nord.root.css Normal file
View File

@@ -0,0 +1,27 @@
:root {
--bg: #2e3440;
--bg-deep: #242933;
--fg: #d8dee9;
--fg-muted: #a5b0c1;
--fg-link: #88c0d0;
--fg-heading: #eceff4;
--code-bg: #3b4252;
--code-border: #4c566a;
--code-fg: #d8dee9;
--code-sel: #ebcb8b;
--code-prop: #8fbcbb;
--code-val: #81a1c1;
--code-var: #a3be8c;
--code-com: #616e88;
--adm-note-bg: #3b4252;
--adm-note-border: #88c0d0;
--adm-tip-bg: #3b4340;
--adm-tip-border: #a3be8c;
--adm-important-bg: #3b4044;
--adm-important-border: #b48ead;
--adm-warning-bg: #3b4038;
--adm-warning-border: #ebcb8b;
--adm-caution-bg: #3b3840;
--adm-caution-border: #bf616a;
--thead-bg: #3b4252;
}

27
styles/onedark.root.css Normal file
View File

@@ -0,0 +1,27 @@
:root {
--bg: #282c34;
--bg-deep: #21252b;
--fg: #abb2bf;
--fg-muted: #636d83;
--fg-link: #61afef;
--fg-heading: #c8ccd4;
--code-bg: #21252b;
--code-border: #3e4451;
--code-fg: #abb2bf;
--code-sel: #e5c07b;
--code-prop: #e06c75;
--code-val: #56b6c2;
--code-var: #98c379;
--code-com: #5c6370;
--adm-note-bg: #2c313c;
--adm-note-border: #61afef;
--adm-tip-bg: #2c3a30;
--adm-tip-border: #98c379;
--adm-important-bg: #33303c;
--adm-important-border: #c678dd;
--adm-warning-bg: #3a352c;
--adm-warning-border: #e5c07b;
--adm-caution-bg: #3a2c2e;
--adm-caution-border: #e06c75;
--thead-bg: #21252b;
}

27
styles/onelight.root.css Normal file
View File

@@ -0,0 +1,27 @@
:root {
--bg: #fafafa;
--bg-deep: #eaeaeb;
--fg: #383a42;
--fg-muted: #a0a1a7;
--fg-link: #4078f2;
--fg-heading: #383a42;
--code-bg: #eaeaeb;
--code-border: #d4d4d5;
--code-fg: #383a42;
--code-sel: #986801;
--code-prop: #e45649;
--code-val: #0184bc;
--code-var: #50a14f;
--code-com: #a0a1a7;
--adm-note-bg: #e8eefa;
--adm-note-border: #4078f2;
--adm-tip-bg: #e8f5e8;
--adm-tip-border: #50a14f;
--adm-important-bg: #f2e8f5;
--adm-important-border: #a626a4;
--adm-warning-bg: #f5f0e0;
--adm-warning-border: #986801;
--adm-caution-bg: #fae8e8;
--adm-caution-border: #e45649;
--thead-bg: #eaeaeb;
}

View File

@@ -0,0 +1,27 @@
:root {
--bg: #faf4ed;
--bg-deep: #f2e9e1;
--fg: #575279;
--fg-muted: #9893a5;
--fg-link: #907aa9;
--fg-heading: #286983;
--code-bg: #f2e9e1;
--code-border: #dfdad9;
--code-fg: #575279;
--code-sel: #ea9d34;
--code-prop: #b4637a;
--code-val: #56949f;
--code-var: #286983;
--code-com: #9893a5;
--adm-note-bg: #f0e8f5;
--adm-note-border: #907aa9;
--adm-tip-bg: #e8f0ee;
--adm-tip-border: #56949f;
--adm-important-bg: #f0e8f0;
--adm-important-border: #907aa9;
--adm-warning-bg: #f5f0e0;
--adm-warning-border: #ea9d34;
--adm-caution-bg: #f5e5e8;
--adm-caution-border: #b4637a;
--thead-bg: #f2e9e1;
}

27
styles/rosepine.root.css Normal file
View File

@@ -0,0 +1,27 @@
:root {
--bg: #191724;
--bg-deep: #13111e;
--fg: #e0def4;
--fg-muted: #908caa;
--fg-link: #c4a7e7;
--fg-heading: #ebbcba;
--code-bg: #1f1d2e;
--code-border: #26233a;
--code-fg: #e0def4;
--code-sel: #f6c177;
--code-prop: #eb6f92;
--code-val: #9ccfd8;
--code-var: #31748f;
--code-com: #6e6a86;
--adm-note-bg: #1f1d2e;
--adm-note-border: #c4a7e7;
--adm-tip-bg: #1a2332;
--adm-tip-border: #9ccfd8;
--adm-important-bg: #2a1f2e;
--adm-important-border: #c4a7e7;
--adm-warning-bg: #2a251f;
--adm-warning-border: #f6c177;
--adm-caution-bg: #2a1f22;
--adm-caution-border: #eb6f92;
--thead-bg: #1f1d2e;
}

View File

@@ -0,0 +1,27 @@
:root {
--bg: #002b36;
--bg-deep: #001e26;
--fg: #839496;
--fg-muted: #586e75;
--fg-link: #268bd2;
--fg-heading: #93a1a1;
--code-bg: #073642;
--code-border: #094959;
--code-fg: #839496;
--code-sel: #d33682;
--code-prop: #268bd2;
--code-val: #2aa198;
--code-var: #859900;
--code-com: #586e75;
--adm-note-bg: #073642;
--adm-note-border: #268bd2;
--adm-tip-bg: #07382e;
--adm-tip-border: #2aa198;
--adm-important-bg: #2a0736;
--adm-important-border: #d33682;
--adm-warning-bg: #363007;
--adm-warning-border: #b58900;
--adm-caution-bg: #360a07;
--adm-caution-border: #cb4b16;
--thead-bg: #073642;
}

27
styles/solarized.root.css Normal file
View File

@@ -0,0 +1,27 @@
:root {
--bg: #fdf6e3;
--bg-deep: #eee8d5;
--fg: #657b83;
--fg-muted: #93a1a1;
--fg-link: #268bd2;
--fg-heading: #586e75;
--code-bg: #eee8d5;
--code-border: #d3cbb7;
--code-fg: #657b83;
--code-sel: #d33682;
--code-prop: #268bd2;
--code-val: #2aa198;
--code-var: #859900;
--code-com: #93a1a1;
--adm-note-bg: #eee8d5;
--adm-note-border: #268bd2;
--adm-tip-bg: #e8f5e0;
--adm-tip-border: #859900;
--adm-important-bg: #f0e8f5;
--adm-important-border: #6c71c4;
--adm-warning-bg: #fdf0e3;
--adm-warning-border: #b58900;
--adm-caution-bg: #fde8e8;
--adm-caution-border: #dc322f;
--thead-bg: #eee8d5;
}

185
tests/test_builder.sh Normal file
View File

@@ -0,0 +1,185 @@
test_needs_rebuild_no_output() {
. "$project_dir/lib/config.sh"
. "$project_dir/lib/runtime.sh"
. "$project_dir/lib/builder.sh"
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
echo "test" > "$tmpdir/src.md"
needs_rebuild "$tmpdir/src.md" "$tmpdir/out.html"
result=$?
assert_eq "0" "$result" "rebuild when output missing"
rm -rf "$tmpdir"
}
test_needs_rebuild_output_newer() {
. "$project_dir/lib/config.sh"
. "$project_dir/lib/runtime.sh"
. "$project_dir/lib/builder.sh"
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
echo "test" > "$tmpdir/src.md"
sleep 1
echo "test" > "$tmpdir/out.html"
needs_rebuild "$tmpdir/src.md" "$tmpdir/out.html"
result=$?
assert_eq "1" "$result" "no rebuild when output newer"
rm -rf "$tmpdir"
}
test_needs_rebuild_source_newer() {
. "$project_dir/lib/config.sh"
. "$project_dir/lib/runtime.sh"
. "$project_dir/lib/builder.sh"
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
echo "old" > "$tmpdir/out.html"
sleep 1
echo "new" > "$tmpdir/src.md"
needs_rebuild "$tmpdir/src.md" "$tmpdir/out.html"
result=$?
assert_eq "0" "$result" "rebuild when source newer"
rm -rf "$tmpdir"
}
test_escape_html_text() {
. "$project_dir/lib/generator.sh"
result=$(escape_html_text "<script>&test>")
assert_eq "&lt;script&gt;&amp;test&gt;" "$result" "escape html text"
}
test_escape_html_attr() {
. "$project_dir/lib/generator.sh"
result=$(escape_html_attr 'value with "quotes" & <tags>')
assert_eq "value with &quot;quotes&quot; &amp; &lt;tags&gt;" "$result" "escape html attr"
}
test_nav_links_empty() {
. "$project_dir/lib/config.sh"
. "$project_dir/lib/generator.sh"
nav_links=""
result=$(nav_links_html)
assert_eq "" "$result" "empty nav links"
}
test_nav_links_markdown() {
. "$project_dir/lib/config.sh"
. "$project_dir/lib/generator.sh"
nav_links="[Docs](/docs), [About](/about)"
result=$(nav_links_html)
assert_contains '<li><a href="/docs">Docs</a></li>' "$result" "nav links markdown docs"
assert_contains '<li><a href="/about">About</a></li>' "$result" "nav links markdown about"
}
test_nav_links_plain() {
. "$project_dir/lib/config.sh"
. "$project_dir/lib/generator.sh"
nav_links="https://example.com"
result=$(nav_links_html)
assert_contains '<li><a href="https://example.com">' "$result" "nav links plain url"
}
test_find_closest() {
. "$project_dir/lib/config.sh"
. "$project_dir/lib/generator.sh"
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir/sub/deep"
echo "root" > "$tmpdir/template.html"
echo "sub" > "$tmpdir/sub/template.html"
src="$tmpdir"
result=$(find_closest "template.html" "$tmpdir/sub/deep")
assert_eq "$tmpdir/sub/template.html" "$result" "find closest in parent"
result=$(find_closest "template.html" "$tmpdir")
assert_eq "$tmpdir/template.html" "$result" "find closest in current"
rm -rf "$tmpdir"
}
test_find_closest_fallback_to_src() {
. "$project_dir/lib/config.sh"
. "$project_dir/lib/generator.sh"
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir/sub"
echo "root" > "$tmpdir/template.html"
src="$tmpdir"
result=$(find_closest "template.html" "$tmpdir/sub")
assert_eq "$tmpdir/template.html" "$result" "find closest falls back to src"
rm -rf "$tmpdir"
}
test_custom_404_md() {
. "$project_dir/lib/config.sh"
. "$project_dir/lib/runtime.sh"
. "$project_dir/lib/metadata.sh"
. "$project_dir/lib/manifest.sh"
. "$project_dir/lib/generator.sh"
. "$project_dir/lib/builder.sh"
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
cat > "$tmpdir/site.conf" <<EOF
title = "Test"
error_page = "not_found.html"
EOF
printf '# Custom 404\n\nPage not found, sorry.\n' > "$tmpdir/not_found.md"
src="$tmpdir"
out="$tmpdir/out"
KEWT_TMPDIR="$tmpdir/tmp"
mkdir -p "$out" "$KEWT_TMPDIR"
awk_dir="$project_dir/awk"
script_dir="$project_dir"
style="kewt"
template="$KEWT_TMPDIR/default_template.html"
printf '%s\n' "$DEFAULT_TMPL" > "$template"
nav=""
footer=""
header_brand=""
head_extra=""
asset_version=""
lang="en"
current_url=""
fm_title=""
fm_content_warning=""
fm_description=""
generate_page_title="true"
logo_as_favicon="false"
favicon=""
display_logo="false"
display_title="true"
logo=""
search_in_header="false"
search_in_footer="false"
cw_hide_url="true"
enable_header_links="false"
custom_admonitions=""
build_error_page
assert_file_exists "$out/not_found.html" "custom 404 html generated"
assert_contains "Custom 404" "$(cat "$out/not_found.html")" "custom 404 has custom content"
rm -rf "$tmpdir"
}

129
tests/test_config.sh Executable file
View File

@@ -0,0 +1,129 @@
test_config_defaults() {
. "$project_dir/lib/config.sh"
assert_eq "kewt" "$title" "default title"
assert_eq "kewt" "$style" "default style"
assert_eq "en" "$lang" "default lang"
assert_eq "false" "$draft_by_default" "default draft_by_default"
assert_eq "true" "$dir_indexes" "default dir_indexes"
assert_eq "true" "$single_file_index" "default single_file_index"
assert_eq "false" "$flatten" "default flatten"
assert_eq "Home" "$home_name" "default home_name"
assert_eq "true" "$show_home_in_nav" "default show_home_in_nav"
assert_eq "false" "$generate_feed" "default generate_feed"
assert_eq "rss.xml" "$feed_file" "default feed_file"
assert_eq "12" "$posts_per_page" "default posts_per_page"
assert_eq "false" "$generate_tags" "default generate_tags"
assert_eq "tags" "$tags_dir" "default tags_dir"
assert_eq "false" "$generate_search" "default generate_search"
assert_eq "true" "$enable_header_links" "default enable_header_links"
assert_eq "true" "$cw_hide_url" "default cw_hide_url"
}
test_config_reset() {
. "$project_dir/lib/config.sh"
title="custom"
style="nord"
generate_feed="true"
reset_config
assert_eq "kewt" "$title" "reset title"
assert_eq "kewt" "$style" "reset style"
assert_eq "false" "$generate_feed" "reset generate_feed"
}
test_config_load() {
. "$project_dir/lib/config.sh"
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
cat > "$tmpdir/test.conf" <<EOF
title = "My Site"
style = "nord"
generate_feed = true
base_url = "https://example.com"
posts_per_page = 5
EOF
reset_config
load_config "$tmpdir/test.conf"
assert_eq "My Site" "$title" "load title"
assert_eq "nord" "$style" "load style"
assert_eq "true" "$generate_feed" "load generate_feed"
assert_eq "https://example.com" "$base_url" "load base_url"
assert_eq "5" "$posts_per_page" "load posts_per_page"
rm -rf "$tmpdir"
}
test_config_load_quoted() {
. "$project_dir/lib/config.sh"
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
cat > "$tmpdir/test.conf" <<'EOF'
footer = "made with <a href=\"https://kewt.krzak.org\">kewt</a>"
nav_links = "[Docs](/docs), [About](/about)"
EOF
reset_config
load_config "$tmpdir/test.conf"
assert_eq 'made with <a href="https://kewt.krzak.org">kewt</a>' "$footer" "load quoted footer"
assert_eq "[Docs](/docs), [About](/about)" "$nav_links" "load quoted nav_links"
rm -rf "$tmpdir"
}
test_config_load_skips_comments() {
. "$project_dir/lib/config.sh"
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
cat > "$tmpdir/test.conf" <<EOF
# This is a comment
title = "Test Site"
# Another comment
style = "kewt"
EOF
reset_config
load_config "$tmpdir/test.conf"
assert_eq "Test Site" "$title" "load with comments title"
assert_eq "kewt" "$style" "load with comments style"
rm -rf "$tmpdir"
}
test_config_load_missing_file() {
. "$project_dir/lib/config.sh"
reset_config
load_config "/nonexistent/path/site.conf"
assert_eq "kewt" "$title" "missing file keeps defaults"
}
test_config_feed_full_content_default() {
. "$project_dir/lib/config.sh"
assert_eq "false" "$feed_full_content" "default feed_full_content"
}
test_config_feed_full_content_load() {
. "$project_dir/lib/config.sh"
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
cat > "$tmpdir/test.conf" <<EOF
feed_full_content = true
EOF
reset_config
load_config "$tmpdir/test.conf"
assert_eq "true" "$feed_full_content" "load feed_full_content"
rm -rf "$tmpdir"
}

43
tests/test_manifest.sh Normal file
View File

@@ -0,0 +1,43 @@
test_draft_mode_includes_drafts_in_manifest() {
. "$project_dir/lib/config.sh"
. "$project_dir/lib/runtime.sh"
. "$project_dir/lib/metadata.sh"
. "$project_dir/lib/manifest.sh"
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
cat > "$tmpdir/site.conf" <<EOF
title = "Test"
dir_indexes = true
EOF
printf '# Normal Page\n' > "$tmpdir/normal.md"
printf -- '---\ndraft = true\n---\n# Draft Page\n' > "$tmpdir/draft.md"
src="$tmpdir"
out="$tmpdir/out"
KEWT_TMPDIR="$tmpdir/tmp"
mkdir -p "$KEWT_TMPDIR"
awk_dir="$project_dir/awk"
script_dir="$project_dir"
reset_config
load_config "$tmpdir/site.conf"
IGNORE_ARGS="-name '.kewtignore' -o -path '$src/.*'"
HIDE_ARGS="-name '.kewtignore' -o -name '.kewthide' -o -name '.kewtpreserve' -o -path '$src/.*'"
PRESERVE_ARGS="-false"
draft_mode="false"
build_markdown_manifest
visible_count=$(wc -l < "$manifest_visible_list")
assert_eq "1" "$visible_count" "without draft mode only normal page visible"
draft_mode="true"
build_markdown_manifest
visible_count=$(wc -l < "$manifest_visible_list")
assert_eq "2" "$visible_count" "with draft mode both pages visible"
rm -rf "$tmpdir"
}

146
tests/test_markdown.sh Normal file
View File

@@ -0,0 +1,146 @@
test_markdown_heading() {
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
printf '# Hello World\n\nSome content.\n' > "$tmpdir/test.md"
result=$(ENABLE_HEADER_LINKS="false" CUSTOM_ADMONITIONS="" MARKDOWN_SITE_ROOT="$tmpdir" MARKDOWN_FALLBACK_FILE="" sh "$project_dir/markdown.sh" "$tmpdir/test.md")
assert_contains "<h1" "$result" "markdown heading tag"
assert_contains "Hello World" "$result" "markdown heading text"
rm -rf "$tmpdir"
}
test_markdown_paragraph() {
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
printf 'This is a paragraph.\n' > "$tmpdir/test.md"
result=$(ENABLE_HEADER_LINKS="false" CUSTOM_ADMONITIONS="" MARKDOWN_SITE_ROOT="$tmpdir" MARKDOWN_FALLBACK_FILE="" sh "$project_dir/markdown.sh" "$tmpdir/test.md")
assert_contains "<p>" "$result" "markdown paragraph open"
assert_contains "This is a paragraph." "$result" "markdown paragraph text"
rm -rf "$tmpdir"
}
test_markdown_bold() {
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
printf 'This is **bold** text.\n' > "$tmpdir/test.md"
result=$(ENABLE_HEADER_LINKS="false" CUSTOM_ADMONITIONS="" MARKDOWN_SITE_ROOT="$tmpdir" MARKDOWN_FALLBACK_FILE="" sh "$project_dir/markdown.sh" "$tmpdir/test.md")
assert_contains "<strong>bold</strong>" "$result" "markdown bold"
rm -rf "$tmpdir"
}
test_markdown_italic() {
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
printf 'This is *italic* text.\n' > "$tmpdir/test.md"
result=$(ENABLE_HEADER_LINKS="false" CUSTOM_ADMONITIONS="" MARKDOWN_SITE_ROOT="$tmpdir" MARKDOWN_FALLBACK_FILE="" sh "$project_dir/markdown.sh" "$tmpdir/test.md")
assert_contains "<em>italic</em>" "$result" "markdown italic"
rm -rf "$tmpdir"
}
test_markdown_link() {
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
printf '[click here](https://example.com)\n' > "$tmpdir/test.md"
result=$(ENABLE_HEADER_LINKS="false" CUSTOM_ADMONITIONS="" MARKDOWN_SITE_ROOT="$tmpdir" MARKDOWN_FALLBACK_FILE="" sh "$project_dir/markdown.sh" "$tmpdir/test.md")
assert_contains 'href="https://example.com"' "$result" "markdown link href"
assert_contains "click here" "$result" "markdown link text"
rm -rf "$tmpdir"
}
test_markdown_code_block() {
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
cat > "$tmpdir/test.md" <<EOF
\`\`\`
code here
\`\`\`
EOF
result=$(ENABLE_HEADER_LINKS="false" CUSTOM_ADMONITIONS="" MARKDOWN_SITE_ROOT="$tmpdir" MARKDOWN_FALLBACK_FILE="" sh "$project_dir/markdown.sh" "$tmpdir/test.md")
assert_contains "<pre>" "$result" "markdown code block pre"
assert_contains "<code>" "$result" "markdown code block code"
rm -rf "$tmpdir"
}
test_markdown_unordered_list() {
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
printf -- '- item one\n- item two\n' > "$tmpdir/test.md"
result=$(ENABLE_HEADER_LINKS="false" CUSTOM_ADMONITIONS="" MARKDOWN_SITE_ROOT="$tmpdir" MARKDOWN_FALLBACK_FILE="" sh "$project_dir/markdown.sh" "$tmpdir/test.md")
assert_contains "<ul>" "$result" "markdown list ul"
assert_contains "<li>item one</li>" "$result" "markdown list item one"
assert_contains "<li>item two</li>" "$result" "markdown list item two"
rm -rf "$tmpdir"
}
test_markdown_blockquote() {
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
printf '> This is a quote\n' > "$tmpdir/test.md"
result=$(ENABLE_HEADER_LINKS="false" CUSTOM_ADMONITIONS="" MARKDOWN_SITE_ROOT="$tmpdir" MARKDOWN_FALLBACK_FILE="" sh "$project_dir/markdown.sh" "$tmpdir/test.md")
assert_contains "<blockquote>" "$result" "markdown blockquote"
rm -rf "$tmpdir"
}
test_markdown_frontmatter_stripped() {
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
cat > "$tmpdir/test.md" <<EOF
---
title = "Test"
---
# Heading
EOF
result=$(ENABLE_HEADER_LINKS="false" CUSTOM_ADMONITIONS="" MARKDOWN_SITE_ROOT="$tmpdir" MARKDOWN_FALLBACK_FILE="" sh "$project_dir/markdown.sh" "$tmpdir/test.md")
assert_contains "<h1" "$result" "markdown frontmatter heading present"
assert_contains "Heading" "$result" "markdown frontmatter heading text"
result_not_contains=$(echo "$result" | grep -c 'title = "Test"')
assert_eq "0" "$result_not_contains" "markdown frontmatter not in output"
rm -rf "$tmpdir"
}
test_markdown_header_links() {
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
printf '# My Heading\n' > "$tmpdir/test.md"
result=$(ENABLE_HEADER_LINKS="true" CUSTOM_ADMONITIONS="" MARKDOWN_SITE_ROOT="$tmpdir" MARKDOWN_FALLBACK_FILE="" sh "$project_dir/markdown.sh" "$tmpdir/test.md")
assert_contains 'id="my-heading"' "$result" "markdown header link id"
assert_contains 'href="#my-heading"' "$result" "markdown header link href"
rm -rf "$tmpdir"
}
test_markdown_pipe_table() {
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
cat > "$tmpdir/test.md" <<EOF
| A | B |
|---|---|
| 1 | 2 |
EOF
result=$(ENABLE_HEADER_LINKS="false" CUSTOM_ADMONITIONS="" MARKDOWN_SITE_ROOT="$tmpdir" MARKDOWN_FALLBACK_FILE="" sh "$project_dir/markdown.sh" "$tmpdir/test.md")
assert_contains "<table>" "$result" "markdown table"
assert_contains "<th>A</th>" "$result" "markdown table header"
assert_contains "<td>1</td>" "$result" "markdown table cell"
rm -rf "$tmpdir"
}

92
tests/test_metadata.sh Executable file
View File

@@ -0,0 +1,92 @@
test_strip_markdown_bold() {
. "$project_dir/lib/metadata.sh"
result=$(strip_markdown_text "**bold text**")
assert_eq "bold text" "$result" "strip bold"
}
test_strip_markdown_italic() {
. "$project_dir/lib/metadata.sh"
result=$(strip_markdown_text "*italic text*")
assert_eq "italic text" "$result" "strip italic"
}
test_strip_markdown_link() {
. "$project_dir/lib/metadata.sh"
result=$(strip_markdown_text "[link](http://example.com)")
assert_eq "link" "$result" "strip link"
}
test_strip_markdown_code() {
. "$project_dir/lib/metadata.sh"
result=$(strip_markdown_text "\`code\`")
assert_eq "code" "$result" "strip code"
}
test_first_heading() {
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
printf '# Hello World\n\nSome content\n' > "$tmpdir/test.md"
. "$project_dir/lib/metadata.sh"
result=$(first_heading_from_markdown "$tmpdir/test.md")
assert_eq "Hello World" "$result" "first heading"
rm -rf "$tmpdir"
}
test_first_heading_no_heading() {
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
printf 'Random content\n' > "$tmpdir/test.md"
. "$project_dir/lib/metadata.sh"
result=$(first_heading_from_markdown "$tmpdir/test.md")
assert_eq "" "$result" "no heading returns empty"
rm -rf "$tmpdir"
}
test_parse_frontmatter() {
tmpdir="${TMPDIR:-/tmp}/kewt_test.$$"
mkdir -p "$tmpdir"
cat > "$tmpdir/test.md" <<'EOF'
---
title = "My Post"
date = "2026-05-20 10:00"
draft = false
description = "A test post"
tags = "test, example"
---
# Content
EOF
export KEWT_TMPDIR="$tmpdir"
export awk_dir="$project_dir/awk"
. "$project_dir/lib/metadata.sh"
. "$project_dir/lib/config.sh"
parse_frontmatter "$tmpdir/test.md"
assert_eq "My Post" "$fm_title" "parse title"
assert_eq "2026-05-20 10:00" "$fm_date" "parse date"
assert_eq "false" "$fm_draft" "parse draft"
assert_eq "A test post" "$fm_description" "parse description"
assert_eq "test, example" "$fm_tags" "parse tags"
rm -rf "$tmpdir"
}
test_set_post_datetime_from_date() {
. "$project_dir/lib/metadata.sh"
set_post_datetime "2026-05-20 14:30" "fallback"
assert_eq "2026-05-20" "$post_date" "post date from date field"
assert_eq "14:30" "$post_time" "post time from date field"
}
test_set_post_datetime_from_filename() {
. "$project_dir/lib/metadata.sh"
set_post_datetime "" "2026-05-20-14-30-slug"
assert_eq "2026-05-20" "$post_date" "post date from filename"
assert_eq "14:30" "$post_time" "post time from filename"
}

93
tests/test_runner.sh Executable file
View File

@@ -0,0 +1,93 @@
#!/bin/sh
script_dir=$(CDPATH="" cd -- "$(dirname -- "$0")" && pwd)
project_dir=$(CDPATH="" cd -- "$script_dir/.." && pwd)
passed=0
failed=0
total=0
assert_eq() {
total=$((total + 1))
expected="$1"
actual="$2"
label="$3"
if [ "$expected" = "$actual" ]; then
passed=$((passed + 1))
printf " PASS: %s\n" "$label"
else
failed=$((failed + 1))
printf " FAIL: %s\n" "$label"
printf " expected: %s\n" "$expected"
printf " actual: %s\n" "$actual"
fi
}
assert_contains() {
total=$((total + 1))
needle="$1"
haystack="$2"
label="$3"
case "$haystack" in
*"$needle"*)
passed=$((passed + 1))
printf " PASS: %s\n" "$label"
;;
*)
failed=$((failed + 1))
printf " FAIL: %s\n" "$label"
printf " expected to contain: %s\n" "$needle"
printf " actual: %s\n" "$haystack"
;;
esac
}
assert_file_exists() {
total=$((total + 1))
path="$1"
label="$2"
if [ -f "$path" ]; then
passed=$((passed + 1))
printf " PASS: %s\n" "$label"
else
failed=$((failed + 1))
printf " FAIL: %s\n" "$label"
printf " file not found: %s\n" "$path"
fi
}
run_test_file() {
test_file="$1"
test_name=$(basename "$test_file" .sh)
printf "\n== %s ==\n" "$test_name"
saved_total=$total
saved_passed=$passed
saved_failed=$failed
. "$test_file"
for func in $(grep '^test_' "$test_file" | sed 's/().*//' | sort -u); do
$func
done
file_total=$((total - saved_total))
file_passed=$((passed - saved_passed))
file_failed=$((failed - saved_failed))
if [ "$file_total" -eq 0 ]; then
printf " (no tests found)\n"
fi
}
for test_file in "$script_dir"/test_*.sh; do
[ "$(basename "$test_file")" = "test_runner.sh" ] && continue
run_test_file "$test_file"
done
printf "\n== Results ==\n"
printf " %d passed, %d failed, %d total\n" "$passed" "$failed" "$total"
[ "$failed" -eq 0 ] && exit 0 || exit 1

88
tests/test_runtime.sh Executable file
View File

@@ -0,0 +1,88 @@
test_url_encode_spaces() {
. "$project_dir/lib/runtime.sh"
result=$(encode_url_path "hello world")
assert_eq "hello%20world" "$result" "encode spaces"
}
test_url_encode_hash() {
. "$project_dir/lib/runtime.sh"
result=$(encode_url_path "file#section")
assert_eq "file%23section" "$result" "encode hash"
}
test_url_encode_question() {
. "$project_dir/lib/runtime.sh"
result=$(encode_url_path "search?q=test")
assert_eq "search%3Fq=test" "$result" "encode question mark"
}
test_url_encode_percent() {
. "$project_dir/lib/runtime.sh"
result=$(encode_url_path "100%")
assert_eq "100%25" "$result" "encode percent"
}
test_markdown_file_url() {
. "$project_dir/lib/runtime.sh"
result=$(markdown_file_url "docs/readme.md")
assert_eq "/docs/readme.html" "$result" "markdown file url"
}
test_directory_index_url_root() {
. "$project_dir/lib/runtime.sh"
result=$(directory_index_url "")
assert_eq "/index.html" "$result" "root index url"
}
test_directory_index_url_subdir() {
. "$project_dir/lib/runtime.sh"
result=$(directory_index_url "docs")
assert_eq "/docs/index.html" "$result" "subdir index url"
}
test_directory_index_url_dot() {
. "$project_dir/lib/runtime.sh"
result=$(directory_index_url ".")
assert_eq "/index.html" "$result" "dot index url"
}
test_format_rfc2822_utc() {
. "$project_dir/lib/runtime.sh"
result=$(format_rfc2822_utc "2026-05-20" "14:30")
assert_eq "Wed, 20 May 2026 14:30:00 +0000" "$result" "rfc2822 format"
}
test_format_rfc2822_utc_default_time() {
. "$project_dir/lib/runtime.sh"
result=$(format_rfc2822_utc "2026-01-01")
assert_eq "Thu, 01 Jan 2026 00:00:00 +0000" "$result" "rfc2822 default time"
}
test_trim_whitespace() {
. "$project_dir/lib/runtime.sh"
result=$(trim_whitespace " hello ")
assert_eq "hello" "$result" "trim whitespace"
}
test_trim_whitespace_tabs() {
. "$project_dir/lib/runtime.sh"
result=$(trim_whitespace " world ")
assert_eq "world" "$result" "trim tabs"
}
test_is_posts_directory_rel() {
. "$project_dir/lib/config.sh"
. "$project_dir/lib/runtime.sh"
posts_dir="posts"
result=$(is_posts_directory_rel "posts" && echo "true" || echo "false")
assert_eq "true" "$result" "posts dir match"
result=$(is_posts_directory_rel "blog" && echo "true" || echo "false")
assert_eq "false" "$result" "posts dir no match"
posts_dir="./posts"
result=$(is_posts_directory_rel "posts" && echo "true" || echo "false")
assert_eq "true" "$result" "posts dir with dot prefix config"
}

View File

@@ -17,10 +17,10 @@ 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"
LC_ALL=C sed '1,/^#==PAYLOAD==$/d' "$0" | tar -xz -C "$tmpdir"
# Pass control to the extracted script
KEWT_INVOKED_AS="$0" "$tmpdir/kewt.sh" "$@"
KEWT_INVOKED_AS="$0" sh "$tmpdir/kewt.sh" "$@"
exit $?
#==PAYLOAD==
@@ -28,10 +28,10 @@ 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/"
cp -r "$REPO_ROOT/kewt.sh" "$REPO_ROOT/markdown.sh" "$REPO_ROOT/awk" "$REPO_ROOT/styles" "$REPO_ROOT/lib" "$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"
tar -cz -C "$tmpbuild" kewt.sh markdown.sh awk styles lib >> "$OUT_FILE"
rm -rf "$tmpbuild"
chmod +x "$OUT_FILE"