Files
onenote-tools/src/onenote/onepkg/package.ts

218 lines
6.7 KiB
TypeScript

import type {
OneNotePackageDto,
OneNotePackageEntryDto,
OneNotePackagedSectionDto,
OneNoteSectionDto,
} from '../model/dto.js';
import type { ParserDiagnostic } from '../model/diagnostics.js';
import { checkCancellation } from './cancellation.js';
import { extractCabinet } from './cabinet.js';
import { extractCabinetSet } from './cabinet-set.js';
import { diagnosticFromUnknown, OnePkgError, warning } from './errors.js';
import { buildOneNotePackageTree } from './toc-hierarchy.js';
import type {
CabOpenOptions,
CabExtractionResult,
CabSetPart,
ExtractedCabFile,
OnePkgCancellation,
} from './types.js';
export interface OneNotePackageSectionParseRequest {
sourceName: string;
path: string;
normalizedPath: string;
bytes: Uint8Array;
cancellation?: OnePkgCancellation;
}
export type OneNotePackageSectionParseOutcome =
| {
status: 'parsed';
value: OneNoteSectionDto;
diagnostics?: ParserDiagnostic[];
}
| { status: 'unsupported'; diagnostics?: ParserDiagnostic[] };
/**
* Deliberately independent from the CAB implementation: a `.one` parser is
* injected and receives only one extracted section plus cancellation context.
*/
export type OneNotePackageSectionParser = (
request: OneNotePackageSectionParseRequest
) =>
| OneNotePackageSectionParseOutcome
| Promise<OneNotePackageSectionParseOutcome>;
export interface OneNotePackageOpenOptions extends CabOpenOptions {
sourceName?: string;
parseSection?: OneNotePackageSectionParser;
}
export interface ParsedOneNotePackageSection {
normalizedPath: string;
value: OneNoteSectionDto;
}
export interface OneNotePackageOpenResult {
package: OneNotePackageDto;
/** Extracted in memory only. CAB paths are never written to the filesystem. */
extractedEntries: ExtractedCabFile[];
parsedSections: ParsedOneNotePackageSection[];
}
function entryKind(path: string): OneNotePackageEntryDto['kind'] {
const lower = path.toLowerCase();
if (lower.endsWith('.one')) return 'section';
if (lower.endsWith('.onetoc2')) return 'table-of-contents';
return 'other';
}
function compressionLabel(entry: ExtractedCabFile): string {
return entry.compression.kind === 'lzx'
? `${entry.compression.label} 2^${entry.compression.windowBits}`
: entry.compression.label;
}
function sectionDisplayName(path: string): string {
const fileName = path.slice(path.lastIndexOf('/') + 1);
return fileName.toLowerCase().endsWith('.one')
? fileName.slice(0, -4)
: fileName;
}
export async function openOneNotePackage(
bytes: Uint8Array,
options: OneNotePackageOpenOptions = {}
): Promise<OneNotePackageOpenResult> {
const sourceName = options.sourceName ?? 'notebook.onepkg';
const extraction = await extractCabinet(bytes, options);
return buildOpenedPackage(extraction, sourceName, bytes.byteLength, options);
}
export async function openOneNotePackageSet(
parts: CabSetPart[],
options: OneNotePackageOpenOptions = {}
): Promise<OneNotePackageOpenResult> {
const extraction = await extractCabinetSet(parts, options);
const sourceName =
options.sourceName ??
parts.find((part) => part.fileName.toLowerCase().endsWith('.onepkg'))
?.fileName ??
extraction.parts[0]?.fileName ??
'notebook.onepkg';
return buildOpenedPackage(
extraction,
sourceName,
parts.reduce((sum, part) => sum + part.bytes.byteLength, 0),
options
);
}
async function buildOpenedPackage(
extraction: CabExtractionResult,
sourceName: string,
sourceSize: number,
options: OneNotePackageOpenOptions
): Promise<OneNotePackageOpenResult> {
const diagnostics = [...extraction.diagnostics];
const packageEntries: OneNotePackageEntryDto[] =
extraction.extractedEntries.map((entry) => ({
path: entry.path,
normalizedPath: entry.normalizedPath,
kind: entryKind(entry.normalizedPath),
uncompressedSize: entry.uncompressedSize,
compressionMethod: compressionLabel(entry),
parseStatus: 'not-requested',
}));
const parsedSections: ParsedOneNotePackageSection[] = [];
const packagedSections: OneNotePackagedSectionDto[] = [];
for (let index = 0; index < extraction.extractedEntries.length; index += 1) {
const entry = extraction.extractedEntries[index]!;
const packageEntry = packageEntries[index]!;
if (packageEntry.kind !== 'section') continue;
const sectionDiagnostics: ParserDiagnostic[] = [];
let parsedSection: OneNoteSectionDto | undefined;
if (options.parseSection !== undefined) {
checkCancellation(options.cancellation);
try {
const outcome = await options.parseSection({
sourceName: entry.normalizedPath,
path: entry.path,
normalizedPath: entry.normalizedPath,
bytes: entry.bytes,
cancellation: options.cancellation,
});
if (outcome.status === 'parsed') {
packageEntry.parseStatus = 'parsed';
parsedSection = outcome.value;
parsedSections.push({
normalizedPath: entry.normalizedPath,
value: outcome.value,
});
} else {
packageEntry.parseStatus = 'unsupported';
if (outcome.diagnostics === undefined) {
sectionDiagnostics.push(
warning(
'one-section-unsupported',
`Section parser does not support ${entry.normalizedPath}.`,
{ structure: entry.normalizedPath }
)
);
}
}
sectionDiagnostics.push(...(outcome.diagnostics ?? []));
} catch (error) {
if (
error instanceof OnePkgError &&
error.diagnostic.code === 'onepkg-cancelled'
) {
throw error;
}
packageEntry.parseStatus = 'failed';
sectionDiagnostics.push({
...diagnosticFromUnknown(
error,
'one-section-parse-failed',
`Could not parse ${entry.normalizedPath}`,
entry.normalizedPath
),
recoverable: true,
});
}
}
packagedSections.push({
id: entry.normalizedPath,
path: entry.normalizedPath,
displayName: sectionDisplayName(entry.normalizedPath),
parseStatus: packageEntry.parseStatus,
section: parsedSection,
diagnostics: sectionDiagnostics,
});
}
const treeResult = await buildOneNotePackageTree(
extraction.extractedEntries,
packagedSections,
options
);
diagnostics.push(...treeResult.diagnostics);
return {
package: {
format: 'onepkg',
sourceName,
sourceSize,
entries: packageEntries,
sections: packagedSections,
tree: treeResult.tree,
diagnostics,
},
extractedEntries: extraction.extractedEntries,
parsedSections,
};
}