7 Commits

Author SHA1 Message Date
930e1ac4dc Fedora packaging (hopefully)
Some checks failed
Deploy Website / deploy-website (push) Successful in 39s
Publish kewt-git to AUR / publish-aur-git (push) Successful in 21s
Release Standalone Builder / build (release) Successful in 27s
Release Standalone Builder / publish-fedora (release) Failing after 15s
Release Standalone Builder / publish-aur (release) Successful in 22s
Release Standalone Builder / publish-homebrew (release) Successful in 7s
2026-05-20 10:30:10 +02:00
cd9550ee8a Add tests and refactor config 2026-05-20 08:30:21 +02:00
8b760b2897 fix: homebrew permissions on macOS (hopefully)
All checks were successful
Deploy Website / deploy-website (push) Successful in 40s
Publish kewt-git to AUR / publish-aur-git (push) Successful in 20s
Release Standalone Builder / build (release) Successful in 26s
Release Standalone Builder / publish-aur (release) Successful in 20s
Release Standalone Builder / publish-homebrew (release) Successful in 7s
2026-05-12 21:00:19 +02:00
e3bf598dd9 packaging: template cleanup 2026-05-12 20:53:13 +02:00
a086565ede fix: header links
All checks were successful
Deploy Website / deploy-website (push) Successful in 21s
Release Standalone Builder / build (release) Successful in 23s
Release Standalone Builder / publish-aur (release) Successful in 20s
Release Standalone Builder / publish-homebrew (release) Successful in 6s
2026-05-06 13:21:25 +02:00
79bc467bf6 fix: styling
Some checks failed
Deploy Website / deploy-website (push) Successful in 28s
Release Standalone Builder / build (release) Successful in 29s
Release Standalone Builder / publish-aur (release) Failing after 4m6s
Release Standalone Builder / publish-homebrew (release) Successful in 7s
2026-05-05 20:50:59 +02:00
8a5fa202de fix: rsync permissions
All checks were successful
Deploy Website / deploy-website (push) Successful in 27s
2026-05-05 20:44:47 +02:00
34 changed files with 1822 additions and 617 deletions

View File

@@ -37,6 +37,7 @@ jobs:
- name: Deploy website - name: Deploy website
run: | run: |
rsync -az --delete \ rsync -rlz --delete --omit-dir-times \
--exclude '/logs/' \
-e "ssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no" \ -e "ssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no" \
out/ "${{ secrets.REMOTE_HOST }}:/var/www/kewt.krzak.org/" out/ "${{ secrets.REMOTE_HOST }}:/var/www/kewt.krzak.org/"

View File

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

View File

