no-invalid-html.js (74 lines of code) (raw):

import { XParser } from '@netflix/x-element/x-parser.js'; const onToken = () => {}; const validate = strings => XParser.parse(strings, onToken); // Attempts to put a squiggle beneath the whole _line_ which is problematic. const getLoc = (node, quasis, strings, context) => { if (context) { const { index, start } = context; const quasi = quasis[index]; if (quasi) { // Most cases will fall here. const startLine = quasi.loc.start.line; const startColumn = quasi.loc.start.column; const string = strings[index]; const stringPrefix = string.slice(0, start ?? 0); const problemStartLine = stringPrefix.split('\n').length; const problemLine = string.split('\n')[problemStartLine - 1]; const indentationMatch = problemLine.match(/(^ *)[^ ]/); const indentation = indentationMatch ? indentationMatch[1].length : 0; const line = startLine + problemStartLine - 1; if (line === startLine) { return { start: { line, column: startColumn + 1 + indentation }, end: { line, column: startColumn + 1 + problemLine.length } }; } else { return { start: { line, column: indentation }, end: { line, column: problemLine.length } }; } } else { // Certain exit criteria will fall here (e.g., missing closing tag). const nodeEndLine = node.loc.end.line; const nodeEndColumn = node.loc.end.column; const line = nodeEndLine; return { start: { line, column: nodeEndColumn - 1 }, end: { line, column: nodeEndColumn } }; } } else { return node.tag.loc; } }; // Shortens the full error since we will have contextualization within the IDE. const format = error => { const message = error.message; const firstLine = message.split('\n')[0]; return firstLine; }; const validateTaggedTemplateExpression = (context, node) => { if (node.tag.name === 'html') { const quasis = node.quasi.quasis; if (quasis) { const strings = quasis.map(value => value.value.cooked); strings.raw = quasis.map(value => value.value.raw); Object.freeze(strings.raw); Object.freeze(strings); try { validate(strings); } catch (error) { // https://eslint.org/docs/latest/extend/custom-rules#reporting-problems context.report({ node, message: format(error), loc: getLoc(node, quasis, strings, XParser.getErrorContext(error)), }); } } } }; export default { meta: { name: 'XElement', type: 'problem', docs: { description: 'Enforce html template validation for x-element.', url: 'https://github.com/Netflix/eslint-plugin-x-element', }, }, create: context => { return { TaggedTemplateExpression: node => { validateTaggedTemplateExpression(context, node); }, }; }, };