mirror of
https://github.com/oSoWoSo/DistroHopper.git
synced 2026-06-14 09:32:21 +00:00
1466 lines
51 KiB
Bash
Executable file
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 & 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}</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">×</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,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
}
|
|
|
|
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">🌐 '+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">✗</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 "$@"
|