"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.JqhtmlHoverProvider = exports.JqhtmlDefinitionProvider = void 0; const vscode = __importStar(require("vscode")); const componentIndex_1 = require("./componentIndex"); /** * JQHTML Definition Provider * * Provides "Go to Definition" functionality for JQHTML components. * When a user Ctrl/Cmd+clicks on a component name or uses F12, * this provider will locate the component definition. */ class JqhtmlDefinitionProvider { constructor(componentIndex) { this.componentIndex = componentIndex; } provideDefinition(document, position, token) { return __awaiter(this, void 0, void 0, function* () { console.log(`\n========== JQHTML: provideDefinition called ==========`); console.log(`File: ${document.uri.fsPath}`); console.log(`Position: Line ${position.line + 1}, Character ${position.character}`); const line = document.lineAt(position.line).text; console.log(`JQHTML: Line text: "${line}"`); // Check if we're in a $ attribute with unquoted value const dollarAttrResult = this.checkDollarAttributeContext(document, position, line); if (dollarAttrResult) { console.log(`JQHTML: In $ attribute context:`, dollarAttrResult); return yield this.handleDollarAttributeDefinition(document, position, dollarAttrResult); } // IMPORTANT: Check for slot syntax BEFORE extracting word // This prevents slot names from being treated as component names // Check if we're in a slot tag by looking for 0 ? line.charAt(wordRange.start.character - 1) : ''; // Check if this word is inside an extends="" attribute value let beforeWord = line.substring(0, wordRange.start.character); if (beforeWord.match(/extends\s*=\s*["']?\s*$/)) { console.log(`JQHTML: "${word}" is in extends attribute, treating as component reference`); // This is in extends="ComponentName", treat as component reference const componentDef = this.componentIndex.findComponent(word); if (!componentDef) { console.log(`JQHTML: Component '${word}' not found in index`); return undefined; } // Verify the file still exists try { yield vscode.workspace.fs.stat(componentDef.uri); } catch (error) { console.log(`JQHTML: Component '${word}' definition file no longer exists`); this.componentIndex.reindexWorkspace(); return undefined; } console.log(`JQHTML: Found definition for '${word}' at ${componentDef.uri.fsPath}:${componentDef.position.line + 1}`); return new vscode.Location(componentDef.uri, componentDef.position); } // Check if this word is in a tag context // Look for < before the component name (accounting for Define: prefix) let isInTagContext = false; // Check for opening tag: after the word, and < before it somewhere if ((afterWord.match(/^[\s>]/) || afterWord.length === 0) && beforeWord.includes('<')) { // Verify we're not in an attribute value const lastLessThan = beforeWord.lastIndexOf('<'); const lastGreaterThan = beforeWord.lastIndexOf('>'); if (lastLessThan > lastGreaterThan) { isInTagContext = true; } } } if (!isInTagContext) { console.log(`JQHTML: "${word}" not in tag context, ignoring`); return undefined; } console.log(`JQHTML: "${word}" IS in tag context, looking up in index...`); console.log(`JQHTML: Current index size: ${this.componentIndex.getAllComponentNames().length} components`); console.log(`JQHTML: Index contains: ${this.componentIndex.getAllComponentNames().join(', ')}`); // Look up the component in our index const componentDef = this.componentIndex.findComponent(word); if (!componentDef) { // Component not found in index // // DIAGNOSTIC HISTORY: // - Issue: Go to Definition goes to random CSS file instead of component // - Symptom: Component correctly identified (e.g., "Contacts_Datagrid") // Tag context correctly detected // BUT findComponent() returns undefined // - Root cause: Component not indexed by the indexing system // // POSSIBLE REASONS: // 1. Component file not in workspace or not discovered during indexing // 2. Component definition syntax not matching regex in componentIndex.ts // 3. File watcher didn't detect the file creation/modification // 4. Index hasn't run yet (extension just activated) // // WHEN THIS RETURNS UNDEFINED: // VS Code falls back to built-in text search providers, which may find // the component name in CSS class names (.Contacts_Datagrid), causing // navigation to wrong files. // // NEXT DIAGNOSTIC STEPS: // 1. Check if component file exists and is in workspace // 2. Verify Define tag syntax matches indexing regex // 3. Check file watcher is working (create new component, see if indexed) // 4. Manually trigger reindex and check if component appears console.log(`JQHTML: Component '${word}' not found in index`); console.log(`JQHTML: RETURNING UNDEFINED - VS Code may fall back to other definition providers!`); return undefined; } // Verify the file still exists (catches stale index entries) try { yield vscode.workspace.fs.stat(componentDef.uri); } catch (error) { // File no longer exists - trigger reindex and return undefined console.log(`JQHTML: Component '${word}' definition file no longer exists: ${componentDef.uri.fsPath}`); console.log(`JQHTML: Triggering workspace reindex...`); this.componentIndex.reindexWorkspace(); // Async, non-blocking return undefined; } console.log(`JQHTML: Found definition for '${word}' at ${componentDef.uri.fsPath}:${componentDef.position.line + 1}`); console.log(`JQHTML: RETURNING LOCATION - This should be the ONLY result!`); console.log(`========== JQHTML: provideDefinition done ==========\n`); // Return the location of the component definition return new vscode.Location(componentDef.uri, componentDef.position); }); } /** * Check if cursor is in a $ attribute with unquoted value like $handler=Controller.method * Returns the parsed segments and position info, or undefined if not in such context */ checkDollarAttributeContext(document, position, line) { var _a; const char = position.character; const beforeCursor = line.substring(0, char); const afterCursor = line.substring(char); // Look for pattern: $attributeName=FirstSegment.secondSegment // Match: $ followed by word, =, then identifier chains const dollarAttrMatch = beforeCursor.match(/\$\w+\s*=\s*([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)$/); if (!dollarAttrMatch) { return undefined; } // Get the full expression (before cursor + until space or >) const expressionBeforeCursor = dollarAttrMatch[1]; const expressionAfterCursor = ((_a = afterCursor.match(/^([a-zA-Z0-9_$]*)/)) === null || _a === void 0 ? void 0 : _a[1]) || ''; const fullExpression = expressionBeforeCursor + expressionAfterCursor; console.log(`JQHTML: Full $ attribute expression: "${fullExpression}"`); // Split by dots const segments = fullExpression.split('.'); // Determine which segment we're on based on cursor position const expressionStartChar = char - expressionBeforeCursor.length; const cursorOffsetInExpression = char - expressionStartChar; let currentSegmentIndex = 0; let charCount = 0; for (let i = 0; i < segments.length; i++) { const segmentLength = segments[i].length; if (cursorOffsetInExpression <= charCount + segmentLength) { currentSegmentIndex = i; break; } charCount += segmentLength + 1; // +1 for the dot } console.log(`JQHTML: Cursor on segment ${currentSegmentIndex}: "${segments[currentSegmentIndex]}"`); // Handle "this" keyword - resolve to containing Define component let className = segments[0]; if (className === 'this') { const containingComponent = this.findContainingDefineComponent(document, position.line); if (!containingComponent) { console.log(`JQHTML: "this" used but no containing Define component found`); return undefined; } className = containingComponent; console.log(`JQHTML: Resolved "this" to component: ${className}`); } if (currentSegmentIndex === 0) { // First segment - just the class name return { className, isFirstSegment: true }; } else { // Second or later segment - class + member return { className, memberName: segments[currentSegmentIndex], isFirstSegment: false }; } } /** * Find the containing for a given line */ findContainingDefineComponent(document, currentLine) { // Search backwards from current line to find for (let i = currentLine; i >= 0; i--) { const lineText = document.lineAt(i).text; const defineMatch = lineText.match(/ * - Only searches JS (PHP search skipped) */ handleDollarAttributeDefinition(document, position, context) { return __awaiter(this, void 0, void 0, function* () { console.log(`JQHTML: Looking for class "${context.className}"${context.memberName ? `, member "${context.memberName}"` : ''}`); const isThisKeyword = context.className === 'this'; // Priority 1: Search PHP classes/methods (skip if "this" keyword) if (!isThisKeyword) { const phpResult = yield this.searchPhpDefinition(context); if (phpResult) { return phpResult; } } // Priority 2: Search JS classes/methods const jsClassResult = yield this.searchJsClassDefinition(context); if (jsClassResult) { return jsClassResult; } // Priority 3: Search standalone JS functions (only for single segment, not "this") if (context.isFirstSegment && !context.memberName && !isThisKeyword) { const jsFunctionResult = yield this.searchStandaloneJsFunction(context.className); if (jsFunctionResult) { return jsFunctionResult; } } console.log(`JQHTML: No definition found for "${context.className}"${context.memberName ? `.${context.memberName}` : ''}`); return undefined; }); } /** * Search for PHP class and optionally method * * IMPLEMENTATION: Uses VS Code's workspace symbol provider API to query Intelephense's symbol index. * * This approach: * - Leverages Intelephense's existing indexed symbol database * - No temporary documents or window management issues * - Fast symbol lookup via vscode.executeWorkspaceSymbolProvider * - Gracefully falls back if Intelephense not installed * - No manual indexing or file scanning required * * We query workspace symbols by name (class or method), then filter results * to find PHP symbols and navigate to their definitions. */ searchPhpDefinition(context) { return __awaiter(this, void 0, void 0, function* () { try { // Check if Intelephense is installed const intelephenseExt = vscode.extensions.getExtension('bmewburn.vscode-intelephense-client'); if (!intelephenseExt) { console.log(`JQHTML: Intelephense extension not installed, skipping PHP lookup`); return undefined; } // Search for the class first console.log(`JQHTML: Searching workspace symbols for PHP class: ${context.className}`); const classSymbols = yield vscode.commands.executeCommand('vscode.executeWorkspaceSymbolProvider', context.className); if (!classSymbols || classSymbols.length === 0) { console.log(`JQHTML: No workspace symbols found for class: ${context.className}`); return undefined; } // Filter to PHP class symbols const phpClassSymbol = classSymbols.find(s => s.name === context.className && s.kind === vscode.SymbolKind.Class && s.location.uri.fsPath.endsWith('.php')); if (!phpClassSymbol) { console.log(`JQHTML: No PHP class symbol found for: ${context.className}`); return undefined; } console.log(`JQHTML: Found PHP class ${context.className} in ${phpClassSymbol.location.uri.fsPath}`); // If we're looking for the class itself (first segment, no member) if (context.isFirstSegment && !context.memberName) { return phpClassSymbol.location; } // If we're looking for a method within the class if (context.memberName) { console.log(`JQHTML: Searching for method: ${context.memberName} in class ${context.className}`); // Search for the method name in workspace symbols const methodSymbols = yield vscode.commands.executeCommand('vscode.executeWorkspaceSymbolProvider', context.memberName); // Filter to methods in the same PHP file as the class const phpMethodSymbol = methodSymbols === null || methodSymbols === void 0 ? void 0 : methodSymbols.find(s => s.name === context.memberName && s.kind === vscode.SymbolKind.Method && s.location.uri.toString() === phpClassSymbol.location.uri.toString()); if (phpMethodSymbol) { console.log(`JQHTML: Found PHP method ${context.memberName} in ${phpMethodSymbol.location.uri.fsPath}`); return phpMethodSymbol.location; } // Method not found - fall back to class definition console.log(`JQHTML: Method ${context.memberName} not found, falling back to class definition`); return phpClassSymbol.location; } return phpClassSymbol.location; } catch (error) { console.error(`JQHTML: Error using workspace symbol provider:`, error); return undefined; } }); } /** * Search for JS class and optionally method */ searchJsClassDefinition(context) { return __awaiter(this, void 0, void 0, function* () { const jsFiles = yield vscode.workspace.findFiles('**/*.js', '**/node_modules/**'); for (const fileUri of jsFiles) { const fileDoc = yield vscode.workspace.openTextDocument(fileUri); const fileText = fileDoc.getText(); // Look for class definition: class ClassName const classRegex = new RegExp(`^\\s*(?:export\\s+)?class\\s+${context.className}\\b`, 'm'); const classMatch = classRegex.exec(fileText); if (classMatch) { console.log(`JQHTML: Found JS class ${context.className} in ${fileUri.fsPath}`); // If we're on the first segment, go to the class definition if (context.isFirstSegment && !context.memberName) { const classPos = fileDoc.positionAt(classMatch.index + classMatch[0].indexOf(context.className)); return new vscode.Location(fileUri, classPos); } // If we're on a later segment, try to find the method/property in the class if (context.memberName) { const memberLocation = this.findJsClassMember(fileDoc, fileText, classMatch.index, context.memberName); if (memberLocation) { console.log(`JQHTML: Found JS method ${context.memberName} in class ${context.className}`); return new vscode.Location(fileUri, memberLocation); } // Method not found, but we found the class - fall back to class definition console.log(`JQHTML: JS method ${context.memberName} not found, falling back to JS class definition`); const classPos = fileDoc.positionAt(classMatch.index + classMatch[0].indexOf(context.className)); return new vscode.Location(fileUri, classPos); } } } return undefined; }); } /** * Search for standalone JS function (not in a class) * Only called for single-segment expressions where first segment is not "this" */ searchStandaloneJsFunction(functionName) { return __awaiter(this, void 0, void 0, function* () { const jsFiles = yield vscode.workspace.findFiles('**/*.js', '**/node_modules/**'); for (const fileUri of jsFiles) { const fileDoc = yield vscode.workspace.openTextDocument(fileUri); const fileText = fileDoc.getText(); // Look for function declaration: function functionName // or const/let/var functionName = function const functionRegex = new RegExp(`^\\s*(?:export\\s+)?(?:async\\s+)?function\\s+${functionName}\\b`, 'm'); const functionMatch = functionRegex.exec(fileText); if (functionMatch) { console.log(`JQHTML: Found standalone JS function ${functionName} in ${fileUri.fsPath}`); const functionPos = fileDoc.positionAt(functionMatch.index + functionMatch[0].indexOf(functionName)); return new vscode.Location(fileUri, functionPos); } // Also check for: const functionName = ... const constFunctionRegex = new RegExp(`^\\s*(?:export\\s+)?(?:const|let|var)\\s+${functionName}\\s*=`, 'm'); const constMatch = constFunctionRegex.exec(fileText); if (constMatch) { console.log(`JQHTML: Found standalone JS function ${functionName} (const/let/var) in ${fileUri.fsPath}`); const functionPos = fileDoc.positionAt(constMatch.index + constMatch[0].indexOf(functionName)); return new vscode.Location(fileUri, functionPos); } } return undefined; }); } /** * Find a method or property within a JS class definition */ findJsClassMember(document, fileText, classStartIndex, memberName) { // Find the class body (starts at { after class declaration) const classBodyStart = fileText.indexOf('{', classStartIndex); if (classBodyStart === -1) return undefined; // Find matching closing brace let braceCount = 1; let classBodyEnd = classBodyStart + 1; while (classBodyEnd < fileText.length && braceCount > 0) { if (fileText[classBodyEnd] === '{') braceCount++; if (fileText[classBodyEnd] === '}') braceCount--; classBodyEnd++; } const classBody = fileText.substring(classBodyStart, classBodyEnd); // Look for method: methodName() { or property: methodName = const methodRegex = new RegExp(`^\\s*(?:async\\s+)?${memberName}\\s*[=(]`, 'm'); const methodMatch = methodRegex.exec(classBody); if (methodMatch) { const absoluteIndex = classBodyStart + methodMatch.index + methodMatch[0].indexOf(memberName); return document.positionAt(absoluteIndex); } return undefined; } /** * Handle goto definition for slot tags () * * IMPLEMENTATION SCOPE (Narrow, for now): * - Handles direct extends="ComponentName" on tags * - Handles direct invocation tags * - Does NOT traverse full inheritance chain (TODO: add later) * - Just looks for direct parent component * * LOGIC: * 1. Extract slot name from cursor position * 2. Find parent component: * - If inside , use Parent * - If inside invocation, use Parent * 3. Find Parent.jqhtml file * 4. Search for <%= content('SlotName') %> * 5. Navigate to that line */ handleSlotDefinition(document, position, slotName) { return __awaiter(this, void 0, void 0, function* () { console.log(`JQHTML: Handling slot definition for: ${slotName}`); // Find the parent component that defines this slot const parentComponentName = this.findParentComponentForSlot(document, position); if (!parentComponentName) { console.log(`JQHTML: Could not determine parent component for slot`); return undefined; } console.log(`JQHTML: Parent component for slot: ${parentComponentName}`); // Debug: Show what's in the index const allComponents = this.componentIndex.getAllComponentNames(); console.log(`JQHTML: Index currently contains ${allComponents.length} components:`, allComponents.join(', ')); // Find the parent component definition file const parentComponent = this.componentIndex.findComponent(parentComponentName); if (!parentComponent) { console.log(`JQHTML: Parent component '${parentComponentName}' not found in index`); return undefined; } console.log(`JQHTML: Found parent component file: ${parentComponent.uri.fsPath}`); // Search for content('SlotName') in the parent component file const slotUsageLocation = yield this.findSlotUsageInTemplate(parentComponent.uri, slotName); if (!slotUsageLocation) { console.log(`JQHTML: Slot usage content('${slotName}') not found in ${parentComponent.uri.fsPath}`); return undefined; } console.log(`JQHTML: Found slot usage at line ${slotUsageLocation.range.start.line + 1}`); return slotUsageLocation; }); } /** * Find the parent component that should define this slot * * Looks for either: * 1. - check if slots are top-level * 2. - find enclosing component invocation tag */ findParentComponentForSlot(document, position) { const currentLine = position.line; // Strategy 1: Look for where slots are at top level // Scan upward to find the Define tag let defineTagStartLine = -1; for (let i = currentLine; i >= 0; i--) { const lineText = document.lineAt(i).text; // Check if we found a = 0) { // Collect all lines from Define tag start until we find the closing > let tagContent = ''; for (let i = defineTagStartLine; i < document.lineCount; i++) { const lineText = document.lineAt(i).text; tagContent += lineText + ' '; // Stop when we find the closing > of the opening tag if (lineText.includes('>')) { break; } } // Now check if this multi-line tag has extends attribute const extendsMatch = tagContent.match(/\bextends\s*=\s*["']([A-Z][A-Za-z0-9_]*)["']/); if (extendsMatch) { const parentComponentName = extendsMatch[1]; console.log(`JQHTML: Found extends="${parentComponentName}" in Define tag`); // TODO: Verify that the slot is at top level (not nested inside other tags) // For now, we assume if we found a Define with extends, that's the parent return parentComponentName; } else { console.log(`JQHTML: Define tag found but no extends attribute`); } } // Strategy 2: Look for enclosing invocation tag // Scan upward to find opening tag let tagStack = []; for (let i = currentLine; i >= 0; i--) { const lineText = document.lineAt(i).text; // Find all component tags on this line (both opening and closing) // Component tags: or const tagRegex = /<\/?([A-Z][A-Za-z0-9_]*)[^>]*>/g; let match; // Collect all tags on this line const tagsOnLine = []; while ((match = tagRegex.exec(lineText)) !== null) { const fullMatch = match[0]; const componentName = match[1]; const isClosing = fullMatch.startsWith('= 0; j--) { const { tag, isClosing } = tagsOnLine[j]; if (isClosing) { // Closing tag - add to stack tagStack.push(tag); } else { // Opening tag if (tagStack.length > 0 && tagStack[tagStack.length - 1] === tag) { // This opening tag matches the last closing tag on stack - they cancel out tagStack.pop(); } else { // This is an unclosed opening tag - this is our parent! console.log(`JQHTML: Found enclosing component invocation: <${tag}>`); return tag; } } } } console.log(`JQHTML: No parent component found for slot`); return undefined; } /** * Search for <%= content('SlotName') %> in a template file */ findSlotUsageInTemplate(templateUri, slotName) { return __awaiter(this, void 0, void 0, function* () { try { const templateDoc = yield vscode.workspace.openTextDocument(templateUri); const templateText = templateDoc.getText(); // Search for content('SlotName') or content("SlotName") // Also handle optional whitespace const contentRegex = new RegExp(`<%=\\s*content\\s*\\(\\s*['"]${slotName}['"]\\s*\\)`, 'g'); const match = contentRegex.exec(templateText); if (match) { const matchPosition = templateDoc.positionAt(match.index); console.log(`JQHTML: Found content('${slotName}') at line ${matchPosition.line + 1}`); // Return location pointing to the slot name within content('SlotName') const slotNameStartIndex = match.index + match[0].indexOf(slotName); const slotNamePosition = templateDoc.positionAt(slotNameStartIndex); const slotNameRange = new vscode.Range(slotNamePosition, new vscode.Position(slotNamePosition.line, slotNamePosition.character + slotName.length)); return new vscode.Location(templateUri, slotNameRange); } console.log(`JQHTML: No content('${slotName}') found in template`); return undefined; } catch (error) { console.error(`JQHTML: Error reading template file:`, error); return undefined; } }); } } exports.JqhtmlDefinitionProvider = JqhtmlDefinitionProvider; /** * JQHTML Hover Provider * * Provides hover information for JQHTML components. * Shows the file and line where the component is defined. */ class JqhtmlHoverProvider { constructor(componentIndex) { this.componentIndex = componentIndex; } provideHover(document, position, token) { return __awaiter(this, void 0, void 0, function* () { const line = document.lineAt(position.line).text; const char = position.character; // Check for $redrawable attribute const redrawableMatch = line.match(/\$redrawable(?=\s|>|\/)/); if (redrawableMatch && line.indexOf('$redrawable') <= char && char <= line.indexOf('$redrawable') + '$redrawable'.length) { const markdown = new vscode.MarkdownString(); markdown.appendMarkdown(`**\`$redrawable\` Attribute**\n\n`); markdown.appendMarkdown(`Converts this tag into an anonymous component class, allowing it to be redrawn on demand.\n\n`); markdown.appendMarkdown(`**Usage:**\n\`\`\`javascript\nthis.$sid('element_id').render()\n\`\`\`\n\n`); markdown.appendMarkdown(`Call \`render()\` on the element's scoped ID to trigger a re-render of just this element without affecting the rest of the component.`); const wordRange = new vscode.Range(new vscode.Position(position.line, line.indexOf('$redrawable')), new vscode.Position(position.line, line.indexOf('$redrawable') + '$redrawable'.length)); return new vscode.Hover(markdown, wordRange); } // Check for tag="" attribute on components (Define tags or component invocations) // Look backwards from cursor to find if we're in a tag="" attribute const beforeCursor = line.substring(0, char); const afterCursor = line.substring(char); // Check if we're hovering over "tag" attribute name or its value const tagAttrMatch = beforeCursor.match(/<(Define:[A-Z][A-Za-z0-9_]*|[A-Z][A-Za-z0-9_]*)[^>]*\btag\s*=\s*["']?(\w*)$/); if (tagAttrMatch) { // We're in or near a tag attribute const markdown = new vscode.MarkdownString(); markdown.appendMarkdown(`**\`tag\` Attribute**\n\n`); markdown.appendMarkdown(`Sets the HTML element type for this component.\n\n`); markdown.appendMarkdown(`**Default:** \`div\`\n\n`); markdown.appendMarkdown(`**Examples:**\n`); markdown.appendMarkdown(`- \`tag="button"\` - Creates a \`