@@ -20,51 +20,80 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.21' go-version: "1.21"
- name: Upload Release Asset - name: Upload Release Asset
uses: https://gitea.com/actions/release-action@main uses: https://gitea.com/actions/release-action@main
with: with:
files: |- files: |-
kewt kewt
api_key: '${{secrets.GITEA_TOKEN}}' packaging/bash/kewt.bash
api_key: "${{secrets.GITEA_TOKEN}}"
- name: Push to GitHub Release - name: Push to GitHub Release
run: | run: |
TAG="${GITHUB_REF#refs/tags/}" TAG="${GITHUB_REF#refs/tags/}"
# Fetch release body from Gitea
RELEASE_BODY=$(curl -sL \ RELEASE_BODY=$(curl -sL \
"https://git.krzak.org/api/v1/repos/N0VA/kewt/releases/tags/${TAG}" \ "https://git.krzak.org/api/v1/repos/N0VA/kewt/releases/tags/${TAG}" \
| jq -r '.body // ""') | jq -r '.body // ""')
# Build JSON payload
PAYLOAD=$(jq -n \ PAYLOAD=$(jq -n \
--arg tag "$TAG" \ --arg tag "$TAG" \
--arg name "Release $TAG" \ --arg name "Release $TAG" \
--arg body "$RELEASE_BODY" \ --arg body "$RELEASE_BODY" \
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}') '{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')
# Create the release on GitHub
curl -sL -X POST \ curl -sL -X POST \
-H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \ -H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \
-H "Accept: application/vnd.github+json" \ -H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/n0va-bot/kewt/releases" \ "https://api.github.com/repos/n0va-bot/kewt/releases" \
-d "$PAYLOAD" || true -d "$PAYLOAD" || true
# Get the release ID
RELEASE_ID=$(curl -sL \ RELEASE_ID=$(curl -sL \
-H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \ -H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \
-H "Accept: application/vnd.github+json" \ -H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/n0va-bot/kewt/releases/tags/${TAG}" | jq -r '.id') "https://api.github.com/repos/n0va-bot/kewt/releases/tags/${TAG}" | jq -r '.id')
# Upload the asset
curl -sL -X POST \ curl -sL -X POST \
-H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \ -H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \
-H "Content-Type: application/octet-stream" \ -H "Content-Type: application/octet-stream" \
"https://uploads.github.com/repos/n0va-bot/kewt/releases/${RELEASE_ID}/assets?name=kewt" \ "https://uploads.github.com/repos/n0va-bot/kewt/releases/${RELEASE_ID}/assets?name=kewt" \
--data-binary @kewt --data-binary @kewt
publish-fedora:
runs-on: local
needs: build
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
run: |
sudo apt-get update || true
sudo apt-get install -y rpm || true
- name: Build Fedora Assets
run: |
if command -v rpmbuild >/dev/null; then
make srpm
else
echo "rpmbuild not found, generating spec file only"
TAG="${GITHUB_REF#refs/tags/}"
VERSION="${TAG#v}"
sed -e "s/VERSION_PLACEHOLDER/${VERSION}/g" packaging/fedora/kewt.spec.template > packaging/fedora/kewt.spec
fi
- name: Upload Fedora Assets
uses: https://gitea.com/actions/release-action@main
with:
files: |-
kewt-*.src.rpm
packaging/fedora/kewt.spec
api_key: "${{secrets.GITEA_TOKEN}}"
publish-aur: publish-aur:
runs-on: local runs-on: local
needs: build needs: build
@@ -75,35 +104,46 @@ jobs:
- name: Setup Arch Linux environment - name: Setup Arch Linux environment
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y pacman-package-manager curl jq || true sudo apt-get install -y openssh-client curl jq
- name: Render PKGBUILD and SRCINFO - name: Render PKGBUILD and SRCINFO
run: | run: |
VERSION=${GITHUB_REF#refs/tags/v} VERSION=${GITHUB_REF#refs/tags/v}
VERSION=${VERSION#refs/tags/} VERSION=${VERSION#refs/tags/}
curl -sL -o kewt https://git.krzak.org/N0VA/kewt/releases/download/v${VERSION}/kewt curl -sL -o kewt https://git.krzak.org/N0VA/kewt/releases/download/v${VERSION}/kewt
CHECKSUM=$(sha256sum kewt | awk '{print $1}') CHECKSUM=$(sha256sum kewt | awk '{print $1}')
mkdir -p aur-work mkdir -p aur-work
sed -e "s/VERSION_PLACEHOLDER/${VERSION}/g" \ sed -e "s/VERSION_PLACEHOLDER/${VERSION}/g" \
-e "s/SHA256SUM_PLACEHOLDER/${CHECKSUM}/g" \ -e "s/SHA256SUM_PLACEHOLDER/${CHECKSUM}/g" \
packaging/AUR/PKGBUILD.template > aur-work/PKGBUILD packaging/AUR/PKGBUILD.template > aur-work/PKGBUILD
sed -e "s/VERSION_PLACEHOLDER/${VERSION}/g" \ sed -e "s/VERSION_PLACEHOLDER/${VERSION}/g" \
-e "s/SHA256SUM_PLACEHOLDER/${CHECKSUM}/g" \ -e "s/SHA256SUM_PLACEHOLDER/${CHECKSUM}/g" \
packaging/AUR/.SRCINFO.template > aur-work/.SRCINFO packaging/AUR/.SRCINFO.template > aur-work/.SRCINFO
- name: Setup AUR SSH
run: |
mkdir -p ~/.ssh
printf '%s\n' "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
- name: Publish to AUR - name: Publish to AUR
uses: KSXGitHub/github-actions-deploy-aur@v3.0.1 run: |
with: rm -rf aur-repo
pkgname: kewt-bin git clone "ssh://aur@aur.archlinux.org/kewt-bin.git" aur-repo
pkgbuild: ./aur-work/PKGBUILD cp aur-work/PKGBUILD aur-repo/PKGBUILD
commit_username: ${{ github.actor }} cp aur-work/.SRCINFO aur-repo/.SRCINFO
commit_email: ${{ github.actor }}@users.noreply.github.com
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} cd aur-repo
commit_message: "Update kewt-bin to ${{ github.ref_name }}" git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.github.com"
git add PKGBUILD .SRCINFO
git commit -m "Update kewt-bin to ${GITHUB_REF_NAME}" || exit 0
GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" git push origin HEAD:master
publish-homebrew: publish-homebrew:
runs-on: local runs-on: local

2
.gitignore vendored
View File

@@ -1,2 +1,2 @@
out/ out/
kewt kewt

1
.shellcheckrc Normal file
View File

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

View File

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

View File

@@ -23,10 +23,17 @@ function unique_id(raw_id, candidate) {
seen_ids[candidate]++ seen_ids[candidate]++
return candidate "-" seen_ids[candidate] return candidate "-" seen_ids[candidate]
} }
function has_inline_link(line) {
return (index(line, "<a ") > 0 || index(line, "<a\t") > 0)
}
function print_heading(tag, line, id) { function print_heading(tag, line, id) {
id = unique_id(strip_markdown(line)) id = unique_id(strip_markdown(line))
if (enable_header_links == "true") { if (enable_header_links == "true") {
print "<" tag " id=\"" id "\">" line " <a href=\"#" id "\" class=\"header-anchor\" aria-label=\"Link to this section\">#</a></" tag ">" if (has_inline_link(line)) {
print "<" tag " id=\"" id "\">" line " <a href=\"#" id "\" class=\"header-anchor\" aria-label=\"Link to this section\">#</a></" tag ">"
} else {
print "<" tag " id=\"" id "\"><a href=\"#" id "\" class=\"header-link\">" line "<span class=\"header-anchor\" aria-hidden=\"true\">#</span></a></" tag ">"
}
} else { } else {
print "<" tag " id=\"" id "\">" line "</" tag ">" print "<" tag " id=\"" id "\">" line "</" tag ">"
} }

119
awk/reference_links.awk Normal file
View File

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

24
kewt.sh
View File

@@ -45,6 +45,8 @@ positional_count=0
watch_mode="false" watch_mode="false"
serve_mode="false" serve_mode="false"
serve_port="" serve_port=""
draft_mode="false"
dry_run_mode="false"
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
@@ -89,6 +91,8 @@ _kewt() {
'--to[Output directory]:directory:_directories' '--to[Output directory]:directory:_directories'
'(-w --watch)'{-w,--watch}'[Watch for file changes and rebuild automatically]' '(-w --watch)'{-w,--watch}'[Watch for file changes and rebuild automatically]'
'(-s --serve)'{-s,--serve}'[Start a local HTTP server after building]::port:' '(-s --serve)'{-s,--serve}'[Start a local HTTP server after building]::port:'
'(-d --draft)'{-d,--draft}'[Include draft pages in the build]'
'(-)--dry-run[Show what would be built without writing any files]'
) )
_arguments -S -C $args '*: :_directories' _arguments -S -C $args '*: :_directories'
@@ -141,6 +145,12 @@ EOFCOMPS
shift shift
fi fi
;; ;;
--draft|-d)
draft_mode="true"
;;
--dry-run)
dry_run_mode="true"
;;
--*) --*)
die "Unknown option: $1" die "Unknown option: $1"
;; ;;
@@ -201,10 +211,24 @@ refresh_build_context
if [ "$clean_mode" = "true" ]; then if [ "$clean_mode" = "true" ]; then
[ -d "$out" ] && rm -rf "$out" [ -d "$out" ] && rm -rf "$out"
fi fi
if [ "$dry_run_mode" = "true" ]; then
dry_run_out="$KEWT_TMPDIR/dry_run_out"
mkdir -p "$dry_run_out"
out="$dry_run_out"
fi
mkdir -p "$out" mkdir -p "$out"
build_site build_site
if [ "$dry_run_mode" = "true" ]; then
echo ""
echo "Dry run complete. Files that would be generated:"
find "$dry_run_out" -type f | sed "s|^$dry_run_out/||" | sort
exit 0
fi
if [ "$serve_mode" = "true" ]; then if [ "$serve_mode" = "true" ]; then
port="${serve_port:-8000}" port="${serve_port:-8000}"
if command -v python3 >/dev/null 2>&1; then if command -v python3 >/dev/null 2>&1; then

View File

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

View File

@@ -1,4 +1,5 @@
#!/bin/sh #!/bin/sh
# shellcheck disable=SC2153
usage() { usage() {
invoked_as=$(basename "${KEWT_INVOKED_AS:-$0}") invoked_as=$(basename "${KEWT_INVOKED_AS:-$0}")
@@ -21,6 +22,8 @@ Options:
--post Create a new empty post file in the configured posts_dir with current date and time as name --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) --generate-template [path] Generate a new template file at <path> (default: template.html)
--version Show version information. --version Show version information.
--draft, -d Include draft pages in the build.
--dry-run Show what would be built without writing any files.
--from <src> Source directory (default: site) --from <src> Source directory (default: site)
--to <out> Output directory (default: out) --to <out> Output directory (default: out)
--watch, -w Watch for file changes and rebuild automatically. --watch, -w Watch for file changes and rebuild automatically.
@@ -97,11 +100,9 @@ update_site() {
target_conf="$update_dir/site.conf" target_conf="$update_dir/site.conf"
target_tmpl="$update_dir/template.html" target_tmpl="$update_dir/template.html"
# Generate default site.conf
default_conf="$KEWT_TMPDIR/default_site.conf" default_conf="$KEWT_TMPDIR/default_site.conf"
printf '%s\n' "$DEFAULT_CONF" > "$default_conf" printf '%s\n' "$DEFAULT_CONF" > "$default_conf"
# Update site.conf
if [ ! -f "$target_conf" ]; then if [ ! -f "$target_conf" ]; then
echo "No site.conf found in '$update_dir'; nothing to update." echo "No site.conf found in '$update_dir'; nothing to update."
else else
@@ -126,7 +127,6 @@ update_site() {
fi fi
fi fi
# Update template.html
if [ -f "$target_tmpl" ]; then if [ -f "$target_tmpl" ]; then
default_tmpl="$KEWT_TMPDIR/default_template.html" default_tmpl="$KEWT_TMPDIR/default_template.html"
printf '%s\n' "$DEFAULT_TMPL" > "$default_tmpl" printf '%s\n' "$DEFAULT_TMPL" > "$default_tmpl"

View File

@@ -34,7 +34,8 @@ tags_dir = "tags"
generate_search = false generate_search = false
search_in_footer = false search_in_footer = false
search_in_header = false search_in_header = false
include_cw_pages_in_search = false' include_cw_pages_in_search = false
feed_full_content = false'
DEFAULT_TMPL='<!doctype html> DEFAULT_TMPL='<!doctype html>
<html lang="{{LANG}}"> <html lang="{{LANG}}">
@@ -62,106 +63,89 @@ DEFAULT_TMPL='<!doctype html>
</body> </body>
</html>' </html>'
_parse_conf_val() {
_pv_val="$1"
case "$_pv_val" in
\"*\")
_pv_val=${_pv_val#\"}
_pv_val=${_pv_val%\"}
printf '%s' "$_pv_val" | sed 's/\\"/\"/g; s/\\\\/\\/g'
;;
\'*\')
_pv_val=${_pv_val#\'}
_pv_val=${_pv_val%\'}
printf '%s' "$_pv_val" | sed "s/\\\\'/'/g; s/\\\\/\\/g"
;;
*)
printf '%s' "$_pv_val"
;;
esac
}
_load_conf_line() {
case "$1" in
''|'#'*) return ;;
*=*) ;;
*) return ;;
esac
_lc_key=${1%%=*}
_lc_val=${1#*=}
_lc_key=$(printf '%s' "$_lc_key" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
_lc_val=$(_parse_conf_val "$(printf '%s' "$_lc_val" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')")
case "$_lc_key" in
title) title="$_lc_val" ;;
style) style="${_lc_val#/}" ;;
dir_indexes) dir_indexes="$_lc_val" ;;
single_file_index) single_file_index="$_lc_val" ;;
flatten) flatten="$_lc_val" ;;
order) order="$_lc_val" ;;
home_name) home_name="$_lc_val" ;;
show_home_in_nav) show_home_in_nav="$_lc_val" ;;
nav_links) nav_links="$_lc_val" ;;
nav_extra) nav_extra="$_lc_val" ;;
footer) footer="$_lc_val" ;;
logo) logo="${_lc_val#/}" ;;
display_logo) display_logo="$_lc_val" ;;
display_title) display_title="$_lc_val" ;;
logo_as_favicon) logo_as_favicon="$_lc_val" ;;
favicon) favicon="${_lc_val#/}" ;;
generate_page_title) generate_page_title="$_lc_val" ;;
error_page) error_page="${_lc_val#/}" ;;
versioning) versioning="$_lc_val" ;;
enable_header_links) enable_header_links="$_lc_val" ;;
base_url) base_url="$_lc_val" ;;
generate_feed) generate_feed="$_lc_val" ;;
feed_file) feed_file="${_lc_val#/}" ;;
posts_dir) posts_dir="${_lc_val#/}" ;;
posts_per_page) posts_per_page="$_lc_val" ;;
custom_admonitions) custom_admonitions="$_lc_val" ;;
cw_hide_url) cw_hide_url="$_lc_val" ;;
lang) lang="$_lc_val" ;;
draft_by_default) draft_by_default="$_lc_val" ;;
generate_tags) generate_tags="$_lc_val" ;;
tags_dir) tags_dir="${_lc_val#/}" ;;
generate_search) generate_search="$_lc_val" ;;
search_in_footer) search_in_footer="$_lc_val" ;;
search_in_header) search_in_header="$_lc_val" ;;
include_cw_pages_in_search) include_cw_pages_in_search="$_lc_val" ;;
feed_full_content) feed_full_content="$_lc_val" ;;
esac
}
reset_config() { reset_config() {
title="kewt" while IFS= read -r _rc_line; do
style="kewt" _load_conf_line "$_rc_line"
lang="en" done <<EOF
draft_by_default="false" $DEFAULT_CONF
dir_indexes="true" EOF
single_file_index="true"
flatten="false"
order=""
home_name="Home"
show_home_in_nav="true"
nav_links=""
nav_extra=""
footer="made with <a href=\"https://kewt.krzak.org\">kewt</a>"
logo=""
display_logo="false"
display_title="true"
logo_as_favicon="true"
favicon=""
generate_page_title="true"
error_page="not_found.html"
versioning="false"
enable_header_links="true"
base_url=""
generate_feed="false"
feed_file="rss.xml"
posts_dir=""
posts_per_page="12"
custom_admonitions=""
cw_hide_url="true"
generate_tags="false"
tags_dir="tags"
generate_search="false"
search_in_footer="false"
search_in_header="false"
include_cw_pages_in_search="false"
} }
load_config() { load_config() {
[ -f "$1" ] || return [ -f "$1" ] || return
while IFS= read -r line || [ -n "$line" ]; do while IFS= read -r _lc_line || [ -n "$_lc_line" ]; do
case "$line" in _load_conf_line "$_lc_line"
''|'#'*) continue ;;
*=*) ;;
*) continue ;;
esac
key=${line%%=*}
val=${line#*=}
key=$(printf '%s' "$key" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
val=$(printf '%s' "$val" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$val" in
\"*\")
val=${val#\"}; val=${val%\"}
val=$(printf '%s' "$val" | sed 's/\\"/\"/g; s/\\\\/\\/g')
;;
\'*\')
val=${val#\'}; val=${val%\'}
val=$(printf '%s' "$val" | sed "s/\\\\'/'/g; s/\\\\/\\/g")
;;
esac
case "$key" in
title) title="$val" ;;
style) style="${val#/}" ;;
dir_indexes) dir_indexes="$val" ;;
single_file_index) single_file_index="$val" ;;
flatten) flatten="$val" ;;
order) order="$val" ;;
home_name) home_name="$val" ;;
show_home_in_nav) show_home_in_nav="$val" ;;
nav_links) nav_links="$val" ;;
nav_extra) nav_extra="$val" ;;
footer) footer="$val" ;;
logo) logo="${val#/}" ;;
display_logo) display_logo="$val" ;;
display_title) display_title="$val" ;;
logo_as_favicon) logo_as_favicon="$val" ;;
favicon) favicon="${val#/}" ;;
generate_page_title) generate_page_title="$val" ;;
error_page) error_page="${val#/}" ;;
versioning) versioning="$val" ;;
enable_header_links) enable_header_links="$val" ;;
base_url) base_url="$val" ;;
generate_feed) generate_feed="$val" ;;
feed_file) feed_file="${val#/}" ;;
posts_dir) posts_dir="${val#/}" ;;
posts_per_page) posts_per_page="$val" ;;
custom_admonitions) custom_admonitions="$val" ;;
cw_hide_url) cw_hide_url="$val" ;;
lang) lang="$val" ;;
draft_by_default) draft_by_default="$val" ;;
generate_tags) generate_tags="$val" ;;
tags_dir) tags_dir="${val#/}" ;;
generate_search) generate_search="$val" ;;
search_in_footer) search_in_footer="$val" ;;
search_in_header) search_in_header="$val" ;;
include_cw_pages_in_search) include_cw_pages_in_search="$val" ;;
esac
done < "$1" done < "$1"
} }

