This repository has been archived by the owner on Oct 16, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathcontent-rules.js
173 lines (148 loc) · 5.09 KB
/
content-rules.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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
const assert = require('assert');
const jsdom = require('jsdom');
const maxLength = 180;
const maxFirstSentenceLength = 150;
const allowedTags = {
A: ['href'],
CODE: [],
EM: [],
STRONG: [],
};
const fragmentToDom = html => (new jsdom.JSDOM(html)).window.document.querySelector('body');
const forbiddenTags = (dom) => {
const tagSet = new Set();
dom.querySelectorAll('*').forEach(elem => tagSet.add(elem.tagName));
return Array.from(tagSet).filter(v => !Object.keys(allowedTags).includes(v));
};
const forbiddenAttrs = (dom) => {
const badAttrs = [];
dom.querySelectorAll('*').forEach((elem) => {
const allowedAttrs = allowedTags[elem.tagName] || [];
const attrNames = Array.from(elem.attributes).map(value => value.name);
attrNames.filter(attr => !allowedAttrs.includes(attr))
.forEach(attr => badAttrs.push(`${elem.tagName}.${attr}`));
});
return badAttrs;
};
const ok = { passes: true, errors: [] };
const contentRules = [
{
name: 'max-sentence-length',
description: `First sentence should not exceed ${maxFirstSentenceLength} characters`,
bad: 'This is a very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very, very long first sentence. This is the second sentence.',
good: 'This is a short first sentence. This is the second sentence.',
check(dom) {
const firstSentence = dom.textContent.replace(/\.(?!\d)/g, '.\x1f').split('\x1f')[0];
const passes = firstSentence.length <= maxFirstSentenceLength;
if (passes) {
return ok;
}
return {
passes,
errors: [
`First sentence may be too long. Expected ≤${maxFirstSentenceLength}; got ${firstSentence.length}`,
`> ${firstSentence.slice(0, maxFirstSentenceLength)}\x1b[41m${firstSentence.slice(maxFirstSentenceLength)}\x1b[0m`,
],
};
},
},
{
name: 'max-length',
description: `Overall length should not exceed ${maxLength} characters`,
bad: "This is an example. This is an example. This is an example. This is an example. This is an example. This is an example. This is an example. This is an example. This is an example. But now we've gone on too long.",
good: 'This is short and sweet.',
check(dom) {
const text = dom.textContent;
const passes = text.length <= maxLength;
if (passes) {
return ok;
}
return {
passes,
errors: [
`Summary is too long. Expected ≤${maxLength} characters; got ${text.length}`,
`> ${text.slice(0, maxLength)}\x1b[41m${text.slice(maxLength)}\x1b[0m`,
],
};
},
},
{
name: 'no-nbsps',
description: '" " shouldn\'t be used',
bad: 'Use literal spaces.',
good: 'Use literal spaces.',
check(dom) {
const html = dom.innerHTML;
const passes = !html.includes(' ');
if (passes) {
return ok;
}
return {
passes,
errors: [
'Contains ` ` instead of literal spaces.',
`> ${html.replace(/ /g, '\x1b[41m \x1b[0m')}`,
],
};
},
},
{
name: 'no-forbidden-tags',
description: 'Only use allowed tags',
bad: "<div>I am a poet<br> and didn't even know it.</div>",
good: "I am a poet and didn't even know it.",
check(dom) {
const badTags = forbiddenTags(dom);
const passes = badTags.length === 0;
if (passes) {
return ok;
}
const badElems = dom.querySelectorAll(badTags.join(', '));
const badHTML = [...badElems].map(elem => `> ${elem.parentNode.innerHTML}`);
const errors = [
`Contains forbidden tags: \x1b[41m${badTags.join(', ')}\x1b[0m`,
...badHTML,
];
return {
passes,
errors,
};
},
},
{
name: 'no-forbidden-attrs',
description: 'Only use allowed attributes',
bad: 'This has two bad attributes: <a data-random="v7mm9m5c" href="https://developer.mozilla.org/">MDN Web Docs</a><br data-forbidden-tag="">',
good: '<a href="https://developer.mozilla.org/">MDN Web Docs</a>',
check(dom) {
const badAttrs = forbiddenAttrs(dom);
const passes = badAttrs.length === 0;
if (passes) {
return ok;
}
return {
passes,
errors: [
`Contains forbidden attributes: \x1b[41m${badAttrs.join(', ')}\x1b[0m`,
],
};
},
},
];
const testRules = () => {
contentRules.forEach((rule) => {
const good = rule.check(fragmentToDom(rule.good));
const bad = rule.check(fragmentToDom(rule.bad));
assert.ok(good.passes, `Expected \`true\` for result of "${rule.description}" good example.`);
assert.deepStrictEqual(good.errors, [], `Expected 0 errors for "${rule.description}" good example.`);
assert.ok(bad.passes === false, `Expected \`false\` for result of "${rule.description}" bad example.`);
assert.ok(bad.errors.length > 0, `Expected errors for "${rule.description}" bad example.`);
});
};
if (require.main === module) {
testRules();
}
module.exports = {
contentRules,
fragmentToDom,
};