@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
JavaScript
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`);
}
}
}
}