<context>
// cat/src/compiler.js

const fs = require('fs-extra');
const path = require('path');
const glob = require('glob');
const { parseComponent, parseScript, generateClassCode } = require('./component');
const { parsePageComponent, generatePageClassCode, generateRouterClassCode } = require('./page');

/**
 * Detect used components in the provided HTML contents by parsing the HTML tags.
 * @param {Array<string>} htmlContents - Array of HTML file contents.
 * @param {Set<string>} availableComponents - Set of all available component tag names.
 * @returns {Set<string>} - Set of used component tag names.
 */
function detectUsedComponents(htmlContents, availableComponents) {
  const usedComponents = new Set();

  // Regex to match all tags in the HTML
  const tagRegex = /<\s*([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*>/g;

  htmlContents.forEach(content => {
    let match;
    while ((match = tagRegex.exec(content)) !== null) {
      let tag = match[1];
      if (availableComponents.has(tag)) {
        usedComponents.add(tag);
      }
    }
  });

  return usedComponents;
}

/**
 * Compiler Function
 * @param {string} indexPath - Path to the index.html file.
 * @returns {Promise<string>} - Compiled HTML content with component and page registrations.
 */
async function compile(indexPath) {
  // Read the index.html content
  let indexContent = await fs.readFile(indexPath, 'utf-8');

  // Define project root
  const projectRoot = path.dirname(indexPath);

  // -----------------------
  // Step 1: Process Components
  // -----------------------

  // Find all .comp.html files in the project directory
  const compFiles = glob.sync('**/*.comp.html', {
    cwd: projectRoot,
    absolute: true,
    ignore: ['node_modules/**', 'cat/**'] // Ignore node_modules and cat directory to prevent recursion
  });

  const componentMap = new Map(); // Map of tagName to compiled class code
  const parsedComponents = new Set(); // To track parsed components

  // Parse and compile each component file
  for (const file of compFiles) {
    try {
      const { tagName, html, script, style } = await parseComponent(file);

      if (!parsedComponents.has(tagName)) {
        // Parse script to extract properties and methods
        const { properties, methods } = parseScript(script);

        // Generate class code
        const classCode = generateClassCode({
          tagName,
          html,
          style,
          properties,
          methods,
        });

        componentMap.set(tagName, classCode);
        parsedComponents.add(tagName);

        console.log(`🔍 Parsed and compiled component: <${tagName}> from ${path.relative(projectRoot, file)}`);
      } else {
        console.log(`ℹ️  Component <${tagName}> from ${path.relative(projectRoot, file)} has already been parsed.`);
      }
    } catch (error) {
      console.error(`❌ Failed to parse ${file}: ${error.message}`);
    }
  }

  if (componentMap.size === 0) {
    console.warn('⚠️  No components found to compile.');
  }

  // -----------------------
  // Step 2: Process Pages
  // -----------------------

  // Find all .page.html files in the project directory
  const pageFiles = glob.sync('**/*.page.html', {
    cwd: projectRoot,
    absolute: true,
    ignore: ['node_modules/**', 'cat/**']
  });

  // Initialize pageCounter
  let pageCounter = 1;

  const pageClassMap = new Map(); // Map of route to page class details
  const pageHtmlContents = []; // For component usage detection
  let collectedPageStyles = ''; // Collect styles from all pages

  for (const file of pageFiles) {
    try {
      const { route, html, script, style } = await parsePageComponent(file);

      const className = `_Page${pageCounter++}`;

      // Parse script to extract properties and methods
      const { properties, methods } = parseScript(script);

      // Generate class code
      const classCode = generatePageClassCode({
        className,
        html,
        style,
        properties,
        methods,
      });

      pageClassMap.set(route, { className, classCode });

      console.log(`📄 Parsed and compiled page: ${className} for route &quot;${route}&quot; from ${path.relative(projectRoot, file)}`);

      // Collect html content
      pageHtmlContents.push(html);

      // Collect styles
      if (style) {
        collectedPageStyles += style + '\n';
      }

    } catch (error) {
      console.error(`❌ Failed to parse page ${file}: ${error.message}`);
    }
  }

  if (pageClassMap.size === 0) {
    console.warn('⚠️  No pages found to compile.');
  }

  // -----------------------
  // Step 3: Detect Used Components
  // -----------------------

  // Find all .html files in the project directory for usage detection
  const htmlFiles = glob.sync('**/*.html', {
    cwd: projectRoot,
    absolute: true,
    ignore: ['node_modules/**', 'cat/**']
  });

  const htmlContents = [];
  for (const file of htmlFiles) {
    try {
      const content = await fs.readFile(file, 'utf-8');
      htmlContents.push(content);
    } catch (error) {
      console.error(`❌ Failed to read HTML file ${file}: ${error.message}`);
    }
  }

  // Combine with pages' html content
  const allHtmlContents = htmlContents.concat(pageHtmlContents);

  // Detect which components are used across all HTML files and page html
  const availableComponents = new Set(componentMap.keys());
  const usedComponents = detectUsedComponents(allHtmlContents, availableComponents);

  if (usedComponents.size === 0) {
    console.warn('⚠️  No used components found in HTML files.');
  } else {
    console.log(`📦 Used components: ${[...usedComponents].join(', ')}`);
  }

  // -----------------------
  // Step 4: Verify <load> Tags
  // -----------------------

  // Find all <load route=&quot;...&quot;> and <load name=&quot;...&quot;/> tags in indexContent
  const loadTagRegex = /<load\s+[^>]*(route|name)=[&quot;']([^&quot;']+)[&quot;'][^>]*>/gi;
  const loadRoutes = [];
  let match;
  while ((match = loadTagRegex.exec(indexContent)) !== null) {
    const attrType = match[1];
    const value = match[2];
    if (attrType === 'route') {
      loadRoutes.push(value);
    }
  }

  // Verify that each load route exists in the pageClassMap
  loadRoutes.forEach(route => {
    if (!pageClassMap.has(route)) {
      console.error(`❌ No page found for route &quot;${route}&quot;. Please ensure a .page.html file exists with route=&quot;${route}&quot;.`);
    }
  });

  // -----------------------
  // Step 5: Inject Page Styles into <head>
  // -----------------------

  if (collectedPageStyles.trim()) {
    const styleTag = `<style>
${collectedPageStyles}
</style>
`;

    // Inject the style tag into <head>
    if (indexContent.includes('</head>')) {
      indexContent = indexContent.replace('</head>', `${styleTag}</head>`);
      console.log('🎨 Injected page styles into <head>.');
    } else {
      // If no </head> tag, prepend the styles
      indexContent = styleTag + '\n' + indexContent;
    }
  }

  // -----------------------
  // Step 6: Generate Registration Script
  // -----------------------

  const registrationScript = generateCompleteScript(componentMap, usedComponents, pageClassMap);

  // -----------------------
  // Step 7: Inject Registration Script
  // -----------------------

  if (registrationScript.trim()) {
    // Inject the registration script before the closing </body> tag
    const scriptTag = `<script>
${registrationScript}
</script>
`;

    if (indexContent.includes('</body>')) {
      indexContent = indexContent.replace('</body>', `${scriptTag}</body>`);
    } else {
      // If no </body> tag, append the script at the end
      indexContent += `\n${scriptTag}`;
    }
    console.log('📝 Injected component and page scripts.');
  }

  // -----------------------
  // Step 8: Return Compiled Content
  // -----------------------

  return indexContent;
}

/**
 * Generate the complete registration script for components and pages, including the Router class.
 * @param {Map<string, string>} componentsMap - Map of tagName to compiled class code.
 * @param {Set<string>} usedComponents - Set of used component tag names.
 * @param {Map<string, {className: string, classCode: string}>} pageClassMap - Map of route to page class details.
 * @returns {string} - Combined JavaScript code.
 */
function generateCompleteScript(componentsMap, usedComponents, pageClassMap) {
  let script = '';

  // Generate components' class definitions
  usedComponents.forEach(tagName => {
    const classCode = componentsMap.get(tagName);
    if (classCode) {
      script += `
${classCode}
      `;
    }
  });

  // Generate pages' class definitions
  pageClassMap.forEach(({ className, classCode }, route) => {
    script += `
${classCode}
      `;
  });

  // Generate Router class code
  script += generateRouterClassCode(pageClassMap);

  return script;
}

module.exports = { compile };

// cat/src/component.js

const fs = require('fs-extra');
const path = require('path');
const acorn = require('acorn'); // Import Acorn for JavaScript parsing
const { parseDocument } = require('htmlparser2');

/**
 * Parse a component file to extract tagName, HTML, script, and style sections.
 * @param {string} filePath - Path to the .comp.html file.
 * @returns {Promise<{ tagName: string, html: string, script: string, style: string }>} - Component details.
 */
async function parseComponent(filePath) {
  const content = await fs.readFile(filePath, 'utf-8');

  // Extract the outermost tag name and inner HTML
  const componentRegex = /<([\w-]+)>([\s\S]*?)<\/\1>/;
  const componentMatch = content.match(componentRegex);

  if (!componentMatch) {
    throw new Error(`Invalid component structure in ${path.basename(filePath)}`);
  }

  const tagName = componentMatch[1];
  let innerContent = componentMatch[2];

  // Extract all <script> sections
  const scriptRegex = /<script>([\s\S]*?)<\/script>/g;
  let scriptContent = '';
  let scriptMatch;
  while ((scriptMatch = scriptRegex.exec(content)) !== null) {
    scriptContent += scriptMatch[1].trim() + '\n';
  }

  // Extract all <style> sections
  const styleRegex = /<style>([\s\S]*?)<\/style>/g;
  let styleContent = '';
  let styleMatch;
  while ((styleMatch = styleRegex.exec(content)) !== null) {
    styleContent += styleMatch[1].trim() + '\n';
  }

  // Remove all <script> and <style> tags from innerContent
  innerContent = innerContent
    .replace(scriptRegex, '')
    .replace(styleRegex, '')
    .trim();

  return {
    tagName,
    html: innerContent,
    script: scriptContent,
    style: styleContent,
  };
}

/**
 * Parse the script content to extract properties and methods.
 * @param {string} script - The script content.
 * @returns {{ properties: Array, methods: Array }} - Parsed properties and methods.
 */
function parseScript(script) {
  const properties = [];
  const methods = [];

  try {
    // Parse the script content into an AST
    const ast = acorn.parse(script, { ecmaVersion: 2020, locations: true });

    ast.body.forEach(node => {
      if (node.type === 'VariableDeclaration') {
        node.declarations.forEach(declarator => {
          if (declarator.id.type === 'Identifier') {
            const varName = declarator.id.name;
            const varValue = script.substring(declarator.init.start, declarator.init.end);
            properties.push({ name: varName, value: varValue });
          }
        });
      } else if (node.type === 'FunctionDeclaration') {
        const funcName = node.id.name;
        const params = node.params.map(param => script.substring(param.start, param.end)).join(', ');
        const body = script.substring(node.body.start, node.body.end);
        methods.push({ name: funcName, params, body });
      }
    });
  } catch (error) {
    console.error(`Error parsing script: ${error.message}`);
  }

  return { properties, methods };
}

/**
 * Convert a kebab-case or other tag name to PascalCase for class naming.
 * @param {string} tagName - The custom element tag name.
 * @returns {string} - PascalCase string.
 */
function toPascalCase(tagName) {
  return tagName
    .split('-')
    .map(word => word.charAt(0).toUpperCase() + word.slice(1))
    .join('');
}

/**
 * Sanitize a string to be a valid JavaScript identifier.
 * @param {string} name - The original name.
 * @returns {string} - Sanitized identifier.
 */
function sanitizeVariableName(name) {
  // Replace invalid characters with underscores
  let sanitized = name.replace(/[^A-Za-z0-9_$]/g, '_');
  // If the name starts with a digit, prefix with underscore
  if (/^\d/.test(sanitized)) {
    sanitized = '_' + sanitized;
  }
  return sanitized;
}

/**
 * Generate the class code for the component.
 * @param {object} component - Component details.
 * @returns {string} - The compiled class code.
 */
function generateClassCode({ tagName, html, style, properties, methods }) {
  const className = toPascalCase(tagName);
  let classCode = `class ${className} extends HTMLElement {
    constructor() {
      super();
  `;

  // Initialize properties
  properties.forEach(prop => {
    classCode += `    this._${prop.name} = ${prop.value};
  `;
  });

  classCode += `
      this.attachShadow({ mode: 'open' });
      this._buildDOM();
    }

    _buildDOM() {
      const fragment = document.createDocumentFragment();
  `;

  // Add styles
  if (style) {
    classCode += `    const style = document.createElement('style');
      style.textContent = \`
  ${style}
      \`;
      fragment.appendChild(style);

  `;
  }

  // Generate DOM creation code
  const { code: domCode, cachedNodes, eventListeners } = generateDOMCode(html);
  classCode += domCode;

  classCode += `
      this.shadowRoot.appendChild(fragment);
    }
  `;

  // connectedCallback
  classCode += `
    connectedCallback() {
  `;
  
  // Attach event listeners
  if (eventListeners &amp;&amp; eventListeners.length > 0) {
    eventListeners.forEach(listener => {
      classCode += `    this.${listener.elementVarName}.addEventListener('${listener.event}', this.${listener.handlerName}.bind(this));
  `;
    });
  }

  classCode += `  }

    disconnectedCallback() {
  `;

  // Remove event listeners
  if (eventListeners &amp;&amp; eventListeners.length > 0) {
    eventListeners.forEach(listener => {
      classCode += `    this.${listener.elementVarName}.removeEventListener('${listener.event}', this.${listener.handlerName}.bind(this));
  `;
    });
  }

  classCode += `  }
  `;

  // Generate getters and setters
  Object.keys(cachedNodes).forEach(propName => {
    const nodes = cachedNodes[propName];

    classCode += `
    get ${propName}() { return this._${propName}; }
    set ${propName}(value) {
      if (this._${propName} !== value) {
        this._${propName} = value;
  `;
    
    nodes.forEach(node => {
      if (node.type === 'text') {
        classCode += `      this.${node.varName}.textContent = ${node.expression};
  `;
      } else if (node.type === 'attribute') {
        classCode += `      this.${node.elementVarName}.setAttribute('${node.attrName}', ${node.expression});
  `;
      }
    });

    classCode += `    }
    }
  `;
  });

  // Add methods
  methods.forEach(method => {
    const transformedBody = transformMethodBody(method.body, properties.map(p => p.name));
    classCode += `
    ${method.name}(${method.params}) {
  ${transformedBody}
    }
  `;
  });

  classCode += `}
    
  customElements.define('${tagName}', ${className});
  `;

  return classCode;
}

/**
 * Transform method body to replace variable references with 'this.' prefix.
 * @param {string} body - The method body.
 * @param {Array<string>} propNames - The property names to replace.
 * @returns {string} - The transformed method body.
 */
function transformMethodBody(body, propNames) {
  let transformedBody = body;

  propNames.forEach(propName => {
    // Replace assignments like 'propName = value;' with 'this.propName = value;'
    const assignRegex = new RegExp(`([^\\.\\w$])${propName}\\s*=`, 'g');
    transformedBody = transformedBody.replace(assignRegex, `$1this.${propName} =`);

    // Replace variable references with 'this.propName'
    const varRegex = new RegExp(`([^\\.\\w$])${propName}([^\\w$])`, 'g');
    transformedBody = transformedBody.replace(varRegex, `$1this.${propName}$2`);
  });

  // Ensure braces are correctly balanced and formatted
  transformedBody = transformedBody.trim();

  // Remove extra outer braces if they exist
  if (transformedBody.startsWith('{') &amp;&amp; transformedBody.endsWith('}')) {
    transformedBody = transformedBody.slice(1, -1).trim();
  }

  // Properly indent the method body
  transformedBody = transformedBody
    .split('\n')
    .map(line => '    ' + line.trim())
    .join('\n');

  return transformedBody;
}

/**
 * Generate DOM creation code from HTML using htmlparser2.
 * @param {string} html - The inner HTML string of the component.
 * @returns {object} - Object containing generated code, cached nodes, and event listeners.
 */
function generateDOMCode(html) {
  const dom = parseDocument(html).children; // Array of DOM nodes

  let code = '';
  let varCounter = 0;
  const cachedNodes = {}; // For properties to nodes
  const eventListeners = [];

  function traverse(node, parentVarName) {
    if (node.type === 'tag') {
      const sanitizedTagName = sanitizeVariableName(node.name);
      const varName = sanitizedTagName + ++varCounter;

      // Decide whether to assign to 'this.' or 'const'
      let declaration;
      if (needsReference(node)) {
        declaration = `this.${varName}`;
      } else {
        declaration = `const ${varName}`;
      }

      const varReference = declaration.replace(/^const /, '');
      code += `    ${declaration} = document.createElement('${node.name}');
  `;

      // Handle attributes
      const attribs = node.attribs || {};
      for (let [attrName, attrValue] of Object.entries(attribs)) {
        if (attrName === 'class' || attrName === 'style' || attrName.startsWith('data-') || attrName === 'id' || attrName === 'src' || attrName === 'alt') {
          // Handle attributes with embedded expressions
          const { expression, propNames } = generateAttributeExpression(attrValue);
          code += `    ${varReference}.setAttribute('${attrName}', ${expression});
  `;

          // Cache nodes for reactive updates
          propNames.forEach(propName => {
            if (!cachedNodes[propName]) cachedNodes[propName] = [];
            cachedNodes[propName].push({
              type: 'attribute',
              elementVarName: varReference.replace(/^this\./, ''),
              attrName: attrName,
              expression: generateAttributeExpression(attrValue).expression,
            });
          });
        } else if (attrName.startsWith('on')) {
          // Event listener
          const event = attrName.substring(2);
          const handlerName = attrValue.replace(/\(\)$/, '').trim();
          eventListeners.push({ elementVarName: varReference.replace(/^this\./, ''), event, handlerName });
        } else {
          // For other attributes
          code += `    ${varReference}.setAttribute('${attrName}', '${attrValue}');
  `;
        }
      }

      // Append to parent
      const parentRef = parentVarName ? parentVarName : 'fragment';
      code += `    ${parentRef}.appendChild(${varReference});
  `;

      // Traverse children
      if (node.children &amp;&amp; node.children.length) {
        for (let child of node.children) {
          traverse(child, varReference);
        }
      }
    } else if (node.type === 'text') {
      const textContent = node.data;

      if (textContent.trim()) {
        // Handle text nodes with expressions
        const { expression, propNames } = generateTextExpression(textContent);

        const varName = 'textNode' + ++varCounter;

        // Decide whether to assign to 'this.' or 'const'
        let declaration;
        if (expression.includes('this._')) {
          declaration = `this.${varName}`;
        } else {
          declaration = `const ${varName}`;
        }

        const varReference = declaration.replace(/^const /, '');
        code += `    ${declaration} = document.createTextNode(${expression});
  `;
        code += `    ${parentVarName}.appendChild(${varReference});
  `;

        // Cache nodes for reactive updates
        propNames.forEach(propName => {
          if (!cachedNodes[propName]) cachedNodes[propName] = [];
          cachedNodes[propName].push({
            type: 'text',
            varName: varReference.replace(/^this\./, ''),
            expression: generateTextExpression(textContent).expression,
          });
        });
      }
    }
  }

  function needsReference(node) {
    // Determines if the element needs to be assigned to 'this.'
    // Elements that are:
    // - Targets of event listeners
    // - Contain reactive expressions
    // Need to be accessible outside of _buildDOM
    if (node.attribs) {
      const hasEventListener = Object.keys(node.attribs).some(attr => attr.startsWith('on'));
      const hasReactiveAttr = Object.values(node.attribs).some(value => /\{([\w$]+)\}/.test(value));
      if (hasEventListener || hasReactiveAttr) {
        return true;
      }
    }

    if (node.children &amp;&amp; node.children.some(child => child.type === 'text' &amp;&amp; /\{([\w$]+)\}/.test(child.data))) {
      return true;
    }

    return false;
  }

  function generateTextExpression(textContent) {
    const exprRegex = /\{([\w$]+)\}/g;
    let segments = [];
    let propNames = [];
    let lastIndex = 0;
    let match;

    while ((match = exprRegex.exec(textContent)) !== null) {
      if (match.index > lastIndex) {
        segments.push(JSON.stringify(textContent.slice(lastIndex, match.index)));
      }
      segments.push(`this._${match[1]}`);
      propNames.push(match[1]);
      lastIndex = exprRegex.lastIndex;
    }

    if (lastIndex < textContent.length) {
      segments.push(JSON.stringify(textContent.slice(lastIndex)));
    }

    return {
      expression: segments.join(' + '),
      propNames,
    };
  }

  function generateAttributeExpression(attrValue) {
    const exprRegex = /\{([\w$]+)\}/g;
    let segments = [];
    let propNames = [];
    let lastIndex = 0;
    let match;

    while ((match = exprRegex.exec(attrValue)) !== null) {
      if (match.index > lastIndex) {
        segments.push(JSON.stringify(attrValue.slice(lastIndex, match.index)));
      }
      segments.push(`this._${match[1]}`);
      propNames.push(match[1]);
      lastIndex = exprRegex.lastIndex;
    }

    if (lastIndex < attrValue.length) {
      segments.push(JSON.stringify(attrValue.slice(lastIndex)));
    }

    return {
      expression: segments.join(' + '),
      propNames,
    };
  }

  dom.forEach(node => traverse(node, null));

  return { code, cachedNodes, eventListeners };
}

module.exports = {
  parseComponent,
  parseScript,
  toPascalCase,
  generateClassCode,
  transformMethodBody,
  generateDOMCode,
  sanitizeVariableName
};

// cat/src/page.js

const fs = require('fs-extra');
const path = require('path');
const { generateDOMCode, transformMethodBody, parseScript, sanitizeVariableName } = require('./component'); // Reuse functions from component.js

/**
 * Parse a page file to extract route, HTML, script, and style sections.
 * @param {string} filePath - Path to the .page.html file.
 * @returns {Promise<{ route: string, html: string, script: string, style: string }>} - Page details.
 */
async function parsePageComponent(filePath) {
  const content = await fs.readFile(filePath, 'utf-8');

  // Extract the <page> tag with route attribute
  const pageRegex = /<page\s+[^>]*route=[&quot;']([^&quot;']+)[&quot;'][^>]*>([\s\S]*?)<\/page>/;
  const pageMatch = content.match(pageRegex);

  if (!pageMatch) {
    throw new Error(`Invalid page structure or missing route attribute in ${path.basename(filePath)}`);
  }

  const route = pageMatch[1];
  let innerContent = pageMatch[2];

  // Extract all <script> sections
  const scriptRegex = /<script>([\s\S]*?)<\/script>/g;
  let scriptContent = '';
  let scriptMatch;
  while ((scriptMatch = scriptRegex.exec(content)) !== null) {
    scriptContent += scriptMatch[1].trim() + '\n';
  }

  // Extract all <style> sections
  const styleRegex = /<style>([\s\S]*?)<\/style>/g;
  let styleContent = '';
  let styleMatch;
  while ((styleMatch = styleRegex.exec(content)) !== null) {
    styleContent += styleMatch[1].trim() + '\n';
  }

  // Remove all <script> and <style> tags from innerContent
  innerContent = innerContent
    .replace(scriptRegex, '')
    .replace(styleRegex, '')
    .trim();

  // Process <load name=&quot;...&quot;/> tags to replace with wrappers at build time
  function replaceLoadTags(html) {
    return html.replace(/<load\s+[^>]*name\s*=\s*[&quot;']([^&quot;']+)[&quot;'][^>]*\/?>/gi, function(match, p1) {
      // Replace with <div data-load-name=&quot;p1&quot;></div>
      return `<div data-load-name=&quot;${p1}&quot;></div>`;
    });
  }

  // Update innerContent
  innerContent = replaceLoadTags(innerContent);

  return {
    route,
    html: innerContent,
    script: scriptContent,
    style: styleContent,
  };
}

/**
 * Generate the class code for the page.
 * @param {object} page - Page details.
 * @returns {string} - The compiled class code.
 */
function generatePageClassCode({ className, html, style, properties, methods }) {
  let classCode = `class ${className} {
      constructor() {
    `;

  // Initialize properties
  properties.forEach(prop => {
    classCode += `    this._${prop.name} = ${prop.value};
    `;
  });

  classCode += `
        this._element = null;
        this._cachedNodes = {};
        this._eventListeners = [];
        this._buildDOM();
      }

      _buildDOM() {
        const fragment = document.createDocumentFragment();
    `;

  // Generate DOM creation code
  const { code: domCode, cachedNodes, eventListeners } = generateDOMCode(html);
  classCode += domCode;

  classCode += `
        this._element = fragment;
      }
    `;

  // Generate getters and setters
  if (cachedNodes) {
    Object.keys(cachedNodes).forEach(propName => {
      const nodes = cachedNodes[propName];

      classCode += `
      get ${propName}() { return this._${propName}; }
      set ${propName}(value) {
        if (this._${propName} !== value) {
          this._${propName} = value;
    `;

      nodes.forEach(node => {
        if (node.type === 'text') {
          classCode += `      this.${node.varName}.textContent = ${node.expression};
    `;
        } else if (node.type === 'attribute') {
          classCode += `      this.${node.elementVarName}.setAttribute('${node.attrName}', ${node.expression});
    `;
        }
      });

      classCode += `    }
      }
    `;
    });
  }

  // Add methods
  methods.forEach(method => {
    const transformedBody = transformMethodBody(method.body, properties.map(p => p.name));
    classCode += `
      ${method.name}(${method.params}) {
    ${transformedBody}
      }
    `;
  });

  // Add mount and unmount methods
  classCode += `
      mount(container) {
        if (!this._element) {
          this._buildDOM();
        }
        // Clear the container and append the page content
        container.innerHTML = '';
        container.appendChild(this._element);
  
        // Attach event listeners
    `;

  if (eventListeners &amp;&amp; eventListeners.length > 0) {
    eventListeners.forEach(listener => {
      classCode += `      this.${listener.elementVarName}.addEventListener('${listener.event}', this.${listener.handlerName}.bind(this));
    `;
    });
  }

  // Register containers and setup route handlers
  classCode += `
        // Register containers from this page with the Router
        if (typeof appRouter !== 'undefined') {
          appRouter.registerContainersFrom(this._element);
          appRouter.setupRouteHandlers(this._element);
        }
      }

      unmount() {
        if (this._element &amp;&amp; this._element.parentNode) {
          // Remove event listeners
    `;

  if (eventListeners &amp;&amp; eventListeners.length > 0) {
    eventListeners.forEach(listener => {
      classCode += `        this.${listener.elementVarName}.removeEventListener('${listener.event}', this.${listener.handlerName}.bind(this));
    `;
    });
  }

  // Unregister containers from the Router
  classCode += `
          // Unregister containers from this page
          if (typeof appRouter !== 'undefined') {
            appRouter.unregisterContainersFrom(this._element);
          }

          // Remove the element from the container
          // Since we're clearing the container's innerHTML in mount, this may not be necessary
        }
      }
    `;

  classCode += `}\n`;

  return classCode;
}

/**
 * Generate the Router class code.
 * @param {Map<string, {className: string, classCode: string}>} pageClassMap - Map of route to page class details.
 * @returns {string} - The Router class code.
 */
function generateRouterClassCode(pageClassMap) {
  // Generate list of routes and page class names
  let routesArray = [];

  pageClassMap.forEach(({ className }, route) => {
    routesArray.push(`{ route: '${route}', pageClass: ${className} }`);
  });

  const routesList = `[${routesArray.join(', ')}]`;

  let routerClassCode = `
class Router {
  constructor() {
    this.routes = ${routesList};
    this.containers = {}; // Cache for load containers
    this.pageInstances = {}; // Cache for mounted page instances per container
    document.addEventListener('DOMContentLoaded', () => {
      this.initializeLoadContainers();
      this.setupRouteHandlers();
      this.loadDefaultContent();
    });
  }

  initializeLoadContainers() {
    const loadTags = document.querySelectorAll('load');
    loadTags.forEach(loadTag => {
      const loadName = loadTag.getAttribute('name');
      const route = loadTag.getAttribute('route');
      if (loadName) {
        // This load acts as a wrapper
        const wrapper = document.createElement('div');
        wrapper.setAttribute('data-load-name', loadName);
        loadTag.replaceWith(wrapper);
        this.containers[loadName] = wrapper;

        // If route is specified, load the page into the wrapper
        if (route) {
          this.loadContentIntoContainer(route, loadName);
        }
      } else if (route) {
        // This load should be replaced immediately by the page content
        const routeEntry = this.routes.find(r => r.route === route);
        if (routeEntry) {
          const pageInstance = new routeEntry.pageClass();
          pageInstance._element = null; // Ensure the DOM is built fresh
          pageInstance._buildDOM();
          loadTag.replaceWith(pageInstance._element);

          // Register containers and setup handlers within the replaced content
          this.registerContainersFrom(pageInstance._element);
          this.setupRouteHandlers(pageInstance._element);
        } else {
          console.warn(\`No page found for route &quot;\${route}&quot;\`);
        }
      } else {
        console.warn('<load> tag must have either a name or a route attribute.');
      }
    });
  }

  setupRouteHandlers(rootElement = document) {
    const routeElements = rootElement.querySelectorAll('[route][load]');
    routeElements.forEach(element => {
      element.addEventListener('click', (event) => {
        event.preventDefault();
        const route = element.getAttribute('route');
        const loadName = element.getAttribute('load');
        this.loadContentIntoContainer(route, loadName);
        this.updateCurrentAttribute(element, loadName);
      });
    });
  }

  loadContentIntoContainer(route, loadName) {
    const container = this.containers[loadName];
    if (!container) {
      console.warn(\`No container found with name &quot;\${loadName}&quot;\`);
      return;
    }

    const routeEntry = this.routes.find(r => r.route === route);
    if (routeEntry) {
      // Unmount previous page instance if exists
      const prevInstance = this.pageInstances[loadName];
      if (prevInstance) {
        prevInstance.unmount();
      }
      const pageInstance = new routeEntry.pageClass();
      pageInstance.mount(container);
      this.pageInstances[loadName] = pageInstance;
    } else {
      console.warn(\`No page found for route &quot;\${route}&quot;\`);
    }
  }

  loadDefaultContent() {
    // For each container, find the element with 'current' attribute
    for (const loadName in this.containers) {
      const container = this.containers[loadName];
      const currentElement = document.querySelector(\`[route][load=&quot;\${loadName}&quot;][current]\`);
      if (currentElement) {
        const route = currentElement.getAttribute('route');
        this.loadContentIntoContainer(route, loadName);
      } else if (!container.innerHTML.trim()) {
        // If container is empty and no current element, you may define a default route
      }
    }
  }

  updateCurrentAttribute(clickedElement, loadName) {
    // Remove 'current' attribute from all elements with the same loadName
    const elements = document.querySelectorAll(\`[route][load=&quot;\${loadName}&quot;]\`);
    elements.forEach(el => {
      el.removeAttribute('current');
    });

    // Add 'current' attribute to the clicked element
    clickedElement.setAttribute('current', '');
  }

  registerContainersFrom(element) {
    const containerElements = element.querySelectorAll('[data-load-name]');
    containerElements.forEach(containerElement => {
      const loadName = containerElement.getAttribute('data-load-name');
      this.containers[loadName] = containerElement;
    });
  }

  unregisterContainersFrom(element) {
    const containerElements = element.querySelectorAll('[data-load-name]');
    containerElements.forEach(containerElement => {
      const loadName = containerElement.getAttribute('data-load-name');
      delete this.containers[loadName];
    });
  }
}

const appRouter = new Router();
`;

  return routerClassCode;
}

module.exports = {
  parsePageComponent,
  parseScript,
  generatePageClassCode,
  generateRouterClassCode
};
</context>

<instruction>
Output each one of these files, each one in their own code box, ready to copy and page. Nothing else.
</instruction>