Pārlūkot izejas kodu

added components
fixed a bug in implementation attributes

Matheus Giovani 2 gadi atpakaļ
vecāks
revīzija
389f6daf28

+ 2 - 2
nodemon.json

@@ -1,7 +1,7 @@
 {
     "$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/nodemon.json",
-    "watch": ["./src/webpack-loader/**/*", "./src/**/*"],
-    "ext": "js",
+    "watch": ["./src/*"],
+    "ext": "js,ts",
     "exec": "webpack",
     "env": {
         "NODE_OPTIONS": "-r source-map-support/register"

+ 2 - 2
packages/compiler/src/core/Compiler.ts

@@ -11,8 +11,8 @@ import { createWriteStream, existsSync, mkdirSync } from "fs";
 import { PugToVirtualDOM } from "./compiler/HTMLToVirtualDOM";
 
 export enum CompilationType {
-    TEMPLATE,
-    COMPONENT
+    TEMPLATE = 0,
+    COMPONENT = 1
 }
 
 export interface ICompilerOptions {

+ 7 - 4
packages/compiler/src/core/Plugin.ts

@@ -4,7 +4,7 @@ import { Hook } from "./plugin/Hook";
 
 import { ConditionalHook } from "./plugin/hooks/ConditionalHook";
 import { PupperToAlpineHook } from "./plugin/hooks/PupperToAlpineHook";
-import { ImportHook } from "./plugin/hooks/ImportHook";
+import { PrepareImportsPhase } from "./plugin/phases/PrepareImportsPhase";
 import { CompilerNode } from "../model/core/nodes/CompilerNode";
 import { StyleAndScriptHook } from "./plugin/hooks/StyleAndScriptHook";
 import { ListenerHook } from "./plugin/hooks/ListenerHook";
@@ -21,6 +21,7 @@ import { PrepareComponents } from "./plugin/phases/PrepareComponentsHook";
 import { CompilationType, PupperCompiler } from "./Compiler";
 
 import lex from "pug-lexer";
+import { ImportHook } from "./plugin/hooks/ImportHook";
 
 type THookConstructor = { new(plugin: Plugin): Hook };
 type THookArray = THookConstructor[];
@@ -65,8 +66,8 @@ export { PugToken, PugAST, PugNode, PugNodeAttribute, PugNodes, CompilerNode as
 export default class Plugin implements PugPlugin {
     public static Hooks: THookArray = [
         ConditionalHook,
-        PupperToAlpineHook,
         ImportHook,
+        PupperToAlpineHook,
         StyleAndScriptHook,
         ListenerHook
     ];
@@ -76,6 +77,7 @@ export default class Plugin implements PugPlugin {
      * Phases are executed before hooks.
      */
     public static Phases: THookArray = [
+        PrepareImportsPhase,
         PrepareComponents
     ];
 
@@ -157,6 +159,7 @@ export default class Plugin implements PugPlugin {
     public prepareHooks() {
         const hookOrder: string[] = [];
 
+        // Include the phases if not compiling a template
         if (this.compiler.compilationType !== CompilationType.TEMPLATE) {
             Plugin.Phases
                 .map((Phase) => new Phase(this))
@@ -199,7 +202,7 @@ export default class Plugin implements PugPlugin {
                 hook.prepareFilters();
 
                 hookOrder.push(hook.constructor.name);
-            });
+            })
     }
 
     /**
@@ -207,7 +210,7 @@ export default class Plugin implements PugPlugin {
      * @returns 
      */
     public detectIdentation() {
-        return this.compiler.contents.match(/^[\t ]+/m)[0];
+        return this.compiler.contents.match(/^[\t ]+/m)?.[0];
     }
 
     /**

+ 2 - 2
packages/compiler/src/core/compiler/HTMLToVirtualDOM.ts

@@ -15,7 +15,7 @@ export class PugToVirtualDOM {
     constructor(
         protected compiler: PupperCompiler
     ) {
-        this.identation = compiler.plugin.detectIdentation();
+        this.identation = compiler.plugin.detectIdentation() || "\t";
     }
 
     /**
@@ -171,7 +171,7 @@ export class PugToVirtualDOM {
         this.write(`h("${node.name}"`);
 
         // If the node has attributes
-        if (node.attrs.length) {
+        if (node.attrs?.length) {
             this.writeLn(", {");
             this.ident();
 

+ 2 - 3
packages/compiler/src/core/plugin/hooks/ConditionalHook.ts

@@ -1,5 +1,4 @@
 import { CompilerNode } from "../../../model/core/nodes/CompilerNode";
-import Plugin from "../../Plugin";
 import { Hook } from "../Hook";
 import { ConditionalNode } from "../nodes/ConditionalNode";
 import { TagNode } from "../nodes/TagNode";
@@ -15,7 +14,7 @@ export class ConditionalHook extends Hook {
                 // Replace with a <$ x-if />
                 const conditional = node.replaceWith({
                     type: "Tag",
-                    name: "$p",
+                    name: "$",
                     attributes: {
                         "x-if": node.getProp("test")
                     }
@@ -26,7 +25,7 @@ export class ConditionalHook extends Hook {
                     CompilerNode.fromCustomNode(
                         {
                             type: "Tag",
-                            name: "$p",
+                            name: "$",
                             attributes: {
                                 "x-if-cond": "consequent"
                             },

+ 15 - 102
packages/compiler/src/core/plugin/hooks/ImportHook.ts

@@ -1,114 +1,27 @@
-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.compiler.makeParseError("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();
+            if (!node.isType("Tag")) {
+                continue;
+            }
 
-                    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.compiler.makeParseError("Imported tags can't have a body.", {
-                            line: node.getLine(),
-                            column: node.getColumn()
-                        });
+            // If it's trying to import a previously imported template
+            if (this.plugin.sharedData.imports?.[node.getProp("name")] !== undefined) {
+                // Replace with the template
+                node.replaceWith({
+                    type: "Tag",
+                    name: "$",
+                    selfClosing: true,
+                    isInline: false,
+                    attributes: {
+                        "x-component": node.getProp("name"),
+                        ...node.getMappedAttributes()
                     }
-
-                    node.replaceWith({
-                        type: "Tag",
-                        name: "div",
-                        selfClosing: true,
-                        isInline: false,
-                        attributes: {
-                            "x-data": node.getAttribute("data")?.trim(),
-                            "x-template": node.getProp("name")
-                        }
-                    });
-                }
+                });
             }
         }
 

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

@@ -119,6 +119,17 @@ export class TagNode extends BlockedCompilerNode<Pug.Nodes.TagNode> {
         });
     }
 
+    /**
+     * Retrieves all attributes as a key-value object.
+     * @returns 
+     */
+    public getMappedAttributes() {
+        return this.getAttributes().reduce((obj, curr) => {
+            obj[curr.name] = curr.val;
+            return obj;
+        }, {} as Record<string, any>);
+    }
+
     /**
      * Retrieves all raw CSS classes related to this node.
      * @returns 

+ 38 - 24
packages/compiler/src/core/plugin/phases/PrepareComponentsHook.ts

@@ -4,11 +4,11 @@ import postcss, { Rule } from "postcss";
 import { IPluginNode } from "../../Plugin";
 import { Hook } from "../Hook";
 import { TagNode } from "../nodes/TagNode";
-import { ScriptParser } from "../hooks/component/ScriptParser";
+import { ScriptParser } from "./component/ScriptParser";
 import { AstNode } from "../nodes/AstNode";
 import { PupperCompiler } from "../../Compiler";
-import { ConsumeChildrenAsString } from "../../../util/NodeUtil";
-import { ReadBetweenTokens, ReadLinesUntilOutdent, ReadTagWithAttributes } from "../../../util/LexingUtils";
+import { consumeChildrenAsString } from "../../../util/NodeUtil";
+import { readBetweenTokens, readLinesUntilOutdent, readTagWithAttributes } from "../../../util/LexingUtils";
 
 export const DefaultExportSymbol = Symbol("ExportedComponent");
 
@@ -75,7 +75,7 @@ export class PrepareComponents extends Hook {
             }
 
             // Read the implementation contents
-            let implContent = ReadLinesUntilOutdent(lines.slice(codeIndex + 1), identation);
+            let implContent = readLinesUntilOutdent(lines.slice(codeIndex + 1), identation);
             const implContentLines = implContent.split("\n");
 
             for(let implIndex = 0; implIndex < implContentLines.length; implIndex++) {
@@ -87,42 +87,42 @@ export class PrepareComponents extends Hook {
                 }
 
                 // Read the tag
-                const tagData = ReadTagWithAttributes(implContentLines.slice(implIndex));
+                const tagData = readTagWithAttributes(implContentLines.slice(implIndex));
                 let identifier = tagData.tag;
 
                 // If the tag contents (tag + attributes) doesn't end with a dot
                 if (!tagData.content.endsWith(".")) {
-                    this.compiler.debugger.log("\n-------------------------");
-                    this.compiler.debugger.log(tagData.content);
-                    this.compiler.debugger.log("-------------------------\n");
+                    //this.compiler.debugger.log("\n-------------------------");
+                    //this.compiler.debugger.log(tagData.content);
+                    //this.compiler.debugger.log("-------------------------\n");
 
-                    this.compiler.debugger.log("before");
-                    this.compiler.debugger.log(implContent + "\n");
+                    //this.compiler.debugger.log("before");
+                    //this.compiler.debugger.log(implContent + "\n");
 
                     // Add a dot to it
                     implContent = implContent.replace(tagData.content, tagData.content + ".");
 
-                    this.compiler.debugger.log("after");
-                    this.compiler.debugger.log(implContent);
+                    //this.compiler.debugger.log("after");
+                    //this.compiler.debugger.log(implContent);
                 }
 
                 // If it's a "when"
                 if (identifier.startsWith("when")) {
-                    this.compiler.debugger.log(">> replacing \"when\" with \"event-when\" for", identifier);
+                    //this.compiler.debugger.log(">> replacing \"when\" with \"event-when\" for", identifier);
 
                     // Replace it with the internal "p-when"
                     implContent = implContent.replace(identifier, identifier.replace("when", "event-when"));
                 } else
                 // If it's not an event or a listener
                 if (!identifier.startsWith("event") && !identifier.startsWith("listener")) {
-                    this.compiler.debugger.log(">> adding method identifier for", identifier);
+                    //this.compiler.debugger.log(">> adding method identifier for", identifier);
 
                     // Assume it's a method then
                     implContent = implContent.replace(identifier, identifier.replace(identifier, "method" + identifier));
                 }
 
                 // Try matching params against the identifier
-                const matchedParams = ReadBetweenTokens(tagData.attributes, "(", ")");
+                const matchedParams = readBetweenTokens(tagData.attributes, "(", ")");
 
                 // If matched
                 if (matchedParams) {
@@ -130,16 +130,28 @@ export class PrepareComponents extends Hook {
                     const singleParams = matchedParams.matchAll(singleParamRegExp);
 
                     let attributes = tagData.attributes;
+                    let currentIndex = 0;
 
                     // Iterate over all params
                     for(let param of singleParams) {
-                        // If it doesn't have a initializer
+                        // If it already have a initializer
                         if (param[2] !== undefined) {
                             continue;
                         }
 
+                        const identifier = param[0].trim();
+                        const newIdentifier = /*js*/`${identifier} = undefined`;
+
+                        currentIndex = attributes.indexOf(param[0], currentIndex);
+
                         // Strictly add an "undefined" initializer to it
-                        attributes = attributes.replace(param[0].trim(), param[0].trim() + " = undefined");
+                        attributes = (
+                            attributes.substring(0, currentIndex) + 
+                            newIdentifier +
+                            attributes.substring(currentIndex + identifier.length)
+                        );
+
+                        currentIndex += newIdentifier.length;
                     }
 
                     // Replace the attributes with the new ones
@@ -157,7 +169,7 @@ export class PrepareComponents extends Hook {
                 ...implContent.split("\n")
             );
 
-            this.compiler.debugger.log(lines.join("\n"));
+            //this.compiler.debugger.log(lines.join("\n"));
 
             break;
         }
@@ -255,15 +267,16 @@ export class PrepareComponents extends Hook {
 
         // If has a script
         if (script) {
-            component.script = ConsumeChildrenAsString(script);
+            component.script = consumeChildrenAsString(script);
         }
 
         // If has a style
         if (style) {
-            component.style = ConsumeChildrenAsString(style);
+            component.style = consumeChildrenAsString(style);
 
             // If it's sass or scss
             if (style.getAttribute("lang") === "sass" || style.getAttribute("lang") === "scss") {
+                // Compile it
                 component.style = sass.compileString(component.style, {
                     style: this.compiler.options.debug ? "expanded" : "compressed"
                 }).css;
@@ -285,7 +298,7 @@ export class PrepareComponents extends Hook {
 
         // If has data
         if (data) {
-            component.data = ConsumeChildrenAsString(data);
+            component.data = consumeChildrenAsString(data);
         }
 
         // If has implementation
@@ -313,6 +326,7 @@ export class PrepareComponents extends Hook {
                 .map((line) => line.replace(identation, ""))
                 .join("\n");
 
+            // Create a new compiler for the template
             const compiler = new PupperCompiler(this.plugin.options);
             compiler.setSharedData(this.plugin.sharedData);
 
@@ -385,7 +399,7 @@ export class PrepareComponents extends Hook {
                             name: attr.name,
                             initializer: attr.val === "undefined" ? undefined : String(attr.val)
                         })),
-                        body: ConsumeChildrenAsString(child)
+                        body: consumeChildrenAsString(child)
                     });
                 break;
 
@@ -398,7 +412,7 @@ export class PrepareComponents extends Hook {
                             name: attr.name,
                             initializer: attr.val
                         })),
-                        body: ConsumeChildrenAsString(child)
+                        body: consumeChildrenAsString(child)
                     });
                 break;
 
