| docslib | ||
| examples | ||
| tests | ||
| .gitignore | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
SharedDocsLib
SharedDocsLib — небольшой Python-фреймворк для документации. Страницы и HTML-компоненты описываются функциями с декораторами, а библиотека собирает Obsidian-подобный автономный клиент и отдаёт его через FastAPI.
Возможности
- страницы с номерами
1,1.1,1.1.2и автоматическим деревом; - собственные
@Componentи набор безопасных базовых компонентов; - поиск по всему контенту, светлая/тёмная тема, breadcrumbs, Previous/Next;
- изображения из
assets_dir, единожды закодированные в base64 внутри manifest; - полностью автономный
index.html: начальный manifest вместе с CSS и JavaScript встроен; - хранение manifest в IndexedDB и лёгкая проверка обновления через файл
version; index.zip, содержащий толькоindex.html;- live-preview с перекомпиляцией изменившегося Python-модуля.
UI не содержит языко-зависимых служебных подписей. Базовый шрифт — system-ui 16px, а служебные UTF-8-пиктограммы используют Arial. Навигационные стрелки отрисовываются CSS-границами без текстовых символов. AMOLED-тема включена по умолчанию и сохраняется локально после переключения.
На десктопе компактный header содержит toggle боковой панели, полное название документации, поиск и переключатель темы. Sidebar по умолчанию открыт, его состояние не сохраняется между загрузками, а при сворачивании внутренняя раскладка остаётся неподвижной и обрезается границей панели без переноса текста. Поисковый placeholder медленно печатает первые слова случайного названия, добавляет троеточие только в конце и сохраняет готовый текст 5 секунд; при вводе поле раскрывает ранжированные результаты вниз. Проверка manifest выполняется автоматически в фоне без отдельной кнопки обновления.
На мобильных устройствах используется viewport 100dvh и нижний dock. Кнопка ☰ заменяет содержимое страницы копией desktop-sidebar, которая выезжает с левой границы экрана; между header-кнопками и деревом доступен круглый переключатель темы. Выбор страницы в открытом мобильном меню закрывает его и показывает новый контент моментально, без page-transition. Ввод в нижний поиск заменяет содержимое результатами, расположенными снизу вверх по релевантности. При остальных переходах контент вместе с кнопками предыдущей и следующей страницы плавно смещается на 30 px по направлению навигации и меняет прозрачность.
Установка из Git
uv venv
source .venv/bin/activate
uv pip install "git+https://git.snw.su/snw/SharedDocsLib.git@main"
Для разработки и запуска примера из клонированного репозитория:
uv sync --extra dev
uv run python examples/basic.py
После запуска доступны:
GET http://127.0.0.1:1234/manifest.jsonGET http://127.0.0.1:1234/versionGET http://127.0.0.1:1234/index.htmlGET http://127.0.0.1:1234/index.zipGET http://127.0.0.1:1234/live(только приlive_preview=True)
Минимальный пример
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",
custom_js="./assets/inject.js",
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. URL файла version автоматически вычисляется рядом с manifest. Для совместимости также принят вариант manifest_parh из первоначального API, но в новом коде лучше использовать правильное имя.
Статическая сборка
build принимает параметры компиляции из run, не связанные с сервером, и создаёт готовый статический комплект. По умолчанию используется каталог ./build; отсутствующие родительские каталоги создаются автоматически.
from docslib import HeaderButton, build
build(
build_directory="./dist/docs",
title="My docs",
assets_dir="./assets",
custom_css="./assets/inject.css",
custom_js="./assets/inject.js",
manifest_path="./manifest.json",
header_buttons=[
HeaderButton("Знакомство", page1),
HeaderButton("API reference", subpage1),
],
)
В build_directory записываются ровно четыре файла:
index.html— полностью автономный клиент;manifest.json— manifest для публикации отдельным файлом;version— короткий текстовый файл, содержащийrevisionmanifest;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.
Обычные компоненты экранируют пользовательский текст. RawHTML и HTML из собственных @Component считаются доверенными и вставляются как есть.
Функция страницы может вернуть строку, список/генератор строк, вложенные списки или None.
Ссылки между страницами
LocalLink создаёт обычную браузерную ссылку с hash-route. Поэтому ссылку можно копировать, открывать в новой вкладке и активировать с клавиатуры. Клиент перехватывает только обычный клик и показывает нужную страницу без перезагрузки.
# По объекту функции страницы — предпочтительный вариант:
LocalLink("Установка", install_page)
# По точному номеру:
LocalLink("Установка", "1.2")
# По уникальному названию:
LocalLink("Установка", "Install")
# По названию внутри раздела, если названия повторяются:
LocalLink("Установка для сервера", "Install", section="2")
Цель по функции разрешается во время компиляции, когда все @Page уже зарегистрированы, поэтому можно ссылаться и на функцию, объявленную ниже по файлу. Если название неоднозначно или страница отсутствует, компиляция завершится понятной ошибкой. LocLink является коротким псевдонимом LocalLink.
Ключевые кнопки в header
Для навигации верхнего уровня используйте HeaderButton. Локальная цель поддерживает те же варианты, что и LocalLink: объект функции страницы, номер, уникальный заголовок или заголовок внутри указанного раздела.
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/…", открывает страницу без перезагрузки и подсвечивается на всех вложенных страницах своего раздела. Старые словари остаются совместимыми:
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, словарь ресурсов и полный клиентский runtime. Системные файлы шаблона и пользовательские дополнения хранятся раздельно, а при загрузке объединяются в указанном порядке:
{
"schema_version": 2,
"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": []},
"client": {
"css": {"system": "…", "custom": "…"},
"js": {"system": "…", "custom": "…"}
}
}
При открытии минимальный HTML-loader показывает тематический fixed-overlay размером 100vw × 100dvh с индикатором загрузки. Он немедленно читает manifest из IndexedDB; если записи ещё нет, сначала сохраняет встроенную копию. Затем loader применяет CSS, запускает системный JavaScript, рендерит документацию и удаляет overlay.
Только после первого рендера клиент запрашивает маленький файл version: пользователь уже может читать документацию и работать с интерфейсом. Если его хеш совпадает с активной ревизией, manifest.json вообще не скачивается. При несовпадении клиент загружает manifest, сохраняет обновление в IndexedDB без замены текущей страницы и применяет его при следующем открытии. К обоим URL добавляется уникальный ?no_cache=…, запрос выполняется с cache: "no-store", а сервер отвечает заголовками no-store, no-cache, Pragma и Expires. При недоступном сервере продолжает работать уже отрендеренная кешированная или встроенная версия.
custom_css и custom_js принимают пути к UTF-8-файлам. Пользовательский CSS добавляется после системного, поэтому может переопределять тему. Пользовательский JavaScript загружается после системного runtime и до события docslib:rendered:
document.addEventListener("docslib:rendered", (event) => {
console.log(event.detail.manifest.revision);
}, { once: true });
При обнаружении новой ревизии клиент отправляет всплывающее DOM-событие on_manifest_update. Обработчик из custom_js регистрируется до начала фоновой проверки:
document.addEventListener("on_manifest_update", (event) => {
const {
manifest,
currentManifest,
revision,
previousRevision,
source,
cached,
applied,
} = event.detail;
console.log(`Manifest ${previousRevision} → ${revision}`, {
manifest,
currentManifest,
source, // "remote" или "embedded"
cached, // удалось ли записать обновление в IndexedDB
applied, // false: оно применится при следующем открытии
});
});
Сразу после несовпадения хеша из version, но ещё до запроса большого manifest.json, отправляется on_update_available:
document.addEventListener("on_update_available", (event) => {
const { revision, currentRevision, versionUrl, manifestUrl } = event.detail;
console.log(`Доступно обновление ${currentRevision} → ${revision}`, {
versionUrl,
manifestUrl,
});
});
Код из custom_js считается доверенным и выполняется в контексте страницы. Изменение системного или пользовательского CSS/JS меняет revision, поэтому обновлённый runtime сохраняется в IndexedDB и применяется после перезагрузки клиента.
Версия библиотеки, которой был скомпилирован физический index.html, доступна пользовательскому JavaScript в глобальной строковой переменной:
console.log(globalThis.COMPILED_FROM_VERSION); // например, "0.3.7"
Присваивание добавляется в bootstrap-скрипт самой HTML-ки. Автообновление manifest не меняет COMPILED_FROM_VERSION: значение обновится только после новой компиляции и замены index.html.
Для печати всей документации из custom_js или консоли доступна глобальная функция:
PrintFullDocumentation();
Она сразу закрывает интерфейс тематическим loading-overlay, формирует единый документ из всех страниц в порядке manifest, подставляет и декодирует все изображения, ожидает готовности шрифтов и только затем открывает системный диалог печати. Каждая страница документации начинается с нового печатного листа. Печатная тема всегда использует белый фон, чёрный текст и компактные заголовки независимо от выбранной экранной темы.
Live-preview
В live-режиме браузер опрашивает только /live. Сервер следит за файлами, в которых объявлены страницы, а также за системными и пользовательскими CSS/JS. При изменении он пересобирает manifest; клиент сохраняет его и перезагружается с новым runtime. В запускаемом файле вызов run() обязательно помещать под if __name__ == "__main__", чтобы watcher не запускал второй сервер.
HTTP-загрузка и исполнение произвольных .py файлов намеренно отсутствуют: такая ручка была бы удалённым выполнением кода. Live-preview работает с локальными исходниками проекта.
Свой клиент
Встроенные исходники находятся в docslib/templates/: index.html, loader.js, client.css, client.js. loader.js является небольшим bootstrap-кодом HTML, а client.css и client.js попадают в manifest. Скопируйте папку, измените файлы и передайте template_dir="./my-template". Компилятор проверяет обязательные placeholders и всё равно выдаёт один автономный HTML.
Для тестов:
pytest