blob: a7643eb7f7ae3b34bf9a9c441ce7fcce5a4e8979 [file] [edit]
/* This file is a part of @mdn/browser-compat-data
* See LICENSE file for more information. */
import chalk from 'chalk-template';
import HTMLParser from '@desertnet/html-parser';
import { Linter, Logger, LinterData, VALID_ELEMENTS } from '../utils.js';
import {
BrowserName,
CompatStatement,
SupportStatement,
} from '../../types/types.js';
const parser = new HTMLParser();
/**
* Recursively test a DOM node for valid elements
* @param {any} node The DOM node to test
* @param {BrowserName} browser The browser the notes belong to
* @param {string} feature The identifier of the feature
* @param {logger} logger The logger to output errors to
*/
const testNode = (
node,
browser: BrowserName,
feature: string,
logger: Logger,
): void => {
if (node.type == 'TAG') {
const tag = node.tagName?.toLowerCase();
if (tag && !VALID_ELEMENTS.includes(tag)) {
// Ensure we're only using select nodes
logger.error(
chalk`Notes for {bold ${feature}} in {bold ${browser}} have a {bold disallowed HTML element} ({bold <${tag}>}). Allowed HTML elements are: ${VALID_ELEMENTS.join(
', ',
)}`,
);
}
// Ensure nodes only contain specific attributes
const attrs = node.attributes.map((x) => x._name);
if (tag === 'a') {
if (attrs.length !== 1 || !attrs.includes('href')) {
// Ensure 'a' nodes only contain an 'href'
logger.error(
chalk`Notes for {bold ${feature}} in {bold ${browser}} have an HTML element ({bold <${tag}>}) with {bold attributes other than href}. {bold <a>} elements may only have an {bold href} attribute.`,
);
}
} else {
if (attrs.length > 0) {
// Ensure nodes (besides 'a') contain no attributes
logger.error(
chalk`Notes for {bold ${feature}} in {bold ${browser}} have an HTML element ({bold <${tag}>}) with {bold attributes}. Elements other than {bold <a>} may {bold not} have any attributes.`,
);
}
}
}
for (const childNode of node.children || []) {
testNode(childNode, browser, feature, logger);
}
};
/**
* Test a string for valid HTML
* @param {string} string The string to test
* @param {BrowserName} browser The browser the notes belong to
* @param {string} feature The identifier of the feature
* @param {logger} logger The logger to output errors to
*/
const validateHTML = (
string: string,
browser: BrowserName,
feature: string,
logger: Logger,
): void => {
const htmlErrors = HTMLParser.validate(string);
if (htmlErrors.length === 0) {
// If HTML is valid, ensure we're only using valid elements
testNode(parser.parse(string), browser, feature, logger);
} else {
logger.error(
chalk`Notes for {bold ${feature}} in {bold ${browser}} have invalid HTML: ${htmlErrors
.map((x) => x._message)
.flat()}`,
);
}
if (string.includes(' ')) {
logger.error(
chalk`Notes for {bold ${feature}} in {bold ${browser}} have double-spaces. Notes are required to have single spaces only.`,
);
}
if (string.includes('\n')) {
logger.error(
chalk`Notes for {bold ${feature}} in {bold ${browser}} may not contain newlines.`,
);
}
};
/**
* Check the notes in the data
* @param {string|string[]} notes The notes to test
* @param {BrowserName} browser The browser the notes belong to
* @param {string} feature The identifier of the feature
* @param {logger} logger The logger to output errors to
*/
const checkNotes = (
notes: string | string[],
browser: BrowserName,
feature: string,
logger: Logger,
): void => {
if (Array.isArray(notes)) {
for (const note of notes) {
validateHTML(note, browser, feature, logger);
}
} else {
validateHTML(notes, browser, feature, logger);
}
};
/**
* Process the data for notes errors
* @param {CompatStatement} data The data to test
* @param {Logger} logger The logger to output errors to
* @param {string} feature The identifier of the feature
*/
const processData = (
data: CompatStatement,
logger: Logger,
feature: string,
): void => {
for (const [browser, support] of Object.entries(data.support) as [
BrowserName,
SupportStatement,
][]) {
if (Array.isArray(support)) {
for (const s of support) {
if (s.notes) {
checkNotes(s.notes, browser, feature, logger);
}
}
} else {
if (support.notes) {
checkNotes(support.notes, browser, feature, logger);
}
}
}
};
export default {
name: 'Notes',
description: 'Test the notes in each support statement',
scope: 'feature',
/**
* Test the data
* @param {Logger} logger The logger to output errors to
* @param {LinterData} root The data to test
*/
check: (logger: Logger, { data, path: { full } }: LinterData) => {
processData(data, logger, full);
},
} as Linter;