```
project-root/
├── cat/
│   ├── bin/
│   │   └── cat.js
│   └── src/
│       └── compiler.js
|       └── component.js
|       └── page.js
├── index.html
├── package.json
└── cat.config.json
```

```
// cat/src/compiler.js

const fs = require('fs-extra');
const path = require('path');
const glob = require('glob');
const { parseComponent, parseScript, generateClassCode, transformMethodBody, generateDOMCode } = require('./component');
const { parsePage, detectLoadRoutes } = require('./page');
const { parseDocument } = require('htmlparser2');

/**
 * 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;
}

/**
 * Generate the registration script for the used components.
 * @param {Map<string, string>} components - Map of tagName to compiled class code.
 * @returns {string} - JavaScript code for registering components.
 */
function generateRegistrationScript(components) {
  let script = '';

  components.forEach((classCode) => {
    script += `
${classCode}
  `;
  });

  return script;
}

/**
 * Compiler Function
 * @param {string} indexPath - Path to the index.html file.
 * @returns {Promise<string>} - Compiled HTML content with component registrations and page injections.
 */
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/**']
  });

  const routeMap = new Map(); // Map of route to page content

  for (const file of pageFiles) {
    try {
      const { route, content } = await parsePage(file);
      if (routeMap.has(route)) {
        console.warn(`⚠️  Duplicate route "${route}" found in ${path.relative(projectRoot, file)}. Overwriting previous route.`);
      }
      routeMap.set(route, content);
      console.log(`📄 Mapped route "${route}" to ${path.relative(projectRoot, file)}`);
    } catch (error) {
      console.error(`❌ Failed to parse page ${file}: ${error.message}`);
    }
  }

  if (routeMap.size === 0) {
    console.warn('⚠️  No pages found to map routes.');
  }

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

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

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

  // -----------------------
  // Step 4: Handle <load> Tags and Inject Page Content
  // -----------------------

  // Detect all <load> routes in all HTML files
  const loadRoutes = detectLoadRoutes(htmlContents);

  // Prepare a map of route to page content
  // Verify that each load route exists in the routeMap
  loadRoutes.forEach(route => {
    if (!routeMap.has(route)) {
      console.error(`❌ No page found for route "${route}". Please ensure a .page.html file exists with route="${route}".`);
    }
  });

  // Now, replace <load route="..."> tags in indexContent with the corresponding page content
  const loadTagRegex = /<load\s+[^>]*route=["']([^"']+)["'][^>]*>/gi;

  indexContent = indexContent.replace(loadTagRegex, (match, route) => {
    if (routeMap.has(route)) {
      console.log(`🔄 Injecting content for route "${route}"`);
      return routeMap.get(route);
    } else {
      console.warn(`⚠️  No content found for route "${route}". Keeping <load> tag.`);
      return match; // Keep the <load> tag if no content is found
    }
  });

  // -----------------------
  // Step 5: Generate Registration Script
  // -----------------------

  const registrationScript = generateRegistrationScript(
    Array.from(usedComponents).reduce((map, tag) => map.set(tag, componentMap.get(tag)), new Map())
  );

  // -----------------------
  // Step 6: 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 registration scripts.');
  }

  // -----------------------
  // Step 7: Return Compiled Content
  // -----------------------

  return indexContent;
}

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('');
}

/**
 * Escape backticks in templates to avoid breaking template literals.
 * @param {string} str - The template string.
 * @returns {string} - Escaped string.
 */
function escapeBackticks(str) {
  return str.replace(/`/g, '\\`');
}

/**
 * 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 && 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 && 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('{') && 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 varName = node.name + ++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 && 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 && node.children.some(child => child.type === 'text' && /\{([\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,
  escapeBackticks,
  generateClassCode,
  transformMethodBody,
  generateDOMCode
};
```

```
// cat/src/page.js

const fs = require('fs-extra');
const path = require('path');
const glob = require('glob');

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

  // Regex to match the <page> tag and extract the route attribute
  const pageTagRegex = /<page\s+[^>]*route=["']([^"']+)["'][^>]*>/i;
  const match = content.match(pageTagRegex);

  if (!match) {
    throw new Error(`No valid <page> tag with 'route' attribute found in ${path.basename(filePath)}`);
  }

  const route = match[1];

  // Extract the inner HTML of the <page> tag
  const innerContentRegex = new RegExp(`<page[^>]*>([\\s\\S]*?)<\\/page>`, 'i');
  const innerMatch = content.match(innerContentRegex);

  const pageContent = innerMatch ? innerMatch[1].trim() : '';

  return { route, content: pageContent };
}

/**
 * Detect all <load> tags in the provided HTML contents and extract their routes.
 * @param {Array<string>} htmlContents - Array of HTML file contents.
 * @returns {Array<string>} - Array of routes found in <load> tags.
 */
function detectLoadRoutes(htmlContents) {
  const loadRoutes = [];

  // Regex to match <load route="..."> tags
  const loadTagRegex = /<load\s+[^>]*route=["']([^"']+)["'][^>]*>/gi;

  htmlContents.forEach(content => {
    let match;
    while ((match = loadTagRegex.exec(content)) !== null) {
      const route = match[1];
      loadRoutes.push(route);
    }
  });

  return loadRoutes;
}

module.exports = {
  parsePage,
  detectLoadRoutes
};
```

## Objective

Enhance the Cat Framework to support page routing and dynamic content loading using `.page.html` files.

## Background

The current system compiles `.comp.html` files into custom elements. We need to extend this functionality to handle `.page.html` files as special components that can be dynamically loaded based on routes.

## Requirements

1. Update `component.js`:
    
    - Modify the compilation process for `.page.html` files:
        
        - Generate a class with a random name (e.g., `Page_12345`)
        - Do not register these as custom elements
        
    - Ensure `.page.html` files can handle dynamic {expressions} similar to components
    
2. Create a new `router.js`:
    
    - Implement a Router class that:
        
        - Maintains a list of all page class names and their associated routes
        - Searches for `<load>` tags with matching routes
        - Constructs and renders the appropriate page content inside the `<load>` tag
        
    
3. Update `compiler.js`:
    
    - Modify the compilation process to:
        
        - Compile `.page.html` files using the updated `component.js`
        - Generate the Router class with the list of pages and routes
        - Inject the Router initialization code into the final HTML
        
    
4. Modify `page.js`:
    
    - Update `parsePage` function to extract both the route and the page content
    - Implement a function to generate the random class name for pages
    

## Implementation Steps

1. Component.js modifications:
    
    - Create a new function `generatePageClass` similar to `generateClassCode`
    - Ensure this new function generates a class with a random name
    - Modify `parseComponent` to handle `.page.html` files differently
    
2. Router.js creation:
    
    - Implement the Router class with methods for:
        
        - Registering pages and routes
        - Matching routes to `<load>` tags
        - Rendering page content
        
    
3. Compiler.js updates:
    
    - Modify the compilation process to handle `.page.html` files
    - Generate code for Router initialization
    - Inject Router code into the final HTML output
    
4. Page.js enhancements:
    
    - Update `parsePage` to return both route and content
    - Implement a `generatePageClassName` function