{"version":3,"file":"soffice.mjs","names":[],"sources":["../../../../src/batteries/media/engines/soffice.ts"],"sourcesContent":["/**\n * The LibreOffice-backed {@link @nhtio/adk/batteries/media/contracts!MediaEngine}: document,\n * spreadsheet, and presentation conversion via the `soffice` binary.\n *\n * @module @nhtio/adk/batteries/media/engines/soffice\n *\n * @remarks\n * Document conversion is the one capability in the media battery with no mature\n * cross-environment equivalent, so this engine is binary-backed by design. It composes the\n * two BYO runtime contracts: a {@link @nhtio/adk/batteries/media/contracts!BinaryExecutor}\n * runs the invocation (bundled: `execa_executor`) and a\n * {@link @nhtio/adk/batteries/media/contracts!ScratchWorkspace} exchanges bytes with it\n * (bundled: `fs_workspace`). The executor and workspace must agree on path visibility —\n * that pairing is the consumer's composition decision.\n *\n * One engine, one capability kind: the conversion matrix is declared as convert capability\n * groups (each format silo converts within itself plus to PDF/HTML; PDF converts to\n * html/txt/docx/odt). The spreadsheet group covers ODS/legacy-xls to xlsx — what used to be\n * a separate \"normalize\" engine is just an edge in the matrix.\n */\n\nimport { E_INVALID_MEDIA_PIPELINE_CONFIG } from '../exceptions'\nimport { EMPTY_MIME, implementsBinaryExecutor, implementsScratchWorkspace } from '../contracts'\nimport type {\n  MediaEngine,\n  ConvertCapability,\n  ConvertRequest,\n  ConvertResult,\n  BinaryExecutor,\n  ScratchWorkspaceFactory,\n} from '../contracts'\n\n/** Options for {@link sofficeEngine}. */\nexport interface SofficeEngineOptions {\n  /** Path (or resolvable name) of the soffice binary. */\n  path: string\n  /** Runs the soffice invocation. Required — no platform default. */\n  executor: BinaryExecutor\n  /** Mints a scratch dir per invocation whose paths the executor can open. Required. */\n  workspace: ScratchWorkspaceFactory\n  /** Per-invocation timeout. Default 120000. */\n  timeoutMs?: number\n}\n\nconst MIME_BY_TARGET: Record<string, string> = {\n  pdf: 'application/pdf',\n  html: 'text/html',\n  txt: 'text/plain',\n  md: 'text/markdown',\n  csv: 'text/csv',\n  json: 'application/json',\n  docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n  doc: 'application/msword',\n  rtf: 'application/rtf',\n  odt: 'application/vnd.oasis.opendocument.text',\n  xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n  xls: 'application/vnd.ms-excel',\n  ods: 'application/vnd.oasis.opendocument.spreadsheet',\n  pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n  ppt: 'application/vnd.ms-powerpoint',\n  odp: 'application/vnd.oasis.opendocument.presentation',\n}\n\nconst EXT_BY_MIME: Record<string, string> = {\n  'application/pdf': 'pdf',\n  'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',\n  'application/msword': 'doc',\n  'application/rtf': 'rtf',\n  'text/rtf': 'rtf',\n  'application/vnd.oasis.opendocument.text': 'odt',\n  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',\n  'application/vnd.ms-excel': 'xls',\n  'application/vnd.oasis.opendocument.spreadsheet': 'ods',\n  'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',\n  'application/vnd.ms-powerpoint': 'ppt',\n  'application/vnd.oasis.opendocument.presentation': 'odp',\n  'text/plain': 'txt',\n  'text/html': 'html',\n  'text/csv': 'csv',\n}\n\n/** Server-parity conversion matrix: same-silo targets plus pdf/html. */\nconst TARGETS_BY_EXT: Record<string, readonly string[]> = {\n  docx: ['pdf', 'html', 'txt', 'md', 'odt', 'doc', 'rtf'],\n  odt: ['pdf', 'html', 'txt', 'md', 'docx', 'doc', 'rtf'],\n  doc: ['pdf', 'html', 'txt', 'md', 'docx', 'odt', 'rtf'],\n  rtf: ['pdf', 'html', 'txt', 'md', 'docx', 'odt', 'doc'],\n  xlsx: ['pdf', 'html', 'csv', 'json', 'ods', 'xls'],\n  ods: ['pdf', 'html', 'csv', 'json', 'xlsx', 'xls'],\n  xls: ['pdf', 'html', 'csv', 'json', 'xlsx', 'ods'],\n  pptx: ['pdf', 'html', 'odp', 'ppt'],\n  odp: ['pdf', 'html', 'pptx', 'ppt'],\n  ppt: ['pdf', 'html', 'pptx', 'odp'],\n  pdf: ['html', 'txt', 'docx', 'odt'],\n}\n\n/** soffice convert-to filter token per target (md goes via txt; json via csv). */\nconst CONVERT_TOKEN: Record<string, string> = {\n  md: 'txt:Text',\n  json: 'csv',\n}\n\n/**\n * Seed extension per generation target: which silo's empty file to materialize when the\n * source is {@link EMPTY_MIME}. LibreOffice treats a zero-byte input as an empty document of\n * the extension's type (verified across all three silos, both directions, on 25.x — pinned by\n * a binary-gated spec), so generation needs no templates at all. The seed is always a\n * DIFFERENT extension than the target within the same silo, because soffice won't \"convert\"\n * docx to docx.\n */\nconst SEED_EXT_FOR_TARGET: Record<string, string> = {\n  docx: 'odt',\n  odt: 'docx',\n  doc: 'docx',\n  rtf: 'docx',\n  pdf: 'docx',\n  html: 'docx',\n  txt: 'docx',\n  md: 'docx',\n  xlsx: 'ods',\n  ods: 'xlsx',\n  xls: 'xlsx',\n  csv: 'xlsx',\n  json: 'xlsx',\n  pptx: 'odp',\n  odp: 'pptx',\n  ppt: 'pptx',\n}\n\n/** Every format this engine can generate from {@link EMPTY_MIME}. */\nconst GENERATION_TARGETS: readonly string[] = Object.keys(SEED_EXT_FOR_TARGET)\n\nconst validateOptions = (options: SofficeEngineOptions, name: string): void => {\n  if (typeof options?.path !== 'string' || options.path.length === 0) {\n    throw new E_INVALID_MEDIA_PIPELINE_CONFIG([`${name} requires the soffice binary path`])\n  }\n  if (!implementsBinaryExecutor(options.executor)) {\n    throw new E_INVALID_MEDIA_PIPELINE_CONFIG([\n      `${name} requires an executor implementing the BinaryExecutor contract`,\n    ])\n  }\n  if (typeof options.workspace !== 'function') {\n    throw new E_INVALID_MEDIA_PIPELINE_CONFIG([\n      `${name} requires a workspace factory (e.g. fsScratchWorkspace({ root }))`,\n    ])\n  }\n}\n\nconst runSoffice = async (\n  options: SofficeEngineOptions,\n  bytes: Uint8Array,\n  inputExt: string,\n  convertTo: string,\n  outputExt: string,\n  signal?: AbortSignal\n): Promise<Uint8Array> => {\n  const workspace = await options.workspace()\n  if (!implementsScratchWorkspace(workspace)) {\n    throw new E_INVALID_MEDIA_PIPELINE_CONFIG([\n      'the workspace factory minted a value that does not implement ScratchWorkspace',\n    ])\n  }\n  try {\n    const inputPath = await workspace.materialize(bytes, `input.${inputExt}`)\n    const result = await options.executor.exec({\n      cmd: options.path,\n      args: [\n        '--headless',\n        '--nologo',\n        '--nodefault',\n        '--norestore',\n        '--nolockcheck',\n        '--convert-to',\n        convertTo,\n        '--outdir',\n        workspace.dir(),\n        inputPath,\n      ],\n      timeoutMs: options.timeoutMs ?? 120_000,\n      signal,\n    })\n    if (result.failed) {\n      const detail = result.stderr || result.stdout || `exit code ${result.exitCode}`\n      throw new Error(`LibreOffice conversion failed: ${detail}`)\n    }\n    const files = await workspace.list()\n    const produced = files.find(\n      (f) => f.toLowerCase().endsWith(`.${outputExt}`) && f !== `input.${inputExt}`\n    )\n    if (!produced) {\n      throw new Error(\n        `LibreOffice reported success but produced no .${outputExt} output (files: ${files.join(', ')})`\n      )\n    }\n    return workspace.read(`${workspace.dir()}/${produced}`)\n  } finally {\n    await workspace.dispose()\n  }\n}\n\n/** The MIME types of one extension group, for capability `from` declarations. */\nconst MIMES_OF = (exts: readonly string[]): string[] =>\n  Object.entries(EXT_BY_MIME)\n    .filter(([, ext]) => exts.includes(ext))\n    .map(([mime]) => mime)\n\n/**\n * Construct the LibreOffice engine.\n *\n * @param options - Binary path, executor, workspace factory, timeout.\n * @returns The engine, declaring one convert capability group per format silo.\n */\nexport const sofficeEngine = (options: SofficeEngineOptions): MediaEngine => {\n  validateOptions(options, 'sofficeEngine')\n\n  const convert = async (request: ConvertRequest): Promise<ConvertResult> => {\n    const mime = request.mimeType.toLowerCase().split(';')[0].trim()\n    // Generation: a zero-byte seed file in the target's silo converts as an empty document.\n    const inputExt = mime === EMPTY_MIME ? SEED_EXT_FOR_TARGET[request.to] : EXT_BY_MIME[mime]\n    if (!inputExt) throw new Error(`unsupported input MIME for conversion: ${request.mimeType}`)\n    const targets = mime === EMPTY_MIME ? GENERATION_TARGETS : (TARGETS_BY_EXT[inputExt] ?? [])\n    if (!targets.includes(request.to)) {\n      throw new Error(\n        `cannot convert ${inputExt} to ${request.to}; supported: ${targets.join(', ')}`\n      )\n    }\n    const convertTo = CONVERT_TOKEN[request.to] ?? request.to\n    const outputExt = request.to === 'md' ? 'txt' : request.to === 'json' ? 'csv' : request.to\n    let bytes = await runSoffice(\n      options,\n      request.bytes,\n      inputExt,\n      convertTo,\n      outputExt,\n      request.signal\n    )\n    let mimeType = MIME_BY_TARGET[request.to] ?? 'application/octet-stream'\n    if (request.to === 'json') {\n      // soffice emits CSV; lower to a JSON array-of-arrays for predictable structure.\n      const text = new TextDecoder().decode(bytes)\n      const rows = text\n        .split(/\\r?\\n/)\n        .filter((line) => line.length > 0)\n        .map((line) => line.split(','))\n      bytes = new TextEncoder().encode(JSON.stringify(rows))\n      mimeType = MIME_BY_TARGET.json\n    }\n    return { outputs: [{ bytes, mimeType }] }\n  }\n\n  // One capability group per uniform from×to block of the matrix.\n  const group = (exts: readonly string[], to: readonly string[]): ConvertCapability => ({\n    from: MIMES_OF(exts),\n    to,\n    convert,\n  })\n\n  return {\n    id: 'soffice',\n    converts: [\n      group(['docx'], TARGETS_BY_EXT.docx),\n      group(['odt'], TARGETS_BY_EXT.odt),\n      group(['doc'], TARGETS_BY_EXT.doc),\n      group(['rtf'], TARGETS_BY_EXT.rtf),\n      group(['xlsx'], TARGETS_BY_EXT.xlsx),\n      group(['ods'], TARGETS_BY_EXT.ods),\n      group(['xls'], TARGETS_BY_EXT.xls),\n      group(['pptx'], TARGETS_BY_EXT.pptx),\n      group(['odp'], TARGETS_BY_EXT.odp),\n      group(['ppt'], TARGETS_BY_EXT.ppt),\n      group(['pdf'], TARGETS_BY_EXT.pdf),\n      // Generation: zero-byte seed in the target's silo (see SEED_EXT_FOR_TARGET).\n      { from: [EMPTY_MIME], to: GENERATION_TARGETS, convert },\n    ],\n  }\n}\n"],"mappings":"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"}