htmlhint-plugin-blocked-words
Version:
An [HTMLHint](https://htmlhint.com/) plugin that allows users to block arbitrary phrases in HTML code.
301 lines (300 loc) • 14 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BlockWordsRule = exports.blockedWordMessage = void 0;
const augment_vir_1 = require("augment-vir");
const create_rule_1 = require("../create-rule");
const defaultBlockWordsOptions = {
all: [],
attributeNames: [],
attributeValues: [],
tagNames: [],
text: [],
};
const allowedOptionKeys = Object.keys(defaultBlockWordsOptions);
function checkOptions(input) {
return (input &&
typeof input === 'object' &&
Object.keys(input).every((key) => {
return (allowedOptionKeys.includes(key) &&
Array.isArray(input[key]) &&
input[key].every((entry) => typeof entry === 'string'));
}));
}
const blockWordsRuleName = 'block-words';
function invalidOptionsMessage(invalidOptions) {
return `Expected an object with keys from "${allowedOptionKeys.join(', ')}" and string array values for rule ${blockWordsRuleName} but got ${JSON.stringify(invalidOptions)}`;
}
function blockedWordMessage(blockedMatch, blockRegExp, tagName, type) {
const contextMessage = type ? `${type} ` : '';
const tagMessage = tagName ? ` in ${tagName}` : '';
return `Blocked ${contextMessage}word from ${blockRegExp} detected${tagMessage}: "${blockedMatch}"`;
}
exports.blockedWordMessage = blockedWordMessage;
function generateReports(checkText, blockedWords, tagName, type) {
const messages = [];
blockedWords.forEach((blockedWord) => {
const checker = new RegExp(blockedWord.toLowerCase(), 'g');
const match = (0, augment_vir_1.safeMatch)(checkText.toLowerCase(), checker);
match.forEach((matchedText) => {
messages.push(blockedWordMessage(matchedText, checker, tagName, type));
});
return undefined;
});
return messages.length ? messages : undefined;
}
function runReports({ checkText, blockedWords, type, event, reporter, }) {
var _a;
const tagName = (_a = event.tagName) === null || _a === void 0 ? void 0 : _a.toLowerCase();
const reports = generateReports(checkText, blockedWords, tagName, type);
reports === null || reports === void 0 ? void 0 : reports.forEach((report) => { var _a; return reporter.error(report, event.line, event.col, exports.BlockWordsRule, (_a = event.raw) !== null && _a !== void 0 ? _a : ''); });
}
exports.BlockWordsRule = (0, create_rule_1.createHtmlHintRule)({
id: blockWordsRuleName,
defaultOptions: defaultBlockWordsOptions,
description: 'Block a user defined set of words from appears in attributes. Like the default "id-class-ad-disabled" rule but more powerful as you as the user get to set the list of blocked words.',
init: (parser, reporter, options) => {
if (!options) {
return;
}
if (!checkOptions(options)) {
reporter.error(invalidOptionsMessage(options), 1, 1, exports.BlockWordsRule, '');
return;
}
parser.addListener('all', (event) => {
// ignore comments
if (event.type === 'comment') {
return;
}
if (options.tagNames && event.type === 'tagstart' && event.tagName) {
const tagName = event.tagName.toLowerCase();
runReports({
blockedWords: options.tagNames,
checkText: tagName,
event,
reporter,
type: 'tag name',
});
}
if (options.text && event.type === 'text' && event.raw) {
runReports({
blockedWords: options.text,
checkText: event.raw,
event,
reporter,
type: 'text',
});
}
if (event.type === 'tagstart' && event.raw) {
Object.entries(parser.getMapAttrs(event.attrs)).forEach(([attributeName, attributeValue,]) => {
if (options.attributeValues) {
runReports({
blockedWords: options.attributeValues,
checkText: attributeValue,
event,
reporter,
type: 'attribute',
});
}
if (options.attributeNames) {
runReports({
blockedWords: options.attributeNames,
checkText: attributeName,
event,
reporter,
type: 'attribute',
});
}
});
}
if (event.raw && options.all) {
runReports({
blockedWords: options.all,
checkText: event.raw,
event,
reporter,
type: undefined,
});
}
});
},
tests: [
{
description: 'should block an easy word all word that is within a class',
html: `<html><head></head><body>What do we have here?<div class="bad-name"></div></body></html>`,
ruleOptions: { all: ['bad-name'] },
failures: [blockedWordMessage('bad-name', new RegExp('bad-name', 'g'), 'div')],
},
{
description: 'should error on option properties that are not an array',
html: ``,
ruleOptions: { all: 'invalid-option' },
failures: [invalidOptionsMessage({ all: 'invalid-option' })],
},
{
description: 'should error on non-object options',
html: ``,
ruleOptions: 'invalid-option',
failures: [invalidOptionsMessage('invalid-option')],
},
{
description: 'should error on property arrays that are not purely string arrays',
html: ``,
ruleOptions: {
all: [
4,
'invalid-option',
],
},
failures: [
invalidOptionsMessage({
all: [
4,
'invalid-option',
],
}),
],
},
{
description: 'should work with default rules',
html: `<html><head></head><body>What do we have here?<div class="bad-name" class="oops"></div></body></html>`,
ruleOptions: { all: ['bad-name'] },
otherOptions: { 'attr-no-duplication': true },
failures: [
'Duplicate of attribute name [ class ] was found.',
blockedWordMessage('bad-name', new RegExp('bad-name', 'g'), 'div'),
],
},
{
description: 'should block an all word in a tag name and class name',
html: `<html><head></head><body>What do we have here?<thing-bad-name-thing class="bad-name"></thing-bad-name-thing></body></html>`,
ruleOptions: { all: ['bad-name'] },
failures: [
blockedWordMessage('bad-name', new RegExp('bad-name', 'g'), 'thing-bad-name-thing'),
blockedWordMessage('bad-name', new RegExp('bad-name', 'g'), 'thing-bad-name-thing'),
blockedWordMessage('bad-name', new RegExp('bad-name', 'g'), 'thing-bad-name-thing'),
],
},
{
description: 'should not interfere with override comments for default rules',
html: `
<!-- htmlhint id-class-ad-disabled:false -->
<html>
<head></head>
<body>
What do we have here?
<thing-ad-name-thing class="bad-name"></thing-ad-name-thing>
</body>
</html>`,
ruleOptions: { all: ['bad-name'] },
otherOptions: { 'id-class-ad-disabled': true },
failures: [
blockedWordMessage('bad-name', new RegExp('bad-name', 'g'), 'thing-ad-name-thing'),
],
},
{
description: 'should block all matches of attribute value',
html: `
<!-- htmlhint id-class-ad-disabled:false -->
<html>
<head></head>
<body>
What do we have here?
<thing-ad-name-thing class="share-panel"></thing-ad-name-thing>
<thing-ad-name-thing class="other class names before share-panel"></thing-ad-name-thing>
<thing-ad-name-thing class="share-panel other class names after"></thing-ad-name-thing>
<thing-ad-name-thing class="class names before share-panel and class names after"></thing-ad-name-thing>
</body>
</html>`,
ruleOptions: { attributeValues: ['(?:^|\\s)share-panel(?:$|\\s)'] },
otherOptions: { 'id-class-ad-disabled': true },
failures: [
blockedWordMessage('share-panel', new RegExp('(?:^|\\s)share-panel(?:$|\\s)', 'g'), 'thing-ad-name-thing', 'attribute'),
blockedWordMessage(' share-panel', new RegExp('(?:^|\\s)share-panel(?:$|\\s)', 'g'), 'thing-ad-name-thing', 'attribute'),
blockedWordMessage('share-panel ', new RegExp('(?:^|\\s)share-panel(?:$|\\s)', 'g'), 'thing-ad-name-thing', 'attribute'),
blockedWordMessage(' share-panel ', new RegExp('(?:^|\\s)share-panel(?:$|\\s)', 'g'), 'thing-ad-name-thing', 'attribute'),
],
},
{
description: 'should works with word boundaries',
html: `
<!-- htmlhint id-class-ad-disabled:false -->
<html>
<head></head>
<body>
What do we have here?
<thing-ad-name-thing class="share-panel"></thing-ad-name-thing>
<thing-ad-name-thing class="other class names before share-panel"></thing-ad-name-thing>
<thing-ad-name-thing class="share-panel other class names after"></thing-ad-name-thing>
<thing-ad-name-thing class="class names before share-panel and class names after"></thing-ad-name-thing>
</body>
</html>`,
ruleOptions: { attributeValues: ['\\bshare-panel\\b'] },
otherOptions: { 'id-class-ad-disabled': true },
failures: [
blockedWordMessage('share-panel', new RegExp('\\bshare-panel\\b', 'g'), 'thing-ad-name-thing', 'attribute'),
blockedWordMessage('share-panel', new RegExp('\\bshare-panel\\b', 'g'), 'thing-ad-name-thing', 'attribute'),
blockedWordMessage('share-panel', new RegExp('\\bshare-panel\\b', 'g'), 'thing-ad-name-thing', 'attribute'),
blockedWordMessage('share-panel', new RegExp('\\bshare-panel\\b', 'g'), 'thing-ad-name-thing', 'attribute'),
],
},
{
description: 'should get disabled by htmlhint comment',
html: `
<!-- htmlhint ${blockWordsRuleName}:false -->
<html>
<head></head>
<body>
What do we have here?
<thing-ad-name-thing class="bad-name"></thing-ad-name-thing>
</body>
</html>`,
ruleOptions: { all: ['bad-name'] },
},
{
description: 'should block only attributes when set to do so',
html: `
<html>
<head></head>
<body>
What do we have here?
<thing-bad-name-thing class="bad-name lots of other words bad-name more words bad-name"></thing-bad-name-thing>
</body>
</html>`,
ruleOptions: { attributeValues: ['bad-name'] },
failures: [
blockedWordMessage('bad-name', new RegExp('bad-name', 'g'), 'thing-bad-name-thing', 'attribute'),
blockedWordMessage('bad-name', new RegExp('bad-name', 'g'), 'thing-bad-name-thing', 'attribute'),
blockedWordMessage('bad-name', new RegExp('bad-name', 'g'), 'thing-bad-name-thing', 'attribute'),
],
},
{
description: 'should block attribute names',
html: `
<html>
<head></head>
<body>
What do we have here?
<thing-bad-name-thing bad-attribute-name="whatever"></thing-bad-name-thing>
</body>
</html>`,
ruleOptions: { attributeNames: ['bad-attribute-name'] },
failures: [
blockedWordMessage('bad-attribute-name', new RegExp('bad-attribute-name', 'g'), 'thing-bad-name-thing', 'attribute'),
],
},
{
description: 'should block a word in tag names only',
html: `<html><head></head><body>What do we have here?<thing-bad-name-thing class="bad-name"></thing-bad-name-thing></body></html>`,
ruleOptions: { tagNames: ['bad-name'] },
failures: [
blockedWordMessage('bad-name', new RegExp('bad-name', 'g'), 'thing-bad-name-thing', 'tag name'),
],
},
{
description: 'should not block anything when no options are given',
html: `<html><head></head><body>What do we have here?<thing-bad-name-thing class="bad-name"></thing-bad-name-thing></body></html>`,
ruleOptions: {},
},
],
});
exports.default = exports.BlockWordsRule;