1 | 'use strict'
|
2 |
|
3 | module.exports = factory
|
4 |
|
5 | var search = require('nlcst-search')
|
6 | var visit = require('unist-util-visit')
|
7 | var convert = require('unist-util-is/convert')
|
8 | var toString = require('nlcst-to-string')
|
9 | var normalize = require('nlcst-normalize')
|
10 | var quotation = require('quotation')
|
11 |
|
12 | var pid = 'retext-equality'
|
13 | var english = 'en'
|
14 | var dash = '-'
|
15 | var dashLetter = /-([a-z])/g
|
16 |
|
17 | var word = convert('WordNode')
|
18 | var whiteSpace = convert('WhiteSpaceNode')
|
19 | var punctuation = convert('PunctuationNode')
|
20 |
|
21 | function factory(patterns, lang) {
|
22 |
|
23 | var source = pid + (lang === english ? '' : dash + lang)
|
24 |
|
25 |
|
26 |
|
27 | var handlers = {or: or, basic: basic}
|
28 |
|
29 |
|
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 |
|
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 |
|
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 |
|
144 |
|
145 |
|
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 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
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 |
|
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 |
|
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 |
|
263 |
|
264 | function 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 |
|
279 | function join(value, joiner) {
|
280 | return typeof value === 'string' ? value : value.join(joiner || ', ')
|
281 | }
|
282 |
|
283 |
|
284 |
|
285 |
|
286 | function isCapitalized(value) {
|
287 | var char = value.charAt(0)
|
288 | return char.toUpperCase() === char
|
289 | }
|
290 |
|
291 |
|
292 | function 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 |
|
304 | function titleCase($0, $1) {
|
305 | return $1.charAt(0).toUpperCase()
|
306 | }
|