6 Commits

Author SHA1 Message Date
8814c12480 feat: include_cw_pages_in_search
All checks were successful
Lint / shellcheck (push) Successful in 19s
Publish kewt-git to AUR / publish-aur-git (push) Successful in 33s
2026-04-01 12:39:06 +02:00
0c0f249226 feat: modular building and search 2026-04-01 12:35:26 +02:00
b29a5274e1 feat: zsh completions 2026-04-01 12:34:54 +02:00
00f4bbb5f0 docs: quickstart.md 2026-04-01 12:34:05 +02:00
7fe204f9f9 feat: previous and next buttons 2026-04-01 09:33:33 +02:00
81d3caff45 feat/fix: sort the blog dir by date
All checks were successful
Lint / shellcheck (push) Successful in 20s
Release Standalone Builder / build (release) Successful in 32s
Release Standalone Builder / publish-aur (release) Successful in 35s
Release Standalone Builder / publish-homebrew (release) Successful in 8s
2026-04-01 09:02:46 +02:00
17 changed files with 1662 additions and 1031 deletions

View File

@@ -1,5 +1,6 @@
PREFIX ?= /usr/local
BINDIR = $(PREFIX)/bin
ZSHCOMPDIR ?= $(PREFIX)/share/zsh/site-functions
all: kewt
@@ -9,9 +10,12 @@ kewt:
install: kewt
install -d $(DESTDIR)$(BINDIR)
install -m 755 kewt $(DESTDIR)$(BINDIR)/kewt
install -d $(DESTDIR)$(ZSHCOMPDIR)
install -m 644 packaging/zsh/_kewt $(DESTDIR)$(ZSHCOMPDIR)/_kewt
uninstall:
rm -f $(DESTDIR)$(BINDIR)/kewt
rm -f $(DESTDIR)$(ZSHCOMPDIR)/_kewt
clean:
rm -f kewt

1056
kewt.sh

File diff suppressed because it is too large Load Diff

708
lib/builder.sh Normal file
View File

