Перетащите книгу сюда
Чужой документ открывается байтами: оболочка доносит файл, разбор остаётся за швом контракта, а отказ приходит словами того, кто открывал — на витрине это заглушка, она берёт csv, tsv и txt, а на xlsx честно отказывает.

В рамке выше работает тот самый проект, чей код лежит ниже, — собранный, но не переписанный: те же файлы, только пропущенные через сборку. Считает в нём не ядро, а дублёр транспорта, поэтому числа в ячейках ничего не значат: смотреть нужно на код.
Страница, которую открывает браузер.
<!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="open-bytes" 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"> Документ хозяина открывается БАЙТАМИ: оболочка читает файл и отдаёт его за шов контракта вместе с именем — расширение выбирает разбор, файловой системы ядро не касается вовсе. Своего разбора здесь нет ни строчки. Этот проект собран на ЗАГЛУШКЕ ядра: она разбирает разделённый текст (csv, tsv, txt), а на xlsx отказывает — своими словами и своим кодом, а не пустой книгой. </div> </header>
<!-- РАМКА ДЛЯ БРОСКА — СВОЯ, а не подписка на документ: подписка забирала бы бросок у всей чужой страницы, на которой стоит лист. --> <section data-open-drop class="m-4 flex flex-col items-center gap-3 rounded-card border-2 border-dashed border-base-300 px-4 py-6 text-center" > <div class="text-sm">Бросьте файл книги в эту рамку</div> <div class="text-xs opacity-60">или выберите его обычным диалогом</div> <input type="file" data-open-picker class="file-input file-input-sm file-input-bordered w-72" /> <!-- Расписка о последней попытке: до первой здесь честное «ещё ничего не приносили», а не пустая книга, притворившаяся открытой. --> <div id="answer"> <div class="text-xs opacity-50" data-open-idle>ещё ничего не приносили</div> </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 { documentBytesToBase64 } from "@f1/sheets";import { EXECUTOR, transport } from "./book.js";import { hostAsks } from "./host.js";import "./styles.css";
/** * ПЕРЕТАЩИТЕ КНИГУ СЮДА — ВТОРАЯ ПОЛОВИНА ДОРОГИ. * * Первая работала давно: документ ОТДАЁТСЯ хозяину байтами * (`save_workbook_subset_to_bytes` плюс расписка `HostSink`). Принять чужой * документ было нечем — операция `open_workbook_subset_from_bytes` в контракте * была, а звали её ниоткуда. * * ЧТО ЗДЕСЬ ДЕЛАЕТ ОБОЛОЧКА — И ЧТО НЕ ДЕЛАЕТ. Она читает файл в БАЙТЫ и * отдаёт их за шов контракта вместе с именем файла. Всё. Ни разбора xlsx, ни * разбора csv, ни взгляда в заголовок файла ради выбора дороги: «что это за * документ» — семантика, то есть ядро (инвариант №1). Расширение имени выбирает * разбор ТАМ, и сделано это ровно так же, как при открытии по пути. * * ПОЧЕМУ ПРИМЕР ЧЕСТЕН, КОГДА ОТКАЗЫВАЕТ. Этот проект собран на ЗАГЛУШКЕ ядра * (`book.ts`), и та разбирает только разделённый текст — то же, что сама * собирает при сохранении. Пакет xlsx она не разберёт и не притворится, что * разобрала: страница показывает ЕЁ отказ, её словами и с её кодом. Пустая * книга вместо отказа выглядела бы как «открылось» — это и есть тот fake-green, * ради запрета которого заведён инвариант №3. С настоящим ядром на этой же * дороге откроется и xlsx, и ods, и xlsb: код оболочки не изменится ни строкой, * а имя исполнителя в расписке — ровно одной, в `book.ts`. * * ДВА ВХОДА, потому что их два у человека: файл БРОСАЮТ на страницу или выбирают * диалогом. Дорога после этого одна — байты и имя, и объявлена она один раз. */
const { locale } = hostAsks();
const zone = document.querySelector<HTMLElement>("[data-open-drop]")!;const picker = document.querySelector<HTMLInputElement>("[data-open-picker]")!;const answer = document.getElementById("answer")!;
const core = createSheetsCore({ mount: document.getElementById("app")!, transport, locale });
/** * ОДНА ДОРОГА НА ОБА ВХОДА: и брошенный файл, и выбранный диалогом приходят * сюда. Оболочка делает ровно два шага — читает байты и зовёт SDK. */async function take(file: File): Promise<void> { const bytes = new Uint8Array(await file.arrayBuffer()); const workbook = core.f1.getActiveWorkbook(); const said = await workbook.openFromBytesAsync(file.name, documentBytesToBase64(bytes)); answer.innerHTML = said.ok ? `<div class="alert alert-success py-2 text-start text-xs" data-open-answer><span>Открыто ${EXECUTOR}: <b>${file.name}</b> (${bytes.length} байт) — лист «${said.sheetName}», листов ${said.sheetCount}, занято ${said.usedRange}.</span></div>` : `<div class="alert alert-error py-2 text-start text-xs" data-open-refusal><span><b>${file.name}</b> не открыт: ${said.reason}${said.code ? ` (${said.code})` : ""}</span></div>`;}
/* ПОДПИСКА — НА СВОЮ РАМКУ, а не на документ: подписка на документ забирала бы бросок у всей чужой страницы, на которой стоит лист. */zone.addEventListener("dragover", (event) => { /* Без этого браузер откроет файл вместо страницы — то есть уронит пример на самом жесте, ради которого он и написан. */ event.preventDefault(); zone.setAttribute("data-open-over", "true"); zone.classList.add("border-primary", "bg-primary/5");});zone.addEventListener("dragleave", () => { zone.removeAttribute("data-open-over"); zone.classList.remove("border-primary", "bg-primary/5");});zone.addEventListener("drop", (event) => { event.preventDefault(); zone.removeAttribute("data-open-over"); zone.classList.remove("border-primary", "bg-primary/5"); const file = (event as DragEvent).dataTransfer?.files?.[0]; if (file) void take(file);});
picker.addEventListener("change", () => { const file = picker.files?.[0]; if (file) void take(file);});
import.meta.hot?.dispose(() => core.destroy());Книга примера: данные, с которыми лист открывается.
import { createTransport, transportExecutorName, transportSettingFromSearch } from "@f1/contract";
/** * КНИГА-ЗАГЛУШКА — ВМЕСТО НАСТОЯЩЕГО ЯДРА. * * Транспорт контракта — единственное место, где лист разговаривает с ядром * (инвариант №1). В продукте здесь стоит `wasm` или мост хоста, и меняется * РОВНО эта строка. *//** * ТРАНСПОРТ ВЫБИРАЕТСЯ НАСТРОЙКОЙ, А НЕ ВПАЯН — проверено на живом ядре. * Умолчание — заглушка; `?core=http://127.0.0.1:5507` уводит на живое ядро * через мост `@f1/core-host`. Виджеты об этом не знают: выбор живёт здесь. */const setting = transportSettingFromSearch(globalThis.location?.search ?? "", { cellAt: (row, col) => (row === 0 && col === 0 ? "перетащите книгу сюда" : undefined),});export const transport = createTransport(setting);
/** * КТО НА САМОМ ДЕЛЕ ОТКРЫЛ ДОКУМЕНТ — и почему это написано рядом с транспортом. * * Расписка успеха говорила «Открыто ядром», а за ней стояла строка выше: * `createMockTransport`, то есть заглушка. Настоящее ядро в деле не участвует * вовсе, и приписывать ему чужую работу — тот же fake-green, ради запрета * которого заведён инвариант №3, только в словах, а не в кнопке. Проверено * замером: на книге в полмиллиона строк эта самая «ядром» открытая книга * показывала `A1:AA500000`, тогда как ядро отвечает `A1:O500000`. * * Имя исполнителя стоит ВПЛОТНУЮ к строке, которая его выбирает: заменят * транспорт — правка на соседней строке, а не в другом конце файла. Правдивость * подписи стережёт отдельная проверка витрины. */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-open-bytes", "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.