feat: add local OneNote and ONEPKG reader
This commit is contained in:
177
src/onenote/onepkg/package.ts
Normal file
177
src/onenote/onepkg/package.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
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 { diagnosticFromUnknown, OnePkgError, warning } from './errors.js';
|
||||
import type {
|
||||
CabOpenOptions,
|
||||
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);
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
package: {
|
||||
format: 'onepkg',
|
||||
sourceName,
|
||||
sourceSize: bytes.byteLength,
|
||||
entries: packageEntries,
|
||||
sections: packagedSections,
|
||||
diagnostics,
|
||||
},
|
||||
extractedEntries: extraction.extractedEntries,
|
||||
parsedSections,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user