@@ -0,0 +1,708 @@
needs_rebuild() {
src_file="$1"
out_file="$2"
[ ! -f "$out_file" ] && return 0
[ "$src_file" -nt "$out_file" ] && return 0
[ -f "./site.conf" ] && [ "./site.conf" -nt "$out_file" ] && return 0
[ -f "$src/site.conf" ] && [ "$src/site.conf" -nt "$out_file" ] && return 0
[ -f "$template" ] && [ "$template" -nt "$out_file" ] && return 0
[ -f "$script_dir/styles/$style.css" ] && [ "$script_dir/styles/$style.css" -nt "$out_file" ] && return 0
return 1
}
build_site() {
echo "Building site from '$src' to '$out'..."
eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while read -r dir; do
rel_dir="${dir#"$src"}"
rel_dir="${rel_dir#/}"
[ -z "$rel_dir" ] && rel_dir="."
out_dir="$out/$rel_dir"
mkdir -p "$out_dir"
if [ -f "$dir/styles.css" ]; then
if needs_rebuild "$dir/styles.css" "$out_dir/styles.css"; then
copy_style_with_resolved_vars "$dir/styles.css" "$out_dir/styles.css"
fi
elif [ -f "$dir/style.css" ]; then
if needs_rebuild "$dir/style.css" "$out_dir/styles.css"; then
copy_style_with_resolved_vars "$dir/style.css" "$out_dir/styles.css"
fi
fi
[ "$dir_indexes" != "true" ] && continue
has_custom_index="false"
has_list="false"
if [ -f "$dir/index.md" ]; then
has_custom_index="true"
if grep -q '^[[:space:]]*{{LIST}}[[:space:]]*$' "$dir/index.md" 2>/dev/null; then
has_list="true"
fi
fi
if [ "$has_custom_index" = "false" ] || [ "$has_list" = "true" ]; then
is_posts_dir="false"
if [ -n "$posts_dir" ] && { [ "$rel_dir" = "$posts_dir" ] || [ "./$rel_dir" = "$posts_dir" ]; }; then
is_posts_dir="true"
fi
if [ "$single_file_index" = "true" ] && [ "$is_posts_dir" = "false" ] && [ "$has_list" = "false" ]; then
md_count=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md" | wc -l)
if [ "$md_count" -eq 1 ]; then
md_file=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md")
is_home="false"; [ "$dir" = "$src" ] && is_home="true"
target_url="/$rel_dir/index.html"
[ "$rel_dir" = "." ] && target_url="/index.html"
if needs_rebuild "$md_file" "$out_dir/index.html"; then
parse_frontmatter "$md_file"
if [ -n "$fm_content_warning" ]; then
content_out_file="$out_dir/content.html"
content_rel_url="/$rel_dir/content.html"
[ "$rel_dir" = "." ] && content_rel_url="/content.html"
is_cw_content_page="true"
render_markdown "$md_file" "$is_home" "$target_url" > "$content_out_file"
is_cw_content_page="false"
generate_content_warning_page "$fm_title" "$fm_content_warning" "$content_rel_url" "$target_url" "$out_dir/index.html" "false"
else
render_markdown "$md_file" "$is_home" "$target_url" > "$out_dir/index.html"
fi
fi
continue
fi
fi
temp_index="$KEWT_TMPDIR/index.md"
temp_list="$KEWT_TMPDIR/list.md"
: > "$temp_list"
if [ "$has_custom_index" = "false" ]; then
display_dir="${rel_dir#.}"
[ -z "$display_dir" ] && display_dir="/"
echo "# Index of $display_dir" > "$temp_index"
echo "" >> "$temp_index"
fi
sort_args=""
# If this is the posts dir reverse
if [ "$rel_dir" = "$posts_dir" ] || [ "./$rel_dir" = "$posts_dir" ]; then
sort_args="-r"
fi
temp_entries="$KEWT_TMPDIR/entries_$$.txt"
: > "$temp_entries"
find "$dir" ! -name "$(basename "$dir")" -prune ! -name ".*" -print | while read -r entry; do
name="${entry##*/}"
case "$name" in
template.html|site.conf|style.css|index.md) continue ;;
esac
if [ -d "$entry" ]; then
echo "${name}|- [${name}/](${name}/index.html)" >> "$temp_entries"
elif [ "${entry%.md}" != "$entry" ]; then
label="${name%.md}"
# Parse frontmatter for date/title/draft
parse_frontmatter "$entry"
[ "$fm_draft" = "true" ] && continue
# Try to get first heading
post_h="$fm_title"
if [ -z "$post_h" ]; then
post_h=$(grep -m 1 '^# ' "$entry" | sed 's/^# *//')
if [ -n "$post_h" ]; then
post_h=$(echo "$post_h" | sed -e 's/\[//g' -e 's/\]//g' -e 's/!//g' -e 's/\*//g' -e 's/_//g' -e 's/`//g' -e 's/([^)]*)//g' | sed 's/\\//g')
fi
fi
is_post_entry="false"
if [ "$rel_dir" = "$posts_dir" ] || [ "./$rel_dir" = "$posts_dir" ]; then
is_post_entry="true"
fi
if [ -n "$post_h" ]; then
if [ "$is_post_entry" = "true" ]; then
# Use frontmatter date if available, else parse from filename
if [ -n "$fm_date" ]; then
p_date=$(echo "$fm_date" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
p_time=""
if echo "$fm_date" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?[0-9]\{2\}[:\-][0-9]\{2\}'; then
p_time=$(echo "$fm_date" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
else
p_date=$(echo "${name%.md}" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
p_time="00:00"
if echo "${name%.md}" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}'; then
p_time=$(echo "${name%.md}" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
fi
if [ -n "$p_time" ]; then
label="$post_h - $p_date $p_time"
else
label="$post_h - $p_date"
fi
else
label="$post_h"
fi
elif [ "$is_post_entry" = "true" ]; then
# No heading; use date
if [ -n "$fm_date" ]; then
p_date=$(echo "$fm_date" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
p_time=""
if echo "$fm_date" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?[0-9]\{2\}[:\-][0-9]\{2\}'; then
p_time=$(echo "$fm_date" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
if [ -n "$p_time" ]; then
label="$p_date $p_time"
else
label="$p_date"
fi
else
p_date=$(echo "${name%.md}" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
p_time="00:00"
if echo "${name%.md}" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}'; then
p_time=$(echo "${name%.md}" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
label="$p_date $p_time"
fi
fi
if [ "$is_post_entry" = "true" ]; then
sort_key="${p_date} ${p_time}"
else
sort_key="$name"
fi
echo "${sort_key}|- [$label](${name%.md}.html)|$name|${name%.md}.html" >> "$temp_entries"
else
echo "${name}|- [$name]($name)|$name|$name" >> "$temp_entries"
fi
done
if [ "$is_posts_dir" = "true" ]; then
LC_ALL=C sort $sort_args "$temp_entries" > "$KEWT_TMPDIR/sorted_entries_$$.txt"
cut -d'|' -f2 "$KEWT_TMPDIR/sorted_entries_$$.txt" >> "$temp_list"
mkdir -p "$KEWT_TMPDIR/prevnext"
awk -F'|' '
{
name[NR] = $3
url[NR] = $4
}
END {
for(i=1; i<=NR; i++) {
prev_str = ""
next_str = ""
if(i > 1) {
next_str = "[Next >](" url[i-1] ")"
}
if(i < NR) {
prev_str = "[< Previous](" url[i+1] ")"
}
if (prev_str != "" || next_str != "") {
out = "'"$KEWT_TMPDIR"'/prevnext/" name[i]
printf "%s|%s\n", prev_str, next_str > out
}
}
}
' "$KEWT_TMPDIR/sorted_entries_$$.txt"
rm -f "$KEWT_TMPDIR/sorted_entries_$$.txt"
else
LC_ALL=C sort $sort_args "$temp_entries" | cut -d'|' -f2 >> "$temp_list"
fi
rm -f "$temp_entries"
is_home="false"; [ "$dir" = "$src" ] && is_home="true"
target_url="/$rel_dir/index.html"
[ "$rel_dir" = "." ] && target_url="/index.html"
num_items=$(wc -l < "$temp_list")
if [ "$is_posts_dir" = "true" ] && [ -n "$posts_per_page" ] && [ "$posts_per_page" -gt 0 ] && [ "$num_items" -gt "$posts_per_page" ]; then
num_pages=$(( (num_items + posts_per_page - 1) / posts_per_page ))
for p in $(seq 1 $num_pages); do
chunk_list="$KEWT_TMPDIR/chunk.md"
start_line=$(( (p - 1) * posts_per_page + 1 ))
tail -n +$start_line "$temp_list" | head -n "$posts_per_page" > "$chunk_list"
base_url_dir="$(dirname "$target_url")"
[ "$base_url_dir" = "/" ] && base_url_dir=""
nav_html="<div class=\"pagination\">"
if [ "$p" -gt 1 ]; then
if [ "$p" -eq 2 ]; then
nav_html="$nav_html <a href=\"$base_url_dir/index.html\" class=\"prev-page\">&laquo; Prev</a> "
else
nav_html="$nav_html <a href=\"$base_url_dir/page/$((p-1))/index.html\" class=\"prev-page\">&laquo; Prev</a> "
fi
fi
nav_html="$nav_html <span class=\"page-number\">Page $p of $num_pages</span> "
if [ "$p" -lt "$num_pages" ]; then
nav_html="$nav_html <a href=\"$base_url_dir/page/$((p+1))/index.html\" class=\"next-page\">Next &raquo;</a> "
fi
nav_html="$nav_html</div>"
echo "" >> "$chunk_list"
echo "$nav_html" >> "$chunk_list"
temp_index_p="$KEWT_TMPDIR/index_p$p.md"
if [ "$has_custom_index" = "false" ]; then
display_dir="${rel_dir#.}"
[ -z "$display_dir" ] && display_dir="/"
echo "# Index of $display_dir" > "$temp_index_p"
echo "" >> "$temp_index_p"
else
: > "$temp_index_p"
fi
if [ "$has_custom_index" = "true" ]; then
awk '
/^[[:space:]]*\{\{LIST\}\}[[:space:]]*$/ {
while((getline line < "'"$chunk_list"'") > 0) print line
close("'"$chunk_list"'")
next
}
{ print }
' "$dir/index.md" >> "$temp_index_p"
else
cat "$chunk_list" >> "$temp_index_p"
fi
if [ "$p" -eq 1 ]; then
out_file="$out_dir/index.html"
target_url_p="$target_url"
else
out_file="$out_dir/page/$p/index.html"
target_url_p="$base_url_dir/page/$p/index.html"
mkdir -p "$(dirname "$out_file")"
fi
render_markdown "$temp_index_p" "$is_home" "$target_url_p" > "$out_file"
rm -f "$temp_index_p" "$chunk_list"
done
else
if [ "$has_custom_index" = "true" ]; then
awk '
/^[[:space:]]*\{\{LIST\}\}[[:space:]]*$/ {
while((getline line < "'"$temp_list"'") > 0) print line
close("'"$temp_list"'")
next
}
{ print }
' "$dir/index.md" > "$temp_index"
else
cat "$temp_list" >> "$temp_index"
fi
do_rebuild="false"
needs_rebuild "$dir" "$out_dir/index.html" && do_rebuild="true"
[ "$has_custom_index" = "true" ] && needs_rebuild "$dir/index.md" "$out_dir/index.html" && do_rebuild="true"
if [ "$do_rebuild" = "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"
content_rel_url="/$rel_dir/content.html"
[ "$rel_dir" = "." ] && content_rel_url="/content.html"
is_cw_content_page="true"
render_markdown "$temp_index" "$is_home" "$target_url" > "$content_out_file"
is_cw_content_page="false"
generate_content_warning_page "$fm_title" "$fm_content_warning" "$content_rel_url" "$target_url" "$out_dir/index.html" "false"
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 "$script_dir/styles/$style.css" ] && needs_rebuild "$script_dir/styles/$style.css" "$out/styles.css"; then
copy_style_with_resolved_vars "$script_dir/styles/$style.css" "$out/styles.css"
fi
eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type f -print" | sort | while IFS= read -r file; do
rel_path="${file#"$src"}"
rel_path="${rel_path#/}"
dir_rel=$(dirname "$rel_path")
out_dir="$out/$dir_rel"
case "${file##*/}" in
template.html|site.conf|style.css|styles.css) continue ;;
esac
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 [ -n "$posts_dir" ] && { [ "$dir_rel" = "$posts_dir" ] || [ "./$dir_rel" = "$posts_dir" ]; }; 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
md_count=$(find "$(dirname "$file")" ! -name "$(basename "$(dirname "$file")")" -prune -name "*.md" | wc -l)
[ "$md_count" -eq 1 ] && continue
fi
if [ "${file%.md}" != "$file" ] && [ "$is_preserved" -eq 0 ]; then
# Skip draft files
parse_frontmatter "$file"
if [ "$fm_draft" = "true" ]; then
continue
fi
is_home="false"; [ "$file" = "$src/index.md" ] && is_home="true"
out_file="$out/${rel_path%.md}.html"
if needs_rebuild "$file" "$out_file"; then
if [ -n "$fm_content_warning" ]; then
content_out_file="$out/${rel_path%.md}-content.html"
content_rel_url="/${rel_path%.md}-content.html"
orig_rel_url="/${rel_path%.md}.html"
is_cw_content_page="true"
render_markdown "$file" "$is_home" "$orig_rel_url" > "$content_out_file"
is_cw_content_page="false"
generate_content_warning_page "$fm_title" "$fm_content_warning" "$content_rel_url" "$orig_rel_url" "$out_file" "false"
else
render_markdown "$file" "$is_home" > "$out_file"
fi
fi
else
if needs_rebuild "$file" "$out/$rel_path"; then
cp "$file" "$out/$rel_path"
fi
fi
done
if [ -n "$error_page" ] && [ ! -f "$out/$error_page" ]; then
temp_404="$KEWT_TMPDIR/404_gen.md"
echo "# 404 - Not Found" > "$temp_404"
echo "" >> "$temp_404"
echo "The requested page could not be found." >> "$temp_404"
render_markdown "$temp_404" "false" "/$error_page" > "$out/$error_page"
rm -f "$temp_404"
fi
if [ -n "$base_url" ]; then
sitemap_file="$out/sitemap.xml"
base_url="${base_url%/}"
today=$(date +%Y-%m-%d)
printf '<?xml version="1.0" encoding="UTF-8"?>\n' > "$sitemap_file"
printf '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' >> "$sitemap_file"
find "$out" -type f -name "*.html" -print | sort | while IFS= read -r html_file; do
rel_url="${html_file#"$out"}"
# Don't include 404 in the sitemap (duh)
[ "${rel_url#/}" = "$error_page" ] && continue
{
printf ' <url>\n'
printf ' <loc>%s%s</loc>\n' "$base_url" "$rel_url"
printf ' <lastmod>%s</lastmod>\n' "$today"
printf ' </url>\n'
} >> "$sitemap_file"
done
printf '</urlset>\n' >> "$sitemap_file"
fi
if [ "$generate_feed" = "true" ] && [ -n "$base_url" ]; then
feed_path="$out/$feed_file"
base_url_feed="${base_url%/}"
build_date=$(date -u '+%a, %d %b %Y %H:%M:%S +0000')
printf '<?xml version="1.0" encoding="UTF-8"?>\n' > "$feed_path"
{
printf '<rss version="2.0">\n'
printf ' <channel>\n'
printf ' <title>%s</title>\n' "$title"
printf ' <link>%s</link>\n' "$base_url_feed"
printf ' <description>%s</description>\n' "$title"
printf ' <lastBuildDate>%s</lastBuildDate>\n' "$build_date"
} >> "$feed_path"
temp_feed_files="$KEWT_TMPDIR/feed_files_$$.txt"
: > "$temp_feed_files"
find "$src" -type f -name '*.md' -path "*${posts_dir:-__no_posts__}*" -print | while IFS= read -r post_file; do
post_basename=$(basename "$post_file" .md)
# Parse frontmatter to get date
parse_frontmatter "$post_file"
[ "$fm_draft" = "true" ] && continue
if [ -n "$fm_date" ]; then
post_date=$(echo "$fm_date" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
post_time="00:00"
if echo "$fm_date" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?[0-9]\{2\}[:\-][0-9]\{2\}'; then
post_time=$(echo "$fm_date" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
else
post_date=$(echo "$post_basename" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
post_time="00:00"
if echo "$post_basename" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}'; then
post_time=$(echo "$post_basename" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
fi
echo "${post_date} ${post_time}|${post_file}" >> "$temp_feed_files"
done
LC_ALL=C sort -r "$temp_feed_files" | cut -d'|' -f2- | while IFS= read -r post_file; do
post_basename=$(basename "$post_file" .md)
# Parse frontmatter
parse_frontmatter "$post_file"
[ "$fm_draft" = "true" ] && continue
# Use frontmatter date, fallback to filename
if [ -n "$fm_date" ]; then
post_date=$(echo "$fm_date" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
post_time="00:00"
if echo "$fm_date" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?[0-9]\{2\}[:\-][0-9]\{2\}'; then
post_time=$(echo "$fm_date" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}[ T_-]\?\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
else
post_date=$(echo "$post_basename" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
post_time="00:00"
if echo "$post_basename" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}'; then
post_time=$(echo "$post_basename" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
fi
post_slug=$(echo "$post_basename" | sed -e 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}//' -e 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}//' -e 's/^[_\-]//')
post_heading="$fm_title"
if [ -z "$post_heading" ]; then
post_heading=$(grep -m 1 '^# ' "$post_file" | sed 's/^# *//')
fi
if [ -z "$post_heading" ]; then
if [ -n "$post_slug" ] && ! echo "$post_slug" | grep -q '^[0-9]\+$'; then
post_heading=$(echo "$post_slug" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)}1')
else
post_heading="Post"
fi
fi
post_heading=$(echo "$post_heading" | sed -e 's/\[//g' -e 's/\]//g' -e 's/!//g' -e 's/\*//g' -e 's/_//g' -e 's/`//g' -e 's/([^)]*)//g' | sed 's/\\//g')
feed_post_title="$post_heading - $post_date $post_time"
rel_path="${post_file#"$src"}"
rel_path="${rel_path#/}"
post_url="$base_url_feed/${rel_path%.md}.html"
if date -u -d "$post_date $post_time" '+%a, %d %b %Y %H:%M:%S +0000' >/dev/null 2>&1; then
pub_date=$(date -u -d "$post_date $post_time" '+%a, %d %b %Y %H:%M:%S +0000')
else
pub_year=$(echo "$post_date" | cut -d- -f1)
pub_month=$(echo "$post_date" | cut -d- -f2)
pub_day=$(echo "$post_date" | cut -d- -f3)
# zero-padded
pub_day=$(printf '%02d' "${pub_day#0}")
case "$pub_month" in
01) pub_mon="Jan" ;; 02) pub_mon="Feb" ;; 03) pub_mon="Mar" ;;
04) pub_mon="Apr" ;; 05) pub_mon="May" ;; 06) pub_mon="Jun" ;;
07) pub_mon="Jul" ;; 08) pub_mon="Aug" ;; 09) pub_mon="Sep" ;;
10) pub_mon="Oct" ;; 11) pub_mon="Nov" ;; 12) pub_mon="Dec" ;;
esac
pub_date="Mon, ${pub_day} ${pub_mon} ${pub_year} ${post_time}:00 +0000"
fi
{
printf ' <item>\n'
printf ' <title>%s</title>\n' "$feed_post_title"
printf ' <link>%s</link>\n' "$post_url"
printf ' <guid>%s</guid>\n' "$post_url"
printf ' <pubDate>%s</pubDate>\n' "$pub_date"
printf ' </item>\n'
} >> "$feed_path"
done
printf ' </channel>\n' >> "$feed_path"
printf '</rss>\n' >> "$feed_path"
fi
if [ "$generate_search" = "true" ] || [ "$generate_tags" = "true" ]; then
if [ "$generate_search" = "true" ]; then
printf '[\n' > "$out/search.json"
fi
first_search_item="true"
temp_tags="$KEWT_TMPDIR/tags_$$.txt"
: > "$temp_tags"
eval "find \"$src\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -name \"*.md\" -print" | sort | while IFS= read -r md_file; do
is_index="false"
[ "$(basename "$md_file")" = "index.md" ] && is_index="true"
rel_path="${md_file#"$src"}"
rel_path="${rel_path#/}"
if [ "$is_index" = "true" ]; then
if [ "$rel_path" = "index.md" ]; then
md_url="/index.html"
else
md_url="/${rel_path%/index.md}/index.html"
fi
else
md_url="/${rel_path%.md}.html"
if [ "$single_file_index" = "true" ]; then
dir_of_file="$(dirname "$md_file")"
rel_dir_of_file="${dir_of_file#"$src"}"
rel_dir_of_file="${rel_dir_of_file#/}"
[ -z "$rel_dir_of_file" ] && rel_dir_of_file="."
is_posts_dir_search="false"
if [ -n "$posts_dir" ] && { [ "$rel_dir_of_file" = "$posts_dir" ] || [ "./$rel_dir_of_file" = "$posts_dir" ]; }; then
is_posts_dir_search="true"
fi
if [ "$is_posts_dir_search" = "false" ] && [ ! -f "$dir_of_file/index.md" ]; then
md_count_search=$(find "$dir_of_file" ! -name "$(basename "$dir_of_file")" -prune -name "*.md" | wc -l)
if [ "$md_count_search" -eq 1 ]; then
if [ "$rel_dir_of_file" = "." ]; then
md_url="/index.html"
else
md_url="/$rel_dir_of_file/index.html"
fi
fi
fi
fi
fi
parse_frontmatter "$md_file"
[ "$fm_draft" = "true" ] && continue
md_heading="$fm_title"
if [ -z "$md_heading" ]; then
md_heading=$(grep -m 1 '^# ' "$md_file" | sed 's/^# *//; s/ *$//')
if [ -n "$md_heading" ]; then
md_heading=$(echo "$md_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')
fi
fi
if [ -z "$md_heading" ]; then
basename_no_ext=$(basename "$md_file" .md)
if [ "$basename_no_ext" != "index" ] && [ "$basename_no_ext" != "404_gen" ]; then
md_heading=$(echo "$basename_no_ext" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')
else
md_heading="$title - Page"
fi
fi
if [ "$generate_search" = "true" ]; then
if [ -z "$fm_content_warning" ] || [ "$include_cw_pages_in_search" = "true" ]; then
md_content=$(awk '{
if (NR == 1 && $0 == "---") { in_fm = 1; next }
if (in_fm && $0 == "---") { in_fm = 0; next }
if (in_fm) next
if ($0 ~ /^```/) { in_code = !in_code; next }
if (in_code) next
print
}' "$md_file" | sed \
-e 's/^#\{1,6\} //' \
-e 's/\*\*\([^*]*\)\*\*/\1/g' \
-e 's/\*\([^*]*\)\*/\1/g' \
-e 's/__\([^_]*\)__/\1/g' \
-e 's/_\([^_]*\)_/\1/g' \
-e 's/`\([^`]*\)`/\1/g' \
-e 's/\[\([^]]*\)](\([^)]*\))/\1/g' \
-e 's/!\[\([^]]*\)](\([^)]*\))//g' \
-e 's/^[[:space:]]*[-*+] //' \
-e 's/^[[:space:]]*[0-9]\{1,\}\. //' \
-e 's/^>[[:space:]]*//' \
-e 's/<[^>]*>//g' \
-e '/^[[:space:]]*$/d' \
-e 's/|//g' \
-e 's/^[[:space:]]*---[[:space:]]*$//' \
| tr '\n' ' ' | sed -e 's/ */ /g' -e 's/\\/\\\\/g' -e 's/"/\\"/g' | head -c 500)
if [ "$first_search_item" = "false" ]; then
printf ',\n' >> "$out/search.json"
fi
printf ' {"url": "%s", "title": "%s", "content": "%s"}' "$md_url" "$md_heading" "$md_content" >> "$out/search.json"
first_search_item="false"
fi
fi
if [ "$generate_tags" = "true" ] && [ -n "$fm_tags" ]; then
old_ifs=$IFS
IFS=','
for tag in $fm_tags; do
tag=$(echo "$tag" | sed 's/^[ \t]*//;s/[ \t]*$//')
[ -z "$tag" ] && continue
printf '%s|%s|%s\n' "$tag" "$md_url" "$md_heading" >> "$temp_tags"
done
IFS=$old_ifs
fi
done
if [ "$generate_search" = "true" ]; then
printf '\n]\n' >> "$out/search.json"
cp "$script_dir/lib/search.js" "$out/search.js"
search_md="$KEWT_TMPDIR/search_$$.md"
printf '%s\n' '# Search' '' \
'<form class="kewt-search-page" action="/search.html" method="get">' \
' <input type="text" id="search-box" name="q" placeholder="Search..." required>' \
' <button type="submit">Search</button>' \
'</form>' '' \
'<div id="search-results-list">' \
' <p>Loading...</p>' \
'</div>' '' \
'<script src="/search.js"></script>' > "$search_md"
render_markdown "$search_md" "false" "/search.html" > "$out/search.html"
rm -f "$search_md"
fi
if [ "$generate_tags" = "true" ]; then
tags_out_dir="$out/$tags_dir"
mkdir -p "$tags_out_dir"
tags_index_md="$KEWT_TMPDIR/tags_index_$$.md"
echo "# Tags" > "$tags_index_md"
echo "" >> "$tags_index_md"
cut -d'|' -f1 "$temp_tags" | sort -u | while IFS= read -r tag; do
tag_slug=$(echo "$tag" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g')
echo "- [$tag](/$(echo $tags_dir | sed 's|^\/||; s|\/$||')/$tag_slug.html)" >> "$tags_index_md"
tag_page_md="$KEWT_TMPDIR/tag_page_$$.md"
echo "# Tag: $tag" > "$tag_page_md"
echo "" >> "$tag_page_md"
echo "Posts tagged with **$tag**:" >> "$tag_page_md"
echo "" >> "$tag_page_md"
grep "^${tag}|" "$temp_tags" | while IFS='|' read -r _t t_url t_title; do
echo "- [$t_title]($t_url)" >> "$tag_page_md"
done
render_markdown "$tag_page_md" "false" "/$tags_dir/$tag_slug.html" > "$tags_out_dir/$tag_slug.html"
rm -f "$tag_page_md"
done
render_markdown "$tags_index_md" "false" "/$tags_dir/index.html" > "$tags_out_dir/index.html"
rm -f "$tags_index_md"
fi
rm -f "$temp_tags"
fi
echo "Build complete."
}

140
lib/commands.sh Normal file
View File

@@ -0,0 +1,140 @@
usage() {
invoked_as=$(basename "${KEWT_INVOKED_AS:-$0}")
cat <<EOF
Usage: $invoked_as [--from <src>] [--to <out>]
$invoked_as [src] [out]
$invoked_as --new [title]
$invoked_as --update [dir]
$invoked_as --post
$invoked_as --generate-template
$invoked_as --version
$invoked_as --help
Options:
--help Show this help message.
--new [title] Create a new site directory (default: site)
--update [dir] Update site.conf and template.html with latest defaults (defaults to current directory)
--post Create a new empty post file in the configured posts_dir with current date and time as name
--generate-template [path] Generate a new template file at <path> (default: template.html)
--version Show version information.
--from <src> Source directory (default: site)
--to <out> Output directory (default: out)
--watch, -w Watch for file changes and rebuild automatically.
--serve, -s [port] Start a local HTTP server after building (default port: 8000).
EOF
}
generate_template() {
_gt_path="$1"
[ -e "$_gt_path" ] && die "File '$_gt_path' already exists."
_gt_dir=$(dirname "$_gt_path")
[ -d "$_gt_dir" ] || mkdir -p "$_gt_dir"
printf '%s\n' "$DEFAULT_TMPL" > "$_gt_path"
echo "Generated template at '$_gt_path'."
exit 0
}
create_new_site() {
new_title="$1"
new_dir="site"
[ -n "$new_title" ] && new_dir="$new_title"
[ -e "$new_dir" ] && die "Target '$new_dir' already exists."
mkdir -p "$new_dir"
printf '%s\n' "$DEFAULT_CONF" > "$new_dir/site.conf"
printf '%s\n' "$DEFAULT_TMPL" > "$new_dir/template.html"
printf "# _kewt_ website\n" > "$new_dir/index.md"
if [ -n "$new_title" ]; then
AWK_NEW_TITLE="$new_title" awk -f "$awk_dir/update_site_conf.awk" "$new_dir/site.conf" > "$new_dir/site.conf.tmp" && mv "$new_dir/site.conf.tmp" "$new_dir/site.conf"
fi
echo "Created new site at '$new_dir'."
exit 0
}
create_new_post() {
post_src_dir="$1"
post_user_title="$2"
target_dir="$post_src_dir"
if [ -n "$posts_dir" ]; then
target_dir="$post_src_dir/$posts_dir"
fi
mkdir -p "$target_dir"
base_filename="$(date +%Y-%m-%d-%H-%M)"
filename="${base_filename}.md"
file_path="$target_dir/$filename"
counter=1
while [ -e "$file_path" ]; do
filename="${base_filename}_${counter}.md"
file_path="$target_dir/$filename"
counter=$((counter + 1))
done
post_date_val="$(date "+%Y-%m-%d %H:%M")"
if [ -n "$post_user_title" ]; then
printf -- '---\ntitle = "%s"\ndate = "%s"\ndraft = %s\n---\n# %s\n' "$post_user_title" "$post_date_val" "$draft_by_default" "$post_user_title" > "$file_path"
else
printf -- '---\ndate = "%s"\ndraft = %s\n---\n' "$post_date_val" "$draft_by_default" > "$file_path"
fi
echo "Created new post at '$file_path'."
exit 0
}
update_site() {
update_dir="${1:-.}"
[ -d "$update_dir" ] || die "Directory '$update_dir' does not exist."
target_conf="$update_dir/site.conf"
target_tmpl="$update_dir/template.html"
# Generate default site.conf
default_conf="$KEWT_TMPDIR/default_site.conf"
printf '%s\n' "$DEFAULT_CONF" > "$default_conf"
# Update site.conf
if [ ! -f "$target_conf" ]; then
echo "No site.conf found in '$update_dir'; nothing to update."
else
added=0
while IFS= read -r line; do
case "$line" in
''|'#'*) continue ;;
*=*) ;;
*) continue ;;
esac
key=$(printf '%s' "${line%%=*}" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
if ! grep -q "^[[:space:]]*${key}[[:space:]]*=" "$target_conf"; then
printf '%s\n' "$line" >> "$target_conf"
echo " Added: $key"
added=$((added + 1))
fi
done < "$default_conf"
if [ "$added" -eq 0 ]; then
echo "site.conf is already up to date."
else
echo "Added $added new key(s) to '$target_conf'."
fi
fi
# Update template.html
if [ -f "$target_tmpl" ]; then
default_tmpl="$KEWT_TMPDIR/default_template.html"
printf '%s\n' "$DEFAULT_TMPL" > "$default_tmpl"
if cmp -s "$default_tmpl" "$target_tmpl" 2>/dev/null; then
echo "template.html is already up to date."
else
cp "$default_tmpl" "${target_tmpl}.default"
echo "template.html has local changes; saved latest default as '${target_tmpl}.default'."
echo ""
diff "$target_tmpl" "${target_tmpl}.default" || true
fi
fi
exit 0
}

