#!/usr/bin/env python3
from io import StringIO
from json import loads
from glob import glob
from pathlib import Path
import re
from pytablewriter import MarkdownTableWriter
import requests
import zipfile
import shutil
from contextlib import suppress
from os import getenv, path
DOCS_LANG = getenv("DOCS_LANG", "en")
LANG = DOCS_LANG.split("-")[0].lower()
# PRO plugins to exclude from the generated features documentation.
# Match is done on the plugin "id" field from plugin.json.
# Keep "alerting" here while it is in alpha testing.
PRO_PLUGINS_IGNORE = ("alerting",)
PRO_PLUGIN_DOCS = {
"migration": {"advanced_anchor": "migration-pro"},
"anti-ddos": {"advanced_anchor": "anti-ddos-pro"},
"user-manager": {
"advanced_anchor": "user-manager-pro",
"youtube": {
"url": "https://www.youtube-nocookie.com/embed/EIohiUf9Fg4",
"title": "User Manager",
},
},
"ui-single-sign-on": {"advanced_anchor": "ui-single-sign-on-pro"},
"easy-resolve": {
"advanced_anchor": "easy-resolve-pro",
"youtube": {
"url": "https://www.youtube-nocookie.com/embed/45vX0WJqjxo",
"title": "Easy Resolve",
},
},
"load-balancer": {
"advanced_anchor": "load-balancer-pro",
"youtube": {
"url": "https://www.youtube-nocookie.com/embed/cOVp0rAt5nw?si=iVhDio8o8S4F_uag",
"title": "Load Balancer",
},
},
"custom-pages": {"advanced_anchor": "custom-pages-pro"},
"openid-connect": {
"advanced_anchor": "openid-connect-pro",
"youtube": {
"url": "https://www.youtube-nocookie.com/embed/0e4lcXTIIfs",
"title": "OpenID Connect",
},
},
"ldap-sso": {"advanced_anchor": "ldap-sso-pro"},
"openapi-validator": {
"advanced_anchor": "openapi-validator-pro",
"youtube": {
"url": "https://www.youtube-nocookie.com/embed/3oZOO1XdSlc",
"title": "OpenAPI Validator",
},
},
"cache": {"advanced_anchor": "cache-pro"},
"acme": {"advanced_anchor": "acme"},
"wildcard": {"advanced_anchor": "wildcard-pro"},
}
PRO_PLUGIN_DOCS["loadbalancer"] = PRO_PLUGIN_DOCS["load-balancer"]
I18N = {
"en": {
"features_title": "# Features",
"intro_text": "This section contains the full list of settings supported by BunkerWeb. If you are not yet familiar with BunkerWeb, you should first read the [concepts](concepts.md) section of the documentation. Please follow the instructions for your own [integration](integrations.md) on how to apply the settings.",
"global_settings": "## Global settings",
"stream_support": "STREAM support",
# Table headers
"header_setting": "Setting",
"header_default": "Default",
"header_context": "Context",
"header_multiple": "Multiple",
"header_description": "Description",
# Yes/No
"yes": "yes",
"no": "no",
# Badge
"pro_badge": " (PRO)",
"pro_advanced_link": "For a more detailed guide, see the [advanced usages]({href}) documentation.",
},
"fr": {
"features_title": "# Fonctionnalités",
"intro_text": "Cette section contient la liste complète des paramètres pris en charge par BunkerWeb. Si vous n’êtes pas encore familier avec BunkerWeb, commencez par lire la section [concepts](concepts.md) de la documentation. Suivez ensuite les instructions de votre [intégration](integrations.md) pour appliquer les paramètres.",
"global_settings": "## Paramètres globaux",
"stream_support": "Prise en charge STREAM",
# Table headers
"header_setting": "Paramètre",
"header_default": "Valeur par défaut",
"header_context": "Contexte",
"header_multiple": "Multiple",
"header_description": "Description",
# Yes/No
"yes": "oui",
"no": "non",
# Badge
"pro_badge": " (PRO)",
"pro_advanced_link": "Pour un guide plus détaillé, consultez la documentation des [utilisations avancées]({href}).",
},
"de": {
"features_title": "# Funktionen",
"intro_text": "Dieser Abschnitt enthält die vollständige Liste der von BunkerWeb unterstützten Einstellungen. Wenn Sie mit BunkerWeb noch nicht vertraut sind, lesen Sie zuerst den Abschnitt [Konzepte](concepts.md) der Dokumentation. Befolgen Sie anschließend die Anweisungen für Ihre [Integration](integrations.md), um die Einstellungen anzuwenden.",
"global_settings": "## Globale Einstellungen",
"stream_support": "STREAM-Unterstützung",
"header_setting": "Einstellung",
"header_default": "Standardwert",
"header_context": "Kontext",
"header_multiple": "Mehrfach",
"header_description": "Beschreibung",
"yes": "ja",
"no": "nein",
"pro_badge": " (PRO)",
"pro_advanced_link": "Eine ausführlichere Anleitung finden Sie in der Dokumentation zur [erweiterten Nutzung]({href}).",
},
"es": {
"features_title": "# Características",
"intro_text": "Esta sección contiene la lista completa de ajustes admitidos por BunkerWeb. Si aún no está familiarizado con BunkerWeb, primero lea la sección de [conceptos](concepts.md) de la documentación. Siga las instrucciones de su [integración](integrations.md) para aplicar los ajustes.",
"global_settings": "## Configuración global",
"stream_support": "Compatibilidad con STREAM",
"header_setting": "Parámetro",
"header_default": "Valor predeterminado",
"header_context": "Contexto",
"header_multiple": "Múltiple",
"header_description": "Descripción",
"yes": "sí",
"no": "no",
"pro_badge": " (PRO)",
"pro_advanced_link": "Para una guía más detallada, consulta la documentación de [usos avanzados]({href}).",
},
"zh": {
"features_title": "# 功能",
"intro_text": "本节包含 BunkerWeb 支持的完整设置列表。若尚未熟悉 BunkerWeb,建议先阅读文档中的[概念](concepts.md)部分。请根据您的[集成](integrations.md)说明应用相应的设置。",
"global_settings": "## 全局设置",
"stream_support": "STREAM 支持",
"header_setting": "参数",
"header_default": "默认值",
"header_context": "上下文",
"header_multiple": "可重复",
"header_description": "描述",
"yes": "是",
"no": "否",
"pro_badge": " (PRO)",
"pro_advanced_link": "如需更详细的指南,请参阅[高级用法]({href})文档。",
},
}
def tr(key: str):
base = I18N.get("en", {})
return I18N.get(LANG, base).get(key, base.get(key, key))
def normalize_doc_key(value):
return re.sub(r"[^a-z0-9]+", "-", value.casefold()).strip("-")
def get_pro_plugin_docs(data):
for value in (data.get("id", ""), data.get("name", "")):
if not value:
continue
plugin_docs = PRO_PLUGIN_DOCS.get(normalize_doc_key(value))
if plugin_docs:
return plugin_docs
return {}
def generate_docs_for_lang(lang: str):
"""Generate documentation for a specific language."""
global LANG
LANG = lang
def tr_lang(key: str):
base = I18N.get("en", {})
return I18N.get(lang, base).get(key, base.get(key, key))
doc = StringIO()
print(f"{tr_lang('features_title')}\n", file=doc)
print(f"{tr_lang('intro_text')}\n", file=doc)
# Print global settings
print(f"{tr_lang('global_settings')}\n", file=doc)
print(f"\n{stream_support('partial')}\n", file=doc)
# Check if README.md exists for global settings and use its content instead
global_readme = get_global_readme_content(lang)
if global_readme:
print(global_readme, file=doc)
else:
with open("src/common/settings.json", "r") as f:
print(print_md_table(loads(f.read()), tr_lang), file=doc)
print(file=doc)
# Get core plugins
core_settings = {}
for core in glob("src/common/core/*/plugin.json"):
plugin_dir = path.dirname(core)
with open(core, "r") as f:
with suppress(Exception):
core_plugin = loads(f.read())
if len(core_plugin["settings"]) > 0:
core_plugin["dir"] = plugin_dir
core_settings[core_plugin["name"]] = core_plugin
# Get PRO plugins
if getenv("VERSION"):
version = getenv("VERSION")
else:
with open("src/VERSION", "r") as f:
version = f.read().strip()
# Download and extract PRO plugins if not already done
pro_zip_path = Path(f"v{version}.zip")
pro_dir_path = Path(f"v{version}")
if not pro_zip_path.exists():
url = f"https://assets.bunkerity.com/bw-pro/preview/v{version}.zip"
response = requests.get(url)
response.raise_for_status()
pro_zip_path.write_bytes(response.content)
if not pro_dir_path.exists():
with zipfile.ZipFile(pro_zip_path, "r") as zip_ref:
zip_ref.extractall(pro_dir_path)
for pro in glob(f"v{version}/*/plugin.json"):
plugin_dir = path.dirname(pro)
with open(pro, "r") as f:
with suppress(Exception):
pro_plugin = loads(f.read())
if pro_plugin.get("id") in PRO_PLUGINS_IGNORE:
print(f"Skipping PRO plugin '{pro_plugin.get('id')}' (in PRO_PLUGINS_IGNORE)")
continue
pro_plugin["dir"] = plugin_dir
core_settings[pro_plugin["name"]] = pro_plugin
core_settings[pro_plugin["name"]]["is_pro"] = True
# Print plugins and their settings
for _, data in sorted(core_settings.items(), key=lambda item: item[0].casefold()):
pro_crown = ""
if "is_pro" in data:
pro_crown = (
f" "
+ tr_lang("pro_badge")
+ "\n"
)
print(f"## {data['name']}{pro_crown}\n", file=doc)
pro_docs = get_pro_plugin_docs(data) if "is_pro" in data else {}
yt_info = pro_docs.get("youtube")
if yt_info:
print(
f"