34 Commits

Author SHA1 Message Date
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
43 changed files with 2183 additions and 340 deletions

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:
release:
types: [published]
workflow_dispatch:
jobs:
build:
@@ -90,25 +91,9 @@ jobs:
-e "s/SHA256SUM_PLACEHOLDER/${CHECKSUM}/g" \
packaging/AUR/PKGBUILD.template > aur-work/PKGBUILD
cat > aur-work/.SRCINFO << SRCEOF
pkgbase = kewt-bin
pkgdesc = A minimalist, 100% POSIX, static site generator inspired by werc and kew
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
sed -e "s/VERSION_PLACEHOLDER/${VERSION}/g" \
-e "s/SHA256SUM_PLACEHOLDER/${CHECKSUM}/g" \
packaging/AUR/.SRCINFO.template > aur-work/.SRCINFO
- name: Publish to AUR
uses: KSXGitHub/github-actions-deploy-aur@v3.0.1
@@ -119,3 +104,34 @@ jobs:
commit_email: ${{ github.actor }}@users.noreply.github.com
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
commit_message: "Update kewt-bin to ${{ github.ref_name }}"
publish-homebrew:
runs-on: local
needs: build
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Render Formula and push to GitHub
run: |
TAG="${GITHUB_REF#refs/tags/}"
VERSION="${TAG#v}"
curl -sL -o kewt-binary \
"https://git.krzak.org/N0VA/kewt/releases/download/${TAG}/kewt"
CHECKSUM=$(sha256sum kewt-binary | awk '{print $1}')
rm -f kewt-binary
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

2
.gitignore vendored
View File

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

View File

@@ -18,9 +18,8 @@ PERFORMANCE OF THIS SOFTWARE.
---
This project incorporates code (CSS style) from the 'kew' project, which is also licensed under the ISC License:
Copyright (c) 2023 uint23
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

19
Makefile Normal file
View File

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

View File