162
lib/config.sh Normal file
View File

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

289
lib/generator.sh Normal file
View File

@@ -0,0 +1,289 @@
SEARCH_FORM_FOOTER='<form class="kewt-search-footer" action="/search.html" method="get"><input type="text" name="q" placeholder="Search..." required><button type="submit">Go</button></form>'
SEARCH_FORM_HEADER='<form class="kewt-search-header" action="/search.html" method="get"><input type="text" name="q" placeholder="Search..." required><button type="submit">Go</button></form>'
SEARCH_FORM_NAV='<div class="kewt-search-nav"><form action="/search.html" method="get"><input type="text" name="q" placeholder="Search..." required><button type="submit">Go</button></form></div>'
generate_nav() {
dinfo=$(eval "find \"$1\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -print" | sort | AWK_SRC="$1" awk -f "$awk_dir/collect_dir_info.awk")
find_cmd="find \"$1\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -name \"*.md\" -print"
if [ -n "$posts_dir" ] && [ -d "$1/$posts_dir" ]; then
find_cmd="$find_cmd && echo \"$1/$posts_dir/index.md\""
fi
eval "$find_cmd" | sort -u | AWK_SRC="$1" AWK_SINGLE_FILE_INDEX="$single_file_index" AWK_FLATTEN="$flatten" AWK_ORDER="$order" AWK_HOME_NAME="$home_name" AWK_SHOW_HOME_IN_NAV="$show_home_in_nav" AWK_DINFO="$dinfo" awk -f "$awk_dir/generate_sidebar.awk"
}
escape_html_text() {
printf '%s' "$1" | sed \
-e 's/&/\&amp;/g' \
-e 's/</\&lt;/g' \
-e 's/>/\&gt;/g'
}
escape_html_attr() {
printf '%s' "$1" | sed \
-e 's/&/\&amp;/g' \
-e 's/"/\&quot;/g' \
-e 's/</\&lt;/g' \
-e 's/>/\&gt;/g'
}
parse_frontmatter() {
_fm_file="$1"
_fm_out="$KEWT_TMPDIR/fm_vals.txt"
: > "$_fm_out"
awk -v fm_out="$_fm_out" -f "$awk_dir/frontmatter.awk" "$_fm_file" > /dev/null
fm_title=""
fm_date=""
fm_draft=""
fm_description=""
fm_content_warning=""
fm_tags=""
while IFS='=' read -r _fk _fv; do
case "$_fk" in
title) fm_title="$_fv" ;;
date) fm_date="$_fv" ;;
draft) fm_draft="$_fv" ;;
description) fm_description="$_fv" ;;
content_warning) fm_content_warning="$_fv" ;;
tags) fm_tags="$_fv" ;;
esac
done < "$_fm_out"
rm -f "$_fm_out"
}
nav_links_html() {
[ -n "$nav_links" ] || return
old_ifs=$IFS
set -f
IFS=','
# shellcheck disable=SC2086
set -- $nav_links
IFS=$old_ifs
set +f
[ $# -gt 0 ] || return
printf '<ul class="nav-extra-links">\n'
for raw_link in "$@"; do
link=$(printf '%s' "$raw_link" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -n "$link" ] || continue
case "$link" in
\[*\]\(*\))
label=${link#\[}
label=${label%%\]*}
link_url=${link#*](}
link_url=${link_url%)}
;;
*)
link_url=$link
label=$(printf '%s' "$link" | sed \
-e 's|^[A-Za-z][A-Za-z0-9+.-]*://||' \
-e 's|/$||')
[ -n "$label" ] || label="$link"
;;
esac
[ -n "$link_url" ] || continue
[ -n "$label" ] || label="$link_url"
link_attr=$(escape_html_attr "$link_url")
label_text=$(escape_html_text "$label")
printf '<li><a href="%s">%s</a></li>\n' "$link_attr" "$label_text"
done
printf '</ul>'
}
find_closest() {
target="$1"
start_dir="$2"
curr="$start_dir"
while [ "$curr" != "$src" ] && [ "$curr" != "." ] && [ "$curr" != "/" ]; do
if [ -f "$curr/$target" ]; then
echo "$curr/$target"
return
fi
curr=$(dirname "$curr")
done
if [ -f "$src/$target" ]; then
echo "$src/$target"
fi
}
copy_style_with_resolved_vars() {
src_style="$1"
out_style="$2"
awk -f "$awk_dir/replace_variables.awk" "$src_style" > "$out_style"
}
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="/${rel_path%.md}.html"
fi
content_file="$file"
if [ -n "$posts_dir" ] && [ "$file" != "$src/$posts_dir/index.md" ]; then
rel_dir_of_url=$(dirname "$current_url")
rel_dir_of_url="${rel_dir_of_url#/}"
if { [ "$rel_dir_of_url" = "$posts_dir" ] || [ "./$rel_dir_of_url" = "$posts_dir" ]; } && [ "$(basename "$current_url")" != "index.html" ]; then
temp_post_with_backlink="$KEWT_TMPDIR/post_with_backlink_$$.md"
printf "[< Back](index.html)\n\n" > "$temp_post_with_backlink"
awk -f "$awk_dir/frontmatter.awk" "$file" >> "$temp_post_with_backlink"
post_md_name="$(basename "$current_url" .html).md"
prevnext_file="$KEWT_TMPDIR/prevnext/$post_md_name"
if [ -f "$prevnext_file" ]; then
IFS='|' read -r prev_str next_str < "$prevnext_file"
printf "\n\n---\n<div class=\"post-nav\">\n" >> "$temp_post_with_backlink"
if [ -n "$prev_str" ]; then
printf "<span class=\"prev-post\">%s</span>\n" "$prev_str" >> "$temp_post_with_backlink"
fi
if [ -n "$next_str" ]; then
printf "<span class=\"next-post\">%s</span>\n" "$next_str" >> "$temp_post_with_backlink"
fi
printf "</div>\n" >> "$temp_post_with_backlink"
fi
content_file="$temp_post_with_backlink"
fi
fi
local_template=$(find_closest "template.html" "$(dirname "$file")")
[ -z "$local_template" ] && local_template="$template"
closest_style_src=$(find_closest "styles.css" "$(dirname "$file")")
[ -z "$closest_style_src" ] && closest_style_src=$(find_closest "style.css" "$(dirname "$file")")
if [ -n "$closest_style_src" ]; then
style_rel_to_src="${closest_style_src#"$src"/}"
case "$closest_style_src" in
"$src/styles.css") style_rel_to_src="styles.css" ;;
"$src/style.css") style_rel_to_src="style.css" ;;
esac
style_path="/${style_rel_to_src%styles.css}"
style_path="${style_path%style.css}styles.css"
else
style_path="/styles.css"
fi
logo_html=""
if [ "$display_logo" = "true" ] && [ -n "$logo" ]; then
logo_html="<img class=\"site-logo\" src=\"$logo\" alt=\"$title\" />"
fi
brand_text=""
if [ "$display_title" = "true" ]; then
brand_text="$title"
fi
if [ -n "$logo_html" ] && [ -n "$brand_text" ]; then
header_brand="<a href=\"/index.html\">$logo_html $brand_text</a>"
elif [ -n "$logo_html" ]; then
header_brand="<a href=\"/index.html\">$logo_html</a>"
elif [ -n "$brand_text" ]; then
header_brand="<a href=\"/index.html\">$brand_text</a>"
else
header_brand="<a href=\"/index.html\">$title</a>"
fi
favicon_src=""
if [ "$logo_as_favicon" = "true" ] && [ -n "$logo" ]; then
favicon_src="$logo"
elif [ -n "$favicon" ]; then
favicon_src="$favicon"
fi
head_extra=""
if [ -n "$favicon_src" ]; then
if echo "$favicon_src" | grep -q "^http"; then
head_extra="<link rel=\"icon\" href=\"$favicon_src\" />"
elif echo "$favicon_src" | grep -q "^/"; then
head_extra="<link rel=\"icon\" href=\"$favicon_src\" />"
else
head_extra="<link rel=\"icon\" href=\"/$favicon_src\" />"
fi
fi
parse_frontmatter "$file"
page_title="$title"
if [ -n "$fm_title" ]; then
page_title="$fm_title - $title"
elif [ "$generate_page_title" = "true" ] && [ -n "$file" ] && [ -f "$file" ]; then
if [ "$is_home" = "true" ] && [ -n "$home_name" ]; then
page_title="$home_name - $title"
else
first_heading=$(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
head_extra_og="<meta property=\"og:title\" content=\"$(escape_html_attr "$page_title")\" />"
if [ -n "$fm_description" ]; then
head_extra_og="$head_extra_og
<meta property=\"og:description\" content=\"$(escape_html_attr "$fm_description")\" />"
fi
og_url="${base_url%/}${current_url}"
head_extra_og="$head_extra_og
<meta property=\"og:url\" content=\"$(escape_html_attr "$og_url")\" />"
if [ -n "$head_extra" ]; then
head_extra="$head_extra
$head_extra_og"
else
head_extra="$head_extra_og"
fi
if [ "$is_cw_content_page" = "true" ] && [ "$cw_hide_url" = "true" ]; then
head_extra="$head_extra
<script>window.history.replaceState(null, '', '$current_url');</script>"
fi
final_footer="$footer"
if [ "$search_in_footer" = "true" ]; then
final_footer="$footer $SEARCH_FORM_FOOTER"
fi
final_nav="$nav"
final_header_brand="$header_brand"
if [ "$search_in_header" = "true" ]; then
final_header_brand="$header_brand $SEARCH_FORM_HEADER"
final_nav="$SEARCH_FORM_NAV
$nav"
fi
ENABLE_HEADER_LINKS="$enable_header_links" CUSTOM_ADMONITIONS="$custom_admonitions" MARKDOWN_SITE_ROOT="$src" MARKDOWN_FALLBACK_FILE="$script_dir/styles/$style.css" sh "$script_dir/markdown.sh" "$content_file" | AWK_LANG="$lang" AWK_CURRENT_URL="$current_url" AWK_TITLE="$page_title" AWK_NAV="$final_nav" AWK_FOOTER="$final_footer" AWK_STYLE_PATH="${style_path}" AWK_HEADER_BRAND="$final_header_brand" AWK_HEAD_EXTRA="$head_extra" AWK_VERSION="$asset_version" AWK_CONTENT_WARNING="$fm_content_warning" awk -f "$awk_dir/render_template.awk" "$local_template"
}
generate_content_warning_page() {
_fm_title="$1"
_fm_content_warning="$2"
_content_rel_url="$3"
_target_url="$4"
_out_file="$5"
_is_home="$6"
_temp_cw="$KEWT_TMPDIR/cw_$$.md"
_cw_text="${_fm_content_warning}"
[ "$_cw_text" = "true" ] && _cw_text="This content may be sensitive."
cat <<EOF > "$_temp_cw"
---
title = "$_fm_title"
---
> [!CAUTION]
> **Content Warning:** $_cw_text
<a href="$(basename "$_content_rel_url")" class="cw-button">Reveal Content</a>
EOF
render_markdown "$_temp_cw" "$_is_home" "$_target_url" > "$_out_file"
rm -f "$_temp_cw"
}

