UNPKG

7.01 kBJavaScriptView Raw
1'use strict'
2
3module.exports = factory
4
5var search = require('nlcst-search')
6var visit = require('unist-util-visit')
7var convert = require('unist-util-is/convert')
8var toString = require('nlcst-to-string')
9var normalize = require('nlcst-normalize')
10var quotation = require('quotation')
11
12var pid = 'retext-equality'
13var english = 'en'
14var dash = '-'
15var dashLetter = /-([a-z])/g
16
17var word = convert('WordNode')
18var whiteSpace = convert('WhiteSpaceNode')
19var punctuation = convert('PunctuationNode')
20
21function factory(patterns, lang) {
22 /* istanbul ignore next - needed for other languages. */
23 var source = pid + (lang === english ? '' : dash + lang)
24
25 // Several pattern types can be handled.
26 // Handlers are stored in this map by type.
27 var handlers = {or: or, basic: basic}
28
29 // Internal mapping.
30 var byId = []
31 var byIndex = []
32 var apostrophes = []
33 var list = []
34
35 unpack()
36
37 equality.displayName = [pid, lang].join(dash).replace(dashLetter, titleCase)
38
39 return equality
40
41 function equality(options) {
42 var settings = options || {}
43 var ignore = settings.ignore || []
44 var noBinary = settings.noBinary
45 var noNormalize = []
46 var normalize = []
47 var length = list.length
48 var index = -1
49 var item
50
51 while (++index < length) {
52 item = list[index]
53
54 if (ignore.indexOf(item) !== -1) {
55 continue
56 }
57
58 if (apostrophes.indexOf(item) === -1) {
59 normalize.push(item)
60 } else {
61 noNormalize.push(item)
62 }
63 }
64
65 return transformer
66
67 function transformer(tree, file) {
68 visit(tree, 'ParagraphNode', visitor)
69
70 function visitor(node) {
71 var matches = {}
72 var key
73 var type
74
75 search(node, normalize, handle)
76 search(node, noNormalize, handle, true)
77
78 // Ignore or emit offending words based on their pattern.
79 for (key in matches) {
80 type = byId[key].type
81
82 if (type === 'or' && noBinary) {
83 type = 'basic'
84 }
85
86 handlers[type](matches[key], byId[key], file)
87 }
88
89 return visit.SKIP
90
91 // Handle a match.
92 function handle(match, position, parent, phrase) {
93 var index = list.indexOf(phrase)
94 var pattern = byIndex[index]
95 var id = pattern.id
96
97 if (phrase !== phrase.toLowerCase() && toString(match) !== phrase) {
98 return
99 }
100
101 if (!(id in matches)) {
102 matches[id] = []
103 }
104
105 matches[id].push({
106 type: pattern.inconsiderate[phrase],
107 parent: parent,
108 nodes: match,
109 start: position,
110 end: position + match.length - 1
111 })
112 }
113 }
114 }
115 }
116
117 function unpack() {
118 var index = -1
119 var length = patterns.length
120 var pattern
121 var inconsiderate
122 var phrase
123 var id
124
125 while (++index < length) {
126 pattern = patterns[index]
127 inconsiderate = pattern.inconsiderate
128 id = pattern.id
129
130 byId[id] = pattern
131
132 for (phrase in inconsiderate) {
133 list.push(phrase)
134 byIndex.push(pattern)
135
136 if (pattern.apostrophe) {
137 apostrophes.push(phrase)
138 }
139 }
140 }
141 }
142
143 // Handle matches for a `basic` pattern.
144 // **Basic** patterns need no extra logic, every match is emitted as a
145 // warning.
146 function basic(matches, pattern, file) {
147 var note = pattern.note
148 var id = pattern.id
149 var length = matches.length
150 var index = -1
151 var match
152 var nodes
153
154 while (++index < length) {
155 match = matches[index]
156 nodes = match.nodes
157
158 warn(
159 file,
160 id,
161 toString(nodes),
162 pattern.considerate,
163 nodes[0],
164 note,
165 pattern.condition
166 )
167 }
168 }
169
170 // Handle matches for an **or** pattern.
171 // **Or** patterns emit a warning unless every category is present.
172 //
173 // For example, when `him` and `her` occur adjacent to each other, they are not
174 // warned about.
175 // But when they occur alone, they are.
176 function or(matches, pattern, file) {
177 var length = matches.length
178 var note = pattern.note
179 var id = pattern.id
180 var index = -1
181 var match
182 var next
183 var siblings
184 var sibling
185 var value
186 var nodes
187 var start
188 var end
189
190 while (++index < length) {
191 match = matches[index]
192 siblings = match.parent.children
193 nodes = match.nodes
194 value = toString(nodes)
195 next = matches[index + 1]
196
197 if (next && next.parent === match.parent && next.type !== match.type) {
198 start = match.end
199 end = next.start
200
201 while (++start < end) {
202 sibling = siblings[start]
203
204 if (
205 whiteSpace(sibling) ||
206 (word(sibling) && /(and|or)/.test(normalize(sibling))) ||
207 (punctuation(sibling) && normalize(sibling) === '/')
208 ) {
209 continue
210 }
211
212 break
213 }
214
215 // If we didn't break…
216 if (start === end) {
217 index++
218 continue
219 }
220 }
221
222 warn(
223 file,
224 id,
225 value,
226 pattern.considerate,
227 nodes[0],
228 note,
229 pattern.condition
230 )
231 }
232 }
233
234 // Warn on `file` about `actual` (at `node`) with `suggestion`s.
235 function warn(file, id, actual, suggestion, node, note, condition, joiner) {
236 var expected = suggestion
237 var message
238
239 if (expected) {
240 expected = Object.keys(expected)
241
242 if (isCapitalized(actual)) {
243 expected = capitalize(expected)
244 }
245 }
246
247 message = file.message(
248 reason(actual, expected, condition, joiner),
249 node,
250 [source, id].join(':')
251 )
252
253 message.actual = actual
254 message.expected = expected
255
256 if (note) {
257 message.note = note
258 }
259 }
260}
261
262// Create a human readable warning message for `violation` and suggest
263// `suggestion`.
264function reason(violation, suggestion, condition, joiner) {
265 var reason =
266 join(quotation(violation, '`'), joiner) +
267 ' may be insensitive' +
268 (condition ? ', ' + condition : '') +
269 ', '
270
271 reason += suggestion
272 ? 'use ' + join(quotation(suggestion, '`'), joiner) + ' instead'
273 : 'try not to use it'
274
275 return reason
276}
277
278// Join `value`, if joinable, with `joiner` or `', '`.
279function join(value, joiner) {
280 return typeof value === 'string' ? value : value.join(joiner || ', ')
281}
282
283// Check whether the first character of a given value is upper-case.
284// Supports a string, or a list of strings.
285// Defers to the standard library for what defines a “upper case” letter.
286function isCapitalized(value) {
287 var char = value.charAt(0)
288 return char.toUpperCase() === char
289}
290
291// Capitalize a list of values.
292function capitalize(value) {
293 var result = []
294 var index = -1
295 var length = value.length
296
297 while (++index < length) {
298 result[index] = value[index].charAt(0).toUpperCase() + value[index].slice(1)
299 }
300
301 return result
302}
303
304function titleCase($0, $1) {
305 return $1.charAt(0).toUpperCase()
306}