Skip to content

Commit 67c7556

Browse files
committed
[new] - Spelling suggestions for aria-* props using edit distance (#52)
* [new] - Initial commit for error message suggestions. This is purely based on spelling with an arbitrary maximum edit distance threshold. * [new] - Stretch out suggestion API to allow dictionary, threshold, and limit params. * [fix] - bring in edit distance dependency and clean up suggestion API.
1 parent 1a8f7a8 commit 67c7556

File tree

5 files changed

+104
-6
lines changed

5 files changed

+104
-6
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
},
4141
"license": "MIT",
4242
"dependencies": {
43+
"damerau-levenshtein": "^1.0.0",
4344
"object-assign": "^4.0.1"
4445
}
4546
}

src/rules/aria-props.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,19 @@
99
// ----------------------------------------------------------------------------
1010

1111
import ariaAttributes from '../util/attributes/ARIA';
12+
import getSuggestion from '../util/getSuggestion';
1213

13-
const errorMessage = name => `${name}: This attribute is an invalid ARIA attribute.`;
14+
const errorMessage = name => {
15+
const dictionary = Object.keys(ariaAttributes).map(aria => aria.toLowerCase());
16+
const suggestions = getSuggestion(name, dictionary);
17+
const message = `${name}: This attribute is an invalid ARIA attribute.`;
18+
19+
if (suggestions.length > 0) {
20+
return `${message} Did you mean to use ${suggestions}?`;
21+
}
22+
23+
return message;
24+
};
1425

1526
module.exports = context => ({
1627
JSXAttribute: attribute => {

src/util/getSuggestion.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
import editDistance from 'damerau-levenshtein';
4+
5+
6+
// Minimum edit distance to be considered a good suggestion.
7+
const THRESHOLD = 2;
8+
9+
/**
10+
* Returns an array of suggestions given a word and a dictionary and limit of suggestions
11+
* to return.
12+
*/
13+
export default function getSuggestion(word, dictionary = [], limit = 2) {
14+
const distances = dictionary.reduce((suggestions, dictionaryWord) => {
15+
suggestions[dictionaryWord] = editDistance(word.toUpperCase(), dictionaryWord.toUpperCase()).steps;
16+
return suggestions;
17+
}, {});
18+
19+
return Object.keys(distances)
20+
.filter(suggestion => distances[suggestion] <= THRESHOLD)
21+
.sort((a, b) => distances[a] - distances[b]) // Sort by distance
22+
.slice(0, limit);
23+
}

tests/src/rules/aria-props.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
// -----------------------------------------------------------------------------
1111

1212
import rule from '../../../src/rules/aria-props';
13+
import ariaAttributes from '../../../src/util/attributes/ARIA';
14+
import getSuggestion from '../../../src/util/getSuggestion';
1315
import { RuleTester } from 'eslint';
1416

1517
const parserOptions = {
@@ -25,12 +27,23 @@ const parserOptions = {
2527

2628
const ruleTester = new RuleTester();
2729

28-
const errorMessage = name => ({
29-
message: `${name}: This attribute is an invalid ARIA attribute.`,
30-
type: 'JSXAttribute'
31-
});
30+
const errorMessage = name => {
31+
const dictionary = Object.keys(ariaAttributes).map(aria => aria.toLowerCase());
32+
const suggestions = getSuggestion(name, dictionary);
33+
const message = `${name}: This attribute is an invalid ARIA attribute.`;
3234

33-
import ariaAttributes from '../../../src/util/attributes/ARIA';
35+
if (suggestions.length > 0) {
36+
return {
37+
type: 'JSXAttribute',
38+
message: `${message} Did you mean to use ${suggestions}?`
39+
};
40+
}
41+
42+
return {
43+
type: 'JSXAttribute',
44+
message
45+
};
46+
};
3447

3548
// Create basic test cases using all valid role types.
3649
const basicValidityTests = Object.keys(ariaAttributes).map(prop => ({

tests/src/util/getSuggestion.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/* eslint-env mocha */
2+
'use strict';
3+
4+
import assert from 'assert';
5+
import getSuggestion from '../../../src/util/getSuggestion';
6+
7+
describe('spell check suggestion API', () => {
8+
it('should return no suggestions given empty word and no dictionary', () => {
9+
const word = '';
10+
const expected = [];
11+
const actual = getSuggestion(word);
12+
13+
assert.deepEqual(expected, actual);
14+
});
15+
16+
it('should return no suggestions given real word and no dictionary', () => {
17+
const word = 'foo';
18+
const expected = [];
19+
const actual = getSuggestion(word);
20+
21+
assert.deepEqual(expected, actual);
22+
});
23+
24+
it('should return correct suggestion given real word and a dictionary', () => {
25+
const word = 'fo';
26+
const dictionary = [ 'foo', 'bar', 'baz' ];
27+
const expected = [ 'foo' ];
28+
const actual = getSuggestion(word, dictionary);
29+
30+
assert.deepEqual(expected, actual);
31+
});
32+
33+
it('should return multiple correct suggestions given real word and a dictionary', () => {
34+
const word = 'theer';
35+
const dictionary = [ 'there', 'their', 'foo', 'bar' ];
36+
const expected = [ 'their', 'there' ];
37+
const actual = getSuggestion(word, dictionary);
38+
39+
assert.deepEqual(expected, actual);
40+
});
41+
42+
it('should return one correct suggestion given real word and a dictionary and a limit of 1', () => {
43+
const word = 'theer';
44+
const dictionary = [ 'there', 'their', 'foo', 'bar' ];
45+
const expected = [ 'their' ];
46+
const actual = getSuggestion(word, dictionary, 1);
47+
48+
assert.deepEqual(expected, actual);
49+
});
50+
});

0 commit comments

Comments
 (0)