1
0
Эх сурвалжийг харах

fixes some minor bugs and adds new documentation

Matheus Giovani 2 жил өмнө
parent
commit
463c879255

+ 2 - 2
nodemon.json

@@ -1,7 +1,7 @@
 {
     "$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",
     "env": {
         "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 {
     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
         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;
                 }
 
-                // 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 (!tagData.content.endsWith(".")) {
+                if (!parsedTag.content.endsWith(".")) {
                     //this.compiler.debugger.log("\n-------------------------");
                     //this.compiler.debugger.log(tagData.content);
                     //this.compiler.debugger.log("-------------------------\n");
@@ -100,7 +100,7 @@ export class PrepareComponents extends Hook {
                     //this.compiler.debugger.log(implContent + "\n");
 
                     // 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(implContent);
@@ -122,26 +122,29 @@ export class PrepareComponents extends Hook {
                 }
 
                 // Try matching params against the identifier
-                const matchedParams = readBetweenTokens(tagData.attributes, "(", ")");
+                const matchedParams = readBetweenTokens(parsedTag.attributes, "(", ")");
 
                 // If matched
                 if (matchedParams) {
                     // Extract all single params
                     const singleParams = matchedParams.matchAll(singleParamRegExp);
 
-                    let attributes = tagData.attributes;
+                    let attributes = parsedTag.attributes;
                     let currentIndex = 0;
 
                     // Iterate over all params
                     for(let param of singleParams) {
                         // If it already have a initializer
                         if (param[2] !== undefined) {
+                            // Skip it
+                            currentIndex += param[0].length;
                             continue;
                         }
 
-                        const identifier = param[0].trim();
+                        const identifier = param[0];
                         const newIdentifier = /*js*/`${identifier} = undefined`;
 
+                        // Calculate the position for the last replacement
                         currentIndex = attributes.indexOf(param[0], currentIndex);
 
                         // Strictly add an "undefined" initializer to it
@@ -155,7 +158,7 @@ export class PrepareComponents extends Hook {
                     }
 
                     // Replace the attributes with the new ones
-                    implContent = implContent.replace(tagData.attributes, attributes);
+                    implContent = implContent.replace(parsedTag.attributes, attributes);
 
                     // Skip the params lines
                     implLine += matchedParams.split("\n").length;
@@ -164,8 +167,11 @@ export class PrepareComponents extends Hook {
 
             // Replace the implementation contents
             lines.splice(
+                // From the first line of implementation code
                 codeIndex + 1,
-                implContentLines.length,
+                // To the last line of implementation code
+                implContentLines.length - 1,
+                // Insering the new implementation code
                 ...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 
  */
 export function readLinesUntilOutdent(lines: string[], ident: string) {
-    let index = 0;
-    let line = "";
+    let line = 0;
+    let lineContents = "";
 
     let content = "";
 
     do {
-        line = lines[index];
+        lineContents = lines[line];
 
-        if (line === undefined) {
+        if (lineContents === undefined) {
             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;
 }

+ 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-sass.js";
 import "prismjs/components/prism-typescript.js";
+import "prismjs/components/prism-bash.js";
 
 import webpack from "webpack";
 

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

@@ -14,16 +14,11 @@ template
             //- Load the landing page component
             LandingComponent
 
-style(lang="sass")
-    .hint {
-        padding: 1rem;
-        border-radius: 0.25rem;
-        
-        &.tip {
-            background-color: var(--info);
-            color: #fff;
-        }
-    }
-
 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")
 
 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")
 
     .m-3
         .row
             //- Sidebar
             .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
             .col-12.col-lg-6.col-xl-7.mt-3.mt-lg-0
-                div
+                .p-3
                     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
     icon = IconImage
     sections = [
         {
             id: "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",
@@ -49,14 +99,24 @@ data
                 {
                     id: "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
-    #changeToSection(data, e = undefined)
+    #changeToSection(data, e)
         e?.preventDefault();
 
         const template = document.createElement("template");
@@ -76,24 +136,41 @@ implementation
 
     when#mounted
         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) {
-                this.changeToSection(currentSection);
+                this.changeToSection({ parent: parentSection, section: currentSection });
             }
         });

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

@@ -20,4 +20,4 @@ template
 
             .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/>
+
 ### Attributes
 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.
@@ -20,6 +24,12 @@ data
     authenticated = false
 ```
 
+<br/>
+
+---
+
+<br/>
+
 ### Multi-line Attributes
 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 type h from "virtual-dom/h";
-import Debugger from "../util/Debugger";
 import { SlotNode } from "./vdom/nodes/SlotNode";
 
 /**
@@ -219,10 +218,6 @@ export class Component {
             throw new Error("Invalid mounting target " + target);
         }
 
-        if ("mounted" in this.$component) {
-            this.$component.mounted.call(this);
-        }
-
         return rendered;
     }
 }

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

@@ -177,6 +177,11 @@ export class Renderer {
 
             this.rendererNode.addEventListener("$created", () => {
                 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.
      * @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 = replacement;
     }