// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. // SPDX-License-Identifier: MPL-2.0 import { strToU8, zipSync, type Zippable } from 'fflate'; import { forceFilenameExtension, sanitizeExportFilename } from './naming.js'; import { createOneNoteExportPlan, enforceOutputByteLength, utf8ByteLength, type OneNoteExportPlan, } from './plan.js'; import { serializeHtmlWithPlan, serializeMarkdownWithPlan, serializePlainTextWithPlan, serializeStructuredJsonWithPlan, } from './serializers.js'; import { OneNoteExportError, type OneNoteExportOptions, type OneNoteExportSnapshot, } from './types.js'; export interface OneNoteStaticNotebookManifest { schemaVersion: number; exportKind: 'onenote-tools-static-notebook'; app: OneNoteExportSnapshot['app']; parser: OneNoteExportSnapshot['parser']; supportLevel: OneNoteExportSnapshot['supportLevel']; supportNotes: string[]; source: OneNoteExportSnapshot['source']; notebookName: string; files: { viewer: 'index.html'; structuredData: 'notebook.json'; plainText: 'notebook.txt'; markdown: 'notebook.md'; warnings: 'warnings.json'; }; counts: { sections: number; pages: number; blocks: number; resources: number; diagnostics: number; exportWarnings: number; unsupportedContent: number; }; sections: Array<{ id: string; name: string; sourcePath?: string; pageCount: number; }>; resources: Array<{ sectionId: string; id: string; kind: 'image' | 'attachment'; path: string; filename: string; mediaType: string; size: number; }>; } export interface OneNoteExportWarningsManifest { schemaVersion: number; app: OneNoteExportSnapshot['app']; parser: OneNoteExportSnapshot['parser']; supportLevel: OneNoteExportSnapshot['supportLevel']; source: OneNoteExportSnapshot['source']; diagnostics: OneNoteExportPlan['diagnostics']; exportWarnings: OneNoteExportPlan['warnings']; unsupportedContent: OneNoteExportPlan['unsupportedContent']; } export interface OneNoteStaticNotebookZipArtifact { filename: string; mediaType: 'application/zip'; bytes: Uint8Array; manifest: OneNoteStaticNotebookManifest; } /** * Create an application-owned static export. This never writes or claims to * write a .one/.onepkg file, and it never interprets source HTML or SVG. */ export function createOneNoteStaticNotebookZip( snapshot: OneNoteExportSnapshot, options: OneNoteExportOptions = {} ): OneNoteStaticNotebookZipArtifact { const plan = createOneNoteExportPlan(snapshot, options); enforceProjectedStaticNotebookSize(plan); const manifest = createStaticManifest(snapshot, plan); const warnings: OneNoteExportWarningsManifest = { schemaVersion: snapshot.schemaVersion, app: snapshot.app, parser: snapshot.parser, supportLevel: snapshot.supportLevel, source: snapshot.source, diagnostics: plan.diagnostics, exportWarnings: plan.warnings, unsupportedContent: plan.unsupportedContent, }; const files: Zippable = Object.create(null) as Zippable; const members: Array<{ path: string; byteLength: number }> = []; let memberBytes = 0; addTextMember('index.html', serializeHtmlWithPlan(snapshot, plan, true)); addTextMember( 'notebook.json', serializeStructuredJsonWithPlan(snapshot, plan) ); addTextMember('notebook.txt', serializePlainTextWithPlan(snapshot, plan)); addTextMember('notebook.md', serializeMarkdownWithPlan(snapshot, plan, true)); addTextMember('manifest.json', jsonText(manifest)); addTextMember('warnings.json', jsonText(warnings)); for (const resource of plan.resources) { addBinaryMember(resource.path, resource.bytes); } enforceOutputByteLength( plan, projectedZipByteLength(members), 'Projected static notebook ZIP' ); const bytes = zipSync(files, { level: 6, mtime: new Date(1980, 0, 1, 0, 0, 0), os: 3, attrs: 0o644 << 16, }); enforceOutputByteLength(plan, bytes.byteLength, 'Static notebook ZIP'); const safeName = sanitizeExportFilename( snapshot.notebookName, 'onenote-export' ); return { filename: forceFilenameExtension(safeName, 'zip'), mediaType: 'application/zip', bytes, manifest, }; function addTextMember(path: string, value: string): void { ensureMemberCapacity(utf8ByteLength(value)); const bytes = strToU8(value); addMember(path, bytes, bytes.byteLength); } function addBinaryMember(path: string, value: Uint8Array): void { ensureMemberCapacity(value.byteLength); addMember(path, value, value.byteLength); } function addMember( path: string, value: Uint8Array, byteLength: number ): void { ensureMemberCapacity(byteLength); memberBytes += byteLength; members.push({ path, byteLength }); files[path] = value; } function ensureMemberCapacity(byteLength: number): void { if (byteLength > plan.limits.maxOutputBytes - memberBytes) { throw new OneNoteExportError( 'limit-exceeded', `Static notebook members exceed ${plan.limits.maxOutputBytes} bytes` ); } } } function createStaticManifest( snapshot: OneNoteExportSnapshot, plan: OneNoteExportPlan ): OneNoteStaticNotebookManifest { return { schemaVersion: snapshot.schemaVersion, exportKind: 'onenote-tools-static-notebook', app: snapshot.app, parser: snapshot.parser, supportLevel: snapshot.supportLevel, supportNotes: snapshot.supportNotes, source: snapshot.source, notebookName: snapshot.notebookName, files: { viewer: 'index.html', structuredData: 'notebook.json', plainText: 'notebook.txt', markdown: 'notebook.md', warnings: 'warnings.json', }, counts: { sections: snapshot.sections.length, pages: plan.pageCount, blocks: plan.blockCount, resources: plan.resources.length, diagnostics: plan.diagnostics.length, exportWarnings: plan.warnings.length, unsupportedContent: plan.unsupportedContent.length, }, sections: snapshot.sections.map((section) => ({ id: section.id, name: section.name, ...(section.sourcePath ? { sourcePath: section.sourcePath } : {}), pageCount: section.pages.length, })), resources: plan.resources.map((resource) => ({ sectionId: resource.sectionId, id: resource.id, kind: resource.kind, path: resource.path, filename: resource.filename, mediaType: resource.mediaType, size: resource.size, })), }; } function jsonText(value: unknown): string { return `${JSON.stringify(value, null, 2)}\n`; } function enforceProjectedStaticNotebookSize(plan: OneNoteExportPlan): void { const available = plan.limits.maxOutputBytes - plan.totalResourceBytes; if (available < 0 || plan.projectedOutputBytes > Math.floor(available / 6)) { throw new OneNoteExportError( 'limit-exceeded', `Projected static notebook members exceed ${plan.limits.maxOutputBytes} bytes` ); } } function projectedZipByteLength( members: readonly { path: string; byteLength: number }[] ): number { let result = 22; for (const member of members) { const pathBytes = utf8ByteLength(member.path); const deflateOverhead = Math.ceil(member.byteLength / 16_384) * 16 + 128; result = safeByteSum( result, member.byteLength, deflateOverhead, 76, pathBytes * 2 ); } return result; } function safeByteSum(...values: number[]): number { let total = 0; for (const value of values) { if (!Number.isSafeInteger(value) || value < 0) { return Number.POSITIVE_INFINITY; } if (total > Number.MAX_SAFE_INTEGER - value) { return Number.POSITIVE_INFINITY; } total += value; } return total; }