@@ -412,7 +426,7 @@ export class PrepareComponents extends Hook {
                             name: attr.name,
                             initializer: attr.val
                         })),
-                        body: ConsumeChildrenAsString(child),
+                        body: consumeChildrenAsString(child),
                         covers: child.getClasses()
                     });
                 break;

+ 26 - 0
packages/compiler/src/core/plugin/phases/PrepareImportsPhase.ts

@@ -0,0 +1,26 @@
+import { Hook } from "../Hook";
+
+export class PrepareImportsPhase 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*/gm;
+
+    public beforeStart(template: string) {
+        // Create the handler for the imports
+        this.plugin.sharedData.imports = this.plugin.sharedData.imports || {};
+
+        const matches = template.matchAll(PrepareImportsPhase.INLINE_IMPORT_REGEX);
+
+        for(let match of matches) {
+            // Prepare the import
+            this.plugin.sharedData.imports[match.groups.identifier] = match.groups.filename;
+
+            template = template.replace(match.groups.match, "");
+        }
+        
+        return template;
+    }
+};

+ 1 - 1
packages/compiler/src/core/plugin/hooks/component/ScriptParser.ts → packages/compiler/src/core/plugin/phases/component/ScriptParser.ts

@@ -11,7 +11,7 @@ import {
 
 import Plugin from "../../../Plugin";
 
-import { IComponent } from "../../phases/PrepareComponentsHook";
+import { IComponent } from "../PrepareComponentsHook";
 
 export class ScriptParser {
     protected sourceFile: SourceFile;

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

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

+ 1 - 8
packages/compiler/src/model/core/NodeModel.ts

@@ -82,14 +82,7 @@ export abstract class NodeModel<TChildren = any> {
      * @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);
+        return this.parent?.children[this.parent.children.indexOf(this) + 1] || null;
     }
 
     /**

+ 26 - 1
packages/compiler/src/model/core/nodes/CompilerNode.ts

@@ -177,6 +177,17 @@ export class CompilerNode<TNode extends PugNodes = any> extends NodeModel<Compil
         return this.pugNode[prop];
     }
 
+    /**
+     * Sets the value of a pug node property.
+     * @param prop The property to change its value
+     * @param value The new property value.
+     * @returns 
+     */
+    public setProp<TKey extends keyof TNode, TValue extends TNode[TKey]>(prop: TKey, value: TValue) {
+        this.pugNode[prop] = value;
+        return this;
+    }
+
     /**
      * Checks if the pug node has a given property.
      * @param prop The property to be checked.
@@ -236,12 +247,26 @@ export class CompilerNode<TNode extends PugNodes = any> extends NodeModel<Compil
         return this.pugNode.column;
     }
 
+    /**
+     * Removes the current node from the parent.
+     */
+    public delete() {
+        if (this.getIndex() === -1) {
+            throw this.plugin.compiler.makeError("DELETE_FAILED", "Failed to delete a node because it already doesn't exists.", {
+                line: this.getLine(),
+                column: this.getColumn()
+            });
+        }
+
+        this.parent.children.splice(this.getIndex(), 1);
+    }
+
     /**
      * Replaces all node data with the given ones.
      * @param node The new node to be replaced with
      * @returns 
      */
-    public replaceWith(node: TNodes) {
+    public replaceWith<TNode extends TNodes>(node: TNode): TNode|null {
         // Iterate over all possible children containers
         for(let children of this.parent.getChildrenContainers()) {
             // If this container includes the current node as a children

+ 6 - 6
packages/compiler/src/util/LexingUtils.ts

@@ -1,5 +1,5 @@
 type UpperCaseCharacter = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z";
-type SpecialCharacters = "$" | "{" | "}" | "[" | "]" | "," | "." | ";" | "\"" | "'" | "(" | ")";
+type SpecialCharacters = "$" | "{" | "}" | "[" | "]" | "," | "." | ";" | "\"" | "'" | "(" | ")" | "`" | "´" | "~" | "^";
 type Character = UpperCaseCharacter | Lowercase<UpperCaseCharacter> | SpecialCharacters;
 
 interface ITokenState {
@@ -28,7 +28,7 @@ const CharsToEscape = ["'", '"', '`'];
  * @param end The ending token.
  * @returns The contents between the tokens, or null if the starting token couldn't be found.
  */
-export function ReadBetweenTokens(string: string, start: Character, end: Character, options?: {
+export function readBetweenTokens(string: string, start: Character, end: Character, options?: {
     allowNewLines?: boolean
 }) {
     const startIndex = string.indexOf(start);
@@ -103,7 +103,7 @@ export function ReadBetweenTokens(string: string, start: Character, end: Charact
  * @param ident The detected identation.
  * @returns 
  */
-export function ReadLinesUntilOutdent(lines: string[], ident: string) {
+export function readLinesUntilOutdent(lines: string[], ident: string) {
     let index = 0;
     let line = "";
 
@@ -130,7 +130,7 @@ export function ReadLinesUntilOutdent(lines: string[], ident: string) {
  * @param ident The detected identation.
  * @returns 
  */
-export function ReadLinesUntilIdent(lines: string[], ident: string) {
+export function readLinesUntilIdent(lines: string[], ident: string) {
     let index = 0;
     let line = "";
 
@@ -156,7 +156,7 @@ export function ReadLinesUntilIdent(lines: string[], ident: string) {
  * @param contents The lines to be read.
  * @returns 
  */
-export function ReadTagWithAttributes(contents: string[]|string) {
+export function readTagWithAttributes(contents: string[]|string) {
     const state = {
         index: 0,
         column: 0,
@@ -182,7 +182,7 @@ export function ReadTagWithAttributes(contents: string[]|string) {
         // If has found a "start-attribute" token
         if (state.token === "(") {
             // Read the attributes
-            state.attributes = "(" + ReadBetweenTokens(contents.substring(state.index), "(", ")") + ")";
+            state.attributes = "(" + readBetweenTokens(contents.substring(state.index), "(", ")") + ")";
             state.content += state.attributes;
 
             // Skip the read attributes lines

+ 2 - 2
packages/compiler/src/util/NodeUtil.ts

@@ -3,7 +3,7 @@ import { inspect } from "util";
 import { NodeModel } from "../model/core/NodeModel";
 import { CompilerNode } from "../model/core/nodes/CompilerNode";
 
-export function InspectNode(node: NodeModel) {
+export function inspectNode(node: NodeModel) {
     const inspected = inspect(node.toPugNode(), false, 99999, false);
     appendFileSync(process.cwd() + "/.test.js", inspected);
 }
@@ -13,7 +13,7 @@ export function InspectNode(node: NodeModel) {
  * @param node The node to be consumed.
  * @returns 
  */
-export function ConsumeChildrenAsString(node: CompilerNode) {
+export function consumeChildrenAsString(node: CompilerNode) {
     node.plugin.parseChildren(node);
     return node.getChildren().map((child) => child.getProp("val")).join("").trimEnd();
 }

+ 5 - 1
packages/renderer/src/core/Component.ts

@@ -2,6 +2,8 @@ import { reactive } from "../model/Reactivity";
 import { Renderer } from "./vdom/Renderer";
 import { Slot } from "./vdom/renderer/Slot";
 
+import type h from "virtual-dom/h";
+
 /**
  * Represents a component's data.
  */
@@ -16,7 +18,9 @@ export interface IComponent<
     /**
      * The function that renders the template HTML.
      */
-    render?: (...args: any[]) => VirtualDOM.VTree;
+    render?: (data: {
+        h: typeof h
+    }) => VirtualDOM.VTree;
 
     /**
      * Any data to be passed to the template.

+ 31 - 16
packages/renderer/src/core/vdom/directives/Component.ts

@@ -1,31 +1,46 @@
-import { directive, mapAttributes, replaceWith } from "../../../model/Directive";
+import { Component } from "../../../core/Component";
+import { Renderer } from "../../../core/vdom/Renderer";
+import Debugger from "../../../util/Debugger";
+import h from "virtual-dom/h";
+import { directive } from "../../../model/Directive";
 import { evaluateLater } from "../../../model/Evaluator";
 import { effect } from "../../../model/Reactivity";
-
-import Debugger from "../../../util/Debugger";
-
-const debug = Debugger.extend("vdom:component");
-
-mapAttributes(replaceWith(":", "x-bind:"));
+import { walk } from "../../../model/NodeWalker";
 
 /**
  * @directive x-component
  * @description Handles a component.
  */
-directive("component", async (node, { value, expression, scope }) => {
-    const evaluate = expression ? evaluateLater(expression) : () => {};
+directive("component", async (node, { expression, scope }) => {
+    const evaluate = evaluateLater(/*js*/`$component.$component.components?.["${expression}"]`);
 
     await effect(async () => {
         try {
-            const evaluated = await evaluate(scope);
+            const component = await evaluate(scope) as Component;
+
+            Debugger.warn("component %s resolved to %O", expression, component);
+
+            // Remove the component attribute
+            node.removeAttribute("x-component");
+
+            // Pass all attributes as $props to the scope
+            const newScope = scope;
+
+            const attrs = node.getAttributesAndProps();
+            for(let key in attrs) {
+                scope[key] = attrs[key];
+            }
 
-            // Bind the evaluated value to it
-            node.setAttribute(value, evaluated);
-        
             // Remove the original attribute from the node
-            node.removeAttribute("x-component:" + value);
-            
-            node.setDirty();
+            node.replaceWith(
+                await walk(
+                    Renderer.createNode(
+                        component.$component.render({ h }),
+                        node.parent,
+                        node.renderer
+                    ), newScope
+                )
+            );
         } catch(e) {
             console.warn("[pupper.js] failed to bind property:");
             console.error(e);

+ 2 - 1
packages/renderer/src/model/Directive.ts

@@ -1,7 +1,8 @@
 import { RendererNode } from "./vdom/RendererNode";
 import { Renderer, TRendererNodes } from "../core/vdom/Renderer";
+import { TReactiveObj } from "./Reactivity";
 
-export type TScope = Record<string, string | boolean | number>;
+export type TScope = Record<string, string | boolean | number | TReactiveObj>;
 
 export type TAttributeVal = string | number | boolean;
 

+ 1 - 1
packages/renderer/src/model/Reactivity.ts

@@ -1,5 +1,5 @@
 type TEffect = () => any | Promise<any>;
-type TReactiveObj = Record<string | number | symbol, any>;
+export type TReactiveObj = Record<string | number | symbol, any>;
 
 const effects = new Map<TReactiveObj, Record<string | symbol, TEffect[]>>();
 let currentEffect: TEffect = null;

+ 6 - 3
test/templates/ImportedComponent.pupper

@@ -1,12 +1,15 @@
 import ExportedComponent(from="./ExportedComponent.pupper")
+import TestTaggedComponent(from="./TestTaggedComponent.pupper")
 
 template
     div
         div
-            |This component has an imported component!
-            ExportedComponent()
+            em|The following content must be imported from TestTaggedComponent.pupper:
+            TestTaggedComponent(number=1, boolean=true, string="hello world", object={ a: 1 })
 
-        div.my-3
+        hr.my-5
+
+        div
             ="Also mounted into a slot:"
             slot(name="slot")
 

+ 6 - 0
test/templates/TestTaggedComponent.pupper

@@ -0,0 +1,6 @@
+template
+    pre
+        span.d-block="number is " + number
+        span.d-block="boolean is " + boolean
+        span.d-block="string is " + string
+        span.d-block="object is " + object

+ 46 - 10
test/templates/template.pupper

@@ -1,3 +1,5 @@
+import ImportedComponent(from="./ImportedComponent.pupper")
+
 template
     .container-fluid
         //- Stylesheets
@@ -24,8 +26,13 @@ template
                             //- Render the puppies and share the onClickPuppy method with it
                             each index, puppy in puppies
                                 .col-5.mb-5
-                                    .puppy.card.px-0.text-dark(:data-pop="index", :data-id="puppy.id", @click="onClickPuppy(puppy)")
-                                        img.card-img-top(:src="puppy.thumbnail", crossorigin="auto")
+                                    .puppy.card.px-0.text-dark(:data-pop="index", :data-id="puppy.id")
+                                        .img-responsive
+                                            unless puppy.shibe
+                                                .cover
+                                                    a(href="#", @click="replacePuppy(puppy, $event)")|Load a new pupper
+                                            
+                                            img.card-img-top(:src="puppy.thumbnail", crossorigin="auto")
 
                                         .card-header
                                             h5.card-title=puppy.title
@@ -46,12 +53,9 @@ template
                                                             span.badge.badge-info.w-100=property
 
                     div 
-                        |Testing slots: 
+                        h3|Testing slots and components
 
                         slot(name="slot")
-                        slot(name="slot2")
-
-import ImportedComponent(from="./ImportedComponent.pupper")
 
 data
     page = {
@@ -81,15 +85,47 @@ style(lang="scss", scoped)
     .row {
         .puppy {
             box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.3);
+
+            .img-responsive {
+                position: relative;
+
+                .cover {
+                    display: none;
+                    position: absolute;
+                    top: 0;
+                    left: 0;
+                    width: 100%;
+                    height: 100%;
+                    background-color: rgba(0, 0, 0, 0.3);
+
+                    a {
+                        color: #fff;
+                        position: absolute;
+                        top: 50%;
+                        left: 50%;
+                        transform: translate(-50%, -50%);
+                    }
+                }
+                
+                &:hover .cover {
+                    display: block;
+                }
+            }
         }
     }
 
 implementation
     //- Declaring methods
-    #onClickPuppy(
-        puppy
-    )
-        alert("You clicked puppy " + puppy.id + "! :D");
+    #replacePuppy(puppy, e)
+        e.preventDefault();
+        
+        if (!puppy.thumbnail.includes("?random"))
+            puppy.thumbnail += "?random";
+        
+        const url = new URL(puppy.thumbnail);
+        url.searchParams.set("u", +new Date());
+
+        puppy.thumbnail = url.toString();
 
     //- Listening to pupper.js events.
     when#created