diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..e70c898 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1 @@ +disable=SC2034,SC2154 diff --git a/Makefile b/Makefile index 3386e44..a281bdd 100644 --- a/Makefile +++ b/Makefile @@ -20,4 +20,10 @@ uninstall: clean: 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 diff --git a/awk/reference_links.awk b/awk/reference_links.awk new file mode 100644 index 0000000..9a08a88 --- /dev/null +++ b/awk/reference_links.awk @@ -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 "\""" +} + +function resolve_link_ref(text, id, url, title) { + url = refs[id] + title = (id in titles) ? titles[id] : "" + if (url == "") return "[" text "][" id "]" + return "" text "" +} + +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]) + } +} diff --git a/lib/builder.sh b/lib/builder.sh index 4b28a4c..565aeb9 100644 --- a/lib/builder.sh +++ b/lib/builder.sh @@ -1,4 +1,5 @@ #!/bin/sh +# shellcheck disable=SC2129 needs_rebuild() { 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" } -build_site() { -echo "Building site from '$src' to '$out'..." +build_dir_entries_list() { + _bde_dir="$1" + _bde_rel_dir="$2" + _bde_entries_file="$3" -build_markdown_manifest -build_full_nav + find "$_bde_dir" ! -name "$(basename "$_bde_dir")" -prune ! -name ".*" -print | while read -r entry; do + 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 - rel_dir="${dir#"$src"}" - rel_dir="${rel_dir#/}" - [ -z "$rel_dir" ] && rel_dir="." - out_dir="$out/$rel_dir" - mkdir -p "$out_dir" + post_h="$manifest_title" - 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" + is_post_entry="false" + if is_posts_directory_rel "$_bde_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}" >> "$_bde_entries_file" + else + asset_url=$(encode_url_path "$name") + echo "${name}|- [$name]($asset_url)|$name|$asset_url" >> "$_bde_entries_file" 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 + done +} - [ "$dir_indexes" != "true" ] && continue +build_dir_index() { + _bdi_dir="$1" + _bdi_rel_dir="$2" + _bdi_out_dir="$3" has_custom_index="false" has_list="false" - if [ -f "$dir/index.md" ]; then + if [ -f "$_bdi_dir/index.md" ]; then 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" fi fi if [ "$has_custom_index" = "false" ] || [ "$has_list" = "true" ]; then 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" fi 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" - is_home="false"; [ "$dir" = "$src" ] && is_home="true" - target_url=$(directory_index_url "$rel_dir") - if needs_rebuild "$md_file" "$out_dir/index.html"; then + is_home="false"; [ "$_bdi_dir" = "$src" ] && is_home="true" + target_url=$(directory_index_url "$_bdi_rel_dir") + if needs_rebuild "$md_file" "$_bdi_out_dir/index.html"; then parse_frontmatter "$md_file" if [ -n "$fm_content_warning" ]; then - content_out_file="$out_dir/content.html" - if [ "$rel_dir" = "." ]; then + content_out_file="$_bdi_out_dir/content.html" + if [ "$_bdi_rel_dir" = "." ]; then content_rel_url="/content.html" else - content_rel_url="/$(encode_url_path "$rel_dir")/content.html" + content_rel_url="/$(encode_url_path "$_bdi_rel_dir")/content.html" 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 - 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 - continue + return 0 fi fi @@ -96,74 +136,21 @@ eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while : > "$temp_list" if [ "$has_custom_index" = "false" ]; then - display_dir="${rel_dir#.}" + display_dir="${_bdi_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 is_posts_directory_rel "$rel_dir"; then + if is_posts_directory_rel "$_bdi_rel_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|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 + build_dir_entries_list "$_bdi_dir" "$_bdi_rel_dir" "$temp_entries" if [ "$is_posts_dir" = "true" ]; then 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 rm -f "$temp_entries" - is_home="false"; [ "$dir" = "$src" ] && is_home="true" - target_url=$(directory_index_url "$rel_dir") + is_home="false"; [ "$_bdi_dir" = "$src" ] && is_home="true" + target_url=$(directory_index_url "$_bdi_rel_dir") - 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 )) - 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" + 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" + rm -f "$temp_index" "$temp_list" + fi +} - base_url_dir="$(dirname "$target_url")" - [ "$base_url_dir" = "/" ] && base_url_dir="" +render_paginated_index() { + _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="
" - if [ "$p" -gt 1 ]; then - if [ "$p" -eq 2 ]; then - nav_html="$nav_html « Prev " - else - nav_html="$nav_html « Prev " - fi - fi - nav_html="$nav_html Page $p of $num_pages " - if [ "$p" -lt "$num_pages" ]; then - nav_html="$nav_html Next » " - fi - nav_html="$nav_html
" + num_items=$(wc -l < "$_rpi_temp_list") + if [ "$_rpi_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 )) + 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 "$_rpi_temp_list" | head -n "$posts_per_page" > "$chunk_list" - echo "" >> "$chunk_list" - echo "$nav_html" >> "$chunk_list" + base_url_dir="$(dirname "$_rpi_target_url")" + [ "$base_url_dir" = "/" ] && base_url_dir="" - 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" + nav_html="
" + if [ "$p" -gt 1 ]; then + if [ "$p" -eq 2 ]; then + nav_html="$nav_html « Prev " else - : > "$temp_index_p" + nav_html="$nav_html « Prev " fi + fi + nav_html="$nav_html Page $p of $num_pages " + if [ "$p" -lt "$num_pages" ]; then + nav_html="$nav_html Next » " + fi + nav_html="$nav_html
" - 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 + echo "" >> "$chunk_list" + echo "$nav_html" >> "$chunk_list" - 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 + temp_index_p="$KEWT_TMPDIR/index_p$p.md" + if [ "$_rpi_has_custom_index" = "false" ]; then + display_dir="${_rpi_rel_dir#.}" + [ -z "$display_dir" ] && display_dir="/" + echo "# Index of $display_dir" > "$temp_index_p" + echo "" >> "$temp_index_p" + else + : > "$temp_index_p" + fi - render_markdown "$temp_index_p" "$is_home" "$target_url_p" > "$out_file" - rm -f "$temp_index_p" "$chunk_list" - p=$((p + 1)) - done - else - if [ "$has_custom_index" = "true" ]; then + if [ "$_rpi_has_custom_index" = "true" ]; then awk ' /^[[:space:]]*\{\{LIST\}\}[[:space:]]*$/ { - while((getline line < "'"$temp_list"'") > 0) print line - close("'"$temp_list"'") + while((getline line < "'"$chunk_list"'") > 0) print line + close("'"$chunk_list"'") next } { print } - ' "$dir/index.md" > "$temp_index" + ' "$_rpi_dir/index.md" >> "$temp_index_p" else - cat "$temp_list" >> "$temp_index" + cat "$chunk_list" >> "$temp_index_p" 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" - 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" + if [ "$p" -eq 1 ]; then + out_file="$_rpi_out_dir/index.html" + target_url_p="$_rpi_target_url" 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 + + 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 - if needs_rebuild "$file" "$out/$rel_path"; then - cp "$file" "$out/$rel_path" + if [ "$_rpi_has_custom_index" = "true" ]; then + 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 -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 +build_directories() { + 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 + + 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" base_url="${base_url%/}" 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 rel_url="${html_file#"$out"}" - - # Don't include 404 in the sitemap (duh) [ "${rel_url#/}" = "$error_page" ] && continue - { - printf ' \n' - printf ' %s%s\n' "$base_url" "$rel_url" - printf ' %s\n' "$today" - printf ' \n' - } >> "$sitemap_file" + printf ' \n %s%s\n %s\n \n' "$base_url" "$rel_url" "$today" >> "$sitemap_file" done printf '\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" base_url_feed="${base_url%/}" build_date=$(date -u '+%a, %d %b %Y %H:%M:%S +0000') printf '\n' > "$feed_path" - { - printf '\n' - printf ' \n' - printf ' %s\n' "$title" - printf ' %s\n' "$base_url_feed" - printf ' %s\n' "$title" - printf ' %s\n' "$build_date" - } >> "$feed_path" + printf '\n \n %s\n %s\n %s\n %s\n' \ + "$title" "$base_url_feed" "$title" "$build_date" >> "$feed_path" temp_feed_files="$KEWT_TMPDIR/feed_files_$$.txt" : > "$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') fi feed_post_title="$post_heading - $post_date $post_time" - post_url="$base_url_feed$manifest_url" - pub_date=$(format_rfc2822_utc "$post_date" "$post_time") - { - printf ' \n' - printf ' %s\n' "$feed_post_title" - printf ' %s\n' "$post_url" - printf ' %s\n' "$post_url" - printf ' %s\n' "$pub_date" - printf ' \n' - } >> "$feed_path" + printf ' \n %s\n %s\n %s\n %s\n \n' \ + "$feed_post_title" "$post_url" "$post_url" "$pub_date" >> "$feed_path" done - printf ' \n' >> "$feed_path" - printf '\n' >> "$feed_path" -fi + printf ' \n\n' >> "$feed_path" +} -if [ "$generate_search" = "true" ] || [ "$generate_tags" = "true" ]; then - if [ "$generate_search" = "true" ]; then - printf '[\n' > "$out/search.json" - fi +build_search_index() { + printf '[\n' > "$out/search.json" first_search_item="true" - temp_tags="$KEWT_TMPDIR/tags_$$.txt" - : > "$temp_tags" while IFS= read -r rel_path; do load_manifest_entry "$rel_path" || continue @@ -529,80 +527,113 @@ if [ "$generate_search" = "true" ] || [ "$generate_tags" = "true" ]; then [ "$manifest_draft" = "true" ] && continue md_heading="$manifest_title" - if [ "$generate_search" = "true" ]; then - if [ -z "$manifest_content_warning" ] || [ "$include_cw_pages_in_search" = "true" ]; then - md_content="$manifest_search_content" + if [ -z "$manifest_content_warning" ] || [ "$include_cw_pages_in_search" = "true" ]; then + md_content="$manifest_search_content" 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 + 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' '' \ + '
' \ + ' ' \ + ' ' \ + '
' '' \ + '
' \ + '

Loading...

' \ + '
' '' \ + '' > "$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 IFS=',' for tag in $manifest_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" + printf '%s|%s|%s\n' "$tag" "$manifest_url" "$md_heading" >> "$temp_tags" done IFS=$old_ifs fi 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 - 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' '' \ - '
' \ - ' ' \ - ' ' \ - '
' '' \ - '
' \ - '

Loading...

' \ - '
' '' \ - '' > "$search_md" - render_markdown "$search_md" "false" "/search.html" > "$out/search.html" - rm -f "$search_md" + build_search_index 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" + build_tags fi - rm -f "$temp_tags" -fi -echo "Build complete." + echo "Build complete." } diff --git a/lib/commands.sh b/lib/commands.sh index 4f5948b..5e2fde1 100644 --- a/lib/commands.sh +++ b/lib/commands.sh @@ -1,4 +1,5 @@ #!/bin/sh +# shellcheck disable=SC2153 usage() { invoked_as=$(basename "${KEWT_INVOKED_AS:-$0}") @@ -97,11 +98,9 @@ update_site() { 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 @@ -126,7 +125,6 @@ update_site() { fi fi - # Update template.html if [ -f "$target_tmpl" ]; then default_tmpl="$KEWT_TMPDIR/default_template.html" printf '%s\n' "$DEFAULT_TMPL" > "$default_tmpl" diff --git a/lib/config.sh b/lib/config.sh index 7e4cdb3..ca386e7 100644 --- a/lib/config.sh +++ b/lib/config.sh @@ -62,106 +62,88 @@ DEFAULT_TMPL=' ' +_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() { - 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 kewt" - 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" + while IFS= read -r _rc_line; do + _load_conf_line "$_rc_line" + done </\>/g' } + escape_html_attr() { printf '%s' "$1" | sed \ -e 's/&/\&/g' \ @@ -50,13 +53,13 @@ escape_html_attr() { -e 's//\>/g' } + nav_links_html() { [ -n "$nav_links" ] || return old_ifs=$IFS set -f IFS=',' - # shellcheck disable=SC2086 set -- $nav_links IFS=$old_ifs set +f @@ -93,6 +96,7 @@ nav_links_html() { done printf '' } + find_closest() { target="$1" start_dir="$2" @@ -108,11 +112,13 @@ find_closest() { 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" } + merge_root_style() { root_file="$1" base_css="$2" @@ -134,51 +140,15 @@ merge_root_style() { ' "$base_css" } | awk -f "$awk_dir/replace_variables.awk" > "$out_file" } -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 - - 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
\n" >> "$temp_post_with_backlink" - if [ -n "$prev_str" ]; then - printf "%s\n" "$prev_str" >> "$temp_post_with_backlink" - fi - if [ -n "$next_str" ]; then - printf "%s\n" "$next_str" >> "$temp_post_with_backlink" - fi - printf "
\n" >> "$temp_post_with_backlink" - fi - content_file="$temp_post_with_backlink" - fi - fi - - local_template=$(find_closest "template.html" "$(dirname "$file")") +resolve_render_template() { + local_template=$(find_closest "template.html" "$(dirname "$1")") [ -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")") +resolve_render_style_path() { + 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 style_rel_to_src="${closest_style_src#"$src"/}" case "$closest_style_src" in @@ -190,7 +160,9 @@ render_markdown() { else style_path="/styles.css" fi +} +build_header_brand_html() { logo_html="" if [ "$display_logo" = "true" ] && [ -n "$logo" ]; then logo_html="\"$title\"" @@ -210,26 +182,27 @@ render_markdown() { else header_brand="$title" fi +} +build_favicon_head() { 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="" - elif echo "$favicon_src" | grep -q "^/"; then - head_extra="" - else - head_extra="" - fi + case "$favicon_src" in + http*|/*) head_extra="" ;; + *) head_extra="" ;; + esac + else + head_extra="" fi +} - parse_frontmatter "$file" - +build_page_title() { page_title="$title" if [ -n "$fm_title" ]; then page_title="$fm_title - $title" @@ -250,7 +223,9 @@ render_markdown() { fi fi fi +} +build_og_tags() { head_extra_og="" if [ -n "$fm_description" ]; then head_extra_og="$head_extra_og @@ -266,17 +241,23 @@ render_markdown() { else head_extra="$head_extra_og" fi +} +build_cw_url_hide() { if [ "$is_cw_content_page" = "true" ] && [ "$cw_hide_url" = "true" ]; then head_extra="$head_extra " fi +} +build_composed_footer() { final_footer="$footer" if [ "$search_in_footer" = "true" ]; then final_footer="$footer $SEARCH_FORM_FOOTER" fi +} +build_composed_nav() { final_nav="$nav" final_header_brand="$header_brand" final_header_search="" @@ -285,9 +266,65 @@ render_markdown() { final_nav="$SEARCH_FORM_NAV $nav" 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
\n" >> "$temp_post_with_backlink" + if [ -n "$prev_str" ]; then + printf "%s\n" "$prev_str" >> "$temp_post_with_backlink" + fi + if [ -n "$next_str" ]; then + printf "%s\n" "$next_str" >> "$temp_post_with_backlink" + fi + printf "
\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" } + generate_content_warning_page() { _fm_title="$1" _fm_content_warning="$2" diff --git a/lib/manifest.sh b/lib/manifest.sh index b7f3b01..16b980b 100644 --- a/lib/manifest.sh +++ b/lib/manifest.sh @@ -1,4 +1,5 @@ #!/bin/sh +# shellcheck disable=SC2016,SC2030,SC2031 shell_quote() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" diff --git a/markdown.sh b/markdown.sh index d75dbcb..5b93a7f 100755 --- a/markdown.sh +++ b/markdown.sh @@ -3,14 +3,11 @@ script_dir=$(CDPATH="" cd -- "$(dirname -- "$0")" && pwd) awk_dir="$script_dir/awk" -sed_inplace() { - script="$1" - file="$2" - tmp="${file}.tmp.$$" - if sed "$script" "$file" > "$tmp" && mv "$tmp" "$file"; then - return 0 - else - rm -f "$tmp" +run_awk() { + _ra_awk_file="$1" + shift + if ! awk -f "$_ra_awk_file" "$@"; then + echo "Error: AWK failed: $_ra_awk_file" >&2 return 1 fi } @@ -21,36 +18,20 @@ cat "$@" > "$temp_file" trap 'rm -f "$temp_file" "$temp_file.tmp" "$temp_file.fm"' EXIT INT TERM -# Frontmatter fm_file="$temp_file.fm" : > "$fm_file" -awk -v fm_out="$fm_file" -f "$awk_dir/frontmatter.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" -# Mask -awk -f "$awk_dir/mask_inline_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" -awk -f "$awk_dir/mask_plain.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" +run_awk "$awk_dir/frontmatter.awk" -v fm_out="$fm_file" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" -# Reference links -refs=$(awk '/^\[[^\]]+\]: */' "$temp_file") -IFS=' -' -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\]|\"\1\"|g" "$temp_file" - sed_inplace "s|\[\([^]]*\)\]\[$ref_id\]|\1|g" "$temp_file" - sed_inplace "s|!\[$ref_id\]\[\]|\"$ref_id\"|g" "$temp_file" - sed_inplace "s|\[$ref_id\]\[\]|$ref_id|g" "$temp_file" -done -sed_inplace "/^\[[^\]]*\]: */d" "$temp_file" - -# Blocks +run_awk "$awk_dir/mask_inline_code.awk" "$temp_file" \ + | run_awk "$awk_dir/mask_plain.awk" \ + | run_awk "$awk_dir/reference_links.awk" \ + > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" loop_count=0 max_iterations=100 -while grep '^>' "$temp_file" >/dev/null; do - awk -f "$awk_dir/blockquote.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" +while grep -q '^>' "$temp_file"; do + run_awk "$awk_dir/blockquote.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" loop_count=$((loop_count + 1)) if [ "$loop_count" -gt "$max_iterations" ]; then echo "Warning: Blockquote processing exceeded $max_iterations iterations on $1. Breaking to prevent infinite loop." >&2 @@ -58,25 +39,19 @@ while grep '^>' "$temp_file" >/dev/null; do fi 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" -awk -f "$awk_dir/fenced_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" -awk -f "$awk_dir/indented_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" -awk -f "$awk_dir/pipe_tables.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" -awk -f "$awk_dir/definition_lists.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" -awk -f "$awk_dir/lists.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file" +run_awk "$awk_dir/blockquote_to_admonition.awk" -v custom_admonitions="$CUSTOM_ADMONITIONS" "$temp_file" \ + | run_awk "$awk_dir/fenced_code.awk" \ + | run_awk "$awk_dir/indented_code.awk" \ + | run_awk "$awk_dir/pipe_tables.awk" \ + | run_awk "$awk_dir/definition_lists.awk" \ + | 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 -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" +rm -f "$temp_file" diff --git a/tests/test_config.sh b/tests/test_config.sh new file mode 100755 index 0000000..4df6cfb --- /dev/null +++ b/tests/test_config.sh @@ -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" < "$tmpdir/test.conf" <<'EOF' +footer = "made with kewt" +nav_links = "[Docs](/docs), [About](/about)" +EOF + + reset_config + load_config "$tmpdir/test.conf" + + assert_eq 'made with kewt' "$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" < "$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" +} diff --git a/tests/test_runner.sh b/tests/test_runner.sh new file mode 100755 index 0000000..40ac361 --- /dev/null +++ b/tests/test_runner.sh @@ -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 diff --git a/tests/test_runtime.sh b/tests/test_runtime.sh new file mode 100755 index 0000000..ff2d128 --- /dev/null +++ b/tests/test_runtime.sh @@ -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" +}