Forráskód Böngészése

fixes some minor bugs and adds new documentation

Matheus Giovani 2 éve
szülő
commit
463c879255

+ 2 - 2
nodemon.json

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

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

@@ -2,7 +2,7 @@ import { Hook } from "../Hook";
 
 
 export class StyleAndScriptHook extends Hook {
 export class StyleAndScriptHook extends Hook {
     public beforeStart(code: string) {
     public beforeStart(code: string) {
-        const matches = code.matchAll(/^\s*(script|style).*[^.]$/gm);
+        const matches = code.matchAll(/^\s*(?<tag>script|style).*[^.]$/gm);
 
 
         // Add dots to ending "script" and "style" tags
         // Add dots to ending "script" and "style" tags
         for(let match of matches) {
         for(let match of matches) {

+ 16 - 10
packages/compiler/src/core/plugin/phases/PrepareComponentsHook.ts

@@ -86,12 +86,12 @@ export class PrepareComponents extends Hook {
                     continue;
                     continue;
                 }
                 }
 
 
-                // Read the tag
-                const tagData = readTagWithAttributes(implContentLines.slice(implIndex));
-                let identifier = tagData.tag;
+                // Parse the tag
+                const parsedTag = readTagWithAttributes(implContentLines.slice(implIndex));
+                let identifier = parsedTag.tag;
 
 
                 // If the tag contents (tag + attributes) doesn't end with a dot
                 // If the tag contents (tag + attributes) doesn't end with a dot
-                if (!tagData.content.endsWith(".")) {
+                if (!parsedTag.content.endsWith(".")) {
                     //this.compiler.debugger.log("\n-------------------------");
                     //this.compiler.debugger.log("\n-------------------------");
                     //this.compiler.debugger.log(tagData.content);
                     //this.compiler.debugger.log(tagData.content);
                     //this.compiler.debugger.log("-------------------------\n");
                     //this.compiler.debugger.log("-------------------------\n");
@@ -100,7 +100,7 @@ export class PrepareComponents extends Hook {
                     //this.compiler.debugger.log(implContent + "\n");
                     //this.compiler.debugger.log(implContent + "\n");
 
 
                     // Add a dot to it
                     // Add a dot to it
-                    implContent = implContent.replace(tagData.content, tagData.content + ".");
+                    implContent = implContent.replace(parsedTag.content, parsedTag.content + ".");
 
 
                     //this.compiler.debugger.log("after");
                     //this.compiler.debugger.log("after");
                     //this.compiler.debugger.log(implContent);
                     //this.compiler.debugger.log(implContent);
@@ -122,26 +122,29 @@ export class PrepareComponents extends Hook {
                 }
                 }
 
 
                 // Try matching params against the identifier
                 // Try matching params against the identifier
-                const matchedParams = readBetweenTokens(tagData.attributes, "(", ")");
+                const matchedParams = readBetweenTokens(parsedTag.attributes, "(", ")");
 
 
                 // If matched
                 // If matched
                 if (matchedParams) {
                 if (matchedParams) {
                     // Extract all single params
                     // Extract all single params
                     const singleParams = matchedParams.matchAll(singleParamRegExp);
                     const singleParams = matchedParams.matchAll(singleParamRegExp);
 
 
-                    let attributes = tagData.attributes;
+                    let attributes = parsedTag.attributes;
                     let currentIndex = 0;
                     let currentIndex = 0;
 
 
                     // Iterate over all params
                     // Iterate over all params
                     for(let param of singleParams) {
                     for(let param of singleParams) {
                         // If it already have a initializer
                         // If it already have a initializer
                         if (param[2] !== undefined) {
                         if (param[2] !== undefined) {
+                            // Skip it
+                            currentIndex += param[0].length;
                             continue;
                             continue;
                         }
                         }
 
 
-                        const identifier = param[0].trim();
+                        const identifier = param[0];
                         const newIdentifier = /*js*/`${identifier} = undefined`;
                         const newIdentifier = /*js*/`${identifier} = undefined`;
 
 
+                        // Calculate the position for the last replacement
                         currentIndex = attributes.indexOf(param[0], currentIndex);
                         currentIndex = attributes.indexOf(param[0], currentIndex);
 
 
                         // Strictly add an "undefined" initializer to it
                         // Strictly add an "undefined" initializer to it
@@ -155,7 +158,7 @@ export class PrepareComponents extends Hook {
                     }
                     }
 
 
                     // Replace the attributes with the new ones
                     // Replace the attributes with the new ones
-                    implContent = implContent.replace(tagData.attributes, attributes);
+                    implContent = implContent.replace(parsedTag.attributes, attributes);
 
 
                     // Skip the params lines
                     // Skip the params lines
                     implLine += matchedParams.split("\n").length;
                     implLine += matchedParams.split("\n").length;
@@ -164,8 +167,11 @@ export class PrepareComponents extends Hook {
 
 
             // Replace the implementation contents
             // Replace the implementation contents
             lines.splice(
             lines.splice(
+                // From the first line of implementation code
                 codeIndex + 1,
                 codeIndex + 1,
-                implContentLines.length,
+                // To the last line of implementation code
+                implContentLines.length - 1,
+                // Insering the new implementation code
                 ...implContent.split("\n")
                 ...implContent.split("\n")
             );
             );
 
 

+ 12 - 7
packages/compiler/src/util/LexingUtils.ts

@@ -104,22 +104,27 @@ export function readBetweenTokens(string: string, start: Character, end: Charact
  * @returns 
  * @returns 
  */
  */
 export function readLinesUntilOutdent(lines: string[], ident: string) {
 export function readLinesUntilOutdent(lines: string[], ident: string) {
-    let index = 0;
-    let line = "";
+    let line = 0;
+    let lineContents = "";
 
 
     let content = "";
     let content = "";
 
 
     do {
     do {
-        line = lines[index];
+        lineContents = lines[line];
 
 
-        if (line === undefined) {
+        if (lineContents === undefined) {
             break;
             break;
         }
         }
 
 
-        content += line + "\n";
+        // If the line isn't idented and isn't empty
+        if (!lineContents.startsWith(ident) && lineContents.trim().length > 0) {
+            break;
+        }
 
 
-        index++;
-    } while (line.length === 0 || line.startsWith(ident));
+        content += lineContents + "\n";
+
+        line++;
+    } while (true);
 
 
     return content;
     return content;
 }
 }

+ 1 - 0
packages/docs/builder.js

@@ -15,6 +15,7 @@ import "prismjs/components/prism-pug.js";
 import "prismjs/components/prism-javascript.js";
 import "prismjs/components/prism-javascript.js";
 import "prismjs/components/prism-sass.js";
 import "prismjs/components/prism-sass.js";
 import "prismjs/components/prism-typescript.js";
 import "prismjs/components/prism-typescript.js";
+import "prismjs/components/prism-bash.js";
 
 
 import webpack from "webpack";
 import webpack from "webpack";
 
 

+ 7 - 12
packages/docs/src/components/App.pupper

@@ -14,16 +14,11 @@ template
             //- Load the landing page component
             //- Load the landing page component
             LandingComponent
             LandingComponent
 
 
-style(lang="sass")
-    .hint {
-        padding: 1rem;
-        border-radius: 0.25rem;
-        
-        &.tip {
-            background-color: var(--info);
-            color: #fff;
-        }
-    }
-
 data
 data
-    isDocs = new URL(window.location.href).hash.includes("docs")
+    isDocs = window.location.hash.includes("docs")
+
+implementation
+    when#mounted
+        window.addEventListener("hashchange", () => {
+            this.isDocs = window.location.hash.includes("docs");
+        });

+ 112 - 35
packages/docs/src/components/DocsComponent.pupper

@@ -1,46 +1,96 @@
-import SyntaxComponent(from="../documentation/essentials/Syntax.md")
-import GettingStarted(from="../documentation/Getting Started.md")
-
 import IconImage(from="url-loader!../img/icon.png")
 import IconImage(from="url-loader!../img/icon.png")
 
 
 template
 template
+    //- Theme for prism.js (syntax highlighting)
     link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css")
     link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css")
 
 
     .m-3
     .m-3
         .row
         .row
             //- Sidebar
             //- Sidebar
             .col-12.col-lg-4.col-xl-3
             .col-12.col-lg-4.col-xl-3
-                img.mb-5(width="25%", :src="icon")
-
-                ul
-                    each section in sections
-                        li
-                            unless section.subSections
-                                a.text-dark(href="#", @click="changeToSection({ section }, $event)")
-                                    strong=section.title
-                            else
-                                strong=section.title
-
-                            if section.subSections
-                                .list-group
-                                    each subSection in section.subSections
-                                        .list-group-item.list-group-item-action(
-                                            role="button",
-                                            @click="changeToSection({ section: subSection, parent: section })"
-                                        )=subSection.title
+                #docs-menu
+                    img.mb-5(width="25%", :src="icon")
+
+                    ul
+                        each section in sections
+                            li
+                                unless section.subSections
+                                    a.text-dark(href="#", @click="changeToSection({ section }, $event)")
+                                        strong=section.title
+                                else
+                                    a.text-dark(href="#")
+                                        strong=section.title
+
+                                if section.subSections
+                                    ul
+                                        each subSection in section.subSections
+                                            li
+                                                a.text-dark(href="#" @click="changeToSection({ section: subSection, parent: section }, $event)")
+                                                    =subSection.title
 
 
             //- Section content
             //- Section content
             .col-12.col-lg-6.col-xl-7.mt-3.mt-lg-0
             .col-12.col-lg-6.col-xl-7.mt-3.mt-lg-0
-                div
+                .p-3
                     slot(name="section")
                     slot(name="section")
 
 
+style(lang="sass")
+    #docs-menu {
+        position: sticky;
+        top: 1rem;
+        
+        ul {
+            padding: 0;
+            margin: 0;
+
+            li {
+                list-style: none;
+
+                a {
+                    padding: 0.25rem;
+                    width: 100%;
+                    display: block;
+                }
+
+                ul {
+                    margin-top: 0.25rem;
+                    margin-left: 0.5rem;
+                }
+
+                &:not(:last-child) {
+                    margin-bottom: 0.25rem;
+                }
+            }
+        }
+    }
+
+    .hint {
+        padding: 1rem;
+        border-radius: 0.25rem;
+        
+        &.tip {
+            background-color: var(--info);
+            color: #fff;
+        }
+    }
+
 data
 data
     icon = IconImage
     icon = IconImage
     sections = [
     sections = [
         {
         {
             id: "getting-started",
             id: "getting-started",
             title: "Getting Started",
             title: "Getting Started",
-            component: GettingStarted
+            subSections: [
+                {
+                    id: "introduction",
+                    title: "Introduction",
+                    component: require("../documentation/getting-started/Introduction.md").default
+                },
+                {
+                    id: "quick-start",
+                    title: "Quick Start",
+                    component: require("../documentation/getting-started/Quick Start.md").default
+                }
+            ]
         },
         },
         {
         {
             id: "essentials",
             id: "essentials",
@@ -49,14 +99,24 @@ data
                 {
                 {
                     id: "syntax",
                     id: "syntax",
                     title: "Syntax",
                     title: "Syntax",
-                    component: SyntaxComponent
+                    component: require("../documentation/essentials/Syntax.md").default
+                },
+                {
+                    id: "conditional-rendering",
+                    title: "Conditional Rendering",
+                    component: require("../documentation/essentials/Conditional Rendering.md").default
+                },
+                {
+                    id: "list-rendering",
+                    title: "List Rendering",
+                    component: require("../documentation/essentials/List Rendering.md").default
                 }
                 }
             ]
             ]
         }
         }
     ]
     ]
 
 
 implementation
 implementation
-    #changeToSection(data, e = undefined)
+    #changeToSection(data, e)
         e?.preventDefault();
         e?.preventDefault();
 
 
         const template = document.createElement("template");
         const template = document.createElement("template");
@@ -76,24 +136,41 @@ implementation
 
 
     when#mounted
     when#mounted
         this.$nextTick(() => {
         this.$nextTick(() => {
-            const sectionParts = window.location.hash.split("docs/")?.[1].split("/");
+            const sectionParts = window.location.hash?.split("docs/")?.[1]?.split("/");
+
+            let currentSection;
+            let parentSection;
 
 
-            let currentSection = this.sections.find((section) => section.id === sectionParts.shift());
-            let part = 0;
+            if (sectionParts) {
+                const firstPart = sectionParts?.shift();
 
 
-            while(part <= sectionParts.length) {
-                const currentPart = sectionParts[part++];
+                parentSection = this.sections.find((section) => section.id === firstPart);
+                currentSection = parentSection;
+                let part = 0;
 
 
-                if (currentSection.id !== currentPart) {
-                    currentSection = currentSection?.subSections.find((s) => s.id === currentPart);
+                while(part <= sectionParts.length - 1) {
+                    const currentPart = sectionParts[part++];
 
 
-                    if (!currentSection) {
-                        break;
+                    if (currentSection.id !== currentPart) {
+                        currentSection = currentSection.subSections?.find((s) => s.id === currentPart);
+
+                        if (!currentSection) {
+                            break;
+                        }
                     }
                     }
                 }
                 }
             }
             }
 
 
+            if (!currentSection) {
+                parentSection = this.sections.find((section) => section.id === "getting-started");
+                currentSection = parentSection.subSections.find((section) => section.id === "introduction")
+            }
+
+            if (currentSection === parentSection) {
+                parentSection = undefined;
+            }
+
             if (currentSection) {
             if (currentSection) {
-                this.changeToSection(currentSection);
+                this.changeToSection({ parent: parentSection, section: currentSection });
             }
             }
         });
         });

+ 1 - 1
packages/docs/src/components/LandingComponent.pupper

@@ -20,4 +20,4 @@ template
 
 
             .mt-3
             .mt-3
 
 
-            a.btn.btn-primary(href="#docs")|Documentation
+            a.btn.btn-primary(href="#/docs")|Documentation

+ 0 - 1
packages/docs/src/documentation/Getting Started.md

@@ -1 +0,0 @@
-## Getting Started

+ 52 - 0
packages/docs/src/documentation/essentials/Conditional Rendering.md

@@ -0,0 +1,52 @@
+## Conditional Rendering
+<br/>
+
+---
+
+<br/>
+
+### `if`
+The tag `if` is used to conditionally render a block.
+The block will only be rendered if the directive's expression returns a thruthy value.
+
+```pug
+if shibe
+    |Shibbers are awesome!
+```
+
+<br/>
+
+---
+
+<br/>
+
+### `else`
+You can use the `else` tag to indicate an "else block" for `if`:
+
+```pug
+button(@click="shibe = !shibe!") Toggle
+
+if shibe
+    |Shibbers are awesome!
+else
+    |🐕😢
+```
+
+<br/>
+
+---
+
+<br/>
+
+### `else if`
+As the `else if` name suggests, serves as an "else if block" for `if`.
+It can also be chained multiple times:
+
+```pug
+if dog === "shiba"
+    |Shibber!
+else if dog === "akita"
+    |Akita!
+else
+    |Not a shibber or an akita.
+```

+ 45 - 0
packages/docs/src/documentation/essentials/List Rendering.md

@@ -0,0 +1,45 @@
+## List Rendering
+
+<br/>
+
+---
+
+<br/>
+
+### `each`
+We can use the tag `each` to render a list of items based on an array.
+The `each` tag requires a special syntax in the form of `item in items`, where `items` is the source data array and `item` is an alias for the array element being iterated on:
+
+```pug
+template
+    each item in items
+        =item.message
+data
+    items = [
+        {
+            message: "Foo"
+        },
+        {
+            message: "Bar"
+        }
+    ]
+```
+
+Inside the `each` scope, template expressions have access to all parent scope properties.
+In addition, `each` also supports an optional second alias for the index of the current item:
+
+```pug
+template
+    each item, index in items
+        |#{prefix} - #{index} - #{item.message}
+data
+    prefix = "Prefix";
+    items = [
+        {
+            message: "Foo"
+        },
+        {
+            message: "Bar"
+        }
+    ]
+```

+ 10 - 0
packages/docs/src/documentation/essentials/Syntax.md

@@ -3,6 +3,10 @@ pupper syntax is based in [pug](https://pugjs.org/api/getting-started.html).
 
 
 <br/>
 <br/>
 
 
+---
+
+<br/>
+
 ### Attributes
 ### Attributes
 It is very semantical and natural to write in pupper.
 It is very semantical and natural to write in pupper.
 Tag and attributes look similar to HTML, but with commas separating the attributes. Their values are just regular JavaScript.
 Tag and attributes look similar to HTML, but with commas separating the attributes. Their values are just regular JavaScript.
@@ -20,6 +24,12 @@ data
     authenticated = false
     authenticated = false
 ```
 ```
 
 
+<br/>
+
+---
+
+<br/>
+
 ### Multi-line Attributes
 ### Multi-line Attributes
 If you have many attributes, you can also spread them across many lines:
 If you have many attributes, you can also spread them across many lines:
 
 

+ 93 - 0
packages/docs/src/documentation/getting-started/Introduction.md

@@ -0,0 +1,93 @@
+## Introduction
+
+### What is pupper?
+pupper is a reactive JavaScript framework and template engine used for building user interfaces.
+It compiles to standard HTML, CSS and JavaScript, and provides a component-based programming model that helps you efficiently develop user interfaces.
+
+Here is a minimal example:
+
+```pug
+template
+    button(@click="count++")
+        |Count is #{count}
+data
+    count = 0
+style
+    button {
+        font-weight: bold;
+    }
+```
+
+The above example demonstrates the two core features of pupper:
+
+- **Declarative Rendering:** pupper extends pug with a template syntax that allows us to declaratively describe HTML output based on JavaScript state.
+
+- **Reactivity:** pupper automatically tracks JavaScript state changes and efficiently updates the DOM when changes happen.
+
+- **Single-file Components:** We author pupper components using a pug-like file format called **Single-File Component** (also known as `*.pupper` files, abbreviated as **SFC**).
+A pupper SFC, as the name suggests, encapsulates the component's logic (JavaScript), template (pug), and styles (Sass or CSS) in a single file.
+
+<br/>
+
+---
+
+<br/>
+
+### API Styles
+pupper components can be authored in three different API styles: Declarative API and Options API.
+
+<br/>
+
+#### Declarative API
+With Declarative API, we define a component's logic using logical tags.
+For example, imports and top-level variables / functions declared in are directly usable in the template.
+
+Here is a component, but using Composition API and declarations instead:
+
+```pug
+template
+    button(@click="increment()")
+        |Count is #{count}
+data
+    count = 0
+style
+    button {
+        font-weight: bold;
+    }
+implementation
+    when#mounted
+        console.log("The initial count is:", this.count);
+
+    #increment
+        this.count++;
+```
+
+#### Options API
+With Options API, we define a component's logic using an exported object of options such as data, methods, and mounted.
+Properties defined by options are exposed on this inside functions, which points to the component instance:
+
+Here is a component, with the exact same template, but using Options API:
+
+```pug
+template
+    button(@click="increment()")
+        |Count is #{count}
+style
+    button {
+        font-weight: bold;
+    }
+script
+    export default {
+        data: {
+            count: 0
+        },
+        methods: {
+            increment() {
+                this.count++;
+            }
+        },
+        mounted() {
+            console.log("The initial count is:", this.count);
+        }
+    }
+```

+ 35 - 0
packages/docs/src/documentation/getting-started/Quick Start.md

@@ -0,0 +1,35 @@
+## Quick Start
+
+A build setup allows us to use pupper Single-File Components (SFCs).
+You can integrate it with your preferred build tool:
+
+- webpack:
+    <br/>
+
+    Install the loader by using your preferred package manager:
+    ```bash
+    yarn add @pupper/webpack-loader -D
+    ```
+
+    or
+
+    ```bash
+    npm i @pupper/webpack-loader -D
+    ```
+
+    <br/>
+
+    *webpack.config.json*
+    ```javascript
+    module.exports = {
+        module: {
+            rules: [
+                {
+                    // Will test for .pup and .pupper files
+                    test: /\.pup(per)?$/,
+                    use: ["@pupperjs/webpack-loader"]
+                }
+            ]
+        }
+    }
+    ```

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

@@ -2,7 +2,6 @@ import { reactive } from "../model/Reactivity";
 import { Renderer } from "./vdom/Renderer";
 import { Renderer } from "./vdom/Renderer";
 
 
 import type h from "virtual-dom/h";
 import type h from "virtual-dom/h";
-import Debugger from "../util/Debugger";
 import { SlotNode } from "./vdom/nodes/SlotNode";
 import { SlotNode } from "./vdom/nodes/SlotNode";
 
 
 /**
 /**
@@ -219,10 +218,6 @@ export class Component {
             throw new Error("Invalid mounting target " + target);
             throw new Error("Invalid mounting target " + target);
         }
         }
 
 
-        if ("mounted" in this.$component) {
-            this.$component.mounted.call(this);
-        }
-
         return rendered;
         return rendered;
     }
     }
 }
 }

+ 5 - 0
packages/renderer/src/core/vdom/Renderer.ts

@@ -177,6 +177,11 @@ export class Renderer {
 
 
             this.rendererNode.addEventListener("$created", () => {
             this.rendererNode.addEventListener("$created", () => {
                 this.component.$rendered = this.rendererNode.element;
                 this.component.$rendered = this.rendererNode.element;
+
+                // Call the mounted function in the component
+                if ("mounted" in this.component.$component) {
+                    this.component.$component.mounted.call(this.component);
+                }
             });
             });
         });
         });
 
 

+ 20 - 1
packages/renderer/src/core/vdom/nodes/SlotNode.ts

@@ -30,7 +30,26 @@ export class SlotNode extends RendererNode<VirtualDOM.VNode> {
      * Replaces the slot contents with a given element.
      * Replaces the slot contents with a given element.
      * @param replacement The element to replace this slot's content.
      * @param replacement The element to replace this slot's content.
      */
      */
-    public replace(replacement: Element) {
+    public replace(replacement: Element|DocumentFragment) {
+        // If it's a template
+        if (replacement instanceof HTMLTemplateElement) {
+            replacement = replacement.content;
+        }
+
+        // If it's a document fragment
+        if (replacement instanceof DocumentFragment) {
+            // If has more than one child
+            if (replacement.childElementCount > 1) {
+                // Append it to a div then
+                const div = document.createElement("div");
+                div.appendChild(replacement);
+
+                replacement = div;
+            } else {
+                replacement = replacement.firstElementChild;
+            }
+        }
+
         this.element.replaceWith(replacement);
         this.element.replaceWith(replacement);
         this.element = replacement;
         this.element = replacement;
     }
     }