#!/usr/bin/env bash #shellcheck disable=SC2089 # # Make website from .md files # # Author: zenobit # Date: February 14, 2026 # License: MIT # BUILD_VERSION='0.0.5' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SRC_DIR="${SRC_DIR:-website-source}" OUT_DIR="${OUT_DIR:-website}" PUBLIC_DIR="${PUBLIC_DIR:-public}" # Source shared functions # shellcheck source=lib.sh source "${SCRIPT_DIR}/lib.sh" # Load .env — project dir takes priority check_variables() { if [ -z "$TITLE" ]; then return 1 fi } if [ -f ".env" ]; then source ".env" check_variables && echo 'Using .env' elif [ -f "${SRC_DIR}/.env" ]; then source "${SRC_DIR}/.env" check_variables && echo 'Using ${SRC_DIR}/.env' elif [ -f "${SCRIPT_DIR}/.env" ]; then source "${SCRIPT_DIR}/.env" check_variables && echo 'Using .env' fi # --------------------------------------------------------------------------- # Repo detection # --------------------------------------------------------------------------- _is_gitea() { local base_url="$1" local response response=$(curl -sfL --max-time 5 "${base_url}/api/v1/meta" 2>/dev/null) [[ "$response" =~ \"version\" ]] && return 0 return 1 } _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 # Prefer SSH remote over HTTPS if available local raw ssh_remote https_remote while IFS= read -r remote; do local url url=$(git remote get-url "$remote" 2>/dev/null) if [[ "$url" =~ ^git@ || "$url" =~ ^ssh:// ]]; then ssh_remote="$url" break else https_remote="${https_remote:-$url}" fi done < <(git remote) raw="${ssh_remote:-$https_remote}" 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|') elif [[ "$raw" =~ ^ssh:// ]]; then https_url=$(echo "$raw" | sed 's|ssh://git@\(.*\)|https://\1|') else https_url="${raw%.git}" fi local base_url slug base_url=$(echo "$https_url" | sed 's|\(https\?://[^/]*\).*|\1|') slug="${https_url#"${base_url}"/}" 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" local proj_path proj_path="${https_url#https://gitlab.com/}" REPO_ZIP_URL="https://gitlab.com/${proj_path}/-/archive/main/${proj_path##*/}-main.zip" REPO_RELEASE_URL="${https_url}/-/releases" elif [[ "$https_url" =~ codeberg\.org ]]; then REPO_PLATFORM="gitea" REPO_URL="$https_url" REPO_ZIP_URL="${https_url}/archive/main.zip" REPO_RELEASE_URL="${https_url}/releases/latest" printf "%-12s %s\n" "Forgejo:" "$REPO_URL" _check_release return else 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 printf "%-12s %s\n" "Gitea:" "$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 printf "%-12s %s\n" "${REPO_PLATFORM}:" "$slug" _check_release } _check_release() { REPO_HAS_RELEASE=0 [ -z "$REPO_URL" ] && return local base_url slug http_code body base_url=$(echo "$REPO_URL" | sed 's|\(https\?://[^/]*\).*|\1|') slug="${REPO_URL#"${base_url}"/}" case "$REPO_PLATFORM" in github) http_code=$(curl -sf -o /dev/null -w "%{http_code}" \ -H "Accept: application/vnd.github+json" \ ${GITHUB_TOKEN:+-H "Authorization: Bearer ${GITHUB_TOKEN}"} \ "https://api.github.com/repos/${slug}/releases/latest" 2>/dev/null) [ "$http_code" = "200" ] && REPO_HAS_RELEASE=1 ;; gitlab) local encoded_slug encoded_slug="${slug//\//%2F}" 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) 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 printf "%-12s %s\n" "Release:" "found" else printf "%-12s ${RED}%s${NC}\n" "Release:" "No" fi } # --------------------------------------------------------------------------- # HTML builders # --------------------------------------------------------------------------- add_validation() { echo '

Valid CSS!

