76 Commits

Author SHA1 Message Date
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
ef16ed4c88 feat: frontmatter
Some checks failed
Lint / shellcheck (push) Successful in 53s
Release Standalone Builder / publish-aur (release) Successful in 36s
Release Standalone Builder / publish-homebrew (release) Failing after 6s
Release Standalone Builder / build (release) Successful in 34s
2026-03-23 11:39:05 +01:00
30b7681234 Update index.md
All checks were successful
Lint / shellcheck (push) Successful in 19s
2026-03-22 07:24:36 +01:00
13b6106efd docs: bpkg
All checks were successful
Lint / shellcheck (push) Successful in 20s
2026-03-22 07:23:24 +01:00
831b081fc7 docs: new contribution instructions
All checks were successful
Lint / shellcheck (push) Successful in 19s
2026-03-21 16:10:33 +01:00
fde423a32b docs: brew
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-20 09:36:53 +01:00
55a82f75a9 fix: link in homebrew
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-20 09:33:45 +01:00
f85abd43c4 fix: brew
Some checks failed
Lint / shellcheck (push) Has been cancelled
Release Standalone Builder / build (release) Successful in 30s
Release Standalone Builder / publish-aur (release) Successful in 32s
Release Standalone Builder / publish-homebrew (release) Successful in 6s
2026-03-20 09:26:20 +01:00
0f66ebf52a dist: brew
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-20 09:23:43 +01:00
55a515ccd5 dist: bpkg preparation
All checks were successful
Lint / shellcheck (push) Successful in 18s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Successful in 34s
2026-03-20 08:46:13 +01:00
de8cbefb8e feat: task lists
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-20 08:32:28 +01:00
cc7fee573f feat: custom admonitions 2026-03-20 08:31:58 +01:00
137be9579a feat: incremental rebuilds 2026-03-20 08:30:25 +01:00
5afd0170e5 fix: less tmp file spam 2026-03-20 08:29:05 +01:00
5a2053cfb4 fix: publish-aur-git shoudln't run so often now
All checks were successful
Lint / shellcheck (push) Successful in 19s
2026-03-19 22:34:31 +01:00
2fc3d6fc6f fix: AUR links
All checks were successful
Lint / shellcheck (push) Successful in 18s
Publish kewt-git to AUR / publish-aur-git (push) Successful in 29s
Release Standalone Builder / build (release) Successful in 30s
Release Standalone Builder / publish-aur (release) Successful in 44s
2026-03-19 21:50:34 +01:00
c5a9355871 fix: config slashes
All checks were successful
Lint / shellcheck (push) Successful in 18s
Publish kewt-git to AUR / publish-aur-git (push) Successful in 30s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Successful in 34s
2026-03-19 21:45:38 +01:00
b8cd129c47 fix: rename to later in the alphabet, just in case
All checks were successful
Lint / shellcheck (push) Successful in 17s
Publish kewt-git to AUR / publish-aur-git (push) Successful in 33s
2026-03-19 21:29:22 +01:00
5e033a65e7 dist: AUR updates and upgrades
All checks were successful
Publish kewt-git to AUR / publish-aur-git (push) Successful in 34s
Lint / shellcheck (push) Successful in 20s
2026-03-19 21:26:17 +01:00
5bf2e2abe5 Update LICENSE
All checks were successful
Lint / shellcheck (push) Successful in 20s
2026-03-19 16:27:56 +01:00
3a2056ff8f Update site/site.conf
All checks were successful
Lint / shellcheck (push) Successful in 20s
2026-03-19 16:20:52 +01:00
fd829a3f22 branding: icon
All checks were successful
Lint / shellcheck (push) Successful in 19s
2026-03-19 16:12:21 +01:00
bad02decba docs: add contributing instructions
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-19 15:58:41 +01:00
2aef6ec4a1 Update site/site.conf
All checks were successful
Lint / shellcheck (push) Successful in 19s
2026-03-19 15:40:39 +01:00
78eac182dc dist: github release descriptions
All checks were successful
Lint / shellcheck (push) Successful in 19s
Release Standalone Builder / build (release) Successful in 30s
Release Standalone Builder / publish-aur (release) Successful in 33s
2026-03-19 15:37:55 +01:00
b7382a20ab feat: new default style and more
All checks were successful
Lint / shellcheck (push) Successful in 20s
2026-03-19 15:35:14 +01:00
d8cf07ee2a docs: move to only be on the website
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-19 15:25:01 +01:00
76e2ae0117 make automatic filesnames not use : for better windows support ig
All checks were successful
Lint / shellcheck (push) Successful in 20s
2026-03-19 09:35:14 +01:00
2e331b5d9a fix: to the fix, clap your hands together, caramelldanse o-o-or whatever
All checks were successful
Lint / shellcheck (push) Successful in 17s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Successful in 35s
2026-03-18 22:26:32 +01:00
9f5d1089a2 feat: --version, fix: --post fixes
All checks were successful
Lint / shellcheck (push) Successful in 19s
2026-03-18 22:18:13 +01:00
9ccba8fd4e feat/fix: Site.conf now uses escaped " so it's more conventional
All checks were successful
Lint / shellcheck (push) Successful in 19s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Successful in 36s
2026-03-18 08:49:41 +01:00
95679abd85 docs: update bianry download url
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-17 13:54:13 +01:00
8b1e793510 docs: new features
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-17 12:56:15 +01:00
19f96553d9 feat: RSS!!!!!!!!!!
All checks were successful
Lint / shellcheck (push) Successful in 17s
Release Standalone Builder / build (release) Successful in 32s
Release Standalone Builder / publish-aur (release) Successful in 33s
2026-03-17 12:46:29 +01:00
1a7525a857 feat: config updating
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-17 11:53:15 +01:00
e7d90d18e8 feat: sitemap 2026-03-17 11:37:17 +01:00
4019d2721d Header links 2026-03-17 11:28:47 +01:00
b58604a4cf feat/docs: LICENSE
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-17 11:17:26 +01:00
99e805b180 fix: Title formatting parsing fix
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-17 02:18:42 +01:00
62075dea4a docs: forgot
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-17 02:07:52 +01:00
7afd041e53 functionality: Dynamic page titles, versioning, automatic 404 page
All checks were successful
Lint / shellcheck (push) Successful in 17s
Release Standalone Builder / build (release) Successful in 32s
Release Standalone Builder / publish-aur (release) Successful in 33s
2026-03-17 01:55:32 +01:00
64d08a0de3 functionality: BetterHelp
All checks were successful
Lint / shellcheck (push) Successful in 19s
Release Standalone Builder / build (release) Successful in 32s
Release Standalone Builder / publish-aur (release) Successful in 35s
2026-03-16 10:33:21 +01:00
dd18bc3367 docs: update readme
All checks were successful
Lint / shellcheck (push) Successful in 39s
2026-03-16 09:33:45 +01:00
86 changed files with 5127 additions and 848 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,42 @@
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 -az --delete \
-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

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

View File

