UNPKG

sf-package-list

Version:

Convert Salesforce package.xml manifests to and from plain-text list format.

145 lines 5.43 kB
const NAMED_ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", }; function decodeEntities(text) { return text.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z]+);/g, (match, entity) => { if (entity.startsWith('#')) { const codePoint = entity[1] === 'x' ? Number.parseInt(entity.slice(2), 16) : Number.parseInt(entity.slice(1), 10); return String.fromCodePoint(codePoint); } return NAMED_ENTITIES[entity] ?? match; }); } /** * Minimal recursive-descent XML parser scoped to Salesforce package.xml manifests: * elements, attributes (skipped -- unused by callers), text, entities, comments, * and the leading <?xml ?> declaration. No DTD/CDATA support since manifests don't use them. */ export function parseXml(xml) { const length = xml.length; let i = 0; function skipWhitespace() { // Stryker disable next-line ConditionalExpression,EqualityOperator -- at i===length, xml[i] is undefined and /\s/.test(undefined) is false, so the loop always stops there regardless of the bound check while (i < length && /\s/.test(xml[i])) i++; } function parseName() { const start = i; // Stryker disable next-line EqualityOperator -- an off-by-one bound (i<=length) only lets the loop run one extra step to length+1; xml.slice clamps beyond the string end and every later bound check uses >=, so the extra step is unobservable while (i < length && !/[\s/>]/.test(xml[i])) i++; if (i === start) { throw new Error(`expected element name at position ${i}`); } return xml.slice(start, i); } function skipAttributes() { while (true) { skipWhitespace(); if (i >= length) { throw new Error('unexpected end of input inside a tag'); } const ch = xml[i]; if (ch === '>' || ch === '/') return; // Stryker disable next-line EqualityOperator -- same off-by-one equivalence as parseName's loop: the extra step to length+1 is unobservable while (i < length && !/[\s=/>]/.test(xml[i])) i++; skipWhitespace(); if (xml[i] !== '=') { throw new Error(`expected "=" in attribute at position ${i}`); } i++; skipWhitespace(); const quote = xml[i]; if (quote !== '"' && quote !== "'") { throw new Error(`expected quoted attribute value at position ${i}`); } i++; const end = xml.indexOf(quote, i); if (end === -1) { throw new Error('unterminated attribute value'); } i = end + 1; } } function skipComment() { const end = xml.indexOf('-->', i + 4); if (end === -1) { throw new Error('unterminated comment'); } i = end + 3; } function skipDeclaration() { const end = xml.indexOf('?>', i + 2); if (end === -1) { throw new Error('unterminated processing instruction'); } i = end + 2; } function parseElement() { i++; // consume '<' const tagName = parseName(); skipAttributes(); if (xml[i] === '/') { i += 2; // consume '/>' return { tagName, children: [] }; } i++; // consume '>' const children = []; while (true) { if (i >= length) { throw new Error(`unterminated element <${tagName}>`); } if (xml.startsWith('<!--', i)) { skipComment(); continue; } if (xml.startsWith('</', i)) { const closeEnd = xml.indexOf('>', i + 2); if (closeEnd === -1) { throw new Error(`unterminated closing tag for <${tagName}>`); } const closeName = xml.slice(i + 2, closeEnd).trim(); if (closeName !== tagName) { throw new Error(`mismatched closing tag </${closeName}> for <${tagName}>`); } i = closeEnd + 1; return { tagName, children }; } if (xml[i] === '<') { children.push(parseElement()); continue; } const nextTag = xml.indexOf('<', i); const textEnd = nextTag === -1 ? length : nextTag; children.push(decodeEntities(xml.slice(i, textEnd))); i = textEnd; } } const roots = []; // Stryker disable next-line EqualityOperator -- an off-by-one bound (i<=length) just defers the loop exit from the while-condition to the `i >= length` break on the next iteration; the outcome is identical while (i < length) { skipWhitespace(); if (i >= length) break; if (xml.startsWith('<!--', i)) { skipComment(); continue; } if (xml.startsWith('<?', i)) { skipDeclaration(); continue; } if (xml[i] !== '<') { throw new Error(`unexpected content outside the root element at position ${i}`); } roots.push(parseElement()); } return roots; } //# sourceMappingURL=xmlParser.js.map