Add chinese and french to the documentation.
Some checks failed
Build / Bazel, cl, windows-latest (push) Has been cancelled
Build / Bazel, clang++, macos-latest (push) Has been cancelled
Build / Bazel, clang++, ubuntu-latest (push) Has been cancelled
Build / Bazel, g++, macos-latest (push) Has been cancelled
Build / Bazel, g++, ubuntu-latest (push) Has been cancelled
Build / CMake, cl, windows-latest (push) Has been cancelled
Build / CMake, gcc, ubuntu-latest (push) Has been cancelled
Build / CMake, llvm, ubuntu-latest (push) Has been cancelled
Build / CMake, llvm, macos-latest (push) Has been cancelled
Build / Test modules (llvm, ubuntu-latest) (push) Has been cancelled
Documentation / documentation (push) Has been cancelled

This commit is contained in:
ArthurSonzogni
2025-11-23 20:12:55 +01:00
parent 69d645ca04
commit 73707b5b00

View File

@@ -8,18 +8,86 @@ import json
from pathlib import Path from pathlib import Path
from typing import List, Dict from typing import List, Dict
class VersionInfo: # --- Configuration ---
"""A structure to hold all information about a single documentation version.""" # URL for the translations repository. This is where other language branches reside.
def __init__(self, name: str, is_main: bool, output_root: Path): TRANSLATIONS_REPO_URL = "git@github.com:ArthurSonzogni/ftxui-translations.git"
self.name = name # --- End Configuration ---
self.is_main = is_main
# Destination directory for the built docs, relative to the output root. # Mapping of language codes to their display names for the dropdown menu.
self.dest_dir = output_root if is_main else output_root / "en" / name # Key: Standard BCP 47/ISO 639-1 Code
# Value: [Native Name, Doxygen Name]
LANG_NAME_MAP = {
"af": ["Afrikaans", "Afrikaans"],
"ar": ["العربية", "Arabic"],
"bg": ["Български", "Bulgarian"],
"ca": ["Català", "Catalan"],
"cs": ["Čeština", "Czech"],
"da": ["Dansk", "Danish"],
"de": ["Deutsch", "German"],
"el": ["Ελληνικά", "Greek"],
"en": ["English", "English"],
"eo": ["Esperanto", "Esperanto"],
"es": ["Español", "Spanish"],
"fi": ["Suomi", "Finnish"],
"fr": ["Français", "French"],
"hi": ["हिन्दी", "Hindi"],
"hr": ["Hrvatski", "Croatian"],
"hu": ["Magyar", "Hungarian"],
"hy": ["Հայերեն", "Armenian"],
"id": ["Bahasa Indonesia", "Indonesian"],
"it": ["Italiano", "Italian"],
"ja": ["日本語", "Japanese-en"],
"ko": ["한국어", "Korean-en"],
"lt": ["Lietuvių", "Lithuanian"],
"lv": ["Latviešu", "Latvian"],
"mk": ["Македонски", "Macedonian"],
"nl": ["Nederlands", "Dutch"],
"no": ["Norsk", "Norwegian"],
"pl": ["Polski", "Polish"],
"pt": ["Português", "Portuguese"],
"ro": ["Română", "Romanian"],
"ru": ["Русский", "Russian"],
"sk": ["Slovenčina", "Slovak"],
"sl": ["Slovenščina", "Slovene"],
"sr": ["Српски", "Serbian"],
"sv": ["Svenska", "Swedish"],
"tr": ["Türkçe", "Turkish"],
"uk": ["Українська", "Ukrainian"],
"vi": ["Tiếng Việt", "Vietnamese"],
"zh-CH": ["中文 (简体)", "Chinese"],
"zh-TW": ["中文 (繁體)", "Chinese-Traditional"],
}
class DocInfo:
"""A structure to hold all information about a single documentation build."""
def __init__(self, lang: str, version_name: str, is_main_version: bool, output_root: Path, is_primary_lang: bool = True):
self.lang = lang
self.version_name = version_name # e.g., "main", "v6.1.9"
self.is_main_version = is_main_version
self.is_primary_lang = is_primary_lang
if self.is_primary_lang:
if self.is_main_version:
# English Main: Deployed to the root directory
self.dest_dir = output_root
else:
# English Versions: Deployed to /en/<version_name>
self.dest_dir = output_root / "en" / version_name
else:
# Translated Docs (only 'main' version for now): Deployed to /<lang_code>/
self.dest_dir = output_root / lang
# The path to this version's index.html, relative to the output root. # The path to this version's index.html, relative to the output root.
self.index_path_from_root = self.dest_dir / "index.html" self.index_path_from_root = self.dest_dir / "index.html"
@property
def key(self) -> str:
"""A unique key for this documentation set (e.g., 'en-main', 'fr-main', 'en-v6.1.9')."""
return f"{self.lang}-{self.version_name}"
def __repr__(self) -> str: def __repr__(self) -> str:
return f"VersionInfo(name='{self.name}', dest_dir='{self.dest_dir}')" return f"DocInfo(lang='{self.lang}', version='{self.version_name}', dest_dir='{self.dest_dir}')"
def run_command(command: List[str], check: bool = True, cwd: Path = None): def run_command(command: List[str], check: bool = True, cwd: Path = None):
""" """
@@ -28,7 +96,6 @@ def run_command(command: List[str], check: bool = True, cwd: Path = None):
command_str = ' '.join(command) command_str = ' '.join(command)
print(f"Executing: {command_str} in {cwd or Path.cwd()}") print(f"Executing: {command_str} in {cwd or Path.cwd()}")
try: try:
# Using capture_output=True to get stdout/stderr
result = subprocess.run( result = subprocess.run(
command, command,
capture_output=True, capture_output=True,
@@ -52,73 +119,110 @@ def run_command(command: List[str], check: bool = True, cwd: Path = None):
print(e.stderr) print(e.stderr)
raise # Re-raise the exception to halt the script raise # Re-raise the exception to halt the script
def get_version_switcher_js( def get_switchers_js(
current_version: VersionInfo, current_doc: DocInfo,
all_versions: List[VersionInfo], all_docs: List[DocInfo],
current_html_file: Path current_html_file: Path
) -> str: ) -> str:
""" """
Generates the JavaScript for the version switcher dropdown. Generates the JavaScript for both the version and language switcher dropdowns.
This version pre-calculates the relative path from the current HTML file
to the index.html of every other version, simplifying the JS logic.
""" """
version_names = [v.name for v in all_versions]
# Create a dictionary mapping version names to their relative URLs. # 1. Prepare Language Data (Links to the main version/entry point of each language)
relative_paths: Dict[str, str] = {} language_map: Dict[str, str] = {}
for version in all_versions: language_display_names: Dict[str, str] = {}
# Calculate the relative path from the *parent directory* of the current HTML file language_doxygen_names: Dict[str, str] = {}
# to the target version's index.html.
path = os.path.relpath(version.index_path_from_root, current_html_file.parent) # Filter for the main entry point of each language (e.g., /fr/, /en/ and /)
relative_paths[version.name] = path main_language_docs = {}
for doc in all_docs:
# Use the main version for its language as the entry point
if doc.is_main_version or (doc.is_primary_lang and doc.version_name == 'main'):
if doc.lang not in main_language_docs:
main_language_docs[doc.lang] = doc
# Calculate relative paths for the language switcher
for lang_code, doc in main_language_docs.items():
relative_path = os.path.relpath(doc.index_path_from_root, current_html_file.parent)
language_map[lang_code] = relative_path
# Use the map for display name, fall back to code if not found
language_display_names[lang_code] = LANG_NAME_MAP.get(lang_code,
lang_code)[0]
language_doxygen_names[lang_code] = LANG_NAME_MAP.get(lang_code,
lang_code)[1]
language_names = list(language_map.keys())
# Sort languages: 'en' first, then others alphabetically.
language_names.sort(key=lambda x: (x != 'en', x))
# 2. Prepare Version Data (Only versions for the current language)
version_docs = [doc for doc in all_docs if doc.lang == current_doc.lang]
version_names = [v.version_name for v in version_docs]
version_paths: Dict[str, str] = {}
# Calculate relative paths for the version switcher
for version_doc in version_docs:
relative_path = os.path.relpath(version_doc.index_path_from_root, current_html_file.parent)
version_paths[version_doc.version_name] = relative_path
# Use json.dumps for safe serialization of data into JavaScript. # Use json.dumps for safe serialization of data into JavaScript.
langs_json = json.dumps(language_names)
lang_paths_json = json.dumps(language_map)
lang_display_json = json.dumps(language_display_names)
versions_json = json.dumps(version_names) versions_json = json.dumps(version_names)
paths_json = json.dumps(relative_paths) version_paths_json = json.dumps(version_paths)
current_version_json = json.dumps(current_version.name) current_lang_json = json.dumps(current_doc.lang)
current_version_json = json.dumps(current_doc.version_name)
# Note: We are using Doxygen's #projectnumber container to inject both switchers.
# We will wrap the original element with a new container.
return f""" return f"""
document.addEventListener('DOMContentLoaded', function() {{ document.addEventListener('DOMContentLoaded', function() {{
const projectNumber = document.getElementById('projectnumber'); const projectNumber = document.getElementById('projectname');
if (!projectNumber) {{ if (!projectNumber) {{
console.warn('Doxygen element with ID "projectnumber" not found. Cannot add version switcher.'); console.warn('Doxygen element with ID "projectnumber" not found. Cannot add version switcher.');
return; return;
}} }}
const langs = {langs_json};
const lang_paths = {lang_paths_json};
const lang_display = {lang_display_json};
const versions = {versions_json}; const versions = {versions_json};
const version_paths = {paths_json}; const version_paths = {version_paths_json};
const currentLang = {current_lang_json};
const currentVersion = {current_version_json}; const currentVersion = {current_version_json};
// Helper function to create a styled select element
const createSelect = (options, current, paths, label, displayMap = null) => {{
const select = document.createElement('select');
select.title = label;
select.onchange = function() {{
const selectedValue = this.value;
if (selectedValue in paths) {{
window.location.href = paths[selectedValue];
}}
}};
// Sort versions: 'main' first, then others numerically descending. // Sort versions: 'main' first, then others numerically descending.
versions.sort((a, b) => {{ options.sort((a, b) => {{
if (a === 'main') return -1; if (a === 'main') return -1;
if (b === 'main') return 1; if (b === 'main') return 1;
return b.localeCompare(a, undefined, {{ numeric: true, sensitivity: 'base' }}); return b.localeCompare(a, undefined, {{ numeric: true, sensitivity: 'base' }});
}}); }});
const select = document.createElement('select'); options.forEach(v => {{
select.onchange = function() {{
const selectedVersion = this.value;
// Navigate directly to the pre-calculated relative path.
if (selectedVersion !== currentVersion) {{
window.location.href = version_paths[selectedVersion];
}}
}};
versions.forEach(v => {{
const option = document.createElement('option'); const option = document.createElement('option');
option.value = v; option.value = v;
option.textContent = v; // Use the displayMap if provided, otherwise default to the value (v)
if (v === currentVersion) {{ option.textContent = displayMap ? displayMap[v] : v;
if (v === current) {{
option.selected = true; option.selected = true;
}} }}
select.appendChild(option); select.appendChild(option);
}}); }});
// Replace the Doxygen project number element with our dropdown.
projectNumber.replaceWith(select);
// Apply some styling to make it look good. // Apply some styling to make it look good.
Object.assign(select.style, {{ Object.assign(select.style, {{
backgroundColor: 'rgba(0, 0, 0, 0.8)', backgroundColor: 'rgba(0, 0, 0, 0.8)',
@@ -128,14 +232,128 @@ document.addEventListener('DOMContentLoaded', function() {{
borderRadius: '5px', borderRadius: '5px',
fontSize: '14px', fontSize: '14px',
fontFamily: 'inherit', fontFamily: 'inherit',
marginLeft: '10px', margin: '0 5px 0 0',
cursor: 'pointer' cursor: 'pointer'
}}); }});
return select;
}};
// 1. Create Language Switcher, passing the language display names map
const langSelect = createSelect(langs, currentLang, lang_paths, 'Select Language', lang_display);
// 2. Create Version Switcher
const versionSelect = createSelect(versions, currentVersion, version_paths, 'Select Version');
// 3. Create FTXUI title.
const ftxuiTitle = document.createElement('span');
ftxuiTitle.textContent = 'FTXUI: ';
Object.assign(ftxuiTitle.style, {{
color: 'white',
fontSize: '20px',
fontWeight: 'bold',
marginRight: '10px'
}});
// 3. Create a container to hold both selectors
const container = document.createElement('div');
container.id = 'version-lang-switchers';
Object.assign(container.style, {{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
width: 'auto'
}});
container.appendChild(ftxuiTitle);
container.appendChild(langSelect);
container.appendChild(versionSelect);
Object.assign(container.style, {{
backgroundColor: 'rgba(0, 0, 0, 0.5)',
padding: '5px 10px',
borderRadius: '8px'
}});
// Replace the Doxygen project number element with our container.
projectNumber.replaceWith(container);
// Clean up the original Doxygen project number text if it still exists nearby
const parent = container.parentElement;
if (parent) {{
const textNode = Array.from(parent.childNodes).find(n => n.nodeType === 3 && n.textContent.trim() !== '');
if (textNode) {{
textNode.remove();
}}
}}
}}); }});
""" """
def build_doc_from_git(doc_info: DocInfo, build_root: Path, repo_url: str, branch_or_tag: str, temp_repo_dir: Path):
"""
Handles checking out the source from a git reference, running the build,
and copying the output to the final destination.
:param doc_info: The DocInfo object for the build.
:param build_root: The temporary root directory for all builds.
:param repo_url: The URL or path to the git repository.
:param branch_or_tag: The git reference (branch or tag) to check out.
:param temp_repo_dir: The path to the temporary cloned repository directory.
"""
print(f"--- Building {doc_info.key} (Source: {branch_or_tag} from {repo_url}) ---")
# 1. Archive the source code from the repository reference.
version_src_dir = build_root / f"src_{doc_info.key}"
version_src_dir.mkdir(parents=True, exist_ok=True)
archive_path = version_src_dir / "source.tar"
# Determine the directory to run git archive from (it needs to be the root of the git repo)
git_run_dir = temp_repo_dir if temp_repo_dir.is_dir() else Path.cwd()
run_command([
"git", "archive", branch_or_tag,
"--format=tar", f"--output={archive_path}"
], cwd=git_run_dir)
run_command(["tar", "-xf", str(archive_path)], cwd=version_src_dir)
archive_path.unlink()
# 2. Configure and build the docs using CMake.
version_build_dir = build_root / f"build_{doc_info.key}"
version_build_dir.mkdir()
# 2.a Update doc/Doxyfile.in to set the correct language.
doxyfile_in_path = version_src_dir / "doc" / "Doxyfile.in"
if not doxyfile_in_path.is_file():
print(f"FATAL: Doxyfile.in not found at {doxyfile_in_path} for {doc_info.key}")
exit(1)
doxyfile_content = doxyfile_in_path.read_text(encoding='utf-8')
# Replace the keyword "English" with the appropriate Doxygen language name.
lang_doxygen_name = LANG_NAME_MAP.get(doc_info.lang, [doc_info.lang,
doc_info.lang])[1]
doxyfile_content = doxyfile_content.replace("English", lang_doxygen_name)
# Assuming CMakeLists.txt is in the root of the extracted source
run_command([
"cmake", str(version_src_dir),
"-DFTXUI_BUILD_DOCS=ON",
'-DFTXUI_BUILD_EXAMPLES=ON' # Required for the Doxygen build target
], cwd=version_build_dir)
run_command(["make", "doc"], cwd=version_build_dir)
# 3. Copy the generated HTML files to the final destination.
doxygen_html_dir = version_build_dir / "doc" / "doxygen" / "html"
if not doxygen_html_dir.is_dir():
print(f"FATAL: Doxygen HTML output not found for {doc_info.key} at {doxygen_html_dir}")
exit(1)
print(f"Copying files to: {doc_info.dest_dir}")
shutil.copytree(doxygen_html_dir, doc_info.dest_dir, dirs_exist_ok=True)
def main(): def main():
"""Main function to build multi-version documentation.""" """Main function to build multi-version and multi-language documentation."""
root_dir = Path.cwd() root_dir = Path.cwd()
output_dir = root_dir / "multiversion_docs" output_dir = root_dir / "multiversion_docs"
@@ -144,77 +362,116 @@ def main():
shutil.rmtree(output_dir) shutil.rmtree(output_dir)
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
print("--- 2. Getting versions from git ---") all_docs: List[DocInfo] = []
# --- 2. Gather English (Primary) Versions from the current repository ---
print("--- 2a. Getting English versions (main repo) ---")
# Get all tags that start with 'v' for versioning
git_tags_result = run_command(["git", "tag", "--list", "v*"]) git_tags_result = run_command(["git", "tag", "--list", "v*"])
# Create a list of version names, starting with 'main'.
version_names = ["main"] + sorted( # Start with 'main', then add sorted tags (reverse numerical order)
english_versions = ["main"] + sorted(
git_tags_result.stdout.splitlines(), git_tags_result.stdout.splitlines(),
reverse=True reverse=True
) )
print(f"Versions to build: {', '.join(version_names)}")
# Pre-compute all version information and paths. print(f"English versions to build: {', '.join(english_versions)}")
versions = [
VersionInfo(name, name == "main", output_dir) for name in english_versions:
for name in version_names is_main = name == "main"
] all_docs.append(DocInfo("en", name, is_main, output_dir, is_primary_lang=True))
with tempfile.TemporaryDirectory() as build_dir_str: with tempfile.TemporaryDirectory() as build_dir_str:
build_dir = Path(build_dir_str) build_dir = Path(build_dir_str)
# --- 3. Build documentation for each version --- temp_translations_repo = build_dir / "translations_repo"
for version in versions:
print(f"\n--- Building docs for version: {version.name} ---")
# Create a temporary directory for this version's source code. # --- 2b. Gather Translated Language Branches ---
version_src_dir = build_dir / f"src_{version.name}" print("\n--- 2b. Cloning translations repo and getting language branches (excluding 'main') ---")
version_src_dir.mkdir() try:
# Clone the translations repository
run_command(["git", "clone", TRANSLATIONS_REPO_URL, str(temp_translations_repo)])
# Check out the version's source code from git. # Get remote branches (e.g., origin/fr, origin/zh-CH) and map them to lang codes
archive_path = version_src_dir / "source.tar" translation_branches_result = run_command(
run_command([ ["git", "branch", "-r", "--list", "origin/*"],
"git", "archive", version.name, cwd=temp_translations_repo
"--format=tar", f"--output={archive_path}" )
])
run_command(["tar", "-xf", str(archive_path)], cwd=version_src_dir)
archive_path.unlink()
# Configure and build the docs using CMake. # Filter and map to language codes
version_build_dir = build_dir / f"build_{version.name}" language_branches = []
version_build_dir.mkdir() for line in translation_branches_result.stdout.splitlines():
run_command([ "cmake", str(version_src_dir), "-DFTXUI_BUILD_DOCS=ON", '-DFTXUI_BUILD_EXAMPLES=ON'], cwd=version_build_dir) branch_name = line.strip()
run_command(["make", "doc"], cwd=version_build_dir)
# Copy the generated HTML files to the final destination. # Ignore lines that are references (like 'origin/HEAD -> origin/main')
doxygen_html_dir = version_build_dir / "doc" / "doxygen" / "html" if '->' in branch_name:
if not doxygen_html_dir.is_dir():
print(f"FATAL: Doxygen HTML output not found for version {version.name}")
exit(1)
print(f"Copying files to: {version.dest_dir}")
shutil.copytree(doxygen_html_dir, version.dest_dir, dirs_exist_ok=True)
# --- 4. Inject version switcher into all HTML files ---
print("\n--- Injecting version switcher JavaScript ---")
for version in versions:
if not version.dest_dir.exists():
print(f"Warning: Destination directory for {version.name} does not exist. Skipping JS injection.")
continue continue
print(f"Processing HTML files in: {version.dest_dir}") # Extract the language code (e.g., 'fr' from 'origin/fr')
if branch_name.startswith('origin/'):
# The name after 'origin/'
lang_code = branch_name.split('origin/')[1]
html_files = [] # Explicitly exclude 'main' as requested by the user
if version.is_main: if lang_code != 'main':
# For the main version, find all HTML files, but explicitly exclude the 'en' directory. language_branches.append(lang_code)
html_files.extend(version.dest_dir.glob("*.html"))
for subdir in version.dest_dir.iterdir(): print(f"Translation languages to build: {', '.join(language_branches)}")
if subdir.is_dir() and subdir.name != 'en':
for lang in language_branches:
# For translations, we treat them as the 'main' version for that language
# The branch name is the language code (e.g., 'fr' branch)
all_docs.append(DocInfo(lang, "main", True, output_dir, is_primary_lang=False))
except Exception as e:
print(f"Warning: Could not clone or process translations repository: {e}")
# Continue with English docs if translation cloning fails
# --- 3. Build documentation for each DocInfo ---
for doc in all_docs:
if doc.is_primary_lang:
# English docs are archived from the current directory (root_dir)
build_doc_from_git(doc, build_dir, "origin", doc.version_name, root_dir)
else:
# Translated docs are archived from the cloned translation repo
# FIX: Use 'origin/<lang>' as the git reference because 'git archive'
# inside the repository only knows remote-tracking branches.
translation_git_ref = f"origin/{doc.lang}"
build_doc_from_git(doc, build_dir, TRANSLATIONS_REPO_URL, translation_git_ref, temp_translations_repo)
# --- 4. Inject version and language switchers into all HTML files ---
print("\n--- Injecting version and language switcher JavaScript ---")
# A set to keep track of processed directories
processed_dirs = set()
for doc in all_docs:
if not doc.dest_dir.exists() or doc.dest_dir in processed_dirs:
continue
print(f"Processing HTML files in: {doc.dest_dir}")
# For the main 'en' directory, we need to handle the structure carefully
# as it contains the root, but we only want to inject once per file.
html_files: List[Path] = []
# We need all language branch names for exclusion, including 'en'.
all_lang_dirs = [d.lang for d in all_docs if not d.is_primary_lang] + ['en']
if doc.is_main_version and doc.is_primary_lang:
# This is the root level ('/')
# Find all HTML files, but explicitly exclude the language subdirectories ('en', 'fr', etc.)
html_files.extend(doc.dest_dir.glob("*.html"))
for subdir in doc.dest_dir.iterdir():
if subdir.is_dir() and subdir.name not in all_lang_dirs:
html_files.extend(subdir.rglob("*.html")) html_files.extend(subdir.rglob("*.html"))
else: else:
# For other versions, their directory is self-contained. # This handles /en/vX.Y.Z/ or /fr/
html_files = list(version.dest_dir.rglob("*.html")) html_files = list(doc.dest_dir.rglob("*.html"))
# Since multiple DocInfos might point to the same file (e.g., index.html),
# we inject the script relative to that specific file's context.
for html_file in html_files: for html_file in html_files:
js_script = get_version_switcher_js(version, versions, html_file) js_script = get_switchers_js(doc, all_docs, html_file)
script_tag = f'<script>{js_script}</script>' script_tag = f'<script>{js_script}</script>'
content = html_file.read_text(encoding='utf-8') content = html_file.read_text(encoding='utf-8')
@@ -222,8 +479,10 @@ def main():
new_content = content.replace("</body>", f"{script_tag}\n</body>") new_content = content.replace("</body>", f"{script_tag}\n</body>")
html_file.write_text(new_content, encoding='utf-8') html_file.write_text(new_content, encoding='utf-8')
processed_dirs.add(doc.dest_dir)
print("\n--- 5. Finalizing ---") print("\n--- 5. Finalizing ---")
print("Multi-version documentation generated successfully!") print("Multi-version and multi-language documentation generated successfully!")
print(f"Output located in: {output_dir.resolve()}") print(f"Output located in: {output_dir.resolve()}")
if __name__ == "__main__": if __name__ == "__main__":