Add static documentation build API
This commit is contained in:
parent
e526df14dd
commit
738aa1e0b6
4 changed files with 93 additions and 5 deletions
30
README.md
30
README.md
|
|
@ -15,7 +15,7 @@ SharedDocsLib — небольшой Python-фреймворк для докум
|
||||||
|
|
||||||
UI не содержит языко-зависимых служебных подписей. Базовый шрифт — `system-ui 16px`; служебные UTF-8-пиктограммы используют Arial, а навигационные стрелки остаются в `system-ui`. AMOLED-тема включена по умолчанию и сохраняется локально после переключения.
|
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. Ввод в нижний поиск заменяет содержимое результатами, расположенными снизу вверх по релевантности. Тема и обновление на мобильном скрыты.
|
На мобильных устройствах используется viewport `100dvh` и нижний dock. Кнопка `☰` заменяет содержимое страницы копией desktop-sidebar. Ввод в нижний поиск заменяет содержимое результатами, расположенными снизу вверх по релевантности. Тема и обновление на мобильном скрыты.
|
||||||
|
|
||||||
|
|
@ -84,6 +84,34 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
`manifest_path` принимает полный путь к JSON либо базовый URL, к которому будет добавлен `manifest.json`. Для совместимости также принят вариант `manifest_parh` из первоначального API, но в новом коде лучше использовать правильное имя.
|
`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`.
|
Доступны `H1`, `H2`, `H3`, `P`, `MarkdownText`, `Image`, `Link`, `LocalLink` (`LocLink`), `Code`, `InlineCode`, `Quote`, `Callout`, `List`, `Table`, `Divider`, `Badge`, `Details`, `Json` и `RawHTML`.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
"""Public API for SharedDocsLib."""
|
"""Public API for SharedDocsLib."""
|
||||||
|
|
||||||
from .hooks import Component, Page
|
from .hooks import Component, Page
|
||||||
from .server import create_app, run
|
from .server import build, create_app, run
|
||||||
from .ui import HeaderButton
|
from .ui import HeaderButton
|
||||||
|
|
||||||
__all__ = ["Component", "HeaderButton", "Page", "create_app", "run"]
|
__all__ = ["Component", "HeaderButton", "Page", "build", "create_app", "run"]
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.1.0"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
|
import json
|
||||||
import runpy
|
import runpy
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -178,6 +179,39 @@ def create_app(
|
||||||
return 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(
|
def run(
|
||||||
*,
|
*,
|
||||||
port: int = 8000,
|
port: int = 8000,
|
||||||
|
|
@ -208,4 +242,4 @@ def run(
|
||||||
uvicorn.run(app, host=host, port=port, log_level=log_level)
|
uvicorn.run(app, host=host, port=port, log_level=log_level)
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["DocumentationSite", "create_app", "run"]
|
__all__ = ["DocumentationSite", "build", "create_app", "run"]
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import zipfile
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from docslib import HeaderButton
|
from docslib import HeaderButton, build
|
||||||
from docslib.components import H1, Image, P
|
from docslib.components import H1, Image, P
|
||||||
from docslib.hooks import Page
|
from docslib.hooks import Page
|
||||||
from docslib.server import create_app
|
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
|
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):
|
def test_live_preview_reloads_a_changed_main_source(tmp_path):
|
||||||
source = tmp_path / "docs.py"
|
source = tmp_path / "docs.py"
|
||||||
source.write_text(
|
source.write_text(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue