Pārlūkot izejas kodu

general refactor
- imports are now added to the final file

Matheus Giovani 3 gadi atpakaļ
vecāks
revīzija
57dbd2e0b8

+ 1 - 0
.gitignore

@@ -8,6 +8,7 @@ yarn-error.log
 yarn.lock
 
 types/
+!src/types
 
 # Logs
 logs

+ 2 - 1
.npmignore

@@ -7,4 +7,5 @@ yarn-error.log
 yarn.lock
 node_modules
 modules/
-.local
+.local
+jest.config.js

+ 4 - 0
jest.config.js

@@ -0,0 +1,4 @@
+module.exports = {
+    verbose: true,
+    preset: "jest-puppeteer"
+};

+ 2 - 0
package.json

@@ -22,6 +22,8 @@
     "@types/node": "^16.7.6",
     "@types/pug": "^2.0.5",
     "debug": "^4.3.2",
+    "jest": "^27.4.7",
+    "jest-puppeteer": "^6.1.0",
     "js-beautify": "^1.14.0",
     "npm-run-all": "^4.1.5",
     "source-map-loader": "^3.0.0",

+ 38 - 34
src/core/Compiler.ts

@@ -1,9 +1,9 @@
 import pug from "pug";
 import fs from "fs";
 import path from "path";
+
 import { Renderer } from "./Renderer";
-import Lexer from "./compiler/Lexer";
-import Parser from "./compiler/Parser";
+import Plugin from "./compiler/Plugin";
 
 export namespace Compiler {
     export interface Options {
@@ -22,12 +22,13 @@ export namespace Compiler {
 
 export default class PupperCompiler {
     /**
-     * Compiles a single template file
+     * Compiles a single template file into a renderer instance
      * @param file The file to be compiled
      * @returns
      */
-    public compileSync(file: string, options: Compiler.Options = {}): Renderer {
+    public compileFile(file: string, options: Compiler.Options = {}): Renderer {
         return this.compile(fs.readFileSync(file, "utf8"), {
+            ...options,
             pug: {
                 basedir: path.dirname(file),
                 filename: file,
@@ -35,25 +36,40 @@ export default class PupperCompiler {
         });
     }
 
+    /**
+     * 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
+     * @param options The compiler options
+     * @returns 
+     */
     public compile(template: string, options: Compiler.Options = {}): Renderer {
-        const parser = new Parser();
+        
 
         try {
             return new Renderer(
-                pug.compile(template, {
-                    name: "pupper",
-                    filename: "pupper.pug",
-                    compileDebug: options.debug || false,
-                    // Always use self to prevent conflicts with other compilers
-                    self: true,
-                    // @ts-ignore
-                    plugins: [{
-                        lex: new Lexer(),
-                        preParse: parser.preParse.bind(this),
-                        postParse: parser.postParse.bind(this)
-                    }],
-                    ...options.pug || {}
-                })
+                pug.compile(template, this.getPugOptions(options))
             );
         } catch(e) {
             throw (options.debug ? e : new Error("Failed to compile template:" + e.message));
@@ -61,28 +77,16 @@ export default class PupperCompiler {
     }
 
     /**
-     * Compiles to a string
+     * Compiles a template string to a string
      * @param template The template to be compiled
      * @param options 
      * @returns 
      */
     public compileToString(template: string, options: Compiler.Options = {}): string {
-        const parser = new Parser();
+        const parser = new Plugin();
 
         try {
-            const rendered =  pug.compileClient(template, {
-                name: "pupper",
-                compileDebug: options.debug || false,
-                // Always use self to prevent conflicts with other compilers
-                self: true,
-                // @ts-ignore
-                plugins: [{
-                    lex: new Lexer(),
-                    preParse: parser.preParse.bind(this),
-                    preLoad: parser.postParse.bind(this)
-                }],
-                ...options.pug || {}
-            });
+            const rendered =  pug.compileClient(template, this.getPugOptions(options));
 
             return /*javascript*/`
                 ${rendered}

+ 20 - 6
src/core/Component.ts

@@ -1,5 +1,6 @@
 import { Renderer } from "./Renderer";
 import type { CompiledTemplate } from "./Renderer";
+import { Reactive } from "./renderer/Reactive";
 
 export interface ComponentSettings {
     /**
@@ -15,7 +16,7 @@ export interface ComponentSettings {
     /**
      * The component methods
      */
-    methods?: Record<string, Function>;
+    methods?: Reactive.ReactiveMethods;
 }
 
 export class Component {
@@ -24,15 +25,28 @@ export class Component {
      */
     public renderer: Renderer;
 
-    public data: Record<any, any>;
-    public methods: Record<string, Function>;
-
     constructor(
         protected settings: ComponentSettings
     ) {
         this.renderer = new Renderer(this.settings.template, this.settings.data);
-        this.data = this.renderer.data;
-        this.methods = this.renderer.methods;
+        this.methods = settings.methods;
+        this.data = settings.data;
+    }
+
+    public get data() {
+        return this.renderer.data;
+    }
+
+    public set data(data: Record<any, any>) {
+        this.renderer.setData(data);
+    }
+
+    public get methods() {
+        return this.renderer.methods;
+    }
+
+    public set methods(methods: Reactive.ReactiveMethods) {
+        this.renderer.methods = methods;
     }
 
     /**

+ 11 - 6
src/core/Renderer.ts

@@ -26,8 +26,6 @@ export enum NodePreparationResult {
     FAILED
 }
 
-type K = keyof HTMLElementEventMap;
-
 export class Renderer {
     private static SYNTAX_REGEX = /(?: +)?\@p\:(?<command>.+)\((?<property>.+?)\)(?: +)?/;
 
@@ -45,7 +43,7 @@ export class Renderer {
      * The methods to be attributed with the elements
      */
     // @ts-ignore
-    public methods: Record<string, (this: HTMLElement, ev: HTMLElementEventMap[K]) => any> = {};
+    public methods: Reactive.ReactiveMethods = {};
 
     /**
      * The DOM element that will receive all children
@@ -62,14 +60,21 @@ export class Renderer {
      * @param template The pug compiled template function
      * @param data The data that will be used for reactivity
      */
-    constructor(template: pug.compileTemplate, data?: Reactive.ReactiveData) {
+    constructor(template: pug.compileTemplate, settings?: {
+        data?: Reactive.ReactiveData,
+        methods?: Reactive.ReactiveMethods
+    }) {
         this.template = template;
 
         // Create the reactor
         this.reactor = new Reactor(this);
 
-        if (data) {
-            this.setData(data);
+        if (settings?.data) {
+            this.setData(settings.data);
+        }
+
+        if (settings?.methods) {
+            this.methods = settings.methods;
         }
     }
 

+ 2 - 2
src/core/compiler/Lexer.ts

@@ -7,7 +7,7 @@ import Bind from "./lexer/tokens/Bind";
 import Import from "./lexer/tokens/Import";
 
 export default class Lexer {
-    public static LexerRegexes: typeof Token[] = [
+    public static Tokens: typeof Token[] = [
         Property,
         ForEach,
         IfToken,
@@ -22,6 +22,6 @@ export default class Lexer {
      * @returns 
      */
     public isExpression(lexer: PugLexer.Lexer, exp: string) {
-        return Lexer.LexerRegexes.some((token) => token.testExpression(exp));
+        return Lexer.Tokens.some((token) => token.testExpression(exp));
     }
 }

+ 0 - 54
src/core/compiler/Parser.ts

@@ -1,54 +0,0 @@
-import Lexer from "./Lexer";
-
-import { LexTokenType } from "pug-lexer";
-
-export interface PugToken {
-    type: LexTokenType,
-    loc?: Record<string, any>,
-    val?: string,
-    name?: string,
-    mustEscape?: boolean
-}
-
-export interface PugBlock {
-    type: "Block",
-    nodes: PugNode[]
-}
-
-export interface PugNode extends Record<string, any> {
-    type: string,
-    start?: number,
-    end?: number,
-    block?: PugBlock,
-    attrs?: {
-        name: string,
-        val: string,
-        mustEscape: boolean
-    }[]
-}
-
-export default class Parser {
-    /**
-     * Called before starts parsing
-     * @param lexer The pug lexer instance
-     * @param exp The expression to be checked against
-     * @returns 
-     */
-    public preParse(tokens: PugToken[]) {
-        Lexer.LexerRegexes.forEach((token) => {
-            const t = new token();
-            tokens = t.lex(tokens);
-        });       
-
-        return tokens;
-    }
-
-    public postParse(block: PugBlock) {
-        Lexer.LexerRegexes.forEach((token) => {
-            const t = new token();
-            block.nodes = t.parse(block.nodes);
-        }); 
-
-        return block;
-    }
-}

+ 76 - 0
src/core/compiler/Plugin.ts

@@ -0,0 +1,76 @@
+import Lexer from "./Lexer";
+import Token from "./lexer/Token";
+
+import { PugPlugin, PugToken, PugAST, PugNode } from "pug";
+
+export { PugToken, PugAST, PugNode };
+
+/**
+ * Documentation for this class is available in the PugPlugin interface
+ */
+export default class Plugin implements PugPlugin {
+    /**
+     * The instances of the tokens that will be used to parse the template file
+     */
+    private tokens: Token[] = [];
+
+    /**
+     * A handler for the plugin hooks
+     */
+    private hooks: Record<string, Function[]> = {};
+
+    public lex = new Lexer();
+
+    constructor() {
+        for(let token of Lexer.Tokens) {
+            this.tokens.push(new token(this));
+        }
+    }
+
+    public addHook(hook: string, callback: Function) {
+        if (this.hooks[hook] === undefined) {
+            this.hooks[hook] = [];
+        }
+
+        return this.hooks[hook].push(callback);
+    }
+
+    public applyFilters(hook: string, initialValue: any) {
+        // If has no hooks, return the initial value
+        if (this.hooks[hook] === undefined) {
+            return initialValue;
+        }
+
+        let value = initialValue;
+
+        for(let callback of this.hooks[hook]) {
+            value = callback(value);
+        }
+
+        return value;
+    }
+
+    public preParse(tokens: PugToken[]) {
+        for(let token of this.tokens) {
+            token.lex(tokens);
+        }    
+
+        return this.applyFilters("preParse", tokens);
+    }
+
+    public postParse(block: PugAST) {
+        for(let token of this.tokens) {
+            block.nodes = token.parse(block.nodes);
+        }
+
+        return this.applyFilters("postParse", block);
+    }
+
+    public postCodeGen(code: string): string {
+        for(let token of this.tokens) {
+            code = token.afterCompile(code);
+        }
+
+        return this.applyFilters("postCodeGen", code);
+    }
+}

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

@@ -1,8 +1,16 @@
-import { PugNode, PugToken } from "../Parser";
+import Plugin, { PugAST } from "../Plugin";
+
+import { PugToken, PugNode } from "pug";
 
 export default class Token {
     public static readonly REGEX: RegExp;
 
+    constructor(
+        protected parser: Plugin
+    ) {
+        
+    }
+
     /**
      * Tests if the token matches with the given expression
      * @param exp The expression to be tested
@@ -29,4 +37,22 @@ export default class Token {
     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;
+    }
 }

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

@@ -1,4 +1,4 @@
-import { PugToken } from "../../Parser";
+import { PugToken } from "../../Plugin";
 import Token from "../Token";
 
 export default class Bind extends Token {

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

@@ -1,4 +1,4 @@
-import { PugNode } from "../../Parser";
+import { PugNode } from "../../Plugin";
 import Token from "../Token";
 
 export default class ForEach extends Token {

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

@@ -1,4 +1,4 @@
-import { PugNode } from "../../Parser";
+import { PugNode } from "../../Plugin";
 import Token from "../Token";
 
 export default class ForEach extends Token {

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

@@ -1,9 +1,14 @@
-import { PugNode } from "../../Parser";
+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 index = 0; index < nodes.length; index++) {
             const node = nodes[index];
@@ -19,27 +24,39 @@ export default class Import extends Token {
 
                 const { identifier, filename } = condition.groups;
 
-                // Set the tag name
-                node.type = "Code";
-
-                // Setup the attributes for the foreach
-                node.block = {
-                    type: "Block",
-                    nodes: [
-                        {
-                            type: "Text",
-                            val: `const ${identifier} = require("${filename}");`
-                        }
-                    ]
-                };
-            }
+                this.imports[identifier] = filename;
 
-            // Parses the block
-            if (node.block) {
-                node.block.nodes = this.parse(node.block.nodes);
+                // Remove the node from it
+                nodes.splice(index, 1);
+            } else {
+                // 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 = `pupper.__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;
+    }
 };

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

@@ -1,4 +1,4 @@
-import { PugToken } from "../../Parser";
+import { PugToken } from "../../Plugin";
 import Token from "../Token";
 
 export default class Property extends Token {

+ 5 - 0
src/core/renderer/Reactive.ts

@@ -3,9 +3,14 @@ import type { NodeOptions } from "../Renderer";
 import type Reactor from "./Reactor";
 
 export namespace Reactive {
+    type K = keyof HTMLElementEventMap;
+
+    export type HTMLEventCallback = (this: HTMLElement, ev: HTMLElementEventMap[K]) => any;
+
     export type ReactiveData = Record<string, any>;
     export type ReactiveTarget = "text" | "html" | "attribute" | "foreach" | "if";
     export type ReactiveCommand = "escape" | "literal" | null;
+    export type ReactiveMethods = Record<string, HTMLEventCallback>;
 
     export type Context = (ProxyHandler<Reactive.ReactiveData> | Record<any, any>);
 

+ 0 - 1
src/core/renderer/reactors/If.ts

@@ -6,7 +6,6 @@ export default class IfReactor extends Reactive.AbstractReactor {
     public static readonly Type: "if";
 
     public test(path: string) {
-        console.log(this.path, path);
         return super.test(path);
     }
 

+ 1 - 1
src/index.ts

@@ -43,7 +43,7 @@ class PupperStatic {
      * @returns 
      */
     static compileFileSync(file: string, options?: Compiler.Options) {
-        return new PupperCompiler().compileSync(file, options);
+        return new PupperCompiler().compileFile(file, options);
     }
 }
 

+ 3 - 0
src/types/deep-get-set.d.ts

@@ -0,0 +1,3 @@
+declare module "deep-get-set" {
+    export default function(object: object, key: any, value?: any);
+}

+ 3 - 8
src/global.d.ts → src/types/global.d.ts

@@ -1,11 +1,6 @@
-declare module "deep-get-set" {
-    export default function(object: object, key: any, value?: any);
-}
-
-declare module "*.pupper" {
-    export default function(data: object): string;
-}
-
+/**
+ * Because observable-slim is not documented
+ */
 declare module "observable-slim" {
     declare type ObservableChange = {
         type: "add" | "delete" | "update",

+ 163 - 0
src/types/pug.d.ts

@@ -0,0 +1,163 @@
+import type pug from "pug";
+import type PugLexer from "pug-lexer";
+import { LexTokenType } from "pug-lexer";
+
+/**
+ * We use this to document the pug non-documented plugin API
+ */
+declare module "pug" {
+    export interface LexerPlugin {
+        /**
+         * Checks if a given expression is valid
+         * @param lexer The pug lexer instance
+         * @param exp The expression to be checked against
+         * @returns 
+         */
+        isExpression: (lexer: PugLexer.Lexer, exp: string) => boolean
+    }
+
+    /**
+     * Represents a pug token
+     */
+    export interface PugToken {
+        type: LexTokenType,
+        loc?: Record<string, any>,
+        val?: string,
+        name?: string,
+        mustEscape?: boolean
+    }
+
+    /**
+     * Represents a pug block
+     */
+    export interface PugAST {
+        type: "Block",
+        nodes: PugNode[]
+    }
+
+    /**
+     * Represents a generic pug node
+     */
+    export interface PugNode extends Record<string, any> {
+        type: string,
+        start?: number,
+        end?: number,
+        block?: PugAST,
+        attrs?: {
+            name: string,
+            val: string,
+            mustEscape: boolean
+        }[]
+    }
+
+    /**
+     * Represents a pug plugin
+     */
+    export interface PugPlugin {
+        /**
+         * The lexer plugin
+         */
+        lex?: LexerPlugin,
+
+        /**
+         * Called before the lexer starts parsing
+         * @param template The string template that will be lexed
+         * @param options Lexer options
+         * @returns
+         */
+        preLex?(template: string, options = {
+            /**
+             * The current filename, can be null
+             */
+            filename?: string
+        }): string;
+
+        /**
+         * Called after the lexer has parsed the template string into tokens
+         * @param tokens The tokens that the lexer has parsed
+         * @param options The pug compiler options
+         * @returns
+         */
+        postLex?(tokens: PugToken[], options: pug.Options): PugToken[];
+
+        /**
+         * Called before the parser starts parsing the tokens into AST
+         * @param tokens An array of tokens to be parsed
+         * @param options The pug compiler options
+         * @returns
+         */
+        preParse?(tokens: PugToken[], options: pug.Options): PugToken[];
+
+        /**
+         * Called after all tokens have been parsed into AST
+         * @param ast The parsed AST
+         * @param options The pug compiler options
+         * @returns
+         */
+        postParse?(ast: PugAST, options: pug.Options): PugAST;
+
+        /**
+         * Called before loading the AST
+         * @param ast The parsed AST
+         * @param options The pug compiler options
+         * @returns
+         */
+        preLoad?(ast: PugAST, options: pug.Options): PugAST;
+
+        /**
+         * Called after the AST have been parsed / loaded
+         * @param ast The parsed AST
+         * @param options 
+         */
+        postLoad?(ast: PugAST, options: pug.Options): PugAST;
+
+        /**
+         * Called before the compiler filters were called
+         * @param ast The parsed AST
+         * @param options The pug compiler options
+         */
+        preFilters?(ast: PugAST, options: pug.Options): PugAST;
+
+        /**
+         * Called after the compiler filters were called
+         * @param ast The parsed AST
+         * @param options The pug compiler options
+         */
+        postFilters?(ast: PugAST, options: pug.Options): PugAST;
+
+        /**
+         * Called before the linker is called
+         * @param ast The parsed AST
+         * @param options The pug compiler options
+         */
+        preLink?(ast: PugAST, options: pug.Options): PugAST;
+
+        /**
+         * Called after the linker is called
+         * @param ast The parsed AST
+         * @param options The pug compiler options
+         */
+        postLink?(ast: PugAST, options: pug.Options): PugAST;
+
+        /**
+         * Called before the pug code is transpiled into Javascript
+         * @param ast The parsed AST
+         * @param options The pug compiler options
+         */
+        preCodeGen?(ast: PugAST, options: pug.Options): PugAST;
+
+        /**
+         * Called after the pug code is transpiled into Javascript
+         * @param code The generated Javascript code
+         * @param options The pug compiler options
+         */
+        postCodeGen?(code: string, options: pug.Options): string;
+    }
+
+    interface Options {
+        /**
+         * Pug plugins
+         */
+        plugins?: PugPlugin[]
+    }
+}

+ 6 - 0
src/types/pupper.d.ts

@@ -0,0 +1,6 @@
+/**
+ * Used to represent whats is a pupper module
+ */
+declare module "*.pupper" {
+    export default function(data: object): string;
+}

+ 1 - 1
test/browser.js

@@ -1,7 +1,7 @@
 const { Component } = require("../out/core/Component");
 
 const pupper = new Component({
-    template: require("./template.pupper"),
+    template: require("./templates/template.pupper"),
     methods: {
         onClickPuppy(e) {
             alert("You clicked a puppy! :D");

+ 2 - 2
test/node.js

@@ -3,10 +3,10 @@ const fs = require("fs");
 const beautify = require("js-beautify");
 
 const result = beautify(
-    pupper.compileToStringSync(fs.readFileSync(__dirname + "/template.pupper"), {
+    pupper.compileToStringSync(fs.readFileSync(__dirname + "/templates/template.pupper"), {
         debug: true,
         pug: {
-            filename: __dirname + "/template.pupper"
+            filename: __dirname + "/templates/template.pupper"
         }
     })
 );

+ 4 - 0
test/templates/foreach.pupper

@@ -0,0 +1,4 @@
+- const items = [1, 2, 3, 4];
+
+foreach(item in items)
+    span=item

+ 0 - 0
test/puppy.pupper → test/templates/puppy.pupper


+ 1 - 1
test/template.pupper → test/templates/template.pupper

@@ -2,7 +2,7 @@
 link(href="https://getbootstrap.com/docs/4.0/dist/css/bootstrap.min.css", rel="stylesheet")
 link(href="https://getbootstrap.com/docs/4.0/examples/cover/cover.css", rel="stylesheet")
 
-//-import(Puppy from "./puppy.pupper")
+import(Puppy from "./puppy.pupper")
 
 .text-center
     .cover-container.d-flex.h-100.p-3.mx-auto.flex-column