Plank help · updated 2026-09-19

Как собрать клиентский документ: маршрут

Маршрут сборки клиентского документа A4 на одной короткой странице: сборщик уже в контейнере, в /opt/plank/doc, скрипт сборки кладут в /workspace/scripts/doc/, фирменный набор — один JSON, compose() переставляет повисший хвост, а проверка читает готовый PDF до выдачи.

Agents: fetch the raw markdown of this page at /ru/help/documents.md

Как собрать клиентский документ: маршрут

Документ читают в одиночку с расстояния вытянутой руки и отправляют PDF-ом или печатают на A4: бриф, предложение, отчёт, выжимка договора. Это не веб-страница и не презентация.

python3 -c "import plank_doc"                       # уже в контейнере? тогда работаем
# напишите /workspace/scripts/doc/build_acme.py с `from plank_doc import *`
python3 /workspace/scripts/doc/build_acme.py
python3 /opt/plank/doc/plank_doc_qa.py /workspace/acme.pdf      # код возврата должен быть 0

1. Сборщик уже здесь, в /opt/plank/doc

Он приезжает внутри контейнера, потому что без WeasyPrint он не отрисует ничего, а WeasyPrint едет в том же образе. import plank_doc работает из любого каталога. Скрипт сборки всё равно кладите в /workspace/scripts/doc/ — это папка рабочего пространства: она переживает перезапуски, и её видят и личный, и общий чат, чего домашняя папка чата не умеет.

Если import plank_doc не прошёл: поставьте его в /workspace/scripts/doc/

Этот контейнер собран раньше, чем появился сборщик. Загрузка — путь обновления и запасной вариант; каталог назначения в обоих случаях /workspace/scripts/doc/. Выполните:

mkdir -p /workspace/scripts/doc/plank_doc
K=https://plank.md/help/kits/client-documents/files/scripts/doc
curl -sSfL -o /workspace/scripts/doc/plank_doc_qa.py $K/plank_doc_qa.py
for f in __init__ scale color brand blocks css document; do
  curl -sSfL -o /workspace/scripts/doc/plank_doc/$f.py $K/plank_doc/$f.py
done

Это пакет из семи модулей плюс проверка, а не один файл. Все файлы с размерами и хешами перечислены в plank.md/help/kits/client-documents/manifest.json, если нужно проверить загрузку. Скрипт сборки, лежащий в /workspace/scripts/doc/, ставит свой каталог первым в путь импорта — так что скачанная копия побеждает встроенную без единой настройки.

2. Никогда не пишите эти файлы сами

plank_doc/ и plank_doc_qa.py берутся из контейнера или из этого манифеста и больше ниоткуда. Не пишите собственный файл с таким именем, чтобы разблокироваться. Самодельный модуль под нашим именем импортируется без ошибок, не измеряет ничего, и все последующие сборки в /workspace/scripts/doc/ молча берут его. Если настоящий получить не удаётся — скажите об этом и соберите PDF иначе, под другим именем файла.

3. Фирменный набор

Один JSON в рабочем пространстве, который называет ваш скрипт сборки. Семь полей и закрытый набор логотипов:

{
  "name": "ACME", "ink": "#101014", "ground": "#FFFFFF",
  "accent": "#FFD400", "accent_ink": "#000000",
  "typeface": "Inter", "confidentiality": "Конфиденциально",
  "logos": { "light_on_dark": "assets/acme-white.png",
             "dark_on_light": "assets/acme-black.png" }
}

accent_ink — то, что читается на акценте, когда акцент является заливкой. Это решение бренда, его не угадывают. Сам акцент — заливка и линейка, но никогда не текст на бумаге: затемнённый accent_text сборщик выводит сам. Нужны обе полярности логотипа (исключение одно — mark_only, для марки, читаемой на любом фоне), а набор, объявивший белый логотип как dark_on_light, отклоняется по пикселям самого файла. Отказ называет поле и отношение контраста. Чините набор, а не порог.

4. Собирайте через compose, а не build

# /workspace/scripts/doc/build_acme_brief.py
from plank_doc import *
from plank_doc.document import compose

brand = Brand.from_kit("/workspace/scripts/doc/brand/acme.json")
doc = Document(brand=brand, kind="Бриф", footer_left="ACME · проект", footer_right="Конфиденциально")
doc.add(Masthead(kind="Бриф"), Title("Заголовок", "Подзаголовок"), ..., Colophon(["…"]))
compose(doc, "/workspace/acme-brief.pdf")

compose собирает, измеряет собственную последнюю страницу и переставляет хвост, если тот повис. Блоки выбирают по СМЫСЛУ — SpecTable, Callout, Entry, Bullets, KeyFacts — и никогда по виду: ни один из них не принимает ни цвет, ни кегль. Одиннадцать повторов «жирный подзаголовок, потом абзац» — это ненаписанная таблица.

5. Прогоните проверку до того, как отдать файл

python3 /opt/plank/doc/plank_doc_qa.py /workspace/acme-brief.pdf
# или /workspace/scripts/doc/plank_doc_qa.py, если вы скачали его туда

Она читает готовый PDF — любой, от любого производителя — и возвращает ненулевой код при ошибке: логотип на фоне, где его не видно; страница, оборвавшаяся на середине листа; слово ниже порога WCAG AA (так выглядит пропущенный accent_ink или акцент, пущенный текстом, к моменту, когда документ дошёл до читателя); краска в обрезном поле. Каждая означает, что читатель увидит поломку. Почините и прогоните снова.

Если вы всё же отдаёте документ, забракованный проверкой, напишите об этом с числом и перечнем: «проверка показывает 5 ошибок — 4 пустые страницы и 1 невидимый логотип, — которые я не исправил». Отдать забракованный документ, не назвав число, — ровно то, ради чего этот шаг существует.

6. Навык рабочего пространства не заменяет этот маршрут

Навык, шаблон или AGENTS.md могут владеть внешним видом — цветами, шрифтом, составом разделов, — и вы им следуете. Инструментом они не владеют: если там прямо не назван другой сборщик и не объяснено почему, документ всё равно собирается через plank_doc из /workspace/scripts/doc/ или /opt/plank/doc и всё равно проверяется через plank_doc_qa.py. Навык, который молчит о способе сборки, не выбирает другой маршрут.

Экранные HTML-материалы — другая задача и другой шаблон: HTML-материалы. Презентация — третья: Как собрать презентацию.