7 Commits

Author SHA1 Message Date
1a7525a857 feat: config updating
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-17 11:53:15 +01:00
e7d90d18e8 feat: sitemap 2026-03-17 11:37:17 +01:00
4019d2721d Header links 2026-03-17 11:28:47 +01:00
b58604a4cf feat/docs: LICENSE
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-17 11:17:26 +01:00
99e805b180 fix: Title formatting parsing fix
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-17 02:18:42 +01:00
62075dea4a docs: forgot
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-17 02:07:52 +01:00
7afd041e53 functionality: Dynamic page titles, versioning, automatic 404 page
All checks were successful
Lint / shellcheck (push) Successful in 17s
Release Standalone Builder / build (release) Successful in 32s
Release Standalone Builder / publish-aur (release) Successful in 33s
2026-03-17 01:55:32 +01:00
9 changed files with 261 additions and 58 deletions

35
LICENSE Normal file
View File

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

View File

@@ -63,6 +63,9 @@ display_logo = false
display_title = true display_title = true
logo_as_favicon = true logo_as_favicon = true
favicon = "" favicon = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
``` ```
- `title` site title - `title` site title
@@ -81,6 +84,9 @@ favicon = ""
- `display_title` show title text in header - `display_title` show title text in header
- `logo_as_favicon` use `logo` as favicon - `logo_as_favicon` use `logo` as favicon
- `favicon` explicit favicon path (used when `logo_as_favicon` is false or no logo is set) - `favicon` explicit favicon path (used when `logo_as_favicon` is false or no logo is set)
- `generate_page_title` automatically generate title text from the first markdown heading or filename (default: true)
- `error_page` filename for the generated 404 error page (default: "not_found.html", empty to disable)
- `versioning` append a version query parameter (`?v=timestamp`) to css asset urls to bypass cache (default: false)
## Ignores ## Ignores

View File

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

197
kewt.sh
View File

