Prechádzať zdrojové kódy

entire refactor with custom node parser

Matheus Giovani 2 rokov pred
rodič
commit
53337b9ad0
42 zmenil súbory, kde vykonal 1503 pridanie a 540 odobranie
  1. 1 4
      .gitignore
  2. 2 0
      packages/compiler/.gitignore
  3. 19 0
      packages/compiler/README.md
  4. 4 1
      packages/compiler/package.json
  5. 0 29
      packages/compiler/src/core/Lexer.ts
  6. 233 40
      packages/compiler/src/core/Plugin.ts
  7. 0 58
      packages/compiler/src/core/lexer/Token.ts
  8. 0 52
      packages/compiler/src/core/lexer/tokens/ForEach.ts
  9. 0 67
      packages/compiler/src/core/lexer/tokens/If.ts
  10. 0 92
      packages/compiler/src/core/lexer/tokens/Import.ts
  11. 106 0
      packages/compiler/src/core/plugin/Hook.ts
  12. 41 45
      packages/compiler/src/core/plugin/hooks/ComponentHook.ts
  13. 23 0
      packages/compiler/src/core/plugin/hooks/ForEachHook.ts
  14. 42 0
      packages/compiler/src/core/plugin/hooks/IfHook.ts
  15. 117 0
      packages/compiler/src/core/plugin/hooks/ImportHook.ts
  16. 4 9
      packages/compiler/src/core/plugin/hooks/PropertyHook.ts
  17. 10 8
      packages/compiler/src/core/plugin/hooks/PupperToAlpineHook.ts
  18. 24 0
      packages/compiler/src/core/plugin/hooks/StyleAndScriptHook.ts
  19. 11 2
      packages/compiler/src/core/plugin/hooks/component/ScriptParser.ts
  20. 1 1
      packages/compiler/src/core/plugin/hooks/component/StyleParser.ts
  21. 37 0
      packages/compiler/src/core/plugin/nodes/AstNode.ts
  22. 122 0
      packages/compiler/src/core/plugin/nodes/ConditionalNode.ts
  23. 28 0
      packages/compiler/src/core/plugin/nodes/EachNode.ts
  24. 10 0
      packages/compiler/src/core/plugin/nodes/MixinNode.ts
  25. 57 0
      packages/compiler/src/core/plugin/nodes/TagNode.ts
  26. 1 1
      packages/compiler/src/index.ts
  27. 108 0
      packages/compiler/src/model/core/NodeModel.ts
  28. 42 0
      packages/compiler/src/model/core/nodes/BlockedCompilerNode.ts
  29. 272 0
      packages/compiler/src/model/core/nodes/CompilerNode.ts
  30. 8 0
      packages/compiler/src/typings/pug-error.d.ts
  31. 97 17
      packages/compiler/src/typings/pug.d.ts
  32. 8 0
      packages/compiler/src/util/NodeUtil.ts
  33. 1 1
      packages/compiler/tsconfig.json
  34. 19 16
      packages/renderer/src/core/Component.ts
  35. 1 0
      packages/vscode-extension/pupper-js/.gitignore
  36. 1 65
      packages/vscode-extension/pupper-js/README.md
  37. 28 15
      packages/vscode-extension/pupper-js/package.json
  38. 7 2
      test/index.js
  39. 1 1
      test/templates/ExportedComponent.pupper
  40. 2 2
      test/templates/ImportedComponent.pupper
  41. 14 10
      test/templates/template.pupper
  42. 1 2
      tsconfig.json

+ 1 - 4
.gitignore

@@ -7,7 +7,7 @@ node_modules
 yarn-error.log
 yarn-error.log
 yarn.lock
 yarn.lock
 
 
-!src/types
+**/types
 
 
 # Logs
 # Logs
 logs
 logs
@@ -52,9 +52,6 @@ build/Release
 node_modules/
 node_modules/
 jspm_packages/
 jspm_packages/
 
 
-# TypeScript v1 declaration files
-typings/
-
 # TypeScript cache
 # TypeScript cache
 *.tsbuildinfo
 *.tsbuildinfo
 
 

+ 2 - 0
packages/compiler/.gitignore

@@ -0,0 +1,2 @@
+types/
+out/

+ 19 - 0
packages/compiler/README.md