44
lib/search.js Normal file
View File

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

View File

@@ -26,4 +26,6 @@ build() {
package() {
cd "${pkgname}"
install -Dm755 kewt "${pkgdir}/usr/bin/kewt"
install -d "${pkgdir}/usr/share/zsh/site-functions"
"${pkgdir}/usr/bin/kewt" --dump-zsh-completions > "${pkgdir}/usr/share/zsh/site-functions/_kewt"
}

View File

@@ -18,4 +18,6 @@ build() {
package() {
install -Dm755 "${srcdir}/${pkgname}-${pkgver}.sh" "${pkgdir}/usr/bin/kewt"
install -d "${pkgdir}/usr/share/zsh/site-functions"
"${pkgdir}/usr/bin/kewt" --dump-zsh-completions > "${pkgdir}/usr/share/zsh/site-functions/_kewt"
}

View File

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

22
packaging/zsh/_kewt Normal file
View File

@@ -0,0 +1,22 @@
#compdef kewt
_kewt() {
local -a args
args=(
'--help[Show help message]'
'(-h)--help[Show help message]'
'(-)--new[Create a new site directory]'
'(-)--update[Update site.conf and template.html with latest defaults]'
'(-)--post[Create a new empty post file in the configured posts_dir]'
'(-)--generate-template[Generate a new template file]'
'(-v --version)'{-v,--version}'[Show version information]'
'--from[Source directory]:directory:_directories'
'--to[Output directory]:directory:_directories'
'(-w --watch)'{-w,--watch}'[Watch for file changes and rebuild automatically]'
'(-s --serve)'{-s,--serve}'[Start a local HTTP server after building]::port:'
)
_arguments -S -C $args '*: :_directories'
}
_kewt "$@"

View File

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

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

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

View File

@@ -41,5 +41,4 @@ It's meant to be a static site generator, like _[kew](https://github.com/uint23/
***
> [!WARNING]
> The base that all of this is built upon was coded at night, while sleepy and a bit sick, and after walking for about 4 hours around a forest, so...
## [Quickstart guide](/docs/quickstart.html)

View File

@@ -23,3 +23,8 @@ custom_admonitions = ""
generate_feed = false
feed_file = "rss.xml"
posts_dir = ""
generate_tags = false
generate_search = true
search_in_footer = true
search_in_header = true
include_cw_pages_in_search = false

View File

@@ -48,6 +48,9 @@ header h1 {
font-weight: bold;
font-style: italic;
color: var(--fg-heading);
display: flex;
align-items: center;
flex-wrap: wrap;
}
.site-logo {
@@ -218,6 +221,7 @@ pre code {
background: var(--adm-caution-bg);
border-color: var(--adm-caution-border);
}
.cw-button {
display: inline-block;
padding: 8px 16px;
@@ -322,3 +326,214 @@ hr {
margin: 0 0.2em 0.25em -1.6em;
vertical-align: middle;
}
.kewt-search-page {
display: flex;
gap: 8px;
margin: 20px 0;
}
.kewt-search-page input[type="text"] {
flex: 1;
padding: 8px 12px;
font-size: 16px;
font-family: inherit;
background: var(--code-bg);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
outline: none;
}
.kewt-search-page input[type="text"]:focus {
border-color: var(--fg-link);
}
.kewt-search-page button {
padding: 8px 20px;
font-size: 16px;
font-family: inherit;
background: var(--bg-deep);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
cursor: pointer;
}
.kewt-search-page button:hover {
background: var(--fg);
color: var(--bg);
}
.search-result {
margin: 16px 0;
padding: 12px;
background: var(--code-bg);
border: 1px solid var(--code-border);
border-radius: 3px;
}
.search-result a {
font-size: 18px;
font-weight: bold;
color: var(--fg-link);
}
.search-result p {
margin: 6px 0 0 0;
color: var(--fg-muted);
font-size: 14px;
}
/* Footer search */
.kewt-search-footer {
display: inline-flex;
gap: 4px;
margin-left: 12px;
vertical-align: middle;
}
.kewt-search-footer input[type="text"] {
padding: 3px 8px;
font-size: 14px;
font-family: inherit;
background: var(--code-bg);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
outline: none;
width: 120px;
}
.kewt-search-footer input[type="text"]:focus {
border-color: var(--fg-link);
}
.kewt-search-footer button {
padding: 3px 10px;
font-size: 14px;
font-family: inherit;
background: var(--bg-deep);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
cursor: pointer;
}
.kewt-search-footer button:hover {
background: var(--fg);
color: var(--bg);
}
.kewt-search-header {
display: inline-flex;
gap: 4px;
margin-left: auto;
vertical-align: middle;
font-style: normal;
}
.kewt-search-header input[type="text"] {
padding: 4px 8px;
font-size: 14px;
font-family: inherit;
background: var(--code-bg);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
outline: none;
width: 160px;
}
.kewt-search-header input[type="text"]:focus {
border-color: var(--fg-link);
}
.kewt-search-header button {
padding: 4px 10px;
font-size: 14px;
font-family: inherit;
background: var(--bg-deep);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
cursor: pointer;
}
.kewt-search-header button:hover {
background: var(--fg);
color: var(--bg);
}
.kewt-search-nav {
display: none;
padding: 8px 0 12px 0;
margin-bottom: 8px;
border-bottom: 1px solid var(--code-border);
}
.kewt-search-nav form {
display: flex;
gap: 4px;
}
.kewt-search-nav input[type="text"] {
flex: 1;
padding: 6px 8px;
font-size: 14px;
font-family: inherit;
background: var(--code-bg);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
outline: none;
}
.kewt-search-nav input[type="text"]:focus {
border-color: var(--fg-link);
}
.kewt-search-nav button {
padding: 6px 10px;
font-size: 14px;
font-family: inherit;
background: var(--bg-deep);
color: var(--fg);
border: 1px solid var(--code-border);
border-radius: 3px;
cursor: pointer;
}
.kewt-search-nav button:hover {
background: var(--fg);
color: var(--bg);
}
@media screen and (max-width: 600px) {
.kewt-search-header {
display: none;
}
.kewt-search-nav {
display: block;
}
.kewt-search-page {
flex-direction: column;
}
.kewt-search-page button {
align-self: flex-start;
}
.kewt-search-footer {
display: flex;
margin-left: 0;
margin-top: 8px;
}
.kewt-search-footer input[type="text"] {
flex: 1;
width: auto;
}
}

View File

@@ -28,10 +28,10 @@ EOF
VERSION=$(git describe --tags 2>/dev/null || echo "standalone")
tmpbuild=$(mktemp -d)
cp -r "$REPO_ROOT/kewt.sh" "$REPO_ROOT/markdown.sh" "$REPO_ROOT/awk" "$REPO_ROOT/styles" "$tmpbuild/"
cp -r "$REPO_ROOT/kewt.sh" "$REPO_ROOT/markdown.sh" "$REPO_ROOT/awk" "$REPO_ROOT/styles" "$REPO_ROOT/lib" "$tmpbuild/"
sed -e "s/kewt version git/kewt version $VERSION/" "$tmpbuild/kewt.sh" > "$tmpbuild/kewt.sh.tmp" && mv "$tmpbuild/kewt.sh.tmp" "$tmpbuild/kewt.sh"
chmod +x "$tmpbuild/kewt.sh" "$tmpbuild/markdown.sh"
tar -cz -C "$tmpbuild" kewt.sh markdown.sh awk styles >> "$OUT_FILE"
tar -cz -C "$tmpbuild" kewt.sh markdown.sh awk styles lib >> "$OUT_FILE"
rm -rf "$tmpbuild"
chmod +x "$OUT_FILE"