From 738aa1e0b6ee593c322807ad469ab8b7e3f491e8 Mon Sep 17 00:00:00 2001 From: Server <10.9.8.250@reg.snw.su> Date: Mon, 10 Aug 2026 00:28:38 +0000 Subject: [PATCH] Add static documentation build API --- README.md | 30 +++++++++++++++++++++++++++++- docslib/__init__.py | 4 ++-- docslib/server.py | 36 +++++++++++++++++++++++++++++++++++- tests/test_server.py | 28 +++++++++++++++++++++++++++- 4 files changed, 93 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0c33706..a107b24 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ SharedDocsLib — небольшой Python-фреймворк для докум UI не содержит языко-зависимых служебных подписей. Базовый шрифт — `system-ui 16px`; служебные UTF-8-пиктограммы используют Arial, а навигационные стрелки остаются в `system-ui`. AMOLED-тема включена по умолчанию и сохраняется локально после переключения. -На десктопе компактный header содержит полное название документации и search/theme/refresh, а слева постоянно видны header-кнопки и дерево страниц. Поисковый placeholder медленно печатает первые слова случайного названия, добавляет троеточие только в конце и сохраняет готовый текст 10 секунд; при вводе поле раскрывает ранжированные результаты вниз. Кнопка `⟳` вращается во время проверки manifest; при наведении она заменяет поиск и тему, показывая ISO-дату и короткий `revision`, а при нажатии синхронизирует manifest и перезагружает страницу. +На десктопе компактный header содержит полное название документации и search/theme/refresh, а слева постоянно видны header-кнопки и дерево страниц. Поисковый placeholder медленно печатает первые слова случайного названия, добавляет троеточие только в конце и сохраняет готовый текст 5 секунд; при вводе поле раскрывает ранжированные результаты вниз. Кнопка `⟳` вращается во время проверки manifest; при наведении она заменяет поиск и тему, показывая ISO-дату и короткий `revision`, а при нажатии синхронизирует manifest и перезагружает страницу. На мобильных устройствах используется viewport `100dvh` и нижний dock. Кнопка `☰` заменяет содержимое страницы копией desktop-sidebar. Ввод в нижний поиск заменяет содержимое результатами, расположенными снизу вверх по релевантности. Тема и обновление на мобильном скрыты. @@ -84,6 +84,34 @@ if __name__ == "__main__": `manifest_path` принимает полный путь к JSON либо базовый URL, к которому будет добавлен `manifest.json`. Для совместимости также принят вариант `manifest_parh` из первоначального API, но в новом коде лучше использовать правильное имя. +## Статическая сборка + +`build` принимает параметры компиляции из `run`, не связанные с сервером, и создаёт готовый статический комплект. По умолчанию используется каталог `./build`; отсутствующие родительские каталоги создаются автоматически. + +```python +from docslib import HeaderButton, build + +build( + build_directory="./dist/docs", + title="My docs", + assets_dir="./assets", + custom_css="./assets/inject.css", + manifest_path="./manifest.json", + header_buttons=[ + HeaderButton("Знакомство", page1), + HeaderButton("API reference", subpage1), + ], +) +``` + +В `build_directory` записываются ровно три файла: + +- `index.html` — полностью автономный клиент; +- `manifest.json` — manifest для публикации отдельным файлом; +- `index.zip` — архив, содержащий только `index.html`. + +Функция возвращает `BuildResult` с теми же данными в памяти. Серверные параметры `host`, `port`, `live_preview`, `cors_origins` и `log_level` ей не требуются. + ## Компоненты Доступны `H1`, `H2`, `H3`, `P`, `MarkdownText`, `Image`, `Link`, `LocalLink` (`LocLink`), `Code`, `InlineCode`, `Quote`, `Callout`, `List`, `Table`, `Divider`, `Badge`, `Details`, `Json` и `RawHTML`. diff --git a/docslib/__init__.py b/docslib/__init__.py index 99ed2a6..53a1c86 100644 --- a/docslib/__init__.py +++ b/docslib/__init__.py @@ -1,8 +1,8 @@ """Public API for SharedDocsLib.""" from .hooks import Component, Page -from .server import create_app, run +from .server import build, create_app, run from .ui import HeaderButton -__all__ = ["Component", "HeaderButton", "Page", "create_app", "run"] +__all__ = ["Component", "HeaderButton", "Page", "build", "create_app", "run"] __version__ = "0.1.0" diff --git a/docslib/server.py b/docslib/server.py index 7fc3201..9a07be6 100644 --- a/docslib/server.py +++ b/docslib/server.py @@ -1,6 +1,7 @@ from __future__ import annotations import importlib +import json import runpy import sys from pathlib import Path @@ -178,6 +179,39 @@ def create_app( return app +def build( + *, + build_directory: str | Path = "./build", + title: str = "Documentation", + assets_dir: str | Path = "./assets", + custom_css: str | Path | None = None, + template_dir: str | Path | None = None, + manifest_path: str | None = None, + manifest_parh: str | None = None, + header_buttons: Sequence[HeaderButton | Mapping[str, object]] | None = None, +) -> BuildResult: + """Compile registered pages and write a static documentation bundle.""" + compiler = Compiler( + title=title, + assets_dir=assets_dir, + custom_css=custom_css, + template_dir=template_dir, + manifest_url=_manifest_url(manifest_path, manifest_parh), + live_preview=False, + header_buttons=header_buttons, + ) + result = compiler.build() + output = Path(build_directory).expanduser().resolve() + output.mkdir(parents=True, exist_ok=True) + (output / "index.html").write_bytes(result.html) + (output / "manifest.json").write_text( + json.dumps(result.manifest, ensure_ascii=False, separators=(",", ":")), + encoding="utf-8", + ) + (output / "index.zip").write_bytes(result.zip_archive) + return result + + def run( *, port: int = 8000, @@ -208,4 +242,4 @@ def run( uvicorn.run(app, host=host, port=port, log_level=log_level) -__all__ = ["DocumentationSite", "create_app", "run"] +__all__ = ["DocumentationSite", "build", "create_app", "run"] diff --git a/tests/test_server.py b/tests/test_server.py index f451ea5..10fb94b 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -7,7 +7,7 @@ import zipfile from fastapi.testclient import TestClient -from docslib import HeaderButton +from docslib import HeaderButton, build from docslib.components import H1, Image, P from docslib.hooks import Page from docslib.server import create_app @@ -121,6 +121,32 @@ def test_all_endpoints_and_self_contained_build(tmp_path): assert client.get("/live").status_code == 404 +def test_build_writes_static_bundle(tmp_path): + @Page("1", "Static page") + def static_page(): + return H1("Static documentation") + + output = tmp_path / "nested" / "site" + result = build( + build_directory=output, + title="Static docs", + manifest_path="https://docs.example/manifest.json", + header_buttons=[HeaderButton("Home", static_page)], + ) + + assert sorted(path.name for path in output.iterdir()) == [ + "index.html", + "index.zip", + "manifest.json", + ] + assert output.joinpath("index.html").read_bytes() == result.html + assert json.loads(output.joinpath("manifest.json").read_text(encoding="utf-8")) == result.manifest + assert "https://docs.example/manifest.json" in result.html.decode("utf-8") + with zipfile.ZipFile(output / "index.zip") as archive: + assert archive.namelist() == ["index.html"] + assert archive.read("index.html") == result.html + + def test_live_preview_reloads_a_changed_main_source(tmp_path): source = tmp_path / "docs.py" source.write_text(