No description
Find a file
2026-08-14 11:53:04 +00:00
docslib Refine mobile menu navigation 2026-08-14 11:53:04 +00:00
examples Expose HTML compile version and update event 2026-08-10 23:03:13 +00:00
tests Refine mobile menu navigation 2026-08-14 11:53:04 +00:00
.gitignore Initial SharedDocsLib implementation 2026-08-09 17:59:49 +00:00
LICENSE Initial SharedDocsLib implementation 2026-08-09 17:59:49 +00:00
pyproject.toml Expose HTML compile version and update event 2026-08-10 23:03:13 +00:00
README.md Refine mobile menu navigation 2026-08-14 11:53:04 +00:00
uv.lock Expose HTML compile version and update event 2026-08-10 23:03:13 +00:00

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.json
  • GET http://127.0.0.1:1234/version
  • 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)

Минимальный пример

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 — короткий текстовый файл, содержащий revision 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.

Обычные компоненты экранируют пользовательский текст. 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