Add tests and refactor config

This commit is contained in:
2026-05-20 08:30:21 +02:00
parent 8b760b2897
commit cd9550ee8a
13 changed files with 1070 additions and 558 deletions

1
.shellcheckrc Normal file
View File

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

View File

@@ -20,4 +20,10 @@ uninstall:
clean: clean:
rm -f kewt rm -f kewt
.PHONY: all install uninstall clean test:
sh tests/test_runner.sh
shellcheck:
shellcheck kewt.sh markdown.sh lib/*.sh
.PHONY: all install uninstall clean test shellcheck

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

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,104 @@ 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
label="${name%.md}"
[ "$manifest_draft" = "true" ] && continue
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 +136,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 +184,242 @@ 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
[ "$manifest_draft" = "true" ] && continue
is_home="false"; [ "$file" = "$src/index.md" ] && is_home="true"
out_file="$out/${rel_path%.md}.html"
if needs_rebuild "$file" "$out_file"; then
fm_title="$manifest_title"
fm_content_warning="$manifest_content_warning"
if [ -n "$manifest_content_warning" ]; then
content_out_file="$out/${rel_path%.md}-content.html"
content_rel_url="/$(encode_url_path "${rel_path%.md}")-content.html"
orig_rel_url="$manifest_url"
write_content_warning_outputs "$file" "$content_out_file" "$content_rel_url" "$orig_rel_url" "$out_file" "$is_home"
else
render_markdown "$file" "$is_home" > "$out_file"
fi
fi
else
if needs_rebuild "$file" "$out/$rel_path"; then
cp "$file" "$out/$rel_path"
fi
fi
done
}
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 +429,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 +473,19 @@ 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")
{ printf ' <item>\n <title>%s</title>\n <link>%s</link>\n <guid>%s</guid>\n <pubDate>%s</pubDate>\n </item>\n' \
printf ' <item>\n' "$feed_post_title" "$post_url" "$post_url" "$pub_date" >> "$feed_path"
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 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 +527,113 @@ 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" ] && [ ! -f "$out/$error_page" ] || return
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"
}
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}")
@@ -97,11 +98,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 +125,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

@@ -62,106 +62,88 @@ 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" ;;
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")"

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 -f "$awk_dir/definition_lists.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" | run_awk "$awk_dir/definition_lists.awk" \
awk -f "$awk_dir/lists.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" | run_awk "$awk_dir/lists.awk" \
| 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 enable_header_links="$ENABLE_HEADER_LINKS" -f "$awk_dir/headers.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"

106
tests/test_config.sh Executable file
View File

@@ -0,0 +1,106 @@
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"
}

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

71
tests/test_runtime.sh Executable file
View File

@@ -0,0 +1,71 @@
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"
}