@bernierllc/validators-html-syntax
Version:
HTML syntax validation primitive - malformed tags, nesting, duplicate IDs, unclosed tags
83 lines (81 loc) • 3.25 kB
JavaScript
;
/*
Copyright (c) 2025 Bernier LLC
This file is licensed to the client under a limited-use license.
The client may use and modify this code *only within the scope of the project it was delivered for*.
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.duplicateIdsRule = void 0;
const validators_core_1 = require("@bernierllc/validators-core");
/**
* Detects duplicate ID attributes in HTML
*/
exports.duplicateIdsRule = (0, validators_core_1.defineRule)({
meta: {
id: 'html-syntax/duplicate-ids',
title: 'Duplicate ID Attributes',
description: 'Detects duplicate ID attributes in HTML elements',
domain: 'parsing',
tags: ['html', 'syntax', 'ids'],
fixable: false,
},
create: (ctx) => {
return (html) => {
const doc = ctx.utils.parseHtml(html);
if (!doc) {
return;
}
// Track all IDs and their elements
const idMap = new Map();
const elements = doc.querySelectorAll('[id]');
for (const element of elements) {
const id = element.getAttribute('id');
if (!id)
continue;
if (!idMap.has(id)) {
idMap.set(id, []);
}
idMap.get(id).push(element);
}
// Report duplicates
for (const [id, elements] of idMap.entries()) {
if (elements.length > 1) {
const locations = elements.map((el) => {
const tagName = el.tagName.toLowerCase();
const path = getElementPath(el);
return `<${tagName} id="${id}"> at ${path}`;
});
ctx.report({
message: `Duplicate ID "${id}" found ${elements.length} times in the document`,
severity: 'error',
domain: 'parsing',
tags: ['html', 'duplicate-id'],
suggestion: `Each ID must be unique. Found duplicate ID in: ${locations.join(', ')}`,
fixable: false,
evidence: {
snippet: elements[0].outerHTML.substring(0, 200),
context: {
id,
occurrences: elements.length,
locations,
},
},
});
}
}
};
},
});
function getElementPath(element) {
const path = [];
let current = element;
while (current && current.parentElement) {
const tagName = current.tagName.toLowerCase();
const siblings = Array.from(current.parentElement.children);
const index = siblings.indexOf(current);
path.unshift(`${tagName}:nth-child(${index + 1})`);
current = current.parentElement;
}
return path.join(' > ') || element.tagName.toLowerCase();
}