UNPKG

@bernierllc/validators-html-syntax

Version:

HTML syntax validation primitive - malformed tags, nesting, duplicate IDs, unclosed tags

83 lines (81 loc) 3.25 kB
"use strict"; /* 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(); }