UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

844 lines (843 loc) 37.5 kB
import { BaseLanguageHandler } from './base.js'; import { getNodeText } from '../astAnalyzer.js'; import logger from '../../../logger.js'; import path from 'path'; export class HtmlHandler extends BaseLanguageHandler { options; getFunctionQueryPatterns() { return [ 'element', 'script_element', 'style_element', 'start_tag', 'attribute' ]; } getClassQueryPatterns() { return [ 'document', 'element', 'doctype' ]; } getImportQueryPatterns() { return [ 'element', 'attribute' ]; } extractFunctionName(node, sourceCode, _options) { try { if (node.type === 'element') { const tagNameNode = node.childForFieldName('tag_name'); if (tagNameNode) { const tagName = getNodeText(tagNameNode, sourceCode); if (tagName === 'script') { return 'script_element'; } else if (tagName === 'style') { return 'style_element'; } else if (tagName === 'link' && this.hasStylesheetAttribute(node, sourceCode)) { return 'stylesheet_link'; } else if (tagName === 'form') { return this.extractFormName(node, sourceCode); } else if (tagName === 'input' || tagName === 'button') { return this.extractInputName(node, sourceCode); } else if (tagName === 'a') { return this.extractLinkName(node, sourceCode); } else if (['div', 'section', 'article', 'main', 'aside', 'header', 'footer', 'nav'].includes(tagName)) { return this.extractContainerName(node, sourceCode); } else if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { return this.extractHeadingName(node, sourceCode); } else if (['ul', 'ol', 'dl'].includes(tagName)) { return this.extractListName(node, sourceCode); } else if (tagName === 'table') { return this.extractTableName(node, sourceCode); } return `element_${tagName}`; } } if (node.type === 'script_element') { const attributesNode = node.childForFieldName('start_tag')?.childForFieldName('attributes'); if (attributesNode) { const typeAttr = this.findAttribute(attributesNode, 'type', sourceCode); if (typeAttr) { const typeValue = this.getAttributeValue(typeAttr, sourceCode); if (typeValue === 'module') { return 'es_module_script'; } else if (typeValue === 'application/json') { return 'json_script'; } else if (typeValue === 'text/template' || typeValue === 'text/x-template') { return 'template_script'; } } const srcAttr = this.findAttribute(attributesNode, 'src', sourceCode); if (srcAttr) { const srcValue = this.getAttributeValue(srcAttr, sourceCode); if (srcValue) { const filename = path.basename(srcValue); return `script_${filename}`; } } const idAttr = this.findAttribute(attributesNode, 'id', sourceCode); if (idAttr) { const idValue = this.getAttributeValue(idAttr, sourceCode); if (idValue) { return `script_${idValue}`; } } } return 'inline_script'; } if (node.type === 'style_element') { return 'inline_style'; } if (node.type === 'start_tag') { const tagNameNode = node.childForFieldName('tag_name'); if (tagNameNode) { return `tag_${getNodeText(tagNameNode, sourceCode)}`; } } if (node.type === 'attribute') { const nameNode = node.childForFieldName('name'); if (nameNode) { const name = getNodeText(nameNode, sourceCode); if (name.startsWith('on')) { return `event_${name.substring(2)}`; } if (name.startsWith('data-')) { return `data_${name.substring(5)}`; } if (['id', 'class', 'name', 'src', 'href', 'rel', 'type'].includes(name)) { const valueNode = node.childForFieldName('value'); if (valueNode) { const value = getNodeText(valueNode, sourceCode).replace(/^["']|["']$/g, ''); return `${name}_${value}`; } } return `attribute_${name}`; } } return 'html_element'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting HTML function name'); return 'html_element'; } } hasStylesheetAttribute(node, sourceCode) { try { const startTagNode = node.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { const relAttr = this.findAttribute(attributesNode, 'rel', sourceCode); if (relAttr) { const relValue = this.getAttributeValue(relAttr, sourceCode); return relValue === 'stylesheet'; } } } return false; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error checking if HTML element has stylesheet attribute'); return false; } } findAttribute(attributesNode, name, sourceCode) { try { for (let i = 0; i < attributesNode.childCount; i++) { const attr = attributesNode.child(i); if (attr?.type === 'attribute') { const nameNode = attr.childForFieldName('name'); if (nameNode && getNodeText(nameNode, sourceCode) === name) { return attr; } } } return null; } catch (error) { logger.warn({ err: error, nodeType: attributesNode.type }, 'Error finding HTML attribute'); return null; } } getAttributeValue(attributeNode, sourceCode) { try { const valueNode = attributeNode.childForFieldName('value'); if (valueNode) { return getNodeText(valueNode, sourceCode).replace(/^["']|["']$/g, ''); } return ''; } catch (error) { logger.warn({ err: error, nodeType: attributeNode.type }, 'Error getting HTML attribute value'); return ''; } } extractFormName(node, sourceCode) { try { const startTagNode = node.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { const idAttr = this.findAttribute(attributesNode, 'id', sourceCode); if (idAttr) { const idValue = this.getAttributeValue(idAttr, sourceCode); if (idValue) { return `form_${idValue}`; } } const nameAttr = this.findAttribute(attributesNode, 'name', sourceCode); if (nameAttr) { const nameValue = this.getAttributeValue(nameAttr, sourceCode); if (nameValue) { return `form_${nameValue}`; } } const actionAttr = this.findAttribute(attributesNode, 'action', sourceCode); if (actionAttr) { const actionValue = this.getAttributeValue(actionAttr, sourceCode); if (actionValue) { const actionPath = actionValue.split('?')[0]; const actionName = path.basename(actionPath); return `form_${actionName}`; } } const methodAttr = this.findAttribute(attributesNode, 'method', sourceCode); if (methodAttr) { const methodValue = this.getAttributeValue(methodAttr, sourceCode); if (methodValue) { return `${methodValue.toLowerCase()}_form`; } } } } return 'form'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting HTML form name'); return 'form'; } } extractInputName(node, sourceCode) { try { const startTagNode = node.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { const typeAttr = this.findAttribute(attributesNode, 'type', sourceCode); let typeValue = ''; if (typeAttr) { typeValue = this.getAttributeValue(typeAttr, sourceCode); } const idAttr = this.findAttribute(attributesNode, 'id', sourceCode); if (idAttr) { const idValue = this.getAttributeValue(idAttr, sourceCode); if (idValue) { return typeValue ? `${typeValue}_${idValue}` : `input_${idValue}`; } } const nameAttr = this.findAttribute(attributesNode, 'name', sourceCode); if (nameAttr) { const nameValue = this.getAttributeValue(nameAttr, sourceCode); if (nameValue) { return typeValue ? `${typeValue}_${nameValue}` : `input_${nameValue}`; } } if (typeValue) { return `${typeValue}_input`; } } } const tagNameNode = node.childForFieldName('tag_name'); if (tagNameNode) { return `${getNodeText(tagNameNode, sourceCode)}_element`; } return 'input_element'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting HTML input name'); return 'input_element'; } } extractLinkName(node, sourceCode) { try { const startTagNode = node.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { const idAttr = this.findAttribute(attributesNode, 'id', sourceCode); if (idAttr) { const idValue = this.getAttributeValue(idAttr, sourceCode); if (idValue) { return `link_${idValue}`; } } const hrefAttr = this.findAttribute(attributesNode, 'href', sourceCode); if (hrefAttr) { const hrefValue = this.getAttributeValue(hrefAttr, sourceCode); if (hrefValue) { if (hrefValue.startsWith('#')) { return `anchor_${hrefValue.substring(1)}`; } else if (hrefValue.startsWith('mailto:')) { return 'email_link'; } else if (hrefValue.startsWith('tel:')) { return 'phone_link'; } else if (hrefValue.startsWith('javascript:')) { return 'javascript_link'; } else { const hrefPath = hrefValue.split('?')[0]; const hrefName = path.basename(hrefPath); return `link_${hrefName}`; } } } } } const textNode = this.findTextNode(node); if (textNode) { const text = getNodeText(textNode, sourceCode).trim(); if (text.length > 0) { const linkText = text.substring(0, 20).toLowerCase().replace(/[^a-z0-9]+/g, '_'); return `link_${linkText}`; } } return 'link'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting HTML link name'); return 'link'; } } findTextNode(node) { try { for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child?.type === 'text') { return child; } else if (child?.childCount) { const textNode = this.findTextNode(child); if (textNode) { return textNode; } } } return null; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error finding HTML text node'); return null; } } extractContainerName(node, sourceCode) { try { const startTagNode = node.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { const idAttr = this.findAttribute(attributesNode, 'id', sourceCode); if (idAttr) { const idValue = this.getAttributeValue(idAttr, sourceCode); if (idValue) { return `container_${idValue}`; } } const classAttr = this.findAttribute(attributesNode, 'class', sourceCode); if (classAttr) { const classValue = this.getAttributeValue(classAttr, sourceCode); if (classValue) { const classes = classValue.split(/\s+/); if (classes.length > 0) { return `container_${classes[0]}`; } } } const roleAttr = this.findAttribute(attributesNode, 'role', sourceCode); if (roleAttr) { const roleValue = this.getAttributeValue(roleAttr, sourceCode); if (roleValue) { return `${roleValue}_container`; } } } } const tagNameNode = node.childForFieldName('tag_name'); if (tagNameNode) { return `${getNodeText(tagNameNode, sourceCode)}_container`; } return 'container'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting HTML container name'); return 'container'; } } extractHeadingName(node, sourceCode) { try { const startTagNode = node.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { const idAttr = this.findAttribute(attributesNode, 'id', sourceCode); if (idAttr) { const idValue = this.getAttributeValue(idAttr, sourceCode); if (idValue) { return `heading_${idValue}`; } } } } const textNode = this.findTextNode(node); if (textNode) { const text = getNodeText(textNode, sourceCode).trim(); if (text.length > 0) { const headingText = text.substring(0, 20).toLowerCase().replace(/[^a-z0-9]+/g, '_'); return `heading_${headingText}`; } } const tagNameNode = node.childForFieldName('tag_name'); if (tagNameNode) { return `${getNodeText(tagNameNode, sourceCode)}_heading`; } return 'heading'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting HTML heading name'); return 'heading'; } } extractListName(node, sourceCode) { try { const startTagNode = node.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { const idAttr = this.findAttribute(attributesNode, 'id', sourceCode); if (idAttr) { const idValue = this.getAttributeValue(idAttr, sourceCode); if (idValue) { return `list_${idValue}`; } } const classAttr = this.findAttribute(attributesNode, 'class', sourceCode); if (classAttr) { const classValue = this.getAttributeValue(classAttr, sourceCode); if (classValue) { const classes = classValue.split(/\s+/); if (classes.length > 0) { return `list_${classes[0]}`; } } } } } const tagNameNode = node.childForFieldName('tag_name'); if (tagNameNode) { const tagName = getNodeText(tagNameNode, sourceCode); if (tagName === 'ul') { return 'unordered_list'; } else if (tagName === 'ol') { return 'ordered_list'; } else if (tagName === 'dl') { return 'definition_list'; } } return 'list'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting HTML list name'); return 'list'; } } extractTableName(node, sourceCode) { try { const startTagNode = node.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { const idAttr = this.findAttribute(attributesNode, 'id', sourceCode); if (idAttr) { const idValue = this.getAttributeValue(idAttr, sourceCode); if (idValue) { return `table_${idValue}`; } } const classAttr = this.findAttribute(attributesNode, 'class', sourceCode); if (classAttr) { const classValue = this.getAttributeValue(classAttr, sourceCode); if (classValue) { const classes = classValue.split(/\s+/); if (classes.length > 0) { return `table_${classes[0]}`; } } } } } for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child?.type === 'element') { const tagNameNode = child.childForFieldName('tag_name'); if (tagNameNode && getNodeText(tagNameNode, sourceCode) === 'caption') { const textNode = this.findTextNode(child); if (textNode) { const text = getNodeText(textNode, sourceCode).trim(); if (text.length > 0) { const captionText = text.substring(0, 20).toLowerCase().replace(/[^a-z0-9]+/g, '_'); return `table_${captionText}`; } } } } } return 'table'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting HTML table name'); return 'table'; } } extractClassName(node, sourceCode) { try { if (node.type === 'document') { const titleNode = this.findTitleNode(node); if (titleNode) { const textNode = this.findTextNode(titleNode); if (textNode) { const text = getNodeText(textNode, sourceCode).trim(); if (text.length > 0) { const titleText = text.substring(0, 30).toLowerCase().replace(/[^a-z0-9]+/g, '_'); return `HTML_${titleText}`; } } } if (this.options?.filePath) { return `HTML_${path.basename(this.options.filePath, path.extname(this.options.filePath))}`; } } else if (node.type === 'element') { const tagNameNode = node.childForFieldName('tag_name'); if (tagNameNode) { const tagName = getNodeText(tagNameNode, sourceCode); if (tagName === 'html') { return 'HTML_Document'; } else if (tagName === 'head') { return 'HTML_Head'; } else if (tagName === 'body') { return 'HTML_Body'; } const startTagNode = node.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { const idAttr = this.findAttribute(attributesNode, 'id', sourceCode); if (idAttr) { const idValue = this.getAttributeValue(idAttr, sourceCode); if (idValue) { return `${tagName}_${idValue}`; } } } } return `${tagName}_element`; } } else if (node.type === 'doctype') { return 'HTML_Doctype'; } return 'HTML_Element'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting HTML class name'); return 'HTML_Element'; } } findTitleNode(node) { try { let headNode = null; for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child?.type === 'element') { const tagNameNode = child.childForFieldName('tag_name'); if (tagNameNode && getNodeText(tagNameNode, node.text) === 'html') { for (let j = 0; j < child.childCount; j++) { const htmlChild = child.child(j); if (htmlChild?.type === 'element') { const htmlChildTagNameNode = htmlChild.childForFieldName('tag_name'); if (htmlChildTagNameNode && getNodeText(htmlChildTagNameNode, node.text) === 'head') { headNode = htmlChild; break; } } } break; } } } if (headNode) { for (let i = 0; i < headNode.childCount; i++) { const child = headNode.child(i); if (child?.type === 'element') { const tagNameNode = child.childForFieldName('tag_name'); if (tagNameNode && getNodeText(tagNameNode, node.text) === 'title') { return child; } } } } return null; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error finding HTML title node'); return null; } } extractImportPath(node, sourceCode) { try { if (node.type === 'element') { const tagNameNode = node.childForFieldName('tag_name'); if (tagNameNode) { const tagName = getNodeText(tagNameNode, sourceCode); if (tagName === 'link') { const startTagNode = node.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { const hrefAttr = this.findAttribute(attributesNode, 'href', sourceCode); if (hrefAttr) { return this.getAttributeValue(hrefAttr, sourceCode); } } } } if (tagName === 'script') { const startTagNode = node.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { const srcAttr = this.findAttribute(attributesNode, 'src', sourceCode); if (srcAttr) { return this.getAttributeValue(srcAttr, sourceCode); } } } } if (tagName === 'img') { const startTagNode = node.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { const srcAttr = this.findAttribute(attributesNode, 'src', sourceCode); if (srcAttr) { return this.getAttributeValue(srcAttr, sourceCode); } } } } if (tagName === 'iframe') { const startTagNode = node.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { const srcAttr = this.findAttribute(attributesNode, 'src', sourceCode); if (srcAttr) { return this.getAttributeValue(srcAttr, sourceCode); } } } } } } else if (node.type === 'attribute') { const nameNode = node.childForFieldName('name'); if (nameNode) { const name = getNodeText(nameNode, sourceCode); if (name === 'href' || name === 'src' || name === 'data-src' || name === 'import') { const valueNode = node.childForFieldName('value'); if (valueNode) { return getNodeText(valueNode, sourceCode).replace(/^["']|["']$/g, ''); } } } } return 'unknown'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting HTML import path'); return 'unknown'; } } extractFunctionComment(node, sourceCode) { try { if (node.type === 'element') { const startTagNode = node.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { const titleAttr = this.findAttribute(attributesNode, 'title', sourceCode); if (titleAttr) { return this.getAttributeValue(titleAttr, sourceCode); } const altAttr = this.findAttribute(attributesNode, 'alt', sourceCode); if (altAttr) { return this.getAttributeValue(altAttr, sourceCode); } const ariaLabelAttr = this.findAttribute(attributesNode, 'aria-label', sourceCode); if (ariaLabelAttr) { return this.getAttributeValue(ariaLabelAttr, sourceCode); } } } } const current = node; let prev = current.previousNamedSibling; while (prev && prev.type !== 'comment') { prev = prev.previousNamedSibling; } if (prev && prev.type === 'comment') { const commentText = getNodeText(prev, sourceCode); return commentText .replace(/^<!--\s*|\s*-->$/g, '') .trim(); } return undefined; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting HTML function comment'); return undefined; } } extractClassComment(node, sourceCode) { try { if (node.type === 'document') { const metaDescription = this.findMetaDescription(node, sourceCode); if (metaDescription) { return metaDescription; } } const current = node; let prev = current.previousNamedSibling; while (prev && prev.type !== 'comment') { prev = prev.previousNamedSibling; } if (prev && prev.type === 'comment') { const commentText = getNodeText(prev, sourceCode); return commentText .replace(/^<!--\s*|\s*-->$/g, '') .trim(); } return undefined; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting HTML class comment'); return undefined; } } findMetaDescription(node, sourceCode) { try { let headNode = null; for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child?.type === 'element') { const tagNameNode = child.childForFieldName('tag_name'); if (tagNameNode && getNodeText(tagNameNode, sourceCode) === 'html') { for (let j = 0; j < child.childCount; j++) { const htmlChild = child.child(j); if (htmlChild?.type === 'element') { const htmlChildTagNameNode = htmlChild.childForFieldName('tag_name'); if (htmlChildTagNameNode && getNodeText(htmlChildTagNameNode, sourceCode) === 'head') { headNode = htmlChild; break; } } } break; } } } if (headNode) { for (let i = 0; i < headNode.childCount; i++) { const child = headNode.child(i); if (child?.type === 'element') { const tagNameNode = child.childForFieldName('tag_name'); if (tagNameNode && getNodeText(tagNameNode, sourceCode) === 'meta') { const startTagNode = child.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { const nameAttr = this.findAttribute(attributesNode, 'name', sourceCode); if (nameAttr && this.getAttributeValue(nameAttr, sourceCode) === 'description') { const contentAttr = this.findAttribute(attributesNode, 'content', sourceCode); if (contentAttr) { return this.getAttributeValue(contentAttr, sourceCode); } } } } } } } } return null; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error finding HTML meta description'); return null; } } detectFramework(sourceCode) { try { if (sourceCode.includes('bootstrap.') || sourceCode.includes('class="container') || sourceCode.includes('class="row')) { return 'bootstrap'; } if (sourceCode.includes('tailwind') || sourceCode.includes('class="text-') || sourceCode.includes('class="bg-')) { return 'tailwind'; } if (sourceCode.includes('react.') || sourceCode.includes('ReactDOM') || sourceCode.includes('data-reactroot')) { return 'react'; } if (sourceCode.includes('ng-') || sourceCode.includes('angular.') || sourceCode.includes('[(ngModel)]')) { return 'angular'; } if (sourceCode.includes('vue.') || sourceCode.includes('v-') || sourceCode.includes('data-v-')) { return 'vue'; } return null; } catch (error) { logger.warn({ err: error }, 'Error detecting HTML framework'); return null; } } }