@@ -11,13 +11,15 @@ usage() {
Usage: $invoked_as [--from <src>] [--to <out>] Usage: $invoked_as [--from <src>] [--to <out>]
$invoked_as [src] [out] $invoked_as [src] [out]
$invoked_as --new [title] $invoked_as --new [title]
$invoked_as --update [dir]
$invoked_as --help $invoked_as --help
Options: Options:
--help Show this help message. --help Show this help message.
--new [title] Create a new site directory (default: site) --new [title] Create a new site directory (default: site)
--from <src> Source directory (default: site) --update [dir] Update site.conf and template.html with latest defaults (defaults to current directory)
--to <out> Output directory (default: out) --from <src> Source directory (default: site)
--to <out> Output directory (default: out)
EOF EOF
} }
@@ -46,6 +48,11 @@ display_logo = false
display_title = true display_title = true
logo_as_favicon = true logo_as_favicon = true
favicon = "" favicon = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
enable_header_links = true
base_url = ""
EOF EOF
fi fi
@@ -97,6 +104,103 @@ create_new_site() {
exit 0 exit 0
} }
update_site() {
update_dir="${1:-.}"
[ -d "$update_dir" ] || die "Directory '$update_dir' does not exist."
target_conf="$update_dir/site.conf"
target_tmpl="$update_dir/template.html"
# Generate default site.conf
default_conf="$KEWT_TMPDIR/default_site.conf"
cat > "$default_conf" <<'CONFEOF'
title = "kewt"
style = "kewt"
dir_indexes = true
single_file_index = true
flatten = false
order = ""
home_name = "Home"
show_home_in_nav = true
nav_links = ""
nav_extra = ""
footer = "made with <a href="https://kewt.krzak.org">kewt</a>"
logo = ""
display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
enable_header_links = true
base_url = ""
CONFEOF
# Update site.conf: add missing keys
if [ ! -f "$target_conf" ]; then
echo "No site.conf found in '$update_dir'; nothing to update."
else
added=0
while IFS= read -r line; do
case "$line" in
''|'#'*) continue ;;
*=*) ;;
*) continue ;;
esac
key=$(printf '%s' "${line%%=*}" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
if ! grep -q "^[[:space:]]*${key}[[:space:]]*=" "$target_conf"; then
printf '%s\n' "$line" >> "$target_conf"
echo " Added: $key"
added=$((added + 1))
fi
done < "$default_conf"
if [ "$added" -eq 0 ]; then
echo "site.conf is already up to date."
else
echo "Added $added new key(s) to '$target_conf'."
fi
fi
# Update template.html
if [ -f "$target_tmpl" ]; then
default_tmpl="$KEWT_TMPDIR/default_template.html"
cat > "$default_tmpl" <<'TMPLEOF'
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>{{TITLE}}</title>
<link rel="stylesheet" href="{{CSS}}" type="text/css" />
{{HEAD_EXTRA}}
</head>
<body>
<header>
<h1>{{HEADER_BRAND}}</h1>
</header>
<nav id="side-bar">{{NAV}}</nav>
<article>{{CONTENT}}</article>
<footer>{{FOOTER}}</footer>
</body>
</html>
TMPLEOF
if cmp -s "$default_tmpl" "$target_tmpl" 2>/dev/null; then
echo "template.html is already up to date."
else
cp "$default_tmpl" "${target_tmpl}.default"
echo "template.html has local changes; saved latest default as '${target_tmpl}.default'."
echo ""
diff "$target_tmpl" "${target_tmpl}.default" || true
fi
fi
exit 0
}
src="" src=""
@@ -118,6 +222,14 @@ while [ $# -gt 0 ]; do
shift shift
fi fi
;; ;;
--update)
update_dir="."
if [ $# -gt 1 ] && [ "${2#-}" = "$2" ]; then
update_dir="$2"
shift
fi
update_site "$update_dir"
;;
--from) --from)
[ $# -lt 2 ] && die "--from requires a value." [ $# -lt 2 ] && die "--from requires a value."
src="$2" src="$2"
@@ -276,6 +388,11 @@ display_logo="false"
display_title="true" display_title="true"
logo_as_favicon="true" logo_as_favicon="true"
favicon="" favicon=""
generate_page_title="true"
error_page="not_found.html"
versioning="false"
enable_header_links="true"
base_url=""
load_config() { load_config() {
[ -f "$1" ] || return [ -f "$1" ] || return
@@ -313,6 +430,11 @@ load_config() {
display_title) display_title="$val" ;; display_title) display_title="$val" ;;
logo_as_favicon) logo_as_favicon="$val" ;; logo_as_favicon) logo_as_favicon="$val" ;;
favicon) favicon="$val" ;; favicon) favicon="$val" ;;
generate_page_title) generate_page_title="$val" ;;
error_page) error_page="$val" ;;
versioning) versioning="$val" ;;
enable_header_links) enable_header_links="$val" ;;
base_url) base_url="$val" ;;
esac esac
done < "$1" done < "$1"
} }
@@ -320,6 +442,11 @@ load_config() {
load_config "./site.conf" load_config "./site.conf"
load_config "$src/site.conf" load_config "$src/site.conf"
asset_version=""
if [ "$versioning" = "true" ]; then
asset_version="?v=$(date +%s)"
fi
escape_html_text() { escape_html_text() {
printf '%s' "$1" | sed \ printf '%s' "$1" | sed \
-e 's/&/\&amp;/g' \ -e 's/&/\&amp;/g' \
@@ -421,6 +548,7 @@ copy_style_with_resolved_vars() {
render_markdown() { render_markdown() {
file="$1" file="$1"
is_home="$2"
local_template=$(find_closest "template.html" "$(dirname "$file")") local_template=$(find_closest "template.html" "$(dirname "$file")")
[ -z "$local_template" ] && local_template="$template" [ -z "$local_template" ] && local_template="$template"
@@ -469,7 +597,26 @@ render_markdown() {
head_extra="<link rel=\"icon\" href=\"$favicon_src\" />" head_extra="<link rel=\"icon\" href=\"$favicon_src\" />"
fi fi
MARKDOWN_SITE_ROOT="$src" MARKDOWN_FALLBACK_FILE="$script_dir/styles/$style.css" sh "$script_dir/markdown.sh" "$file" | awk -v title="$title" -v nav="$nav" -v footer="$footer" -v style_path="$style_path" -v header_brand="$header_brand" -v head_extra="$head_extra" -f "$awk_dir/render_template.awk" "$local_template" page_title="$title"
if [ "$generate_page_title" = "true" ] && [ -n "$file" ] && [ -f "$file" ]; then
if [ "$is_home" = "true" ] && [ -n "$home_name" ]; then
page_title="$home_name - $title"
else
first_heading=$(grep -m 1 '^# ' "$file" | sed 's/^# *//; s/ *$//')
if [ -n "$first_heading" ]; then
first_heading=$(echo "$first_heading" | sed -e 's/\[//g' -e 's/\]//g' -e 's/!//g' -e 's/\*//g' -e 's/_//g' -e 's/`//g' -e 's/([^)]*)//g' | sed 's/\\//g')
page_title="$first_heading - $title"
else
basename_no_ext=$(basename "$file" .md)
if [ "$basename_no_ext" != "index" ] && [ "$basename_no_ext" != "404_gen" ]; then
cap_basename=$(echo "$basename_no_ext" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')
page_title="$cap_basename - $title"
fi
fi
fi
fi
ENABLE_HEADER_LINKS="$enable_header_links" MARKDOWN_SITE_ROOT="$src" MARKDOWN_FALLBACK_FILE="$script_dir/styles/$style.css" sh "$script_dir/markdown.sh" "$file" | awk -v title="$page_title" -v nav="$nav" -v footer="$footer" -v style_path="${style_path}${asset_version}" -v header_brand="$header_brand" -v head_extra="$head_extra" -f "$awk_dir/render_template.awk" "$local_template"
} }
echo "Building site from '$src' to '$out'..." echo "Building site from '$src' to '$out'..."
@@ -494,7 +641,8 @@ eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while
md_count=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md" | wc -l) md_count=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md" | wc -l)
if [ "$md_count" -eq 1 ]; then if [ "$md_count" -eq 1 ]; then
md_file=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md") md_file=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md")
render_markdown "$md_file" > "$out_dir/index.html" is_home="false"; [ "$dir" = "$src" ] && is_home="true"
render_markdown "$md_file" "$is_home" > "$out_dir/index.html"
continue continue
fi fi
fi fi
@@ -517,7 +665,8 @@ eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while
echo "- [$name]($name)" >> "$temp_index" echo "- [$name]($name)" >> "$temp_index"
fi fi
done done
render_markdown "$temp_index" > "$out_dir/index.html" is_home="false"; [ "$dir" = "$src" ] && is_home="true"
render_markdown "$temp_index" "$is_home" > "$out_dir/index.html"
rm "$temp_index" rm "$temp_index"
fi fi
done done
@@ -547,12 +696,44 @@ eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type f -print" | sort | while
fi fi
if [ "${file%.md}" != "$file" ] && [ "$is_preserved" -eq 0 ]; then if [ "${file%.md}" != "$file" ] && [ "$is_preserved" -eq 0 ]; then
is_home="false"; [ "$file" = "$src/index.md" ] && is_home="true"
out_file="$out/${rel_path%.md}.html" out_file="$out/${rel_path%.md}.html"
render_markdown "$file" > "$out_file" render_markdown "$file" "$is_home" > "$out_file"
else else
cp "$file" "$out/$rel_path" cp "$file" "$out/$rel_path"
fi fi
done done
if [ -n "$error_page" ] && [ ! -f "$out/$error_page" ]; then
temp_404="$KEWT_TMPDIR/404_gen.md"
echo "# 404 - Not Found" > "$temp_404"
echo "" >> "$temp_404"
echo "The requested page could not be found." >> "$temp_404"
render_markdown "$temp_404" > "$out/$error_page"
rm -f "$temp_404"
fi
if [ -n "$base_url" ]; then
sitemap_file="$out/sitemap.xml"
base_url="${base_url%/}"
today=$(date +%Y-%m-%d)
printf '<?xml version="1.0" encoding="UTF-8"?>\n' > "$sitemap_file"
printf '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' >> "$sitemap_file"
find "$out" -type f -name "*.html" -print | sort | while IFS= read -r html_file; do
rel_url="${html_file#"$out"}"
# Don't include 404 in the sitemap (duh)
[ "${rel_url#/}" = "$error_page" ] && continue
printf ' <url>\n' >> "$sitemap_file"
printf ' <loc>%s%s</loc>\n' "$base_url" "$rel_url" >> "$sitemap_file"
printf ' <lastmod>%s</lastmod>\n' "$today" >> "$sitemap_file"
printf ' </url>\n' >> "$sitemap_file"
done
printf '</urlset>\n' >> "$sitemap_file"
fi
echo "Build complete." echo "Build complete."

View File

@@ -51,7 +51,7 @@ awk -f "$awk_dir/blockquote_to_admonition.awk" "$temp_file" > "$temp_file.tmp" &
awk -f "$awk_dir/fenced_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" awk -f "$awk_dir/fenced_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/indented_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" awk -f "$awk_dir/indented_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/pipe_tables.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" awk -f "$awk_dir/pipe_tables.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/headers.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" awk -v enable_header_links="$ENABLE_HEADER_LINKS" -f "$awk_dir/headers.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/lists.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" awk -f "$awk_dir/lists.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# Spacing # Spacing

View File

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

View File

@@ -63,6 +63,9 @@ display_logo = false
display_title = true display_title = true
logo_as_favicon = true logo_as_favicon = true
favicon = "" favicon = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
``` ```
- `title` site title - `title` site title
@@ -81,6 +84,9 @@ favicon = ""
- `display_title` show title text in header - `display_title` show title text in header
- `logo_as_favicon` use `logo` as favicon - `logo_as_favicon` use `logo` as favicon
- `favicon` explicit favicon path (used when `logo_as_favicon` is false or no logo is set) - `favicon` explicit favicon path (used when `logo_as_favicon` is false or no logo is set)
- `generate_page_title` automatically generate title text from the first markdown heading or filename (default: true)
- `error_page` filename for the generated 404 error page (default: "not_found.html", empty to disable)
- `versioning` append a version query parameter (`?v=timestamp`) to css asset urls to bypass cache (default: false)
## Ignores ## Ignores

View File

@@ -10,3 +10,12 @@ display_title = true
logo_as_favicon = true logo_as_favicon = true
favicon = "" favicon = ""
order = "" order = ""
home_name = "Home"
show_home_in_nav = true
nav_links = ""
nav_extra = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
enable_header_links = true
base_url = "https://kewt.krzak.org"

View File

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