1
0
Matheus Giovani 2 жил өмнө
parent
commit
5b38900041

+ 2 - 0
packages/compiler/package.json

@@ -17,6 +17,8 @@
     "pug-error": "^2.0.0",
     "pug-linker": "^4.0.0",
     "pug-parser": "^6.0.0",
+    "snabbdom": "^3.5.0",
+    "snabbdom-virtualize": "^0.7.0",
     "ts-morph": "^15.1.0"
   },
   "types": "./types/",

+ 36 - 7
packages/compiler/src/core/Compiler.ts

@@ -8,6 +8,7 @@ import link from "pug-linker";
 import codeGen from "pug-code-gen";
 import { Console } from "console";
 import { createWriteStream } from "fs";
+import { PugToVirtualDOM } from "./compiler/HTMLToVirtualDOM";
 
 export enum CompilationType {
     TEMPLATE,
@@ -31,6 +32,11 @@ export interface ICompilerOptions {
      * @internal Not meant to be used externally.
      */
     pug?: pug.Options;
+
+    /**
+     * If wants to use Virtual DOM or normal DOM inputs.
+     */
+    useVirtualDom?: boolean;
 }
 
 export class PupperCompiler {
@@ -39,10 +45,19 @@ export class PupperCompiler {
      */
     public contents: string;
 
+    /**
+     * The compiler plugin instance.
+     */
     public plugin = new Plugin(this);
 
+    /**
+     * The type of the compilation that this compiler is currently handling
+     */
     public compilationType: CompilationType;
 
+    /**
+     * An exclusive console instance for debugging purposes.
+     */
     public debugger = new Console(createWriteStream(process.cwd() + "/.logs/log.log"), createWriteStream(process.cwd() + "/.logs/error.log"));
 
     constructor(
@@ -107,6 +122,11 @@ export class PupperCompiler {
         return content.replace(/\r\n/g, "\n");
     }
 
+    /**
+     * Lexes and parses a template string into an AST.
+     * @param template The template to be parsed.
+     * @returns 
+     */
     protected lexAndParseString(template: string) {
         let carrier: any;
 
@@ -139,11 +159,19 @@ export class PupperCompiler {
         return carrier as PugAST;
     }
 
+    /**
+     * Generates a JavaScript function that renders the 
+     * @param ast The AST to be converted.
+     * @returns 
+     */
     protected generateJavaScript(ast: pug.PugAST): string {
+        // Allow hooking
         ast = this.plugin.preCodeGen(ast);
 
+        // Generate the code
         let code = codeGen(ast, this.makePugOptions());
 
+        // Allow hooking
         code = this.plugin.postCodeGen(code);
 
         return code;
@@ -156,7 +184,6 @@ export class PupperCompiler {
      */
     public compileComponent(template: string): string {
         this.contents = this.normalizeLines(template);
-
         this.compilationType = CompilationType.COMPONENT;
 
         const ast = this.lexAndParseString(this.contents);
@@ -171,17 +198,19 @@ export class PupperCompiler {
      * @returns 
      */
     public compileTemplate(template: string): string {
-        const pugOptions = this.makePugOptions();
         this.contents = this.normalizeLines(template);
-
         this.compilationType = CompilationType.TEMPLATE;
 
-        this.plugin.prepareHooks();
+        const ast = this.lexAndParseString(this.contents);
+        let rendered: string;
 
-        const fn = pug.compile(this.contents, pugOptions);
-        const rendered = fn();
+        if (this.options.useVirtualDom !== false) {
+            rendered = /*js*/`function $h({ h }) {\nreturn ${PugToVirtualDOM.virtualize(this, ast)};\n}`;
+        } else {
+            rendered = eval("return (" + this.generateJavaScript(ast) + "())");
+        }
 
-        return rendered;///*js*/`function $h(h) { return ${htmlToHs({ syntax: "h" })(rendered)}; }`;
+        return rendered;
     }
 
     /**

+ 152 - 0
packages/compiler/src/core/compiler/HTMLToVirtualDOM.ts

@@ -0,0 +1,152 @@
+import { Pug } from "../../typings/pug";
+import { PupperCompiler } from "../Compiler";
+import { PugAST, PugNodes } from "../Plugin";
+
+export class PugToVirtualDOM {
+    public static virtualize(compiler: PupperCompiler, ast: PugAST) {
+        return new PugToVirtualDOM(compiler).virtualize(ast);
+    }
+
+    protected identation: string;
+    protected identLevel: number = 0;
+
+    constructor(
+        protected compiler: PupperCompiler
+    ) {
+        this.identation = compiler.plugin.detectIdentation();
+    }
+
+    /**
+     * Idents a string with the current identation level.
+     * @param string The string to be idented.
+     * @returns 
+     */
+    protected ident(string: string = "") {
+        return this.identation.repeat(this.identLevel) + string;
+    }
+
+    /**
+     * Virtualizes a single node.
+     * @param node The node to be virtualized.
+     * @returns 
+     */
+    public virtualizeNode(node: Partial<PugNodes>) {
+        let content = this.ident("h(");
+
+        switch(node.type) {
+            case "Tag":
+                content += `"${node.name}"`;
+
+                // If the node has attributes
+                if (node.attrs.length) {
+                    content += `, {\n`;
+                    this.identLevel++;
+
+                        content += this.ident(`attrs: {\n`);
+                        this.identLevel++;
+
+                            content += node.attrs
+                            .reduce((arr: Pug.Nodes.TagNode["attrs"], attr) => {
+                                const existing = arr.find((at) => at.name === attr.name);
+
+                                if (typeof attr.val === "string") {
+                                    if (attr.val.match(/^['"]/)) {
+                                        attr.val = attr.val.substring(1, attr.val.length - 1);
+                                    }
+                                }
+
+                                if (existing) {
+                                    existing.val += " " + attr.val;
+                                } else {
+                                    arr.push(attr);
+                                }
+
+                                return arr;
+                            }, [])
+                            .map((attr) => {
+                                return this.ident(
+                                    `"${attr.name}": ${typeof attr.val === "string" ? '"' + attr.val.trim() + '"' : attr.val}`
+                                )
+                            })
+                            .join(",\n")
+
+                        this.identLevel--;
+                        content += "\n";
+                        content += this.ident(`}\n`);
+
+                    this.identLevel--;
+                    content += this.ident(`}`);
+                }
+
+                if (node.block?.nodes?.length) {
+                    this.identLevel++;
+
+                    content += `, [\n`;
+                        content += node.block.nodes.map((node) => {
+                            return this.virtualizeNode(node);
+                        })
+                        .filter((res) => !!res)
+                        .join(",\n");
+
+                        this.identLevel--;
+
+                    content += "\n" + this.ident(`]`);
+                }
+            break;
+
+            case "Text":
+                // If it's empty
+                if (node.val.trim().length === 0) {
+                    // Ignore it
+                    return null;
+                }
+
+                return this.ident("\"" + node.val.replace(/"/g, '\\"').trim() + "\"");
+
+            case "Code":
+                if (node.val.trim().length === 0) {
+                    // Ignore it
+                    return null;
+                }
+
+                if (node.val.match(/^["']/)) {
+                    node.val = node.val.substring(1, node.val.length - 1);
+                }
+
+                return `h("span", { attrs: { "x-${node.mustEscape ? "text": "html"}": "${node.val.replace(/"/g, '\"')}" } })`;
+
+            // Ignore comments
+            case "Comment":
+                return null;
+
+            default:
+                this.compiler.debugger.log("unknown node type", node);
+        }
+
+        return content + ")";
+    }
+
+    /**
+     * Converts a string into a virtual DOM string.
+     * @param ast The AST to be virtualized.
+     * @returns
+     */
+    public virtualize(ast: PugAST) {
+        let final = "";
+
+        if (ast.nodes.length > 1) {
+            final = this.virtualizeNode({
+                type: "Tag",
+                name: "div",
+                block: {
+                    type: "Block",
+                    nodes: ast.nodes
+                }
+            });
+        } else {
+            final = this.virtualizeNode(ast.nodes[0]);
+        }
+
+        return final;
+    }
+}

+ 1 - 1
packages/compiler/src/core/plugin/hooks/ListenerHook.ts

@@ -27,7 +27,7 @@ export class ListenerHook extends Hook {
 
                 // Set them                        
                 for(let event of eventNames) {
-                    node.setAttribute("x-bind:" + event, "$$p_" + listenerName);
+                    node.setAttribute("x-on:" + event, "$$p_" + listenerName);
                 }
             }
         });

+ 15 - 0
packages/compiler/src/core/plugin/hooks/PropertyHook.ts

@@ -1,3 +1,4 @@
+import { CompilerNode } from "../../../model/core/nodes/CompilerNode";
 import { PugToken } from "../../Plugin";
 import { Hook } from "../Hook";
 
@@ -12,7 +13,16 @@ export class PropertyHook extends Hook {
     }
 
     public lex(tokens: PugToken[]) {
+        let insideAttribute = false;
+
         return tokens.map((token) => {
+            if (token.type === "start-attributes") {
+                insideAttribute = true;
+            } else
+            if (token.type === "end-attributes") {
+                insideAttribute = false;
+            }
+            
             // We want only attribute and code tokens
             if (token.type !== "attribute" && token.type !== "code") {
                 return token;
@@ -35,9 +45,14 @@ export class PropertyHook extends Hook {
                 } else {
                     const textOrHtml = fn === "escape" ? "text" : "html";
 
+                    token.type = "code";
                     token.val = /*html*/`"<span x-${textOrHtml}=\\"${value}\\"></span>"`;
                     token.mustEscape = false;
                 }
+
+                if (insideAttribute) {
+                    token.type = "attribute";
+                }
             }
 
             return token;

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

@@ -42,7 +42,7 @@ export class EachNode extends CompilerNode<Pug.Nodes.EachNode> {
 
         return CompilerNode.parseNodeIntoPugNode({
             type: "Tag",
-            name: "Template",
+            name: "template",
             attributes: {
                 "x-for": parsedConditional
             },

+ 10 - 2
packages/compiler/src/model/core/nodes/CompilerNode.ts

@@ -139,7 +139,7 @@ export class CompilerNode<TNode extends PugNodes = any> extends NodeModel<Compil
      * @param type The children node type.
      * @returns 
      */
-    public findFirstChildByType(type: string) {
+    public findFirstChildByType(type: PugNodes["type"]) {
         return this.children.find((child) => child.isType(type));
     }
 
@@ -183,10 +183,18 @@ export class CompilerNode<TNode extends PugNodes = any> extends NodeModel<Compil
      * @param type The type to be checked.
      * @returns 
      */
-    public isType(type: string) {
+    public isType(type: PugNodes["type"]) {
         return this.pugNode.type === type;
     }
 
+    /**
+     * Sets the compiler node type.
+     * @param type The new node type
+     */
+    public setType(type: PugNodes["type"]) {
+        this.pugNode.type = type;
+    }
+
     /**
      * Checks if the node has the given tag name.
      * @param name The name to be checked.

+ 23 - 1
packages/compiler/src/typings/pug.d.ts

@@ -198,6 +198,25 @@ export declare namespace Pug {
         export declare interface MixinNode extends PugNode {
             type: "Mixin";
         }
+
+        export declare interface TextNode extends PugNode {
+            type: "Text";
+            val: string;
+        }
+
+        export declare interface CodeNode extends PugNode {
+            type: "Code";
+            val: string;
+            isInline: boolean;
+            mustEscape: boolean;
+            buffer: boolean;
+        }
+
+        export declare interface CommentNode extends PugNode {
+            type: "Comment";
+            val: string;
+            buffer: boolean;
+        }
     }
 }
 
@@ -237,6 +256,9 @@ declare module "pug" {
         Pug.Nodes.TagNode |
         Pug.Nodes.ConditionalNode |
         Pug.Nodes.EachNode |
-        Pug.Nodes.MixinNode
+        Pug.Nodes.MixinNode |
+        Pug.Nodes.TextNode | 
+        Pug.Nodes.CommentNode |
+        Pug.Nodes.CodeNode
     );
 }

+ 3 - 2
packages/renderer/package.json

@@ -13,13 +13,14 @@
   "dependencies": {
     "alpinejs": "^3.10.2",
     "morphdom": "^2.6.1",
-    "pug": "^3.0.2"
+    "pug": "^3.0.2",
+    "snabbdom": "^3.5.0"
   },
   "types": "./types/",
   "devDependencies": {
     "@types/alpinejs": "^3.7.0",
     "@types/node": "^16.7.6",
-    "debug": "^4.3.2",
+    "debug": "^4.3.4",
     "npm-run-all": "^4.1.5",
     "tsc": "^2.0.3",
     "typescript": "^4.4.2",

+ 74 - 56
packages/renderer/src/core/Component.ts

@@ -1,5 +1,7 @@
 import Alpine from "alpinejs";
+import { VNode } from "snabbdom";
 import { DOMParser } from "./DomParser";
+import { VDomRenderer } from "./VDomRenderer";
 
 /**
  * Represents a slot.
@@ -19,7 +21,7 @@ interface Slot {
 /**
  * Represents a component's data.
  */
-interface IComponent<
+export interface IComponent<
     TData extends Record<string, any>,
     TMethods extends Record<string, CallableFunction>
 > {
@@ -30,7 +32,7 @@ interface IComponent<
     /**
      * The function that renders the template HTML.
      */
-    render?: (...args: any[]) => string;
+    render?: (...args: any[]) => string | VNode;
 
     /**
      * Any data to be passed to the template.
@@ -73,7 +75,7 @@ export class PupperComponent {
     /**
      * The state related to this component.
      */
-    private $state: Record<string, any> = {};
+    public $state: Record<string, any> = {};
 
     /**
      * Any slots references.
@@ -92,32 +94,42 @@ export class PupperComponent {
 
     protected parser: DOMParser;
 
+    /**
+     * If it's the first time that the component is being rendered.
+     */
+    public firstRender = true;
+
+    /**
+     * The virtual DOM renderer instance.
+     */
+    protected renderer = new VDomRenderer(this);
+
     constructor(
         /**
          * The component properties.
          */
-        protected component: IComponent<any, any>
+        public $component: IComponent<any, any>
     ) {
         // If has methods
-        if (component?.methods) {
-            for(let method in component.methods) {
-                this.$state[method] = component.methods[method];
+        if ($component?.methods) {
+            for(let method in $component.methods) {
+                this.$state[method] = $component.methods[method];
             }
         }
 
         // If has data
-        if (component?.data) {
-            if (typeof component.data === "function") {
-                component.data = component.data();
+        if ($component?.data) {
+            if (typeof $component.data === "function") {
+                $component.data = $component.data();
             }
 
-            for(let key in component.data) {
+            for(let key in $component.data) {
                 // If it's already registered
                 if (key in this.$state) {
                     throw new Error("There's already a property named " + key + " registered in the component. Property names should be unique.");
                 }
 
-                this.$state[key] = component.data[key];
+                this.$state[key] = $component.data[key];
             }
         }
 
@@ -139,8 +151,8 @@ export class PupperComponent {
             Object.defineProperty(this, key, attributes);
         }
 
-        if (this.component?.created) {
-            this.component.created.call(this);
+        if (this.$component?.created) {
+            this.$component.created.call(this);
         }
     }
 
@@ -200,54 +212,59 @@ export class PupperComponent {
     /**
      * Renders the template function into a div tag.
      */
-    public render() {
-        const tree = this.component.render();
-        let renderContainer = this.renderStringToTemplate(tree);
-
-        // Find all slots, templates and references
-        const slots = Array.from(renderContainer.content.querySelectorAll("slot"));
-        const templates = Array.from(renderContainer.content.querySelectorAll("template"));
-        const refs = Array.from(renderContainer.content.querySelectorAll("[ref]"));
-
-        // Iterate over all slots
-        for(let slot of slots) {
-            // Replace it with a comment tag
-            const comment = this.replaceWithCommentPlaceholder(slot);
-
-            // If it's a named slot
-            if (slot.hasAttribute("name")) {
-                // Save it
-                this.$slots[slot.getAttribute("name")] = {
-                    container: comment,
-                    fallbackNodes: [...comment.childNodes]
-                };
+    public async render() {
+        let renderContainer: HTMLElement;
+
+        if (this.firstRender) {
+            this.firstRender = false;
+
+            renderContainer = await this.renderer.renderFirst();
+
+            // Find all slots, templates and references
+            const slots = Array.from(renderContainer.querySelectorAll("slot"));
+            const templates = Array.from(renderContainer.querySelectorAll("template"));
+            const refs = Array.from(renderContainer.querySelectorAll("[ref]"));
+
+            // Iterate over all slots
+            for(let slot of slots) {
+                // Replace it with a comment tag
+                const comment = this.replaceWithCommentPlaceholder(slot);
+
+                // If it's a named slot
+                if (slot.hasAttribute("name")) {
+                    // Save it
+                    this.$slots[slot.getAttribute("name")] = {
+                        container: comment,
+                        fallbackNodes: [...comment.childNodes]
+                    };
+                }
             }
-        }
-
-        // Iterate over all templates
-        for(let childrenTemplate of templates) {
-            // If it's a named template
-            if (childrenTemplate.hasAttribute("name")) {
-                // Remove it from the DOM
-                childrenTemplate.parentElement.removeChild(childrenTemplate);
 
-                // Save it
-                this.$templates[childrenTemplate.getAttribute("name")] = () => {
-                    return [...childrenTemplate.content.children].map((node) => node.innerHTML);
-                };
+            // Iterate over all templates
+            for(let childrenTemplate of templates) {
+                // If it's a named template
+                if (childrenTemplate.hasAttribute("name")) {
+                    // Remove it from the DOM
+                    childrenTemplate.parentElement.removeChild(childrenTemplate);
+
+                    // Save it
+                    this.$templates[childrenTemplate.getAttribute("name")] = () => {
+                        return [...childrenTemplate.content.children].map((node) => node.innerHTML);
+                    };
+                }
             }
-        }
 
-        // Iterate over all references
-        for(let ref of refs) {
-            // Save it
-            this.$refs[ref.getAttribute("ref")] = ref as HTMLElement;
+            // Iterate over all references
+            for(let ref of refs) {
+                // Save it
+                this.$refs[ref.getAttribute("ref")] = ref as HTMLElement;
 
-            // Remove the attribute
-            ref.removeAttribute("ref");
+                // Remove the attribute
+                ref.removeAttribute("ref");
+            }
         }
 
-        return renderContainer.content.firstChild as HTMLElement;
+        return renderContainer;
     }
 
     /**
@@ -258,7 +275,8 @@ export class PupperComponent {
     public async mount(target: HTMLElement | Slot) {
         this.$identifier = "p_" + String((Math.random() + 1).toString(36).substring(2));
 
-        const rendered = this.render();
+        const rendered = await this.render();
+        
         rendered.setAttribute("x-data", this.$identifier);
 
         // Initialize the data

+ 1 - 1
packages/renderer/src/core/DomParser.ts

@@ -66,7 +66,7 @@ export class DOMParser {
      * @param node The node to be initialized.
      */
     protected initNode(node: HTMLElement) {
-        Alpine.initTree(node);
+        //Alpine.initTree(node);
     }
 
     /**

+ 386 - 0
packages/renderer/src/core/VDomRenderer.ts

@@ -0,0 +1,386 @@
+import { PupperComponent } from "./Component";
+import {
+    h,
+    propsModule,
+    attributesModule,
+    styleModule,
+    eventListenersModule,
+    init,
+    VNode
+} from "snabbdom";
+
+import { ISafeAsyncFunction, SafeAsyncFunction } from "./evaluator/SafeAsyncFunction";
+import { IsNumeric, IsObject } from "../util/ObjectUtils";
+
+const debug = require("debug")("pupper:vdom");
+
+/**
+ * Most of the evaluation functions were taken from alpine.js
+ * Thanks, alpine.js!
+ */
+export class VDomRenderer {
+    patch: ReturnType<typeof init>;
+
+    protected evaluatorMemo: Record<string, ISafeAsyncFunction> = {};
+
+    /**
+     * The stack of states that formulates the context for rendering elements.
+     */
+    protected stateStack: Record<string, any>[] = [];
+
+    constructor(
+        protected component: PupperComponent
+    ) {
+        this.patch = init([
+            propsModule,
+            attributesModule,
+            styleModule,
+            eventListenersModule
+        ]);
+
+        this.stateStack.push(component.$state);
+    }
+
+    /**
+     * Evaluates an expression string into a function.
+     * @see https://github.com/alpinejs/alpine/blob/71ee8361207628b1faa14e97533373e9ebee468a/packages/alpinejs/src/evaluator.js#L61
+     * @param expression The expression to be evaluated.
+     * @returns 
+     */
+    protected evaluateString(expression: string) {
+        // If this expression has already been evaluated
+        if (this.evaluatorMemo[expression]) {
+            return this.evaluatorMemo[expression];
+        }
+    
+        // Some expressions that are useful in Alpine are not valid as the right side of an expression.
+        // Here we'll detect if the expression isn't valid for an assignement and wrap it in a self-
+        // calling function so that we don't throw an error AND a "return" statement can b e used.
+        let rightSideSafeExpression = 0
+            // Support expressions starting with "if" statements like: "if (...) doSomething()"
+            || /^[\n\s]*if.*\(.*\)/.test(expression)
+            // Support expressions starting with "let/const" like: "let foo = 'bar'"
+            || /^(let|const)\s/.test(expression)
+                ? `(() => { ${expression} })()`
+                : expression
+
+        let func: ISafeAsyncFunction;
+
+        try {
+            func = SafeAsyncFunction(rightSideSafeExpression);
+        } catch (err) {
+            console.warn("pupper.js warning: invalid expression", rightSideSafeExpression);
+            throw err;
+        }
+
+        this.evaluatorMemo[expression] = func;
+
+        return func;
+    }
+
+    protected generateState() {
+        return this.stateStack.reduce((carrier, curr) => {
+            for(let key in curr) {
+                carrier[key] = curr[key];
+            }
+
+            return carrier;
+        }, {});
+    }
+
+    /**
+     * Evaluates an expression.
+     * @param expression The expression to be evaluated.
+     * @returns 
+     */
+    protected async evaluate<TExpressionResult = any>(expression: string | number | boolean | CallableFunction) {
+        const state = this.generateState();
+
+        if (typeof expression === "function") {
+            return await expression(this.component.$state);
+        }
+
+        const func = this.evaluateString(String(expression));
+        func.result = undefined;
+        func.finished = false;
+
+        try {
+            return await func(func, state) as TExpressionResult;
+        } catch(e) {
+            console.warn("pupper.js warning: failed to evaluate " + expression);
+            console.warn(e);
+        }
+
+        return undefined;
+    }
+
+    protected async maybeEvaluate(expression: string | number | boolean | CallableFunction) {
+        try {
+            return await this.evaluate(expression);
+        } catch(e) {
+
+        }
+
+        return expression;
+    }
+
+    /**
+     * Parses a "for" expression
+     * @note This was taken from VueJS 2.* core. Thanks Vue!
+     * @param expression The expression to be parsed.
+     * @returns 
+     */
+    protected parseForExpression(expression: string | number | boolean | CallableFunction) {
+        let forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
+        let stripParensRE = /^\s*\(|\)\s*$/g
+        let forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
+        let inMatch = String(expression).match(forAliasRE)
+
+        if (!inMatch) return
+
+        let res: {
+            items?: string;
+            index?: string;
+            item?: string;
+            collection?: string;
+        } = {};
+
+        res.items = inMatch[2].trim()
+        let item = inMatch[1].replace(stripParensRE, "").trim();
+        let iteratorMatch = item.match(forIteratorRE)
+
+        if (iteratorMatch) {
+            res.item = item.replace(forIteratorRE, "").trim();
+            res.index = iteratorMatch[1].trim()
+
+            if (iteratorMatch[2]) {
+                res.collection = iteratorMatch[2].trim();
+            }
+        } else {
+            res.item = item;
+        }
+
+        return res;
+    }
+
+    /**
+     * Evaluates a conditional expression.
+     * @param node The node to be evaluated.
+     * @param exp The if expression to be evaluated.
+     * @returns 
+     */
+    protected async evaluateIf(node: VNode, exp: string | number | boolean | CallableFunction) {
+        const evaluated = await this.evaluate<boolean>(exp);
+        
+        debug("evaluated if \"%s\" as %s", exp, evaluated);
+
+        return evaluated ? await this.parseNode(node.children[0]) : null;
+    }
+
+    /**
+     * Evaluates a loop expression.
+     * @param node The node to be evaluated.
+     * @param exp The loop expression to be evaluated.
+     * @returns 
+     */
+    protected async evaluateLoop(node: VNode, exp: string | number | boolean | CallableFunction) {
+        const parsed = this.parseForExpression(exp);
+        let items: any[] = await this.evaluate(parsed.items);
+
+        debug("evaluated for \"%s\" as %O", exp, parsed);
+
+        // Support number literals, eg.: x-for="i in 100"
+        if (IsNumeric(items)) {
+            items = Array.from(Array(items).keys(), (i) => i + 1);
+        } else
+        // If it's an object
+        if (IsObject(items)) {
+            // Retrieve the entries from it
+            items = Object.entries(items);
+        } else
+        // If nothing is found, default to an empty array.
+        if (items === undefined) {
+            items = [];
+        }
+
+        // The final node that will receive the children nodes.
+        let finalNode: VNode = {
+            children: [],
+            sel: "div",
+            data: {},
+            elm: node.elm,
+            key: node.key,
+            text: null
+        };
+
+        // Iterate over all evaluated items
+        for(let item in items) {
+            // Push the current item to the state stack
+            this.stateStack.push({
+                [parsed.item]: items[item],
+                [parsed.index]: item,
+                [parsed.collection]: items
+            });
+
+            // Create the children from it
+            const cloned = typeof node.children[0] === "string" ? node.children[0] : JSON.parse(JSON.stringify(node.children[0]));
+            const parsedNode = await this.parseNode(cloned);
+
+            finalNode.children.push(parsedNode);
+
+            // Remove the current item from the state stack
+            this.stateStack.pop();
+        }
+
+        return finalNode;
+    }
+
+    protected async parseNode(node: VNode | string): Promise<VNode | string | null> {
+        // If it's null
+        if (!node) {
+            // Ignore it
+            return null;
+        }
+
+        // Ignore if it's a string
+        if (typeof node === "string") {
+            return node;
+        }
+
+        debug("evaluating %s %O", node.sel || "text", node);
+
+        // If it's a template tag
+        if (node.sel === "template") {
+            // If it's an "if"
+            if ("x-if" in node.data.attrs) {
+                // Evaluate and return the result of it
+                return await this.evaluateIf(node, node.data.attrs["x-if"]);
+            } else
+            // If it's a "for"
+            if ("x-for" in node.data.attrs) {
+                return await this.evaluateLoop(node, node.data.attrs["x-for"]);
+            } else {
+                console.warn("pupper.js has found an unknown template node", node);
+            }
+
+            // Prevent from going further
+            return node;
+        }
+
+        if (node.data !== undefined) {
+            if ("attrs" in node.data) {
+                // If has a "x-text" attribute
+                if ("x-text" in node.data.attrs) {
+                    node.children = node.children || [];
+
+                    // Append the text to the children
+                    node.children.push({
+                        children: undefined,
+                        sel: undefined,
+                        data: undefined,
+                        elm: undefined,
+                        key: undefined,
+                        text: await this.maybeEvaluate(node.data.attrs["x-text"])
+                    });
+
+                    delete node.data.attrs["x-text"];
+                } else
+                // If has a "x-html" attribute
+                if ("x-html" in node.data.attrs) {
+                    node.children = node.children || [];
+
+                    // Append the HTML to the children
+                    node.children.push({
+                        children: undefined,
+                        sel: undefined,
+                        data: undefined,
+                        elm: undefined,
+                        key: undefined,
+                        text: await this.evaluate(node.data.attrs["x-html"])
+                    });
+
+                    delete node.data.attrs["x-html"];
+                }
+
+                // Find events
+                const events = Object.keys(node.data.attrs).filter((prop) => prop.startsWith("x-on:"));
+
+                // Iterate over all events
+                for(let prop of events) {
+                    const event = prop.replace("x-on:", "");
+
+                    // Bind the event to it
+                    node.data.on = node.data.on || {};
+                    const lastStack = [...this.stateStack];
+
+                    const fn = node.data.attrs[prop];
+
+                    node.data.on[event] = async ($event) => {
+                        debug("handled %s event", event);
+
+                        const savedStack = this.stateStack;
+
+                        this.stateStack = lastStack;
+                        this.stateStack.push({ $event });
+
+                        await this.maybeEvaluate(fn);
+
+                        this.stateStack = savedStack;
+                    };
+
+                    debug("binding event \"%s\" to \"%s\"", event, node.data.attrs[prop]);
+
+                    // Remove the prop from the node
+                    delete node.data.attrs[prop];
+                }
+
+                // Find property bindings
+                const bindings = Object.keys(node.data.attrs).filter((prop) => prop.startsWith("x-bind:"));
+
+                // Iterate over all bindings
+                for(let prop of bindings) {
+                    const bindingProp = prop.replace("x-bind:", "");
+
+                    // Bind the property to it
+                    node.data.attrs[bindingProp] = await this.maybeEvaluate(
+                        node.data.attrs[prop]
+                    );
+
+                    debug("binding prop \"%s\" to \"%s\"", bindingProp, node.data.attrs[bindingProp]);
+
+                    // Remove the prop from the node
+                    delete node.data.attrs[prop];
+                }
+            }
+        }
+
+        // Parse children if needed
+        if (node.children) {
+            for(let i = 0; i < node.children.length; i++) {
+                node.children[i] = await this.parseNode(node.children[i]);
+            }
+
+            // Filtren null ones
+            node.children = node.children.filter((node) => !!node);
+        }
+
+        return node;
+    }
+
+    /**
+     * Renders the virtual dom for the first time.
+     * @returns 
+     */
+    public async renderFirst() {
+        debug("first render");
+
+        const vdom = this.component.$component.render({ h }) as VNode;
+
+        await this.parseNode(vdom);
+
+        const template = document.createElement("div");
+        this.patch(template, vdom);
+
+        return template;
+    }
+}

+ 28 - 0
packages/renderer/src/core/evaluator/SafeAsyncFunction.ts

@@ -0,0 +1,28 @@
+/**
+ * A constructor for async functions.
+ */
+export const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor as AsyncGeneratorFunction;
+
+/**
+ * Used to represent a safe async evaluator.
+ */
+export interface ISafeAsyncFunction extends AsyncGeneratorFunction {
+    result: any;
+    finished: boolean;
+}
+
+/**
+ * Generates a safe async function to be executed.
+ * @param rightSideSafeExpression The safe right side expression.
+ * @returns 
+ */
+export function SafeAsyncFunction(rightSideSafeExpression: string) {
+    return new AsyncFunction(["__self", "scope"], /*js*/`
+        with (scope) {
+            __self.result = ${rightSideSafeExpression}
+        };
+        
+        __self.finished = true;
+        return __self.result;
+    `) as any as ISafeAsyncFunction;
+}

+ 2 - 2
packages/renderer/src/types/pupper.d.ts

@@ -1,8 +1,8 @@
-import { PupperComponent } from "../core/Component";
+import { PupperComponent, IComponent } from "../core/Component";
 
 /**
  * Used to represent whats is a pupper module
  */
 declare module "*.pupper" {
-    export default function(data: object): PupperComponent;
+    export default function(data: IComponent): PupperComponent;
 }

+ 17 - 0
packages/renderer/src/util/ObjectUtils.ts

@@ -0,0 +1,17 @@
+/**
+ * Checks if a subject is numeric.
+ * @param subject The subject to be checked.
+ * @returns 
+ */
+export function IsNumeric(subject: any) {
+    return !Array.isArray(subject) && !isNaN(subject);
+}
+
+/**
+ * Checks if a subject is an object.
+ * @param subject The subject to be checked.
+ * @returns 
+ */
+export function IsObject(subject: any) {
+    return typeof subject === "object" && !Array.isArray(subject);
+}

+ 6 - 6
test/templates/template.pupper

@@ -8,13 +8,13 @@ template
             .cover-container.d-flex.h-100.p-3.mx-auto.flex-column
                 header.masthead.mb-auto 
                     .inner
-                        h3.masthead-brand={{ page.title }}
+                        h3.masthead-brand=page.title
 
                 //- Main contents
                 main.inner.cover.my-3(role="main")
-                    h1.cover-heading={{ page.description }}
+                    h1.cover-heading=page.description
 
-                    p.lead={{ page.lead }}
+                    p.lead=page.lead
 
                     .row.mt-5.justify-content-around.align-items-center
                         if puppies === undefined || puppies.length === 0
@@ -27,18 +27,18 @@ template
                                         img.card-img-top(:src="puppy.thumbnail", crossorigin="auto")
 
                                         .card-header
-                                            h5.card-title={{ puppy.title }}
+                                            h5.card-title=puppy.title
                                             small.text-muted|Served by pupper.js
 
                                         .card-body
-                                            ={- puppy.description -}
+                                            !=puppy.description
 
                                             if puppy.shibe === true
                                                 p.text-warning|shibe!!!
 
                                             if puppy.properties
                                                 each property in puppy.properties
-                                                    span.badge.badge-info={{property}}
+                                                    span.badge.badge-info=property
 
                     div 
                         |Testing slots: