import type { ProcessGraph, ProcessGraphNode, ProcessGraphFlow, BpmnNodeType, BpmnElementId, } from './types' // Mapeo de tags XML BPMN → nuestro tipo normalizado const TAG_TO_TYPE: Record = { startevent: 'startEvent', endevent: 'endEvent', task: 'task', usertask: 'userTask', servicetask: 'serviceTask', scripttask: 'scriptTask', manualtask: 'manualTask', businessruletask: 'businessRuleTask', subprocess: 'subprocess', exclusivegateway: 'exclusiveGateway', parallelgateway: 'parallelGateway', inclusivegateway: 'inclusiveGateway', eventbasedgateway: 'eventBasedGateway', intermediatethrowevent: 'intermediateThrowEvent', intermediatecatchevent: 'intermediateCatchEvent', } function normalizeLocalName(tagName: string): string { // Eliminar namespace prefix (bpmn:Task → task) const local = tagName.includes(':') ? tagName.split(':')[1] : tagName return local.toLowerCase() } function getAttr(el: Element, name: string): string { return el.getAttribute(name) ?? '' } function parseFlowRefs(el: Element, tagName: string): string[] { return Array.from(el.getElementsByTagNameNS('*', tagName)).map((ref) => ref.textContent?.trim() ?? '') } export class BpmnParseError extends Error { constructor(message: string) { super(message) this.name = 'BpmnParseError' } } export function parseBpmnXml(xml: string): ProcessGraph { let doc: Document try { const parser = new DOMParser() doc = parser.parseFromString(xml, 'application/xml') } catch { throw new BpmnParseError('El archivo no es un XML válido') } const parserError = doc.querySelector('parsererror') if (parserError) { throw new BpmnParseError('XML malformado: ' + parserError.textContent?.slice(0, 200)) } const nodes = new Map() const flows = new Map() const startEventIds: BpmnElementId[] = [] // Buscar TODOS los procesos del documento. Las colaboraciones tienen múltiples // como hijos directos de , uno por pool. const definitions = doc.documentElement const processEls = Array.from(definitions.children).filter( (el) => normalizeLocalName(el.tagName) === 'process' ) if (processEls.length === 0) { throw new BpmnParseError('No se encontró un elemento en el BPMN') } // Parsear cada proceso y acumular nodos y flujos en mapas compartidos for (const processEl of processEls) { for (const child of Array.from(processEl.children)) { const local = normalizeLocalName(child.tagName) const type: BpmnNodeType = TAG_TO_TYPE[local] ?? 'unknown' if (local === 'sequenceflow') { const flow: ProcessGraphFlow = { id: getAttr(child, 'id'), sourceRef: getAttr(child, 'sourceRef'), targetRef: getAttr(child, 'targetRef'), name: getAttr(child, 'name') || undefined, } if (flow.id) flows.set(flow.id, flow) continue } if (type === 'unknown') continue const id = getAttr(child, 'id') if (!id) continue const node: ProcessGraphNode = { id, name: getAttr(child, 'name'), type, outgoing: parseFlowRefs(child, 'outgoing'), incoming: parseFlowRefs(child, 'incoming'), } nodes.set(id, node) if (type === 'startEvent') startEventIds.push(id) } } if (nodes.size === 0) { throw new BpmnParseError('El proceso no contiene elementos reconocibles') } // Pools de soporte (SAP, bandeja manual, etc.) pueden no tener StartEvent propio — // exigimos al menos uno entre todos los pools del diagrama. if (startEventIds.length === 0) { throw new BpmnParseError('El proceso no tiene evento de inicio (StartEvent)') } return { nodes, flows, startEventIds } } // Extrae solo los IDs y nombres de los task/subprocess — util para inicializar actividades export function extractActivityElements( graph: ProcessGraph ): Array<{ bpmnElementId: BpmnElementId; name: string; type: 'task' | 'subprocess' }> { const result = [] for (const node of graph.nodes.values()) { if ( ['task', 'userTask', 'serviceTask', 'scriptTask', 'manualTask', 'businessRuleTask'].includes(node.type) ) { result.push({ bpmnElementId: node.id, name: node.name || node.id, type: 'task' as const }) } else if (node.type === 'subprocess') { result.push({ bpmnElementId: node.id, name: node.name || node.id, type: 'subprocess' as const }) } } return result } // Extrae gateways divergentes (los que tienen >1 outgoing) export function extractGatewayElements( graph: ProcessGraph ): Array<{ bpmnElementId: BpmnElementId; name: string; gatewayType: string; outgoing: string[] }> { const result = [] for (const node of graph.nodes.values()) { if ( ['exclusiveGateway', 'parallelGateway', 'inclusiveGateway', 'eventBasedGateway'].includes(node.type) && node.outgoing.length > 1 ) { result.push({ bpmnElementId: node.id, name: node.name || node.id, gatewayType: node.type, outgoing: node.outgoing, }) } } return result }