All files / src/web/flowable/roundTrip xmlUtils.ts

88.77% Statements 87/98
68.18% Branches 45/66
96.96% Functions 32/33
88.42% Lines 84/95

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224        4x     467064x       202548x               87458x 87458x 85534x     1924x 651x     1273x 1261x     12x 8x     4x       4188x       4188x       1037x 1037x             1037x 1037x       1037x 1037x 1037x 30536x 30536x 30536x     1037x       1037x 1037x 390x     647x 18522x         64748x       790x       790x       172x       27180x 27180x 10x         4752x       35882x       7066x 7066x 7066x 7066x 7066x       184x 184x 184x       53x 187x     53x       180x       75x 75x             74x 74x         74x 74x       24x 24x         24x       118x 118x 118x         92x 78x 78x       92x 1x     91x     91x 91x       91x         53x 53x       53x       138400x       6504x 6504x 6504x 12976x 2955x   12976x     6504x  
import { XMLSerializer, type Attr as XmlAttr, type Document as XmlDocument, type Element as XmlElement, type Node as XmlNode } from '@xmldom/xmldom';
import { parseXmlDocument } from '../xmlParser';
import { ACTIVITI_NAMESPACE, BPMN_MODEL_NAMESPACE } from './constants';
 
export const serializer = new XMLSerializer();
 
export function getElementChildren(element: XmlElement): XmlElement[] {
	return Array.from(element.childNodes).filter((node): node is XmlElement => node.nodeType === node.ELEMENT_NODE);
}
 
export function getLocalName(node: XmlNode): string {
	return node.localName || node.nodeName.split(':').pop() || node.nodeName;
}
 
function matchesQualifiedNode(
	node: Pick<XmlNode, 'nodeName' | 'namespaceURI' | 'localName'>,
	localName: string,
	namespaceUri?: string,
): boolean {
	const nodeLocalName = node.localName || node.nodeName.split(':').pop() || node.nodeName;
	if (nodeLocalName !== localName) {
		return false;
	}
 
	if (!namespaceUri) {
		return true;
	}
 
	if ((node.namespaceURI || '') === namespaceUri) {
		return true;
	}
 
	if (!node.namespaceURI && namespaceUri === ACTIVITI_NAMESPACE) {
		return node.nodeName === `activiti:${localName}`;
	}
 
	return false;
}
 
export function isElementNamed(node: XmlNode, localName: string, namespaceUri?: string): node is XmlElement {
	return node.nodeType === node.ELEMENT_NODE && matchesQualifiedNode(node, localName, namespaceUri);
}
 
export function isActivitiElement(node: XmlNode, localName: string): node is XmlElement {
	return isElementNamed(node, localName, ACTIVITI_NAMESPACE);
}
 
function getTraversalRoot(root: XmlDocument | XmlElement): XmlElement | undefined {
	Eif ('documentElement' in root) {
		return root.documentElement || undefined;
	}
 
	return root;
}
 
function collectDescendantElements(root: XmlDocument | XmlElement): XmlElement[] {
	const traversalRoot = getTraversalRoot(root);
	Iif (!traversalRoot) {
		return [];
	}
 
	const elements: XmlElement[] = [];
	const queue: XmlElement[] = [traversalRoot];
	while (queue.length > 0) {
		const [element] = queue.splice(0, 1);
		elements.push(element);
		queue.push(...getElementChildren(element));
	}
 
	return elements;
}
 
export function getElementsByLocalName(root: XmlDocument | XmlElement, localName: string, namespaceUri?: string): XmlElement[] {
	const elements = collectDescendantElements(root);
	if (localName === '*') {
		return elements;
	}
 
	return elements.filter((node): node is XmlElement => {
		return matchesQualifiedNode(node, localName, namespaceUri);
	});
}
 
function findAttribute(element: XmlElement, localName: string, namespaceUri?: string): XmlAttr | undefined {
	return Array.from(element.attributes).find((attribute) => matchesQualifiedNode(attribute, localName, namespaceUri));
}
 
export function getAttributeValue(element: XmlElement, localName: string, namespaceUri?: string): string {
	return findAttribute(element, localName, namespaceUri)?.value || '';
}
 
export function getActivitiAttribute(element: XmlElement, localName: string): string {
	return getAttributeValue(element, localName, ACTIVITI_NAMESPACE);
}
 
export function setActivitiAttribute(element: XmlElement, localName: string, value: string): void {
	element.setAttributeNS(ACTIVITI_NAMESPACE, `activiti:${localName}`, value);
}
 
export function removeActivitiAttribute(element: XmlElement, localName: string): void {
	const attribute = findAttribute(element, localName, ACTIVITI_NAMESPACE);
	if (attribute) {
		element.removeAttributeNode(attribute);
	}
}
 
