272 lines
7.8 KiB
TypeScript
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;
|
|
}
|