DistroHopper/web-create
2026-05-27 17:00:06 +02:00

1466 lines
51 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
#
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 '
<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="Valid CSS!" />
</a>
</p>
' >> "$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><ul>' > "$nav_file"
if [ -n "${NAV_EXTRA_LINK_URL:-}" ]; then
echo " <li><a href=\"${NAV_EXTRA_LINK_URL}\">${NAV_EXTRA_LINK_TEXT:-${NAV_EXTRA_LINK_URL}}</a></li>" >> "$nav_file"
fi
echo " <li>⏮️</li>" >> "$nav_file"
local home_active=""
[ "$current" = "index" ] && home_active=' class="active"'
echo " <li><a href=\"index.html\"${home_active}>Home</a></li>" >> "$nav_file"
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
[ -n "$REPO_ZIP_URL" ] && echo " <li><a href=\"${REPO_ZIP_URL}\">📦repo zip</a></li>" >> "$nav_file"
[ -n "$REPO_URL" ] && echo " <li><a href=\"${REPO_URL}\">🔗git</a></li>" >> "$nav_file"
echo " <li>⏭️</li>" >> "$nav_file"
# Root .md pages
for md in *.md; do
[ -f "$md" ] || continue
[ "$md" = "README.md" ] && continue
[ "$md" = "test.md" ] && continue
local slug label active
slug="${md%.md}"
label="${slug//_/ }"
active=""
[ "$slug" = "$current" ] && active=' class="active"'
echo " <li><a href=\"${slug}.html\"${active}>${label}</a></li>" >> "$nav_file"
done
# Add static links to Distros, Rosette, and All pages
local distros_active rosette_active all_active
[ "$current" = "distros" ] && distros_active=' class="active"'
[ "$current" = "rosette" ] && rosette_active=' class="active"'
[ "$current" = "all" ] && all_active=' class="active"'
echo " <li><a href=\"distros.html\"${distros_active}>Distros</a></li>" >> "$nav_file"
echo " <li><a href=\"rosette.html\"${rosette_active}>Rosette</a></li>" >> "$nav_file"
echo " <li><a href=\"all.html\"${all_active}>All</a></li>" >> "$nav_file"
echo '</ul></nav>' >> "$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
<script data-goatcounter="https://${url}/count"
async src="//gc.zgo.at/count.js"></script>
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
<script src="https://giscus.app/client.js"
data-repo="${repo}"
data-repo-id="${repo_id}"
data-category="${category}"
data-category-id="${category_id}"
data-mapping="pathname"
data-strict="0"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="bottom"
data-theme="preferred_color_scheme"
data-lang="cs"
crossorigin="anonymous"
async></script>
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|<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 &amp; 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\">&#x2139; 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}</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4/dist/js-yaml.min.js"></script>
<style>
.rosette-section { margin: 1rem 0; border: 1px solid var(--border-color); border-radius: 4px; overflow: hidden; }
.rosette-header { display: flex; justify-content: space-between; align-items: center; padding: 6px 12px; background: var(--bg-light); border-bottom: 1px solid var(--border-color); font-size: 0.85rem; font-weight: bold; }
.rosette-link { font-size: 0.8rem; font-weight: normal; color: var(--link-color); text-decoration: none; }
.rosette-link:hover { text-decoration: underline; }
.rosette-table { width: 100%; border-collapse: collapse; font-family: "Courier New", monospace; font-size: 0.78rem; }
.rosette-table tr { border-bottom: 1px solid var(--border-color); }
.rosette-table tr:last-child { border-bottom: none; }
.rosette-table td { padding: 4px 10px; vertical-align: top; }
.r-action { color: var(--accent-color); white-space: nowrap; width: 70px; }
.r-cmd { color: var(--text-color); }
.r-desc { color: var(--text-color); opacity: 0.55; font-size: 0.72rem; }
</style>
</head>
<body>
<div id="particles-js" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:0;pointer-events:none"></div>
<div style="position:relative;z-index:1">
${toggle_html}
${nav_html}
<main class="container distros-container">
<div class="page-header">
<h1>${h1}</h1>
<p>${desc}</p>
</div>
<div class="distros-filters">
<input type="text" id="distroSearch" class="distros-search" placeholder="Search distributions...">
<div class="filter-buttons">
<button class="filter-btn active" data-filter-support="all">All</button>
${support_filter_html}
</div>
<div class="filter-buttons filter-buttons-categories" id="catFilters"></div>
</div>
<div class="distros-grid" id="distroGrid">
${cards}
</div>
</main>
<footer class="site-footer">
<p>Data for unsupported distributions provided by <a href="https://distrowatch.com" target="_blank" rel="noopener">DistroWatch.com</a>. Page design inspired by <a href="https://github.com/ufuayk" target="_blank" rel="noopener">ufuayk</a>.</p>
</footer>
</div>
<div class="modal-overlay" id="modalOverlay">
<div class="modal-content">
<button class="modal-close" onclick="closeModal()" style="float:right;margin:-0.5rem -0.5rem 0 0">&times;</button>
<div id="modalBody"></div>
</div>
</div>
<script src="icons-available.js"></script>
<script>
${data_js}
var _giscusRepo = '${giscus_repo}';
var _giscusRepoId = '${giscus_repo_id}';
var _giscusCategory = '${giscus_category}';
var _giscusCategoryId = '${giscus_category_id}';
function _dwInfoHtml(dw, showSource) {
if (!dw) return '';
var rows = '';
if (dw.dw_desktop) rows += '<tr><th>Desktop</th><td>'+dw.dw_desktop+'</td></tr>';
if (dw.dw_arch) rows += '<tr><th>Architecture</th><td>'+dw.dw_arch+'</td></tr>';
if (dw.dw_category) rows += '<tr><th>Category</th><td>'+dw.dw_category+'</td></tr>';
if (dw.basedof && dw.basedof !== '-') rows += '<tr><th>Based on</th><td>'+dw.basedof+'</td></tr>';
var html = '';
if (rows) html += '<table class="dw-meta-table">'+rows+'</table>';
if (dw.description) html += '<p class="distro-description">'+dw.description+'</p>';
if (showSource && dw.dw_url) html += '<p class="modal-note">Source: <a href="'+dw.dw_url+'" target="_blank" rel="noopener">DistroWatch.com</a></p>';
return html;
}
function _distroInfoHtml(d) {
if (!d) return '';
var rows = '';
if (d.dw_desktop) rows += '<tr><th>Desktop</th><td>'+d.dw_desktop+'</td></tr>';
if (d.architectures) rows += '<tr><th>Architecture</th><td>'+d.architectures+'</td></tr>';
if (d.category) rows += '<tr><th>Category</th><td>'+d.category+'</td></tr>';
if (d.basedof && d.basedof !== '-') rows += '<tr><th>Based on</th><td>'+d.basedof+'</td></tr>';
var html = '';
if (rows) html += '<table class="dw-meta-table">'+rows+'</table>';
if (d.description) html += '<p class="distro-description">'+d.description+'</p>';
return html;
}
var _rosetteCache = {};
var _rosetteKnown = ['alpine','arch','debian','fedora','opensuse','void','nix','macos','gentoo','slackware'];
var _rosetteMap = {'ubuntu':'debian','nixos':'nix','suse':'opensuse'};
function _getRosetteId(osname, basedof) {
if (_rosetteKnown.indexOf(osname) !== -1) return osname;
if (_rosetteMap[osname]) return _rosetteMap[osname];
if (basedof) {
var parts = basedof.toLowerCase().split(/[\s,]+/);
for (var i = 0; i < parts.length; i++) {
var p = parts[i].trim();
if (_rosetteKnown.indexOf(p) !== -1) return p;
if (_rosetteMap[p]) return _rosetteMap[p];
}
}
return null;
}
async function _fetchRosette(rosetteId) {
if (_rosetteCache[rosetteId] !== undefined) return _rosetteCache[rosetteId];
try {
var txt = await fetch('./data/rosette/' + rosetteId + '.yaml').then(function(r){ return r.text(); });
_rosetteCache[rosetteId] = jsyaml.load(txt);
} catch(e) {
_rosetteCache[rosetteId] = null;
}
return _rosetteCache[rosetteId];
}
function _rosetteEsc(s) {
return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function _rosettePreviewHtml(data, rosetteId) {
var pkg = data && data.commands && data.commands.package;
if (!pkg) return '';
var keyActions = ['install','remove','search','update','list'];
var rows = '';
keyActions.forEach(function(a) {
if (pkg[a]) rows += '<tr><td class="r-action">'+a+'</td><td class="r-cmd">'+_rosetteEsc(pkg[a].cmd)+'</td><td class="r-desc">'+_rosetteEsc(pkg[a].desc)+'</td></tr>';
});
if (!rows) return '';
return '<div class="rosette-section">'
+ '<div class="rosette-header"><span>\u{1F4E6} Package Manager (' + _rosetteEsc(data.name||rosetteId) + ')</span>'
+ '<a href="rosette.html?os='+rosetteId+'" class="rosette-link">full rosette →</a></div>'
+ '<table class="rosette-table">' + rows + '</table>'
+ '</div>';
}
function showDownloads(osname) {
var d = window._distroData && window._distroData[osname];
var dw = window._dwData && window._dwData[osname];
var card = document.querySelector('[data-osname="'+osname+'"]');
var pretty = card ? card.querySelector('h3 a, h3') : null;
var name = pretty ? (pretty.textContent.trim()||osname) : osname;
if (dw && dw.pretty && name === osname) name = dw.pretty;
var colors = ['#E95420','#3C3B37','#A80030','#41B549','#87CF3E','#2F2F2F','#178CDA','#1d6fa4'];
var bg = colors[name.charCodeAt(0) % 8];
var dwBase = 'https://distrowatch.com/images/yvzhuwbpy/';
var iconsAvailable = window.iconsAvailable || {};
// Get icon src based on build-time availability
var iconSrc;
var info = iconsAvailable[osname];
if (info && info.svg) iconSrc = 'icons/'+osname+'.svg';
else if (info && info.png) iconSrc = 'icons/'+osname+'.png';
else iconSrc = dwBase + osname + '.png';
var isDwIcon = iconSrc.indexOf(dwBase) === 0;
var iconHtml = isDwIcon
? '<img class="modal-distro-icon" src="'+iconSrc+'" alt="'+name+'" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'">'
+ '<div class="modal-distro-icon-fallback" style="background:'+bg+';display:none">'+name.charAt(0).toUpperCase()+'</div>'
: '<img class="modal-distro-icon" src="'+iconSrc+'" alt="'+name+'">'
+ '<div class="modal-distro-icon-fallback" style="background:'+bg+';display:none">'+name.charAt(0).toUpperCase()+'</div>';
var hp = (dw && dw.homepage) || (d && d.homepage) || '';
var basedof = (dw && dw.basedof) || (d && d.basedof) || '';
var html = '<div class="modal-distro-header">'
+ iconHtml
+ '<div class="modal-distro-title"><h2>'+name+'</h2>'
+ (basedof && basedof !== '-' ? '<div class="modal-basedof">Based on: '+basedof+'</div>' : '')
+ (hp ? '<a href="'+hp+'" target="_blank" rel="noopener" class="modal-homepage-link">&#x1f310; '+hp+'</a>' : '')
+ '</div></div>';
if (d) {
html += _distroInfoHtml(d);
html += '<div id="modal-rosette-placeholder"></div>';
var rels = d.releases ? d.releases.trim().split(/\s+/).filter(Boolean) : [];
var eds = d.editions ? d.editions.trim().split(/\s+/).filter(Boolean) : [];
var archs = d.architectures ? d.architectures.trim().split(/\s+/).filter(Boolean) : [];
var urls = d.urls || {};
function _buildMatrix(filterArch) {
var urlsForArch = {};
if (filterArch) {
Object.keys(urls).forEach(function(k) {
var parts = k.split(' ');
var archIdx = parts.indexOf(filterArch);
if (archIdx !== -1) {
var stripped = parts.filter(function(_,i){ return i !== archIdx; }).join(' ');
urlsForArch[stripped] = urls[k];
}
});
} else {
urlsForArch = urls;
}
function dlCell(key) {
var url = urlsForArch[key];
return url
? '<td><a href="'+url+'" target="_blank" rel="noopener" class="dl-link" title="'+key+'">${floppy_inline}</a></td>'
: '<td><span class="dl-disabled">&#x2717;</span></td>';
}
var t = '';
if (rels.length && eds.length) {
t += '<div class="table-wrap"><table class="release-table"><tr><th>Version</th>';
eds.forEach(function(e){ t += '<th class="th-edition"><span>'+e+'</span></th>'; });
t += '<th class="th-spacer"></th></tr>';
rels.forEach(function(r){
t += '<tr><td>'+r+'</td>';
eds.forEach(function(e){ t += dlCell(r+' '+e); });
t += '<td class="th-spacer"></td></tr>';
});
t += '</table></div>';
} else if (rels.length) {
t += '<table class="release-table"><tr><th>Version</th><th>Download</th></tr>';
rels.forEach(function(r){
t += '<tr><td>'+r+'</td>'+dlCell(r)+'</tr>';
});
t += '</table>';
}
return t;
}
var archesInUrls = archs.filter(function(a){ return Object.keys(urls).some(function(k){ return k.split(' ').indexOf(a) !== -1; }); });
if (archesInUrls.length > 1) {
html += '<div class="filter-buttons arch-switcher">';
archesInUrls.forEach(function(a, i) {
html += '<button class="filter-btn'+(i===0?' active':'')+'" data-arch-btn="'+a+'" onclick="_switchArch(this)">'+a+'</button>';
});
html += '</div>';
archesInUrls.forEach(function(a, i) {
html += '<div data-arch-matrix="'+a+'"'+(i===0?'':' style="display:none"')+'>';
html += _buildMatrix(a);
html += '</div>';
});
} else {
html += _buildMatrix(archesInUrls[0] || null);
}
html += '<p class="modal-note">Or <a href="https://github.com/oSoWoSo/DistroHopper" target="_blank" rel="noopener">install DistroHopper</a> and quickly try any supported OS</p>';
} else {
html += _dwInfoHtml(dw, true);
html += '<div id="modal-rosette-placeholder"></div>';
if (!dw) html += '<p style="opacity:0.6;font-size:0.9rem">No data available.</p>';
}
document.getElementById('modalBody').innerHTML = html;
document.getElementById('modalOverlay').classList.add('active');
var _rosetteId = _getRosetteId(osname, basedof);
if (_rosetteId) {
_fetchRosette(_rosetteId).then(function(data) {
var ph = document.getElementById('modal-rosette-placeholder');
if (ph) ph.outerHTML = _rosettePreviewHtml(data, _rosetteId);
});
}
document.querySelectorAll('#modalBody .release-table').forEach(function(tbl) {
tbl.addEventListener('mouseover', function(e) {
var cell = e.target.closest('td,th');
if (!cell || cell.classList.contains('th-spacer')) return;
var idx = cell.cellIndex;
tbl.querySelectorAll('.col-hover').forEach(function(c){ c.classList.remove('col-hover'); });
tbl.querySelectorAll('tr').forEach(function(row){
var c = row.cells[idx];
if (c && !c.classList.contains('th-spacer')) c.classList.add('col-hover');
});
});
tbl.addEventListener('mouseleave', function() {
tbl.querySelectorAll('.col-hover').forEach(function(c){ c.classList.remove('col-hover'); });
});
});
// Add giscus comments to modal
if (_giscusRepo && _giscusCategoryId) {
_loadGiscus(osname);
}
}
function _loadGiscus(osname) {
// Remove existing giscus if present
var existing = document.getElementById('giscus-root');
if (existing) existing.remove();
var container = document.createElement('div');
container.id = 'giscus-root';
container.className = 'giscus-container';
container.style.marginTop = '2rem';
document.getElementById('modalBody').appendChild(container);
var script = document.createElement('script');
script.src = 'https://giscus.app/client.js';
script.setAttribute('data-repo', _giscusRepo);
script.setAttribute('data-repo-id', _giscusRepoId);
script.setAttribute('data-category', _giscusCategory);
script.setAttribute('data-category-id', _giscusCategoryId);
script.setAttribute('data-mapping', 'pathname');
script.setAttribute('data-term', osname);
script.setAttribute('data-strict', '0');
script.setAttribute('data-reactions-enabled', '1');
script.setAttribute('data-emit-metadata', '0');
script.setAttribute('data-input-position', 'bottom');
script.setAttribute('data-theme', 'preferred_color_scheme');
script.setAttribute('data-lang', 'cs');
script.crossorigin = 'anonymous';
script.async = true;
container.appendChild(script);
}
function _switchArch(btn) {
var arch = btn.dataset.archBtn;
var modal = document.getElementById('modalBody');
modal.querySelectorAll('[data-arch-btn]').forEach(function(b) {
b.classList.toggle('active', b.dataset.archBtn === arch);
});
modal.querySelectorAll('[data-arch-matrix]').forEach(function(m) {
m.style.display = m.dataset.archMatrix === arch ? '' : 'none';
});
}
function closeModal() {
document.getElementById('modalOverlay').classList.remove('active');
}
document.getElementById('modalOverlay').addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
function setSearch(q) {
var inp = document.getElementById('distroSearch');
if (inp) { inp.value = q; filterDistros(); }
}
// Category filter buttons — built dynamically from cards present in the DOM
function _buildCategoryFilters() {
var catCount = {};
document.querySelectorAll('.distro-card').forEach(function(card) {
(card.dataset.categories || '').split(',').forEach(function(c) {
c = c.trim();
if (c) catCount[c] = (catCount[c]||0) + 1;
});
});
var container = document.getElementById('catFilters');
if (!container) return;
container.innerHTML = '';
Object.keys(catCount).sort().forEach(function(cat) {
var btn = document.createElement('button');
btn.className = 'filter-btn';
btn.dataset.filterCat = cat;
btn.textContent = cat;
btn.addEventListener('click', function() {
var active = _activeFilters.category === cat;
_activeFilters.category = active ? null : cat;
document.querySelectorAll('[data-filter-cat]').forEach(function(b) {
b.classList.toggle('active', b.dataset.filterCat === _activeFilters.category);
});
filterDistros();
});
container.appendChild(btn);
});
}
var _activeFilters = { support: null, category: null };
function filterDistros() {
var q = ((document.getElementById('distroSearch')||{}).value || '').toLowerCase();
document.querySelectorAll('.distro-card').forEach(function(card) {
var text = (card.dataset.osname||'') + ' '
+ ((card.querySelector('h3 a,h3')||{textContent:''}).textContent||'') + ' '
+ (card.dataset.basedof||'') + ' '
+ (card.dataset.categories||'');
var nameMatch = !q || text.toLowerCase().includes(q);
var isSupported = card.dataset.supported === 'true';
var supportMatch = !_activeFilters.support
|| (_activeFilters.support === 'supported' && isSupported)
|| (_activeFilters.support === 'unsupported' && !isSupported);
var catMatch = !_activeFilters.category
|| (card.dataset.categories || '').split(',').some(function(c) {
return c.trim() === _activeFilters.category;
});
card.style.display = (nameMatch && supportMatch && catMatch) ? '' : 'none';
});
}
document.getElementById('distroSearch').addEventListener('input', filterDistros);
document.querySelectorAll('[data-filter-support]').forEach(function(btn) {
btn.addEventListener('click', function() {
var f = this.dataset.filterSupport;
if (f === 'all') {
_activeFilters.support = null;
_activeFilters.category = null;
var inp = document.getElementById('distroSearch');
if (inp) inp.value = '';
document.querySelectorAll('[data-filter-cat]').forEach(function(b) { b.classList.remove('active'); });
document.querySelectorAll('[data-filter-support]').forEach(function(b) {
b.classList.toggle('active', b.dataset.filterSupport === 'all');
});
} else {
_activeFilters.support = f;
document.querySelectorAll('[data-filter-support]').forEach(function(b) {
b.classList.toggle('active', b.dataset.filterSupport === f);
});
}
filterDistros();
});
});
_buildCategoryFilters();
</script>
${unsupported_section}
${svg_color_html}
<script src="particles.min.js"></script>
${particles_init_html}
</body>
</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-->', 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 "$@"