export function isSameElementType(left: XmlElement, right: XmlElement): boolean {
	return getLocalName(left) === getLocalName(right) && (left.namespaceURI || '') === (right.namespaceURI || '');
}
 
function replaceAllPlain(value: string, search: string, replacement: string): string {
	return value.split(search).join(replacement);
}
 
export function escapeXml(value: string): string {
	let escapedValue = replaceAllPlain(value, '&', '&amp;');
	escapedValue = replaceAllPlain(escapedValue, '<', '&lt;');
	escapedValue = replaceAllPlain(escapedValue, '>', '&gt;');
	escapedValue = replaceAllPlain(escapedValue, '"', '&quot;');
	return replaceAllPlain(escapedValue, "'", '&apos;');
}
 
export function escapeXmlText(value: string): string {
	let escapedValue = replaceAllPlain(value, '&', '&amp;');
	escapedValue = replaceAllPlain(escapedValue, '<', '&lt;');
	return replaceAllPlain(escapedValue, '>', '&gt;');
}
 
function buildNamespaceWrapper(namespaces: Record<string, string>): string {
	const namespaceAttributes = Object.entries(namespaces)
		.map(([prefix, uri]) => `xmlns:${prefix}="${escapeXml(uri)}"`)
		.join(' ');
 
	return `<root xmlns="${BPMN_MODEL_NAMESPACE}" ${namespaceAttributes}>`;
}
 
export function getNodeDocument(node: XmlNode): XmlDocument {
	return (node.ownerDocument || node) as XmlDocument;
}
 
export function insertAfter(parent: XmlNode, newNode: XmlNode, referenceNode: XmlNode): void {
	if (referenceNode.nextSibling) {
		parent.insertBefore(newNode, referenceNode.nextSibling);
	} else E{
		parent.appendChild(newNode);
	}
}
 
export function insertBeforeNode(parent: XmlNode, newNode: XmlNode, referenceNode: XmlNode): void {
	const beforeCapableReference = referenceNode as XmlNode & { before?: (node: XmlNode) => void };
	Iif (typeof beforeCapableReference.before === 'function') {
		beforeCapableReference.before(newNode);
		return;
	}
 
	const insertBefore = parent.insertBefore.bind(parent);
	insertBefore(newNode, referenceNode);
}
 
export function replaceNode(targetNode: XmlNode, replacementNode: XmlNode): void {
	const replaceCapableNode = targetNode as XmlNode & { replaceWith?: (node: XmlNode) => void };
	Iif (typeof replaceCapableNode.replaceWith === 'function') {
		replaceCapableNode.replaceWith(replacementNode);
		return;
	}
 
	targetNode.parentNode?.replaceChild(replacementNode, targetNode);
}
 
export function detachNode(node: XmlNode): void {
	Eif (node.parentNode) {
		const removeChild = node.parentNode.removeChild.bind(node.parentNode);
		removeChild(node);
	}
}
 
export function setTextContentPreservingComments(element: XmlElement, value: string): void {
	for (const child of Array.from(element.childNodes)) {
		Eif (child.nodeType === child.TEXT_NODE || child.nodeType === child.CDATA_SECTION_NODE) {
			detachNode(child);
		}
	}
 
	if (!value) {
		return;
	}
 
	const firstNonTextChild = Array.from(element.childNodes).find((child) => {
		return child.nodeType !== child.TEXT_NODE && child.nodeType !== child.CDATA_SECTION_NODE;
	});
	const textNode = getNodeDocument(element).createTextNode(value);
	Iif (firstNonTextChild) {
		const insertBefore = element.insertBefore.bind(element);
		insertBefore(textNode, firstNonTextChild);
	} else {
		element.appendChild(textNode);
	}
}
 
export function parseXmlFragment(xml: string, namespaces: Record<string, string>): XmlElement[] {
	const fragmentDocument = parseXmlDocument(`${buildNamespaceWrapper(namespaces)}${xml}</root>`);
	Iif (!fragmentDocument.documentElement) {
		return [];
	}
 
	return getElementChildren(fragmentDocument.documentElement);
}
 
export function findDirectChild(element: XmlElement, localName: string): XmlElement | undefined {
	return getElementChildren(element).find((child) => getLocalName(child) === localName);
}
 
export function buildXmlIdentity(scope: string, element: XmlElement): string {
	let index = 0;
	let sibling = element.previousSibling;
	while (sibling) {
		if (sibling.nodeType === sibling.ELEMENT_NODE && isSameElementType(sibling as XmlElement, element)) {
			index++;
		}
		sibling = sibling.previousSibling;
	}
 
	return `${scope}:${element.namespaceURI || ''}:${getLocalName(element)}:${index}`;
}