179 lines
11 KiB
Markdown
179 lines
11 KiB
Markdown
# SharedDocsLib
|
||
|
||
SharedDocsLib — небольшой Python-фреймворк для документации. Страницы и HTML-компоненты описываются функциями с декораторами, а библиотека собирает Obsidian-подобный автономный клиент и отдаёт его через FastAPI.
|
||
|
||
## Возможности
|
||
|
||
- страницы с номерами `1`, `1.1`, `1.1.2` и автоматическим деревом;
|
||
- собственные `@Component` и набор безопасных базовых компонентов;
|
||
- поиск по всему контенту, светлая/тёмная тема, breadcrumbs, Previous/Next;
|
||
- изображения из `assets_dir`, единожды закодированные в base64 внутри manifest;
|
||
- полностью автономный `index.html`: CSS, JavaScript и начальный manifest встроены;
|
||
- хранение manifest в IndexedDB и одна проверка обновления при загрузке;
|
||
- `index.zip`, содержащий только `index.html`;
|
||
- live-preview с перекомпиляцией изменившегося Python-модуля.
|
||
|
||
UI не содержит языко-зависимых служебных подписей. Базовый шрифт — `system-ui 16px`; служебные UTF-8-пиктограммы используют Arial, а навигационные стрелки остаются в `system-ui`. AMOLED-тема включена по умолчанию и сохраняется локально после переключения.
|
||
|
||
На десктопе компактный header содержит полное название документации и search/theme/refresh, а слева постоянно видны header-кнопки и дерево страниц. Поисковый placeholder медленно печатает первые слова случайного названия, добавляет троеточие только в конце и сохраняет готовый текст 10 секунд; при вводе поле раскрывает ранжированные результаты вниз. Кнопка `⟳` вращается во время проверки manifest; при наведении она заменяет поиск и тему, показывая ISO-дату и короткий `revision`, а при нажатии синхронизирует manifest и перезагружает страницу.
|
||
|
||
На мобильных устройствах используется viewport `100dvh` и нижний dock. Кнопка `☰` заменяет содержимое страницы копией desktop-sidebar. Ввод в нижний поиск заменяет содержимое результатами, расположенными снизу вверх по релевантности. Тема и обновление на мобильном скрыты.
|
||
|
||
## Установка из Git
|
||
|
||
```bash
|
||
uv venv
|
||
source .venv/bin/activate
|
||
uv pip install "git+https://git.snw.su/snw/SharedDocsLib.git@main"
|
||
```
|
||
|
||
Для разработки и запуска примера из клонированного репозитория:
|
||
|
||
```bash
|
||
uv sync --extra dev
|
||
uv run python examples/basic.py
|
||
```
|
||
|
||
После запуска доступны:
|
||
|
||
- `GET http://127.0.0.1:1234/manifest.json`
|
||
- `GET http://127.0.0.1:1234/index.html`
|
||
- `GET http://127.0.0.1:1234/index.zip`
|
||
- `GET http://127.0.0.1:1234/live` (только при `live_preview=True`)
|
||
|
||
## Минимальный пример
|
||
|
||
```python
|
||
from docslib import HeaderButton, run
|
||
from docslib.hooks import Component, Page
|
||
from docslib.components import H1, Image, LocalLink, P
|
||
|
||
@Component
|
||
def Lead(text):
|
||
return f'<p class="lead">{text}</p>'
|
||
|
||
@Page("1.", "Page title")
|
||
def page1():
|
||
return [
|
||
H1("Hello"),
|
||
P("Documentation text"),
|
||
Image("photo.png", "Description"),
|
||
LocalLink("Open nested page", subpage1),
|
||
]
|
||
|
||
@Page("1.1", "Sub-page")
|
||
def subpage1():
|
||
return [H1("Nested page")]
|
||
|
||
if __name__ == "__main__":
|
||
run(
|
||
port=1234,
|
||
host="0.0.0.0",
|
||
title="My docs",
|
||
assets_dir="./assets",
|
||
custom_css="./assets/inject.css",
|
||
manifest_path="http://127.0.0.1:1234/",
|
||
live_preview=True,
|
||
header_buttons=[
|
||
HeaderButton("Знакомство", page1),
|
||
HeaderButton("API reference", subpage1),
|
||
HeaderButton.external("Project", "https://example.com")
|
||
],
|
||
)
|
||
```
|
||
|
||
`manifest_path` принимает полный путь к JSON либо базовый URL, к которому будет добавлен `manifest.json`. Для совместимости также принят вариант `manifest_parh` из первоначального API, но в новом коде лучше использовать правильное имя.
|
||
|
||
## Компоненты
|
||
|
||
Доступны `H1`, `H2`, `H3`, `P`, `MarkdownText`, `Image`, `Link`, `LocalLink` (`LocLink`), `Code`, `InlineCode`, `Quote`, `Callout`, `List`, `Table`, `Divider`, `Badge`, `Details`, `Json` и `RawHTML`.
|
||
|
||
Обычные компоненты экранируют пользовательский текст. `RawHTML` и HTML из собственных `@Component` считаются доверенными и вставляются как есть.
|
||
|
||
Функция страницы может вернуть строку, список/генератор строк, вложенные списки или `None`.
|
||
|
||
### Ссылки между страницами
|
||
|
||
`LocalLink` создаёт обычную браузерную ссылку с hash-route. Поэтому ссылку можно копировать, открывать в новой вкладке и активировать с клавиатуры. Клиент перехватывает только обычный клик и показывает нужную страницу без перезагрузки.
|
||
|
||
```python
|
||
# По объекту функции страницы — предпочтительный вариант:
|
||
LocalLink("Установка", install_page)
|
||
|
||
# По точному номеру:
|
||
LocalLink("Установка", "1.2")
|
||
|
||
# По уникальному названию:
|
||
LocalLink("Установка", "Install")
|
||
|
||
# По названию внутри раздела, если названия повторяются:
|
||
LocalLink("Установка для сервера", "Install", section="2")
|
||
```
|
||
|
||
Цель по функции разрешается во время компиляции, когда все `@Page` уже зарегистрированы, поэтому можно ссылаться и на функцию, объявленную ниже по файлу. Если название неоднозначно или страница отсутствует, компиляция завершится понятной ошибкой. `LocLink` является коротким псевдонимом `LocalLink`.
|
||
|
||
### Ключевые кнопки в header
|
||
|
||
Для навигации верхнего уровня используйте `HeaderButton`. Локальная цель поддерживает те же варианты, что и `LocalLink`: объект функции страницы, номер, уникальный заголовок или заголовок внутри указанного раздела.
|
||
|
||
```python
|
||
run(
|
||
header_buttons=[
|
||
# Короткая форма по функции страницы:
|
||
HeaderButton("Знакомство", introduction_page),
|
||
|
||
# Явная форма по номеру:
|
||
HeaderButton.local("API reference", "3"),
|
||
|
||
# По названию в определённом разделе:
|
||
HeaderButton.local("Установка", "Install", section="2"),
|
||
|
||
# Внешняя ссылка:
|
||
HeaderButton.external("GitHub", "https://github.com/example/project"),
|
||
]
|
||
)
|
||
```
|
||
|
||
Локальная header-кнопка компилируется в обычный `href="#/page/…"`, открывает страницу без перезагрузки и подсвечивается на всех вложенных страницах своего раздела. Старые словари остаются совместимыми:
|
||
|
||
```python
|
||
header_buttons=[
|
||
{"label": "Знакомство", "page": introduction_page},
|
||
{"label": "API", "page": "API reference", "section": "3"},
|
||
{"label": "GitHub", "href": "https://github.com/", "target": "_blank"},
|
||
]
|
||
```
|
||
|
||
## Manifest
|
||
|
||
Manifest имеет версию схемы, заголовок, `revision`, `updated_at`, страницы, настройки UI и словарь ресурсов:
|
||
|
||
```json
|
||
{
|
||
"schema_version": 1,
|
||
"title": "My docs",
|
||
"revision": "…",
|
||
"updated_at": "…",
|
||
"pages": [{"number": "1", "title": "Home", "content": "<h1>…</h1>"}],
|
||
"assets": {"photo.png": {"mime": "image/png", "data": "base64…"}},
|
||
"ui": {"header_buttons": []}
|
||
}
|
||
```
|
||
|
||
При первом открытии встроенный manifest сохраняется в IndexedDB. Затем клиент один раз запрашивает `manifest_path`. Если `revision` или `updated_at` отличаются, новый manifest заменяет старый и страница перезагружается. При недоступном сервере продолжает работать кеш или встроенная копия.
|
||
|
||
## Live-preview
|
||
|
||
В live-режиме браузер опрашивает только `/live`. Сервер следит за файлами, в которых объявлены страницы, и при изменении повторно выполняет их, после чего клиент получает новый manifest. В запускаемом файле вызов `run()` обязательно помещать под `if __name__ == "__main__"`, чтобы watcher не запускал второй сервер.
|
||
|
||
HTTP-загрузка и исполнение произвольных `.py` файлов намеренно отсутствуют: такая ручка была бы удалённым выполнением кода. Live-preview работает с локальными исходниками проекта.
|
||
|
||
## Свой клиент
|
||
|
||
Встроенные исходники находятся в `docslib/templates/`: `index.html`, `client.css`, `client.js`. Скопируйте папку, измените файлы и передайте `template_dir="./my-template"`. Компилятор проверяет обязательные placeholders и всё равно выдаёт один автономный HTML.
|
||
|
||
Для тестов:
|
||
|
||
```bash
|
||
pytest
|
||
```
|