Skip to content

Commit

Permalink
feat(common): add utility functions for xmlns attributes (#173)
Browse files Browse the repository at this point in the history
  • Loading branch information
tal-sapan authored May 17, 2020
1 parent af66232 commit 20d6c09
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 8 deletions.
16 changes: 11 additions & 5 deletions packages/ast/lib/build-ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ const {
isArray,
assign
} = require("lodash");
const { findNextTextualToken } = require("@xml-tools/common");
const {
findNextTextualToken,
isXMLNamespaceKey,
getXMLNamespaceKeyPrefix
} = require("@xml-tools/common");

const { getAstChildrenReflective } = require("./utils");
const { DEFAULT_NS } = require("./constants");
Expand Down Expand Up @@ -256,13 +260,15 @@ function updateNamespaces(element, prevNamespaces = []) {
(result, attrib) => {
/* istanbul ignore else - Defensive Coding, not actually possible branch */
if (attrib.key !== invalidSyntax) {
const nsMatch = /^xmlns(?::([^:]+))?$/.exec(attrib.key);
if (nsMatch !== null) {
const prefix = nsMatch[1];
if (
isXMLNamespaceKey({ key: attrib.key, includeEmptyPrefix: false }) ===
true
) {
const prefix = getXMLNamespaceKeyPrefix(attrib.key);
// TODO: Support un-defining namespaces (including the default one)
if (attrib.value) {
const uri = attrib.value;
if (prefix !== undefined) {
if (prefix !== "") {
result[prefix] = uri;
} else {
// default namespace
Expand Down
21 changes: 21 additions & 0 deletions packages/common/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,24 @@ export function findNextTextualToken(
tokenVector: IToken[],
prevTokEndOffset: number
): IToken | null;

/**
* Check if an xml attribute key is an xmlns key.
* Attribute keys which are xmlns keys: "xmlns", "xmlns:core".
* Attribute keys which are not xmlns keys: "myattr", "xmlns:with:extra:colon".
* "xmlns:" is considered an xmlns key if opts.includeEmptyPrefix is true.
*
* @param opts.key - the attribute key
* @param opts.includeEmptyPrefix - should true be returned when there is no prefix (key === "xmlns:")
*/
export function isXMLNamespaceKey(opts: {
key: string;
includeEmptyPrefix: boolean;
}): boolean;

/**
* Get the attribute name, without its "xmlns:" prefix, from an xmlns attribute key.
* If the attribute key is not an xmlns key, undefined is returned.
* @param key - the attribute key
*/
export function getXMLNamespaceKeyPrefix(key: string): string | undefined;
8 changes: 7 additions & 1 deletion packages/common/lib/api.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
const { findNextTextualToken } = require("./find-next-textual-token");
const {
isXMLNamespaceKey,
getXMLNamespaceKeyPrefix
} = require("./xml-ns-key.js");

module.exports = {
findNextTextualToken: findNextTextualToken
findNextTextualToken: findNextTextualToken,
isXMLNamespaceKey: isXMLNamespaceKey,
getXMLNamespaceKeyPrefix: getXMLNamespaceKeyPrefix
};
54 changes: 54 additions & 0 deletions packages/common/lib/xml-ns-key.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// The xml parser takes care of validating the attribute name.
// If the user started the attribute name with "xmlns:" we can assume that
// they meant for it to be an xml namespace attribute.
// xmlns attributes explicitly can't contain ":" after the "xmlns:" part.
const namespaceRegex = /^xmlns(?<prefixWithColon>:(?<prefix>[^:]*))?$/;

/**
* See comment in api.d.ts.
*
* @param {string} key
* @param {boolean} includeEmptyPrefix
* @returns {boolean}
*/
function isXMLNamespaceKey({ key, includeEmptyPrefix }) {
if (typeof key !== "string") {
return false;
}
const matchArr = key.match(namespaceRegex);

// No match - this is not an xmlns key
if (matchArr === null) {
return false;
}

return !!(
includeEmptyPrefix === true ||
// "xmlns" case
!matchArr.groups.prefixWithColon ||
// "xmlns:<prefix>" case
matchArr.groups.prefix
);
}

/**
* See comment in api.d.ts.
*
* @param {string} key
* @returns {string|undefined}
*/
function getXMLNamespaceKeyPrefix(key) {
if (typeof key !== "string") {
return undefined;
}
const matchArr = key.match(namespaceRegex);
if (matchArr === null) {
return undefined;
}
return (matchArr.groups && matchArr.groups.prefix) || "";
}

module.exports = {
isXMLNamespaceKey: isXMLNamespaceKey,
getXMLNamespaceKeyPrefix: getXMLNamespaceKeyPrefix
};
106 changes: 106 additions & 0 deletions packages/common/test/xml-ns-key-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const { expect } = require("chai");
const { isXMLNamespaceKey, getXMLNamespaceKeyPrefix } = require("../");

describe("The XML-Tools Common Utils", () => {
context("isXMLNamespaceKey", () => {
it("will return true for attribute name starting with xmlns:", () => {
expect(isXMLNamespaceKey({ key: "xmlns:a", includeEmptyPrefix: true })).to
.be.true;
expect(isXMLNamespaceKey({ key: "xmlns:a", includeEmptyPrefix: false }))
.to.be.true;
});

it("will return true for attribute name starting with xmlns: that contains dots", () => {
expect(
isXMLNamespaceKey({ key: "xmlns:a.b.c", includeEmptyPrefix: true })
).to.be.true;
expect(
isXMLNamespaceKey({ key: "xmlns:a.b.c", includeEmptyPrefix: false })
).to.be.true;
});

it("will return true for the default namespace attribute", () => {
expect(isXMLNamespaceKey({ key: "xmlns", includeEmptyPrefix: true })).to
.be.true;
expect(isXMLNamespaceKey({ key: "xmlns", includeEmptyPrefix: false })).to
.be.true;
});

it("will return true for xmlns attribute without a name when includeEmptyPrefix is true", () => {
expect(isXMLNamespaceKey({ key: "xmlns:", includeEmptyPrefix: true })).to
.be.true;
});

it("will return false for xmlns attribute without a name when includeEmptyPrefix is false", () => {
expect(isXMLNamespaceKey({ key: "xmlns:", includeEmptyPrefix: false })).to
.be.false;
});

it("will return false for the non-xmlns attribute", () => {
expect(isXMLNamespaceKey({ key: "abc", includeEmptyPrefix: true })).to.be
.false;
expect(isXMLNamespaceKey({ key: "abc", includeEmptyPrefix: false })).to.be
.false;
});

it("will return false for non-xmlns attribute that starts with xmlns", () => {
expect(isXMLNamespaceKey({ key: "xmlnst", includeEmptyPrefix: true })).to
.be.false;
expect(isXMLNamespaceKey({ key: "xmlnst", includeEmptyPrefix: false })).to
.be.false;
});

it("will return false for attribute name starting with xmlns: that contains additional colons", () => {
expect(
isXMLNamespaceKey({ key: "xmlns:a.b:c", includeEmptyPrefix: true })
).to.be.false;
expect(
isXMLNamespaceKey({ key: "xmlns:a.b:c", includeEmptyPrefix: false })
).to.be.false;
});

it("will return false for undefined", () => {
expect(isXMLNamespaceKey({ key: undefined, includeEmptyPrefix: true })).to
.be.false;
expect(isXMLNamespaceKey({ key: undefined, includeEmptyPrefix: false }))
.to.be.false;
});

it("will return false for null", () => {
expect(isXMLNamespaceKey({ key: null, includeEmptyPrefix: true })).to.be
.false;
expect(isXMLNamespaceKey({ key: null, includeEmptyPrefix: false })).to.be
.false;
});
});

context("getNamespaceKeyPrefix", () => {
it("will return the name without xmlns prefix for xmlns attribute with name", () => {
expect(getXMLNamespaceKeyPrefix("xmlns:abc")).to.eql("abc");
});

it("will return the name without xmlns prefix for xmlns attribute with name that contains dots", () => {
expect(getXMLNamespaceKeyPrefix("xmlns:abc.efg")).to.eql("abc.efg");
});

it("will return empty string when prefix is empty", () => {
expect(getXMLNamespaceKeyPrefix("xmlns:")).to.be.empty;
});

it("will return undefined when key does not start with xmlns", () => {
expect(getXMLNamespaceKeyPrefix("abc")).to.be.undefined;
});

it("will return undefined when key starts with xmlns but is not an xmlns key", () => {
expect(getXMLNamespaceKeyPrefix("xmlns*")).to.be.undefined;
});

it("will return undefined for undefined", () => {
expect(getXMLNamespaceKeyPrefix(undefined)).to.be.undefined;
});

it("will return undefined for null", () => {
expect(getXMLNamespaceKeyPrefix(null)).to.be.undefined;
});
});
});
5 changes: 3 additions & 2 deletions packages/simple-schema/lib/validators/unknown-attributes.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { map, includes, forEach } = require("lodash");
const { isXMLNamespaceKey } = require("@xml-tools/common");
const { tokenToOffsetPosition } = require("./utils");

const NAMESPACE_ATTRRIBUTE_PATTERN = /^xmlns(:[^=]*)?$/;
/**
* @param {XMLElement} elem
* @param {XSSElement} schema
Expand All @@ -23,7 +23,8 @@ function validateUnknownAttributes(elem, schema) {
if (attrib.key !== null) {
if (
includes(allowedAttribNames, attrib.key) === false &&
NAMESPACE_ATTRRIBUTE_PATTERN.test(attrib.key) === false
isXMLNamespaceKey({ key: attrib.key, includeEmptyPrefix: true }) ===
false
) {
issues.push({
msg: `Unknown Attribute: <${
Expand Down

0 comments on commit 20d6c09

Please sign in to comment.