Pārlūkot izejas kodu

initialized partial components

Matheus Giovani 3 gadi atpakaļ
vecāks
revīzija
4c6772e491

+ 46 - 46
src/core/Compiler.ts

@@ -2,7 +2,7 @@ import pug from "pug";
 import fs from "fs";
 import path from "path";
 
-import { Renderer } from "./Renderer";
+import { CompiledTemplate, Renderer } from "./Renderer";
 import Plugin from "./compiler/Plugin";
 
 export namespace Compiler {
@@ -21,43 +21,6 @@ export namespace Compiler {
 }
 
 export default class PupperCompiler {
-    /**
-     * Compiles a single template file into a renderer instance
-     * @param file The file to be compiled
-     * @returns
-     */
-    public compileFile(file: string, options: Compiler.Options = {}): Renderer {
-        return this.compile(fs.readFileSync(file, "utf8"), {
-            ...options,
-            pug: {
-                basedir: path.dirname(file),
-                filename: file,
-            }
-        });
-    }
-
-    /**
-     * Parses the compiler options into pug options
-     * and put our plugins into it
-     * @param options The compiler options
-     * @returns 
-     */
-    private getPugOptions(options: Compiler.Options = {}): pug.Options {
-        // Create a new parser for this pug instance
-        const parser = new Plugin();
-
-        return {
-            name: "pupper",
-            filename: "pupper.pug",
-            compileDebug: options.debug || false,
-            // Always use self to prevent conflicts with other compilers
-            self: true,
-            // @ts-ignore
-            plugins: [parser],
-            ...options.pug || {}
-        };
-    }
-
     /**
      * Compiles a template string into a renderer instance
      * @param template The template string to be compiled
@@ -65,11 +28,9 @@ export default class PupperCompiler {
      * @returns 
      */
     public compile(template: string, options: Compiler.Options = {}): Renderer {
-        
-
         try {
             return new Renderer(
-                pug.compile(template, this.getPugOptions(options))
+                pug.compile(template, this.getPugOptions(options)) as CompiledTemplate
             );
         } catch(e) {
             throw (options.debug ? e : new Error("Failed to compile template:" + e.message));
@@ -77,23 +38,62 @@ export default class PupperCompiler {
     }
 
     /**
-     * Compiles a template string to a string
+     * Compiles a single template file into a renderer instance
+     * @param file The file to be compiled
+     * @returns
+     */
+    public compileFile(file: string, options: Compiler.Options = {}): Renderer {
+        const pugOptions = this.getPugOptions(options);
+        pugOptions.basedir = path.dirname(file);
+        pugOptions.filename = file;
+
+        return this.compile(fs.readFileSync(file, "utf8"), pugOptions);
+    }
+
+    /**
+     * Compiles a pupper template to a Javascript module
      * @param template The template to be compiled
      * @param options 
      * @returns 
      */
     public compileToString(template: string, options: Compiler.Options = {}): string {
-        const parser = new Plugin();
-
         try {
-            const rendered =  pug.compileClient(template, this.getPugOptions(options));
+            const pugOptions = this.getPugOptions(options);
+            const rendered = pug.compileClient(template, pugOptions);
 
             return /*javascript*/`
                 ${rendered}
-                module.exports = pupper;
+                module.exports = ${pugOptions.name};
             `;
         } catch(e) {
             throw (options.debug ? e : new Error("Failed to compile template:" + e.message));
         }
     }
+
+    /**
+     * Parses the compiler options into pug options
+     * and put our plugins into it
+     * @param options The compiler options
+     * @returns 
+     */
+    private getPugOptions(options: Compiler.Options = {}): pug.Options {
+        const pugOptions: pug.Options = {
+            // We use "render" as the function name
+            name: "render",
+            // The default filename (when no filename is given) is template.pupper
+            filename: "template.pupper",
+            compileDebug: options.debug || false,
+            // Always use self to prevent conflicts with other compilers
+            self: true,
+            plugins: [],
+            ...options.pug
+        };
+
+        // Create a new parser for this pug instance
+        pugOptions.plugins.push(
+            new Plugin(pugOptions)
+        );
+
+        return pugOptions;
+    }
 }

+ 48 - 5
src/core/Renderer.ts

@@ -6,7 +6,17 @@ import { Reactive } from "./renderer/Reactive";
 
 const debug = require("debug")("pupperjs:renderer");
 
-export type CompiledTemplate = pug.compileTemplate;
+/**
+ * Represents the final contents of a pupper.js file
+ * It's the render function itself plus some useful things
+ */
+export type CompiledTemplate = pug.compileTemplate & {
+    /**
+     * A handler for all imports that this compiled template uses
+     * where the key is the tag name, and the value is the compiled template
+     */
+    imports?: Record<string, CompiledTemplate>;
+}
 
 export interface NodeOptions {
     /**
@@ -32,7 +42,7 @@ export class Renderer {
     /**
      * The pug compiled template function
      */
-    private template: pug.compileTemplate;
+    private template: CompiledTemplate;
 
     /**
      * The reactive data
@@ -60,7 +70,7 @@ export class Renderer {
      * @param template The pug compiled template function
      * @param data The data that will be used for reactivity
      */
-    constructor(template: pug.compileTemplate, settings?: {
+    constructor(template: CompiledTemplate, settings?: {
         data?: Reactive.ReactiveData,
         methods?: Reactive.ReactiveMethods
     }) {
@@ -88,6 +98,16 @@ export class Renderer {
         return {
             deepGetSet,
 
+            /**
+             * The methods related to this renderer
+             */
+            $methods: this.methods,
+
+            /**
+             * The data related to this renderer
+             */
+            $data: this.data,
+
             /**
              * Pupper helpers
              */
@@ -224,9 +244,9 @@ export class Renderer {
         const target = document.createElement("div");
         target.classList.add("pupper");
         
-        this.dom = this.renderTo(this.dom);
+        this.dom = this.renderTo(target);
 
-        return this.dom;
+        return target;
     }
 
     /**
@@ -428,6 +448,29 @@ export class Renderer {
                 });
             });
         } else
+        // Check if it's an import
+        if (element.tagName === "P:IMPORT") {
+            const template = element.getAttribute("template");
+            const data = element.getAttribute("data");
+            const methods = element.getAttribute("methods");
+
+            // Get the compiled template
+            const compiledTemplate: CompiledTemplate = this.template.imports?.[template];
+
+            // If the template doesn't exists
+            if (compiledTemplate === undefined) {
+                throw new Error("Tried to import an unknown template named " + template)
+            }
+
+            // Create the renderer for this template
+            const renderer = new Renderer(compiledTemplate, {
+                data: this.data,
+                methods: this.methods
+            });
+
+            // Render the template and replace the element with it
+            element.replaceWith(...renderer.render().childNodes);
+        } else
         // Check if it's an HTML element
         if (element instanceof HTMLElement) {
             // Iterate over all the attributes

+ 13 - 2
src/core/compiler/Plugin.ts

@@ -1,7 +1,7 @@
 import Lexer from "./Lexer";
 import Token from "./lexer/Token";
 
-import { PugPlugin, PugToken, PugAST, PugNode } from "pug";
+import { PugPlugin, PugToken, PugAST, PugNode, Options } from "pug";
 
 export { PugToken, PugAST, PugNode };
 
@@ -19,14 +19,25 @@ export default class Plugin implements PugPlugin {
      */
     private hooks: Record<string, Function[]> = {};
 
+    /**
+     * Any data to be shared between hooks and phases
+     */
+    public sharedData: Record<any, any> = {};
+
     public lex = new Lexer();
 
-    constructor() {
+    constructor(
+        protected options: Options
+    ) {
         for(let token of Lexer.Tokens) {
             this.tokens.push(new token(this));
         }
     }
 
+    public getOptions() {
+        return this.options;
+    }
+
     public addHook(hook: string, callback: Function) {
         if (this.hooks[hook] === undefined) {
             this.hooks[hook] = [];

+ 1 - 1
src/core/compiler/lexer/Token.ts

@@ -6,7 +6,7 @@ export default class Token {
     public static readonly REGEX: RegExp;
 
     constructor(
-        protected parser: Plugin
+        protected plugin: Plugin
     ) {
         
     }

+ 1 - 1
src/core/compiler/lexer/tokens/If.ts

@@ -11,7 +11,7 @@ export default class ForEach extends Token {
                 // Clone it
                 const conditional = { ...node };
 
-                // Replace with a tag
+                // Replace with an if markup tag
                 node.type = "Tag";
                 node.name = "p:if";
                 node.selfClosing = false;

+ 65 - 18
src/core/compiler/lexer/tokens/Import.ts

@@ -10,30 +10,77 @@ export default class Import extends Token {
     protected imports: Record<string, string> = {};
 
     public parse(nodes: PugNode[]) {
-        for(let index = 0; index < nodes.length; index++) {
-            const node = nodes[index];
+        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 a foreach
-            if (node.type === "Tag" && 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.");
+                    }
 
-                // 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.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.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;
 
-                const { identifier, filename } = condition.groups;
+                    // Replace it with an import markup tag
+                    node.name = "p:import";
+                    node.selfClosing = true;
+                    node.isInline = false;
 
-                this.imports[identifier] = filename;
+                    // Prevent pug from escaping the attributes
+                    node.attrs = node.attrs.map((attr) => {
+                        attr.mustEscape = false;
+                        
+                        switch(attr.name) {
+                            case "data":
+                            case "methods":
+                                attr.val = attr.val.trim();
 
-                // Remove the node from it
-                nodes.splice(index, 1);
-            } else {
-                // Parses the block
-                if (node.block) {
-                    node.block.nodes = this.parse(node.block.nodes);
+                                // If it's a JSON object
+                                if (attr.val.startsWith("{") && attr.val.endsWith("}")) {
+                                    // Remove whitespace
+                                    attr.val = `"${attr.val.replace(/^\s+|\s+$|\s+(?=\s)/g, " ")}"`;
+                                }
+                            break;
+
+                            default:
+                                throw new Error("Invalid template attribute " + attr.name);
+                        }
+
+                        return attr;
+                    });
+
+                    // Add the template name to the variables
+                    node.attrs.unshift({
+                        name: "template",
+                        val: `"${templateName}"`,
+                        mustEscape: false
+                    });
                 }
             }
+
+            // Parses the block
+            if (node.block) {
+                node.block.nodes = this.parse(node.block.nodes);
+            }
         }
 
         return nodes;
@@ -45,7 +92,7 @@ export default class Import extends Token {
         // Check if has any import
         if (importNames.length) {
             // Prepare the import handler
-            let imports = `pupper.__imports = {`;
+            let imports = `${this.plugin.getOptions().name}.imports = {`;
 
             // Add all imports to it
             imports += importNames.map((name) => {

+ 0 - 12
src/index.ts

@@ -1,24 +1,12 @@
 import { Compiler } from "./core/Compiler";
 import PupperCompiler from "./core/Compiler";
 import { Renderer } from "./core/Renderer";
-import type { compileTemplate } from "pug";
-import { Reactive } from "./core/renderer/Reactive";
 
 class PupperStatic {
     static readonly Compiler = PupperCompiler;
     static readonly Renderer = Renderer;
     static readonly Pupper = import("./pupper");
 
-    /**
-     * Creates a renderer instance
-     * @param template The compiled template function
-     * @param data The reactive data, optional
-     * @returns 
-     */
-    static createRenderer(template: compileTemplate, data?: Reactive.ReactiveData) {
-        return new Renderer(template, data);
-    }
-
     /**
      * Compiles a string
      * @param file The string to be compiled

+ 0 - 0
src/types/global.d.ts → src/types/observable-slim.d.ts


+ 7 - 13
test/templates/template.pupper

@@ -19,19 +19,13 @@ import(Puppy from "./puppy.pupper")
             .row.mt-5.justify-content-around.align-items-center
                 //- Reactive puppies
                 foreach(puppy of puppies)
-                    .col-5.mb-5
-                        .puppy.card.px-0(data-id={{ puppy.id }}, style={ cursor: "pointer" }, @click="onClickPuppy").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-body
-                                ={- puppy.description -}
-
-                                if (puppy.shibe)
-                                    p.text-warning|shibe!!!
+                    //- Render the puppy and share the onClickPuppy method with it
+                    Puppy(
+                        data={ puppy },
+                        methods={
+                            onClickPuppy: $methods.onClickPuppy
+                        }
+                    )
 
         footer.mastfoot.mt-auto
             .inner