/** * Parser for .rels (Relationship) XML files in OOXML packages. * These files map relationship IDs (rId1, rId2, ...) to targets. */ import { parseXml } from './xml-parser' export interface RelEntry { type: string target: string targetMode?: string } /** * Parse a .rels XML string into a Map of relationship ID -> RelEntry. * * Example input: * ```xml * * * * ``` */ export function parseRels(xmlString: string): Map { const result = new Map() if (!xmlString) return result const root = parseXml(xmlString) if (!root.exists()) return result const relationships = root.children('Relationship') for (const rel of relationships) { const id = rel.attr('Id') const type = rel.attr('Type') const target = rel.attr('Target') const targetMode = rel.attr('TargetMode') if (id && type !== undefined && target !== undefined) { result.set(id, { type, target, targetMode }) } } return result } /** * Resolve a relative target path against a base path. * * Examples: * resolveRelTarget('ppt/slides', '../slideLayouts/slideLayout1.xml') * → 'ppt/slideLayouts/slideLayout1.xml' * * resolveRelTarget('ppt/slides', 'media/image1.png') * → 'ppt/slides/media/image1.png' * * resolveRelTarget('ppt', 'slides/slide1.xml') * → 'ppt/slides/slide1.xml' */ export function resolveRelTarget(basePath: string, target: string): string { // Absolute targets (start with /) are returned as-is (strip leading /) if (target.startsWith('/')) { return target.slice(1) } // Split the base path into segments const baseParts = basePath.replace(/\\/g, '/').split('/').filter(Boolean) const targetParts = target.replace(/\\/g, '/').split('/').filter(Boolean) // Walk through target parts, resolving '..' by popping base parts const resolved = [...baseParts] for (const part of targetParts) { if (part === '..') { resolved.pop() } else if (part !== '.') { resolved.push(part) } } return resolved.join('/') }