DistroHopper/create-web
2026-02-19 15:00:19 +01:00

425 lines
11 KiB
Bash
Executable file

#!/usr/bin/env bash
#shellcheck disable=SC2089
#
# Make website from .md files
#
# Author: zenobit <zen@duck.com>
# Date: February 14, 2026
# License: MIT
#
count=$(find ./ -name '*.md' | wc -l)
# Functions for colored bordered messages (gum) or ANSI colored messages
if command -v gum -v >/dev/null; then
_r() {
gum style --border rounded --border-foreground "#e50203" --padding "0 1" "${@}"
}
_re() {
gum style --border rounded --border-foreground "#e50203" --padding "0 1" "${@}"
exit 1
}
_g() {
gum style --border rounded --border-foreground "#14a113" --padding "0 1" "$@"
}
_b() {
gum style --border rounded --border-foreground "#004EFB" --padding "0 1" "$@"
}
_y() {
gum style --border rounded --border-foreground "#fdde13" --padding "0 1" "$@"
}
ask() {
gum confirm "$@"
}
_header() {
local text green red yellow
text="${1}"
red=$(gum style --border rounded --border-foreground "#e50203" --padding "0 1" "$text")
yellow=$(gum style --border rounded --border-foreground "#fdde13" --padding "0 1" "$red")
green=$(gum style --border rounded --border-foreground "#14a113" --padding "0 1" "$yellow")
echo "$green"
}
_footer() {
local text green red yellow
text="${1}"
green=$(gum style --border rounded --border-foreground "#14a113" --padding "0 1" "$text")
yellow=$(gum style --border rounded --border-foreground "#fdde13" --padding "0 1" "$green")
red=$(gum style --border rounded --border-foreground "#e50203" --padding "0 1" "$yellow")
echo "$red"
}
else
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
#CYAN='\033[0;36m'
NC='\033[0m'
_g() {
echo -e "${GREEN}${1}${NC}"
}
_r() {
echo -e "${RED}${1}${NC}"
}
_re() {
echo -e "${RED}${1}${NC}" >&2
exit 1
}
_b() {
echo -e "${BLUE}${1}${NC}"
}
_y() {
echo -e "${YELLOW}${1}${NC}"
}
ask() {
_y "Press Enter to continue
anything else for Quit..."
echo "$1"
read -n 1 -s key
if [[ ! $key == '' ]]; then
exit 1
fi
}
_header() {
echo -e "\n${RED}${1}${NC}\n"
}
_footer() {
echo -e "\n${RED}${1}${NC}\n"
}
fi
export -f _r _re _g _b _y
# ---------------------------------------------------------------------------
# Optional override: set FORGE_URL to your Gitea/Forgejo base URL to skip
# auto-detection, e.g. in .env or before calling the script:
# export FORGE_URL="https://git.example.com"
# ---------------------------------------------------------------------------
[ -f .env ] && source .env
# Try to detect a Gitea/Forgejo instance by probing /api/v1/meta
# Returns 0 (true) if confirmed, 1 if not
_is_gitea() {
local base_url="$1"
local response
response=$(curl -sf --max-time 5 "${base_url}/api/v1/meta" 2>/dev/null)
# Gitea/Forgejo meta endpoint returns JSON with a "version" field
[[ "$response" =~ \"version\" ]] && return 0
return 1
}
# Detect repository URL and platform from git remote origin
_detect_repo() {
if ! git rev-parse --git-dir >/dev/null 2>&1; then
_r "Not a git repository — repo buttons will be disabled"
REPO_URL="" REPO_PLATFORM="" REPO_ZIP_URL="" REPO_RELEASE_URL=""
return
fi
# Always use the main remote repository
local raw
raw=$(git remote get-url "$(git remote | head -n 1)" 2>/dev/null)
if [ -z "$raw" ]; then
_r "No git remote found — repo buttons will be disabled"
REPO_URL="" REPO_PLATFORM="" REPO_ZIP_URL="" REPO_RELEASE_URL=""
return
fi
# Normalize SSH → HTTPS
local https_url
if [[ "$raw" =~ ^git@ ]]; then
https_url=$(echo "$raw" | sed 's|git@\(.*\):\(.*\)\.git|https://\1/\2|; s|git@\(.*\):\(.*\)|https://\1/\2|')
else
https_url="${raw%.git}"
fi
# Extract base URL (scheme + host) and slug (user/repo)
local base_url slug
base_url=$(echo "$https_url" | grep -oP 'https?://[^/]+')
slug=$(echo "$https_url" | sed "s|${base_url}/||")
# Detect platform — known hosts first, then Gitea/Forgejo probe
if [[ "$https_url" =~ github\.com ]]; then
REPO_PLATFORM="github"
REPO_URL="$https_url"
REPO_ZIP_URL="${https_url}/archive/refs/heads/main.zip"
REPO_RELEASE_URL="${https_url}/releases/latest"
elif [[ "$https_url" =~ gitlab\.com ]]; then
REPO_PLATFORM="gitlab"
REPO_URL="$https_url"
# GitLab: project path needed for archive URL
local proj_path
proj_path=$(echo "$https_url" | sed 's|https://gitlab\.com/||')
REPO_ZIP_URL="https://gitlab.com/${proj_path}/-/archive/main/${proj_path##*/}-main.zip"
REPO_RELEASE_URL="${https_url}/-/releases"
else
# Gitea/Forgejo: use FORGE_URL override or auto-detect via API probe
local forge_base="${FORGE_URL:-}"
if [ -n "$forge_base" ]; then
_b "FORGE_URL override: $forge_base"
REPO_PLATFORM="gitea"
elif _is_gitea "$base_url"; then
_g "Gitea/Forgejo detected at: $base_url"
forge_base="$base_url"
REPO_PLATFORM="gitea"
else
_y "Unknown platform — only source link will be shown"
REPO_PLATFORM="other"
REPO_URL="$https_url"
REPO_ZIP_URL=""
REPO_RELEASE_URL=""
_check_release
return
fi
REPO_URL="$https_url"
REPO_ZIP_URL="${forge_base}/${slug}/archive/main.zip"
REPO_RELEASE_URL="${https_url}/releases/latest"
fi
_g "Repo detected [$REPO_PLATFORM]: $REPO_URL"
# Check if any release exists via API
_check_release
}
# Query platform API to detect if at least one release exists
_check_release() {
REPO_HAS_RELEASE=0
[ -z "$REPO_URL" ] && return
local base_url slug http_code body
base_url=$(echo "$REPO_URL" | grep -oP 'https?://[^/]+')
slug=$(echo "$REPO_URL" | sed "s|${base_url}/||")
case "$REPO_PLATFORM" in
github)
http_code=$(curl -sf -o /dev/null -w "%{http_code}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${slug}/releases/latest" 2>/dev/null)
# 200 = release exists, 404 = no releases
[ "$http_code" = "200" ] && REPO_HAS_RELEASE=1
;;
gitlab)
local encoded_slug
encoded_slug=$(echo "$slug" | sed 's|/|%2F|g')
body=$(curl -sf \
"https://gitlab.com/api/v4/projects/${encoded_slug}/releases?per_page=1" 2>/dev/null)
[ -n "$body" ] && [ "$body" != "[]" ] && REPO_HAS_RELEASE=1
;;
gitea)
# Gitea/Forgejo: GET /api/v1/repos/{owner}/{repo}/releases?limit=1
body=$(curl -sf --max-time 5 \
"${base_url}/api/v1/repos/${slug}/releases?limit=1" 2>/dev/null)
[ -n "$body" ] && [ "$body" != "[]" ] && [ "$body" != "null" ] && REPO_HAS_RELEASE=1
;;
*)
REPO_HAS_RELEASE=0
;;
esac
if [ "$REPO_HAS_RELEASE" = "1" ]; then
_g "Release found"
else
_y "No release found — release button will be shown as strikethrough"
fi
}
add_validation() {
echo '
<p>
<a href="https://jigsaw.w3.org/css-validator/check/referer">
<img style="border:0;width:88px;height:31px"
src="https://jigsaw.w3.org/css-validator/images/vcss-blue"
alt="Ověřit CSS!" />
</a>
</p>
' >> "$output"
}
# Build nav snippet for a given page with active class
# Injects: oSoWoSo | Home | [Download Release] [Download ZIP] [Source] | other pages...
_build_nav() {
local current="$1"
local nav_file="$2"
echo '<nav><ul>' > "$nav_file"
# oSoWoSo button — always first
echo ' <li><a href="https://osowoso.org">oSoWoSo</a></li>' >> "$nav_file"
# Home button
local home_active=""
[ "$current" = "index" ] && home_active=' class="active"'
echo " <li><a href=\"index.html\"${home_active}>Home</a></li>" >> "$nav_file"
# Repo buttons (only when URLs are available)
if [ -n "$REPO_RELEASE_URL" ]; then
if [ "${REPO_HAS_RELEASE:-0}" = "1" ]; then
echo " <li><a href=\"${REPO_RELEASE_URL}\">⏬ release</a></li>" >> "$nav_file"
else
echo " <li><a class=\"no-release\" href=\"${REPO_RELEASE_URL}\">⏬ release</a></li>" >> "$nav_file"
fi
fi
if [ -n "$REPO_ZIP_URL" ]; then
echo " <li><a href=\"${REPO_ZIP_URL}\">📦repo zip</a></li>" >> "$nav_file"
fi
if [ -n "$REPO_URL" ]; then
echo " <li><a href=\"${REPO_URL}\">🔗git</a></li>" >> "$nav_file"
fi
echo " <li>⏭️</a></li>" >> "$nav_file"
# Other .md pages
for md in *.md; do
[ -f "$md" ] || continue
[ "$md" = "README.md" ] && continue
local name=$(echo "${md%.md}" | sed 's/_/ /g')
local active=""
[ "$name" = "$current" ] && active=' class="active"'
echo " <li><a href=\"${name}.html\"${active}>${name}</a></li>" >> "$nav_file"
done
echo '</ul></nav>' >> "$nav_file"
}
# Target: build
target_build() { ## Builds the website, optional: title argument
_detect_repo
if [ -z "$1" ]; then
_r "No website title provided
using generated one!"
title=$(basename "$(pwd)" | sed 's/_/ /g')
else
title="$1"
fi
_g "Creating: $title"
if [ -f docs/CNAME ]; then
cp docs/CNAME src/
fi
rm -rf docs && mkdir -p docs
cp -r src/* docs/
rm -f docs/toggle.html docs/toc.html docs/svg-color.html
if [ "$count" = 1 ]; then
for file in *.md; do
[ -f "$file" ] || continue
local nav_file="docs/_nav.html"
_build_nav "index" "$nav_file"
echo "Processing: $file → index.html"
# single file
pandoc "$file" -f gfm -s \
--css=style.css \
--toc \
--toc-depth=3 \
--include-before-body=src/toggle.html \
--include-before-body=src/toc.html \
--include-before-body="$nav_file" \
--include-after-body=src/svg-color.html \
--metadata title="$title" \
-o docs/index.html
rm -f "$nav_file"
done
elif [ "$count" -gt 1 ]; then
for file in *.md; do
[ -f "$file" ] || continue
if [ "$file" = "README.md" ]; then
output="docs/index.html"
meta_title="$title"
current="index"
else
output="docs/${file%.md}.html"
meta_title=$(echo "${file%.md}" | sed 's/_/ /g')
current="${file%.md}"
fi
local nav_file="docs/_nav_${current}.html"
_build_nav "$current" "$nav_file"
echo "Processing: $file$output"
# multi file
pandoc "$file" -f gfm -s \
--css=style.css \
--toc \
--toc-depth=3 \
--include-before-body=src/toggle.html \
--include-before-body=src/toc.html \
--include-before-body="$nav_file" \
--include-after-body=src/svg-color.html \
--metadata title="$meta_title" \
-o "$output"
rm -f "$nav_file"
done
else
_re "ERROR: No .md files found!"
fi
_g "Finished"
}
# Target: help
target_help() { ## Show this help message
_b 'OSOWOSO Website Build System'
echo ""
echo "Usage: $0 [target]"
echo ""
echo "Available targets:"
echo ""
awk 'BEGIN {FS = "## "} /^target_[a-zA-Z_-]+\(\).*## / {
sub(/target_/, "", $1)
sub(/\(\) \{/, "", $1)
gsub(/^[ \t]+/, "", $1)
printf " \033[36m%-15s\033[0m %s\n", $1, $2
}' "$0" | sort || _re 'ERROR: No functions found!
Create function you want use for arguments and help as:
target_"Name of argument aka function"() { ## Your usage message here.'
echo ""
}
# Target: serve
target_serve() { ## Serve site locally out of docs folder
if [ ! -f docs/index.html ]; then
_r "Site not built!"
ask 'Build first?' && target_build "$@" || _re "Exiting"
fi
_g 'Press Ctrl+C to stop'
python3 -m http.server -d docs 8000
}
# Main script logic
main() {
local target="${1:-help}"
target="${target#target_}"
local func_name="target_${target//-/_}"
if declare -f "$func_name" > /dev/null; then
shift
"$func_name" "$@"
else
#shellcheck disable=SC2016,2086
_re 'Unknown target: $target. Run '$0 help' for available targets.'
fi
}
# Run main with all arguments
main "$@"