View File

@@ -1,4 +1,5 @@
#!/bin/sh #!/bin/sh
# shellcheck disable=SC2086
SEARCH_FORM_FOOTER='<form class="kewt-search-footer" action="/search.html" method="get"><input type="text" name="q" placeholder="Search..." required><button type="submit">Go</button></form>' SEARCH_FORM_FOOTER='<form class="kewt-search-footer" action="/search.html" method="get"><input type="text" name="q" placeholder="Search..." required><button type="submit">Go</button></form>'
SEARCH_FORM_HEADER='<form class="kewt-search-header" action="/search.html" method="get"><input type="text" name="q" placeholder="Search..." required><button type="submit">Go</button></form>' SEARCH_FORM_HEADER='<form class="kewt-search-header" action="/search.html" method="get"><input type="text" name="q" placeholder="Search..." required><button type="submit">Go</button></form>'
@@ -37,12 +38,14 @@ generate_nav() {
sort -u "$nav_input" | AWK_SRC="$1" AWK_SINGLE_FILE_INDEX="$single_file_index" AWK_FLATTEN="$flatten" AWK_ORDER="$order" AWK_HOME_NAME="$home_name" AWK_SHOW_HOME_IN_NAV="$show_home_in_nav" AWK_DINFO="$dinfo" awk -f "$awk_dir/generate_sidebar.awk" sort -u "$nav_input" | AWK_SRC="$1" AWK_SINGLE_FILE_INDEX="$single_file_index" AWK_FLATTEN="$flatten" AWK_ORDER="$order" AWK_HOME_NAME="$home_name" AWK_SHOW_HOME_IN_NAV="$show_home_in_nav" AWK_DINFO="$dinfo" awk -f "$awk_dir/generate_sidebar.awk"
} }
escape_html_text() { escape_html_text() {
printf '%s' "$1" | sed \ printf '%s' "$1" | sed \
-e 's/&/\&amp;/g' \ -e 's/&/\&amp;/g' \
-e 's/</\&lt;/g' \ -e 's/</\&lt;/g' \
-e 's/>/\&gt;/g' -e 's/>/\&gt;/g'
} }
escape_html_attr() { escape_html_attr() {
printf '%s' "$1" | sed \ printf '%s' "$1" | sed \
-e 's/&/\&amp;/g' \ -e 's/&/\&amp;/g' \
@@ -50,13 +53,13 @@ escape_html_attr() {
-e 's/</\&lt;/g' \ -e 's/</\&lt;/g' \
-e 's/>/\&gt;/g' -e 's/>/\&gt;/g'
} }
nav_links_html() { nav_links_html() {
[ -n "$nav_links" ] || return [ -n "$nav_links" ] || return
old_ifs=$IFS old_ifs=$IFS
set -f set -f
IFS=',' IFS=','
# shellcheck disable=SC2086
set -- $nav_links set -- $nav_links
IFS=$old_ifs IFS=$old_ifs
set +f set +f
@@ -93,6 +96,7 @@ nav_links_html() {
done done
printf '</ul>' printf '</ul>'
} }
find_closest() { find_closest() {
target="$1" target="$1"
start_dir="$2" start_dir="$2"
@@ -108,11 +112,13 @@ find_closest() {
echo "$src/$target" echo "$src/$target"
fi fi
} }
copy_style_with_resolved_vars() { copy_style_with_resolved_vars() {
src_style="$1" src_style="$1"
out_style="$2" out_style="$2"
awk -f "$awk_dir/replace_variables.awk" "$src_style" > "$out_style" awk -f "$awk_dir/replace_variables.awk" "$src_style" > "$out_style"
} }
merge_root_style() { merge_root_style() {
root_file="$1" root_file="$1"
base_css="$2" base_css="$2"
@@ -134,51 +140,15 @@ merge_root_style() {
' "$base_css" ' "$base_css"
} | awk -f "$awk_dir/replace_variables.awk" > "$out_file" } | awk -f "$awk_dir/replace_variables.awk" > "$out_file"
} }
render_markdown() {
file="$1"
is_home="$2"
url_override="$3"
if [ -n "$url_override" ]; then resolve_render_template() {
current_url="$url_override" local_template=$(find_closest "template.html" "$(dirname "$1")")
else
rel_path="${file#"$src"}"
rel_path="${rel_path#/}"
current_url=$(markdown_file_url "$rel_path")
fi
content_file="$file"
if [ -n "$posts_dir" ] && [ "$file" != "$src/$posts_dir/index.md" ]; then
rel_dir_of_url=$(dirname "$current_url")
rel_dir_of_url="${rel_dir_of_url#/}"
if { [ "$rel_dir_of_url" = "$posts_dir" ] || [ "./$rel_dir_of_url" = "$posts_dir" ]; } && [ "$(basename "$current_url")" != "index.html" ]; then
temp_post_with_backlink="$KEWT_TMPDIR/post_with_backlink_$$.md"
printf "[< Back](index.html)\n\n" > "$temp_post_with_backlink"
awk -f "$awk_dir/frontmatter.awk" "$file" >> "$temp_post_with_backlink"
post_md_name="$(basename "$current_url" .html).md"
prevnext_file="$KEWT_TMPDIR/prevnext/$post_md_name"
if [ -f "$prevnext_file" ]; then
IFS='|' read -r prev_str next_str < "$prevnext_file"
printf "\n\n---\n<div class=\"post-nav\">\n" >> "$temp_post_with_backlink"
if [ -n "$prev_str" ]; then
printf "<span class=\"prev-post\">%s</span>\n" "$prev_str" >> "$temp_post_with_backlink"
fi
if [ -n "$next_str" ]; then
printf "<span class=\"next-post\">%s</span>\n" "$next_str" >> "$temp_post_with_backlink"
fi
printf "</div>\n" >> "$temp_post_with_backlink"
fi
content_file="$temp_post_with_backlink"
fi
fi
local_template=$(find_closest "template.html" "$(dirname "$file")")
[ -z "$local_template" ] && local_template="$template" [ -z "$local_template" ] && local_template="$template"
}
closest_style_src=$(find_closest "styles.css" "$(dirname "$file")") resolve_render_style_path() {
[ -z "$closest_style_src" ] && closest_style_src=$(find_closest "style.css" "$(dirname "$file")") closest_style_src=$(find_closest "styles.css" "$(dirname "$1")")
[ -z "$closest_style_src" ] && closest_style_src=$(find_closest "style.css" "$(dirname "$1")")
if [ -n "$closest_style_src" ]; then if [ -n "$closest_style_src" ]; then
style_rel_to_src="${closest_style_src#"$src"/}" style_rel_to_src="${closest_style_src#"$src"/}"
case "$closest_style_src" in case "$closest_style_src" in
@@ -190,7 +160,9 @@ render_markdown() {
else else
style_path="/styles.css" style_path="/styles.css"
fi fi
}
build_header_brand_html() {
logo_html="" logo_html=""
if [ "$display_logo" = "true" ] && [ -n "$logo" ]; then if [ "$display_logo" = "true" ] && [ -n "$logo" ]; then
logo_html="<img class=\"site-logo\" src=\"$logo\" alt=\"$title\" />" logo_html="<img class=\"site-logo\" src=\"$logo\" alt=\"$title\" />"
@@ -210,26 +182,27 @@ render_markdown() {
else else
header_brand="<a href=\"/index.html\">$title</a>" header_brand="<a href=\"/index.html\">$title</a>"
fi fi
}
build_favicon_head() {
favicon_src="" favicon_src=""
if [ "$logo_as_favicon" = "true" ] && [ -n "$logo" ]; then if [ "$logo_as_favicon" = "true" ] && [ -n "$logo" ]; then
favicon_src="$logo" favicon_src="$logo"
elif [ -n "$favicon" ]; then elif [ -n "$favicon" ]; then
favicon_src="$favicon" favicon_src="$favicon"
fi fi
head_extra=""
if [ -n "$favicon_src" ]; then if [ -n "$favicon_src" ]; then
if echo "$favicon_src" | grep -q "^http"; then case "$favicon_src" in
head_extra="<link rel=\"icon\" href=\"$favicon_src\" />" http*|/*) head_extra="<link rel=\"icon\" href=\"$favicon_src\" />" ;;
elif echo "$favicon_src" | grep -q "^/"; then *) head_extra="<link rel=\"icon\" href=\"/$favicon_src\" />" ;;
head_extra="<link rel=\"icon\" href=\"$favicon_src\" />" esac
else else
head_extra="<link rel=\"icon\" href=\"/$favicon_src\" />" head_extra=""
fi
fi fi
}
parse_frontmatter "$file" build_page_title() {
page_title="$title" page_title="$title"
if [ -n "$fm_title" ]; then if [ -n "$fm_title" ]; then
page_title="$fm_title - $title" page_title="$fm_title - $title"
@@ -250,7 +223,9 @@ render_markdown() {
fi fi
fi fi
fi fi
}
build_og_tags() {
head_extra_og="<meta property=\"og:title\" content=\"$(escape_html_attr "$page_title")\" />" head_extra_og="<meta property=\"og:title\" content=\"$(escape_html_attr "$page_title")\" />"
if [ -n "$fm_description" ]; then if [ -n "$fm_description" ]; then
head_extra_og="$head_extra_og head_extra_og="$head_extra_og
@@ -266,17 +241,23 @@ render_markdown() {
else else
head_extra="$head_extra_og" head_extra="$head_extra_og"
fi fi
}
build_cw_url_hide() {
if [ "$is_cw_content_page" = "true" ] && [ "$cw_hide_url" = "true" ]; then if [ "$is_cw_content_page" = "true" ] && [ "$cw_hide_url" = "true" ]; then
head_extra="$head_extra head_extra="$head_extra
<script>window.history.replaceState(null, '', '$current_url');</script>" <script>window.history.replaceState(null, '', '$current_url');</script>"
fi fi
}
build_composed_footer() {
final_footer="$footer" final_footer="$footer"
if [ "$search_in_footer" = "true" ]; then if [ "$search_in_footer" = "true" ]; then
final_footer="$footer $SEARCH_FORM_FOOTER" final_footer="$footer $SEARCH_FORM_FOOTER"
fi fi
}
build_composed_nav() {
final_nav="$nav" final_nav="$nav"
final_header_brand="$header_brand" final_header_brand="$header_brand"
final_header_search="" final_header_search=""
@@ -285,9 +266,65 @@ render_markdown() {
final_nav="$SEARCH_FORM_NAV final_nav="$SEARCH_FORM_NAV
$nav" $nav"
fi fi
}
prepare_post_content() {
content_file="$file"
if [ -n "$posts_dir" ] && [ "$file" != "$src/$posts_dir/index.md" ]; then
rel_dir_of_url=$(dirname "$current_url")
rel_dir_of_url="${rel_dir_of_url#/}"
if { [ "$rel_dir_of_url" = "$posts_dir" ] || [ "./$rel_dir_of_url" = "$posts_dir" ]; } && [ "$(basename "$current_url")" != "index.html" ]; then
temp_post_with_backlink="$KEWT_TMPDIR/post_with_backlink_$$.md"
printf "[< Back](index.html)\n\n" > "$temp_post_with_backlink"
awk -f "$awk_dir/frontmatter.awk" "$file" >> "$temp_post_with_backlink"
post_md_name="$(basename "$current_url" .html).md"
prevnext_file="$KEWT_TMPDIR/prevnext/$post_md_name"
if [ -f "$prevnext_file" ]; then
IFS='|' read -r prev_str next_str < "$prevnext_file"
printf "\n\n---\n<div class=\"post-nav\">\n" >> "$temp_post_with_backlink"
if [ -n "$prev_str" ]; then
printf "<span class=\"prev-post\">%s</span>\n" "$prev_str" >> "$temp_post_with_backlink"
fi
if [ -n "$next_str" ]; then
printf "<span class=\"next-post\">%s</span>\n" "$next_str" >> "$temp_post_with_backlink"
fi
printf "</div>\n" >> "$temp_post_with_backlink"
fi
content_file="$temp_post_with_backlink"
fi
fi
}
render_markdown() {
file="$1"
is_home="$2"
url_override="$3"
if [ -n "$url_override" ]; then
current_url="$url_override"
else
rel_path="${file#"$src"}"
rel_path="${rel_path#/}"
current_url=$(markdown_file_url "$rel_path")
fi
prepare_post_content
resolve_render_template "$file"
resolve_render_style_path "$file"
build_header_brand_html
build_favicon_head
parse_frontmatter "$file"
build_page_title
build_og_tags
build_cw_url_hide
build_composed_footer
build_composed_nav
ENABLE_HEADER_LINKS="$enable_header_links" CUSTOM_ADMONITIONS="$custom_admonitions" MARKDOWN_SITE_ROOT="$src" MARKDOWN_FALLBACK_FILE="$script_dir/styles/$style.css" sh "$script_dir/markdown.sh" "$content_file" | AWK_LANG="$lang" AWK_CURRENT_URL="$current_url" AWK_TITLE="$page_title" AWK_NAV="$final_nav" AWK_FOOTER="$final_footer" AWK_STYLE_PATH="${style_path}" AWK_HEADER_BRAND="$final_header_brand" AWK_HEADER_SEARCH="$final_header_search" AWK_HEAD_EXTRA="$head_extra" AWK_VERSION="$asset_version" AWK_CONTENT_WARNING="$fm_content_warning" awk -f "$awk_dir/render_template.awk" "$local_template" ENABLE_HEADER_LINKS="$enable_header_links" CUSTOM_ADMONITIONS="$custom_admonitions" MARKDOWN_SITE_ROOT="$src" MARKDOWN_FALLBACK_FILE="$script_dir/styles/$style.css" sh "$script_dir/markdown.sh" "$content_file" | AWK_LANG="$lang" AWK_CURRENT_URL="$current_url" AWK_TITLE="$page_title" AWK_NAV="$final_nav" AWK_FOOTER="$final_footer" AWK_STYLE_PATH="${style_path}" AWK_HEADER_BRAND="$final_header_brand" AWK_HEADER_SEARCH="$final_header_search" AWK_HEAD_EXTRA="$head_extra" AWK_VERSION="$asset_version" AWK_CONTENT_WARNING="$fm_content_warning" awk -f "$awk_dir/render_template.awk" "$local_template"
} }
generate_content_warning_page() { generate_content_warning_page() {
_fm_title="$1" _fm_title="$1"
_fm_content_warning="$2" _fm_content_warning="$2"

View File

@@ -1,4 +1,5 @@
#!/bin/sh #!/bin/sh
# shellcheck disable=SC2016,SC2030,SC2031
shell_quote() { shell_quote() {
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
@@ -186,8 +187,10 @@ build_markdown_manifest() {
eval "find \"$src\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -name \"*.md\" -print" | sort | while IFS= read -r visible_file; do eval "find \"$src\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -name \"*.md\" -print" | sort | while IFS= read -r visible_file; do
visible_rel_path="${visible_file#"$src"/}" visible_rel_path="${visible_file#"$src"/}"
load_manifest_entry "$visible_rel_path" || continue load_manifest_entry "$visible_rel_path" || continue
[ "$manifest_draft" = "true" ] && continue if [ "${draft_mode:-false}" != "true" ]; then
manifest_dir_hidden_by_draft_index "$manifest_dir_rel" && continue [ "$manifest_draft" = "true" ] && continue
manifest_dir_hidden_by_draft_index "$manifest_dir_rel" && continue
fi
printf '%s\n' "$visible_rel_path" >> "$manifest_visible_list" printf '%s\n' "$visible_rel_path" >> "$manifest_visible_list"
done done
} }

View File

@@ -3,14 +3,11 @@
script_dir=$(CDPATH="" cd -- "$(dirname -- "$0")" && pwd) script_dir=$(CDPATH="" cd -- "$(dirname -- "$0")" && pwd)
awk_dir="$script_dir/awk" awk_dir="$script_dir/awk"
sed_inplace() { run_awk() {
script="$1" _ra_awk_file="$1"
file="$2" shift
tmp="${file}.tmp.$$" if ! awk -f "$_ra_awk_file" "$@"; then
if sed "$script" "$file" > "$tmp" && mv "$tmp" "$file"; then echo "Error: AWK failed: $_ra_awk_file" >&2
return 0
else
rm -f "$tmp"
return 1 return 1
fi fi
} }
@@ -21,36 +18,20 @@ cat "$@" > "$temp_file"
trap 'rm -f "$temp_file" "$temp_file.tmp" "$temp_file.fm"' 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="$temp_file.fm"
: > "$fm_file" : > "$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 run_awk "$awk_dir/frontmatter.awk" -v fm_out="$fm_file" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/mask_inline_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/mask_plain.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# Reference links run_awk "$awk_dir/mask_inline_code.awk" "$temp_file" \
refs=$(awk '/^\[[^\]]+\]: */' "$temp_file") | run_awk "$awk_dir/mask_plain.awk" \
IFS=' | run_awk "$awk_dir/reference_links.awk" \
' > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
for ref in $refs; do
ref_id=$(echo "$ref" | sed 's/^\[\(.*\)\]: .*/\1/')
ref_url=$(echo "$ref" | sed 's/^\[.*\]: \([^ ]*\).*/\1/')
ref_title=$(echo "$ref" | sed -n 's/^\[.*\]: [^ ]* "\(.*\)"/\1/p' | sed 's@|@!@g')
sed_inplace "s|!\[\([^]]*\)\]\[$ref_id\]|<img src=\"$ref_url\" title=\"$ref_title\" alt=\"\1\" />|g" "$temp_file"
sed_inplace "s|\[\([^]]*\)\]\[$ref_id\]|<a href=\"$ref_url\" title=\"$ref_title\">\1</a>|g" "$temp_file"
sed_inplace "s|!\[$ref_id\]\[\]|<img src=\"$ref_url\" title=\"$ref_title\" alt=\"$ref_id\" />|g" "$temp_file"
sed_inplace "s|\[$ref_id\]\[\]|<a href=\"$ref_url\" title=\"$ref_title\">$ref_id</a>|g" "$temp_file"
done
sed_inplace "/^\[[^\]]*\]: */d" "$temp_file"
# Blocks
loop_count=0 loop_count=0
max_iterations=100 max_iterations=100
while grep '^>' "$temp_file" >/dev/null; do while grep -q '^>' "$temp_file"; do
awk -f "$awk_dir/blockquote.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" run_awk "$awk_dir/blockquote.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
loop_count=$((loop_count + 1)) loop_count=$((loop_count + 1))
if [ "$loop_count" -gt "$max_iterations" ]; then if [ "$loop_count" -gt "$max_iterations" ]; then
echo "Warning: Blockquote processing exceeded $max_iterations iterations on $1. Breaking to prevent infinite loop." >&2 echo "Warning: Blockquote processing exceeded $max_iterations iterations on $1. Breaking to prevent infinite loop." >&2
@@ -58,25 +39,19 @@ while grep '^>' "$temp_file" >/dev/null; do
fi fi
done done
awk -v custom_admonitions="$CUSTOM_ADMONITIONS" -f "$awk_dir/blockquote_to_admonition.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" run_awk "$awk_dir/blockquote_to_admonition.awk" -v custom_admonitions="$CUSTOM_ADMONITIONS" "$temp_file" \
awk -f "$awk_dir/fenced_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" | run_awk "$awk_dir/fenced_code.awk" \
awk -f "$awk_dir/indented_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" | run_awk "$awk_dir/indented_code.awk" \
awk -f "$awk_dir/pipe_tables.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" | run_awk "$awk_dir/pipe_tables.awk" \
awk -v enable_header_links="$ENABLE_HEADER_LINKS" -f "$awk_dir/headers.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" | run_awk "$awk_dir/definition_lists.awk" \
awk -f "$awk_dir/definition_lists.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" | run_awk "$awk_dir/lists.awk" \
awk -f "$awk_dir/lists.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" | run_awk "$awk_dir/toc.awk" \
| run_awk "$awk_dir/footnotes.awk" \
| run_awk "$awk_dir/breaks.awk" \
| run_awk "$awk_dir/paragraphs.awk" \
| run_awk "$awk_dir/emoji.awk" -v emoji_file="$awk_dir/emoji.tsv" \
| run_awk "$awk_dir/markdown_inline.awk" \
| run_awk "$awk_dir/headers.awk" -v enable_header_links="$ENABLE_HEADER_LINKS" \
| run_awk "$awk_dir/markdown_embed.awk" -v input_file="$1" -v site_root="$MARKDOWN_SITE_ROOT" -v fallback_file="$MARKDOWN_FALLBACK_FILE" -v script_dir="$script_dir"
# TOC rm -f "$temp_file"
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 -v emoji_file="$awk_dir/emoji.tsv" -f "$awk_dir/emoji.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/markdown_inline.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -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"

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,9 @@ license=('ISC')
depends=('sh') depends=('sh')
provides=('kewt') provides=('kewt')
conflicts=('kewt' 'kewt-git') conflicts=('kewt' 'kewt-git')
source=("${pkgname}-${pkgver}.sh::https://git.krzak.org/N0VA/kewt/releases/download/v${pkgver}/kewt") source=("${pkgname}-${pkgver}.sh::https://git.krzak.org/N0VA/kewt/releases/download/v${pkgver}/kewt"
sha256sums=('SHA256SUM_PLACEHOLDER') "${pkgname}-${pkgver}.bash::https://git.krzak.org/N0VA/kewt/releases/download/v${pkgver}/kewt.bash")
sha256sums=('SHA256SUM_PLACEHOLDER' 'SKIP')
build() { build() {
chmod +x "${srcdir}/${pkgname}-${pkgver}.sh" chmod +x "${srcdir}/${pkgname}-${pkgver}.sh"
@@ -20,4 +21,5 @@ package() {
install -Dm755 "${srcdir}/${pkgname}-${pkgver}.sh" "${pkgdir}/usr/bin/kewt" install -Dm755 "${srcdir}/${pkgname}-${pkgver}.sh" "${pkgdir}/usr/bin/kewt"
install -d "${pkgdir}/usr/share/zsh/site-functions" install -d "${pkgdir}/usr/share/zsh/site-functions"
"${pkgdir}/usr/bin/kewt" --dump-zsh-completions > "${pkgdir}/usr/share/zsh/site-functions/_kewt" "${pkgdir}/usr/bin/kewt" --dump-zsh-completions > "${pkgdir}/usr/share/zsh/site-functions/_kewt"
install -Dm644 "${srcdir}/${pkgname}-${pkgver}.bash" "${pkgdir}/usr/share/bash-completion/completions/kewt"
} }

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

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

View File

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

View File

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

View File

@@ -17,6 +17,8 @@ _kewt() {
'--to[Output directory]:directory:_directories' '--to[Output directory]:directory:_directories'
'(-w --watch)'{-w,--watch}'[Watch for file changes and rebuild automatically]' '(-w --watch)'{-w,--watch}'[Watch for file changes and rebuild automatically]'
'(-s --serve)'{-s,--serve}'[Start a local HTTP server after building]::port:' '(-s --serve)'{-s,--serve}'[Start a local HTTP server after building]::port:'
'(-d --draft)'{-d,--draft}'[Include draft pages in the build]'
'(-)--dry-run[Show what would be built without writing any files]'
) )
_arguments -S -C $args '*: :_directories' _arguments -S -C $args '*: :_directories'

View File

@@ -38,6 +38,14 @@ sudo make install
brew tap n0va-bot/tap brew tap n0va-bot/tap
brew install kewt brew install kewt
``` ```
### Fedora
```sh
sudo dnf copr enable n0va-bot/kewt
sudo dnf install kewt
```
### bpkg ### bpkg
```sh ```sh

View File

@@ -1,12 +1,9 @@
--- ---
title = "Heaven" title = "Heaven"
content_warning = "This page may contain CSS" content_warning = "This page may have bad effects on people with an allergy to cats"
--- ---
# Heaven # Heaven
Told you Told you
Probably should've mentioned the catgirl too ![catgirl.jpg]
| --- | --- |
| ```!![/styles.css]``` | <img style="vertical-align: top;" src="catgirl.jpg"> |

View File

@@ -37,6 +37,11 @@ body {
} }
header { header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding: 20px; padding: 20px;
padding-bottom: 0; padding-bottom: 0;
border-bottom: 1px solid var(--code-border); border-bottom: 1px solid var(--code-border);
@@ -45,6 +50,8 @@ header {
header h1 { header h1 {
margin: 0; margin: 0;
flex: 1 1 auto;
min-width: 0;
font-size: 35px; font-size: 35px;
font-weight: bold; font-weight: bold;
font-style: italic; font-style: italic;
@@ -114,6 +121,12 @@ a {
margin-left: -10px; margin-left: -10px;
} }
#side-bar a.current-page:hover,
#side-bar a.current-page:focus {
color: var(--fg);
background: transparent;
}
a:hover { a:hover {
background: var(--fg); background: var(--fg);
color: var(--bg); color: var(--bg);
@@ -131,6 +144,53 @@ h3 {
font-weight: normal; font-weight: normal;
} }
.header-link {
color: var(--fg);
display: inline;
text-decoration: underline;
text-decoration-color: var(--fg-muted);
}
.header-link:hover,
.header-link:focus {
background: var(--fg);
color: var(--bg);
text-decoration-color: var(--fg);
}
.header-anchor {
opacity: 0;
margin-left: 0.25em;
transition: opacity 0.15s ease;
}
h1:hover > .header-anchor,
h2:hover > .header-anchor,
h3:hover > .header-anchor,
h4:hover > .header-anchor,
h5:hover > .header-anchor,
h6:hover > .header-anchor,
h1:focus-within > .header-anchor,
h2:focus-within > .header-anchor,
h3:focus-within > .header-anchor,
h4:focus-within > .header-anchor,
h5:focus-within > .header-anchor,
h6:focus-within > .header-anchor,
h1:hover .header-link .header-anchor,
h2:hover .header-link .header-anchor,
h3:hover .header-link .header-anchor,
h4:hover .header-link .header-anchor,
h5:hover .header-link .header-anchor,
h6:hover .header-link .header-anchor,
h1:focus-within .header-link .header-anchor,
h2:focus-within .header-link .header-anchor,
h3:focus-within .header-link .header-anchor,
h4:focus-within .header-link .header-anchor,
h5:focus-within .header-link .header-anchor,
h6:focus-within .header-link .header-anchor {
opacity: 1;
}
pre { pre {
background: var(--code-bg); background: var(--code-bg);
color: var(--code-fg); color: var(--code-fg);
@@ -455,9 +515,10 @@ tr:nth-child(even) {
.kewt-search-header { .kewt-search-header {
display: inline-flex; display: inline-flex;
gap: 4px; gap: 4px;
margin-left: auto; margin-left: 0;
vertical-align: middle; vertical-align: middle;
font-style: normal; font-style: normal;
flex: 0 0 auto;
} }
.kewt-search-header input[type="text"] { .kewt-search-header input[type="text"] {

185
tests/test_builder.sh Normal file
View File

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

129
tests/test_config.sh Executable file
View File

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

43
tests/test_manifest.sh Normal file
View File

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

146
tests/test_markdown.sh Normal file
View File

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

92
tests/test_metadata.sh Executable file
View File

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

93
tests/test_runner.sh Executable file
View File

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

88
tests/test_runtime.sh Executable file
View File

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

View File

@@ -17,10 +17,10 @@ tmpdir=$(mktemp -d "/tmp/kewt.XXXXXX")
trap 'rm -rf "$tmpdir"' EXIT HUP INT TERM trap 'rm -rf "$tmpdir"' EXIT HUP INT TERM
# Extract payload # Extract payload
sed '1,/^#==PAYLOAD==$/d' "$0" | tar -xz -C "$tmpdir" LC_ALL=C sed '1,/^#==PAYLOAD==$/d' "$0" | tar -xz -C "$tmpdir"
# Pass control to the extracted script # Pass control to the extracted script
KEWT_INVOKED_AS="$0" "$tmpdir/kewt.sh" "$@" KEWT_INVOKED_AS="$0" sh "$tmpdir/kewt.sh" "$@"
exit $? exit $?
#==PAYLOAD== #==PAYLOAD==