@@ -1,4 +1,4 @@
# _kewt_
# ![kewt](/icon.svg)
### Pronounced "cute"
***
@@ -10,18 +10,12 @@
_kewt_ is a minimalist ssg inspired by _[werc](http://werc.cat-v.org/)_ and _[kew](https://github.com/uint23/kew)_
## Quick Install
## [Installation](https://kewt.krzak.org/#installation)
```sh
curl -L -o kewt https://git.krzak.org/N0VA/kewt/releases/download/latest/kewt
chmod +x kewt
```
## Contributing
On Arch Linux, _kewt_ is available on the AUR:
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`.
- [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
## License
## Credits
- _kew_ css style adapted from _[kew](https://github.com/uint23/kew)_ by [uint23](https://github.com/uint23)
ISC

View File

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

View File

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

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

857
awk/emoji.awk Normal file
View File

@@ -0,0 +1,857 @@
BEGIN {
map[":+1:"] = "👍"
map[":100:"] = "💯"
map[":1234:"] = "🔢"
map[":8ball:"] = "🎱"
map[":a:"] = "🅰️"
map[":ab:"] = "🆎"
map[":abc:"] = "🔤"
map[":abcd:"] = "🔡"
map[":accept:"] = "🉑"
map[":aerial_tramway:"] = "🚡"
map[":airplane:"] = "✈️"
map[":alarm_clock:"] = "⏰"
map[":alien:"] = "👽"
map[":ambulance:"] = "🚑"
map[":anchor:"] = "⚓"
map[":angel:"] = "👼"
map[":anger:"] = "💢"
map[":angry:"] = "😠"
map[":anguished:"] = "😧"
map[":ant:"] = "🐜"
map[":apple:"] = "🍎"
map[":aquarius:"] = "♒"
map[":aries:"] = "♈"
map[":arrow_backward:"] = "◀️"
map[":arrow_double_down:"] = "⏬"
map[":arrow_double_up:"] = "⏫"
map[":arrow_down:"] = "⬇️"
map[":arrow_down_small:"] = "🔽"
map[":arrow_forward:"] = "▶️"
map[":arrow_heading_down:"] = "⤵️"
map[":arrow_heading_up:"] = "⤴️"
map[":arrow_left:"] = "⬅️"
map[":arrow_lower_left:"] = "↙️"
map[":arrow_lower_right:"] = "↘️"
map[":arrow_right:"] = "➡️"
map[":arrow_right_hook:"] = "↪️"
map[":arrow_up:"] = "⬆️"
map[":arrow_up_down:"] = "↕️"
map[":arrow_up_small:"] = "🔼"
map[":arrow_upper_left:"] = "↖️"
map[":arrow_upper_right:"] = "↗️"
map[":arrows_clockwise:"] = "🔃"
map[":arrows_counterclockwise:"] = "🔄"
map[":art:"] = "🎨"
map[":articulated_lorry:"] = "🚛"
map[":astonished:"] = "😲"
map[":atm:"] = "🏧"
map[":b:"] = "🅱️"
map[":baby:"] = "👶"
map[":baby_bottle:"] = "🍼"
map[":baby_chick:"] = "🐤"
map[":baby_symbol:"] = "🚼"
map[":baggage_claim:"] = "🛄"
map[":balloon:"] = "🎈"
map[":ballot_box_with_check:"] = "☑️"
map[":bamboo:"] = "🎍"
map[":banana:"] = "🍌"
map[":bangbang:"] = "‼️"
map[":bank:"] = "🏦"
map[":bar_chart:"] = "📊"
map[":barber:"] = "💈"
map[":baseball:"] = "⚾"
map[":basketball:"] = "🏀"
map[":bath:"] = "🛀"
map[":bathtub:"] = "🛁"
map[":battery:"] = "🔋"
map[":bear:"] = "🐻"
map[":beer:"] = "🍺"
map[":beers:"] = "🍻"
map[":beetle:"] = "🪲"
map[":beginner:"] = "🔰"
map[":bell:"] = "🔔"
map[":bento:"] = "🍱"
map[":bicyclist:"] = "🚴"
map[":bike:"] = "🚲"
map[":bikini:"] = "👙"
map[":bird:"] = "🐦"
map[":birthday:"] = "🎂"
map[":black_circle:"] = "⚫"
map[":black_joker:"] = "🃏"
map[":black_nib:"] = "✒️"
map[":black_square_button:"] = "🔲"
map[":blossom:"] = "🌼"
map[":blowfish:"] = "🐡"
map[":blue_book:"] = "📘"
map[":blue_car:"] = "🚙"
map[":blue_heart:"] = "💙"
map[":blush:"] = "😊"
map[":boar:"] = "🐗"
map[":boat:"] = "⛵"
map[":bomb:"] = "💣"
map[":book:"] = "📖"
map[":bookmark:"] = "🔖"
map[":bookmark_tabs:"] = "📑"
map[":books:"] = "📚"
map[":boom:"] = "💥"
map[":boot:"] = "👢"
map[":bouquet:"] = "💐"
map[":bow:"] = "🙇"
map[":bowling:"] = "🎳"
map[":boy:"] = "👦"
map[":bread:"] = "🍞"
map[":bride_with_veil:"] = "👰‍♀️"
map[":bridge_at_night:"] = "🌉"
map[":briefcase:"] = "💼"
map[":broken_heart:"] = "💔"
map[":bug:"] = "🐛"
map[":bulb:"] = "💡"
map[":bullettrain_front:"] = "🚅"
map[":bullettrain_side:"] = "🚄"
map[":bus:"] = "🚌"
map[":busstop:"] = "🚏"
map[":bust_in_silhouette:"] = "👤"
map[":busts_in_silhouette:"] = "👥"
map[":cactus:"] = "🌵"
map[":cake:"] = "🍰"
map[":calendar:"] = "📆"
map[":calling:"] = "📲"
map[":camel:"] = "🐫"
map[":camera:"] = "📷"
map[":cancer:"] = "♋"
map[":candy:"] = "🍬"
map[":capital_abcd:"] = "🔠"
map[":capricorn:"] = "♑"
map[":car:"] = "🚗"
map[":card_index:"] = "📇"
map[":carousel_horse:"] = "🎠"
map[":cat2:"] = "🐈"
map[":cat:"] = "🐱"
map[":cd:"] = "💿"
map[":chart:"] = "💹"
map[":chart_with_downwards_trend:"] = "📉"
map[":chart_with_upwards_trend:"] = "📈"
map[":checkered_flag:"] = "🏁"
map[":cherries:"] = "🍒"
map[":cherry_blossom:"] = "🌸"
map[":chestnut:"] = "🌰"
map[":chicken:"] = "🐔"
map[":children_crossing:"] = "🚸"
map[":chocolate_bar:"] = "🍫"
map[":christmas_tree:"] = "🎄"
map[":church:"] = "⛪"
map[":cinema:"] = "🎦"
map[":circus_tent:"] = "🎪"
map[":city_sunrise:"] = "🌇"
map[":city_sunset:"] = "🌆"
map[":cl:"] = "🆑"
map[":clap:"] = "👏"
map[":clapper:"] = "🎬"
map[":clipboard:"] = "📋"
map[":clock1030:"] = "🕥"
map[":clock10:"] = "🕙"
map[":clock1130:"] = "🕦"
map[":clock11:"] = "🕚"
map[":clock1230:"] = "🕧"
map[":clock12:"] = "🕛"
map[":clock130:"] = "🕜"
map[":clock1:"] = "🕐"
map[":clock230:"] = "🕝"
map[":clock2:"] = "🕑"
map[":clock330:"] = "🕞"
map[":clock3:"] = "🕒"
map[":clock430:"] = "🕟"
map[":clock4:"] = "🕓"
map[":clock530:"] = "🕠"
map[":clock5:"] = "🕔"
map[":clock630:"] = "🕡"
map[":clock6:"] = "🕕"
map[":clock730:"] = "🕢"
map[":clock7:"] = "🕖"
map[":clock830:"] = "🕣"
map[":clock8:"] = "🕗"
map[":clock930:"] = "🕤"
map[":clock9:"] = "🕘"
map[":closed_book:"] = "📕"
map[":closed_lock_with_key:"] = "🔐"
map[":closed_umbrella:"] = "🌂"
map[":cloud:"] = "☁️"
map[":clubs:"] = "♣️"
map[":cn:"] = "🇨🇳"
map[":cocktail:"] = "🍸"
map[":coffee:"] = "☕"
map[":cold_sweat:"] = "😰"
map[":computer:"] = "💻"
map[":confetti_ball:"] = "🎊"
map[":confounded:"] = "😖"
map[":confused:"] = "😕"
map[":congratulations:"] = "㊗️"
map[":construction:"] = "🚧"
map[":construction_worker:"] = "👷"
map[":convenience_store:"] = "🏪"
map[":cookie:"] = "🍪"
map[":cool:"] = "🆒"
map[":cop:"] = "👮"
map[":copyright:"] = "©️"
map[":corn:"] = "🌽"
map[":couple:"] = "👫"
map[":couple_with_heart:"] = "💑"
map[":couplekiss:"] = "💏"
map[":cow2:"] = "🐄"
map[":cow:"] = "🐮"
map[":credit_card:"] = "💳"
map[":crocodile:"] = "🐊"
map[":crossed_flags:"] = "🎌"
map[":crown:"] = "👑"
map[":cry:"] = "😢"
map[":crying_cat_face:"] = "😿"
map[":crystal_ball:"] = "🔮"
map[":cupid:"] = "💘"
map[":curly_loop:"] = "➰"
map[":currency_exchange:"] = "💱"
map[":curry:"] = "🍛"
map[":custard:"] = "🍮"
map[":customs:"] = "🛃"
map[":cyclone:"] = "🌀"
map[":dancer:"] = "💃"
map[":dancers:"] = "👯"
map[":dango:"] = "🍡"
map[":dart:"] = "🎯"
map[":dash:"] = "💨"
map[":date:"] = "📅"
map[":de:"] = "🇩🇪"
map[":deciduous_tree:"] = "🌳"
map[":department_store:"] = "🏬"
map[":diamond_shape_with_a_dot_inside:"] = "💠"
map[":diamonds:"] = "♦️"
map[":disappointed:"] = "😞"
map[":disappointed_relieved:"] = "😥"
map[":dizzy:"] = "💫"
map[":dizzy_face:"] = "😵"
map[":do_not_litter:"] = "🚯"
map[":dog2:"] = "🐕"
map[":dog:"] = "🐶"
map[":dollar:"] = "💵"
map[":dolls:"] = "🎎"
map[":dolphin:"] = "🐬"
map[":door:"] = "🚪"
map[":doughnut:"] = "🍩"
map[":dragon:"] = "🐉"
map[":dragon_face:"] = "🐲"
map[":dress:"] = "👗"
map[":dromedary_camel:"] = "🐪"
map[":droplet:"] = "💧"
map[":dvd:"] = "📀"
map[":ear:"] = "👂"
map[":ear_of_rice:"] = "🌾"
map[":earth_africa:"] = "🌍"
map[":earth_americas:"] = "🌎"
map[":earth_asia:"] = "🌏"
map[":egg:"] = "🥚"
map[":eggplant:"] = "🍆"
map[":eight:"] = "8⃣"
map[":eight_pointed_black_star:"] = "✴️"
map[":eight_spoked_asterisk:"] = "✳️"
map[":electric_plug:"] = "🔌"
map[":elephant:"] = "🐘"
map[":email:"] = "📧"
map[":end:"] = "🔚"
map[":envelope:"] = "✉️"
map[":es:"] = "🇪🇸"
map[":euro:"] = "💶"
map[":european_castle:"] = "🏰"
map[":european_post_office:"] = "🏤"
map[":evergreen_tree:"] = "🌲"
map[":exclamation:"] = "❗"
map[":expressionless:"] = "😑"
map[":eyeglasses:"] = "👓"
map[":eyes:"] = "👀"
map[":factory:"] = "🏭"
map[":fallen_leaf:"] = "🍂"
map[":family:"] = "👪"
map[":fast_forward:"] = "⏩"
map[":fax:"] = "📠"
map[":fearful:"] = "😨"
map[":feet:"] = "🐾"
map[":ferris_wheel:"] = "🎡"
map[":file_folder:"] = "📁"
map[":fire:"] = "🔥"
map[":fire_engine:"] = "🚒"
map[":fireworks:"] = "🎆"
map[":first_quarter_moon:"] = "🌓"
map[":first_quarter_moon_with_face:"] = "🌛"
map[":fish:"] = "🐟"
map[":fish_cake:"] = "🍥"
map[":fishing_pole_and_fish:"] = "🎣"
map[":fist:"] = "✊"
map[":five:"] = "5⃣"
map[":flags:"] = "🎏"
map[":flashlight:"] = "🔦"
map[":floppy_disk:"] = "💾"
map[":flower_playing_cards:"] = "🎴"
map[":flushed:"] = "😳"
map[":foggy:"] = "🌁"
map[":football:"] = "🏈"
map[":fork_and_knife:"] = "🍴"
map[":fountain:"] = "⛲"
map[":four:"] = "4⃣"
map[":four_leaf_clover:"] = "🍀"
map[":fr:"] = "🇫🇷"
map[":free:"] = "🆓"
map[":fried_shrimp:"] = "🍤"
map[":fries:"] = "🍟"
map[":frog:"] = "🐸"
map[":frowning:"] = "😦"
map[":fuelpump:"] = "⛽"
map[":full_moon:"] = "🌕"
map[":full_moon_with_face:"] = "🌝"
map[":game_die:"] = "🎲"
map[":gem:"] = "💎"
map[":gemini:"] = "♊"
map[":ghost:"] = "👻"
map[":gift:"] = "🎁"
map[":gift_heart:"] = "💝"
map[":girl:"] = "👧"
map[":globe_with_meridians:"] = "🌐"
map[":goat:"] = "🐐"
map[":golf:"] = "⛳"
map[":grapes:"] = "🍇"
map[":green_apple:"] = "🍏"
map[":green_book:"] = "📗"
map[":green_heart:"] = "💚"
map[":grey_exclamation:"] = "❕"
map[":grey_question:"] = "❔"
map[":grimacing:"] = "😬"
map[":grin:"] = "😁"
map[":grinning:"] = "😀"
map[":guardsman:"] = "💂‍♂️"
map[":guitar:"] = "🎸"
map[":gun:"] = "🔫"
map[":haircut:"] = "💇"
map[":hamburger:"] = "🍔"
map[":hammer:"] = "🔨"
map[":hamster:"] = "🐹"
map[":handbag:"] = "👜"
map[":hankey:"] = "💩"
map[":hash:"] = "#️⃣"
map[":hatched_chick:"] = "🐥"
map[":hatching_chick:"] = "🐣"
map[":headphones:"] = "🎧"
map[":hear_no_evil:"] = "🙉"
map[":heart:"] = "❤️"
map[":heart_decoration:"] = "💟"
map[":heart_eyes:"] = "😍"
map[":heart_eyes_cat:"] = "😻"
map[":heartbeat:"] = "💓"
map[":hearts:"] = "♥️"
map[":heavy_check_mark:"] = "✔️"
map[":heavy_division_sign:"] = "➗"
map[":heavy_dollar_sign:"] = "💲"
map[":heavy_minus_sign:"] = ""
map[":heavy_multiplication_x:"] = "✖️"
map[":heavy_plus_sign:"] = ""
map[":helicopter:"] = "🚁"
map[":herb:"] = "🌿"
map[":hibiscus:"] = "🌺"
map[":high_brightness:"] = "🔆"
map[":high_heel:"] = "👠"
map[":honey_pot:"] = "🍯"
map[":horse:"] = "🐴"
map[":horse_racing:"] = "🏇"
map[":hospital:"] = "🏥"
map[":hotel:"] = "🏨"
map[":hotsprings:"] = "♨️"
map[":hourglass:"] = "⌛"
map[":hourglass_flowing_sand:"] = "⏳"
map[":house:"] = "🏠"
map[":house_with_garden:"] = "🏡"
map[":hushed:"] = "😯"
map[":ice_cream:"] = "🍨"
map[":icecream:"] = "🍦"
map[":id:"] = "🆔"
map[":ideograph_advantage:"] = "🉐"
map[":imp:"] = "👿"
map[":inbox_tray:"] = "📥"
map[":incoming_envelope:"] = "📨"
map[":information_desk_person:"] = "💁"
map[":information_source:"] = ""
map[":innocent:"] = "😇"
map[":interrobang:"] = "⁉️"
map[":iphone:"] = "📱"
map[":it:"] = "🇮🇹"
map[":izakaya_lantern:"] = "🏮"
map[":jack_o_lantern:"] = "🎃"
map[":japan:"] = "🗾"
map[":japanese_castle:"] = "🏯"
map[":japanese_goblin:"] = "👺"
map[":japanese_ogre:"] = "👹"
map[":jeans:"] = "👖"
map[":joy:"] = "😂"
map[":joy_cat:"] = "😹"
map[":jp:"] = "🇯🇵"
map[":key:"] = "🔑"
map[":keycap_ten:"] = "🔟"
map[":kimono:"] = "👘"
map[":kiss:"] = "💋"
map[":kissing:"] = "😗"
map[":kissing_cat:"] = "😽"
map[":kissing_closed_eyes:"] = "😚"
map[":kissing_heart:"] = "😘"
map[":kissing_smiling_eyes:"] = "😙"
map[":koala:"] = "🐨"
map[":koko:"] = "🈁"
map[":kr:"] = "🇰🇷"
map[":large_blue_diamond:"] = "🔷"
map[":large_orange_diamond:"] = "🔶"
map[":last_quarter_moon:"] = "🌗"
map[":last_quarter_moon_with_face:"] = "🌜"
map[":laughing:"] = "😆"
map[":leaves:"] = "🍃"
map[":ledger:"] = "📒"
map[":left_luggage:"] = "🛅"
map[":left_right_arrow:"] = "↔️"
map[":leftwards_arrow_with_hook:"] = "↩️"
map[":lemon:"] = "🍋"
map[":leo:"] = "♌"
map[":leopard:"] = "🐆"
map[":libra:"] = "♎"
map[":light_rail:"] = "🚈"
map[":link:"] = "🔗"
map[":lips:"] = "👄"
map[":lipstick:"] = "💄"
map[":lock:"] = "🔒"
map[":lock_with_ink_pen:"] = "🔏"
map[":lollipop:"] = "🍭"
map[":loop:"] = "➿"
map[":loudspeaker:"] = "📢"
map[":love_hotel:"] = "🏩"
map[":love_letter:"] = "💌"
map[":low_brightness:"] = "🔅"
map[":m:"] = "Ⓜ️"
map[":mag:"] = "🔍"
map[":mag_right:"] = "🔎"
map[":mahjong:"] = "🀄"
map[":mailbox:"] = "📫"
map[":mailbox_closed:"] = "📪"
map[":mailbox_with_mail:"] = "📬"
map[":mailbox_with_no_mail:"] = "📭"
map[":man:"] = "👨"
map[":man_with_gua_pi_mao:"] = "👲"
map[":man_with_turban:"] = "👳‍♂️"
map[":mans_shoe:"] = "👞"
map[":maple_leaf:"] = "🍁"
map[":mask:"] = "😷"
map[":massage:"] = "💆"
map[":meat_on_bone:"] = "🍖"
map[":mega:"] = "📣"
map[":melon:"] = "🍈"
map[":memo:"] = "📝"
map[":mens:"] = "🚹"
map[":metal:"] = "🤘"
map[":metro:"] = "🚇"
map[":microphone:"] = "🎤"
map[":microscope:"] = "🔬"
map[":milky_way:"] = "🌌"
map[":minibus:"] = "🚐"
map[":minidisc:"] = "💽"
map[":mobile_phone_off:"] = "📴"
map[":money_with_wings:"] = "💸"
map[":moneybag:"] = "💰"
map[":monkey:"] = "🐒"
map[":monkey_face:"] = "🐵"
map[":monorail:"] = "🚝"
map[":mortar_board:"] = "🎓"
map[":mount_fuji:"] = "🗻"
map[":mountain_bicyclist:"] = "🚵"
map[":mountain_cableway:"] = "🚠"
map[":mountain_railway:"] = "🚞"
map[":mouse2:"] = "🐁"
map[":mouse:"] = "🐭"
map[":movie_camera:"] = "🎥"
map[":moyai:"] = "🗿"
map[":muscle:"] = "💪"
map[":mushroom:"] = "🍄"
map[":musical_keyboard:"] = "🎹"
map[":musical_note:"] = "🎵"
map[":musical_score:"] = "🎼"
map[":mute:"] = "🔇"
map[":nail_care:"] = "💅"
map[":name_badge:"] = "📛"
map[":necktie:"] = "👔"
map[":negative_squared_cross_mark:"] = "❎"
map[":neutral_face:"] = "😐"
map[":new:"] = "🆕"
map[":new_moon:"] = "🌑"
map[":new_moon_with_face:"] = "🌚"
map[":newspaper:"] = "📰"
map[":ng:"] = "🆖"
map[":nine:"] = "9⃣"
map[":no_bell:"] = "🔕"
map[":no_bicycles:"] = "🚳"
map[":no_entry:"] = "⛔"
map[":no_entry_sign:"] = "🚫"
map[":no_good:"] = "🙅"
map[":no_mobile_phones:"] = "📵"
map[":no_mouth:"] = "😶"
map[":no_pedestrians:"] = "🚷"
map[":no_smoking:"] = "🚭"
map[":nose:"] = "👃"
map[":notebook:"] = "📓"
map[":notebook_with_decorative_cover:"] = "📔"
map[":notes:"] = "🎶"
map[":nut_and_bolt:"] = "🔩"
map[":o2:"] = "🅾️"
map[":o:"] = "⭕"
map[":ocean:"] = "🌊"
map[":octopus:"] = "🐙"
map[":oden:"] = "🍢"
map[":office:"] = "🏢"
map[":ok:"] = "🆗"
map[":ok_hand:"] = "👌"
map[":ok_woman:"] = "🙆‍♀️"
map[":older_man:"] = "👴"
map[":older_woman:"] = "👵"
map[":on:"] = "🔛"
map[":oncoming_automobile:"] = "🚘"
map[":oncoming_bus:"] = "🚍"
map[":oncoming_police_car:"] = "🚔"
map[":oncoming_taxi:"] = "🚖"
map[":one:"] = "1⃣"
map[":open_file_folder:"] = "📂"
map[":open_hands:"] = "👐"
map[":open_mouth:"] = "😮"
map[":ophiuchus:"] = "⛎"
map[":orange_book:"] = "📙"
map[":outbox_tray:"] = "📤"
map[":ox:"] = "🐂"
map[":page_facing_up:"] = "📄"
map[":page_with_curl:"] = "📃"
map[":pager:"] = "📟"
map[":palm_tree:"] = "🌴"
map[":panda_face:"] = "🐼"
map[":paperclip:"] = "📎"
map[":parking:"] = "🅿️"
map[":part_alternation_mark:"] = "〽️"
map[":partly_sunny:"] = "⛅"
map[":passport_control:"] = "🛂"
map[":paw_prints:"] = "🐾"
map[":peach:"] = "🍑"
map[":pear:"] = "🍐"
map[":pencil2:"] = "✏️"
map[":pencil:"] = "📝"
map[":penguin:"] = "🐧"
map[":pensive:"] = "😔"
map[":performing_arts:"] = "🎭"
map[":persevere:"] = "😣"
map[":pig2:"] = "🐖"
map[":pig:"] = "🐷"
map[":pig_nose:"] = "🐽"
map[":pill:"] = "💊"
map[":pineapple:"] = "🍍"
map[":pisces:"] = "♓"
map[":pizza:"] = "🍕"
map[":point_down:"] = "👇"
map[":point_left:"] = "👈"
map[":point_right:"] = "👉"
map[":point_up:"] = "☝️"
map[":point_up_2:"] = "👆"
map[":police_car:"] = "🚓"
map[":poodle:"] = "🐩"
map[":poop:"] = "💩"
map[":post_office:"] = "🏣"
map[":postal_horn:"] = "📯"
map[":postbox:"] = "📮"
map[":potable_water:"] = "🚰"
map[":pouch:"] = "👝"
map[":poultry_leg:"] = "🍗"
map[":pound:"] = "💷"
map[":pouting_cat:"] = "😾"
map[":pray:"] = "🙏"
map[":princess:"] = "👸"
map[":punch:"] = "👊"
map[":purple_heart:"] = "💜"
map[":purse:"] = "👛"
map[":pushpin:"] = "📌"
map[":put_litter_in_its_place:"] = "🚮"
map[":question:"] = "❓"
map[":rabbit2:"] = "🐇"
map[":rabbit:"] = "🐰"
map[":racehorse:"] = "🐎"
map[":radio:"] = "📻"
map[":radio_button:"] = "🔘"
map[":rage:"] = "😡"
map[":railway_car:"] = "🚃"
map[":rainbow:"] = "🌈"
map[":raised_hand:"] = "✋"
map[":raised_hands:"] = "🙌"
map[":raising_hand:"] = "🙋"
map[":ram:"] = "🐏"
map[":ramen:"] = "🍜"
map[":rat:"] = "🐀"
map[":recycle:"] = "♻️"
map[":red_car:"] = "🚗"
map[":red_circle:"] = "🔴"
map[":registered:"] = "®️"
map[":relaxed:"] = "☺️"
map[":relieved:"] = "😌"
map[":repeat:"] = "🔁"
map[":repeat_one:"] = "🔂"
map[":restroom:"] = "🚻"
map[":revolving_hearts:"] = "💞"
map[":rewind:"] = "⏪"
map[":ribbon:"] = "🎀"
map[":rice:"] = "🍚"
map[":rice_ball:"] = "🍙"
map[":rice_cracker:"] = "🍘"
map[":rice_scene:"] = "🎑"
map[":ring:"] = "💍"
map[":rocket:"] = "🚀"
map[":roller_coaster:"] = "🎢"
map[":rooster:"] = "🐓"
map[":rose:"] = "🌹"
map[":rotating_light:"] = "🚨"
map[":round_pushpin:"] = "📍"
map[":rowboat:"] = "🚣"
map[":ru:"] = "🇷🇺"
map[":rugby_football:"] = "🏉"
map[":runner:"] = "🏃"
map[":running_shirt_with_sash:"] = "🎽"
map[":sa:"] = "🈂️"
map[":sagittarius:"] = "♐"
map[":sailboat:"] = "⛵"
map[":sake:"] = "🍶"
map[":sandal:"] = "👡"
map[":santa:"] = "🎅"
map[":satellite:"] = "📡"
map[":satisfied:"] = "😆"
map[":saxophone:"] = "🎷"
map[":school:"] = "🏫"
map[":school_satchel:"] = "🎒"
map[":scissors:"] = "✂️"
map[":scorpius:"] = "♏"
map[":scream:"] = "😱"
map[":scream_cat:"] = "🙀"
map[":scroll:"] = "📜"
map[":seat:"] = "💺"
map[":secret:"] = "㊙️"
map[":see_no_evil:"] = "🙈"
map[":seedling:"] = "🌱"
map[":seven:"] = "7⃣"
map[":shaved_ice:"] = "🍧"
map[":sheep:"] = "🐑"
map[":shell:"] = "🐚"
map[":ship:"] = "🚢"
map[":shirt:"] = "👕"
map[":shit:"] = "💩"
map[":shower:"] = "🚿"
map[":signal_strength:"] = "📶"
map[":six:"] = "6⃣"
map[":six_pointed_star:"] = "🔯"
map[":ski:"] = "🎿"
map[":skull:"] = "💀"
map[":sleeping:"] = "😴"
map[":sleepy:"] = "😪"
map[":slot_machine:"] = "🎰"
map[":small_blue_diamond:"] = "🔹"
map[":small_orange_diamond:"] = "🔸"
map[":small_red_triangle:"] = "🔺"
map[":small_red_triangle_down:"] = "🔻"
map[":smile:"] = "😄"
map[":smile_cat:"] = "😸"
map[":smiley:"] = "😃"
map[":smiley_cat:"] = "😺"
map[":smiling_imp:"] = "😈"
map[":smirk:"] = "😏"
map[":smirk_cat:"] = "😼"
map[":smoking:"] = "🚬"
map[":snail:"] = "🐌"
map[":snake:"] = "🐍"
map[":snowboarder:"] = "🏂"
map[":snowflake:"] = "❄️"
map[":snowman:"] = "⛄"
map[":sob:"] = "😭"
map[":soccer:"] = "⚽"
map[":soon:"] = "🔜"
map[":sos:"] = "🆘"
map[":sound:"] = "🔉"
map[":space_invader:"] = "👾"
map[":spades:"] = "♠️"
map[":spaghetti:"] = "🍝"
map[":sparkler:"] = "🎇"
map[":sparkles:"] = "✨"
map[":sparkling_heart:"] = "💖"
map[":speak_no_evil:"] = "🙊"
map[":speaker:"] = "🔈"
map[":speech_balloon:"] = "💬"
map[":speedboat:"] = "🚤"
map[":star2:"] = "🌟"
map[":star:"] = "⭐"
map[":stars:"] = "🌠"
map[":station:"] = "🚉"
map[":statue_of_liberty:"] = "🗽"
map[":steam_locomotive:"] = "🚂"
map[":stew:"] = "🍲"
map[":straight_ruler:"] = "📏"
map[":strawberry:"] = "🍓"
map[":stuck_out_tongue:"] = "😛"
map[":stuck_out_tongue_closed_eyes:"] = "😝"
map[":stuck_out_tongue_winking_eye:"] = "😜"
map[":sun_with_face:"] = "🌞"
map[":sunflower:"] = "🌻"
map[":sunglasses:"] = "😎"
map[":sunny:"] = "☀️"
map[":sunrise:"] = "🌅"
map[":sunrise_over_mountains:"] = "🌄"
map[":surfer:"] = "🏄"
map[":sushi:"] = "🍣"
map[":suspension_railway:"] = "🚟"
map[":sweat:"] = "😓"
map[":sweat_drops:"] = "💦"
map[":sweat_smile:"] = "😅"
map[":sweet_potato:"] = "🍠"
map[":swimmer:"] = "🏊"
map[":symbols:"] = "🔣"
map[":syringe:"] = "💉"
map[":tada:"] = "🎉"
map[":tanabata_tree:"] = "🎋"
map[":tangerine:"] = "🍊"
map[":taurus:"] = "♉"
map[":taxi:"] = "🚕"
map[":tea:"] = "🍵"
map[":telephone:"] = "☎️"
map[":telephone_receiver:"] = "📞"
map[":telescope:"] = "🔭"
map[":tennis:"] = "🎾"
map[":tent:"] = "⛺"
map[":thought_balloon:"] = "💭"
map[":three:"] = "3⃣"
map[":thumbsdown:"] = "👎"
map[":thumbsup:"] = "👍"
map[":ticket:"] = "🎫"
map[":tiger2:"] = "🐅"
map[":tiger:"] = "🐯"
map[":tired_face:"] = "😫"
map[":tm:"] = "™️"
map[":toilet:"] = "🚽"
map[":tokyo_tower:"] = "🗼"
map[":tomato:"] = "🍅"
map[":tongue:"] = "👅"
map[":top:"] = "🔝"
map[":tophat:"] = "🎩"
map[":tractor:"] = "🚜"
map[":traffic_light:"] = "🚥"
map[":train2:"] = "🚆"
map[":train:"] = "🚋"
map[":tram:"] = "🚊"
map[":triangular_flag_on_post:"] = "🚩"
map[":triangular_ruler:"] = "📐"
map[":trident:"] = "🔱"
map[":triumph:"] = "😤"
map[":trolleybus:"] = "🚎"
map[":trophy:"] = "🏆"
map[":tropical_drink:"] = "🍹"
map[":tropical_fish:"] = "🐠"
map[":truck:"] = "🚚"
map[":trumpet:"] = "🎺"
map[":tulip:"] = "🌷"
map[":turtle:"] = "🐢"
map[":tv:"] = "📺"
map[":twisted_rightwards_arrows:"] = "🔀"
map[":two:"] = "2⃣"
map[":two_men_holding_hands:"] = "👬"
map[":two_women_holding_hands:"] = "👭"
map[":u5272:"] = "🈹"
map[":u5408:"] = "🈴"
map[":u55b6:"] = "🈺"
map[":u6307:"] = "🈯"
map[":u6708:"] = "🈷️"
map[":u6709:"] = "🈶"
map[":u6e80:"] = "🈵"
map[":u7121:"] = "🈚"
map[":u7533:"] = "🈸"
map[":u7981:"] = "🈲"
map[":u7a7a:"] = "🈳"
map[":umbrella:"] = "☔"
map[":unamused:"] = "😒"
map[":underage:"] = "🔞"
map[":unlock:"] = "🔓"
map[":up:"] = "🆙"
map[":us:"] = "🇺🇸"
map[":v:"] = "✌️"
map[":vertical_traffic_light:"] = "🚦"
map[":vhs:"] = "📼"
map[":vibration_mode:"] = "📳"
map[":video_camera:"] = "📹"
map[":video_game:"] = "🎮"
map[":violin:"] = "🎻"
map[":virgo:"] = "♍"
map[":volcano:"] = "🌋"
map[":vs:"] = "🆚"
map[":walking:"] = "🚶"
map[":waning_crescent_moon:"] = "🌘"
map[":waning_gibbous_moon:"] = "🌖"
map[":warning:"] = "⚠️"
map[":watch:"] = "⌚"
map[":water_buffalo:"] = "🐃"
map[":watermelon:"] = "🍉"
map[":wave:"] = "👋"
map[":wavy_dash:"] = "〰️"
map[":waxing_crescent_moon:"] = "🌒"
map[":waxing_gibbous_moon:"] = "🌔"
map[":wc:"] = "🚾"
map[":weary:"] = "😩"
map[":wedding:"] = "💒"
map[":whale2:"] = "🐋"
map[":whale:"] = "🐳"
map[":wheelchair:"] = "♿"
map[":white_check_mark:"] = "✅"
map[":white_circle:"] = "⚪"
map[":white_flower:"] = "💮"
map[":white_square_button:"] = "🔳"
map[":wind_chime:"] = "🎐"
map[":wine_glass:"] = "🍷"
map[":wink:"] = "😉"
map[":wolf:"] = "🐺"
map[":woman:"] = "👩"
map[":womans_clothes:"] = "👚"
map[":womans_hat:"] = "👒"
map[":womens:"] = "🚺"
map[":worried:"] = "😟"
map[":wrench:"] = "🔧"
map[":x:"] = "❌"
map[":yellow_heart:"] = "💛"
map[":yen:"] = "💴"
map[":yum:"] = "😋"
map[":zap:"] = "⚡"
map[":zero:"] = "0⃣"
map[":zzz:"] = "💤"
}
{
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
for (sc in map) {
if (index(out, sc)) {
gsub(sc, map[sc], out)
}
}
for (i = 1; i <= code_count; i++) {
gsub("\034EC" i "\034", code_store[i], out)
}
$0 = out
}
if ($0 ~ /<\/pre>/) in_pre = 0
print
}

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

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

View File

@@ -3,8 +3,14 @@ function strip_markdown(s) {
gsub(/[*_`~]/, "", s)
gsub(/[\[\]]/, "", s)
gsub(/\([^\)]*\)/, "", s)
s = tolower(s)
gsub(/[^a-z0-9 -]/, "", s)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", s)
gsub(/[[:space:]]+/, "-", s)
gsub(/-{2,}/, "-", s)
gsub(/^-+|-+$/, "", s)
if (length(s) > 80) s = substr(s, 1, 80)
gsub(/-+$/, "", s)
return s
}
function print_header(line) {
@@ -32,7 +38,7 @@ BEGIN {
in_pre = 0
}
{
if ($0 ~ /^<pre><code>/) {
if ($0 ~ /^<pre><code/) {
in_pre = 1
if (has_prev && prev != "") { print_header(prev); has_prev = 0 }
print

View File

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

View File

@@ -97,6 +97,23 @@ function read_file(path, out, line, rc) {
return out
}
function read_file_or_render_md(path, ext, cmd, content, line, rc) {
content = ""
if (ext == "md") {
cmd = "sh \"" script_dir "/markdown.sh\" \"" path "\""
while ((cmd | getline line) > 0) {
content = content line "\n"
}
close(cmd)
} else {
while ((rc = getline line < path) > 0) {
content = content line "\n"
}
close(path)
}
return content
}
function escape_html(s, t) {
t = s
gsub(/&/, "\\&amp;", t)
@@ -189,7 +206,8 @@ function render_embed(src, alt, has_alt, force_inline, ext, local_path, conte
if (force_inline && !is_global_url(src)) {
local_path = resolve_local_path(src)
if (local_path != "") {
content = read_file(local_path)
ext = ext_of(src)
content = read_file_or_render_md(local_path, ext)
if (content ~ /\n$/) sub(/\n$/, "", content)
return content
}
@@ -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_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)) {
@@ -217,13 +235,35 @@ function render_embed(src, alt, has_alt, force_inline, ext, local_path, conte
if (is_inline_text_ext(ext)) {
local_path = resolve_local_path(src)
if (local_path != "") {
content = read_file(local_path)
content = read_file_or_render_md(local_path, ext)
if (content ~ /\n$/) sub(/\n$/, "", content)
return content
}
}
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) {
@@ -319,7 +359,7 @@ function apply_td_vertical_align(line, out, rest, seg, td_tag, img_tag, after
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 = ""
rest = line
while (match(rest, /<img[^>]*\/?>/)) {
@@ -329,7 +369,10 @@ function rewrite_img_tags(line, out, rest, tag, src, alt, force_inline_tag, p
src = extract_attr(tag, "src")
alt = extract_attr(tag, "alt")
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.
repl = tag
} else {

View File

@@ -20,9 +20,11 @@ function mask_html_tags(s, out, rest, start, len, tag, token) {
return out rest
}
function restore_html_tags(s, i) {
function restore_html_tags(s, i, val) {
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
}
@@ -58,6 +60,36 @@ function restore_html_tags(s, i) {
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)
while (match(line, /!!\[[^\]]*\]\([^\)]+ "[^"]*"\)/)) {
start = RSTART; len = RLENGTH
@@ -198,5 +230,7 @@ function restore_html_tags(s, i) {
}
}
gsub(/<a href="https?:\/\/[^"]*"/, "& rel=\"noopener noreferrer\"", line)
print line
}

View File

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

View File

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

View File

@@ -9,6 +9,14 @@ function replace_all(text, token, value, pos, token_len, res) {
}
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"]
lang = ENVIRON["AWK_LANG"]
if (current_url != "") {
nav = replace_all(nav, "href=\"" current_url "\"", "href=\"" current_url "\" class=\"current-page\"")
}
@@ -17,6 +25,7 @@ BEGIN {
{
line = $0
line = replace_all(line, "{{TITLE}}", title)
line = replace_all(line, "{{LANG}}", lang)
line = replace_all(line, "{{NAV}}", nav)
line = replace_all(line, "{{FOOTER}}", footer)
line = replace_all(line, "{{CSS}}", style_path)

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:]]*=/ {
print "title = \"" new_title "\""
done = 1

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

21
icon.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 639 B

535
kewt.sh
View File

@@ -13,6 +13,7 @@ Usage: $invoked_as [--from <src>] [--to <out>]
$invoked_as --new [title]
$invoked_as --update [dir]
$invoked_as --post
$invoked_as --generate-template
$invoked_as --version
$invoked_as --help
@@ -21,9 +22,12 @@ Options:
--new [title] Create a new site directory (default: site)
--update [dir] Update site.conf and template.html with latest defaults (defaults to current directory)
--post Create a new empty post file in the configured posts_dir with current date and time as name
--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
}
@@ -31,13 +35,13 @@ script_dir=$(CDPATH="" cd -- "$(dirname -- "$0")" && pwd)
awk_dir="$script_dir/awk"
KEWT_TMPDIR=$(mktemp -d "/tmp/kewt_run.XXXXXX")
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"
DEFAULT_CONF='title = "kewt"
style = "kewt"
lang = "en"
draft_by_default = false
dir_indexes = true
single_file_index = true
flatten = false
@@ -60,13 +64,11 @@ base_url = ""
generate_feed = false
feed_file = "rss.xml"
posts_dir = ""
EOF
fi
posts_per_page = 12
custom_admonitions = ""'
if [ ! -f "./template.html" ]; then
cat > "./template.html" <<'EOF'
<!doctype html>
<html>
DEFAULT_TMPL='<!doctype html>
<html lang="{{LANG}}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -77,8 +79,10 @@ EOF
</head>
<body>
<input type="checkbox" id="nav-toggle" class="nav-toggle" aria-hidden="true" />
<header>
<h1>{{HEADER_BRAND}}</h1>
<label for="nav-toggle" class="nav-toggle-label" aria-hidden="true">&#9776;</label>
</header>
<nav id="side-bar">{{NAV}}</nav>
@@ -86,9 +90,17 @@ EOF
<article>{{CONTENT}}</article>
<footer>{{FOOTER}}</footer>
</body>
</html>
EOF
fi
</html>'
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() {
@@ -98,14 +110,13 @@ create_new_site() {
[ -e "$new_dir" ] && die "Target '$new_dir' already exists."
ensure_root_defaults
mkdir -p "$new_dir"
cp "./site.conf" "$new_dir/site.conf"
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 -v new_title="$new_title" -f "$awk_dir/update_site_conf.awk" "$new_dir/site.conf" > "$new_dir/site.conf.tmp" && mv "$new_dir/site.conf.tmp" "$new_dir/site.conf"
AWK_NEW_TITLE="$new_title" awk -f "$awk_dir/update_site_conf.awk" "$new_dir/site.conf" > "$new_dir/site.conf.tmp" && mv "$new_dir/site.conf.tmp" "$new_dir/site.conf"
fi
echo "Created new site at '$new_dir'."
@@ -114,6 +125,7 @@ create_new_site() {
create_new_post() {
post_src_dir="$1"
post_user_title="$2"
target_dir="$post_src_dir"
if [ -n "$posts_dir" ]; then
@@ -133,7 +145,12 @@ create_new_post() {
counter=$((counter + 1))
done
touch "$file_path"
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
@@ -148,32 +165,7 @@ update_site() {
# Generate default site.conf
default_conf="$KEWT_TMPDIR/default_site.conf"
cat > "$default_conf" <<'CONFEOF'
title = "kewt"
style = "kewt"
dir_indexes = true
single_file_index = true
flatten = false
order = ""
home_name = "Home"
show_home_in_nav = true
nav_links = ""
nav_extra = ""
footer = "made with <a href=\"https://kewt.krzak.org\">kewt</a>"
logo = ""
display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
enable_header_links = true
base_url = ""
generate_feed = false
feed_file = "rss.xml"
posts_dir = ""
CONFEOF
printf '%s\n' "$DEFAULT_CONF" > "$default_conf"
# Update site.conf
if [ ! -f "$target_conf" ]; then
@@ -203,30 +195,7 @@ CONFEOF
# Update template.html
if [ -f "$target_tmpl" ]; then
default_tmpl="$KEWT_TMPDIR/default_template.html"
cat > "$default_tmpl" <<'TMPLEOF'
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{TITLE}}</title>
<link rel="stylesheet" href="{{CSS}}" type="text/css" />
{{HEAD_EXTRA}}
</head>
<body>
<header>
<h1>{{HEADER_BRAND}}</h1>
</header>
<nav id="side-bar">{{NAV}}</nav>
<article>{{CONTENT}}</article>
<footer>{{FOOTER}}</footer>
</body>
</html>
TMPLEOF
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
@@ -240,8 +209,6 @@ TMPLEOF
exit 0
}
src=""
out=""
new_mode="false"
@@ -249,6 +216,8 @@ new_title=""
post_mode="false"
post_title=""
positional_count=0
watch_mode="false"
serve_mode="false"
while [ $# -gt 0 ]; do
case "$1" in
@@ -269,6 +238,18 @@ while [ $# -gt 0 ]; do
;;
--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="."
@@ -288,6 +269,16 @@ while [ $# -gt 0 ]; do
out="$2"
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"
;;
@@ -307,7 +298,7 @@ done
[ "$new_mode" = "true" ] && create_new_site "$new_title"
ensure_root_defaults
if [ -z "$src" ]; then
if [ "$post_mode" = "true" ] && [ -f "./site.conf" ]; then
@@ -421,16 +412,18 @@ 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")
dinfo=$(eval "find \"$1\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -print" | sort | AWK_SRC="$1" awk -f "$awk_dir/collect_dir_info.awk")
find_cmd="find \"$1\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -name \"*.md\" -print"
if [ -n "$posts_dir" ] && [ -d "$1/$posts_dir" ]; then
find_cmd="$find_cmd && echo \"$1/$posts_dir/index.md\""
fi
eval "$find_cmd" | sort -u | awk -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"
eval "$find_cmd" | sort -u | AWK_SRC="$1" AWK_SINGLE_FILE_INDEX="$single_file_index" AWK_FLATTEN="$flatten" AWK_ORDER="$order" AWK_HOME_NAME="$home_name" AWK_SHOW_HOME_IN_NAV="$show_home_in_nav" AWK_DINFO="$dinfo" awk -f "$awk_dir/generate_sidebar.awk"
}
title="kewt"
style="kewt"
lang="en"
draft_by_default="false"
footer="made with <a href=\"https://kewt.krzak.org\">kewt</a>"
dir_indexes="true"
single_file_index="true"
@@ -454,6 +447,8 @@ base_url=""
generate_feed="false"
feed_file="rss.xml"
posts_dir=""
posts_per_page="12"
custom_admonitions=""
load_config() {
[ -f "$1" ] || return
@@ -482,7 +477,7 @@ load_config() {
case "$key" in
title) title="$val" ;;
style) style="$val" ;;
style) style="${val#/}" ;;
dir_indexes) dir_indexes="$val" ;;
single_file_index) single_file_index="$val" ;;
flatten) flatten="$val" ;;
@@ -492,19 +487,23 @@ load_config() {
nav_links) nav_links="$val" ;;
nav_extra) nav_extra="$val" ;;
footer) footer="$val" ;;
logo) logo="$val" ;;
logo) logo="${val#/}" ;;
display_logo) display_logo="$val" ;;
display_title) display_title="$val" ;;
logo_as_favicon) logo_as_favicon="$val" ;;
favicon) favicon="$val" ;;
favicon) favicon="${val#/}" ;;
generate_page_title) generate_page_title="$val" ;;
error_page) error_page="$val" ;;
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" ;;
feed_file) feed_file="${val#/}" ;;
posts_dir) posts_dir="${val#/}" ;;
posts_per_page) posts_per_page="$val" ;;
custom_admonitions) custom_admonitions="$val" ;;
lang) lang="$val" ;;
draft_by_default) draft_by_default="$val" ;;
esac
done < "$1"
}
@@ -516,7 +515,7 @@ if [ -n "$posts_dir" ]; then
HIDE_ARGS="$HIDE_ARGS -o -path '$src/$posts_dir/*'"
fi
[ "$post_mode" = "true" ] && create_new_post "$src"
[ "$post_mode" = "true" ] && create_new_post "$src" "$post_title"
asset_version=""
if [ "$versioning" = "true" ]; then
@@ -538,6 +537,26 @@ escape_html_attr() {
-e 's/>/\&gt;/g'
}
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=""
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" ;;
esac
done < "$_fm_out"
rm -f "$_fm_out"
}
nav_links_html() {
[ -n "$nav_links" ] || return
@@ -584,7 +603,10 @@ nav_links_html() {
template="$src/template.html"
[ -f "$template" ] || template="./template.html"
[ -f "$template" ] || die "Template '$template' not found."
if [ ! -f "$template" ]; then
template="$KEWT_TMPDIR/default_template.html"
printf '%s\n' "$DEFAULT_TMPL" > "$template"
fi
[ -d "$out" ] && rm -rf "$out"
mkdir -p "$out"
@@ -643,7 +665,7 @@ render_markdown() {
if [ "$rel_dir_of_file" = "$posts_dir" ]; then
temp_post_with_backlink="$KEWT_TMPDIR/post_with_backlink.md"
printf "[< Back](index.html)\n\n" > "$temp_post_with_backlink"
cat "$file" >> "$temp_post_with_backlink"
awk -f "$awk_dir/frontmatter.awk" "$file" >> "$temp_post_with_backlink"
content_file="$temp_post_with_backlink"
fi
fi
@@ -693,11 +715,21 @@ render_markdown() {
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 [ "$generate_page_title" = "true" ] && [ -n "$file" ] && [ -f "$file" ]; then
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
@@ -715,9 +747,38 @@ render_markdown() {
fi
fi
ENABLE_HEADER_LINKS="$enable_header_links" MARKDOWN_SITE_ROOT="$src" MARKDOWN_FALLBACK_FILE="$script_dir/styles/$style.css" sh "$script_dir/markdown.sh" "$content_file" | awk -v current_url="$current_url" -v title="$page_title" -v nav="$nav" -v footer="$footer" -v style_path="${style_path}${asset_version}" -v header_brand="$header_brand" -v head_extra="$head_extra" -f "$awk_dir/render_template.awk" "$local_template"
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
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="$nav" AWK_FOOTER="$footer" AWK_STYLE_PATH="${style_path}${asset_version}" AWK_HEADER_BRAND="$header_brand" AWK_HEAD_EXTRA="$head_extra" awk -f "$awk_dir/render_template.awk" "$local_template"
}
needs_rebuild() {
src_file="$1"
out_file="$2"
[ ! -f "$out_file" ] && return 0
[ "$src_file" -nt "$out_file" ] && return 0
[ -f "./site.conf" ] && [ "./site.conf" -nt "$out_file" ] && return 0
[ -f "$src/site.conf" ] && [ "$src/site.conf" -nt "$out_file" ] && return 0
[ -f "$template" ] && [ "$template" -nt "$out_file" ] && return 0
[ -f "$script_dir/styles/$style.css" ] && [ "$script_dir/styles/$style.css" -nt "$out_file" ] && return 0
return 1
}
build_site() {
echo "Building site from '$src' to '$out'..."
eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while read -r dir; do
@@ -728,35 +789,56 @@ eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while
mkdir -p "$out_dir"
if [ -f "$dir/styles.css" ]; then
if needs_rebuild "$dir/styles.css" "$out_dir/styles.css"; then
copy_style_with_resolved_vars "$dir/styles.css" "$out_dir/styles.css"
fi
elif [ -f "$dir/style.css" ]; then
if needs_rebuild "$dir/style.css" "$out_dir/styles.css"; then
copy_style_with_resolved_vars "$dir/style.css" "$out_dir/styles.css"
fi
fi
[ "$dir_indexes" != "true" ] && continue
if [ ! -f "$dir/index.md" ]; then
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 [ -n "$posts_dir" ] && { [ "$rel_dir" = "$posts_dir" ] || [ "./$rel_dir" = "$posts_dir" ]; }; then
is_posts_dir="true"
fi
if [ "$single_file_index" = "true" ] && [ "$is_posts_dir" = "false" ]; then
if [ "$single_file_index" = "true" ] && [ "$is_posts_dir" = "false" ] && [ "$has_list" = "false" ]; then
md_count=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md" | wc -l)
if [ "$md_count" -eq 1 ]; then
md_file=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md")
is_home="false"; [ "$dir" = "$src" ] && is_home="true"
target_url="/$rel_dir/index.html"
[ "$rel_dir" = "." ] && target_url="/index.html"
if needs_rebuild "$md_file" "$out_dir/index.html"; then
render_markdown "$md_file" "$is_home" "$target_url" > "$out_dir/index.html"
fi
continue
fi
fi
temp_index="$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
@@ -770,28 +852,66 @@ eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while
template.html|site.conf|style.css|index.md) continue ;;
esac
if [ -d "$entry" ]; then
echo "- [${name}/](${name}/index.html)" >> "$temp_index"
echo "- [${name}/](${name}/index.html)" >> "$temp_list"
elif [ "${entry%.md}" != "$entry" ]; then
label="${name%.md}"
# Parse frontmatter for date/title/draft
parse_frontmatter "$entry"
[ "$fm_draft" = "true" ] && continue
# Try to get first heading
post_h="$fm_title"
if [ -z "$post_h" ]; then
post_h=$(grep -m 1 '^# ' "$entry" | sed 's/^# *//')
if [ -n "$post_h" ]; then
post_h=$(echo "$post_h" | sed -e 's/\[//g' -e 's/\]//g' -e 's/!//g' -e 's/\*//g' -e 's/_//g' -e 's/`//g' -e 's/([^)]*)//g' | sed 's/\\//g')
fi
fi
is_post_entry="false"
if [ "$rel_dir" = "$posts_dir" ] || [ "./$rel_dir" = "$posts_dir" ]; then
# For posts add date and time
is_post_entry="true"
fi
if [ -n "$post_h" ]; then
if [ "$is_post_entry" = "true" ]; then
# Use frontmatter date if available, else parse from filename
if [ -n "$fm_date" ]; then
p_date=$(echo "$fm_date" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
p_time=""
if echo "$fm_date" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?[0-9]\{2\}[:\-][0-9]\{2\}'; then
p_time=$(echo "$fm_date" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
else
p_date=$(echo "${name%.md}" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
p_time="00:00"
if echo "${name%.md}" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}'; then
p_time=$(echo "${name%.md}" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
fi
if [ -n "$p_time" ]; then
label="$post_h - $p_date $p_time"
else
label="$post_h - $p_date"
fi
else
label="$post_h"
fi
elif [ "$rel_dir" = "$posts_dir" ] || [ "./$rel_dir" = "$posts_dir" ]; then
# No heading and date and time for posts
elif [ "$is_post_entry" = "true" ]; then
# No heading; use date
if [ -n "$fm_date" ]; then
p_date=$(echo "$fm_date" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
p_time=""
if echo "$fm_date" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?[0-9]\{2\}[:\-][0-9]\{2\}'; then
p_time=$(echo "$fm_date" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
if [ -n "$p_time" ]; then
label="$p_date $p_time"
else
label="$p_date"
fi
else
p_date=$(echo "${name%.md}" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
p_time="00:00"
if echo "${name%.md}" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}'; then
@@ -799,20 +919,107 @@ eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while
fi
label="$p_date $p_time"
fi
echo "- [$label](${name%.md}.html)" >> "$temp_index"
else
echo "- [$name]($name)" >> "$temp_index"
fi
done
is_home="false"; [ "$dir" = "$src" ] && is_home="true"
target_url="/$rel_dir/index.html"
[ "$rel_dir" = "." ] && target_url="/index.html"
render_markdown "$temp_index" "$is_home" "$target_url" > "$out_dir/index.html"
rm "$temp_index"
echo "- [$label](${name%.md}.html)" >> "$temp_list"
else
echo "- [$name]($name)" >> "$temp_list"
fi
done
if [ ! -f "$out/styles.css" ] && [ -f "$script_dir/styles/$style.css" ]; then
is_home="false"; [ "$dir" = "$src" ] && is_home="true"
target_url="/$rel_dir/index.html"
[ "$rel_dir" = "." ] && target_url="/index.html"
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 ))
for p in $(seq 1 $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"
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" = "true" ]; then
render_markdown "$temp_index" "$is_home" "$target_url" > "$out_dir/index.html"
fi
fi
rm -f "$temp_index" "$temp_list"
fi
done
if [ -f "$script_dir/styles/$style.css" ] && needs_rebuild "$script_dir/styles/$style.css" "$out/styles.css"; then
copy_style_with_resolved_vars "$script_dir/styles/$style.css" "$out/styles.css"
fi
@@ -826,6 +1033,10 @@ eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type f -print" | sort | while
template.html|site.conf|style.css|styles.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
@@ -842,12 +1053,21 @@ eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type f -print" | sort | while
fi
if [ "${file%.md}" != "$file" ] && [ "$is_preserved" -eq 0 ]; then
# Skip draft files
parse_frontmatter "$file"
if [ "$fm_draft" = "true" ]; then
continue
fi
is_home="false"; [ "$file" = "$src/index.md" ] && is_home="true"
out_file="$out/${rel_path%.md}.html"
if needs_rebuild "$file" "$out_file"; then
render_markdown "$file" "$is_home" > "$out_file"
fi
else
if needs_rebuild "$file" "$out/$rel_path"; then
cp "$file" "$out/$rel_path"
fi
fi
done
if [ -n "$error_page" ] && [ ! -f "$out/$error_page" ]; then
@@ -873,10 +1093,12 @@ if [ -n "$base_url" ]; then
# Don't include 404 in the sitemap (duh)
[ "${rel_url#/}" = "$error_page" ] && continue
printf ' <url>\n' >> "$sitemap_file"
printf ' <loc>%s%s</loc>\n' "$base_url" "$rel_url" >> "$sitemap_file"
printf ' <lastmod>%s</lastmod>\n' "$today" >> "$sitemap_file"
printf ' </url>\n' >> "$sitemap_file"
{
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"
@@ -888,27 +1110,43 @@ if [ "$generate_feed" = "true" ] && [ -n "$base_url" ]; then
build_date=$(date -u '+%a, %d %b %Y %H:%M:%S +0000')
printf '<?xml version="1.0" encoding="UTF-8"?>\n' > "$feed_path"
printf '<rss version="2.0">\n' >> "$feed_path"
printf ' <channel>\n' >> "$feed_path"
printf ' <title>%s</title>\n' "$title" >> "$feed_path"
printf ' <link>%s</link>\n' "$base_url_feed" >> "$feed_path"
printf ' <description>%s</description>\n' "$title" >> "$feed_path"
printf ' <lastBuildDate>%s</lastBuildDate>\n' "$build_date" >> "$feed_path"
{
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"
find "$src" -type f -name '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]*.md' -print | LC_ALL=C sort -r | while IFS= read -r post_file; do
find "$src" -type f -name '*.md' -path "*${posts_dir:-__no_posts__}*" -print | LC_ALL=C sort -r | while IFS= read -r post_file; do
post_basename=$(basename "$post_file" .md)
# Extract YYYY-MM-DD
post_date=$(echo "$post_basename" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
# Extract HH:MM if present (e.g., 2026-03-17-10:30 or 2026-03-17-10:30_1)
# Parse frontmatter
parse_frontmatter "$post_file"
[ "$fm_draft" = "true" ] && continue
# Use frontmatter date, fallback to filename
if [ -n "$fm_date" ]; then
post_date=$(echo "$fm_date" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
post_time="00:00"
if echo "$fm_date" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?[0-9]\{2\}[:\-][0-9]\{2\}'; then
post_time=$(echo "$fm_date" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
else
post_date=$(echo "$post_basename" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
post_time="00:00"
if echo "$post_basename" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}'; then
post_time=$(echo "$post_basename" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
fi
post_slug=$(echo "$post_basename" | sed -e 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}//' -e 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}//' -e 's/^[_\-]//')
post_heading="$fm_title"
if [ -z "$post_heading" ]; then
post_heading=$(grep -m 1 '^# ' "$post_file" | sed 's/^# *//')
fi
if [ -z "$post_heading" ]; then
if [ -n "$post_slug" ] && ! echo "$post_slug" | grep -q '^[0-9]\+$'; then
post_heading=$(echo "$post_slug" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)}1')
@@ -917,29 +1155,37 @@ if [ "$generate_feed" = "true" ] && [ -n "$base_url" ]; then
fi
fi
post_heading=$(echo "$post_heading" | sed -e 's/\[//g' -e 's/\]//g' -e 's/!//g' -e 's/\*//g' -e 's/_//g' -e 's/`//g' -e 's/([^)]*)//g' | sed 's/\\//g')
post_title="$post_heading - $post_date $post_time"
feed_post_title="$post_heading - $post_date $post_time"
rel_path="${post_file#"$src"}"
rel_path="${rel_path#/}"
post_url="$base_url_feed/${rel_path%.md}.html"
if date -u -d "$post_date $post_time" '+%a, %d %b %Y %H:%M:%S +0000' >/dev/null 2>&1; then
pub_date=$(date -u -d "$post_date $post_time" '+%a, %d %b %Y %H:%M:%S +0000')
else
pub_year=$(echo "$post_date" | cut -d- -f1)
pub_month=$(echo "$post_date" | cut -d- -f2)
pub_day=$(echo "$post_date" | cut -d- -f3)
# zero-padded
pub_day=$(printf '%02d' "${pub_day#0}")
case "$pub_month" in
01) pub_mon="Jan" ;; 02) pub_mon="Feb" ;; 03) pub_mon="Mar" ;;
04) pub_mon="Apr" ;; 05) pub_mon="May" ;; 06) pub_mon="Jun" ;;
07) pub_mon="Jul" ;; 08) pub_mon="Aug" ;; 09) pub_mon="Sep" ;;
10) pub_mon="Oct" ;; 11) pub_mon="Nov" ;; 12) pub_mon="Dec" ;;
esac
pub_date="${pub_day} ${pub_mon} ${pub_year} ${post_time}:00 +0000"
pub_date="Mon, ${pub_day} ${pub_mon} ${pub_year} ${post_time}:00 +0000"
fi
printf ' <item>\n' >> "$feed_path"
printf ' <title>%s</title>\n' "$post_title" >> "$feed_path"
printf ' <link>%s</link>\n' "$post_url" >> "$feed_path"
printf ' <guid>%s</guid>\n' "$post_url" >> "$feed_path"
printf ' <pubDate>%s</pubDate>\n' "$pub_date" >> "$feed_path"
printf ' </item>\n' >> "$feed_path"
{
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"
@@ -947,3 +1193,46 @@ if [ "$generate_feed" = "true" ] && [ -n "$base_url" ]; then
fi
echo "Build complete."
}
build_site
if [ "$serve_mode" = "true" ]; then
port="${serve_port:-8000}"
if command -v python3 >/dev/null 2>&1; then
python3 -m http.server "$port" -d "$out" >/dev/null 2>&1 &
server_pid=$!
echo "Serving '$out' on http://localhost:$port (python3)"
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
if [ "$watch_mode" = "true" ]; then
echo "Watching for changes in '$src'..."
touch "$KEWT_TMPDIR/watch_mark"
while true; do
sleep 1
changed="$(find "$src" -type f -newer "$KEWT_TMPDIR/watch_mark" 2>/dev/null | head -n 1)"
[ -z "$changed" ] && [ -f "site.conf" ] && [ "site.conf" -nt "$KEWT_TMPDIR/watch_mark" ] && changed="site.conf"
[ -z "$changed" ] && [ -f "$src/site.conf" ] && [ "$src/site.conf" -nt "$KEWT_TMPDIR/watch_mark" ] && changed="$src/site.conf"
[ -z "$changed" ] && [ -f "$template" ] && [ "$template" -nt "$KEWT_TMPDIR/watch_mark" ] && changed="$template"
[ -z "$changed" ] && [ -d "$script_dir/styles" ] && changed="$(find "$script_dir/styles" -type f -newer "$KEWT_TMPDIR/watch_mark" 2>/dev/null | head -n 1)"
if [ -n "$changed" ]; then
echo ""
echo "Change detected, rebuilding..."
build_site
touch "$KEWT_TMPDIR/watch_mark"
fi
done
elif [ "$serve_mode" = "true" ]; then
wait "$server_pid"
fi

View File

@@ -15,10 +15,15 @@ sed_inplace() {
fi
}
temp_file="/tmp/markdown.$$.md"
temp_file="${KEWT_TMPDIR:-/tmp}/markdown.$$.md"
cat "$@" > "$temp_file"
trap 'rm -f "$temp_file" "$temp_file.tmp"' EXIT INT TERM
trap 'rm -f "$temp_file" "$temp_file.tmp" "$temp_file.fm"' EXIT INT TERM
# Frontmatter
fm_file="$temp_file.fm"
: > "$fm_file"
awk -v fm_out="$fm_file" -f "$awk_dir/frontmatter.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# Mask
awk -f "$awk_dir/mask_inline_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
@@ -40,25 +45,37 @@ done
sed_inplace "/^\[[^\]]*\]: */d" "$temp_file"
# Blocks
sed_inplace "s/^>!\[/> [!/g" "$temp_file"
sed_inplace "s/^>\[!/> [!/g" "$temp_file"
loop_count=0
max_iterations=100
while grep '^>' "$temp_file" >/dev/null; do
awk -f "$awk_dir/blockquote.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
loop_count=$((loop_count + 1))
if [ "$loop_count" -gt "$max_iterations" ]; then
echo "Warning: Blockquote processing exceeded $max_iterations iterations on $1. Breaking to prevent infinite loop." >&2
break
fi
done
awk -f "$awk_dir/blockquote_to_admonition.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -v custom_admonitions="$CUSTOM_ADMONITIONS" -f "$awk_dir/blockquote_to_admonition.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/fenced_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/indented_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/pipe_tables.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -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"
# 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
awk -f "$awk_dir/breaks.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/paragraphs.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# Inline styles
awk -f "$awk_dir/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 -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"

7
package.json Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -4,12 +4,12 @@ pkgver=VERSION_PLACEHOLDER
pkgrel=1
pkgdesc="A minimalist, 100% POSIX, static site generator inspired by werc and kew"
arch=('any')
url="https://git.krzak.org/N0VA/kewt"
license=('MIT')
url="https://kewt.krzak.org"
license=('ISC')
depends=('sh')
provides=('kewt')
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')
build() {

View File

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

View File

@@ -0,0 +1,61 @@
# Configuration
## 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.
## 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"
---
```
- `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.
- `description` - page description, used for Open Graph `og:description` meta tag.
## 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}}` tag will be replaced with the generated list of links to child pages and files, exactly as in case the custom index didn't exist.
## Table of Contents
You can auto-generate a Table of Contents by placing `{{TOC}}` anywhere in your markdown file. It collects all `h2` and `h3` headings and generates an ordered list with anchor links.
## Footnotes
Footnotes use the `[^id]` syntax inline and `[^id]: text` for definitions at the bottom of the file. They are rendered as a numbered `<section>` at the end 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 code blocks are left as-is.

21
site/Docs/embeds.md Normal file
View File

@@ -0,0 +1,21 @@
# 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 `\![]`
## 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

3
site/Docs/index.md Normal file
View File

@@ -0,0 +1,3 @@
# Documentation
{{LIST}}

42
site/Docs/installation.md Normal file
View File

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

77
site/Docs/usage.md Normal file
View File

@@ -0,0 +1,77 @@
# 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.
- `--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. Composable with `--watch`.
## site.conf
```conf
title = "kewt"
style = "kewt"
dir_indexes = true
single_file_index = true
flatten = false
order = ""
home_name = "Home"
show_home_in_nav = true
nav_links = ""
nav_extra = ""
footer = "made with <a href=\"https://kewt.krzak.org\">kewt</a>"
logo = ""
display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
enable_header_links = true
base_url = ""
generate_feed = false
feed_file = "rss.xml"
posts_dir = ""
posts_per_page = 12
custom_admonitions = ""
```
- `title` - site title
- `style` - style file name from `./styles` (without `.css`)
- `dir_indexes` - generate directory index pages when missing `index.md`
- `single_file_index` - if a directory has one markdown file and no `index.md`, use that file as `index.html`
- `flatten` - flatten sidebar directory levels
- `order` - comma separated file/directory name list to order the sidebar (alphabetical by default)
- `home_name` - text for the home link in navigation (default: "Home")
- `show_home_in_nav` - show home link in navigation (default: true)
- `nav_links` - comma separated extra nav links, as bare URLs or Markdown links like `[Label](https://example.com)`
- `nav_extra` - raw HTML appended inside the `<nav>` after the generated link list
- `footer` - footer html/text shown at the bottom of pages
- `logo` - logo image path (used in header if enabled)
- `display_logo` - show logo in header
- `display_title` - show title text in header
- `logo_as_favicon` - use `logo` as favicon
- `favicon` - explicit favicon path (used when `logo_as_favicon` is false or no logo is set)
- `generate_page_title` - automatically generate title text from the first markdown heading or filename (default: true)
- `error_page` - filename for the generated 404 error page (default: "not_found.html", empty to disable)
- `versioning` - append a version query parameter (`?v=timestamp`) to css asset urls to bypass cache (default: false)
- `base_url` - absolute URL of the site, used for sitemap and RSS feed generation
- `generate_feed` - enable RSS feed generation (requires `base_url`)
- `feed_file` - filename for the generated RSS feed (default: "rss.xml")
- `posts_dir` - directory name containing posts (e.g., "posts"). Enables reverse-chronological sorting, title headings in indexes, and automatic backlinks.
- `posts_per_page` - number of posts per page in paginated post indexes (default: 12). Set to 0 to disable pagination.
- `enable_header_links` - turns markdown section headings into clickable anchor links (default: true)
- `custom_admonitions` - comma separated list of custom admonitions

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

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

BIN
site/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

View File

@@ -14,12 +14,15 @@ It's meant to be a static site generator, like _[kew](https://github.com/uint23/
## Features
- No dependencies
- Frontmatter support (title, date, draft, description)
- Supports many embed types
- Automatic css variable replacement for older browsers
- Automatic inlining and embedding of many filetypes with `\![link]` or `\![alt](link)`
- Typed embeds: `\!i`, `\!v`, `\!a`, `\!f`, `\!e`
- Inline html support
- MFM `$font` and `\<plain>` tags
- Admonition support (that's what the blocks like the warning block below are called)
- GFM Admonition support (that's what the blocks like the warning block below are called)
- Task list support (`- [ ]`, `- [x]`)
- RSS/Feed generation and Sitemap support
- Post creation via `--post`
- Automatic 404 page generation
@@ -27,111 +30,16 @@ It's meant to be a static site generator, like _[kew](https://github.com/uint23/
- Code block classes for use with external libraries like highlight.js or prism.js (both tested)
- Clickable markdown header anchors
- Mobile responsive layout
- 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
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/download/latest/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 --version
./kewt.sh --new [title]
./kewt.sh --post
./kewt.sh --from <src> --to <out>
./kewt.sh [src] [out]
```
`--new [title]` creates a new site directory with a copied `site.conf` and a default `index.md`.
`--post` creates a new empty markdown file in the configured `posts_dir` with the current date and time as the name.
## site.conf
```conf
title = "kewt"
style = "kewt"
dir_indexes = true
single_file_index = true
flatten = false
order = ""
home_name = "Home"
show_home_in_nav = true
nav_links = ""
nav_extra = ""
footer = "made with <a href=\"https://kewt.krzak.org\">kewt</a>"
logo = ""
display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
base_url = ""
generate_feed = false
feed_file = "rss.xml"
posts_dir = ""
enable_header_links = true
```
- `title` site title
- `style` style file name from `./styles` (without `.css`)
- `dir_indexes` generate directory index pages when missing `index.md`
- `single_file_index` if a directory has one markdown file and no `index.md`, use that file as `index.html`
- `flatten` flatten sidebar directory levels
- `order` comma separated file/directory name list to order the sidebar (alphabetical by default)
- `home_name` text for the home link in navigation (default: "Home")
- `show_home_in_nav` show home link in navigation (default: true)
- `nav_links` comma separated extra nav links, as bare URLs or Markdown links like `[Label](https://example.com)`
- `nav_extra` raw HTML appended inside the `<nav>` after the generated link list
- `footer` footer html/text shown at the bottom of pages
- `logo` logo image path (used in header if enabled)
- `display_logo` show logo in header
- `display_title` show title text in header
- `logo_as_favicon` use `logo` as favicon
- `favicon` explicit favicon path (used when `logo_as_favicon` is false or no logo is set)
- `generate_page_title` automatically generate title text from the first markdown heading or filename (default: true)
- `error_page` filename for the generated 404 error page (default: "not_found.html", empty to disable)
- `versioning` append a version query parameter (`?v=timestamp`) to css asset urls to bypass cache (default: false)
- `base_url` absolute URL of the site, used for sitemap and RSS feed generation
- `generate_feed` enable RSS feed generation (requires `base_url`)
- `feed_file` filename for the generated RSS feed (default: "rss.xml")
- `posts_dir` directory name containing posts (e.g., "posts"). Enables reverse-chronological sorting, title headings in indexes, and automatic backlinks.
- `enable_header_links` turns markdown section headings into clickable anchor links (default: true)
## 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
- _kew_ css style adapted from _[kew](https://github.com/uint23/kew)_ by [uint23](https://github.com/uint23)
***
> [!WARNING]
> The base that all of this is built upon was coded at night, while sleepy and a bit sick, and after walking for about 4 hours around a forest, so...

View File

@@ -7,15 +7,19 @@ footer = "<a href=\"https://kewt.krzak.org\"><img src=\"/button.gif\" /></a>"
logo = ""
display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
order = ""
logo_as_favicon = false
favicon = "favicon.ico"
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 = false
versioning = true
enable_header_links = true
base_url = "https://kewt.krzak.org"
custom_admonitions = ""
generate_feed = false
feed_file = "rss.xml"
posts_dir = ""

View File

@@ -252,8 +252,27 @@ hr {
border-top: 1px solid var(--code-border);
}
.nav-toggle, .nav-toggle-label {
display: none;
}
@media screen and (max-width: 600px) {
header {
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-toggle-label {
display: block;
font-size: 30px;
cursor: pointer;
color: var(--fg-heading);
user-select: none;
}
#side-bar {
display: none;
position: relative;
top: auto;
left: auto;
@@ -264,6 +283,10 @@ hr {
margin: 0 20px 20px 20px;
}
.nav-toggle:checked ~ #side-bar {
display: block;
}
article {
margin: 0 20px 0 20px;
}
@@ -274,3 +297,12 @@ hr {
padding-top: 30px;
}
}
.task-list-item {
list-style-type: none;
}
.task-list-item-checkbox {
margin: 0 0.2em 0.25em -1.6em;
vertical-align: middle;
}

View File

@@ -26,7 +26,7 @@ exit $?
#==PAYLOAD==
EOF
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "standalone")
VERSION=$(git describe --tags 2>/dev/null || echo "standalone")
tmpbuild=$(mktemp -d)
cp -r "$REPO_ROOT/kewt.sh" "$REPO_ROOT/markdown.sh" "$REPO_ROOT/awk" "$REPO_ROOT/styles" "$tmpbuild/"
sed -e "s/kewt version git/kewt version $VERSION/" "$tmpbuild/kewt.sh" > "$tmpbuild/kewt.sh.tmp" && mv "$tmpbuild/kewt.sh.tmp" "$tmpbuild/kewt.sh"