Add static documentation build API

This commit is contained in:
Server 2026-08-10 00:28:38 +00:00
parent e526df14dd
commit 738aa1e0b6
4 changed files with 93 additions and 5 deletions

View file

@ -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`.

View file

@ -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"

View file

@ -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"]

View file

@ -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(