sf-package-list
Version:
Convert Salesforce package.xml manifests to and from plain-text list format.
145 lines • 5.43 kB
JavaScript
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