diff --git a/src/rules/attr.js b/src/rules/attr.js index eb212c4..48cee4b 100644 --- a/src/rules/attr.js +++ b/src/rules/attr.js @@ -15,6 +15,7 @@ const logger = require("../lib/logger")("rule:attr"); * The following special configs are allowed: * - `{ "rule::selector": {String} }` Default "*". The matching elements must fulfill the other configs. * - `{ "rule::whitelist": {Boolean} }` Default `false`. If true, no other attributes can exist than those specified by the other configs. + * - `{ "rule::order": {Array | Boolean} }` Default `null`. As array, attributes must be defined in the provided order. As `true`, attributes must be defined in alphabetical order. */ /** @@ -30,7 +31,7 @@ const logger = require("../lib/logger")("rule:attr"); * - If whitelist is true, error if there are attributes left */ -const SPECIAL_ATTRIBS = ["rule::selector", "rule::whitelist"]; +const SPECIAL_ATTRIBS = ["rule::selector", "rule::whitelist", "rule::order"]; /** * Executes on a single element. @@ -63,6 +64,41 @@ function executeOnElm($elm, config, reporter, ast) { } } ); + + if (config["rule::order"]) { + const attributes = Object.keys(attrs); + if (attributes.length > 0) { + let order; + if (config["rule::order"] === true) { + // alphabetical ordering + order = attributes.slice(); + order.sort(); + } else { + order = config["rule::order"]; + } + + let prevIndex = order.indexOf(attributes[0]); + for (let i = 1; i < attributes.length; i++) { + const index = order.indexOf(attributes[i]); + if (index === -1) { + // this attribute doesn't need ordering, ignore it + return; + } + + if (prevIndex !== -1 && index < prevIndex) { + reporter.error( + `Wrong ordering of attributes, found "${ + attributes.join(", ")}", expected "${order.join(", ")}"`, + $elm, + ast + ); + break; + } + prevIndex = index; + } + } + } + // check that all configs are met Object.keys(attrs).forEach( attrib => { diff --git a/test/attr.spec.js b/test/attr.spec.js index 807a192..71804dd 100644 --- a/test/attr.spec.js +++ b/test/attr.spec.js @@ -38,6 +38,7 @@ const testSVG = ` + `; function inspect(obj) { @@ -197,4 +198,49 @@ describe("Rule: attr", function(){ "rule::whitelist": true, }); }); + it("should succeed enforcing right attributes ordering", function() { + return testSucceeds({ + "rule::selector": "rect", + "rule::order": ["height", "width", "style"], + }); + }); + it("should fail enforcing wrong attributes ordering", function() { + return testFails({ + "rule::selector": "rect", + "rule::order": ["width", "style", "height"], + }); + }); + it("should succeed enforcing ordering of first attributes", function() { + return testSucceeds({ + "rule::selector": "rect", + "rule::order": ["height", "width"], + }); + }); + it("should succeed enforcing soft ordering of some attributes", function() { + return testSucceeds({ + "rule::selector": "rect", + "rule::order": ["height", "style"], + }); + }); + it("should succeed enforcing alphabetical ordering with true", function() { + return testSucceeds({ + "rule::selector": "svg", + "rule::order": true, + }); + }); + it("should fail enforcing alphabetical ordering", function() { + return testFails({ + "rule::selector": "rect", + "rule::order": true, + }); + }); + it("should succeed enforcing hard ordering with whitelist", function() { + return testSucceeds({ + "role": true, + "viewBox": true, + "rule::selector": "svg", + "rule::whitelist": true, + "rule::order": true, + }); + }); });