Разметка страниц
Где ляжет разрыв — единственный метод SDK, который спрашивает и ничего не меняет; саму печать делает хост, и это граница, а не пробел.

В рамке выше работает тот самый проект, чей код лежит ниже, — собранный, но не переписанный: те же файлы, только пропущенные через сборку. Считает в нём не ядро, а дублёр транспорта, поэтому числа в ячейках ничего не значат: смотреть нужно на код.
Страница, которую открывает браузер.
<!doctype html><!-- Разметка страниц — НАСТОЯЩИЙ МАЛЕНЬКИЙ ПРОЕКТ.
Не «страница витрины», а тот проект, который встраивающий копирует к себе целиком. Галерея примеров ЗАПУСКАЕТ эти файлы и ПОКАЗЫВАЕТ их же.--><html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Разметка страниц — @f1</title> </head> <body> <div data-example="page-breaks" class="flex h-screen w-screen flex-col bg-base-100 text-base-content"> <header class="border-b border-base-300 px-4 py-2"> <div class="text-sm font-semibold">Разметка страниц: где ляжет разрыв</div> <div class="text-xs opacity-70"> <code>compute_page_break_layout</code> — единственная операция семьи, которая ничего не меняет, а СПРАШИВАЕТ: книга после вопроса та же, ответ — только геометрия. Бумагу, ориентацию, масштаб и поля называет вызывающий; где именно режется лист и сколько выйдет страниц — считает тот, кто стоит за швом. <b>САМУ ПЕЧАТЬ ДЕЛАЕТ ХОСТ</b>: своего окна печати мы не рисуем и PDF не собираем — это не пробел, а граница. У Electron она своя, у встраивающего в WebView своя. </div> </header>
<section class="m-4 flex flex-col gap-3 rounded-card border border-base-300 p-4" data-print-panel> <div class="flex flex-wrap items-end gap-2"> <!-- Подписи — Excel для macOS, окно «Параметры страницы», вкладка «Страница» (те же строки, что у нашего окна). --> <label class="form-control"> <span class="label-text text-xs">Размер бумаги:</span> <select data-print-paper class="select select-sm select-bordered"> <option value="1">Письмо US</option> <option value="9" selected>A4</option> <option value="5">Legal</option> </select> </label> <label class="form-control"> <span class="label-text text-xs">Ориентация</span> <select data-print-orientation class="select select-sm select-bordered"> <option value="portrait" selected>книжная</option> <option value="landscape">альбомная</option> </select> </label> <label class="form-control"> <span class="label-text text-xs">Установить:</span> <input data-print-scale type="number" min="10" max="400" value="100" class="input input-sm input-bordered w-24" /> </label> <button data-print-ask class="btn btn-sm btn-primary">Спросить разметку</button> </div>
<div data-print-answer class="text-xs opacity-60">ещё не спрашивали</div> <div data-print-breaks class="text-xs"></div> </section>
<div id="app" class="flex min-h-0 flex-1 flex-col"></div> </div>
<script type="module" src="./src/main.ts"></script> </body></html>Точка входа: здесь лист создаётся и сюда же приходят события.
import { createSheetsCore } from "@f1/preset-sheets-core";import { DEFAULT_PAGE_SETUP } from "@f1/sheets";import { EXECUTOR, transport } from "./book.js";import { hostAsks } from "./host.js";import "./styles.css";
/** * РАЗМЕТКА СТРАНИЦ ЧЕРЕЗ SDK — ЕДИНСТВЕННЫЙ МЕТОД, КОТОРЫЙ СПРАШИВАЕТ. * * `getPageBreaksAsync()` книгу не меняет: ответ — только геометрия. Это то * место, где соблазн посчитать самому больше всего: ширины столбцов и высоты * строк лежат прямо перед глазами. Считать их здесь нельзя — второй расчёт * разойдётся с печатью ровно на ширину одного столбца, и разойдётся МОЛЧА * (инвариант №1). * * ПЕЧАТЬ ЖЕ ДЕЛАЕТ ХОЗЯИН ОКНА. Своего диалога печати продукт не рисует и PDF не * собирает: напечатать лист может только тот, кто умеет нарисовать его * постранично. Кнопки «Печать», которая ничего не печатает, здесь нет — * нарисованная, она была бы ровно тем fake-green, который запрещён инвариантом * №3. Страница показывает то, что ПРАВДА есть: числа ядра, из которых печать и * собирается. * * УМОЛЧАНИЯ НЕ НАШИ: не назвали бумагу — уходит `DEFAULT_PAGE_SETUP` из * `@f1/sheets` (письмо, книжная, 100 %, поля 0,7/0,75″ — те же, что у оболочки * f1). Придумать здесь свои числа значило бы печатать не так, как продукт. */const { locale } = hostAsks();
const paper = document.querySelector<HTMLSelectElement>("[data-print-paper]")!;const orientation = document.querySelector<HTMLSelectElement>("[data-print-orientation]")!;const scale = document.querySelector<HTMLInputElement>("[data-print-scale]")!;const answer = document.querySelector<HTMLElement>("[data-print-answer]")!;const breaks = document.querySelector<HTMLElement>("[data-print-breaks]")!;
const core = createSheetsCore({ mount: document.getElementById("app")!, transport, locale });const sheet = core.f1.getActiveWorkbook().getActiveSheet()!;
function ask(): void { void sheet .getPageBreaksAsync({ pageSetup: { ...DEFAULT_PAGE_SETUP, paperSize: Number(paper.value), orientationLandscape: orientation.value === "landscape", scale: Number(scale.value), }, /* «Разрывов руками не ставили» — это ПУСТОЙ список, а не отсутствие поля: так требует контракт, и молчание он читал бы иначе. */ manualRowBreaks: [], manualColBreaks: [], }) .then((layout) => { if (!layout.ok) { answer.className = "text-xs text-error"; answer.setAttribute("data-print-refusal", layout.code ?? "refused"); answer.textContent = `Разметка не получена: ${layout.reason}${layout.code ? ` (${layout.code})` : ""}`; return; } answer.removeAttribute("data-print-refusal"); answer.className = "text-xs opacity-70"; answer.setAttribute("data-print-pages", String(layout.pageCount)); answer.textContent = `Посчитано ${EXECUTOR}: страниц ${layout.pageCount} ` + `(${layout.pagesAcross} в ширину × ${layout.pagesDown} в высоту) ` + `на занятой области до строки ${layout.usedRowEnd + 1} и столбца ${layout.usedColEnd + 1}.`;
/* Разрыв «перед строкой N» — и вид разрыва тоже слово ядра: `automatic` поставило оно само, `manual` — человек. Смешать их значит показать пользователю его собственный разрыв там, где его нет. */ breaks.textContent = `Разрывы строк: ${layout.rowBreaks.map((line) => `перед ${line.position + 1} (${line.kind})`).join(", ") || "нет"}. ` + `Разрывы столбцов: ${layout.colBreaks.map((line) => `перед ${line.position + 1} (${line.kind})`).join(", ") || "нет"}.`; });}
document.querySelector("[data-print-ask]")!.addEventListener("click", ask);
/* Первый вопрос задаётся сам — но только ПОСЛЕ загрузки: докуда считать, знает ядро (`geometry` его ответа), и без этого честный ответ невозможен. */void core.f1.loadAsync().then(ask);
import.meta.hot?.dispose(() => core.destroy());Книга примера: данные, с которыми лист открывается.
import { createTransport, transportExecutorName, transportSettingFromSearch } from "@f1/contract";
/** * КНИГА-ЗАГЛУШКА — ВМЕСТО НАСТОЯЩЕГО ЯДРА. * * Книга нарочно шире и длиннее одной страницы: на листе в две ячейки разговор о * разрывах не состоится. Занятую область объявляет ФИКСТУРА (`usedRange`), а не * размер загруженного окна: окно — это сколько мы попросили показать, и выдать * его за размер книги значило бы напечатать экран вместо документа. * * ТРАНСПОРТ ВЫБИРАЕТСЯ НАСТРОЙКОЙ: умолчание — заглушка, * `?core=http://127.0.0.1:5507` уводит на живое ядро. Код примера не меняется. */const setting = transportSettingFromSearch(globalThis.location?.search ?? "", { sheets: ["Лист1"], usedRange: { rowEnd: 199, colEnd: 25 }, cellAt: (row, col) => (col === 0 ? `строка ${row + 1}` : `${(row + 1) * (col + 1)}`),});
export const transport = createTransport(setting);
/** * КТО СЧИТАЕТ РАЗМЕТКУ — и почему это написано рядом с транспортом. Ширины * столбцов, высоты строк, поля и масштаб знает ядро; второй расчёт в TypeScript * разошёлся бы с печатью ровно на ширину столбца. Подпись обязана называть * исполнителя правдиво и на заглушке, и на живом ядре. */export const EXECUTOR = transportExecutorName(setting);Сторона хозяина страницы: что приложение отвечает листу и что от него получает.
import { applyLocaleAttributes, resolveLocale, setLocale, type Locale } from "@f1/i18n";
/** * ЧТО ХОЗЯИН СТРАНИЦЫ ДАЁТ ЛИСТУ — тема и язык. * * У встраивающего это уже есть своё: тему он знает из настроек, язык — из * профиля пользователя. Здесь и то и другое читается из адреса * (`?theme=dark&locale=ar`) ровно затем, чтобы на любое состояние можно было * дать ссылку и чтобы его проверял обход: настройка, доступная только щелчком, * недоступна ни ссылке, ни автоматической проверке. * * ПРИЗНАКИ СТАВЯТСЯ НА КОРЕНЬ ДОКУМЕНТА — потому что это делает ХОЗЯИН, а не * лист. Лист читает тему оттуда, где стоит (`:host-context`), а язык получает * СВОЙСТВОМ сборки: два листа на одной странице вправе говорить на разном. * * НАПРАВЛЕНИЕ ПИСЬМА — ИНТЕРФЕЙСА, А НЕ ДОКУМЕНТА. `dir="rtl"` зеркалит хром * страницы; «Лист справа налево» из Excel — свойство КНИГИ, и сетка от * арабского языка интерфейса не переворачивается. Это не пропуск, а решение * (`locale.ts`). */export function hostAsks(): { readonly theme: "light" | "dark"; readonly locale: Locale } { const params = new URLSearchParams(location.search); const theme = params.get("theme") === "dark" ? "dark" : "light"; const locale = resolveLocale(params.get("locale") ?? undefined, "ru");
document.documentElement.setAttribute("data-theme", theme); /* Язык — один на процесс: словарь модульный, и ставит его хозяин страницы. */ setLocale(locale); applyLocaleAttributes(document.documentElement, locale);
return { theme, locale };}Оформление страницы вокруг листа.
/* ЛИСТ СТИЛЕЙ НАБОРА КОМПОНЕНТОВ — ЕДИНСТВЕННАЯ СТРОКА, КОТОРУЮ НЕЛЬЗЯ ЗАБЫТЬ. Собственное оформление лист приносит в теневые корни сам, но утилиты раскладки, которыми он держит свои органы, компилируются ЭТИМ листом. Без него страница не просто выглядит иначе — вкладка ЗАВИСАЕТ (бесконечный цикл замера у сетки без размеров). У потребителя вне репозитория это `@fastoffices/formula-kit` из его node_modules. */@import "@fastoffices/formula-kit"; /* см. примечание выше */html,body { height: 100%; margin: 0;}Из каких пакетов собран пример.
{ "name": "f1-example-page-breaks", "private": true, "type": "module", "description": "Разметка страниц: где ляжет разрыв — считает ядро, а печать делает хост.", "scripts": { "dev": "vite", "build": "vite build" }, "dependencies": { "@f1/preset-sheets-core": "*", "@f1/sheets": "*", "@f1/contract": "*", "@f1/i18n": "*" }, "devDependencies": { "vite": "^7.0.0" }}- Остальные примеры — Примеры F1.
- Правила, на которые ссылается код, — пять правил.
- Из чего собирается лист — Архитектура.
- Что можно вызвать сегодня — справочник Compute Contract.