Kaynağa Gözat

added a simple lexer for the tags

Matheus Giovani 3 yıl önce
ebeveyn
işleme
958b7dbec6

+ 1 - 0
.gitignore

@@ -1,3 +1,4 @@
+.logs
 out/
 test/out/
 node_modules

+ 1 - 0
.npmignore

@@ -8,4 +8,5 @@ yarn.lock
 node_modules
 modules/
 .local
+.logs
 jest.config.js

+ 21 - 5
packages/compiler/src/core/Compiler.ts

@@ -6,9 +6,12 @@ import lex from "pug-lexer";
 import parse from "pug-parser";
 import link from "pug-linker";
 import codeGen from "pug-code-gen";
+import { Console } from "console";
+import { createWriteStream } from "fs";
 
 export enum CompilationType {
-    TEMPLATE
+    TEMPLATE,
+    COMPONENT
 }
 
 export interface ICompilerOptions {
@@ -40,6 +43,8 @@ export class PupperCompiler {
 
     public compilationType: CompilationType;
 
+    public debugger = new Console(createWriteStream(process.cwd() + "/.logs/log.log"), createWriteStream(process.cwd() + "/.logs/error.log"));
+
     constructor(
         /**
          * Any options to be passed to the compiler.
@@ -93,6 +98,15 @@ export class PupperCompiler {
         return this.makeError("LEX_ERROR", "Lexer error: " + message, data);
     }
 
+    /**
+     * Normalizes line endings by replacing \r\n with \n.
+     * @param content The template to be normalized.
+     * @returns 
+     */
+    protected normalizeLines(content: string) {
+        return content.replace(/\r\n/g, "\n");
+    }
+
     protected lexAndParseString(template: string) {
         let carrier: any;
 
@@ -141,9 +155,11 @@ export class PupperCompiler {
      * @returns 
      */
     public compileComponent(template: string): string {
-        this.contents = template;
+        this.contents = this.normalizeLines(template);
+
+        this.compilationType = CompilationType.COMPONENT;
 
-        const ast = this.lexAndParseString(template);
+        const ast = this.lexAndParseString(this.contents);
         let rendered = this.generateJavaScript(ast);
 
         return rendered;
@@ -156,13 +172,13 @@ export class PupperCompiler {
      */
     public compileTemplate(template: string): string {
         const pugOptions = this.makePugOptions();
-        this.contents = template;
+        this.contents = this.normalizeLines(template);
 
         this.compilationType = CompilationType.TEMPLATE;
 
         this.plugin.prepareHooks();
 
-        const fn = pug.compile(template, pugOptions);
+        const fn = pug.compile(this.contents, pugOptions);
         const rendered = fn();
 
         return rendered;///*js*/`function $h(h) { return ${htmlToHs({ syntax: "h" })(rendered)}; }`;

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

@@ -130,7 +130,7 @@ export default class Plugin implements PugPlugin {
     /**
      * A handler for the plugin filters.
      */
-    private filters: Record<string, { callback: Function, hook: THookArray[0] }[]> = {};
+    private filters: Record<string, { callback: Function, hook: Hook }[]> = {};
 
     /**
      * Any data to be shared between hooks and phases.
@@ -226,7 +226,7 @@ export default class Plugin implements PugPlugin {
      * @param callback The filter callback.
      * @returns 
      */
-    public addFilter(filter: string, callback: Function, hook: THookConstructor) {
+    public addFilter(filter: string, callback: Function, hook: Hook) {
         if (this.filters[filter] === undefined) {
             this.filters[filter] = [];
         }

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

@@ -9,16 +9,20 @@ export class ListenerHook extends Hook {
             if (!(node instanceof TagNode)) {
                 return;
             }
-        
+
             // If has a listener
             if (node.hasAttribute("p-listener")) {
                 // Remove the attribute from it
                 const listenerName = node.removeAttribute("p-listener") as string;
+                const component = this.plugin.sharedData.components[DefaultExportSymbol] as IComponent;
+                
+                // If the component has no listeners, ignore it
+                if (component.implementation?.listeners?.length === 0) {
+                    return;
+                }
 
                 // Retrieve all events that this listener covers
-                const eventNames = (
-                    this.plugin.sharedData.components[DefaultExportSymbol] as IComponent
-                ).implementation.listeners
+                const eventNames = component.implementation.listeners
                     .find((e) => e.name === "$$p_" + listenerName).covers;
 
                 // Set them                        

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

@@ -120,7 +120,7 @@ export class ScriptParser {
             const method = this.findOrCreateComponentMethod(when.name);
             let currentText = method.getBodyText();
 
-            if (currentText && !currentText.endsWith("\n") && !currentText.endsWith("\r")) {
+            if (currentText && !currentText.endsWith("\n")) {
                 currentText += "\n";
             }
 

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

@@ -59,7 +59,7 @@ export class TagNode extends BlockedCompilerNode<Pug.Nodes.TagNode> {
 
             this.pugNode.attrs.push(attr);
         } else {
-            attr = this.getAttribute(name);
+            attr = this.getRawAttribute(name);
             attr.val = String(value);
         }
 

+ 87 - 72
packages/compiler/src/core/plugin/phases/PrepareComponentsHook.ts

@@ -4,7 +4,8 @@ import { TagNode } from "../nodes/TagNode";
 import { ScriptParser } from "../hooks/component/ScriptParser";
 import { AstNode } from "../nodes/AstNode";
 import { PupperCompiler } from "../../Compiler";
-import { CompilerNode } from "../../../model/core/nodes/CompilerNode";
+import { ConsumeChildrenAsString } from "../../../util/NodeUtil";
+import { ReadBetweenTokens, ReadLinesUntilOutdent, ReadTagWithAttributes } from "../../../util/LexingUtils";
 
 export const DefaultExportSymbol = Symbol("ExportedComponent");
 
@@ -49,83 +50,107 @@ export class PrepareComponents extends Hook {
         this.plugin.sharedData.components = {};
         this.components = this.plugin.sharedData.components;
 
-        const lines = code.replace(/\r\n/g, "\n").split(/\n/g);
+        const lines = code.split(/\n/g);
         const identation = this.plugin.detectIdentation();
-        const startWithRegExp = new RegExp("^" + identation + "(?!" + identation + ")");
-        const paramsRegExp = /^.+?\((?<params>.*?)\)$/gm;
+        const isIdentationWithTag = new RegExp("^" + identation + "[a-zA-Z\.#]");
         const singleParamRegExp = /([^?=,]+)(=([^,]*))?/g;
 
-        for(let index = 0; index < lines.length; index++) {
-            let line = lines[index];
+        for(let codeIndex = 0; codeIndex < lines.length; codeIndex++) {
+            let codeLine = lines[codeIndex];
 
-            if (line.startsWith("data") && !line.trimEnd().endsWith(".")) {
-                lines[index] = line.trimEnd() + ".";
+            // If it's a data tag and doesn't end with a dot
+            if (codeLine.startsWith("data") && !codeLine.trimEnd().endsWith(".")) {
+                // Add a dot at the end of it
+                lines[codeIndex] = codeLine.trimEnd() + ".";
                 continue;
             }
 
-            if (!line.startsWith("implementation")) {
+            // Skip lines that doesn't start with "implementation"
+            if (!codeLine.startsWith("implementation")) {
                 continue;
             }
 
-            index++;
+            // Read the implementation contents
+            let implContent = ReadLinesUntilOutdent(lines.slice(codeIndex + 1), identation);
+            const implContentLines = implContent.split("\n");
 
-            // Retrieve all lines until a non-identation was found
-            do {
-                line = lines[index];
+            for(let implIndex = 0; implIndex < implContentLines.length; implIndex++) {
+                let implLine = implContentLines[implIndex];
 
-                if (line === undefined) {
-                    break;
+                // If the line starts with a identation and a tag
+                if (!implLine.match(isIdentationWithTag)) {
+                    continue;
                 }
 
-                // Ignore empty lines
-                if (line.length === 0) {
-                    index++;
-                    continue;
+                // Read the tag
+                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("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);
                 }
 
-                // If the line starts with one identation level
-                // but doesn't end with a dot and isn't a comment
-                if (line.match(startWithRegExp) && !line.trim().endsWith(".") && !line.trimStart().startsWith("//")) {
-                    // Append a dot at the end of it
-                    lines[index] = line.trimEnd() + ".";
-
-                    let identifier = line.trimStart();
-
-                    // If it's a "when"
-                    if (identifier.startsWith("when")) {
-                        // Replace it with the internal "p-when"
-                        identifier = identifier.replace("when", "event-when");
-                    } else
-                    // If it's not an event or a listener
-                    if (!identifier.startsWith("event") && !identifier.startsWith("listener")) {
-                        // Assume it's a method then
-                        identifier = identifier.replace(identifier, "method" + identifier);
-                    }
+                // If it's a "when"
+                if (identifier.startsWith("when")) {
+                    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);
+
+                    // Assume it's a method then
+                    implContent = implContent.replace(identifier, identifier.replace(identifier, "method" + identifier));
+                }
 
-                    // Try matching params against the identifier
-                    const matchedParams = paramsRegExp.exec(identifier);
-
-                    // If matched
-                    if (matchedParams) {                        
-                        // Extract all single params
-                        const singleParams = matchedParams.groups.params.matchAll(singleParamRegExp);
-
-                        // Iterate over all params
-                        for(let param of singleParams) {
-                            // If it doesn't have a initializer
-                            if (param[2] === undefined) {
-                                // Strictly add an initializer to it
-                                identifier = identifier.replace(param[0], param[0] + " = undefined");
-                            }
+                // Try matching params against the identifier
+                const matchedParams = ReadBetweenTokens(tagData.attributes, "(", ")");
+
+                // If matched
+                if (matchedParams) {
+                    // Extract all single params
+                    const singleParams = matchedParams.matchAll(singleParamRegExp);
+
+                    // Iterate over all params
+                    for(let param of singleParams) {
+                        // If it doesn't have a initializer
+                        if (param[2] !== undefined) {
+                            continue;
                         }
+
+                        // Strictly add an "undefined" initializer to it
+                        implContent = implContent.replace(param[0].trim(), param[0].trim() + " = undefined");
                     }
 
-                    // Replace the identifier with the new one
-                    lines[index] = lines[index].replace(line.trimStart(), identifier);
+                    // Skip the params lines
+                    implLine += matchedParams.split("\n").length;
                 }
+            }
+
+            // Replace the implementation contents
+            lines.splice(
+                codeIndex + 1,
+                implContentLines.length,
+                ...implContent.split("\n")
+            );
 
-                index++;
-            } while(line.length === 0 || line.startsWith(identation));
+            this.compiler.debugger.log(lines);
+
+            break;
         }
 
         return lines.join("\n");
@@ -139,7 +164,7 @@ export class PrepareComponents extends Hook {
             }
 
             // Parse as a component
-            const component = this.parseComponentNode(node.parent);
+            this.parseComponentNode(node.parent);
 
             break;
         }
@@ -224,7 +249,7 @@ export class PrepareComponents extends Hook {
 
         // If has a script
         if (script) {
-            component.script = this.consumeChildrenAsString(script);
+            component.script = ConsumeChildrenAsString(script);
         }
 
         // If has a style
@@ -234,7 +259,7 @@ export class PrepareComponents extends Hook {
 
         // If has data
         if (data) {
-            component.data = this.consumeChildrenAsString(data);
+            component.data = ConsumeChildrenAsString(data);
         }
 
         // If has methods
@@ -317,7 +342,7 @@ export class PrepareComponents extends Hook {
                             name: attr.name,
                             initializer: attr.val === "undefined" ? undefined : String(attr.val)
                         })),
-                        body: this.consumeChildrenAsString(child)
+                        body: ConsumeChildrenAsString(child)
                     });
                 break;
 
@@ -330,7 +355,7 @@ export class PrepareComponents extends Hook {
                             name: attr.name,
                             initializer: attr.val
                         })),
-                        body: this.consumeChildrenAsString(child)
+                        body: ConsumeChildrenAsString(child)
                     });
                 break;
 
@@ -344,7 +369,7 @@ export class PrepareComponents extends Hook {
                             name: attr.name,
                             initializer: attr.val
                         })),
-                        body: this.consumeChildrenAsString(child),
+                        body: ConsumeChildrenAsString(child),
                         covers: child.getClasses()
                     });
                 break;
@@ -353,14 +378,4 @@ export class PrepareComponents extends Hook {
 
         return implementations;
     }
-
-    /**
-     * Consumes all children nodes values from a node into a string.
-     * @param node The node to be consumed.
-     * @returns 
-     */
-    protected consumeChildrenAsString(node: CompilerNode) {
-        this.plugin.parseChildren(node);
-        return node.getChildren().map((child) => child.getProp("val")).join("").trimEnd();
-    }
 };

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

@@ -105,7 +105,7 @@ export class CompilerNode<TNode extends PugNodes = any> extends NodeModel<Compil
         /**
          * The plugin related to this compiler node.
          */
-        protected plugin?: Plugin
+        public plugin?: Plugin
     ) {
         super(parent);
 

+ 219 - 0
packages/compiler/src/util/LexingUtils.ts

@@ -0,0 +1,219 @@
+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 Character = UpperCaseCharacter | Lowercase<UpperCaseCharacter> | SpecialCharacters;
+
+interface ITokenState {
+    levels: number;
+    line: number;
+    column: number;
+    content: string;
+    index: number;
+    token: string;
+    lastOpenLevel: string | null;
+    escaping: boolean;
+}
+
+class UnexpectedTokenError extends Error {
+    constructor(public state: ITokenState) {
+        super(`Unexpected token "${state.token}" @ ${state.line}:${state.column}`);
+    }
+}
+
+const CharsToEscape = ["'", '"', '`'];
+
+/**
+ * Reads between two tokens.
+ * @param string The content to be read.
+ * @param start The starting token.
+ * @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?: {
+    allowNewLines?: boolean
+}) {
+    const startIndex = string.indexOf(start);
+
+    if (startIndex === -1) {
+        return null;
+    }
+
+    const state: ITokenState = {
+        levels: 0,
+        line: 1,
+        column: 1,
+        content: "",
+        index: startIndex + 1,
+        lastOpenLevel: "",
+        escaping: false,
+        token: ""
+    };
+
+    while(state.index < string.length) {
+        state.token = string[state.index++];
+        state.column++;
+
+        // If a new line was found
+        if ((state.token === "\n")) {
+            // If doesn't allow new lines
+            if (options?.allowNewLines === false) {
+                throw new UnexpectedTokenError(state);
+            }
+
+            state.line++;
+            state.column = 1;
+        }
+
+        // If it's inside a string and it's escaping something
+        if (state.lastOpenLevel && state.token === "\\" && CharsToEscape.includes(string[state.index])) {
+            state.escaping = true;
+        } else
+        // If it's opening a string and hasn't opened a level or it's possibly closing a level
+        if (CharsToEscape.includes(state.token) && (!state.lastOpenLevel || state.lastOpenLevel === state.token)) {
+            // If was escaping
+            if (state.escaping) {
+                // Ignore the following check
+                state.escaping = false;
+            } else
+            // If has an open level
+            if (state.lastOpenLevel) {
+                // Decrease a level
+                state.levels--;
+                state.lastOpenLevel = null;
+            } else {
+                // Increase one level
+                state.levels++;
+                state.lastOpenLevel = state.token;
+            }
+        } else
+        // If reached the ending token
+        if (state.levels === 0 && state.token === end) {
+            break;
+        }
+
+        // Add the token to the contents
+        state.content += state.token;
+    }
+
+    return state.content;
+}
+
+/**
+ * Reads an array of strings until an outdent is found.
+ * @param lines A string array containing the lines to be read.
+ * @param ident The detected identation.
+ * @returns 
+ */
+export function ReadLinesUntilOutdent(lines: string[], ident: string) {
+    let index = 0;
+    let line = "";
+
+    let content = "";
+
+    do {
+        line = lines[index];
+
+        if (line === undefined) {
+            break;
+        }
+
+        content += line + "\n";
+
+        index++;
+    } while (line.length === 0 || line.startsWith(ident));
+
+    return content;
+}
+
+/**
+ * Reads an array of strings until a new identation level is found.
+ * @param lines A string array containing the lines to be read.
+ * @param ident The detected identation.
+ * @returns 
+ */
+export function ReadLinesUntilIdent(lines: string[], ident: string) {
+    let index = 0;
+    let line = "";
+
+    let content = "";
+
+    do {
+        line = lines[index];
+
+        if (line === undefined) {
+            break;
+        }
+
+        content += line + "\n";
+
+        index++;
+    } while (line.length === 0 || line.startsWith(ident + ident) || !line.startsWith(ident));
+
+    return content;
+}
+
+/**
+ * Reads the next tag with their attributes inside.
+ * @param contents The lines to be read.
+ * @returns 
+ */
+export function ReadTagWithAttributes(contents: string[]|string) {
+    const state = {
+        index: 0,
+        column: 0,
+        line: 1,
+        content: "",
+        token: "",
+
+        tag: "",
+        attributes: ""
+    };
+
+    contents = Array.isArray(contents) ? contents.join("\n") : contents;
+
+    while(state.index < contents.length) {
+        state.token = contents[state.index];
+        state.column++;
+
+        // If it's a line break
+        if (state.token === "\n") {
+            state.line++;
+            state.column = 0;
+        } else
+        // If has found a "start-attribute" token
+        if (state.token === "(") {
+            // Read the attributes
+            state.attributes = "(" + ReadBetweenTokens(contents.substring(state.index), "(", ")") + ")";
+            state.content += state.attributes;
+
+            // Skip the read attributes lines
+            state.index += state.attributes.length;
+            state.line += state.attributes.split("\n").length - 1;
+
+            // Skip the current token
+            continue;
+        }
+
+        // If got into a new line
+        if (state.token === "\n") {
+            // No possible attributes here
+            break;
+        }
+
+        // If no attribute has been read yet
+        if (!state.attributes) {
+            // Read it as the tag
+            state.tag += state.token;
+        }
+
+        state.content += state.token;
+        state.index++;
+    }
+
+    return {
+        content: state.content,
+        tag: state.tag.trimStart(),
+        attributes: state.attributes,
+        line: state.line,
+        column: state.column
+    };
+}

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

@@ -1,8 +1,19 @@
 import { appendFileSync } from "fs";
 import { inspect } from "util";
 import { NodeModel } from "../model/core/NodeModel";
+import { CompilerNode } from "../model/core/nodes/CompilerNode";
 
 export function InspectNode(node: NodeModel) {
     const inspected = inspect(node.toPugNode(), false, 99999, false);
     appendFileSync(process.cwd() + "/.test.js", inspected);
+}
+
+/**
+ * Consumes all children nodes values from a node into a string.
+ * @param node The node to be consumed.
+ * @returns 
+ */
+export function ConsumeChildrenAsString(node: CompilerNode) {
+    node.plugin.parseChildren(node);
+    return node.getChildren().map((child) => child.getProp("val")).join("").trimEnd();
 }

+ 3 - 1
test/templates/template.pupper

@@ -77,7 +77,9 @@ data
 
 implementation
     //- Declaring methods
-    #onClickPuppy(puppy)
+    #onClickPuppy(
+        puppy
+    )
         alert("You clicked puppy " + puppy.id + "! :D");
 
     //- Listening to pupper.js events.