Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/__tests__/attributesSpec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,23 @@ describe('XMLParser', () => {

expect(result).toStrictEqual(expected);
});

it('should leave an attribute name holding more than one colon alone', () => {
// Only a single prefix is a namespace prefix. A name with two colons is not
// one this parser can take apart, so it is passed through as written rather
// than guessed at — and a bare `xmlns` is still dropped beside it.
const xmlData = encoder.encode(
`<a xmlns="urn:x" ns:one:two="kept" ns:plain="stripped"></a>`,
);

const result = parse(xmlData, {
attributeNameProcessor: (name) => name,
ignoreAttributes: false,
ignoreNameSpace: true,
});

expect(result).toStrictEqual({
a: { 'ns:one:two': 'kept', plain: 'stripped' },
});
});
});
39 changes: 28 additions & 11 deletions src/traversable/parseAttributesString.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,33 @@ export function parseAttributesString(
return attributes;
}

/**
* Strip a single namespace prefix from an attribute name.
*
* Written without `split` because it runs once per attribute of every element:
* a 344 MB mzML carries about 1.4 million of them, and an array allocated for
* each is 1.4 million arrays for a question two `indexOf` calls answer.
*
* The bare `xmlns` case is deliberate and must not be simplified away. The
* previous form asked `tagName.split(':')[0] === 'xmlns'`, which is true both of
* `xmlns:mz` and of a colon-free `xmlns`, so both were dropped — an early return
* for "no colon" would silently start keeping the second one.
* @param tagName - The attribute's name as written.
* @param options - The parse options, read for `ignoreNameSpace`.
* @returns The local name, or `''` for a namespace declaration.
*/
function resolveNamespace(tagName: string, options: RealParseOptions) {
if (options.ignoreNameSpace) {
const tags = tagName.split(':');
const prefix = tagName.startsWith('/') ? '/' : '';
if (tags[0] === 'xmlns') {
return '';
}
if (tags.length === 2 && tags[1] !== undefined) {
tagName = prefix + tags[1];
}
}
return tagName;
if (!options.ignoreNameSpace) return tagName;

const colon = tagName.indexOf(':');
if (colon === -1) return tagName === 'xmlns' ? '' : tagName;
if (colon === 5 && tagName.startsWith('xmlns')) return '';
// Only a single prefix is stripped; `a:b:c` is left as it was written.
if (tagName.includes(':', colon + 1)) return tagName;

const local = tagName.slice(colon + 1);
return tagName.codePointAt(0) === SLASH ? `/${local}` : local;
}

/** `/`, which opens the name of a closing tag. */
const SLASH = 47;
Loading