@@ -0,0 +1,19 @@
+![pupper.js icon](https://i.imgur.com/dAuCn4B.png "pupper.js icon")
+# @pupperjs/compiler
+The compiler for pupper.js component.
+
+This is a BETA project, it can change drastically over time, so use it with caution for now and stay updated! :D
+
+---
+
+# Under the hood
+- pupper.js uses [pug](https://github.com/pugjs/pug) as the main language component.
+  All the templates are compiled as HTML using pug.
+    
+  -  A custom pug plugin is used for lexing and parsing the instructions into components.
+
+- It also uses [ts-morph](https://github.com/dsherret/ts-morph) for processing the resulting components code.
+
+- For TypeScript compilation, it uses the default compiler, `tsc`.
+
+- For Sass and Scss, it uses [sass](https://github.com/sass/sass).

+ 4 - 1
packages/compiler/package.json

@@ -13,7 +13,9 @@
   "dependencies": {
   "dependencies": {
     "alpinejs": "^3.10.2",
     "alpinejs": "^3.10.2",
     "pug": "^3.0.2",
     "pug": "^3.0.2",
-    "ts-morph": "^15.1.0"
+    "pug-error": "^2.0.0",
+    "ts-morph": "^15.1.0",
+    "tscpaths": "^0.0.9"
   },
   },
   "types": "./types/",
   "types": "./types/",
   "devDependencies": {
   "devDependencies": {
@@ -22,6 +24,7 @@
     "debug": "^4.3.2",
     "debug": "^4.3.2",
     "js-beautify": "^1.14.0",
     "js-beautify": "^1.14.0",
     "tsc": "^2.0.3",
     "tsc": "^2.0.3",
+    "tsconfig-paths": "^4.0.0",
     "typescript": "^4.4.2",
     "typescript": "^4.4.2",
     "webpack": "^5.51.1",
     "webpack": "^5.51.1",
     "webpack-cli": "^4.8.0"
     "webpack-cli": "^4.8.0"

+ 0 - 29
packages/compiler/src/core/Lexer.ts

@@ -1,29 +0,0 @@
-import type PugLexer from "pug-lexer";
-import type Token from "./lexer/Token";
-import ForEach from "./lexer/tokens/ForEach";
-import Property from "./lexer/tokens/Property";
-import IfToken from "./lexer/tokens/If";
-import Import from "./lexer/tokens/Import";
-import PupperToAlpine from "./lexer/tokens/PupperToAlpine";
-import Component from "./lexer/tokens/Component";
-
-export default class Lexer {
-    public static Tokens: typeof Token[] = [
-        IfToken,
-        ForEach,
-        Component,
-        Property,
-        PupperToAlpine,
-        Import
-    ];
-
-    /**
-     * Checks if a given expression is valid
-     * @param lexer The pug lexer instance
-     * @param exp The expression to be checked against
-     * @returns 
-     */
-    public isExpression(lexer: PugLexer.Lexer, exp: string) {
-        return Lexer.Tokens.some((token) => token.testExpression(exp));
-    }
-}

+ 233 - 40
packages/compiler/src/core/Plugin.ts

@@ -1,31 +1,118 @@
-import Lexer from "./Lexer";
-import Token from "./lexer/Token";
 
 
-import { PugPlugin, PugToken, PugAST, PugNode, Options } from "pug";
-import PupperCompiler from "..";
+import type PugLexer from "pug-lexer";
+import type { PugPlugin, PugToken, PugAST, PugNode, PugNodes, PugNodeAttribute, LexerPlugin, Options } from "pug";
+import type PupperCompiler from "..";
 
 
-export { PugToken, PugAST, PugNode };
+import { Hook } from "./plugin/Hook";
+
+import { IfHook } from "./plugin/hooks/IfHook";
+import { ForEachHook } from "./plugin/hooks/ForEachHook";
+import { ComponentHook } from "./plugin/hooks/ComponentHook";
+import { PropertyHook } from "./plugin/hooks/PropertyHook";
+import { PupperToAlpineHook } from "./plugin/hooks/PupperToAlpineHook";
+import { ImportHook } from "./plugin/hooks/ImportHook";
+import { CompilerNode } from "../model/core/nodes/CompilerNode";
+import { StyleAndScriptHook } from "./plugin/hooks/StyleAndScriptHook";
+
+import { AstNode } from "./plugin/nodes/AstNode";
+import { EachNode } from "./plugin/nodes/EachNode";
+import { TagNode } from "./plugin/nodes/TagNode";
+import { NodeModel } from "../model/core/NodeModel";
+import { MixinNode } from "./plugin/nodes/MixinNode";
+import { ConditionalNode } from "./plugin/nodes/ConditionalNode";
+import { InspectNode } from "../util/NodeUtil";
+
+type THookArray = { new(plugin: Plugin): Hook }[];
+
+export type TPugNodesWithTypes = {
+    [key in PugNodes["type"]]: Extract<PugNodes, { type: key }>
+}
+
+export type TPugNodeTypes = Pick<PugNodes, "type">["type"];
+
+/**
+ * Anything that extends a compiler node.
+ */
+export type TCompilerNode<T extends CompilerNode = any> = T;
+
+/**
+ * The relationship between a pug node type and a plugin node.
+ */
+interface INodeModelPugNodeTypeRelationship extends Record<TPugNodeTypes, TCompilerNode> {
+    Tag: TagNode;
+    Conditional: ConditionalNode;
+    Each: EachNode;
+    Mixin: MixinNode;
+    //Block: AstNode;
+}
+
+/**
+ * Retrieves a node model by the pug node type.
+ */
+type TNodeModelByPugNodeType<TNode extends TPugNodeTypes> = Pick<INodeModelPugNodeTypeRelationship, TNode>;
+
+/**
+ * Retrieves the node model by the pug node.
+ */
+type TNodeModelByPugNode<TNode extends PugNodes, TNodeType extends TPugNodeTypes = TNode["type"]> = TNodeModelByPugNodeType<TNodeType>;
+
+export { PugToken, PugAST, PugNode, PugNodeAttribute, PugNodes, CompilerNode as IPluginNode };
 
 
 /**
 /**
  * Documentation for this class is available in the PugPlugin interface
  * Documentation for this class is available in the PugPlugin interface
  */
  */
 export default class Plugin implements PugPlugin {
 export default class Plugin implements PugPlugin {
+    public static Hooks: THookArray = [
+        IfHook,
+        ForEachHook,
+        ComponentHook,
+        PropertyHook,
+        PupperToAlpineHook,
+        ImportHook,
+        StyleAndScriptHook
+    ];
+
     /**
     /**
-     * The instances of the tokens that will be used to parse the template file
+     * Creates a compiler node from a pug node.
+     * @param node The pug node.
+     * @param parent The parent node to this node.
+     * @returns 
      */
      */
-    private tokens: Token[] = [];
+    public static createNode<TNode extends PugNodes>(node: TNode, parent: NodeModel): TNodeModelByPugNode<TNode> | CompilerNode {
+        // If somehow this happens, prevent from going further
+        if (node instanceof CompilerNode) {
+            return node;
+        }
+
+        switch(node.type) {
+            default:
+                return new CompilerNode(node, parent);
+
+            case "Each":
+                return new EachNode(node, parent);
+
+            case "Tag":
+                return new TagNode(node, parent);
+
+            case "Mixin":
+                return new MixinNode(node, parent);
+
+            case "Conditional":
+                return new ConditionalNode(node, parent);
+        }
+    }
 
 
     /**
     /**
-     * A handler for the plugin hooks
+     * A handler for the plugin filters.
      */
      */
-    private hooks: Record<string, Function[]> = {};
+    private filters: Record<string, Function[]> = {};
 
 
     /**
     /**
-     * Any data to be shared between hooks and phases
+     * Any data to be shared between hooks and phases.
      */
      */
     public sharedData: Record<any, any> = {};
     public sharedData: Record<any, any> = {};
 
 
-    public lex = new Lexer();
+    public lex: LexerPlugin;
 
 
     constructor(
     constructor(
         public compiler: PupperCompiler,
         public compiler: PupperCompiler,
@@ -33,59 +120,165 @@ export default class Plugin implements PugPlugin {
             contents?: string
             contents?: string
         }
         }
     ) {
     ) {
-        for(let token of Lexer.Tokens) {
-            this.tokens.push(new token(this));
-        }
+        this.prepareHooks();
+
+        // Create the lexer
+        this.lex = {
+            isExpression: (lexer: PugLexer.Lexer, exp: string) => 
+                this.applyFilters<string, boolean>("testExpression", exp)
+        };
     }
     }
 
 
-    public getOptions() {
+    /**
+     * Prepares a list of ordered hooks.
+     */
+    protected prepareHooks() {
+        const hookOrder: string[] = [];
+
+        Plugin.Hooks
+            // Create the hooks instances
+            .map((Hook) => new Hook(this))
+            .sort((b, a) => {
+                if (a.$before) {
+                    const $before = a.$before?.map((hook) => hook.prototype.constructor.name);
+
+                    // If A needs to run before B
+                    if ($before.includes(b.constructor.name)) {
+                        return -1;
+                    } else {
+                        return 1;
+                    }
+                }
+
+                if (a.$after) {
+                    const $after = a.$after.map((hook) => hook.prototype.constructor.name);
+
+                    // If A needs to run after B
+                    if ($after.includes(b.constructor.name)) {
+                        return 1;
+                    } else {
+                        return -1;
+                    }
+                }
+
+                return 0;
+            })
+            .forEach((hook) => {
+                // Prepare their filters
+                hook.prepareFilters();
+
+                hookOrder.push(hook.constructor.name);
+            });
+    }
+
+    /**
+     * Retrieves the compiler options
+     * @returns 
+     */
+    public getCompilerOptions() {
         return this.options;
         return this.options;
     }
     }
 
 
-    public addHook(hook: string, callback: Function) {
-        if (this.hooks[hook] === undefined) {
-            this.hooks[hook] = [];
+    /**
+     * Adds a filter to a given event.
+     * @param filter The filter to be added.
+     * @param callback The filter callback.
+     * @returns 
+     */
+    public addFilter(filter: string, callback: Function) {
+        if (this.filters[filter] === undefined) {
+            this.filters[filter] = [];
         }
         }
 
 
-        return this.hooks[hook].push(callback);
+        return this.filters[filter].push(callback);
     }
     }
 
 
-    public applyFilters(hook: string, initialValue: any) {
-        // If has no hooks, return the initial value
-        if (this.hooks[hook] === undefined) {
-            return initialValue;
+    public applyFilters<TValue, TResultingValue = TValue>(filter: string, value: TValue): TResultingValue {
+        // If has no filters, return the initial value
+        if (this.filters[filter] === undefined) {
+            return value as any as TResultingValue;
         }
         }
 
 
-        let value = initialValue;
+        try {
+            for(let callback of this.filters[filter]) {
+                value = callback(value);
+            }
+        } catch(e) {
+            console.error(e);
+            throw e;
+        }
+
+        return value as any as TResultingValue;
+    }
+
+    /**
+     * Parses the children of a node.
+     * @param node The node or node array to be parsed.
+     * @returns 
+     */
+    public parseChildren(node: NodeModel|NodeModel[]) {
+        if (Array.isArray(node)) {
+            this.applyFilters("parse", node);
 
 
-        for(let callback of this.hooks[hook]) {
-            value = callback(value);
+            node.forEach((node) => {
+                this.parseChildren(node);
+            });
+        } else {
+            node.setChildren(
+                this.applyFilters("parse", node.getChildren())
+            );
+
+            node.getChildren().forEach((child) => {
+                if (child.hasChildren()) {
+                    this.parseChildren(child);
+                }
+            });
         }
         }
 
 
-        return value;
+        return node;
     }
     }
 
 
-    public preParse(tokens: PugToken[]) {
-        for(let token of this.tokens) {
-            token.lex(tokens);
-        }    
+    /**
+     * Parses an AST.
+     * @param ast The AST to be parsed.
+     * @returns 
+     */
+    public parseNodes(ast: PugAST) {
+        try {
+            const astNode = new AstNode(ast);
+
+            // Parse the AST children
+            this.parseChildren(astNode);
+
+            return astNode.toPugNode();
+        } catch(e) {
+            console.error(e);
+            throw e;
+        }
+    }
+
+    /**
+     * Pug filters implementations
+     */
+
+    public preLex(template: string) {
+        this.options.contents = this.applyFilters("preLex", template);
+        return this.options.contents;
+    }
 
 
-        return this.applyFilters("preParse", tokens);
+    public preParse(tokens: PugToken[]) {
+        return this.applyFilters("lex", tokens);
     }
     }
 
 
     public postParse(block: PugAST) {
     public postParse(block: PugAST) {
-        for(let token of this.tokens) {
-            block.nodes = token.parse(block.nodes);
-        }
+        return this.parseNodes(block);
+    }
 
 
-        return this.applyFilters("postParse", block);
+    public preCodeGen(ast: PugAST): PugAST {
+        return this.applyFilters("preCodeGen", ast);
     }
     }
 
 
     public postCodeGen(code: string): string {
     public postCodeGen(code: string): string {
-        for(let token of this.tokens) {
-            code = token.afterCompile(code);
-        }
-
         return this.applyFilters("postCodeGen", code);
         return this.applyFilters("postCodeGen", code);
     }
     }
 }
 }

+ 0 - 58
packages/compiler/src/core/lexer/Token.ts

@@ -1,58 +0,0 @@
-import Plugin, { PugAST } from "../Plugin";
-
-import { PugToken, PugNode } from "pug";
-
-export default class Token {
-    public static readonly REGEX: RegExp;
-
-    constructor(
-        protected plugin: Plugin
-    ) {
-        
-    }
-
-    /**
-     * Tests if the token matches with the given expression
-     * @param exp The expression to be tested
-     * @returns 
-     */
-    public static testExpression(exp: string): boolean {
-        return false;
-    }
-
-    /**
-     * Lexes the given token and return new ones
-     * @param tokens The tokens to be parsed
-     * @returns Parsed tokens
-     */
-    public lex(tokens: PugToken[]) {
-        return tokens;
-    }
-
-    /**
-     * Parses the given token and return new ones
-     * @param nodes The nodes to be parsed
-     * @returns Parsed nodes
-     */
-    public parse(nodes: PugNode[]) {
-        return nodes;
-    }
-
-    /**
-     * Called before the AST is compiled into Javascript
-     * @param ast The pug AST to be compiled
-     * @returns 
-     */
-    public beforeCompile(ast: PugAST) {
-        return ast;
-    }
-
-    /**
-     * Called after the AST is compiled into Javascript
-     * @param code The generated Javascript code
-     * @returns 
-     */
-    public afterCompile(code: string) {
-        return code;
-    }
-}

+ 0 - 52
packages/compiler/src/core/lexer/tokens/ForEach.ts

@@ -1,52 +0,0 @@
-import { PugNode } from "../../Plugin";
-import Token from "../Token";
-
-export default class ForEach extends Token {
-    public parse(nodes: PugNode[]) {
-        for(let index = 0; index < nodes.length; index++) {
-            const node = nodes[index];
-
-            // Check if it's an each
-            if (node.type === "Each") {
-                // Turn it into a <template x-each>
-                node.type = "Tag";
-                node.name = "template";
-                node.selfClosing = false;
-                node.attributeBlocks = [];
-                node.isInline = false;
-                node.attrs = [
-                    {
-                        name: "x-for",
-                        val: `"${node.val.trim()} of ${node.obj.trim()}"`,
-                        mustEscape: false
-                    }
-                ];
-
-                if (node.block.nodes.length > 1) {
-                    node.block.nodes = [
-                        {
-                            type: "Tag",
-                            name: "div",
-                            selfClosing: false,
-                            attributeBlocks: [],
-                            isInline: false,
-                            attrs: [],
-                            block: node.block
-                        }
-                    ]
-                }
-
-                delete node.obj;
-                delete node.key;
-                delete node.val;
-            }
-
-            // Parses the block
-            if (node.block) {
-                node.block.nodes = this.parse(node.block.nodes);
-            }
-        }
-
-        return nodes;
-    }
-};

+ 0 - 67
packages/compiler/src/core/lexer/tokens/If.ts

@@ -1,67 +0,0 @@
-import { PugNode } from "../../Plugin";
-import Token from "../Token";
-
-export default class ForEach extends Token {
-    public parse(nodes: PugNode[]) {
-        for(let index = 0; index < nodes.length; index++) {
-            const node = nodes[index];
-
-            // Check if it's a conditional
-            if (node.type === "Conditional") {
-                // Clone it
-                const conditional = { ...node };
-
-                // Replace with an if <template x-if>
-                node.type = "Tag";
-                node.name = "template";
-                node.selfClosing = false;
-                node.attributeBlocks = [];
-                node.isInline = false;
-                node.attrs = [{
-                    name: "x-if",
-                    val: `"${conditional.test}"`,
-                    mustEscape: false
-                }];
-                node.block = {
-                    type: "Block",
-                    nodes: this.parse(conditional.consequent.nodes)
-                };
-
-                // <template v-if!>
-                if (!!conditional.alternate) {
-                    nodes.splice(index + 1, 0, 
-                        {
-                            type: "Tag",
-                            name: "template",
-                            start: 0,
-                            end: 0,
-                            attributeBlocks: [],
-                            isInline: false,
-                            selfClosing: false,
-                            attrs: [{
-                                name: "x-if",
-                                val: `"!(${conditional.test})"`,
-                                mustEscape: false
-                            }],
-                            block: {
-                                type: "Block",
-                                nodes: this.parse(conditional.alternate.nodes)
-                            }
-                        }   
-                    );
-                }
-
-                delete node.test;
-                delete node.consequent;
-                delete node.alternate;
-            }
-
-            // Parses the block
-            if (node.block) {
-                node.block.nodes = this.parse(node.block.nodes);
-            }
-        }
-
-        return nodes;
-    }
-};

+ 0 - 92
packages/compiler/src/core/lexer/tokens/Import.ts

@@ -1,92 +0,0 @@
-import { PugNode } from "../../Plugin";
-import Token from "../Token";
-
-export default class Import extends Token {
-    private static readonly IMPORT_CONDITION = /import? (?<identifier>.+?) from \"?\'?(?<filename>.+)\"?\'?$/;
-
-    /**
-     * The imports that will later be putted into the template header
-     */
-    protected imports: Record<string, string> = {};
-
-    public parse(nodes: PugNode[]) {
-        for(let node of nodes) {
-            // Check if it's a tag node
-            if (node.type === "Tag") {
-                // If it's an import tag
-                if (node.name === "import") {
-                    const condition: RegExpMatchArray = ("import " + node.attrs.map((attr) => attr.name).join(" ")).match(Import.IMPORT_CONDITION);
-
-                    // Check if it's an invalid foreach condition
-                    if (!condition) {
-                        throw new TypeError("Invalid import condition. It needs to have an alias and a filename.");
-                    }
-
-                    const { identifier, filename } = condition.groups;
-
-                    this.plugin.sharedData.imports = this.plugin.sharedData.imports || {};
-                    this.plugin.sharedData.imports[identifier] = filename;
-
-                    // Remove the node from it
-                    nodes.splice(nodes.indexOf(node), 1);
-
-                    continue;
-                } else
-                // If it's trying to import a previously imported template
-                if (this.plugin.sharedData.imports?.[node.name] !== undefined) {
-                    // If has a body
-                    if (node.block?.nodes.length > 0) {
-                        throw new Error("Template tags can't have a body.");
-                    }
-
-                    const templateName = node.name;
-
-                    // Replace it with an import markup tag
-                    node.name = "div";
-                    node.selfClosing = true;
-                    node.isInline = false;
-                    node.attrs = [
-                        {
-                            name: "x-data",
-                            val: node.attrs.find((n) => n.name === "data")?.val.trim(),
-                            mustEscape: true
-                        },
-                        {
-                            name: "x-template",
-                            val: templateName,
-                            mustEscape: true
-                        }
-                    ]
-                }
-            }
-
-            // Parses the block
-            if (node.block) {
-                node.block.nodes = this.parse(node.block.nodes);
-            }
-        }
-
-        return nodes;
-    }
-
-    public afterCompile(code: string) {
-        const importNames = Object.keys(this.imports);
-
-        // Check if has any import
-        if (importNames.length) {
-            // Prepare the import handler
-            let imports = `${this.plugin.getOptions().name}.imports = {`;
-
-            // Add all imports to it
-            imports += importNames.map((name) => {
-                return `"${name}": require("${this.imports[name]}")`;
-            }).join(",");
-            
-            imports += `};`
-
-            code += `\n\n${imports}\n`;
-        }
-
-        return code;
-    }
-};

+ 106 - 0
packages/compiler/src/core/plugin/Hook.ts

@@ -0,0 +1,106 @@
+import Plugin, { PugAST, IPluginNode, PugToken, PugNodes } from "../Plugin";
+import PugError from "pug-error";
+import { CompilerNode } from "../../model/core/nodes/CompilerNode";
+
+export interface Hook {
+    /**
+     * Tests if an expression is valid to be lexed.
+     * @param exp The expression to be tested.
+     * @returns 
+     */
+    testExpression?(exp?: string): boolean;
+
+    /**
+     * Executed before the code is lexed.
+     * @param template The input code that is about to be lexed.
+     * @returns
+     */
+    beforeStart?(template: string): string;
+
+    /**
+     * Lexes the given token and return new ones
+     * @param tokens The tokens to be parsed
+     * @returns Parsed tokens
+     */
+
+    lex?(tokens: PugToken[]): PugToken[];
+
+    /**
+     * Parses the given token and return new ones
+     * @param nodes The nodes to be parsed
+     * @returns Parsed nodes
+     */
+    parse?(nodes: IPluginNode[]): IPluginNode[];
+
+    /**
+     * Called before the AST is compiled into Javascript.
+     * @param ast The pug AST to be compiled
+     * @returns 
+     */
+    beforeCompile?(ast: PugAST): PugAST;
+
+    /**
+     * Called after the AST is compiled into Javascript.
+     * @param code The generated Javascript code
+     * @returns 
+     */
+    afterCompile?(code: string): string;
+}
+
+export abstract class Hook {
+    /**
+     * All hooks that this hook needs to be executed before.
+     */
+    public $before?: (typeof Hook)[];
+
+    /**
+     * All hooks that this hook needs to wait for their execution.
+     */
+    public $after?: (typeof Hook)[];
+
+    constructor(
+        protected plugin: Plugin
+    ) {
+        
+    }
+
+    /**
+     * Prepares this hook filters.
+     */
+    public prepareFilters() {
+        if ("beforeStart" in this) {
+            this.plugin.addFilter("preLex", this.beforeStart.bind(this));
+        }
+
+        if ("lex" in this) {
+            this.plugin.addFilter("lex", this.lex.bind(this));
+        }
+
+        if ("parse" in this) {
+            this.plugin.addFilter("parse", this.parse.bind(this));
+        }
+
+        if ("beforeCompile" in this) {
+            this.plugin.addFilter("preCodeGen", this.beforeCompile.bind(this));
+        }
+
+        if ("afterCompile" in this) {
+            this.plugin.addFilter("postCodeGen", this.afterCompile.bind(this));
+        }
+    }
+
+    protected makeNode(node: PugNodes, parent: CompilerNode) {
+        return Plugin.createNode(node, parent);
+    }
+
+    protected makeError(code: string, message: string, data: {
+        line?: number;
+        column?: number;
+    } = {}) {
+        return PugError(code, message, {
+            ...data,
+            filename: this.plugin.options.filename,
+            src: this.plugin.options.contents
+        } as any);
+    }
+}

+ 41 - 45
packages/compiler/src/core/lexer/tokens/Component.ts → packages/compiler/src/core/plugin/hooks/ComponentHook.ts

@@ -1,6 +1,9 @@
-import { PugNode } from "../../Plugin";
-import Token from "../Token";
+import { IPluginNode } from "../../Plugin";
+import { Hook } from "../Hook";
+import { TagNode } from "../nodes/TagNode";
 import { ScriptParser } from "./component/ScriptParser";
 import { ScriptParser } from "./component/ScriptParser";
+import { IfHook } from "./IfHook";
+import { StyleAndScriptHook } from "./StyleAndScriptHook";
 
 
 const DefaultExportSymbol = Symbol("ExportedComponent");
 const DefaultExportSymbol = Symbol("ExportedComponent");
 
 
@@ -13,22 +16,27 @@ export interface IComponent {
     exported?: boolean;
     exported?: boolean;
 }
 }
 
 
-export default class Component extends Token {
+export class ComponentHook extends Hook {
+    public $after = [IfHook, StyleAndScriptHook];
+
     /**
     /**
-     * Parses a pug node into a component.
-     * @param node The pug node to be parsed.
-     * @returns 
+     * The imports that will later be putted into the template header
      */
      */
-    public parseNode(node: PugNode, nextNode: PugNode) {
-        const name = node.attrs.find((node) => node.name === "name")?.val.replace(/"/g, "");
+    protected components: Record<string | symbol, IComponent> = {};
+ 
+    public parseComponentNode(node: TagNode) {
+        const name = node.getAttribute("name")?.replace(/"/g, "");
 
 
-        const template = node.block?.nodes.find((node) => node.type === "Tag" && node.name === "template");
-        const script = node.block?.nodes.find((node) => node.type === "Tag" && node.name === "script");
-        const style = node.block?.nodes.find((node) => node.type === "Tag" && node.name === "style");
+        const template = node.findFirstChildByTagName("template") as TagNode;
+        const script = node.findFirstChildByTagName("script") as TagNode;
+        const style = node.findFirstChildByTagName("style") as TagNode;
 
 
         // If no script tag was found
         // If no script tag was found
         if (!script) {
         if (!script) {
-            throw new Error("Components must have at least a script tag.");
+            throw this.makeError("COMPONENT_HAS_NO_SCRIPT_TAG", "Components must have a a script tag.", {
+                line: node.getLine(),
+                column: node.getColumn()
+            });
         }
         }
 
 
         /**
         /**
@@ -39,7 +47,7 @@ export default class Component extends Token {
             template: null,
             template: null,
             script: null,
             script: null,
             style: null,
             style: null,
-            exported: node.attrs.find((attr) => attr.name === "export") !== undefined
+            exported: node.hasAttribute("export")
         };
         };
 
 
         // If the component is not exported and has no name
         // If the component is not exported and has no name
@@ -55,13 +63,14 @@ export default class Component extends Token {
 
 
         // If has a template
         // If has a template
         if (template) {
         if (template) {
+            this.plugin.parseChildren(template);
             let lines = this.plugin.options.contents.split("\n");
             let lines = this.plugin.options.contents.split("\n");
 
 
-            const nextNodeAfterTemplate = node.block.nodes[node.block.nodes.indexOf(template) + 1];
+            const nextNodeAfterTemplate = template.getNextNode();
 
 
             lines = lines.slice(
             lines = lines.slice(
-                template.line,
-                nextNodeAfterTemplate ? nextNodeAfterTemplate.line - 1 : nextNode.line - 1
+                template.getLine(),
+                nextNodeAfterTemplate ? nextNodeAfterTemplate.getLine() - 1 : (node.hasNext() ? node.getNextNode().getLine() - 1 : lines.length)
             );
             );
 
 
             // Detect identation
             // Detect identation
@@ -78,7 +87,9 @@ export default class Component extends Token {
 
 
         // If has a script
         // If has a script
         if (script) {
         if (script) {
-            const scriptContent = script.block.nodes.map((node) => node.val).join("");
+            this.plugin.parseChildren(script);
+
+            const scriptContent = script.getChildren().map((node) => node.getProp("val")).join("");
             component.script = scriptContent;
             component.script = scriptContent;
         }
         }
 
 
@@ -90,35 +101,20 @@ export default class Component extends Token {
         return component;
         return component;
     }
     }
 
 
-    /**
-     * The imports that will later be putted into the template header
-     */
-    protected components: Record<string | symbol, IComponent> = {};
-
-    public parse(nodes: PugNode[]) {
-        for(let i = 0; i < nodes.length; i++) {
-            const node = nodes[i];
+    public parse(nodes: IPluginNode[]) {
+        for(let node of nodes) {
+            // Check if it's a tag "component" node
+            if (node.isType("Tag") && node.isName("component")) {
+                // Parse the component
+                const component = this.parseComponentNode(node as TagNode);
 
 
-            // Check if it's a tag node
-            if (node.type === "Tag") {
-                // If it's a component tag
-                if (node.name === "component") {
-                    // Parse the component
-                    const component = this.parseNode(node, nodes[i + 1]);
+                // Save the component
+                this.components[component.name] = component;
 
 
-                    // Save the component
-                    this.components[component.name] = component;
-
-                    // Remove the node from the body
-                    nodes.splice(nodes.indexOf(node), 1);
-
-                    continue;
-                }
-            }
+                // Remove the node from the template
+                node.delete();
 
 
-            // Parses the block
-            if (node.block) {
-                node.block.nodes = this.parse(node.block.nodes);
+                continue;
             }
             }
         }
         }
 
 
@@ -133,12 +129,12 @@ export default class Component extends Token {
             // Parse the script
             // Parse the script
             const parsedScript = new ScriptParser(
             const parsedScript = new ScriptParser(
                 exportedComponent,
                 exportedComponent,
-                this.plugin.getOptions().filename,
+                this.plugin.getCompilerOptions().filename,
                 this.components,
                 this.components,
                 this.plugin
                 this.plugin
             ).parse();
             ).parse();
 
 
-            code += `\n\n${parsedScript}\n`;
+            code = `${parsedScript}\n`;
 
 
             if (exportedComponent.style) {
             if (exportedComponent.style) {
                 code += `\n${exportedComponent.style}\n`;
                 code += `\n${exportedComponent.style}\n`;

+ 23 - 0
packages/compiler/src/core/plugin/hooks/ForEachHook.ts

@@ -0,0 +1,23 @@
+import { IPluginNode } from "../../Plugin";
+import { Hook } from "../Hook";
+
+export class ForEachHook extends Hook {
+    public parse(nodes: IPluginNode[]) {
+        for(let node of nodes) {
+            // Check if it's an each
+            if (node.isType("Each")) {
+                // Turn it into a <div x-each>
+                node.replaceWith({
+                    type: "Tag",
+                    name: "template",
+                    attributes: {
+                        "x-for": /*js*/`${node.getProp("val").trim()} of ${node.getProp("obj").trim()}`
+                    },
+                    children: node.getChildren()
+                });
+            }
+        }
+
+        return nodes;
+    }
+};

+ 42 - 0
packages/compiler/src/core/plugin/hooks/IfHook.ts

@@ -0,0 +1,42 @@
+import { Hook } from "../Hook";
+import { ConditionalNode } from "../nodes/ConditionalNode";
+import { TagNode } from "../nodes/TagNode";
+
+export class IfHook extends Hook {
+    public parse(nodes: ConditionalNode[]) {
+        for(let node of nodes) {
+            // Check if it's a conditional
+            if (node.isType("Conditional")) {
+                const consequent = node.getThen();
+                const alternate = node.getElse();
+
+                // Replace with an if <div x-if>
+                // @todo this is actually buggy and not working.
+                // For some reason, the children are not being parsed correctly and
+                // are leaving as pug Javascript inside (like each iteration)
+                const conditional = node.replaceWith({
+                    type: "Tag",
+                    name: "template",
+                    attributes: {
+                        "x-if": node.getProp("test")
+                    },
+                    children: this.plugin.parseChildren(consequent) as any
+                }) as TagNode;
+
+                // <div x-if!>
+                if (node.hasElse()) {
+                    const elseTag = conditional.insertAfter({
+                        type: "Tag",
+                        name: "template",
+                        attributes: {
+                            "x-if": `!(${node.getProp("test")})`
+                        },
+                        children: this.plugin.parseChildren(alternate) as any
+                    });
+                }
+            }
+        }
+
+        return nodes;
+    }
+};

+ 117 - 0
packages/compiler/src/core/plugin/hooks/ImportHook.ts

@@ -0,0 +1,117 @@
+import {  PugToken } from "../../Plugin";
+import { Hook } from "../Hook";
+import { TagNode } from "../nodes/TagNode";
+
+export class ImportHook extends Hook {
+    /**
+     * Inline imports needs to be at root level.
+     * Needs to have an identifier and a filename.
+     * Identifiers can't start with numbers.
+     */
+    public static INLINE_IMPORT_REGEX = /^(?<match>import\s*(?<identifier>[^0-9][a-zA-Z0-9_]+?)\((?:from=)?(['"])(?<filename>.+?)\3\))\s*/m;
+
+    /**
+     * The imports that will later be putted into the template header
+     */
+    protected imports: Record<string, string> = {};
+
+    public beforeStart(template: string) {
+        let match;
+
+        while(match = ImportHook.INLINE_IMPORT_REGEX.exec(template)) {
+            template = template.replace(match.groups.match, `import(identifier="${match.groups.identifier}", from="${match.groups.filename}")`);
+        }
+        
+        return template;
+    }
+
+    public lex(tokens: PugToken[]) {
+        for(let i = 0; i < tokens.length; i++) {
+            const currentToken = tokens[i];
+            const nextToken = tokens[i + 1];
+
+            if (!nextToken) {
+                continue;
+            }
+
+            if (currentToken.type === "tag" && currentToken.val === "import" && nextToken.type === "text") {
+                const fullImport = ("import " + nextToken.val);
+
+                let match = fullImport.match(ImportHook.INLINE_IMPORT_REGEX);
+
+                if (!match) {
+                    throw this.makeError("INVALID_IMPORT", "Invalid import expression.", {
+                        line: currentToken.loc.line,
+                        column: currentToken.loc.column
+                    });
+                }
+
+                tokens = tokens.splice(i + 1, 1, 
+                    {
+                        type: "start-attributes",
+                        loc: currentToken.loc
+                    },
+                    {
+                        type: "attribute",
+                        loc: currentToken.loc,
+                        name: "identifier",
+                        val: match.groups.identifier
+                    },
+                    {
+                        type: "attribute",
+                        loc: currentToken.loc,
+                        name: "filename",
+                        val: match.groups.filename
+                    },
+                    {
+                        type: "end-attributes",
+                        loc: currentToken.loc
+                    }
+                );
+            }
+        }
+
+        return tokens;
+    }
+
+    public parse(nodes: TagNode[]) {
+        for(let node of nodes) {
+            // Check if it's a tag node
+            if (node.isType("Tag")) {
+                // If it's an import tag
+                if (node.isName("import")) {                    
+                    this.plugin.sharedData.imports = this.plugin.sharedData.imports || {};
+                    this.plugin.sharedData.imports[node.getAttribute("identifier").replace(/["']/g, "")] = node.getAttribute("filename") || node.getAttribute("from").replace(/["']/g, "");
+
+                    // Remove the node from the template
+                    node.delete();
+
+                    continue;
+                } else
+                // If it's trying to import a previously imported template
+                if (this.plugin.sharedData.imports?.[node.getProp("name")] !== undefined) {
+                    // If has a body
+                    if (node.hasChildren()) {
+                        throw this.makeError("IMPORT_TAG_WITH_BODY", "Imported tags can't have a body.", {
+                            line: node.getLine(),
+                            column: node.getColumn()
+                        });
+                    }
+
+                    node.replaceWith({
+                        type: "Tag",
+                        name: "div",
+                        selfClosing: true,
+                        isInline: false,
+                        attributes: {
+                            "x-data": node.getAttribute("data")?.trim(),
+                            "x-template": node.getProp("name")
+                        }
+                    });
+                }
+            }
+        }
+
+        return nodes;
+    }
+};

+ 4 - 9
packages/compiler/src/core/lexer/tokens/Property.ts → packages/compiler/src/core/plugin/hooks/PropertyHook.ts

@@ -1,17 +1,12 @@
 import { PugToken } from "../../Plugin";
 import { PugToken } from "../../Plugin";
-import Token from "../Token";
+import { Hook } from "../Hook";
 
 
-export default class Property extends Token {
+export class PropertyHook extends Hook {
     /**
     /**
      * The regex to test if an expression is a valid reactive item
      * The regex to test if an expression is a valid reactive item
      */
      */
     public static REGEX = /\{(?<tag>\{|-) ?(?<exp>(?:[\w+]|\.)+) ?(\}|-)\}/;
     public static REGEX = /\{(?<tag>\{|-) ?(?<exp>(?:[\w+]|\.)+) ?(\}|-)\}/;
 
 
-    /**
-     * Tests if the token matches with the given expression
-     * @param exp The expression to be tested
-     * @returns 
-     */
     public static testExpression(exp: string) {
     public static testExpression(exp: string) {
         return this.REGEX.test(exp);
         return this.REGEX.test(exp);
     }
     }
@@ -24,9 +19,9 @@ export default class Property extends Token {
             }
             }
 
 
             // Check if it's a reactive item
             // Check if it's a reactive item
-            if (token.mustEscape && Property.REGEX.test(token.val)) {
+            if (token.mustEscape && PropertyHook.REGEX.test(token.val)) {
                 // Extract the token value
                 // Extract the token value
-                const result = token.val.match(Property.REGEX).groups;
+                const result = token.val.match(PropertyHook.REGEX).groups;
                 const value = result.exp.replace(/\"/g, "\\\"");
                 const value = result.exp.replace(/\"/g, "\\\"");
 
 
                 const fn = result.tag === "{" ? "escape" : "literal";
                 const fn = result.tag === "{" ? "escape" : "literal";

+ 10 - 8
packages/compiler/src/core/lexer/tokens/PupperToAlpine.ts → packages/compiler/src/core/plugin/hooks/PupperToAlpineHook.ts

@@ -1,8 +1,8 @@
 import { PugToken } from "../../Plugin";
 import { PugToken } from "../../Plugin";
-import Token from "../Token";
+import { Hook } from "../Hook";
 
 
-export default class PupperToAlpine extends Token {
-    public static Directives: Record<string, string> = {
+export class PupperToAlpineHook extends Hook {
+    public static Attributes: Record<string, string> = {
         "p-show": "x-show",
         "p-show": "x-show",
         "p-on": "x-on",
         "p-on": "x-on",
         "p-text": "x-text",
         "p-text": "x-text",
@@ -18,8 +18,7 @@ export default class PupperToAlpine extends Token {
         "ref": "x-ref",
         "ref": "x-ref",
         "p-cloak": "x-cloak",
         "p-cloak": "x-cloak",
         "p-if": "x-if",
         "p-if": "x-if",
-        "p-id": "x-id",
-        "p-teleport": "x-teleport"
+        "p-id": "x-id"
     };
     };
 
 
     public lex(tokens: PugToken[]) {
     public lex(tokens: PugToken[]) {
@@ -29,11 +28,14 @@ export default class PupperToAlpine extends Token {
                 return token;
                 return token;
             }
             }
 
 
-            // If it's a replaceable directive
-            if (token.name in PupperToAlpine.Directives) {
-                token.name = PupperToAlpine.Directives[token.name];
+            // If it's a replaceable attribute
+            if (token.name in PupperToAlpineHook.Attributes) {
+                // Replace it
+                token.name = PupperToAlpineHook.Attributes[token.name];
             }
             }
 
 
+            // If it's a p-insert
+
             return token;
             return token;
         });
         });
     }
     }

+ 24 - 0
packages/compiler/src/core/plugin/hooks/StyleAndScriptHook.ts

@@ -0,0 +1,24 @@
+import { Hook } from "../Hook";
+import { ComponentHook } from "./ComponentHook";
+
+export class StyleAndScriptHook extends Hook {
+    public $before = [ComponentHook];
+
+    public beforeStart(code: string) {
+        const regex = /^\s*(script|style).+?$/;
+
+        // Add dots to ending "script" and "style" tags
+        code = code.split(/[\r\n]/)
+        .filter((line) => line.trim().length)
+        .map((line) => {
+            if (line.match(regex) !== null && !line.trim().endsWith(".")) {
+                return line.trimEnd() + ".";
+            }
+
+            return line;
+        })
+        .join("\n");
+
+        return code;
+    }
+};

+ 11 - 2
packages/compiler/src/core/lexer/tokens/component/ScriptParser.ts → packages/compiler/src/core/plugin/hooks/component/ScriptParser.ts

@@ -1,7 +1,16 @@
-import { Project, ScriptTarget, SourceFile, SyntaxKind, ObjectLiteralExpression, CallExpression, PropertyAccessExpression } from "ts-morph";
+import {
+    Project,
+    ScriptTarget,
+    SourceFile,
+    SyntaxKind,
+    ObjectLiteralExpression,
+    CallExpression,
+    PropertyAccessExpression
+} from "ts-morph";
+
 import Plugin from "../../../Plugin";
 import Plugin from "../../../Plugin";
 
 
-import { IComponent } from "../Component";
+import { IComponent } from "../ComponentHook";
 
 
 export class ScriptParser {
 export class ScriptParser {
     protected sourceFile: SourceFile;
     protected sourceFile: SourceFile;

+ 1 - 1
packages/compiler/src/core/lexer/tokens/component/StyleParser.ts → packages/compiler/src/core/plugin/hooks/component/StyleParser.ts

@@ -1,4 +1,4 @@
-import { IComponent } from "../Component";
+import { IComponent } from "../ComponentHook";
 
 
 export class StyleParser {
 export class StyleParser {
     constructor(
     constructor(

+ 37 - 0
packages/compiler/src/core/plugin/nodes/AstNode.ts

@@ -0,0 +1,37 @@
+import { NodeModel } from "../../../model/core/NodeModel";
+import Plugin, { PugAST } from "../../Plugin";
+
+export class AstNode extends NodeModel {
+    constructor(
+        protected node: PugAST
+    ) {
+        super();
+        
+        node.nodes.forEach((node) => {
+            this.children.push(
+                Plugin.createNode(node, this)
+            );
+        });
+    }
+
+    public countAllNodes(start: NodeModel = this) {
+        let count = 0;
+
+        for(let child of start.getChildren()) {
+            count++;
+
+            if (child.hasChildren()) {
+                count += this.countAllNodes(child);
+            }
+        }
+
+        return count;
+    }
+
+    public toPugNode(): PugAST {
+        return {
+            type: "Block",
+            nodes: this.getChildren().map((child) => child.toPugNode())
+        };
+    }
+}

+ 122 - 0
packages/compiler/src/core/plugin/nodes/ConditionalNode.ts

@@ -0,0 +1,122 @@
+import { NodeModel } from "../../../model/core/NodeModel";
+import { BlockedCompilerNode } from "../../../model/core/nodes/BlockedCompilerNode";
+import { CompilerNode, TNodes } from "../../../model/core/nodes/CompilerNode";
+import { Pug } from "../../../typings/pug";
+import Plugin, { TCompilerNode } from "../../Plugin";
+
+export class ConditionalNode extends BlockedCompilerNode<Pug.Nodes.ConditionalNode> {
+    protected consequent: TCompilerNode[] = [];
+    protected alternate: TCompilerNode[] = [];
+
+    constructor(
+        node: Pug.Nodes.ConditionalNode,
+        parent: NodeModel
+    ) {
+        super(node, parent);
+
+        if (node.consequent) {
+            for(let consequent of node.consequent.nodes) {
+                this.consequent.push(
+                    Plugin.createNode(consequent, this)
+                );
+            }
+        }
+
+        if (node.alternate) {
+            for(let alternate of node.alternate.nodes) {
+                this.alternate.push(
+                    Plugin.createNode(alternate, this)
+                );
+            }
+        }
+    }
+
+    /**
+     * Retrieves the condition to be tested if true or false.
+     * @returns 
+     */
+    public getCondition() {
+        return this.pugNode.test;
+    }
+
+    /**
+     * Sets the condition to be tested if true or false.
+     * @returns 
+     */
+    public setCondition(condition: string) {
+        this.pugNode.test = condition;
+        return this;
+    }
+    
+    /**
+     * If has a conditional consequent.
+     * @returns 
+     */
+    public hasThen() {
+        return !!this.consequent && this.consequent.length;
+    }
+
+    /**
+     * Retrieves the children of the execution consequence.
+     * @returns 
+     */
+    public getThen() {
+        return this.consequent;
+    }
+
+    /**
+     * Sets the nodes for the conditional consequent.
+     * @param nodes The new consequent nodes.
+     * @returns 
+     */
+    public setThen(nodes: CompilerNode[]) {
+        this.consequent = nodes;
+        return this;
+    }
+
+    /**
+     * If has a conditional alternate.
+     * @returns 
+     */
+    public hasElse() {
+        return !!this.alternate && this.alternate.length;
+    }
+
+    /**
+     * Retrieves the children of the conditional alternate.
+     * @returns 
+     */
+    public getElse() {
+        return this.alternate;
+    }
+
+    /**
+     * Sets the nodes for the conditional alternate.
+     * @param nodes The new alternate nodes.
+     * @returns 
+     */
+    public setElse(nodes: CompilerNode[]) {
+        this.alternate = nodes;
+        return this;
+    }
+
+    public getChildrenContainers() {
+        return [this.consequent, this.alternate];
+    }
+
+    public toPugNode(): Pug.Nodes.ConditionalNode {
+        return {
+            ...this.pugNode,
+            type: "Conditional",
+            consequent: {
+                type: "Block",
+                nodes: this.consequent.map((node) => node.toPugNode())
+            },
+            alternate: {
+                type: "Block",
+                nodes: this.alternate.map((node) => node.toPugNode())
+            },
+            block: this.makeBlock()
+        };
+    }
+}

+ 28 - 0
packages/compiler/src/core/plugin/nodes/EachNode.ts

@@ -0,0 +1,28 @@
+import { Pug } from "../../../typings/pug";
+import { CompilerNode } from "../../../model/core/nodes/CompilerNode";
+
+export class EachNode extends CompilerNode<Pug.Nodes.EachNode> {
+    /**
+     * Retrieves the variable name for the object that will be iterated.
+     * @returns 
+     */
+    public getObjectName() {
+        return this.pugNode.obj;
+    }
+
+    /**
+     * Retrieves the variable name for the iteration index.
+     * @returns 
+     */
+    public getIndexName() {
+        return this.pugNode.index;
+    }
+
+    /**
+     * Retrieves the variable name for the iteration value.
+     * @returns 
+     */
+    public getValueName() {
+        return this.pugNode.val;
+    }
+}

+ 10 - 0
packages/compiler/src/core/plugin/nodes/MixinNode.ts

@@ -0,0 +1,10 @@
+import { Pug } from "../../../typings/pug";
+import { CompilerNode } from "../../../model/core/nodes/CompilerNode";
+
+export class MixinNode extends CompilerNode<Pug.Nodes.MixinNode> {
+    public toPugNode() {
+        // This can't be undefined
+        this.pugNode.attributeBlocks = this.pugNode.attributeBlocks || [];
+        return super.toPugNode();
+    }
+}

+ 57 - 0
packages/compiler/src/core/plugin/nodes/TagNode.ts

@@ -0,0 +1,57 @@
+import { Pug } from "../../../typings/pug";
+import { BlockedCompilerNode } from "../../../model/core/nodes/BlockedCompilerNode";
+
+export class TagNode extends BlockedCompilerNode<Pug.Nodes.TagNode> {
+    /**
+     * Checks if the node has the given attribute.
+     * @param name The attribute name.
+     * @returns 
+     */
+    public hasAttribute(name: string) {
+        return this.getAttribute(name) !== undefined;
+    }
+
+    /**
+     * Retrieves a node attribute by name.
+     * @param name The attribute name to be retrieved.
+     * @returns 
+     */
+    public getAttribute(name: string) {
+        return this.pugNode.attrs.find((attr) => attr.name === name)?.val as any;
+    }
+
+    /**
+     * Sets a node attribute value.
+     * @param name The atribute name.
+     * @param value The attribute value.
+     * @returns 
+     */
+    public setAttribute(name: string, value: string | boolean | number) {
+        if (typeof value === "string") {
+            value = `"${value}"`;
+        }
+
+        let attr;
+
+        if (!this.hasAttribute(name)) {
+            attr = {
+                name,
+                val: String(value),
+                mustEscape: false
+            };
+
+            this.pugNode.attrs.push(attr);
+        } else {
+            attr = this.getAttribute(name);
+            attr.val = String(value);
+        }
+
+        return attr;
+    }
+
+    public toPugNode() {
+        // This can't be undefined
+        this.pugNode.attributeBlocks = this.pugNode.attributeBlocks || [];
+        return super.toPugNode();
+    }
+}

+ 1 - 1
packages/compiler/src/index.ts

@@ -45,7 +45,7 @@ export = class PupperCompiler {
 
 
             return rendered;
             return rendered;
         } catch(e) {
         } catch(e) {
-            throw (options.debug ? e : new Error("Failed to compile template:" + e.message));
+            throw (options.debug ? e : new Error("Failed to compile template: " + e.message));
         }
         }
     }
     }
 
 

+ 108 - 0
packages/compiler/src/model/core/NodeModel.ts

@@ -0,0 +1,108 @@
+import Plugin, { PugNodes,  PugAST } from "../../core/Plugin";
+
+export interface IParserNode extends Record<string, any> {
+    type: string;
+    attributes?: Record<string, string | boolean | number>;
+    children?: IParserNode[] | NodeModel[];
+}
+
+export abstract class NodeModel<TChildren = any> {
+    /**
+     * The children nodes to this node.
+     */
+    public children: TChildren[] = [];
+
+    constructor(
+        /**
+         * The parent array that contains this node.
+         */ 
+        public parent?: NodeModel
+    ) {
+        
+    }
+
+    /**
+     * Checks if the node has children nodes.
+     * @returns 
+     */
+    public hasChildren() {
+        return this.children && this.children.length > 0;
+    }
+
+    /**
+     * Retrieves the node children nodes.
+     * @returns 
+     */
+    public getChildren() {
+        return this.children;
+    }
+
+    /**
+     * Sets the node children nodes.
+     * @param children The children nodes.
+     */
+    public setChildren(children: TChildren[]) {
+        this.children = children;
+    }
+
+    /**
+     * Retrieves the index inside the parent children for this node.
+     * @returns 
+     */
+    public getIndex() {
+        return this.parent.getChildren().indexOf(this);
+    }
+
+    /**
+     * Checks if has a previous node.
+     * @returns 
+     */
+    public hasPrev(): boolean {
+        return this.getPrevNode() !== null;
+    }
+
+    /**
+     * Retrieves the previous node to this node.
+     * @returns 
+     */
+    public getPrevNode(): NodeModel | null {
+        return this.parent.children[this.parent.children.indexOf(this) - 1] || null;
+    }
+
+    /**
+     * Checks if has a next node.
+     * @returns 
+     */
+    public hasNext(): boolean {
+        return this.getNextNode() !== null;
+    }
+
+    /**
+     * Retrieves the next node to this node.
+     * @returns 
+     */
+    public getNextNode() {
+        return this.parent.children[this.parent.children.indexOf(this) + 1] || null;
+    }
+
+    /**
+     * Removes the current node from the parent.
+     */
+    public delete() {
+        this.parent.children.splice(this.getIndex(), 1);
+    }
+
+    /**
+     * Retrieves all the containers that can have children nodes.
+     * @returns 
+     */
+    public getChildrenContainers() {
+        return [this.children];
+    }
+
+    /**
+     * Converts the node back into a pug node.
+     * @returns 
+     */
+    abstract toPugNode(): PugNodes | PugAST;
+}

+ 42 - 0
packages/compiler/src/model/core/nodes/BlockedCompilerNode.ts

@@ -0,0 +1,42 @@
+import { PugNodes, PugAST } from "../../../core/Plugin";
+import { NodeModel } from "../NodeModel";
+import { CompilerNode } from "./CompilerNode";
+
+export class BlockedCompilerNode<TNode extends PugNodes = any> extends CompilerNode {
+    constructor(
+        /**
+         * The pug node related to this node.
+         */
+        public pugNode: TNode,
+
+        /**
+         * The parent array that contains this node.
+         */ 
+        public parent?: NodeModel
+    ) {
+        super(pugNode, parent);
+    }
+
+    public makeBlock(children?: PugNodes[]): PugAST {
+        return {
+            type: "Block",
+            nodes: children || []
+        };
+    }
+
+    /**
+     * Converts the node back into a pug node.
+     * @returns 
+     */
+    public toPugNode() {
+        const finalNode = { ...this.pugNode };
+
+        finalNode.block = finalNode.block || this.makeBlock();
+
+        if (this.hasChildren()) {            
+            finalNode.block.nodes = this.children.map((node) => node.toPugNode());
+        }
+
+        return finalNode;
+    }
+}

+ 272 - 0
packages/compiler/src/model/core/nodes/CompilerNode.ts

@@ -0,0 +1,272 @@
+import Plugin, { PugNodes, PugNodeAttribute, TPugNodeTypes, TCompilerNode } from "../../../core/Plugin";
+import { NodeModel } from "../NodeModel";
+
+export interface IParserNode {
+    type: TPugNodeTypes;
+    name?: string;
+    line?: number;
+    attributes?: Record<string, string | boolean | number>;
+    children?: IParserNode[] | CompilerNode[];
+    selfClosing?: boolean;
+    isInline?: boolean;
+}
+
+export type TNodes = PugNodes | CompilerNode | IParserNode;
+
+export class CompilerNode<TNode extends PugNodes = any> extends NodeModel<CompilerNode> {
+    /**
+     * Makes a pug attribute node.
+     * @param key The attribute name.
+     * @param value The attribute value.
+     * @returns 
+     */
+    public static makePugNodeAttribute(key: string, value: string | boolean | number): PugNodeAttribute {
+        if (typeof value === "string") {
+            value = `"${value}"`;
+        }
+
+        return {
+            name: key,
+            val: String(value),
+            mustEscape: false
+        };
+    }
+
+    /**
+     * Parses a node in our format into a pug node.
+     * @param node The node to be parsed.
+     * @returns 
+     */
+    public static parseNodeIntoPugNode(node: IParserNode) {
+        if (!("type" in node)) {
+            throw new Error("No node type was given.");
+        }
+
+        const finalNode: PugNodes = {
+            ...node as any
+        };
+
+        if (node.type === "Tag") {
+            if (!("isInline" in node)) {
+                finalNode.isInline = false;
+            }
+
+            if (!("isInline" in node)) {
+                finalNode.selfClosing = false;
+            }
+        }
+
+        if (node.type === "Tag" || node.type === "Mixin") {
+            if (!("attributeBlocks" in node)) {
+                finalNode.attributeBlocks = [];
+            }
+        }
+
+        if ("attributes" in finalNode) {
+            finalNode.attrs = Object.keys(node.attributes)
+                .map((key) => 
+                    this.makePugNodeAttribute(key, node.attributes[key])
+                );
+
+            delete finalNode.attributes;
+        }
+
+        if ("children" in finalNode) {
+            finalNode.block = {
+                type: "Block",
+                nodes: node.children.map((node) => node instanceof NodeModel ? node : this.parseNodeIntoPugNode(node)) as any
+            };
+
+            delete finalNode.children;
+        }
+
+        return finalNode;
+    }
+
+    /**
+     * The children nodes to this node.
+     */
+    public children: CompilerNode<PugNodes>[] = [];
+
+    constructor(
+        /**
+         * The pug node related to this node.
+         */
+        public pugNode: TNode,
+
+        /**
+         * The parent array that contains this node.
+         */ 
+        public parent?: NodeModel
+    ) {
+        super(parent);
+
+        // If has children
+        if (pugNode.block && pugNode.block.nodes && pugNode.block.nodes.length) {
+            // Parse them into parser nodes
+            pugNode.block.nodes.forEach((node) => {
+                this.children.push(
+                    Plugin.createNode(node, this) as any
+                )
+            });
+        }
+    }
+
+    /**
+     * Finds the first children node by a given type.
+     * @param type The children node type.
+     * @returns 
+     */
+    public findFirstChildByType(type: string) {
+        return this.children.find((child) => child.isType(type));
+    }
+
+    /**
+     * Finds the first children node with "Tag" type and the given tag name.
+     * @param name The children node tag name.
+     * @returns 
+     */
+    public findFirstChildByTagName(name: string) {
+        return this.children.find((child) => child.isType("Tag") && child.isName(name));
+    }
+
+    /**
+     * Retrieves a property from the pug node.
+     * @param prop The property to be retrieved.
+     * @returns 
+     */
+    public getProp<TKey extends keyof TNode, TValue = TNode[TKey]>(prop: TKey): TValue {
+        return this.pugNode[prop];
+    }
+
+    /**
+     * Checks if the pug node has a given property.
+     * @param prop The property to be checked.
+     * @returns 
+     */
+    public hasProp<TKey extends keyof TNode>(prop: TKey) {
+        return prop in this.pugNode;
+    }
+
+    /**
+     * Checks if the node has the given type.
+     * @param type The type to be checked.
+     * @returns 
+     */
+    public isType(type: string) {
+        return this.pugNode.type === type;
+    }
+
+    /**
+     * Checks if the node has the given tag name.
+     * @param name The name to be checked.
+     * @returns 
+     */
+    public isName(name: string) {
+        return this.pugNode.name === name;
+    }
+
+    /**
+     * Retrieves the line where this node is.
+     * @returns 
+     */
+    public getLine() {
+        return this.pugNode.line;
+    }
+
+    /**
+     * Retrieves the column of the line where this node is.
+     * @returns 
+     */
+    public getColumn() {
+        return this.pugNode.column;
+    }
+
+    /**
+     * Replaces all node data with the given ones.
+     * @param node The new node to be replaced with
+     * @returns 
+     */
+    public replaceWith(node: TNodes) {
+        // Iterate over all possible children containers
+        for(let children of this.parent.getChildrenContainers()) {
+            // If this container includes the current node as a children
+            if (children.includes(this)) {
+                // Replace it
+                const newNode = Plugin.createNode(
+                    CompilerNode.parseNodeIntoPugNode(node as any),
+                    this.parent
+                );
+        
+                this.insertAfter(newNode, children);
+                this.deleteFrom(children);
+
+                return newNode;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Deletes the current node from an array.
+     * @param array The array where this node will deleted.
+     * @returns 
+     */
+    protected deleteFrom(array: any[]) {
+        return array.splice(array.indexOf(this), 1);
+    }
+
+    /**
+     * Inserts a node before the current node.
+     * @param node The node to be inserted
+     * @returns 
+     */
+    public prepend<TType extends CompilerNode | PugNodes | IParserNode>(node: TType) {
+        let finalNode = node as CompilerNode;
+
+        if (!(node instanceof CompilerNode)) {
+            finalNode = Plugin.createNode(CompilerNode.parseNodeIntoPugNode(node), this.parent) as any;
+        }
+
+        this.parent.children.splice(this.getIndex() - 1, 0, finalNode);
+        return this;
+    }
+
+    /**
+     * Inserts a node after the current node.
+     * @param node The node to be inserted
+     * @returns 
+     */
+    public insertAfter(node: CompilerNode | TCompilerNode | PugNodes | IParserNode, children = this.parent.children) {
+        let finalNode = node as CompilerNode;
+
+        if (!(node instanceof CompilerNode)) {
+            finalNode = Plugin.createNode(CompilerNode.parseNodeIntoPugNode(node), this.parent) as any;
+        }
+
+        children.splice(this.getIndex() + 1, 0, finalNode);
+        return this;
+    }
+
+    /**
+     * Converts the node back into a pug node.
+     * @returns 
+     */
+    public toPugNode() {
+        const finalNode = { ...this.pugNode };
+
+        if (this.hasChildren() || this.pugNode.block) {        
+            finalNode.block = finalNode.block || {
+                type: "Block",
+                nodes: []                
+            };
+        }
+
+        if (this.hasChildren()) {            
+            finalNode.block.nodes = this.children.map((node) => node.toPugNode());
+        }
+
+        return finalNode;
+    }
+}

+ 8 - 0
packages/compiler/src/typings/pug-error.d.ts

@@ -0,0 +1,8 @@
+declare module "pug-error" {
+    export default (code: string | number, message: string, props: {
+        line: number;
+        column: number;
+        filename: string;
+        src: string;
+    }) => Error;
+}

+ 97 - 17
packages/compiler/src/types/pug.d.ts → packages/compiler/src/typings/pug.d.ts

@@ -2,10 +2,7 @@ import type pug from "pug";
 import type PugLexer from "pug-lexer";
 import type PugLexer from "pug-lexer";
 import { LexTokenType } from "pug-lexer";
 import { LexTokenType } from "pug-lexer";
 
 
-/**
- * We use this to document the pug non-documented plugin API
- */
-declare module "pug" {
+export declare namespace Pug {
     export interface LexerPlugin {
     export interface LexerPlugin {
         /**
         /**
          * Checks if a given expression is valid
          * Checks if a given expression is valid
@@ -28,26 +25,41 @@ declare module "pug" {
     }
     }
 
 
     /**
     /**
-     * Represents a pug block
+     * Represents a pug AST.
      */
      */
     export interface PugAST {
     export interface PugAST {
-        type: "Block",
-        nodes: PugNode[]
+        type: "Block";
+        nodes: PugNodes[];
+    }
+
+    /**
+     * Represents a pug block.
+     */
+    export interface PugBlock {
+        type: "Block";
+        nodes: PugNodes[];
+        line?: number;
+        filename?: string;
+    }
+
+    /**
+     * Represents a single pug node attribute.
+     */
+    export interface PugNodeAttribute {
+        name: string,
+        val: string,
+        mustEscape: boolean
     }
     }
 
 
     /**
     /**
-     * Represents a generic pug node
+     * Represents a generic pug node.
      */
      */
     export interface PugNode extends Record<string, any> {
     export interface PugNode extends Record<string, any> {
-        type: string,
-        start?: number,
-        end?: number,
-        block?: PugAST,
-        attrs?: {
-            name: string,
-            val: string,
-            mustEscape: boolean
-        }[]
+        type?: string;
+        attributeBlocks?: [];
+        line: number;
+        column: number;
+        block?: PugAST;
     }
     }
 
 
     /**
     /**
@@ -160,4 +172,72 @@ declare module "pug" {
          */
          */
         plugins?: PugPlugin[]
         plugins?: PugPlugin[]
     }
     }
+
+    export declare namespace Nodes {
+        export declare interface TagNode extends PugNode {
+            type: "Tag";
+            name: string;
+            selfClosing: boolean;
+            attrs: PugNodeAttribute[];
+            isInline: boolean;
+        }
+
+        export declare interface ConditionalNode extends PugNode {
+            type: "Conditional";
+            test: string;
+            consequent: PugBlock;
+            alternate: PugBlock;
+        }
+
+        export declare interface EachNode extends PugNode {
+            type: "Each";
+            val: string;
+            obj: string;
+            index: string;
+        }
+
+        export declare interface MixinNode extends PugNode {
+            type: "Mixin";
+        }
+    }
+}
+
+/**
+ * We use this to document the pug non-documented plugin API
+ */
+declare module "pug" {
+    export interface LexerPlugin extends Pug.LexerPlugin {
+
+    }
+
+    export interface Options extends Pug.Options {
+
+    }
+
+    export interface PugPlugin extends Pug.PugPlugin {
+
+    }
+
+    export interface PugNode extends Pug.PugNode {
+
+    }
+
+    export interface  PugNodeAttribute extends Pug.PugNodeAttribute {
+
+    }
+
+    export interface PugAST extends Pug.PugAST {
+
+    }
+
+    export interface PugToken extends Pug.PugToken {
+
+    }
+
+    export type PugNodes = (
+        Pug.Nodes.TagNode |
+        Pug.Nodes.ConditionalNode |
+        Pug.Nodes.EachNode |
+        Pug.Nodes.MixinNode
+    );
 }
 }

+ 8 - 0
packages/compiler/src/util/NodeUtil.ts

@@ -0,0 +1,8 @@
+import { appendFileSync, writeFileSync } from "fs";
+import { inspect } from "util";
+import { NodeModel } from "../model/core/NodeModel";
+
+export function InspectNode(node: NodeModel) {
+    const inspected = inspect(node.toPugNode(), false, 99999, false);
+    appendFileSync(process.cwd() + "/.test.js", inspected);
+}

+ 1 - 1
packages/compiler/tsconfig.json

@@ -3,7 +3,7 @@
     "compilerOptions": {
     "compilerOptions": {
         "target": "esnext",
         "target": "esnext",
         "outDir": "./out",
         "outDir": "./out",
-        "declarationDir": "./types",
+        "declarationDir": "./types"
     },
     },
     "include": ["./src/**/*.ts"],
     "include": ["./src/**/*.ts"],
     "exclude": ["node_modules"],
     "exclude": ["node_modules"],

+ 19 - 16
packages/renderer/src/core/Component.ts

@@ -234,7 +234,7 @@ export class PupperComponent {
                 };
                 };
             }
             }
         }
         }
-        
+
         // Iterate over all references
         // Iterate over all references
         for(let ref of refs) {
         for(let ref of refs) {
             // Save it
             // Save it
@@ -246,9 +246,6 @@ export class PupperComponent {
 
 
         const container = renderContainer.content.children[0];
         const container = renderContainer.content.children[0];
 
 
-        this.$identifier = "pup_" + String(Date.now());
-        container.setAttribute("x-data", this.$identifier);
-
         return container;
         return container;
     }
     }
 
 
@@ -258,7 +255,22 @@ export class PupperComponent {
      * @returns 
      * @returns 
      */
      */
     public mount(target: HTMLElement | Slot) {
     public mount(target: HTMLElement | Slot) {
+        this.$identifier = "p_" + String((Math.random() + 1).toString(36).substring(2));
+
         const rendered = this.render();
         const rendered = this.render();
+        rendered.setAttribute("x-data", this.$identifier);
+
+        // Initialize the data
+        Alpine.data(this.$identifier, () => {
+            return {
+                ...this.$data,
+                init() {
+                    if (this.component?.mounted) {
+                        this.component.mounted.call(this);
+                    }
+                }
+            };
+        });
 
 
         if (!(target instanceof HTMLElement)) {
         if (!(target instanceof HTMLElement)) {
             target.container.replaceWith(rendered);
             target.container.replaceWith(rendered);
@@ -267,23 +279,14 @@ export class PupperComponent {
             target.appendChild(rendered);
             target.appendChild(rendered);
         }
         }
 
 
-        // Initialize the data
-        Alpine.data(this.$identifier, () => {
-            return this.$data;
-        });
-
         // Initialize Alpine for it
         // Initialize Alpine for it
-        Alpine.start();
+        Alpine.initTree(rendered);
 
 
         // Remove the identifier
         // Remove the identifier
-        rendered.removeAttribute("x-data");
+        //rendered.removeAttribute("x-data");
 
 
         // Save a reference to the internal Alpine data proxy
         // Save a reference to the internal Alpine data proxy
         // @ts-ignore
         // @ts-ignore
-        this.$data = Alpine.$data(rendered);
-
-        if (this.component?.mounted) {
-            this.component.mounted.call(this);
-        }        
+        this.$data = rendered._x_dataStack[0];      
     }
     }
 }
 }

+ 1 - 0
packages/vscode-extension/pupper-js/.gitignore

@@ -0,0 +1 @@
+*.vsix

+ 1 - 65
packages/vscode-extension/pupper-js/README.md

@@ -1,65 +1 @@
-# pupper-js README
-
-This is the README for your extension "pupper-js". After writing up a brief description, we recommend including the following sections.
-
-## Features
-
-Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file.
-
-For example if there is an image subfolder under your extension project workspace:
-
-\!\[feature X\]\(images/feature-x.png\)
-
-> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow.
-
-## Requirements
-
-If you have any requirements or dependencies, add a section describing those and how to install and configure them.
-
-## Extension Settings
-
-Include if your extension adds any VS Code settings through the `contributes.configuration` extension point.
-
-For example:
-
-This extension contributes the following settings:
-
-* `myExtension.enable`: enable/disable this extension
-* `myExtension.thing`: set to `blah` to do something
-
-## Known Issues
-
-Calling out known issues can help limit users opening duplicate issues against your extension.
-
-## Release Notes
-
-Users appreciate release notes as you update your extension.
-
-### 1.0.0
-
-Initial release of ...
-
-### 1.0.1
-
-Fixed issue #.
-
-### 1.1.0
-
-Added features X, Y, and Z.
-
------------------------------------------------------------------------------------------------------------
-
-## Working with Markdown
-
-**Note:** You can author your README using Visual Studio Code.  Here are some useful editor keyboard shortcuts:
-
-* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux)
-* Toggle preview (`Shift+CMD+V` on macOS or `Shift+Ctrl+V` on Windows and Linux)
-* Press `Ctrl+Space` (Windows, Linux) or `Cmd+Space` (macOS) to see a list of Markdown snippets
-
-### For more information
-
-* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown)
-* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/)
-
-**Enjoy!**
+# Pupper.js Syntax Highlighting

+ 28 - 15
packages/vscode-extension/pupper-js/package.json

@@ -9,21 +9,34 @@
     "categories": [
     "categories": [
         "Programming Languages"
         "Programming Languages"
     ],
     ],
+    "publisher": "Matheus Giovani",
     "contributes": {
     "contributes": {
-        "languages": [{
-            "id": "pupper",
-            "aliases": ["Pupper.js", "pupper"],
-            "extensions": [".pupper"],
-            "configuration": "./language-configuration.json",
-            "icon": {
-                "dark": "./assets/icon.png",
-                "light": "./assets/icon.png"
+        "languages": [
+            {
+                "id": "pupper",
+                "aliases": [
+                    "Pupper.js",
+                    "pupper"
+                ],
+                "extensions": [
+                    ".pupper"
+                ],
+                "configuration": "./language-configuration.json",
+                "icon": {
+                    "dark": "./assets/icon.png",
+                    "light": "./assets/icon.png"
+                }
             }
             }
-        }],
-        "grammars": [{
-            "language": "pupper",
-            "scopeName": "source.pupper",
-            "path": "./syntaxes/pupper.tmLanguage.json"
-        }]
+        ],
+        "grammars": [
+            {
+                "language": "pupper",
+                "scopeName": "source.pupper",
+                "path": "./syntaxes/pupper.tmLanguage.json"
+            }
+        ]
+    },
+    "devDependencies": {
+        "vsce": "^2.9.1"
     }
     }
-}
+}

+ 7 - 2
test/index.js

@@ -4,6 +4,9 @@ import { defineComponent } from "@pupperjs/renderer";
 import ImportedComponent from "./templates/ImportedComponent.pupper";
 import ImportedComponent from "./templates/ImportedComponent.pupper";
 import ExportedComponent from "./templates/ExportedComponent.pupper";
 import ExportedComponent from "./templates/ExportedComponent.pupper";
 
 
+console.log("Imported", ImportedComponent);
+console.log("Exported", ExportedComponent);
+
 const pupper = defineComponent({
 const pupper = defineComponent({
     render: Template,
     render: Template,
     methods: {
     methods: {
@@ -22,13 +25,15 @@ const pupper = defineComponent({
                 id: 1,
                 id: 1,
                 title: "A cutie pup",
                 title: "A cutie pup",
                 description: "Look at this cutie",
                 description: "Look at this cutie",
-                thumbnail: "https://placedog.net/800"
+                thumbnail: "https://placedog.net/800",
+                properties: ["beautiful", "doge"]
             },
             },
             {
             {
                 id: 2,
                 id: 2,
                 title: "Another cute pup",
                 title: "Another cute pup",
                 description: "Isn't it a cute doggo?!",
                 description: "Isn't it a cute doggo?!",
-                thumbnail: "https://placedog.net/400"
+                thumbnail: "https://placedog.net/400",
+                properties: ["wow", "much woof"]
             }
             }
         ]
         ]
     }
     }

+ 1 - 1
test/templates/ExportedComponent.pupper

@@ -7,7 +7,7 @@ component(export)
 
 
         hr
         hr
 
 
-    script(lang="ts", type="text/javascript").
+    script(lang="ts", type="text/javascript")
         import Pupper from "@pupperjs/renderer";
         import Pupper from "@pupperjs/renderer";
 
 
         export default Pupper.defineComponent({
         export default Pupper.defineComponent({

+ 2 - 2
test/templates/ImportedComponent.pupper

@@ -1,4 +1,4 @@
-import(ExportedComponent from "./ExportedComponent.pupper")
+import ExportedComponent(from="./ExportedComponent.pupper")
 
 
 component(export)
 component(export)
     template
     template
@@ -15,7 +15,7 @@ component(export)
 
 
         export default Pupper.defineComponent({
         export default Pupper.defineComponent({
             mounted() {
             mounted() {
-                console.log("Mounted, rendering", ExportedComponent);
+                console.log("Rendering the exported component into slot \"slot\"");
                 ExportedComponent.mount(this.$slots.slot);
                 ExportedComponent.mount(this.$slots.slot);
             }
             }
         });
         });

+ 14 - 10
test/templates/template.pupper

@@ -15,24 +15,28 @@ link(href="https://getbootstrap.com/docs/4.0/examples/cover/cover.css", rel="sty
             p.lead={{ page.lead }}
             p.lead={{ page.lead }}
 
 
             .row.mt-5.justify-content-around.align-items-center
             .row.mt-5.justify-content-around.align-items-center
-                if !puppies
+                if puppies === undefined || puppies.length === 0
                     |Oh noe! No puppies to show :(
                     |Oh noe! No puppies to show :(
                 else
                 else
                     //- Render the puppies and share the onClickPuppy method with it
                     //- Render the puppies and share the onClickPuppy method with it
                     each puppy in puppies
                     each puppy in puppies
                         .col-5.mb-5
                         .col-5.mb-5
-                                .puppy.card.px-0(:data-id="puppy.id", @click="onClickPuppy(puppy)").text-dark
-                                    img.card-img-top(:src="puppy.thumbnail", crossorigin="auto")
+                            .puppy.card.px-0(:data-id="puppy.id", @click="onClickPuppy(puppy)").text-dark
+                                img.card-img-top(:src="puppy.thumbnail", crossorigin="auto")
 
 
-                                    .card-header
-                                        h5.card-title={{ puppy.title }}
-                                        small.text-muted|Served by pupper.js
+                                .card-header
+                                    h5.card-title={{ puppy.title }}
+                                    small.text-muted|Served by pupper.js
 
 
-                                    .card-body
-                                        ={- puppy.description -}
+                                .card-body
+                                    ={- puppy.description -}
 
 
-                                        if puppy.shibe
-                                            p.text-warning|shibe!!!
+                                    if puppy.shibe === true
+                                        p.text-warning|shibe!!!
+
+                                    if puppy.properties
+                                        each property in puppy.properties
+                                            span.badge.badge-info={{property}}
 
 
             div 
             div 
                 |Testing slots: 
                 |Testing slots: 

+ 1 - 2
tsconfig.json

@@ -12,8 +12,7 @@
         "emitDecoratorMetadata": true,
         "emitDecoratorMetadata": true,
         "moduleResolution": "node",
         "moduleResolution": "node",
         "module": "commonjs",
         "module": "commonjs",
-        "skipLibCheck": true,
-        "typeRoots": ["./packages/common/types"]
+        "skipLibCheck": true
     },
     },
     "include": ["./**/*.ts"],
     "include": ["./**/*.ts"],
     "exclude": ["node_modules"],
     "exclude": ["node_modules"],