# 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-тема включена по умолчанию и сохраняется локально после переключения. Accessibility-кнопка создаётся системным JavaScript, поэтому появляется и в ранее скомпилированных HTML после обновления manifest. На десктопе она закреплена в левом нижнем углу ниже loading-overlay, на мобильном располагается рядом с переключателем темы внутри меню. Псевдостраница настроек позволяет менять размер текста от 12 до 28 px, сбрасывать параметры и выбирать только локальные браузерные семейства `system-ui`, `sans-serif`, `serif` и `monospace`; сетевые шрифты не используются. Размер применяется только к sidebar, документации и кнопкам Previous/Next, а семейство — ко всему интерфейсу, кроме UTF-8 glyph-кнопок. Параметры сохраняются в `localStorage`. На десктопе компактный header содержит toggle боковой панели, полное название документации, поиск и переключатель темы. Sidebar по умолчанию открыт, его состояние не сохраняется между загрузками, а при сворачивании внутренняя раскладка остаётся неподвижной и обрезается границей панели без переноса текста. Поисковый placeholder медленно печатает первые слова случайного названия, добавляет троеточие только в конце и сохраняет готовый текст 5 секунд; при вводе поле раскрывает ранжированные результаты вниз. Проверка manifest выполняется автоматически в фоне без отдельной кнопки обновления. Обновлённый runtime сохраняет совместимость со старыми скомпилированными HTML-оболочками: отсутствие desktop-toggle, мобильного переключателя темы или общего контейнера page-transition не останавливает запуск клиента. На мобильных устройствах используется viewport `100dvh` и нижний dock. Кнопка `☰` заменяет содержимое страницы копией desktop-sidebar, которая выезжает с левой границы экрана; между header-кнопками и деревом доступен круглый переключатель темы. Выбор страницы в открытом мобильном меню закрывает его и показывает новый контент моментально, без page-transition. Ввод в нижний поиск заменяет содержимое результатами, расположенными снизу вверх по релевантности. При остальных переходах контент вместе с кнопками предыдущей и следующей страницы плавно смещается на 30 px по направлению навигации и меняет прозрачность. ## Установка из 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/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`) ## Минимальный пример ```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'

{text}

' @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`; отсутствующие родительские каталоги создаются автоматически. ```python 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. Поэтому ссылку можно копировать, открывать в новой вкладке и активировать с клавиатуры. Клиент перехватывает только обычный клик и показывает нужную страницу без перезагрузки. ```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, словарь ресурсов и полный клиентский runtime. Системные файлы шаблона и пользовательские дополнения хранятся раздельно, а при загрузке объединяются в указанном порядке: ```json { "schema_version": 2, "title": "My docs", "revision": "…", "updated_at": "…", "pages": [{"number": "1", "title": "Home", "content": "

"}], "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`: ```javascript document.addEventListener("docslib:rendered", (event) => { console.log(event.detail.manifest.revision); }, { once: true }); ``` При обнаружении новой ревизии клиент отправляет всплывающее DOM-событие `on_manifest_update`. Обработчик из `custom_js` регистрируется до начала фоновой проверки: ```javascript 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`: ```javascript document.addEventListener("on_update_available", (event) => { const { revision, currentRevision, versionUrl, manifestUrl } = event.detail; console.log(`Доступно обновление ${currentRevision} → ${revision}`, { versionUrl, manifestUrl, }); }); ``` Непосредственно перед заменой содержимого при переходе между страницами отправляется всплывающее событие `on_page_change`. В `page` находится полная запись целевой страницы из manifest, а в `previousPage` — предыдущая страница или `null` при первом рендере: ```javascript document.addEventListener("on_page_change", (event) => { const { page, previousPage, number, title, index } = event.detail; console.log(`${previousPage?.number ?? "—"} → ${number}: ${title}`, { page, index, }); }); ``` Код из `custom_js` считается доверенным и выполняется в контексте страницы. Изменение системного или пользовательского CSS/JS меняет `revision`, поэтому обновлённый runtime сохраняется в IndexedDB и применяется после перезагрузки клиента. Версия библиотеки, которой был скомпилирован физический `index.html`, доступна пользовательскому JavaScript в глобальной строковой переменной: ```javascript console.log(globalThis.COMPILED_FROM_VERSION); // например, "0.3.7" ``` Присваивание добавляется в bootstrap-скрипт самой HTML-ки. Автообновление manifest не меняет `COMPILED_FROM_VERSION`: значение обновится только после новой компиляции и замены `index.html`. Для печати всей документации из `custom_js` или консоли доступна глобальная функция: ```javascript 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. Для тестов: ```bash pytest ```