218 lines
6.7 KiB
TypeScript
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,
|
|
};
|
|
}
|