UNPKG

@scarlet-mesh/mcp-rhds

Version:

RHDS MCP Server - All-in-One Model Context Protocol server for Red Hat Design System components with manifest discovery, HTML validation, and developer tooling

189 lines (188 loc) 8.41 kB
import { parse } from 'parse5'; import { BaseService } from './BaseService.js'; import { RHDS_STANDARDS, DEFAULT_PACKAGE } from '../constants/index.js'; import { IdGeneratorService } from './IdGeneratorService.js'; export class ValidationService extends BaseService { manifestService; constructor(manifestService) { super(); this.manifestService = manifestService; } validateId(id, componentType) { const idGenerator = new IdGeneratorService(); return idGenerator.validateIdFormat(id, componentType); } /** * Validates a component's HTML against RHDS standards. * Checks for valid attributes, required attributes, accessibility requirements, and ID conventions. * Returns a ValidationResult object containing errors, warnings, and suggestions. */ async validateComponent(html, options = {}) { const { checkAccessibility = true, checkIds = true, packageName = DEFAULT_PACKAGE } = options; const result = { isValid: true, errors: [], warnings: [], suggestions: [] }; const components = await this.manifestService.getComponents(packageName); const componentMap = new Map(components.map(c => [c.tagName, c])); try { const document = parse(html); this.validateDocument(document, componentMap, result, { checkAccessibility, checkIds }); } catch (error) { result.errors.push({ type: 'unknown-element', message: `Parse error: ${error instanceof Error ? error.message : 'Unknown error'}` }); result.isValid = false; } return result; } /** * Validates attributes of a component against its definition. * Checks for valid, invalid, and missing attributes based on the component's schema. * Returns an object with valid, invalid, and missing attributes. */ validateAttributes(component, attributes) { const validAttrNames = new Set(component.attributes.map(a => a.name)); const providedAttrs = new Set(Object.keys(attributes)); const valid = Object.keys(attributes).filter(name => name === 'id' || validAttrNames.has(name)); const invalid = Object.keys(attributes).filter(name => name !== 'id' && !validAttrNames.has(name)); const missing = component.requiredAttributes.filter(attr => !providedAttrs.has(attr)); return { valid, invalid, missing }; } /** * Validates a document's structure and components against RHDS standards. * Recursively checks each node for valid components, attributes, and accessibility requirements. */ validateDocument(doc, componentMap, result, options) { const validateNode = (node) => { if (node.nodeName && node.nodeName.includes('-')) { this.validateComponentNode(node, componentMap, result, options); } if (node.childNodes) { for (const child of node.childNodes) { validateNode(child); } } }; if (doc.childNodes) { for (const child of doc.childNodes) { validateNode(child); } } } /** * Validates a component node against the RHDS standards. * Checks if the node is a valid RHDS component, validates its attributes, and checks for accessibility requirements. * Updates the result object with errors, warnings, and suggestions. */ validateComponentNode(node, componentMap, result, options) { const tagName = node.nodeName.toLowerCase(); const component = componentMap.get(tagName); if (!component) { if (tagName.startsWith('rh-')) { result.errors.push({ type: 'unknown-element', message: `Unknown RHDS component: ${tagName}`, element: tagName }); result.isValid = false; } return; } const attrs = node.attrs || []; const providedAttrs = new Set(attrs.map((a) => String(a.name))); this.validateComponentAttributes(component, attrs, providedAttrs, result, options); } /** * Validates the attributes of a component against its definition. * Checks for valid, invalid, and missing attributes based on the component's schema. * Updates the result object with errors, warnings, and suggestions. */ validateComponentAttributes(component, attrs, providedAttrs, result, options) { const validAttrNames = new Set(component.attributes.map(a => a.name)); // Validate ID if (options.checkIds) { const idAttr = attrs.find((a) => a.name === 'id'); this.validateComponentId(idAttr, component.tagName, result); } // Validate attributes for (const attr of attrs) { if (attr.name !== 'id' && !validAttrNames.has(attr.name)) { result.errors.push({ type: 'invalid-attribute', message: `Invalid attribute "${attr.name}" on element ${component.tagName}`, element: component.tagName, attribute: attr.name }); result.isValid = false; } } // Check missing required attributes for (const requiredAttr of component.requiredAttributes) { if (!providedAttrs.has(requiredAttr)) { result.errors.push({ type: 'missing-required-attribute', message: `Missing required attribute "${requiredAttr}" on element ${component.tagName}`, element: component.tagName, attribute: requiredAttr }); result.isValid = false; } } // Check accessibility if (options.checkAccessibility) { this.validateAccessibilityAttributes(component, providedAttrs, result); } } /** * Validates the ID of a component. * Checks if the ID is present, follows RHDS conventions, and suggests corrections if necessary. * Updates the result object with warnings and suggestions. */ validateComponentId(idAttr, tagName, result) { const idGenerator = new IdGeneratorService(); if (!idAttr) { result.warnings.push({ type: 'missing-documentation', message: `Component ${tagName} should have an ID for better accessibility and styling`, element: tagName }); result.suggestions.push(`Add id="${idGenerator.generateId(tagName.replace('rh-', ''), 'component')}" to ${tagName}`); } else { const idValidation = idGenerator.validateIdFormat(idAttr.value, tagName); if (!idValidation.isValid) { result.warnings.push({ type: 'naming-convention', message: `ID "${idAttr.value}" doesn't follow RHDS conventions: ${idValidation.errors.join(', ')}`, element: tagName }); if (idValidation.suggestion) { result.suggestions.push(`Consider using id="${idValidation.suggestion}" instead`); } } } } /** * Validates accessibility attributes of a component. * Checks if required accessibility attributes are present and suggests corrections if necessary. * Updates the result object with warnings and suggestions. */ validateAccessibilityAttributes(component, providedAttrs, result) { const accessibilityAttrs = RHDS_STANDARDS.accessibilityRequirements[component.tagName] || []; for (const accessAttr of accessibilityAttrs) { if (!providedAttrs.has(accessAttr)) { result.warnings.push({ type: 'accessibility-concern', message: `Missing accessibility attribute "${accessAttr}" on element ${component.tagName}`, element: component.tagName }); result.suggestions.push(`Add ${accessAttr}="appropriate value" to ${component.tagName} for better accessibility`); } } } }