@@ -3,6 +3,7 @@ name: Release Standalone Builder
on: on:
release: release:
types: [published] types: [published]
workflow_dispatch:
jobs: jobs:
build: build:
@@ -32,12 +33,24 @@ jobs:
run: | run: |
TAG="${GITHUB_REF#refs/tags/}" 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 # Create the release on GitHub
curl -sL -X POST \ curl -sL -X POST \
-H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \ -H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \
-H "Accept: application/vnd.github+json" \ -H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/n0va-bot/kewt/releases" \ "https://api.github.com/repos/n0va-bot/kewt/releases" \
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\",\"draft\":false,\"prerelease\":false}" || true -d "$PAYLOAD" || true
# Get the release ID # Get the release ID
RELEASE_ID=$(curl -sL \ RELEASE_ID=$(curl -sL \
@@ -78,25 +91,9 @@ jobs:
-e "s/SHA256SUM_PLACEHOLDER/${CHECKSUM}/g" \ -e "s/SHA256SUM_PLACEHOLDER/${CHECKSUM}/g" \
packaging/AUR/PKGBUILD.template > aur-work/PKGBUILD packaging/AUR/PKGBUILD.template > aur-work/PKGBUILD
cat > aur-work/.SRCINFO << SRCEOF sed -e "s/VERSION_PLACEHOLDER/${VERSION}/g" \
pkgbase = kewt-bin -e "s/SHA256SUM_PLACEHOLDER/${CHECKSUM}/g" \
pkgdesc = A minimalist, 100% POSIX, static site generator inspired by werc and kew packaging/AUR/.SRCINFO.template > aur-work/.SRCINFO
pkgver = ${VERSION}
pkgrel = 1
url = https://git.krzak.org/N0VA/kewt
arch = any
license = MIT
depends = sh
provides = kewt
conflicts = kewt
conflicts = kewt-git
source = kewt-bin-${VERSION}.sh::https://git.krzak.org/N0VA/kewt/releases/download/v${VERSION}/kewt
sha256sums = ${CHECKSUM}
pkgname = kewt-bin
SRCEOF
# Remove leading whitespace from heredoc
sed -i 's/^ //' aur-work/.SRCINFO
- name: Publish to AUR - name: Publish to AUR
uses: KSXGitHub/github-actions-deploy-aur@v3.0.1 uses: KSXGitHub/github-actions-deploy-aur@v3.0.1
@@ -107,3 +104,34 @@ jobs:
commit_email: ${{ github.actor }}@users.noreply.github.com commit_email: ${{ github.actor }}@users.noreply.github.com
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
commit_message: "Update kewt-bin to ${{ github.ref_name }}" commit_message: "Update kewt-bin to ${{ github.ref_name }}"
publish-homebrew:
runs-on: local
needs: build
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Render Formula and push to GitHub
run: |
TAG="${GITHUB_REF#refs/tags/}"
VERSION="${TAG#v}"
curl -sL -o kewt-binary \
"https://git.krzak.org/N0VA/kewt/releases/download/${TAG}/kewt"
CHECKSUM=$(sha256sum kewt-binary | awk '{print $1}')
rm -f kewt-binary
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
[ -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 -u origin main

34
LICENSE Normal file
View File

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

23
Makefile Normal file
View File

@@ -0,0 +1,23 @@
PREFIX ?= /usr/local
BINDIR = $(PREFIX)/bin
ZSHCOMPDIR ?= $(PREFIX)/share/zsh/site-functions
all: kewt
kewt:
./tools/build-standalone.sh
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
uninstall:
rm -f $(DESTDIR)$(BINDIR)/kewt
rm -f $(DESTDIR)$(ZSHCOMPDIR)/_kewt
clean:
rm -f kewt
.PHONY: all install uninstall clean

106
README.md
View File

@@ -1,107 +1,21 @@
# _kewt_ # ![kewt](/icon.svg)
### Pronounced "cute" ### Pronounced "cute"
***
# [Go to the website](https://kewt.krzak.org) # [Go to the website](https://kewt.krzak.org)
***
_kewt_ is a minimalist ssg inspired by _[werc](http://werc.cat-v.org/)_ and _[kew](https://github.com/uint23/kew)_ _kewt_ is a minimalist ssg inspired by _[werc](http://werc.cat-v.org/)_ and _[kew](https://github.com/uint23/kew)_
It's meant to be a static site generator, like _[kew](https://github.com/uint23/kew)_ but use only default (POSIX) tooling, like _[werc](http://werc.cat-v.org/)_ (and definitely unlike _[kew](https://github.com/uint23/kew)_)
## Features ## [Installation](https://kewt.krzak.org/docs/installation)
- No dependencies ## Contributing
- Supports many embed types
- Automatic css variable replacement for older browsers
- Automatic inlining and embedding of many filetypes with `\![link]` or `\![alt](link)`
- Inline html support
- MFM `$font` and `\<plain>` tags
- Admonition support (that's what the blocks like the warning block below are called)
If you want to **force** a file to be inlined, use `\!![]` instead of `\![]` Either open an issue or pull request on the **home** repository ([N0VA/kewt](https://git.krzak.org/N0VA/kewt)) or message me on my email address ([n0va@krzak.org](mailto:n0va@krzak.org?subject=%5Bkewt%5D%20something)) with the subjectline being `[kewt] something`.
## Installation ## License
You can clone the repository to use `kewt.sh` directly, or you can download the standalone executable, which bundles all dependencies into a single file: ISC
```sh
curl -L -o kewt https://git.krzak.org/N0VA/kewt/releases/latest/download/kewt
chmod +x kewt
```
On Arch Linux, _kewt_ is available on the 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
## Usage
```sh
./kewt.sh --help
./kewt.sh --new [title]
./kewt.sh --from <src> --to <out>
./kewt.sh [src] [out]
```
`--new [title]` creates a new site directory with a copied `site.conf` and a default `index.md`.
## site.conf
```conf
title = "kewt"
style = "kewt"
dir_indexes = true
single_file_index = true
flatten = false
order = ""
home_name = "Home"
show_home_in_nav = true
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 = ""
```
- `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)
## Ignores
- `.kewtignore`: Files/directories to ignore. If empty, the whole directory gets ignored
- `.kewthide`: Files/directories to hide from navigation but still process. Same empty rules as with ignore
- `.kewtpreserve`: Files/directories to copy but not convert markdown to html. Same empty rules again
## Embeds
- `\![link]`:
- local image/audio/video files are embedded as media tags
- local text/code files are inlined directly
- global image/audio/video links are embedded as media tags
- other global links are embedded as `<iframe>`
- `\![alt](link)` works the same, with `alt` used for images
- `\!![]` and `\!![alt](link)` force inline local file contents
## Credits
- Markdown to html conversion based on [markdown.bash](https://github.com/chadbraunduin/markdown.bash) by [chadbraunduin](https://github.com/chadbraunduin)
- Default css style and html template based on _[kew](https://github.com/uint23/kew)_ by [uint23](https://github.com/uint23)
>[!WARNING]
>Most of this was coded at night, while sleepy and a bit sick, and after walking for about 4 hours around a forest, so...

View File

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

View File

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

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: 💤

View File

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

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>"
}
}

46
awk/frontmatter.awk Normal file
View File

@@ -0,0 +1,46 @@
BEGIN {
state = "start"
}
{
if (state == "start") {
if ($0 == "---") {
state = "in_fm"
next
} else {
state = "body"
print
next
}
}
if (state == "in_fm") {
if ($0 == "---") {
state = "body"
next
}
line = $0
if (line ~ /^[[:space:]]*$/ || line ~ /^[[:space:]]*#/) next
if (line !~ /=/) next
key = line
val = line
sub(/=.*/, "", key)
sub(/[^=]*=/, "", val)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", val)
if (val ~ /^".*"$/) {
val = substr(val, 2, length(val) - 2)
gsub(/\\"/, "\"", val)
} else if (val ~ /^'.*'$/) {
val = substr(val, 2, length(val) - 2)
gsub(/\\'/, "'", val)
}
if (fm_out != "") {
print key "=" val >> fm_out
}
next
}
print
}

View File

@@ -3,6 +3,55 @@ function title_from_name(name) {
gsub(/-/, " ", name) gsub(/-/, " ", name)
return 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) { function compare_paths(p1, p2, parts1, parts2, n1, n2, i, name1, name2, lname1, lname2, w1, w2) {
n1 = split(p1, parts1, "/") n1 = split(p1, parts1, "/")
@@ -33,6 +82,13 @@ function compare_paths(p1, p2, parts1, parts2, n1, n2, i, name1, name2, lname
} }
BEGIN { BEGIN {
src = ENVIRON["AWK_SRC"]
single_file_index = ENVIRON["AWK_SINGLE_FILE_INDEX"]
flatten = ENVIRON["AWK_FLATTEN"]
order = ENVIRON["AWK_ORDER"]
home_name = ENVIRON["AWK_HOME_NAME"]
show_home_in_nav = ENVIRON["AWK_SHOW_HOME_IN_NAV"]
dinfo = ENVIRON["AWK_DINFO"]
n_dlines = split(dinfo, dlines, "\n") n_dlines = split(dinfo, dlines, "\n")
for (i = 1; i <= n_dlines; i++) { for (i = 1; i <= n_dlines; i++) {
if (split(dlines[i], dparts, "|") == 3) { if (split(dlines[i], dparts, "|") == 3) {
@@ -125,7 +181,7 @@ END {
continue 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 opened_levels[++depth] = i
} }
@@ -136,9 +192,9 @@ END {
is_single = (single_file_index == "true" && md_count[curr_dir] == 1 && !has_index[curr_dir]) is_single = (single_file_index == "true" && md_count[curr_dir] == 1 && !has_index[curr_dir])
if (parts[n] != "index.md" && !is_single) { if (parts[n] != "index.md" && !is_single) {
path = "/" rel path = url_encode_path("/" rel)
gsub(/\.md$/, ".html", path) 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 prev_n = n

View File

@@ -3,23 +3,45 @@ function strip_markdown(s) {
gsub(/[*_`~]/, "", s) gsub(/[*_`~]/, "", s)
gsub(/[\[\]]/, "", s) gsub(/[\[\]]/, "", s)
gsub(/\([^\)]*\)/, "", s) gsub(/\([^\)]*\)/, "", s)
s = tolower(s)
gsub(/[^a-z0-9 -]/, "", s)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", s) gsub(/^[[:space:]]+|[[:space:]]+$/, "", s)
gsub(/[[:space:]]+/, "-", s) gsub(/[[:space:]]+/, "-", s)
gsub(/-{2,}/, "-", s)
gsub(/^-+|-+$/, "", s)
if (length(s) > 80) s = substr(s, 1, 80)
gsub(/-+$/, "", s)
return 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 print_heading(tag, line, id) {
id = unique_id(strip_markdown(line))
if (enable_header_links == "true") {
print "<" tag " id=\"" id "\">" line " <a href=\"#" id "\" class=\"header-anchor\" aria-label=\"Link to this section\">#</a></" tag ">"
} else {
print "<" tag " id=\"" id "\">" line "</" tag ">"
}
}
function print_header(line) { function print_header(line) {
if (line ~ /^# /) { tag = ""
sub(/^# /, "", line); print "<h1 id=\"" strip_markdown(line) "\">" line "</h1>" if (line ~ /^# /) { tag = "h1"; sub(/^# /, "", line) }
} else if (line ~ /^## /) { else if (line ~ /^## /) { tag = "h2"; sub(/^## /, "", line) }
sub(/^## /, "", line); print "<h2 id=\"" strip_markdown(line) "\">" line "</h2>" else if (line ~ /^### /) { tag = "h3"; sub(/^### /, "", line) }
} else if (line ~ /^### /) { else if (line ~ /^#### /) { tag = "h4"; sub(/^#### /, "", line) }
sub(/^### /, "", line); print "<h3 id=\"" strip_markdown(line) "\">" line "</h3>" else if (line ~ /^##### /) { tag = "h5"; sub(/^##### /, "", line) }
} else if (line ~ /^#### /) { else if (line ~ /^###### /) { tag = "h6"; sub(/^###### /, "", line) }
sub(/^#### /, "", line); print "<h4 id=\"" strip_markdown(line) "\">" line "</h4>"
} else if (line ~ /^##### /) { if (tag != "") {
sub(/^##### /, "", line); print "<h5 id=\"" strip_markdown(line) "\">" line "</h5>" print_heading(tag, line)
} else if (line ~ /^###### /) {
sub(/^###### /, "", line); print "<h6 id=\"" strip_markdown(line) "\">" line "</h6>"
} else { } else {
print line print line
} }
@@ -29,7 +51,7 @@ BEGIN {
in_pre = 0 in_pre = 0
} }
{ {
if ($0 ~ /^<pre><code>/) { if ($0 ~ /^<pre><code/) {
in_pre = 1 in_pre = 1
if (has_prev && prev != "") { print_header(prev); has_prev = 0 } if (has_prev && prev != "") { print_header(prev); has_prev = 0 }
print print
@@ -43,7 +65,7 @@ BEGIN {
if ($0 ~ /^=+$/) { if ($0 ~ /^=+$/) {
if (has_prev && prev != "" && prev !~ /^<[a-z]/) { if (has_prev && prev != "" && prev !~ /^<[a-z]/) {
print "<h1 id=\"" strip_markdown(prev) "\">" prev "</h1>" print_heading("h1", prev)
has_prev = 0 has_prev = 0
} else { } else {
if (has_prev) print_header(prev) if (has_prev) print_header(prev)
@@ -52,7 +74,7 @@ BEGIN {
} }
} else if ($0 ~ /^-+$/) { } else if ($0 ~ /^-+$/) {
if (has_prev && prev != "" && prev !~ /^<[a-z]/) { if (has_prev && prev != "" && prev !~ /^<[a-z]/) {
print "<h2 id=\"" strip_markdown(prev) "\">" prev "</h2>" print_heading("h2", prev)
has_prev = 0 has_prev = 0
} else { } else {
if (has_prev) print_header(prev) if (has_prev) print_header(prev)

View File

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

View File

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

View File

@@ -97,6 +97,23 @@ function read_file(path, out, line, rc) {
return out 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) { function escape_html(s, t) {
t = s t = s
gsub(/&/, "\\&amp;", t) 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)) { if (force_inline && !is_global_url(src)) {
local_path = resolve_local_path(src) local_path = resolve_local_path(src)
if (local_path != "") { 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) if (content ~ /\n$/) sub(/\n$/, "", content)
return content return content
} }
@@ -204,7 +222,7 @@ function render_embed(src, alt, has_alt, force_inline, ext, local_path, conte
} }
if (is_audio_ext(ext)) return "<audio controls src=\"" src "\"></audio>" if (is_audio_ext(ext)) return "<audio controls src=\"" src "\"></audio>"
if (is_video_ext(ext)) return "<video controls src=\"" src "\"></video>" if (is_video_ext(ext)) return "<video controls src=\"" src "\"></video>"
return "<iframe src=\"" src "\"></iframe>" return "<iframe src=\"" src "\" allowfullscreen></iframe>"
} }
if (is_image_ext(ext)) { if (is_image_ext(ext)) {
@@ -217,13 +235,35 @@ function render_embed(src, alt, has_alt, force_inline, ext, local_path, conte
if (is_inline_text_ext(ext)) { if (is_inline_text_ext(ext)) {
local_path = resolve_local_path(src) local_path = resolve_local_path(src)
if (local_path != "") { if (local_path != "") {
content = read_file(local_path) content = read_file_or_render_md(local_path, ext)
if (content ~ /\n$/) sub(/\n$/, "", content) if (content ~ /\n$/) sub(/\n$/, "", content)
return content return content
} }
} }
return "<iframe src=\"" src "\"></iframe>" return "<iframe src=\"" src "\" allowfullscreen></iframe>"
}
function render_typed_embed(etype, src, alt, has_alt, local_path, content) {
if (etype == "i") {
if (has_alt) return "<img alt=\"" alt "\" src=\"" src "\" />"
return "<img src=\"" src "\" />"
}
if (etype == "v") return "<video controls src=\"" src "\"></video>"
if (etype == "a") return "<audio controls src=\"" src "\"></audio>"
if (etype == "f") return "<iframe src=\"" src "\" allowfullscreen></iframe>"
if (etype == "e") {
if (!is_global_url(src)) {
local_path = resolve_local_path(src)
if (local_path != "") {
content = read_file_or_render_md(local_path, ext_of(src))
if (content ~ /\n$/) sub(/\n$/, "", content)
return content
}
}
return render_embed(src, alt, has_alt, 1)
}
return render_embed(src, alt, has_alt, 0)
} }
function extract_attr(tag, attr, pat, m, token) { function extract_attr(tag, attr, pat, m, token) {
@@ -319,7 +359,7 @@ function apply_td_vertical_align(line, out, rest, seg, td_tag, img_tag, after
return out rest return out rest
} }
function rewrite_img_tags(line, out, rest, tag, src, alt, force_inline_tag, pre, post, repl) { function rewrite_img_tags(line, out, rest, tag, src, alt, force_inline_tag, embed_type, pre, post, repl) {
out = "" out = ""
rest = line rest = line
while (match(rest, /<img[^>]*\/?>/)) { while (match(rest, /<img[^>]*\/?>/)) {
@@ -329,9 +369,19 @@ function rewrite_img_tags(line, out, rest, tag, src, alt, force_inline_tag, p
src = extract_attr(tag, "src") src = extract_attr(tag, "src")
alt = extract_attr(tag, "alt") alt = extract_attr(tag, "alt")
force_inline_tag = extract_attr(tag, "data-force-inline") force_inline_tag = extract_attr(tag, "data-force-inline")
if (is_image_ext(ext_of(src)) && force_inline_tag == "") { embed_type = extract_attr(tag, "data-embed-type")
if (embed_type != "") {
repl = render_typed_embed(embed_type, src, alt, (alt != ""))
} else if (is_image_ext(ext_of(src)) && force_inline_tag == "") {
# Preserve hand-written <img> attributes (style/class/etc) for normal images. # Preserve hand-written <img> attributes (style/class/etc) for normal images.
repl = tag 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 { } else {
repl = render_embed(src, alt, (alt != ""), (force_inline_tag != "")) repl = render_embed(src, alt, (alt != ""), (force_inline_tag != ""))
} }
@@ -356,7 +406,12 @@ function rewrite_double_bang_with_parens(line, out, rest, token, inside, src,
src = substr(inside, sep + 2) src = substr(inside, sep + 2)
sub(/\)$/, "", src) 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 out = out pre repl
rest = post rest = post
} }
@@ -373,7 +428,12 @@ function rewrite_double_bang_bare(line, out, rest, token, src, pre, post, rep
src = token src = token
sub(/^!!\[/, "", src) sub(/^!!\[/, "", src)
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 out = out pre repl
rest = post rest = post
} }
@@ -533,6 +593,75 @@ function restore_plain_markers(line) {
return 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 { BEGIN {
input_dir = dirname_of(input_file) input_dir = dirname_of(input_file)
in_pre_code = 0 in_pre_code = 0
@@ -558,6 +687,9 @@ BEGIN {
line = apply_td_vertical_align(line) line = apply_td_vertical_align(line)
line = restore_plain_markers(line) line = restore_plain_markers(line)
if (!(in_pre_code || start_pre)) {
line = break_code_double_bang(line)
}
print line print line
if (start_pre && !end_pre) { if (start_pre && !end_pre) {

View File

@@ -20,9 +20,11 @@ function mask_html_tags(s, out, rest, start, len, tag, token) {
return out rest return out rest
} }
function restore_html_tags(s, i) { function restore_html_tags(s, i, val) {
for (i = 1; i <= html_tag_count; i++) { for (i = 1; i <= html_tag_count; i++) {
gsub(html_tag_token[i], html_tag_value[i], s) val = html_tag_value[i]
gsub(/&/, "\\\\&", val)
gsub(html_tag_token[i], val, s)
} }
return s return s
} }
@@ -58,6 +60,36 @@ function restore_html_tags(s, i) {
line = substr(line, 1, start - 1) repl substr(line, start + len) line = substr(line, 1, start - 1) repl substr(line, start + len)
} }
# typed embeds: !i, !v, !a, !f, !e
while (match(line, /![ivafe]\[[^\]]*\]\([^\)]+ "[^"]*"\)/)) {
start = RSTART; len = RLENGTH
token = substr(line, start, len)
etype = substr(token, 2, 1)
match(token, /\[[^\]]*\]/); alt = substr(token, RSTART + 1, RLENGTH - 2)
match(token, /"[^"]*"/); etitle = substr(token, RSTART + 1, RLENGTH - 2)
match(token, /\([^\)]+/); inner = substr(token, RSTART + 1, RLENGTH - 1)
sub(/[[:space:]]*"[^"]*"/, "", inner); src = inner
repl = "<img data-embed-type=\"" etype "\" alt=\"" alt "\" src=\"" src "\" title=\"" etitle "\" />"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
while (match(line, /![ivafe]\[[^\]]*\]\([^\)]+\)/)) {
start = RSTART; len = RLENGTH
token = substr(line, start, len)
etype = substr(token, 2, 1)
match(token, /\[[^\]]*\]/); alt = substr(token, RSTART + 1, RLENGTH - 2)
match(token, /\([^\)]+/); src = substr(token, RSTART + 1, RLENGTH - 1)
repl = "<img data-embed-type=\"" etype "\" alt=\"" alt "\" src=\"" src "\" />"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
while (match(line, /![ivafe]\[[^\]]+\]/)) {
start = RSTART; len = RLENGTH
token = substr(line, start, len)
etype = substr(token, 2, 1)
src = substr(token, 4, len - 4)
repl = "<img data-embed-type=\"" etype "\" src=\"" src "\" />"
line = substr(line, 1, start - 1) repl substr(line, start + len)
}
# force-inline image syntax (double bang) # force-inline image syntax (double bang)
while (match(line, /!!\[[^\]]*\]\([^\)]+ "[^"]*"\)/)) { while (match(line, /!!\[[^\]]*\]\([^\)]+ "[^"]*"\)/)) {
start = RSTART; len = RLENGTH start = RSTART; len = RLENGTH
@@ -198,5 +230,7 @@ function restore_html_tags(s, i) {
} }
} }
gsub(/<a href="https?:\/\/[^"]*"/, "& rel=\"noopener noreferrer\"", line)
print line print line
} }

View File

@@ -46,14 +46,9 @@ function mask(s, t) {
while (match(substr(line, p), /`+/)) { while (match(substr(line, p), /`+/)) {
pstart = p + RSTART - 1 pstart = p + RSTART - 1
plen = RLENGTH plen = RLENGTH
if (plen >= 3) {
out = out substr(line, p, pstart - p + plen)
p = pstart + plen
continue
}
# Found 1 or 2 backticks at pstart # Found backtick sequence at pstart
# Search for closing marker # Search for closing marker of same length
marker = substr(line, pstart, plen) marker = substr(line, pstart, plen)
tail = substr(line, pstart + plen) tail = substr(line, pstart + plen)
mpos = index(tail, marker) mpos = index(tail, marker)
@@ -69,9 +64,20 @@ function mask(s, t) {
# Found match! # Found match!
content = substr(tail, 1, mpos - 1) content = substr(tail, 1, mpos - 1)
out = out substr(line, p, pstart - p) 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) 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>" out = out "<code>" mask(content) "</code>"
p = pstart + plen + mpos + plen - 1 p = pstart + plen + mpos + plen - 1
} else { } else {

View File

@@ -22,7 +22,7 @@ function mask_plain(s, t) {
gsub(/\$/, "\034P8\034", t) gsub(/\$/, "\034P8\034", t)
return t return t
} }
BEGIN { in_plain = 0 } BEGIN { in_plain = 0; in_script_style = 0 }
{ {
line = $0 line = $0
out = "" out = ""
@@ -48,5 +48,41 @@ BEGIN { in_plain = 0 }
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_pre) {
if (in_p) { print "</p>"; in_p = 0 } if (in_p) { print "</p>"; in_p = 0 }
@@ -13,7 +13,16 @@ BEGIN {
next 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) { if (in_p) {
print "</p>" print "</p>"
in_p = 0 in_p = 0

View File

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

View File

@@ -1,19 +1,41 @@
function replace_all(text, token, value, pos, token_len) { function replace_all(text, token, value, pos, token_len, res) {
token_len = length(token) token_len = length(token)
res = ""
while ((pos = index(text, token)) > 0) { while ((pos = index(text, token)) > 0) {
text = substr(text, 1, pos - 1) value substr(text, pos + token_len) res = res substr(text, 1, pos - 1) value
text = substr(text, pos + token_len)
}
return res text
}
BEGIN {
current_url = ENVIRON["AWK_CURRENT_URL"]
nav = ENVIRON["AWK_NAV"]
title = ENVIRON["AWK_TITLE"]
footer = ENVIRON["AWK_FOOTER"]
style_path = ENVIRON["AWK_STYLE_PATH"]
head_extra = ENVIRON["AWK_HEAD_EXTRA"]
header_brand = ENVIRON["AWK_HEADER_BRAND"]
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\"")
} }
return text
} }
{ {
line = $0 line = $0
line = replace_all(line, "{{TITLE}}", title) line = replace_all(line, "{{TITLE}}", title)
line = replace_all(line, "{{LANG}}", lang)
line = replace_all(line, "{{NAV}}", nav) line = replace_all(line, "{{NAV}}", nav)
line = replace_all(line, "{{FOOTER}}", footer) line = replace_all(line, "{{FOOTER}}", footer)
line = replace_all(line, "{{CSS}}", style_path) line = replace_all(line, "{{CSS}}", style_path)
line = replace_all(line, "{{HEAD_EXTRA}}", head_extra) line = replace_all(line, "{{HEAD_EXTRA}}", head_extra)
line = replace_all(line, "{{HEADER_BRAND}}", header_brand) 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}}") pos = index(line, "{{CONTENT}}")
if (pos > 0) { 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]
}
}

View File

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

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

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

21
icon.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 639 B

645
kewt.sh
View File

@@ -5,105 +5,46 @@ die() {
exit 1 exit 1
} }
usage() { make_temp_dir() {
invoked_as="${KEWT_INVOKED_AS:-$0}" _temp_base="${TMPDIR:-/tmp}/kewt_run.$$"
cat <<EOF _temp_try=0
Usage: $invoked_as [--from <src>] [--to <out>] while [ "$_temp_try" -lt 1000 ]; do
$invoked_as [src] [out] _temp_path="${_temp_base}.${_temp_try}"
$invoked_as --new [title] if (umask 077 && mkdir "$_temp_path") 2>/dev/null; then
$invoked_as --help printf '%s\n' "$_temp_path"
return 0
Options: fi
--help Show this help message. _temp_try=$((_temp_try + 1))
--new [title] Create a new site directory (default: site) done
--from <src> Source directory (default: site) return 1
--to <out> Output directory (default: out)
EOF
} }
script_dir=$(CDPATH="" cd -- "$(dirname -- "$0")" && pwd) script_dir=$(CDPATH="" cd -- "$(dirname -- "$0")" && pwd)
awk_dir="$script_dir/awk" awk_dir="$script_dir/awk"
KEWT_TMPDIR=$(mktemp -d "/tmp/kewt_run.XXXXXX") KEWT_TMPDIR=$(make_temp_dir) || die "Could not create temporary directory."
trap 'rm -rf "$KEWT_TMPDIR"' EXIT HUP INT TERM trap 'rm -rf "$KEWT_TMPDIR"' EXIT
trap 'exit 0' HUP INT TERM
ensure_root_defaults() {
if [ ! -f "./site.conf" ]; then
cat > "./site.conf" <<'EOF'
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 = ""
EOF
fi
if [ ! -f "./template.html" ]; then
cat > "./template.html" <<'EOF'
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>{{TITLE}}</title>
<link rel="stylesheet" href="{{CSS}}" type="text/css" />
{{HEAD_EXTRA}}
</head>
<body>
<header>
<h1>{{HEADER_BRAND}}</h1>
</header>
<nav id="side-bar">{{NAV}}</nav>
<article>{{CONTENT}}</article>
<footer>{{FOOTER}}</footer>
</body>
</html>
EOF
fi
}
create_new_site() {
new_title="$1"
new_dir="site"
[ -n "$new_title" ] && new_dir="$new_title"
[ -e "$new_dir" ] && die "Target '$new_dir' already exists."
ensure_root_defaults
mkdir -p "$new_dir"
cp "./site.conf" "$new_dir/site.conf"
printf "# _kewt_ website\n" > "$new_dir/index.md"
if [ -n "$new_title" ]; then
awk -v new_title="$new_title" -f "$awk_dir/update_site_conf.awk" "$new_dir/site.conf" > "$new_dir/site.conf.tmp" && mv "$new_dir/site.conf.tmp" "$new_dir/site.conf"
fi
echo "Created new site at '$new_dir'."
exit 0
}
. "$script_dir/lib/config.sh"
. "$script_dir/lib/metadata.sh"
. "$script_dir/lib/manifest.sh"
. "$script_dir/lib/commands.sh"
. "$script_dir/lib/generator.sh"
. "$script_dir/lib/builder.sh"
. "$script_dir/lib/runtime.sh"
src="" src=""
out="" out=""
new_mode="false" new_mode="false"
new_title="" new_title=""
clean_mode="true"
post_mode="false"
post_title=""
positional_count=0 positional_count=0
watch_mode="false"
serve_mode="false"
serve_port=""
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
@@ -111,13 +52,75 @@ while [ $# -gt 0 ]; do
usage usage
exit 0 exit 0
;; ;;
--new) --new|--init)
new_mode="true" new_mode="true"
if [ $# -gt 1 ] && [ "${2#-}" = "$2" ]; then if [ $# -gt 1 ] && [ "${2#-}" = "$2" ]; then
new_title="$2" new_title="$2"
shift shift
fi fi
;; ;;
--no-clean)
clean_mode="false"
;;
--clean)
clean_mode="true"
;;
--version|-v)
echo "kewt version git"
exit 0
;;
--dump-zsh-completions)
cat <<'EOFCOMPS'
#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:'
)
_arguments -S -C $args '*: :_directories'
}
_kewt "$@"
EOFCOMPS
exit 0
;;
--post)
post_mode="true"
if [ $# -gt 1 ] && [ "${2#-}" = "$2" ]; then
post_title="$2"
shift
fi
;;
--generate-template)
generate_template_path="template.html"
if [ $# -gt 1 ] && [ "${2#-}" = "$2" ]; then
generate_template_path="$2"
shift
fi
generate_template "$generate_template_path"
;;
--update)
update_dir="."
if [ $# -gt 1 ] && [ "${2#-}" = "$2" ]; then
update_dir="$2"
shift
fi
update_site "$update_dir"
;;
--from) --from)
[ $# -lt 2 ] && die "--from requires a value." [ $# -lt 2 ] && die "--from requires a value."
src="$2" src="$2"
@@ -128,15 +131,33 @@ while [ $# -gt 0 ]; do
out="$2" out="$2"
shift shift
;; ;;
--watch|-w)
watch_mode="true"
;;
--serve|-s)
serve_mode="true"
if [ $# -ge 2 ] && echo "$2" | grep -qE '^[0-9]+$'; then
serve_port="$2"
shift
fi
;;
--*) --*)
die "Unknown option: $1" die "Unknown option: $1"
;; ;;
*) *)
positional_count=$((positional_count + 1)) positional_count=$((positional_count + 1))
if [ "$positional_count" -eq 1 ]; then if [ "$positional_count" -eq 1 ]; then
if [ -z "$src" ]; then src="$1"; else die "Source already set (use either positional or --from)."; fi if [ -z "$src" ]; then
src="$1"
else
die "Source already set (use either positional or --from)."
fi
elif [ "$positional_count" -eq 2 ]; then elif [ "$positional_count" -eq 2 ]; then
if [ -z "$out" ]; then out="$1"; else die "Output already set (use either positional or --to)."; fi if [ -z "$out" ]; then
out="$1"
else
die "Output already set (use either positional or --to)."
fi
else else
die "Too many positional arguments." die "Too many positional arguments."
fi fi
@@ -147,405 +168,81 @@ done
[ "$new_mode" = "true" ] && create_new_site "$new_title" [ "$new_mode" = "true" ] && create_new_site "$new_title"
ensure_root_defaults if [ -z "$src" ]; then
if [ "$post_mode" = "true" ] && [ -f "./site.conf" ]; then
[ -z "$src" ] && src="site" src="."
else
src="site"
fi
fi
[ -z "$out" ] && out="out" [ -z "$out" ] && out="out"
src="${src%/}" src="${src%/}"
out="${out%/}" out="${out%/}"
[ -d "$src" ] || die "Source directory '$src' does not exist." if [ ! -d "$src" ]; then
if [ "$src" = "site" ]; then
IGNORE_ARGS="-name '.kewtignore' -o -path '$src/.*'" usage
exit 1
if [ -f "$src/.kewtignore" ]; then fi
while IFS= read -r line || [ -n "$line" ]; do die "Source directory '$src' does not exist."
case "$line" in
''|'#'*) continue ;;
esac
pattern=$(echo "$line" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -z "$pattern" ] && continue
pattern_clean="${pattern#/}"
pattern_clean="${pattern_clean%/}"
if echo "$pattern" | grep -q "/"; then
IGNORE_ARGS="$IGNORE_ARGS -o -path '$src/$pattern_clean' -o -path '$src/$pattern_clean/*'"
else
IGNORE_ARGS="$IGNORE_ARGS -o -name '$pattern_clean'"
fi
done < "$src/.kewtignore"
fi fi
find "$src" -name .kewtignore > "$KEWT_TMPDIR/kewt_ignore" BASE_IGNORE_ARGS=$(build_rule_args "$src" ".kewtignore" "-name '.kewtignore' -o -path '$src/.*'")
while read -r ki; do BASE_HIDE_ARGS=$(build_rule_args "$src" ".kewthide" "-name '.kewtignore' -o -name '.kewthide' -o -name '.kewtpreserve' -o -path '$src/.*'")
d="${ki%/.kewtignore}" PRESERVE_ARGS=$(build_rule_args "$src" ".kewtpreserve" "-false")
if [ "$d" != "$src" ] && [ "$d" != "." ]; then IGNORE_ARGS="$BASE_IGNORE_ARGS"
IGNORE_ARGS="$IGNORE_ARGS -o -path '$d' -o -path '$d/*'" HIDE_ARGS="$BASE_HIDE_ARGS"
fi
done < "$KEWT_TMPDIR/kewt_ignore"
rm -f "$KEWT_TMPDIR/kewt_ignore"
HIDE_ARGS="-name '.kewtignore' -o -name '.kewthide' -o -name '.kewtpreserve' -o -path '$src/.*'" refresh_build_context
if [ -f "$src/.kewthide" ]; then [ "$post_mode" = "true" ] && create_new_post "$src" "$post_title"
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|'#'*) continue ;;
esac
pattern=$(echo "$line" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -z "$pattern" ] && continue
pattern_clean="${pattern#/}" if [ "$clean_mode" = "true" ]; then
pattern_clean="${pattern_clean%/}" [ -d "$out" ] && rm -rf "$out"
if echo "$pattern" | grep -q "/"; then
HIDE_ARGS="$HIDE_ARGS -o -path '$src/$pattern_clean' -o -path '$src/$pattern_clean/*'"
else
HIDE_ARGS="$HIDE_ARGS -o -name '$pattern_clean'"
fi
done < "$src/.kewthide"
fi fi
find "$src" -name .kewthide > "$KEWT_TMPDIR/kewt_hide"
while read -r kh; do
d="${kh%/.kewthide}"
if [ "$d" != "$src" ] && [ "$d" != "." ]; then
HIDE_ARGS="$HIDE_ARGS -o -path '$d' -o -path '$d/*'"
fi
done < "$KEWT_TMPDIR/kewt_hide"
rm -f "$KEWT_TMPDIR/kewt_hide"
PRESERVE_ARGS="-false"
if [ -f "$src/.kewtpreserve" ]; then
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|'#'*) continue ;;
esac
pattern=$(echo "$line" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -z "$pattern" ] && continue
pattern_clean="${pattern#/}"
pattern_clean="${pattern_clean%/}"
if echo "$pattern" | grep -q "/"; then
PRESERVE_ARGS="$PRESERVE_ARGS -o -path '$src/$pattern_clean' -o -path '$src/$pattern_clean/*'"
else
PRESERVE_ARGS="$PRESERVE_ARGS -o -name '$pattern_clean'"
fi
done < "$src/.kewtpreserve"
fi
find "$src" -name .kewtpreserve > "$KEWT_TMPDIR/kewt_preserve"
while read -r kp; do
d="${kp%/.kewtpreserve}"
if [ "$d" != "$src" ] && [ "$d" != "." ]; then
PRESERVE_ARGS="$PRESERVE_ARGS -o -path '$d' -o -path '$d/*'"
fi
done < "$KEWT_TMPDIR/kewt_preserve"
rm -f "$KEWT_TMPDIR/kewt_preserve"
generate_nav() {
dinfo=$(eval "find \"$1\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -print" | sort | awk -v src="$1" -f "$awk_dir/collect_dir_info.awk")
eval "find \"$1\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -name \"*.md\" -print" | sort | awk -v src="$1" -v single_file_index="$single_file_index" -v flatten="$flatten" -v order="$order" -v home_name="$home_name" -v show_home_in_nav="$show_home_in_nav" -v dinfo="$dinfo" -f "$awk_dir/generate_sidebar.awk"
}
title="kewt"
style="kewt"
footer="made with <a href=\"https://kewt.krzak.org\">kewt</a>"
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=""
load_config() {
[ -f "$1" ] || return
while IFS= read -r line; do
case "$line" in
''|'#'*) continue ;;
*=*) ;;
*) continue ;;
esac
key=${line%%=*}
val=${line#*=}
key=$(printf '%s' "$key" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
val=$(printf '%s' "$val" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$val" in
\"*\") val=${val#\"}; val=${val%\"} ;;
\'*\') val=${val#\'}; val=${val%\'} ;;
esac
case "$key" in
title) title="$val" ;;
style) style="$val" ;;
dir_indexes) dir_indexes="$val" ;;
single_file_index) single_file_index="$val" ;;
flatten) flatten="$val" ;;
order) order="$val" ;;
home_name) home_name="$val" ;;
show_home_in_nav) show_home_in_nav="$val" ;;
nav_links) nav_links="$val" ;;
nav_extra) nav_extra="$val" ;;
footer) footer="$val" ;;
logo) logo="$val" ;;
display_logo) display_logo="$val" ;;
display_title) display_title="$val" ;;
logo_as_favicon) logo_as_favicon="$val" ;;
favicon) favicon="$val" ;;
esac
done < "$1"
}
load_config "./site.conf"
load_config "$src/site.conf"
escape_html_text() {
printf '%s' "$1" | sed \
-e 's/&/\&amp;/g' \
-e 's/</\&lt;/g' \
-e 's/>/\&gt;/g'
}
escape_html_attr() {
printf '%s' "$1" | sed \
-e 's/&/\&amp;/g' \
-e 's/"/\&quot;/g' \
-e 's/</\&lt;/g' \
-e 's/>/\&gt;/g'
}
nav_links_html() {
[ -n "$nav_links" ] || return
old_ifs=$IFS
set -f
IFS=','
# shellcheck disable=SC2086
set -- $nav_links
IFS=$old_ifs
set +f
[ $# -gt 0 ] || return
printf '<ul class="nav-extra-links">\n'
for raw_link in "$@"; do
link=$(printf '%s' "$raw_link" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -n "$link" ] || continue
case "$link" in
\[*\]\(*\))
label=${link#\[}
label=${label%%\]*}
link_url=${link#*](}
link_url=${link_url%)}
;;
*)
link_url=$link
label=$(printf '%s' "$link" | sed \
-e 's|^[A-Za-z][A-Za-z0-9+.-]*://||' \
-e 's|/$||')
[ -n "$label" ] || label="$link"
;;
esac
[ -n "$link_url" ] || continue
[ -n "$label" ] || label="$link_url"
link_attr=$(escape_html_attr "$link_url")
label_text=$(escape_html_text "$label")
printf '<li><a href="%s">%s</a></li>\n' "$link_attr" "$label_text"
done
printf '</ul>'
}
template="$src/template.html"
[ -f "$template" ] || template="./template.html"
[ -f "$template" ] || die "Template '$template' not found."
[ -d "$out" ] && rm -rf "$out"
mkdir -p "$out" mkdir -p "$out"
nav=$(generate_nav "$src") build_site
extra_links=$(nav_links_html)
if [ -n "$extra_links" ]; then if [ "$serve_mode" = "true" ]; then
nav="$nav port="${serve_port:-8000}"
$extra_links" if command -v python3 >/dev/null 2>&1; then
fi python3 -m http.server "$port" -d "$out" >/dev/null 2>&1 &
if [ -n "$nav_extra" ]; then server_pid=$!
nav="$nav echo "Serving '$out' on http://localhost:$port (python3)"
$nav_extra" elif command -v busybox >/dev/null 2>&1; then
busybox httpd -f -p "$port" -h "$out" >/dev/null 2>&1 &
server_pid=$!
echo "Serving '$out' on http://localhost:$port (busybox)"
else
die "Neither python3 nor busybox httpd is available to serve."
fi
trap 'kill $server_pid 2>/dev/null; rm -rf "$KEWT_TMPDIR"' EXIT
trap 'kill $server_pid 2>/dev/null; exit 0' HUP INT TERM
fi fi
find_closest() { if [ "$watch_mode" = "true" ]; then
target="$1" echo "Watching for changes in '$src'..."
start_dir="$2" touch "$KEWT_TMPDIR/watch_mark"
curr="$start_dir" while true; do
while [ "$curr" != "$src" ] && [ "$curr" != "." ] && [ "$curr" != "/" ]; do sleep 1
if [ -f "$curr/$target" ]; then changed=$(watch_for_changes "$KEWT_TMPDIR/watch_mark")
echo "$curr/$target"
return if [ -n "$changed" ]; then
echo ""
echo "Change detected, rebuilding..."
if [ "$clean_mode" = "true" ]; then
find "$out" -mindepth 1 -delete 2>/dev/null
fi
refresh_build_context
build_site
touch "$KEWT_TMPDIR/watch_mark"
fi fi
curr=$(dirname "$curr")
done done
if [ -f "$src/$target" ]; then elif [ "$serve_mode" = "true" ]; then
echo "$src/$target" wait "$server_pid"
fi
}
copy_style_with_resolved_vars() {
src_style="$1"
out_style="$2"
awk -f "$awk_dir/replace_variables.awk" "$src_style" > "$out_style"
}
render_markdown() {
file="$1"
local_template=$(find_closest "template.html" "$(dirname "$file")")
[ -z "$local_template" ] && local_template="$template"
closest_style_src=$(find_closest "styles.css" "$(dirname "$file")")
[ -z "$closest_style_src" ] && closest_style_src=$(find_closest "style.css" "$(dirname "$file")")
if [ -n "$closest_style_src" ]; then
style_rel_to_src="${closest_style_src#"$src"/}"
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="/${style_rel_to_src%styles.css}"
style_path="${style_path%style.css}styles.css"
else
style_path="/styles.css"
fi
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
favicon_src=""
if [ "$logo_as_favicon" = "true" ] && [ -n "$logo" ]; then
favicon_src="$logo"
elif [ -n "$favicon" ]; then
favicon_src="$favicon"
fi
head_extra=""
if [ -n "$favicon_src" ]; then
head_extra="<link rel=\"icon\" href=\"$favicon_src\" />"
fi
MARKDOWN_SITE_ROOT="$src" MARKDOWN_FALLBACK_FILE="$script_dir/styles/$style.css" sh "$script_dir/markdown.sh" "$file" | awk -v title="$title" -v nav="$nav" -v footer="$footer" -v style_path="$style_path" -v header_brand="$header_brand" -v head_extra="$head_extra" -f "$awk_dir/render_template.awk" "$local_template"
}
echo "Building site from '$src' to '$out'..."
eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while read -r dir; do
rel_dir="${dir#"$src"}"
rel_dir="${rel_dir#/}"
[ -z "$rel_dir" ] && rel_dir="."
out_dir="$out/$rel_dir"
mkdir -p "$out_dir"
if [ -f "$dir/styles.css" ]; then
copy_style_with_resolved_vars "$dir/styles.css" "$out_dir/styles.css"
elif [ -f "$dir/style.css" ]; then
copy_style_with_resolved_vars "$dir/style.css" "$out_dir/styles.css"
fi
[ "$dir_indexes" != "true" ] && continue
if [ ! -f "$dir/index.md" ]; then
if [ "$single_file_index" = "true" ]; then
md_count=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md" | wc -l)
if [ "$md_count" -eq 1 ]; then
md_file=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md")
render_markdown "$md_file" > "$out_dir/index.html"
continue
fi
fi
temp_index="$KEWT_TMPDIR/index.md"
display_dir="${rel_dir#.}"
[ -z "$display_dir" ] && display_dir="/"
echo "# Index of $display_dir" > "$temp_index"
echo "" >> "$temp_index"
find "$dir" ! -name "$(basename "$dir")" -prune ! -name ".*" -print | sort | while read -r entry; do
name="${entry##*/}"
case "$name" in
template.html|site.conf|style.css|index.md) continue ;;
esac
if [ -d "$entry" ]; then
echo "- [${name}/](${name}/index.html)" >> "$temp_index"
elif [ "${entry%.md}" != "$entry" ]; then
echo "- [${name%.md}](${name%.md}.html)" >> "$temp_index"
else
echo "- [$name]($name)" >> "$temp_index"
fi
done
render_markdown "$temp_index" > "$out_dir/index.html"
rm "$temp_index"
fi
done
if [ ! -f "$out/styles.css" ] && [ -f "$script_dir/styles/$style.css" ]; then
copy_style_with_resolved_vars "$script_dir/styles/$style.css" "$out/styles.css"
fi fi
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) continue ;;
esac
is_preserved=0
if [ -n "$(eval "find \"$file\" \( $PRESERVE_ARGS \) -print")" ]; then
is_preserved=1
fi
if [ "$single_file_index" = "true" ] && [ "${file%.md}" != "$file" ] && [ "$is_preserved" -eq 0 ] && [ ! -f "$(dirname "$file")/index.md" ]; then
md_count=$(find "$(dirname "$file")" ! -name "$(basename "$(dirname "$file")")" -prune -name "*.md" | wc -l)
[ "$md_count" -eq 1 ] && continue
fi
if [ "${file%.md}" != "$file" ] && [ "$is_preserved" -eq 0 ]; then
out_file="$out/${rel_path%.md}.html"
render_markdown "$file" > "$out_file"
else
cp "$file" "$out/$rel_path"
fi
done
echo "Build complete."

608
lib/builder.sh Normal file
View File

@@ -0,0 +1,608 @@
#!/bin/sh
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_site() {
echo "Building site from '$src' to '$out'..."
build_markdown_manifest
build_full_nav
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
has_custom_index="false"
has_list="false"
if [ -f "$dir/index.md" ]; then
has_custom_index="true"
if grep -q '^[[:space:]]*{{LIST}}[[:space:]]*$' "$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 "$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 "$rel_dir" && [ "$dir_md_count" -eq 1 ]; then
md_file="$src/$dir_first_md"
is_home="false"; [ "$dir" = "$src" ] && is_home="true"
target_url=$(directory_index_url "$rel_dir")
if needs_rebuild "$md_file" "$out_dir/index.html"; then
parse_frontmatter "$md_file"
if [ -n "$fm_content_warning" ]; then
content_out_file="$out_dir/content.html"
if [ "$rel_dir" = "." ]; then
content_rel_url="/content.html"
else
content_rel_url="/$(encode_url_path "$rel_dir")/content.html"
fi
write_content_warning_outputs "$md_file" "$content_out_file" "$content_rel_url" "$target_url" "$out_dir/index.html" "$is_home"
else
render_markdown "$md_file" "$is_home" "$target_url" > "$out_dir/index.html"
fi
fi
continue
fi
fi
temp_index="$KEWT_TMPDIR/index.md"
temp_list="$KEWT_TMPDIR/list.md"
: > "$temp_list"
if [ "$has_custom_index" = "false" ]; then
display_dir="${rel_dir#.}"
[ -z "$display_dir" ] && display_dir="/"
echo "# Index of $display_dir" > "$temp_index"
echo "" >> "$temp_index"
fi
sort_args=""
# If this is the posts dir reverse
if is_posts_directory_rel "$rel_dir"; then
sort_args="-r"
fi
temp_entries="$KEWT_TMPDIR/entries_$$.txt"
: > "$temp_entries"
find "$dir" ! -name "$(basename "$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})" >> "$temp_entries"
elif [ "${entry%.md}" != "$entry" ]; then
entry_rel_path="${entry#"$src"/}"
load_manifest_entry "$entry_rel_path" || continue
label="${name%.md}"
[ "$manifest_draft" = "true" ] && continue
post_h="$manifest_title"
is_post_entry="false"
if is_posts_directory_rel "$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}" >> "$temp_entries"
else
asset_url=$(encode_url_path "$name")
echo "${name}|- [$name]($asset_url)|$name|$asset_url" >> "$temp_entries"
fi
done
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"; [ "$dir" = "$src" ] && is_home="true"
target_url=$(directory_index_url "$rel_dir")
num_items=$(wc -l < "$temp_list")
if [ "$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 "$temp_list" | head -n "$posts_per_page" > "$chunk_list"
base_url_dir="$(dirname "$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 [ "$has_custom_index" = "false" ]; then
display_dir="${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 [ "$has_custom_index" = "true" ]; then
awk '
/^[[:space:]]*\{\{LIST\}\}[[:space:]]*$/ {
while((getline line < "'"$chunk_list"'") > 0) print line
close("'"$chunk_list"'")
next
}
{ print }
' "$dir/index.md" >> "$temp_index_p"
else
cat "$chunk_list" >> "$temp_index_p"
fi
if [ "$p" -eq 1 ]; then
out_file="$out_dir/index.html"
target_url_p="$target_url"
else
out_file="$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" "$is_home" "$target_url_p" > "$out_file"
rm -f "$temp_index_p" "$chunk_list"
p=$((p + 1))
done
else
if [ "$has_custom_index" = "true" ]; then
awk '
/^[[:space:]]*\{\{LIST\}\}[[:space:]]*$/ {
while((getline line < "'"$temp_list"'") > 0) print line
close("'"$temp_list"'")
next
}
{ print }
' "$dir/index.md" > "$temp_index"
else
cat "$temp_list" >> "$temp_index"
fi
do_rebuild="false"
needs_rebuild "$dir" "$out_dir/index.html" && do_rebuild="true"
[ "$has_custom_index" = "true" ] && needs_rebuild "$dir/index.md" "$out_dir/index.html" && do_rebuild="true"
if [ "$do_rebuild" = "false" ] && [ -f "$out_dir/index.html" ]; then
for _child in "$dir"/*; do
[ -e "$_child" ] || continue
if [ "$_child" -nt "$out_dir/index.html" ]; then
do_rebuild="true"
break
fi
done
fi
if [ "$do_rebuild" = "true" ]; then
if [ "$has_custom_index" = "true" ]; then
parse_frontmatter "$dir/index.md"
else
fm_content_warning=""
fi
if [ -n "$fm_content_warning" ]; then
content_out_file="$out_dir/content.html"
if [ "$rel_dir" = "." ]; then
content_rel_url="/content.html"
else
content_rel_url="/$(encode_url_path "$rel_dir")/content.html"
fi
write_content_warning_outputs "$temp_index" "$content_out_file" "$content_rel_url" "$target_url" "$out_dir/index.html" "$is_home"
else
render_markdown "$temp_index" "$is_home" "$target_url" > "$out_dir/index.html"
fi
fi
fi
rm -f "$temp_index" "$temp_list"
fi
done
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
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
[ "$manifest_draft" = "true" ] && continue
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
if [ -n "$error_page" ] && [ ! -f "$out/$error_page" ]; then
temp_404="$KEWT_TMPDIR/404_gen.md"
echo "# 404 - Not Found" > "$temp_404"
echo "" >> "$temp_404"
echo "The requested page could not be found." >> "$temp_404"
render_markdown "$temp_404" "false" "/$error_page" > "$out/$error_page"
rm -f "$temp_404"
fi
if [ -n "$base_url" ]; then
sitemap_file="$out/sitemap.xml"
base_url="${base_url%/}"
today=$(date +%Y-%m-%d)
printf '<?xml version="1.0" encoding="UTF-8"?>\n' > "$sitemap_file"
printf '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' >> "$sitemap_file"
find "$out" -type f -name "*.html" -print | sort | while IFS= read -r html_file; do
rel_url="${html_file#"$out"}"
# Don't include 404 in the sitemap (duh)
[ "${rel_url#/}" = "$error_page" ] && continue
{
printf ' <url>\n'
printf ' <loc>%s%s</loc>\n' "$base_url" "$rel_url"
printf ' <lastmod>%s</lastmod>\n' "$today"
printf ' </url>\n'
} >> "$sitemap_file"
done
printf '</urlset>\n' >> "$sitemap_file"
fi
if [ "$generate_feed" = "true" ] && [ -n "$base_url" ]; then
feed_path="$out/$feed_file"
base_url_feed="${base_url%/}"
build_date=$(date -u '+%a, %d %b %Y %H:%M:%S +0000')
printf '<?xml version="1.0" encoding="UTF-8"?>\n' > "$feed_path"
{
printf '<rss version="2.0">\n'
printf ' <channel>\n'
printf ' <title>%s</title>\n' "$title"
printf ' <link>%s</link>\n' "$base_url_feed"
printf ' <description>%s</description>\n' "$title"
printf ' <lastBuildDate>%s</lastBuildDate>\n' "$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")
{
printf ' <item>\n'
printf ' <title>%s</title>\n' "$feed_post_title"
printf ' <link>%s</link>\n' "$post_url"
printf ' <guid>%s</guid>\n' "$post_url"
printf ' <pubDate>%s</pubDate>\n' "$pub_date"
printf ' </item>\n'
} >> "$feed_path"
done
printf ' </channel>\n' >> "$feed_path"
printf '</rss>\n' >> "$feed_path"
fi
if [ "$generate_search" = "true" ] || [ "$generate_tags" = "true" ]; then
if [ "$generate_search" = "true" ]; then
printf '[\n' > "$out/search.json"
fi
first_search_item="true"
temp_tags="$KEWT_TMPDIR/tags_$$.txt"
: > "$temp_tags"
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 [ "$generate_search" = "true" ]; then
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
fi
if [ "$generate_tags" = "true" ] && [ -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" "$md_url" "$md_heading" >> "$temp_tags"
done
IFS=$old_ifs
fi
done < "$manifest_visible_list"
if [ "$generate_search" = "true" ]; then
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"
fi
if [ "$generate_tags" = "true" ]; then
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"
fi
rm -f "$temp_tags"
fi
echo "Build complete."
}

144
lib/commands.sh Normal file
View File

@@ -0,0 +1,144 @@
#!/bin/sh
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.
--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"
# Generate default site.conf
default_conf="$KEWT_TMPDIR/default_site.conf"
printf '%s\n' "$DEFAULT_CONF" > "$default_conf"
# Update site.conf
if [ ! -f "$target_conf" ]; then
echo "No site.conf found in '$update_dir'; nothing to update."
else
added=0
while IFS= read -r line; do
case "$line" in
''|'#'*) continue ;;
*=*) ;;
*) continue ;;
esac
key=$(printf '%s' "${line%%=*}" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
if ! grep -q "^[[:space:]]*${key}[[:space:]]*=" "$target_conf"; then
printf '%s\n' "$line" >> "$target_conf"
echo " Added: $key"
added=$((added + 1))
fi
done < "$default_conf"
if [ "$added" -eq 0 ]; then
echo "site.conf is already up to date."
else
echo "Added $added new key(s) to '$target_conf'."
fi
fi
# Update template.html
if [ -f "$target_tmpl" ]; then
default_tmpl="$KEWT_TMPDIR/default_template.html"
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
}

168
lib/config.sh Normal file
View File

@@ -0,0 +1,168 @@
#!/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'
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>'
reset_config() {
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"
}
load_config() {
[ -f "$1" ] || return
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|'#'*) continue ;;
*=*) ;;
*) continue ;;
esac
key=${line%%=*}
val=${line#*=}
key=$(printf '%s' "$key" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
val=$(printf '%s' "$val" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$val" in
\"*\")
val=${val#\"}; val=${val%\"}
val=$(printf '%s' "$val" | sed 's/\\"/\"/g; s/\\\\/\\/g')
;;
\'*\')
val=${val#\'}; val=${val%\'}
val=$(printf '%s' "$val" | sed "s/\\\\'/'/g; s/\\\\/\\/g")
;;
esac
case "$key" in
title) title="$val" ;;
style) style="${val#/}" ;;
dir_indexes) dir_indexes="$val" ;;
single_file_index) single_file_index="$val" ;;
flatten) flatten="$val" ;;
order) order="$val" ;;
home_name) home_name="$val" ;;
show_home_in_nav) show_home_in_nav="$val" ;;
nav_links) nav_links="$val" ;;
nav_extra) nav_extra="$val" ;;
footer) footer="$val" ;;
logo) logo="${val#/}" ;;
display_logo) display_logo="$val" ;;
display_title) display_title="$val" ;;
logo_as_favicon) logo_as_favicon="$val" ;;
favicon) favicon="${val#/}" ;;
generate_page_title) generate_page_title="$val" ;;
error_page) error_page="${val#/}" ;;
versioning) versioning="$val" ;;
enable_header_links) enable_header_links="$val" ;;
base_url) base_url="$val" ;;
generate_feed) generate_feed="$val" ;;
feed_file) feed_file="${val#/}" ;;
posts_dir) posts_dir="${val#/}" ;;
posts_per_page) posts_per_page="$val" ;;
custom_admonitions) custom_admonitions="$val" ;;
cw_hide_url) cw_hide_url="$val" ;;
lang) lang="$val" ;;
draft_by_default) draft_by_default="$val" ;;
generate_tags) generate_tags="$val" ;;
tags_dir) tags_dir="${val#/}" ;;
generate_search) generate_search="$val" ;;
search_in_footer) search_in_footer="$val" ;;
search_in_header) search_in_header="$val" ;;
include_cw_pages_in_search) include_cw_pages_in_search="$val" ;;
esac
done < "$1"
}
reset_config

315
lib/generator.sh Normal file
View File

@@ -0,0 +1,315 @@
#!/bin/sh
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=','
# shellcheck disable=SC2086
set -- $nav_links
IFS=$old_ifs
set +f
[ $# -gt 0 ] || return
printf '<ul class="nav-extra-links">\n'
for raw_link in "$@"; do
link=$(printf '%s' "$raw_link" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -n "$link" ] || continue
case "$link" in
\[*\]\(*\))
label=${link#\[}
label=${label%%\]*}
link_url=${link#*](}
link_url=${link_url%)}
;;
*)
link_url=$link
label=$(printf '%s' "$link" | sed \
-e 's|^[A-Za-z][A-Za-z0-9+.-]*://||' \
-e 's|/$||')
[ -n "$label" ] || label="$link"
;;
esac
[ -n "$link_url" ] || continue
[ -n "$label" ] || label="$link_url"
link_attr=$(escape_html_attr "$link_url")
label_text=$(escape_html_text "$label")
printf '<li><a href="%s">%s</a></li>\n' "$link_attr" "$label_text"
done
printf '</ul>'
}
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"
}
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
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
local_template=$(find_closest "template.html" "$(dirname "$file")")
[ -z "$local_template" ] && local_template="$template"
closest_style_src=$(find_closest "styles.css" "$(dirname "$file")")
[ -z "$closest_style_src" ] && closest_style_src=$(find_closest "style.css" "$(dirname "$file")")
if [ -n "$closest_style_src" ]; then
style_rel_to_src="${closest_style_src#"$src"/}"
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
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
favicon_src=""
if [ "$logo_as_favicon" = "true" ] && [ -n "$logo" ]; then
favicon_src="$logo"
elif [ -n "$favicon" ]; then
favicon_src="$favicon"
fi
head_extra=""
if [ -n "$favicon_src" ]; then
if echo "$favicon_src" | grep -q "^http"; then
head_extra="<link rel=\"icon\" href=\"$favicon_src\" />"
elif echo "$favicon_src" | grep -q "^/"; then
head_extra="<link rel=\"icon\" href=\"$favicon_src\" />"
else
head_extra="<link rel=\"icon\" href=\"/$favicon_src\" />"
fi
fi
parse_frontmatter "$file"
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
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
if [ "$is_cw_content_page" = "true" ] && [ "$cw_hide_url" = "true" ]; then
head_extra="$head_extra
<script>window.history.replaceState(null, '', '$current_url');</script>"
fi
final_footer="$footer"
if [ "$search_in_footer" = "true" ]; then
final_footer="$footer $SEARCH_FORM_FOOTER"
fi
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
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"
}

193
lib/manifest.sh Normal file
View File

@@ -0,0 +1,193 @@
#!/bin/sh
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
[ "$manifest_draft" = "true" ] && continue
manifest_dir_hidden_by_draft_index "$manifest_dir_rel" && continue
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

@@ -15,17 +15,23 @@ sed_inplace() {
fi fi
} }
temp_file="/tmp/markdown.$$.md" temp_parent="${KEWT_TMPDIR:-${TMPDIR:-/tmp}}"
temp_file="${temp_parent}/markdown.$$.md"
cat "$@" > "$temp_file" cat "$@" > "$temp_file"
trap 'rm -f "$temp_file" "$temp_file.tmp"' EXIT INT TERM 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 # 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_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" awk -f "$awk_dir/mask_plain.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# Reference links # Reference links
refs=$(cat "$@" | awk '/^\[[^\]]+\]: */') refs=$(awk '/^\[[^\]]+\]: */' "$temp_file")
IFS=' IFS='
' '
for ref in $refs; do for ref in $refs; do
@@ -40,25 +46,37 @@ done
sed_inplace "/^\[[^\]]*\]: */d" "$temp_file" sed_inplace "/^\[[^\]]*\]: */d" "$temp_file"
# Blocks # Blocks
sed_inplace "s/^>!\[/> [!/g" "$temp_file"
sed_inplace "s/^>\[!/> [!/g" "$temp_file"
loop_count=0
max_iterations=100
while grep '^>' "$temp_file" >/dev/null; do while grep '^>' "$temp_file" >/dev/null; do
awk -f "$awk_dir/blockquote.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" awk -f "$awk_dir/blockquote.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
loop_count=$((loop_count + 1))
if [ "$loop_count" -gt "$max_iterations" ]; then
echo "Warning: Blockquote processing exceeded $max_iterations iterations on $1. Breaking to prevent infinite loop." >&2
break
fi
done done
awk -f "$awk_dir/blockquote_to_admonition.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" awk -v custom_admonitions="$CUSTOM_ADMONITIONS" -f "$awk_dir/blockquote_to_admonition.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/fenced_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" awk -f "$awk_dir/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/indented_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/pipe_tables.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" awk -f "$awk_dir/pipe_tables.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/headers.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" awk -v enable_header_links="$ENABLE_HEADER_LINKS" -f "$awk_dir/headers.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/definition_lists.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" awk -f "$awk_dir/lists.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# TOC
awk -f "$awk_dir/toc.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# Footnotes
awk -f "$awk_dir/footnotes.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# Spacing # Spacing
awk -f "$awk_dir/breaks.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" 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" awk -f "$awk_dir/paragraphs.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# Inline styles # Inline styles
awk -v emoji_file="$awk_dir/emoji.tsv" -f "$awk_dir/emoji.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/markdown_inline.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" 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" awk -v input_file="$1" -v site_root="$MARKDOWN_SITE_ROOT" -v fallback_file="$MARKDOWN_FALLBACK_FILE" -v script_dir="$script_dir" -f "$awk_dir/markdown_embed.awk" "$temp_file"
rm "$temp_file" rm "$temp_file"

9
package.json Normal file
View File

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

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
# Maintainer: n0va <n0va@krzak.org> # Maintainer: n0va <n0va@krzak.org>
pkgname=kewt-git pkgname=kewt-git
pkgver=r0.0000000 pkgver=r0.0000000
pkgrel=1 pkgrel=2
pkgdesc="A minimalist, 100% POSIX, static site generator inspired by werc and kew" pkgdesc="A minimalist, 100% POSIX, static site generator inspired by werc and kew"
arch=('any') arch=('any')
url="https://git.krzak.org/N0VA/kewt" url="https://kewt.krzak.org"
license=('MIT') license=('ISC')
makedepends=('git') makedepends=('git')
depends=('sh') depends=('sh')
provides=('kewt') provides=('kewt')
conflicts=('kewt' 'kewt-bin') conflicts=('kewt' 'kewt-bin')
source=("${pkgname}::git+${url}.git") source=("${pkgname}::git+https://git.krzak.org/N0VA/kewt.git")
sha256sums=('SKIP') sha256sums=('SKIP')
pkgver() { pkgver() {
@@ -26,4 +26,6 @@ build() {
package() { package() {
cd "${pkgname}" cd "${pkgname}"
install -Dm755 kewt "${pkgdir}/usr/bin/kewt" 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"
} }

View File

@@ -4,12 +4,12 @@ pkgver=VERSION_PLACEHOLDER
pkgrel=1 pkgrel=1
pkgdesc="A minimalist, 100% POSIX, static site generator inspired by werc and kew" pkgdesc="A minimalist, 100% POSIX, static site generator inspired by werc and kew"
arch=('any') arch=('any')
url="https://git.krzak.org/N0VA/kewt" url="https://kewt.krzak.org"
license=('MIT') license=('ISC')
depends=('sh') depends=('sh')
provides=('kewt') provides=('kewt')
conflicts=('kewt' 'kewt-git') conflicts=('kewt' 'kewt-git')
source=("${pkgname}-${pkgver}.sh::${url}/releases/download/v${pkgver}/kewt") source=("${pkgname}-${pkgver}.sh::https://git.krzak.org/N0VA/kewt/releases/download/v${pkgver}/kewt")
sha256sums=('SHA256SUM_PLACEHOLDER') sha256sums=('SHA256SUM_PLACEHOLDER')
build() { build() {
@@ -18,4 +18,6 @@ build() {
package() { package() {
install -Dm755 "${srcdir}/${pkgname}-${pkgver}.sh" "${pkgdir}/usr/bin/kewt" 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"
} }

View File

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

25
packaging/zsh/_kewt Normal file
View File

@@ -0,0 +1,25 @@
#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:'
)
_arguments -S -C $args '*: :_directories'
}
_kewt "$@"

View File

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

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) ![the greatest drawing of a blobfish in the world](blobfish.bmp)
![mine turtle hello](hello.mp3) ![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. 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}}

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

@@ -0,0 +1,45 @@
---
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
```
### 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`.

BIN
site/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

View File

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 149 KiB

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

@@ -0,0 +1,12 @@
---
title = "Heaven"
content_warning = "This page may contain CSS"
---
# Heaven
Told you
Probably should've mentioned the catgirl too
| --- | --- |
| ```!![/styles.css]``` | <img style="vertical-align: top;" src="catgirl.jpg"> |

View File

@@ -1,8 +1,12 @@
# _kewt_ # _kewt_
### Pronounced "cute" ### Pronounced "cute"
***
# [Go to the repo](https://git.krzak.org/N0VA/kewt) # [Go to the repo](https://git.krzak.org/N0VA/kewt)
***
_kewt_ is a minimalist ssg inspired by _[werc](http://werc.cat-v.org/)_ and _[kew](https://github.com/uint23/kew)_ _kewt_ is a minimalist ssg inspired by _[werc](http://werc.cat-v.org/)_ and _[kew](https://github.com/uint23/kew)_
It's meant to be a static site generator, like _[kew](https://github.com/uint23/kew)_ but use only default (POSIX) tooling, like _[werc](http://werc.cat-v.org/)_ (and definitely unlike _[kew](https://github.com/uint23/kew)_) It's meant to be a static site generator, like _[kew](https://github.com/uint23/kew)_ but use only default (POSIX) tooling, like _[werc](http://werc.cat-v.org/)_ (and definitely unlike _[kew](https://github.com/uint23/kew)_)
@@ -10,98 +14,31 @@ It's meant to be a static site generator, like _[kew](https://github.com/uint23/
## Features ## Features
- No dependencies - No dependencies
- Frontmatter support (title, date, draft, description)
- Supports many embed types - Supports many embed types
- Automatic css variable replacement for older browsers - Automatic css variable replacement for older browsers
- Automatic inlining and embedding of many filetypes with `\![link]` or `\![alt](link)` - Automatic inlining and embedding of many filetypes with `\![link]` or `\![alt](link)`
- Typed embeds: `\!i`, `\!v`, `\!a`, `\!f`, `\!e`
- Inline html support - Inline html support
- MFM `$font` and `\<plain>` tags - MFM `$font` and `\<plain>` tags
- Admonition support (that's what the blocks like the warning block below are called) - GFM Admonition support (that's what the blocks like the warning block below are called)
- Task list support (`- [ ]`, `- [x]`)
- RSS/Feed generation and Sitemap support
- Post creation via `--post`
- Automatic 404 page generation
- `?v=n` support for cache busting
- Code block classes for use with external libraries like highlight.js or prism.js (both tested)
- Clickable markdown header anchors
- Mobile responsive layout
- 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
If you want to **force** a file to be inlined, use `\!![]` instead of `\![]` ***
## Installation ## [Quickstart guide](/docs/quickstart.html)
You can clone the repository to use `kewt.sh` directly, or you can download the standalone executable, which bundles all dependencies into a single file:
```sh
curl -L -o kewt https://git.krzak.org/N0VA/kewt/releases/latest/download/kewt
chmod +x kewt
```
On Arch Linux, _kewt_ is available on the 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
## Usage
```sh
./kewt.sh --help
./kewt.sh --new [title]
./kewt.sh --from <src> --to <out>
./kewt.sh [src] [out]
```
`--new [title]` creates a new site directory with a copied `site.conf` and a default `index.md`.
## site.conf
```conf
title = "kewt"
style = "kewt"
dir_indexes = true
single_file_index = true
flatten = false
order = ""
home_name = "Home"
show_home_in_nav = true
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 = ""
```
- `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)
## Ignores
- `.kewtignore`: Files/directories to ignore. If empty, the whole directory gets ignored
- `.kewthide`: Files/directories to hide from navigation but still process. Same empty rules as with ignore
- `.kewtpreserve`: Files/directories to copy but not convert markdown to html. Same empty rules again
## Embeds
- `\![link]`:
- local image/audio/video files are embedded as media tags
- local text/code files are inlined directly
- global image/audio/video links are embedded as media tags
- other global links are embedded as `<iframe>`
- `\![alt](link)` works the same, with `alt` used for images
- `\!![]` and `\!![alt](link)` force inline local file contents
## Credits
- Markdown to html conversion based on [markdown.bash](https://github.com/chadbraunduin/markdown.bash) by [chadbraunduin](https://github.com/chadbraunduin)
- Default css style and html template based on _[kew](https://github.com/uint23/kew)_ by [uint23](https://github.com/uint23)
>![WARNING]
>Most of this was coded at night, while sleepy and a bit sick, and after walking for about 4 hours around a forest, so...

View File

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

@@ -1,25 +1,29 @@
:root { :root {
--bg: #646c7f; --bg: #4a3b69;
--fg: #fffde0; --bg-deep: #352654;
--fg-link: #fff18f; --fg: #fbf5ff;
--code-bg: #32394a; --fg-muted: #c8b9df;
--code-border: #8f95a4; --fg-link: #dfaeff;
--code-fg: #fffde0; --fg-heading: #debfff;
--code-sel: #fff18f; --code-bg: #31234c;
--code-prop: #ffd27f; --code-border: #8060af;
--code-val: #cde7ff; --code-fg: #fbf5ff;
--code-var: #b9ffbe; --code-sel: #ffef99;
--code-com: #d0d0d0; --code-prop: #ffdfba;
--adm-note-bg: #3f5666; --code-val: #cae2ff;
--adm-note-border: #a8d8ff; --code-var: #caffc2;
--adm-tip-bg: #3f664c; --code-com: #b8aac8;
--adm-tip-border: #b9ffbe; --adm-note-bg: #353866;
--adm-important-bg: #5a4a6c; --adm-note-border: #b8c5ff;
--adm-important-border: #e4c7ff; --adm-tip-bg: #295246;
--adm-warning-bg: #6b5539; --adm-tip-border: #aeffda;
--adm-warning-border: #ffe0a8; --adm-important-bg: #533076;
--adm-caution-bg: #6f3f3f; --adm-important-border: #f4d9ff;
--adm-caution-border: #ffb4b4; --adm-warning-bg: #634631;
--adm-warning-border: #ffe2bd;
--adm-caution-bg: #662d43;
--adm-caution-border: #ffc4d5;
--thead-bg: #3d2d5c;
} }
body { body {
@@ -29,18 +33,25 @@ body {
color: var(--fg); color: var(--fg);
font-family: serif; font-family: serif;
font-size: 16px; font-size: 16px;
line-height: 1.2; line-height: 1.5;
} }
header { header {
padding: 20px; padding: 20px;
padding-bottom: 0;
border-bottom: 1px solid var(--code-border);
margin-bottom: 20px;
} }
header h1 { header h1 {
margin: 0; margin: 0;
font-size: 35px; font-size: 35px;
font-weight: normal; font-weight: bold;
font-style: italic; font-style: italic;
color: var(--fg-heading);
display: flex;
align-items: center;
flex-wrap: wrap;
} }
.site-logo { .site-logo {
@@ -57,18 +68,26 @@ header a {
text-decoration: none; text-decoration: none;
} }
header a:hover {
color: var(--bg-deep);
background: var(--fg);
}
#side-bar { #side-bar {
position: absolute; position: absolute;
top: 80px; top: 80px;
left: 0; left: 0;
width: 200px; width: 200px;
padding-left: 20px; padding-left: 20px;
margin-right: 14px;
border-right: 1px solid var(--code-border);
padding-right: 7px;
} }
.side-title { .side-title {
font-size: 25px; font-size: 25px;
margin: 20px 0 8px 0; margin: 20px 0 8px 0;
color: var(--fg); color: var(--fg-heading);
} }
#side-bar ul { #side-bar ul {
@@ -87,6 +106,14 @@ a {
padding: 1px 2px; padding: 1px 2px;
} }
#side-bar a.current-page {
font-weight: bold;
color: var(--fg);
border-left: 3px solid var(--fg-link);
padding-left: 7px;
margin-left: -10px;
}
a:hover { a:hover {
background: var(--fg); background: var(--fg);
color: var(--bg); color: var(--bg);
@@ -100,7 +127,7 @@ article {
h3 { h3 {
margin-top: 30px; margin-top: 30px;
font-size: 25px; font-size: 25px;
color: var(--fg); color: var(--fg-heading);
font-weight: normal; font-weight: normal;
} }
@@ -196,11 +223,28 @@ pre code {
border-color: var(--adm-caution-border); 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 { footer {
padding-top: 80px; padding-top: 60px;
font-style: italic; font-style: italic;
font-size: 17px; font-size: 17px;
margin-bottom: 20px; margin-bottom: 20px;
color: var(--fg-muted);
} }
article, article,
@@ -220,3 +264,303 @@ footer img {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
} }
hr {
height: 0;
margin: 24px 0;
border: 0;
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;
width: auto;
border-right: none;
border-bottom: 1px solid var(--code-border);
padding: 0 0 20px 0;
margin: 0 20px 20px 20px;
}
.nav-toggle:checked ~ #side-bar {
display: block;
}
article {
margin: 0 20px 0 20px;
}
footer {
margin-left: 20px;
margin-right: 20px;
padding-top: 30px;
}
}
.task-list-item {
list-style-type: none;
}
.task-list-item-checkbox {
margin: 0 0.2em 0.25em -1.6em;
vertical-align: middle;
}
.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: auto;
vertical-align: middle;
font-style: normal;
}
.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;
}

View File

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

View File

@@ -26,7 +26,13 @@ exit $?
#==PAYLOAD== #==PAYLOAD==
EOF EOF
tar -cz -C "$REPO_ROOT" kewt.sh markdown.sh awk styles >> "$OUT_FILE" 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" "$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 lib >> "$OUT_FILE"
rm -rf "$tmpbuild"
chmod +x "$OUT_FILE" chmod +x "$OUT_FILE"