' >> "$output" } # Build main nav for root pages # Includes links to subdirs defined in SUBDIRS _build_nav() { local current="$1" local nav_file="$2" echo '' >> "$nav_file" } _build_goatcounter_snippet() { local url="$1" url=$(echo "$url" | rev | cut -d'/' -f1 | rev) local file="${OUT_DIR}/_goatcounter.html" cat > "$file" < EOF echo "$file" } _build_giscus_snippet() { local repo="$1" repo_id="$2" category="$3" category_id="$4" local file="${OUT_DIR}/_giscus.html" cat > "$file" < EOF echo "$file" } # Recursively build all .md files in a subdir _build_subdir() { local subdir="$1" local title="$2" shift 2 local extra_after=("$@") local out_dir="${OUT_DIR}/${subdir}" mkdir -p "$out_dir" # Copy src assets into subdir output cp ${SRC_DIR}/style.css "$out_dir/" 2>/dev/null for md in "${subdir}"/*.md; do [ -f "$md" ] || continue local file slug output meta_title nav_file file="${md##*/}" slug="${file%.md}" output="${out_dir}/${slug}.html" meta_title="${slug//_/ }" nav_file="${out_dir}/_nav_${slug}.html" _build_subnav "$slug" "$subdir" "$nav_file" printf "${YELLOW}%-12s${NC} %s\n" "processing:" "$md → $output" pandoc "$md" -f gfm -s \ --css=style.css \ --toc \ --toc-depth=3 \ --include-before-body=${SRC_DIR}/toggle.html \ --include-before-body=${SRC_DIR}/toc.html \ --include-before-body="$nav_file" \ --include-after-body=${SRC_DIR}/svg-color.html \ "${extra_after[@]}" \ --metadata title="$meta_title" \ -o "$output" rm -f "$nav_file" done # Recurse into nested subdirs for d in "${subdir}"/*/; do [ -d "$d" ] || continue _build_subdir "${d%/}" "$title" "${extra_after[@]}" done } # --------------------------------------------------------------------------- # Argument definitions # format: "short|long|value|description|action" # action: variable name to store value in, or "target" to set target # --------------------------------------------------------------------------- ARGS=( "h|help||Show this help message|target" "b|build||Build the website|target" "s|serve||Serve site locally|target" "t|title||Set website title|title" "g|goatcounter|<url>|GoatCounter analytics URL|goatcounter_url" "r|giscus-repo|<repo>|Giscus repo (user/repo)|giscus_repo" "i|giscus-repo-id|<id>|Giscus repo ID|giscus_repo_id" "c|giscus-category|<n>|Giscus category name|giscus_category" "C|giscus-cat-id|<id>|Giscus category ID|giscus_category_id" "d|subdir|<dir>|Add subdir to build|subdir_arg" ) _build_optstring() { local optstring="" for entry in "${ARGS[@]}"; do local short value short=$(echo "$entry" | cut -d'|' -f1) value=$(echo "$entry" | cut -d'|' -f3) optstring+="$short" [ -n "$value" ] && optstring+=":" done echo "$optstring" } # --------------------------------------------------------------------------- # Distro card generators # --------------------------------------------------------------------------- _icon_color() { local name="$1" local -a colors=('#E95420' '#3C3B37' '#A80030' '#41B549' '#87CF3E' '#2F2F2F' '#178CDA' '#1d6fa4') local code printf -v code '%d' "'${name:0:1}" 2>/dev/null || code=63 echo "${colors[$((code % 8))]}" } _icon_html() { local osname="$1" pretty="$2" icon_hint="$3" icon_online="${4:-}" local color initial src color=$(_icon_color "$pretty") initial="${pretty:0:1}" initial="${initial^^}" src="" local icon_base="${icon_hint%.*}" # Priority: local icon_hint → local osname → ICON_ONLINE → colored placeholder if [ -n "$icon_base" ] && [ -f "icons/${icon_base}.svg" ]; then src="icons/${icon_base}.svg" elif [ -n "$icon_base" ] && [ -f "icons/${icon_base}.png" ]; then src="icons/${icon_base}.png" elif [ -f "icons/${osname}.svg" ]; then src="icons/${osname}.svg" elif [ -f "icons/${osname}.png" ]; then src="icons/${osname}.png" fi if [ -n "$src" ]; then printf '<img src="%s" alt="%s" class="distro-icon-large" width="128" height="128" loading="lazy" onerror="this.style.display='"'"'none'"'"';this.nextElementSibling.style.display='"'"'flex'"'"'"><div class="distro-icon-fallback-large" style="width:128px;height:128px;background:%s;display:none">%s</div>' \ "$src" "$pretty" "$color" "$initial" elif [ -n "$icon_online" ]; then printf '<img src="%s" alt="%s" class="distro-icon-large" width="128" height="128" loading="lazy" onerror="this.style.display='"'"'none'"'"';this.nextElementSibling.style.display='"'"'flex'"'"'"><div class="distro-icon-fallback-large" style="width:128px;height:128px;background:%s;display:none">%s</div>' \ "$icon_online" "$pretty" "$color" "$initial" else printf '<div class="distro-icon-fallback-large" style="width:128px;height:128px;background:%s">%s</div>' \ "$color" "$initial" fi } _card_html() { local osname="$1" pretty="$2" basedof="$3" description="$4" local homepage="$5" categories="$6" supported="$7" icon_hint="${8}" icon_online="${9:-}" local meta_html="" local osname_safe osname_safe="${osname//\'/\\\'}" if [ "$supported" = "true" ]; then meta_html+='<span class="supported-badge">supported</span>' fi # Show first two DistroWatch category tags if [ -n "$categories" ]; then local IFS_old=$IFS IFS=',' local c i=0 for c in $categories; do c="${c# }" c="${c% }" [ -z "$c" ] && continue [ "$c" = "other" ] && continue meta_html+="<span class=\"category-badge\">${c}</span>" (( i++ )); [ "$i" -ge 2 ] && break done IFS=$IFS_old fi local basedof_html="" if [ -n "$basedof" ] && [ "$basedof" != "-" ]; then local basedof_links="" basedof_word for basedof_word in $basedof; do [ -n "$basedof_links" ] && basedof_links+=" " basedof_links+="<a href=\"#\" onclick=\"setSearch('${basedof_word}');return false;\">${basedof_word}</a>" done basedof_html=" <span class=\"basedof-inline\">Based on: ${basedof_links}</span>" fi local meta_row="" [ -n "$meta_html" ] && meta_row="<div class=\"distro-meta\">${meta_html}</div>" local inline_icon="" icon_hint_base="${icon_hint%.*}" if [ -n "$icon_hint_base" ] && [ -f "icons/${icon_hint_base}.svg" ]; then inline_icon="<img src=\"icons/${icon_hint_base}.svg\" alt=\"\" class=\"card-title-icon\" aria-hidden=\"true\" loading=\"lazy\">" elif [ -n "$icon_hint_base" ] && [ -f "icons/${icon_hint_base}.png" ]; then inline_icon="<img src=\"icons/${icon_hint_base}.png\" alt=\"\" class=\"card-title-icon\" aria-hidden=\"true\" loading=\"lazy\">" elif [ -f "icons/${osname}.svg" ]; then inline_icon="<img src=\"icons/${osname}.svg\" alt=\"\" class=\"card-title-icon\" aria-hidden=\"true\" loading=\"lazy\">" elif [ -f "icons/${osname}.png" ]; then inline_icon="<img src=\"icons/${osname}.png\" alt=\"\" class=\"card-title-icon\" aria-hidden=\"true\" loading=\"lazy\">" elif [ -n "$icon_online" ]; then inline_icon="<img src=\"${icon_online}\" alt=\"\" class=\"card-title-icon\" aria-hidden=\"true\" loading=\"lazy\" onerror=\"this.style.display='none'\">" fi cat <<EOF <div class="distro-card" style="cursor:pointer" onclick="if(event.target.closest('a'))return;showDownloads('${osname_safe}')" data-categories="${categories}" data-supported="${supported}" data-osname="${osname}" data-basedof="${basedof}" data-pretty="${pretty,,}"> <div class="distro-card-icon-area"> $(_icon_html "$osname" "$pretty" "$icon_hint" "$icon_online") </div> <div class="distro-card-body"> <h3>${inline_icon}<a href="${homepage}" target="_blank" rel="noopener">${pretty}</a>${basedof_html}</h3> <hr class="distro-divider"> ${meta_row} <p class="distro-description">${description}</p> <div class="distro-actions"> <button class="download-btn"><img src="icons/floppy.svg" alt="Download"> Direct Download</button> </div> </div> </div> EOF } _read_pub_vars() { local file="$1" local _line _key _val while IFS= read -r _line; do _key="${_line%%=*}" _val="${_line#*=}" _val="${_val#\"}"; _val="${_val%\"}" case "$_key" in OSNAME|PRETTY|ICON|ICON_ONLINE|CATEGORY|BASEDOF|DESCRIPTION|HOMEPAGE|\ RELEASES|EDITIONS|QEMU_ARCH|MAGNET|CHAT|RSS|DW|\ DW_ARCHITECTURE|DW_DESKTOP|DW_CATEGORY|DW_URL) printf -v "$_key" '%s' "$_val" ;; esac done < <(grep -E '^(OSNAME|PRETTY|ICON|ICON_ONLINE|CATEGORY|BASEDOF|DESCRIPTION|HOMEPAGE|RELEASES|EDITIONS|QEMU_ARCH|MAGNET|CHAT|RSS|DW|DW_ARCHITECTURE|DW_DESKTOP|DW_CATEGORY|DW_URL)=' "$file") } _generate_distro_cards() { local supported="true" for pub_file in ${PUBLIC_DIR}/*; do [ -f "$pub_file" ] || continue local OSNAME="" PRETTY="" ICON="" ICON_ONLINE="" CATEGORY="" BASEDOF="" DESCRIPTION="" HOMEPAGE="" _read_pub_vars "$pub_file" [ -z "$OSNAME" ] && continue _card_html "$OSNAME" "$PRETTY" "$BASEDOF" "$DESCRIPTION" "$HOMEPAGE" "$CATEGORY" "$supported" "$ICON" "$ICON_ONLINE" done } _generate_distro_data_js() { echo 'window._distroData = {' local first=1 for pub_file in ${PUBLIC_DIR}/*; do [ -f "$pub_file" ] || continue local OSNAME="" PRETTY="" ICON="" ICON_ONLINE="" CATEGORY="" BASEDOF="" DESCRIPTION="" HOMEPAGE="" RELEASES="" EDITIONS="" QEMU_ARCH="" MAGNET="" CHAT="" RSS="" DW="" DW_DESKTOP="" _read_pub_vars "$pub_file" [ -z "$OSNAME" ] && continue local hp_esc releases_esc basedof_esc editions_esc archs_esc icon_online_esc magnet_esc chat_esc rss_esc dw_esc desc_esc cat_esc dw_desktop_esc hp_esc="${HOMEPAGE//\"/\\\"}" releases_esc="${RELEASES//\"/\\\"}" basedof_esc="${BASEDOF//\"/\\\"}" editions_esc="${EDITIONS//\"/\\\"}" archs_esc="${QEMU_ARCH//\"/\\\"}" icon_online_esc="${ICON_ONLINE//\"/\\\"}" magnet_esc="${MAGNET//\"/\\\"}" chat_esc="${CHAT//\"/\\\"}" rss_esc="${RSS//\"/\\\"}" dw_esc="${DW//\"/\\\"}" desc_esc="${DESCRIPTION//\"/\\\"}" cat_esc="${CATEGORY//\"/\\\"}" dw_desktop_esc="${DW_DESKTOP//\"/\\\"}" # Build URLs map from public/tmp_${OSNAME}.dat (Arch TAB Release TAB Edition TAB URL TAB Status) local tmp_entries="" declare -A _url_seen=() local _dat_file="${PUBLIC_DIR}/tmp_${OSNAME}.dat" while IFS=$'\t' read -r _arch _rel _ed _url _st; do [[ "$_st" == "OK" || "$_st" == "PASS" ]] || continue [[ "$_url" =~ ^https?:// ]] || continue [ "$_ed" = "-" ] && _ed="" local key="$_arch" [ -n "$_rel" ] && key+=" $_rel" [ -n "$_ed" ] && key+=" $_ed" [[ "${_url_seen[$key]+_}" ]] && continue _url_seen[$key]=1 key="${key//\"/\\\"}" _url="${_url//\"/\\\"}" [ -n "$tmp_entries" ] && tmp_entries+="," tmp_entries+="\"${key}\":\"${_url}\"" done < <(cat "${_dat_file}" 2>/dev/null) unset _url_seen local urls_js="" [ -n "$tmp_entries" ] && urls_js=",\"urls\":{${tmp_entries}}" [ "$first" = "0" ] && echo ',' first=0 local extra_fields="" [ -n "$desc_esc" ] && extra_fields+=",\"description\":\"${desc_esc}\"" [ -n "$cat_esc" ] && extra_fields+=",\"category\":\"${cat_esc}\"" [ -n "$dw_desktop_esc" ] && extra_fields+=",\"dw_desktop\":\"${dw_desktop_esc}\"" [ -n "$magnet_esc" ] && extra_fields+=",\"magnet\":\"${magnet_esc}\"" [ -n "$chat_esc" ] && extra_fields+=",\"chat\":\"${chat_esc}\"" [ -n "$rss_esc" ] && extra_fields+=",\"rss\":\"${rss_esc}\"" [ -n "$dw_esc" ] && extra_fields+=",\"dw\":\"${dw_esc}\"" printf ' "%s": {"homepage":"%s","releases":"%s","editions":"%s","basedof":"%s","architectures":"%s","icon_online":"%s"%s%s}' \ "$OSNAME" "$hp_esc" "$releases_esc" "${editions_esc:-}" "$basedof_esc" "$archs_esc" "$icon_online_esc" "$extra_fields" "$urls_js" done echo '' echo '};' # _dwData: DW metadata for ALL distros that have a TODO/distrowatch/ file echo 'window._dwData = {' first=1 for dw_file in TODO/distrowatch/*; do [ -f "$dw_file" ] || continue local OSNAME="" PRETTY="" BASEDOF="" HOMEPAGE="" DESCRIPTION="" local DW_ARCHITECTURE="" DW_DESKTOP="" DW_CATEGORY="" DW_URL="" _read_pub_vars "$dw_file" [ -z "$OSNAME" ] && continue local hp_esc basedof_esc desc_esc arch_esc desktop_esc cat_esc url_esc hp_esc="${HOMEPAGE//\"/\\\"}" basedof_esc="${BASEDOF//\"/\\\"}" desc_esc="${DESCRIPTION//\"/\\\"}" arch_esc="${DW_ARCHITECTURE//\"/\\\"}" desktop_esc="${DW_DESKTOP//\"/\\\"}" cat_esc="${DW_CATEGORY//\"/\\\"}" url_esc="${DW_URL:-"https://distrowatch.com/${OSNAME}"}" url_esc="${url_esc//\"/\\\"}" [ "$first" = "0" ] && echo ',' first=0 printf ' "%s": {"pretty":"%s","homepage":"%s","basedof":"%s","description":"%s","dw_arch":"%s","dw_desktop":"%s","dw_category":"%s","dw_url":"%s"}' \ "$OSNAME" "${PRETTY//\"/\\\"}" "$hp_esc" "$basedof_esc" "$desc_esc" \ "$arch_esc" "$desktop_esc" "$cat_esc" "$url_esc" done echo '' echo '};' } _generate_distros_data_js_file() { local out="${OUT_DIR}/distros-data.js" echo 'window.distrosData = [' > "$out" local first=1 # Unsupported distros — from TODO/distrowatch/ (skip those already in ${PUBLIC_DIR}/) for dw_file in TODO/distrowatch/*; do [ -f "$dw_file" ] || continue local OSNAME="" PRETTY="" BASEDOF="" HOMEPAGE="" DESCRIPTION="" local DW_ARCHITECTURE="" DW_DESKTOP="" DW_CATEGORY="" DW_URL="" _read_pub_vars "$dw_file" [ -z "$OSNAME" ] && continue [ -f "${PUBLIC_DIR}/${OSNAME}" ] && continue # skip supported [ "$first" = "0" ] && echo ',' >> "$out" first=0 local hp_esc basedof_esc desc_esc cat_esc arch_esc desktop_esc url_esc hp_esc="${HOMEPAGE//\"/\\\"}" basedof_esc="${BASEDOF//\"/\\\"}" desc_esc="${DESCRIPTION//\"/\\\"}" cat_esc="${DW_CATEGORY//\"/\\\"}" arch_esc="${DW_ARCHITECTURE//\"/\\\"}" desktop_esc="${DW_DESKTOP//\"/\\\"}" url_esc="${DW_URL:-"https://distrowatch.com/${OSNAME}"}" url_esc="${url_esc//\"/\\\"}" printf ' {"id":"%s","name":"%s","basedof":"%s","description":"%s","homepage":"%s","dw_category":"%s","dw_arch":"%s","dw_desktop":"%s","dw_url":"%s"}' \ "$OSNAME" "${PRETTY//\"/\\\"}" "$basedof_esc" "$desc_esc" "$hp_esc" \ "$cat_esc" "$arch_esc" "$desktop_esc" "$url_esc" >> "$out" done echo '' >> "$out" echo '];' >> "$out" printf "${GREEN}%-12s${NC} %s\n" "generated:" "$out" } # Generate list of available icons at build time _generate_icons_list_js() { local out="${OUT_DIR}/icons-available.js" echo 'window.iconsAvailable = {' > "$out" local first=1 # Collect all icon files from icons/ directory; deduplicate by basename. declare -A _icon_seen=() for icon_file in icons/*; do [ -f "$icon_file" ] || continue local basename basename="${icon_file##*/}" local name="${basename%.*}" [ "${_icon_seen[$name]+x}" ] && continue _icon_seen[$name]=1 local has_svg=0 has_png=0 [ -f "icons/${name}.svg" ] && has_svg=1 [ -f "icons/${name}.png" ] && has_png=1 [ "$first" = "0" ] && echo ',' >> "$out" first=0 if [ "$has_svg" = 1 ] && [ "$has_png" = 1 ]; then printf ' "%s": {"svg":true,"png":true}' "$name" >> "$out" elif [ "$has_svg" = 1 ]; then printf ' "%s": {"svg":true}' "$name" >> "$out" else printf ' "%s": {"png":true}' "$name" >> "$out" fi done echo '};' >> "$out" printf "${GREEN}%-12s${NC} %s\n" "generated:" "$out" } _generate_distros_page() { local type="$1" local giscus_repo="$2" local giscus_repo_id="$3" local giscus_category="$4" local giscus_category_id="$5" local title h1 desc support_filter_html if [ "$type" = "distros" ]; then title="Distros — DistroHopper" h1="Supported Distributions" desc="All distributions directly supported by DistroHopper." support_filter_html='' else title="All Distributions — DistroHopper" h1="All Distributions" desc='All distributions (from DistroWatch). Supported ones <span class="supported-badge">supported</span> show version & edition details.' support_filter_html=' <button class="filter-btn" data-filter-support="supported">Supported</button> <button class="filter-btn" data-filter-support="unsupported">Unsupported</button>' fi local nav_file="${OUT_DIR}/_nav_${type}.html" _build_nav "$type" "$nav_file" local nav_html nav_html=$(cat "$nav_file") rm -f "$nav_file" local toggle_html svg_color_html particles_init_html floppy_inline toggle_html=$(cat ${SRC_DIR}/toggle.html) svg_color_html=$(cat ${SRC_DIR}/svg-color.html) particles_init_html=$(cat ${SRC_DIR}/particles-config.html) floppy_inline=$(sed 's/ width="[0-9]*"//; s/ height="[0-9]*"//' icons/floppy.svg | tr -d '\n' | tr -s ' ') local data_js data_js=$(_generate_distro_data_js) local cards cards=$(_generate_distro_cards "$type") local unsupported_section="" if [ "$type" = "all" ]; then unsupported_section='<script src="distros-data.js"></script> <script> (function(){ if (!window.distrosData) return; var grid = document.getElementById("distroGrid"); var colors = ["#E95420","#3C3B37","#A80030","#41B549","#87CF3E","#2F2F2F","#178CDA","#1d6fa4"]; window.distrosData.forEach(function(d) { if (window._distroData && window._distroData[d.id||d.name]) return; var nm = d.name || "?"; var oid = d.id || nm; var bg = colors[nm.charCodeAt(0) % 8]; var cats = d.dw_category || ""; var catBadges = ""; cats.split(",").slice(0,2).forEach(function(c){ c = c.trim(); if (c) catBadges += "<span class=\"category-badge\">"+c+"</span>"; }); var card = document.createElement("div"); card.className = "distro-card"; card.dataset.categories = cats; card.dataset.supported = "false"; card.dataset.osname = oid; card.dataset.basedof = d.basedof || ""; var iconSrc = d.icon_online || ""; var largeIconHtml = "<img src=\""+iconSrc+"\" alt=\""+nm+"\" class=\"distro-icon-large\" width=\"128\" height=\"128\" loading=\"lazy\" onerror=\"this.style.display=\x27none\x27;this.nextElementSibling.style.display=\x27flex\x27\">" + "<div class=\"distro-icon-fallback-large\" style=\"width:128px;height:128px;background:"+bg+";display:none\">"+nm.charAt(0).toUpperCase()+"</div>"; var titleIconHtml = "<img src=\""+iconSrc+"\" alt=\"\" class=\"card-title-icon\" aria-hidden=\"true\" loading=\"lazy\" onerror=\"this.style.display=\x27none\x27\">"; card.style.cursor = "pointer"; card.onclick = function(e) { if (e.target.closest('a')) return; showDownloads(oid); }; card.innerHTML = "<div class=\"distro-card-icon-area\">" + largeIconHtml + "</div>" + "<div class=\"distro-card-body\">" + "<h3>" + titleIconHtml + (d.homepage?"<a href=\""+d.homepage+"\" target=\"_blank\" rel=\"noopener\">"+nm+"</a>":nm)+"</h3>" + "<hr class=\"distro-divider\">" + "<div class=\"distro-meta\">"+catBadges+"</div>" + "<p class=\"distro-description\">"+(d.description||"")+"</p>" + "<div class=\"distro-actions\">" + "<button class=\"download-btn\">ℹ Info</button>" + "</div>" + "</div>"; grid.appendChild(card); }); _buildCategoryFilters(); if (typeof filterDistros === "function") filterDistros(); })(); </script>' fi cat > "${OUT_DIR}/${type}.html" <<HTML <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${title}
${toggle_html} ${nav_html}
${support_filter_html}
${cards}
${unsupported_section} ${svg_color_html} ${particles_init_html} HTML printf "${GREEN}%-12s${NC} %s\n" "generated:" "${OUT_DIR}/${type}.html" } # --------------------------------------------------------------------------- # Targets # --------------------------------------------------------------------------- target_build() { ## Builds the website local title="$1" local goatcounter_url="$2" local giscus_repo="$3" local giscus_repo_id="$4" local giscus_category="$5" local giscus_category_id="$6" local subdir_arg="$7" local count cname count=$(find ./ -maxdepth 1 -name '*.md' | wc -l) _detect_repo if [ -z "$title" ]; then _r "No website title provided — using generated one!" title=$(basename "$(pwd)" | sed 's/_/ /g') fi _g "Creating: $title" [ -f ${OUT_DIR}/CNAME ] && cp ${OUT_DIR}/CNAME ${SRC_DIR}/ rm -rf "${OUT_DIR}" && mkdir -p "${OUT_DIR}" cp -r ${SRC_DIR}/* ${OUT_DIR}/ rm -f ${OUT_DIR}/toggle.html ${OUT_DIR}/toc.html ${OUT_DIR}/svg-color.html # Inject dynamic nav into rosette.html if [ -f "${OUT_DIR}/rosette.html" ]; then local rosette_nav="${OUT_DIR}/_nav_rosette.html" _build_nav "rosette" "$rosette_nav" python3 -c " import sys with open('${OUT_DIR}/rosette.html') as f: content = f.read() with open('$rosette_nav') as f: nav = f.read() content = content.replace('', nav) with open('${OUT_DIR}/rosette.html', 'w') as f: f.write(content) " rm -f "$rosette_nav" fi cname=$(cat ${OUT_DIR}/CNAME 2>/dev/null) [ -n "$cname" ] && printf "%-12s ${GREEN}%s${NC}\n" "CNAME:" "$cname" local extra_after=() if [ -n "$goatcounter_url" ]; then echo "GoatCounter: $goatcounter_url" extra_after+=("--include-after-body=$(_build_goatcounter_snippet "$goatcounter_url")") fi if [ -n "$giscus_repo" ]; then echo "Giscus: $giscus_repo" extra_after+=("--include-after-body=$(_build_giscus_snippet \ "$giscus_repo" "$giscus_repo_id" "$giscus_category" "$giscus_category_id")") fi # Build root pages if [ "$count" = 1 ]; then for file in *.md; do [ -f "$file" ] || continue local nav_file="${OUT_DIR}/_nav.html" _build_nav "index" "$nav_file" printf "${YELLOW}%-12s${NC} %s\n" "processing:" "$file → index.html" pandoc "$file" -f gfm -s \ --css=style.css \ --toc \ --toc-depth=3 \ --include-before-body=${SRC_DIR}/particles-loader.html \ --include-before-body=${SRC_DIR}/toggle.html \ --include-before-body=${SRC_DIR}/toc.html \ --include-before-body="$nav_file" \ --include-after-body=${SRC_DIR}/svg-color.html \ --include-after-body=${SRC_DIR}/particles-config.html \ "${extra_after[@]}" \ --metadata title="$title" \ -o ${OUT_DIR}/index.html rm -f "$nav_file" done elif [ "$count" -gt 1 ]; then for file in *.md; do [ -f "$file" ] || continue [ "$file" = "test.md" ] && continue local output meta_title current if [ "$file" = "README.md" ]; then output="${OUT_DIR}/index.html" meta_title="$title" current="index" else output="${OUT_DIR}/${file%.md}.html" meta_title="${file%.md}" meta_title="${meta_title//_/ }" current="${file%.md}" fi local nav_file="${OUT_DIR}/_nav_${current}.html" _build_nav "$current" "$nav_file" printf "${YELLOW}%-12s${NC} %s\n" "processing:" "$file → $output" pandoc "$file" -f gfm -s \ --css=style.css \ --toc \ --toc-depth=3 \ --include-before-body=${SRC_DIR}/particles-loader.html \ --include-before-body=${SRC_DIR}/toggle.html \ --include-before-body=${SRC_DIR}/toc.html \ --include-before-body="$nav_file" \ --include-after-body=${SRC_DIR}/svg-color.html \ --include-after-body=${SRC_DIR}/particles-config.html \ "${extra_after[@]}" \ --metadata title="$meta_title" \ -o "$output" rm -f "$nav_file" done else _re "ERROR: No .md files found!" fi # Build subdirs — from .env SUBDIRS array + --subdir argument local all_subdirs=("${SUBDIRS[@]:-}") [ -n "$subdir_arg" ] && all_subdirs+=("$subdir_arg") for subdir in "${all_subdirs[@]:-}"; do [ -d "$subdir" ] || { _y "Subdir not found: $subdir"; continue; } _g "Building subdir: $subdir" _build_subdir "$subdir" "$title" "${extra_after[@]}" done # Copy icons cp -r icons ${OUT_DIR}/ # Copy rosette data if [ -d "data/rosette" ]; then mkdir -p ${OUT_DIR}/data/rosette cp -r data/rosette/* ${OUT_DIR}/data/rosette/ _g "Copied rosette data" fi # Generate icons-available.js (list of icons present at build time) _generate_icons_list_js # Generate distros-data.js from ${PUBLIC_DIR}/ _generate_distros_data_js_file # Generate distros pages _generate_distros_page distros "$giscus_repo" "$giscus_repo_id" "$giscus_category" "$giscus_category_id" _generate_distros_page all "$giscus_repo" "$giscus_repo_id" "$giscus_category" "$giscus_category_id" rm -f ${OUT_DIR}/_goatcounter.html ${OUT_DIR}/_giscus.html } errorMessage='ERROR: No functions found! Create a target function with the format: target_name() { ## Description here' target_help() { ## Show this help message _b "Website Build System v$BUILD_VERSION by oSoWoSo" echo -e "\nUsage: $0 [target] [arguments]\n\nAvailable targets:\n" 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 "$errorMessage" echo -e "\nAvailable arguments:\n" for entry in "${ARGS[@]}"; do local short long value desc short=$(echo "$entry" | cut -d'|' -f1) long=$(echo "$entry" | cut -d'|' -f2) value=$(echo "$entry" | cut -d'|' -f3) desc=$(echo "$entry" | cut -d'|' -f4) printf " \033[33m%-32s\033[0m %s\n" "-${short}, --${long} ${value}" "$desc" done echo -e "\nScript will use .env file if found\n" } target_serve() { ## Serve site locally out of website folder local title="$1" local goatcounter_url="$2" local giscus_repo="$3" local giscus_repo_id="$4" local giscus_category="$5" local giscus_category_id="$6" local subdir_arg="$7" if [ ! -f ${OUT_DIR}/index.html ]; then _r "Site not built!" if _confirm 'Build first?'; then target_build "$title" "$goatcounter_url" \ "$giscus_repo" "$giscus_repo_id" \ "$giscus_category" "$giscus_category_id" \ "$subdir_arg" else _re "Exiting" fi fi _g 'Press Ctrl+C to stop' python3 -m http.server -d "${OUT_DIR}" 8000 } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- main() { # Convert long arguments to short for getopts local args=() while [[ $# -gt 0 ]]; do local matched=0 for entry in "${ARGS[@]}"; do local short long value short=$(echo "$entry" | cut -d'|' -f1) long=$(echo "$entry" | cut -d'|' -f2) value=$(echo "$entry" | cut -d'|' -f3) if [[ "$1" == "--${long}" ]]; then args+=("-${short}") [ -n "$value" ] && { args+=("$2"); shift; } matched=1 break fi done [ "$matched" = 0 ] && args+=("$1") shift done set -- "${args[@]}" # Default values (overridden by .env) local target="help" local title="${TITLE:-}" local goatcounter_url="${GOATCOUNTER_URL:-}" local giscus_repo="${GISCUS_REPO:-}" local giscus_repo_id="${GISCUS_REPO_ID:-}" local giscus_category="${GISCUS_CATEGORY:-}" local giscus_category_id="${GISCUS_CATEGORY_ID:-}" local subdir_arg="" while getopts "$(_build_optstring)" opt; do for entry in "${ARGS[@]}"; do local short long action short=$(echo "$entry" | cut -d'|' -f1) long=$(echo "$entry" | cut -d'|' -f2) action=$(echo "$entry" | cut -d'|' -f5) if [[ "$opt" == "$short" ]]; then if [[ "$action" == "target" ]]; then target="$long" else printf -v "$action" '%s' "$OPTARG" fi break fi done done shift $((OPTIND - 1)) # Positional argument as target (backwards compatibility: ./web-create build) if [[ "$target" == "help" && -n "${1:-}" ]]; then target="${1}" shift fi local func_name="target_${target//-/_}" declare -f "$func_name" > /dev/null || _re "Unknown target: $target. Run $0 --help for available targets." case "$target" in build|serve) "$func_name" "${title:-}" "$goatcounter_url" \ "$giscus_repo" "$giscus_repo_id" \ "$giscus_category" "$giscus_category_id" \ "$subdir_arg" ;; *) "$func_name" "$@" ;; esac } main "$@"