-
Notifications
You must be signed in to change notification settings - Fork 190
/
Copy pathwrite-good.js
112 lines (98 loc) · 3.51 KB
/
write-good.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
const weaselWords = require('weasel-words');
const passiveVoice = require('passive-voice');
const adverbWhere = require('adverb-where');
const tooWordy = require('too-wordy');
const noCliches = require('no-cliches');
const ePrime = require('e-prime');
const lexicalIllusions = require('./lib/lexical-illusions');
const startsWithSo = require('./lib/starts-with-so');
const thereIs = require('./lib/there-is');
const defaultChecks = {
weasel: { fn: weaselWords, explanation: 'is a weasel word' },
illusion: { fn: lexicalIllusions, explanation: 'is repeated' },
so: { fn: startsWithSo, explanation: 'adds no meaning' },
thereIs: { fn: thereIs, explanation: 'is unnecessary verbiage' },
passive: { fn: passiveVoice, explanation: 'may be passive voice' },
adverb: { fn: adverbWhere, explanation: 'can weaken meaning' },
tooWordy: { fn: tooWordy, explanation: 'is wordy or unneeded' },
cliches: { fn: noCliches, explanation: 'is a cliche' },
eprime: { fn: ePrime, explanation: 'is a form of \'to be\'' }
};
// User must explicitly opt-in
const disabledChecks = {
eprime: false
};
function filter(text, suggestions, whitelistTerms = []) {
const whitelistSlices = whitelistTerms.reduce((memo, term) => {
let index = text.indexOf(term);
while (index > 0) {
memo.push({ from: index, to: index + term.length });
index = text.indexOf(term, index + 1);
}
return memo;
}, []);
return suggestions.reduce((memo, suggestion) => {
if (!whitelistSlices.find((slice) => {
const suggestionFrom = suggestion.index;
const suggestionTo = suggestion.index + suggestion.offset;
return (
// suggestion covers entire whitelist term
suggestionFrom <= slice.from && suggestionTo >= slice.to
) || (
// suggestion starts within whitelist term
suggestionFrom >= slice.from && suggestionFrom <= slice.to
) || (
// suggestion ends within whitelist term
suggestionTo >= slice.from && suggestionTo <= slice.to
);
})) {
memo.push(suggestion);
}
return memo;
}, []);
}
function dedup(suggestions) {
const dupsHash = {};
return suggestions.reduce((memo, suggestion) => {
const key = `${suggestion.index}:${suggestion.offset}`;
if (!dupsHash[key]) {
dupsHash[key] = suggestion;
memo.push(suggestion);
} else {
dupsHash[key].reason += ` and ${suggestion.reason.substring(suggestion.offset + 3)}`;
}
return memo;
}, []);
}
function reasonable(text, reason) {
return function reasonableSuggestion(suggestion) {
// eslint-disable-next-line no-param-reassign
suggestion.reason = `"${
text.substr(suggestion.index, suggestion.offset)
}" ${reason}`;
return suggestion;
};
}
module.exports = function writeGood(text, opts = {}) {
const finalOpts = {};
const defaultOpts = Object.assign({}, disabledChecks, opts);
Object.keys(defaultOpts).forEach((optKey) => {
if (optKey !== 'checks') {
finalOpts[optKey] = defaultOpts[optKey];
}
});
const finalChecks = opts.checks || defaultChecks;
let suggestions = [];
Object.keys(finalChecks).forEach((checkName) => {
if (finalOpts[checkName] !== false) {
suggestions = suggestions.concat(
finalChecks[checkName]
.fn(text)
.map(reasonable(text, finalChecks[checkName].explanation))
);
}
});
const filtered = filter(text, suggestions, opts.whitelist);
return dedup(filtered).sort((a, b) => (a.index < b.index ? -1 : 1));
};
module.exports.annotate = require('./lib/annotate');