Files
onenote-tools/src/onenote/export/zip.ts

272 lines
7.8 KiB
TypeScript

// 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;
}