├── .github ├── scripts │ ├── npm-release.js │ ├── package-lock.json │ └── package.json └── workflows │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── compiler-api.md ├── index.md ├── quickstart.assets │ └── image-20201122171456280.png └── quickstart.md ├── examples ├── pages │ ├── component1.prism │ ├── layout1.prism │ ├── page1.prism │ ├── page2.prism │ ├── page3.prism │ ├── page4.prism │ └── page5.prism └── primitives │ ├── attributes.prism │ ├── calculator.prism │ ├── comment.prism │ ├── conditional.prism │ ├── counter-threshold.prism │ ├── counter.prism │ ├── form.prism │ ├── imported.prism │ ├── lifecycle.prism │ ├── link.prism │ ├── list.prism │ ├── modal.prism │ ├── objects.prism │ ├── page.prism │ ├── post.prism │ ├── raw-html.prism │ ├── svg.prism │ └── two-variables.prism ├── inject-bundle.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── post-build.js ├── src ├── builders │ ├── assets.ts │ ├── client-side-routing.ts │ ├── compile-app.ts │ ├── compile-component.ts │ ├── prism-client.ts │ ├── server-side-rendering │ │ ├── rust.ts │ │ └── typescript.ts │ └── template.ts ├── bundle │ ├── component.ts │ ├── helpers.ts │ ├── index.prism │ ├── observable.ts │ ├── others.ts │ ├── render.ts │ ├── router.ts │ ├── server.ts │ └── template.html ├── chef │ ├── README.md │ ├── abstract-asts.ts │ ├── css │ │ ├── at-rules.ts │ │ ├── css.ts │ │ ├── rule.ts │ │ ├── selectors.ts │ │ ├── stylesheet.ts │ │ └── value.ts │ ├── dynamic-url.ts │ ├── filesystem.ts │ ├── helpers.ts │ ├── html │ │ └── html.ts │ ├── javascript │ │ ├── components │ │ │ ├── constructs │ │ │ │ ├── block.ts │ │ │ │ ├── class.ts │ │ │ │ └── function.ts │ │ │ ├── module.ts │ │ │ ├── statements │ │ │ │ ├── comments.ts │ │ │ │ ├── for.ts │ │ │ │ ├── if.ts │ │ │ │ ├── import-export.ts │ │ │ │ ├── statement.ts │ │ │ │ ├── switch.ts │ │ │ │ ├── try-catch.ts │ │ │ │ ├── variable.ts │ │ │ │ └── while.ts │ │ │ ├── types │ │ │ │ ├── decorator.ts │ │ │ │ ├── enum.ts │ │ │ │ ├── interface.ts │ │ │ │ ├── statements.ts │ │ │ │ └── type-signature.ts │ │ │ └── value │ │ │ │ ├── array.ts │ │ │ │ ├── expression.ts │ │ │ │ ├── group.ts │ │ │ │ ├── object.ts │ │ │ │ ├── regex.ts │ │ │ │ ├── template-literal.ts │ │ │ │ └── value.ts │ │ ├── javascript.ts │ │ └── utils │ │ │ ├── reverse.ts │ │ │ ├── types.ts │ │ │ └── variables.ts │ └── rust │ │ ├── dynamic-statement.ts │ │ ├── module.ts │ │ ├── statements │ │ ├── block.ts │ │ ├── derive.ts │ │ ├── for.ts │ │ ├── function.ts │ │ ├── if.ts │ │ ├── mod.ts │ │ ├── struct.ts │ │ ├── use.ts │ │ └── variable.ts │ │ ├── utils │ │ └── js2rust.ts │ │ └── values │ │ ├── expression.ts │ │ ├── value.ts │ │ └── variable.ts ├── cli.ts ├── component.ts ├── filesystem.ts ├── helpers.ts ├── metatags.ts ├── node.ts ├── others │ ├── actions.ts │ └── banners.ts ├── settings.ts ├── templating │ ├── builders │ │ ├── client-render.ts │ │ ├── data-bindings.ts │ │ ├── get-value.ts │ │ ├── server-event-bindings.ts │ │ ├── server-render.ts │ │ └── set-value.ts │ ├── constructs │ │ ├── for.ts │ │ └── if.ts │ ├── helpers.ts │ ├── html-element.ts │ ├── template.ts │ └── text-node.ts └── web.ts ├── static └── logo.png ├── tests ├── chef │ ├── css │ │ ├── css.parse.test.ts │ │ └── css.render.test.ts │ ├── html │ │ ├── html.parse.test.ts │ │ └── html.render.test.ts │ ├── javascript │ │ ├── javascript.parse.test.ts │ │ ├── javascript.render.test.ts │ │ └── javascript.utils.test.ts │ ├── rust │ │ └── rust.render.test.ts │ └── url-params.test.ts └── templating │ ├── bindings.test.ts │ ├── client-side-render.test.ts │ ├── event.test.ts │ ├── runtime-feature-detection.test.ts │ ├── server-side-render.test.ts │ └── templating.test.ts └── tsconfig.json /.github/scripts/npm-release.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core'); 2 | const semver = require('semver'); 3 | const fs = require('fs'); 4 | const path = require("path"); 5 | 6 | try { 7 | const packageJSONFile = path.join(process.env.GITHUB_WORKSPACE, "package.json"); 8 | const packageJSON = JSON.parse(fs.readFileSync(packageJSONFile).toString()); 9 | const versionInput = process.argv[2] || core.getInput("version", {required: true}); 10 | let version; 11 | switch (versionInput.toLowerCase()) { 12 | case "major": 13 | case "minor": 14 | case "patch": 15 | version = semver.inc(packageJSON.version, versionInput); 16 | break; 17 | default: 18 | const parsedVersion = semver.parse(versionInput); 19 | if (parsedVersion === null) { 20 | throw new Error(`Invalid version: "${versionInput}"`); 21 | } else { 22 | version = parsedVersion.version; 23 | } 24 | break; 25 | } 26 | packageJSON.version = version; 27 | fs.writeFileSync(packageJSONFile, JSON.stringify(packageJSON, 0, 4)); 28 | core.info(`😎 Updated package.json version to ${version}`); 29 | core.setOutput("newVersion", version); 30 | } catch (error) { 31 | core.setFailed(error.message); 32 | } -------------------------------------------------------------------------------- /.github/scripts/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scripts", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@actions/core": { 8 | "version": "1.2.7", 9 | "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.7.tgz", 10 | "integrity": "sha512-kzLFD5BgEvq6ubcxdgPbRKGD2Qrgya/5j+wh4LZzqT915I0V3rED+MvjH6NXghbvk1MXknpNNQ3uKjXSEN00Ig==" 11 | }, 12 | "lru-cache": { 13 | "version": "6.0.0", 14 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 15 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 16 | "requires": { 17 | "yallist": "^4.0.0" 18 | } 19 | }, 20 | "semver": { 21 | "version": "7.3.5", 22 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", 23 | "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", 24 | "requires": { 25 | "lru-cache": "^6.0.0" 26 | } 27 | }, 28 | "yallist": { 29 | "version": "4.0.0", 30 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 31 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scripts", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "npm-release.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "node npm-release.js" 10 | }, 11 | "keywords": [], 12 | "author": "kaleidawave", 13 | "dependencies": { 14 | "@actions/core": "^1.2.6", 15 | "semver": "^7.3.5" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: NPM Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "major/minor/patch or semver" 8 | required: true 9 | default: "patch" 10 | 11 | jobs: 12 | publish: 13 | runs-on: windows-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v1 17 | - name: Run update script 18 | id: script 19 | run: | 20 | npm ci 21 | npm run start -- ${{ github.event.inputs.version }} 22 | working-directory: ./.github/scripts 23 | - name: Build package 24 | run: | 25 | npm ci 26 | npm run build 27 | - name: Publish on npm 28 | uses: JS-DevTools/npm-publish@v1 29 | with: 30 | token: ${{ secrets.NPM_REGISTRY_TOKEN }} 31 | - name: Push updated package.json 32 | run: | 33 | git config user.name github-actions 34 | git config user.email github-actions@github.com 35 | git add . 36 | git commit -m "Release: ${{ steps.script.outputs.newVersion }}" 37 | git tag "v${{ steps.script.outputs.newVersion }}" 38 | git push --tags origin main 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | private 3 | bin 4 | out 5 | .git 6 | .vscode 7 | prism.config.json 8 | *.tgz 9 | examples/temp 10 | esm 11 | lib 12 | mjs 13 | cjs 14 | dts 15 | src/bundled-files.ts -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 kaleidawave 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/compiler-api.md: -------------------------------------------------------------------------------- 1 | ### Using the Prism compiler api 2 | 3 | Aside from using the cli, Prism components can be compiled using the `@kaleidawave/prism` package. 4 | 5 | ##### On the web: 6 | 7 | The `web` export 8 | 9 | ```js 10 | // From cdn: 11 | import * as prism from 'https://cdn.skypack.dev/@kaleidawave/prism@latest/web'; 12 | // From npm (requires bundler): 13 | import * as prism from '@kaleidawave/prism/web'; 14 | 15 | const component1 = 16 | ` 20 | `; 25 | 26 | const component2 = 27 | ` 30 | `; 33 | 34 | // Compiling single component 35 | const files1 = prism.compileSingleComponentFromString(component2); 36 | 37 | // With settings *1 38 | const settings = { minify: true }; 39 | 40 | // Compiling multiple components using a map to represent a filesystem 41 | const fs_map = new Map([["/index.prism", component1], ["/component2.prism", component2]]); 42 | const files2 = prism.compileSingleComponentFromFSMap(fs_map, settings); 43 | ``` 44 | 45 | **Notice that the component filenames must be absolute. The entry component is `/index.prism`** 46 | 47 | \*1 Full settings can be found [here](https://github.com/kaleidawave/prism/blob/85b9048035d624dc753a4ecf457d422c07b98d3a/src/settings.ts#L3-L25) 48 | 49 | ##### On node: 50 | 51 | Same as web, node exports `compileSingleComponentFromString` and `compileSingleComponentFromFSMap` for compiling components. 52 | 53 | ```js 54 | // CJS 55 | const prism = require("@kaleidawave/prism"); 56 | // MJS 57 | import * as prism from "@kaleidawave/prism/import"; 58 | ``` 59 | 60 | - TODO FS CALLBACK OVERWRITE EXPLANATION 61 | - TODO BUILDING PRISM APPLICATION -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Prism 2 | 3 | Prism is a *experimental* isomorphic web app compiler. 4 | 5 | ### Reasons to use Prism: 6 | 7 | ##### JIT Hydration: 8 | 9 | Prism uses a incremental hydration technique. A prism app can add event listeners without needing state. State is then progressively loaded in. 10 | 11 | 12 | 13 | And with this system it uses the DOM to hydrate. This means it does not have the explicit state that other solutions rely on: 14 | 15 | ```html 16 | 17 | ``` 18 | 19 | This technique enhances TTI and overall performance. 20 | 21 | ##### Rust SSR: 22 | 23 | Prism can compile SSR functions to Rust. Currently all frontend *prerendering* services require a JS runtime. Next, sapper and nuxt are all based on using a JS runtime server. Prism on the other hand has support for outputting Rust modules that expose methods for *prerendering* Prism apps. 24 | 25 | Exhibit A: [hackernews-prism](https://github.com/kaleidawave/hackernews-prism) 26 | 27 | ##### Small bundle sizes: 28 | 29 | Prism outputs very little JS. Often on par or below the size Svelte outputs and certainty a magnitude smaller than a React app. Especially as there is also no JS state blob on SSR. 30 | 31 | ##### Built in client side routing: 32 | 33 | Routing is done in Prism with the `@Page` decorator. No need to add a separate package. It also has built in design for layouts. 34 | 35 | ##### Others: 36 | 37 | - Declarative component templating 38 | - Single file components 39 | - "Plain JS" 40 | - Compiled to web components 41 | 42 | ### About: 43 | 44 | Prism is experimental. 45 | 46 | If you know are interested by some of the above points and want to try it out give it a shot. But if you are writing something with actual users and want a more stable base I would recommend [svelte](https://github.com/sveltejs/svelte), [preact](https://github.com/preactjs/preact) or [solid](https://github.com/ryansolid/solid). -------------------------------------------------------------------------------- /docs/quickstart.assets/image-20201122171456280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaleidawave/prism/2b1350ba1f16bdc9bfcd795a62b423d9cf56e423/docs/quickstart.assets/image-20201122171456280.png -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quick start 2 | 3 | ### Setup: 4 | 5 | ``` 6 | npm i @kaleidawave/prism 7 | ``` 8 | 9 | Check its installed 10 | 11 | ``` 12 | prism info 13 | ``` 14 | 15 | ``` 16 | ______ ______ __ ______ __ __ 17 | /\ == \ /\ == \ /\ \ /\ ___\ /\ "-./ \ Prism Compiler 18 | \ \ _-/ \ \ __< \ \ \ \ \___ \ \ \ \-./\ \ 1.4.4 19 | \ \_\ \ \_\ \_\ \ \_\ \/\_____\ \ \_\ \ \_\ @kaleidawave 20 | \/_/ \/_/ /_/ \/_/ \/_____/ \/_/ \/_/ 21 | ``` 22 | 23 | > You should see something like this with a version higher or equal to than `1.4.4` 24 | 25 | Create a new prism app with `prism init` 26 | 27 | Should create: 28 | 29 | ``` 30 | 📂 views 31 | 📜 index.prism 32 | 📜 prism.config.json 33 | ``` 34 | 35 | We can start it up and run it with `prism compile-app --run open` 36 | 37 | And we should see a new browser window showing Hello World. 38 | 39 | ### Prism syntax: 40 | 41 | Open up `views/index.prism` 42 | 43 | > For syntax highlighting if using vscode add this to vscode `settings.json` `"files.associations": { "*.prism": "html" }` 44 | 45 | You should see: 46 | 47 | ```html 48 | 51 | 52 | 56 | ``` 57 | 58 | You can see where the "Hello World" came from. You can also see `@Page` which denotes that this component is a page to be rendered under "/" 59 | 60 | #### Adding styles: 61 | 62 | Prism is built on single file components. This means that you can append a style tag which contains the styles for the this page / component. 63 | 64 | ```diff 65 | ... 66 | + 72 | ``` 73 | 74 | #### Adding a new page: 75 | 76 | We can create a new page by creating a new `.prism` file in the `views` directory. We will call it `counter.prism` and we will copy the content of the `index.page`. 77 | 78 | - Set the page to be matched under `/counter` with `@Page("/counter")`. 79 | - Set the h1 text to be `Count: {}`. The curly braces indicate we want to interpolate the `count` value 80 | - Prism currently requires to know type information of the state. The component we extends needs a generic parameter that is the type def of the state. We can use `extends Component<{count: number}>` to depict that the component state has a property of count of type number. 81 | - We can add another decorator to depict the default / initial state of the component using `@Default({count: 4})` 82 | 83 | ```html 84 | 87 | 88 | 93 | ``` 94 | 95 | Save the file, (close the existing server ctrl+c if it is still open). And rebuild and run with `prism compile-app --run`. 96 | 97 | Going to `/counter` we now see our counter with the 4 interpolated into the markup. 98 | 99 | ![image-20201122171456280](quickstart.assets/image-20201122171456280.png) 100 | 101 | Prism outputs web components with a reactive state. We can get the `counter-page` component and update its state in the console: 102 | 103 | ```js 104 | const counterPageInstance = document.querySelector("counter-page"); 105 | counterPageInstance.data.count++; 106 | ``` 107 | 108 | `.data.count` acts like any other object member. You can read its value and mutate it freely while the view will update to its value. 109 | 110 | #### Events: 111 | 112 | Modifying the state through dev tools is great but lets add some buttons to do this. 113 | 114 | ```html 115 | 119 | ... 120 | ``` 121 | 122 | We can add events by adding a new attribute. The attribute key name is `@` in front of the event name and the value is the name of the method on the component definition: 123 | 124 | ```html 125 | ... 126 | 135 | ``` 136 | 137 | #### More coming soon: 138 | 139 | - SSR 140 | - Layouts 141 | - Page metadata 142 | - #if, and #for 143 | -------------------------------------------------------------------------------- /examples/pages/component1.prism: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /examples/pages/layout1.prism: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /examples/pages/page1.prism: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /examples/pages/page2.prism: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /examples/pages/page3.prism: -------------------------------------------------------------------------------- 1 | 15 | 16 | 36 | 37 | -------------------------------------------------------------------------------- /examples/pages/page4.prism: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /examples/pages/page5.prism: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /examples/primitives/attributes.prism: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /examples/primitives/calculator.prism: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /examples/primitives/comment.prism: -------------------------------------------------------------------------------- 1 | 5 | 6 | 14 | 15 | -------------------------------------------------------------------------------- /examples/primitives/conditional.prism: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | 24 | -------------------------------------------------------------------------------- /examples/primitives/counter-threshold.prism: -------------------------------------------------------------------------------- 1 | 13 | 14 | 27 | 28 | -------------------------------------------------------------------------------- /examples/primitives/counter.prism: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /examples/primitives/form.prism: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /examples/primitives/imported.prism: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /examples/primitives/lifecycle.prism: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /examples/primitives/link.prism: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /examples/primitives/list.prism: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /examples/primitives/modal.prism: -------------------------------------------------------------------------------- 1 | 7 | 8 | 29 | 30 | -------------------------------------------------------------------------------- /examples/primitives/objects.prism: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /examples/primitives/page.prism: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /examples/primitives/post.prism: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /examples/primitives/raw-html.prism: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /examples/primitives/svg.prism: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /examples/primitives/two-variables.prism: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /inject-bundle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Run with ts-node 3 | * Generates src/bundled-files.ts by building a exported Map that maps filenames to the bundled files 4 | */ 5 | 6 | import {readdirSync, readFileSync, writeFileSync} from "fs"; 7 | import {join} from "path"; 8 | import {Module} from "./src/chef/javascript/components/module"; 9 | import {Comment} from "./src/chef/javascript/components/statements/comments"; 10 | import {Expression, Operation, VariableReference} from "./src/chef/javascript/components/value/expression"; 11 | import {ArrayLiteral} from "./src/chef/javascript/components/value/array"; 12 | import {Value, Type} from "./src/chef/javascript/components/value/value"; 13 | import {VariableDeclaration} from "./src/chef/javascript/components/statements/variable"; 14 | import {ScriptLanguages} from "./src/chef/helpers"; 15 | 16 | const infoComment = new Comment("Automatically generated from inject-bundle.js", true); 17 | const bundledFilesDotTS = new Module("", [infoComment]); 18 | 19 | const filenameToFileContent: Array<[string, string]> = []; 20 | 21 | for (const filename of readdirSync(join(process.cwd(), "src", "bundle"))) { 22 | const content = readFileSync(join(process.cwd(), "src", "bundle", filename)).toString(); 23 | filenameToFileContent.push([filename, content]); 24 | } 25 | 26 | bundledFilesDotTS.addExport(new VariableDeclaration("fileBundle", { 27 | value: new Expression({ 28 | operation: Operation.Initialize, 29 | lhs: new VariableReference("Map"), 30 | rhs: new ArrayLiteral( 31 | filenameToFileContent.map(([filename, content]) => new ArrayLiteral([ 32 | new Value(Type.string, filename), 33 | new Value(Type.string, content) 34 | ])) 35 | ) 36 | }) 37 | })); 38 | 39 | const prismVersion = JSON.parse(readFileSync("package.json").toString()).version; 40 | 41 | bundledFilesDotTS.addExport(new VariableDeclaration("prismVersion", { 42 | value: new Value(Type.string, prismVersion) 43 | })); 44 | 45 | writeFileSync( 46 | join(process.cwd(), "src", "bundled-files.ts"), 47 | bundledFilesDotTS.render({scriptLanguage: ScriptLanguages.Typescript}) 48 | ); 49 | console.log("Built file bundle"); -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kaleidawave/prism", 3 | "version": "1.5.7", 4 | "description": "Isomorphic web app compiler", 5 | "main": "./cjs/node.cjs", 6 | "browser": "./esm/web.js", 7 | "exports": { 8 | "./package.json": "./package.json", 9 | ".": { 10 | "node": { 11 | "require": "./cjs/node.cjs" 12 | } 13 | }, 14 | "./bin": { 15 | "node": "./lib/node/cli.js" 16 | }, 17 | "./web": { 18 | "browser": { 19 | "import": "./esm/web.js", 20 | "module": "./esm/web.js", 21 | "default": "./lib/browser/web.js" 22 | } 23 | }, 24 | "./import": { 25 | "node": { 26 | "import": "./mjs/import.mjs" 27 | } 28 | } 29 | }, 30 | "scripts": { 31 | "test": "jest", 32 | "build": "npm run build-bundle && npm run build-packemon", 33 | "build-packemon": "packemon build --declaration standard", 34 | "postbuild-packemon": "node post-build.js", 35 | "build-tsc": "tsc", 36 | "build-bundle": "ts-node inject-bundle.ts", 37 | "clean": "packemon clean" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/kaleidawave/prism.git" 42 | }, 43 | "keywords": [ 44 | "web-components", 45 | "ssr", 46 | "template", 47 | "isomorphic", 48 | "framework", 49 | "compiler" 50 | ], 51 | "author": "kaleidawave", 52 | "license": "MIT", 53 | "bugs": { 54 | "url": "https://github.com/kaleidawave/prism/issues" 55 | }, 56 | "files": [ 57 | "cjs", 58 | "cjs/", 59 | "cjs/**/*.{cjs,map}", 60 | "dts/**/*.d.ts", 61 | "esm", 62 | "esm/", 63 | "esm/**/*.{js,map}", 64 | "lib", 65 | "lib/", 66 | "lib/**/*.{js,map}", 67 | "mjs", 68 | "mjs/", 69 | "mjs/**/*.{mjs,map}" 70 | ], 71 | "homepage": "https://github.com/kaleidawave/prism#readme", 72 | "// Only used for testing and development:": "", 73 | "devDependencies": { 74 | "@types/jest": "^25.2.3", 75 | "@types/jest-cli": "^24.3.0", 76 | "@types/node": "^13.13.48", 77 | "jest": "^25.5.4", 78 | "jest-cli": "^26.5.3", 79 | "packemon": "^0.15.0", 80 | "ts-jest": "^26.5.4", 81 | "ts-node": "^9.1.1", 82 | "typescript": "^3.9.9" 83 | }, 84 | "packemon": [ 85 | { 86 | "inputs": { 87 | "index": "src/node.ts" 88 | }, 89 | "format": "cjs", 90 | "platform": "node" 91 | }, 92 | { 93 | "inputs": { 94 | "bin": "src/cli.ts" 95 | }, 96 | "platform": "node" 97 | }, 98 | { 99 | "inputs": { 100 | "web": "src/web.ts" 101 | }, 102 | "platform": "browser", 103 | "support": "current" 104 | }, 105 | { 106 | "inputs": { 107 | "import": "src/web.ts" 108 | }, 109 | "platform": "node", 110 | "format": "mjs", 111 | "support": "current" 112 | } 113 | ], 114 | "jest": { 115 | "preset": "ts-jest" 116 | }, 117 | "engines": { 118 | "node": ">=10.3.0", 119 | "npm": ">=6.1.0" 120 | }, 121 | "dependencies": {}, 122 | "bin": { 123 | "prism": "lib/node/cli.js" 124 | }, 125 | "directories": { 126 | "example": "examples", 127 | "test": "tests" 128 | }, 129 | "type": "commonjs", 130 | "types": "./dts/node.d.ts" 131 | } -------------------------------------------------------------------------------- /post-build.js: -------------------------------------------------------------------------------- 1 | const {readFileSync, writeFileSync} = require("fs"); 2 | 3 | // Cleans up and corrects packemon output 4 | const packageJSON = JSON.parse(readFileSync("package.json")); 5 | packageJSON.main = "./cjs/node.cjs"; 6 | packageJSON.files = packageJSON.files.filter(name => name !== "src/**/*.{ts,tsx,json}"); 7 | packageJSON.types = "./dts/node.d.ts"; 8 | writeFileSync("package.json", JSON.stringify(packageJSON, 0, 4)); -------------------------------------------------------------------------------- /src/builders/assets.ts: -------------------------------------------------------------------------------- 1 | import { filesInFolder } from "../helpers"; 2 | import { Module } from "../chef/javascript/components/module"; 3 | import { Stylesheet } from "../chef/css/stylesheet"; 4 | import { IRenderSettings } from "../chef/helpers"; 5 | import { copyFile } from "../filesystem"; 6 | import { relative, resolve, dirname } from "path"; 7 | 8 | const styleFileExtensions = ["css", "scss"]; 9 | const scriptFileExtensions = ["js", "ts"]; 10 | 11 | /** 12 | * Moves assets from one folder to output folder 13 | * @param assetsFolder 14 | * @param outputFolder 15 | * @returns Array of module and stylesheets found in /scripts or /styles folder 16 | */ 17 | export function moveStaticAssets( 18 | assetsFolder: string, 19 | outputFolder: string, 20 | renderSettings: Partial 21 | ): Array { 22 | const modulesAndStylesheets: Array = []; 23 | 24 | for (const file of filesInFolder(assetsFolder)) { 25 | let moduleOrStylesheet: Module | Stylesheet | null = null; 26 | const folder = dirname(relative(assetsFolder, file)); 27 | if (styleFileExtensions.some(ext => file.endsWith(ext))) { 28 | const stylesheet = Stylesheet.fromFile(file); 29 | if (folder.startsWith("styles")) { 30 | modulesAndStylesheets.push(stylesheet); 31 | continue; 32 | } else { 33 | moduleOrStylesheet = stylesheet; 34 | } 35 | } else if (scriptFileExtensions.some(ext => file.endsWith(ext))) { 36 | const module = Module.fromFile(file); 37 | if (folder.startsWith("scripts")) { 38 | modulesAndStylesheets.push(module); 39 | continue; 40 | } else { 41 | moduleOrStylesheet = module; 42 | } 43 | } 44 | 45 | const relativePath = relative(assetsFolder, file); 46 | let outputPath = resolve(outputFolder, relativePath); 47 | 48 | if (moduleOrStylesheet) { 49 | if (outputPath.endsWith(".ts")) { 50 | outputPath = outputPath.substring(0, outputPath.length - 3) + ".js"; 51 | } else if (outputFolder.endsWith(".scss")) { 52 | outputPath = outputPath.substring(0, outputPath.length - 5) + ".css"; 53 | } 54 | moduleOrStylesheet.writeToFile(renderSettings, outputPath); 55 | } else { 56 | copyFile(file, outputPath); 57 | } 58 | } 59 | return modulesAndStylesheets; 60 | } -------------------------------------------------------------------------------- /src/builders/client-side-routing.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "../component"; 2 | import { Module } from "../chef/javascript/components/module"; 3 | import { ArrayLiteral } from "../chef/javascript/components/value/array"; 4 | import { Value, Type, ValueTypes } from "../chef/javascript/components/value/value"; 5 | import { DynamicUrl, dynamicUrlToRegexPattern } from "../chef/dynamic-url"; 6 | import { ClassDeclaration } from "../chef/javascript/components/constructs/class"; 7 | import { RegExpLiteral } from "../chef/javascript/components/value/regex"; 8 | 9 | // Maps urls to components 10 | const routes: Array<[DynamicUrl, Component]> = []; 11 | let notFoundRoute: Component; 12 | 13 | /** 14 | * TODO check no overwrite 15 | * @param url 16 | * @param component 17 | */ 18 | export function addRoute(url: DynamicUrl, component: Component) { 19 | routes.push([url, component]); 20 | } 21 | 22 | export function getRoutes() { 23 | return routes; 24 | } 25 | 26 | /** 27 | * @param component 28 | */ 29 | export function setNotFoundRoute(component: Component) { 30 | if (notFoundRoute) { 31 | throw Error(`Component "${component.filename}" cannot be set to all routes as "${notFoundRoute.filename}" is already set to match on all routes`); 32 | } 33 | notFoundRoute = component; 34 | } 35 | 36 | /** 37 | * Takes the client side router module and injects a array map the router uses to pair urls to components and layouts 38 | */ 39 | export function injectRoutes(routerModule: Module): void { 40 | 41 | // Use the bundled router and get the router component 42 | const routerComponent: ClassDeclaration = routerModule.classes.find(cls => cls.actualName === "Router")!; 43 | 44 | // Build up array that map patterns to components and their possible layout 45 | const routePairArray = new ArrayLiteral(); 46 | 47 | // Sort routes so that fixed routes take prevalence over dynamic routes 48 | routes.sort((r1, r2) => { 49 | const r1hasDynamicParts = r1[0].some(part => typeof part === "object"), 50 | r2hasDynamicParts = r2[0].some(part => typeof part === "object"); 51 | if (r1hasDynamicParts === r2hasDynamicParts) { 52 | return 0; 53 | } else if (r1hasDynamicParts && !r2hasDynamicParts) { 54 | return 1; 55 | } else { 56 | return -1; 57 | } 58 | }); 59 | 60 | for (const [url, component] of routes) { 61 | const parts: Array = []; 62 | // TODO could to dynamic import for code splitting 63 | parts.push(dynamicUrlToRegexPattern(url)); 64 | 65 | // Push the component tag and if the the page requires a layout 66 | parts.push(new Value(Type.string, component.tagName)); 67 | if (component.usesLayout) { 68 | parts.push(new Value(Type.string, component.usesLayout.tagName,)); 69 | } 70 | 71 | routePairArray.elements.push(new ArrayLiteral(parts)); 72 | } 73 | 74 | // If there is a not found route append a always matching regex pattern 75 | // Important that this comes last as not to return early on other patterns 76 | if (notFoundRoute) { 77 | const parts: Array = []; 78 | parts.push(new RegExpLiteral(".?")); // This regexp matches on anything inc empty strings 79 | parts.push(new Value(Type.string, notFoundRoute.tagName)); 80 | if (notFoundRoute.usesLayout) { 81 | parts.push(new Value(Type.string, notFoundRoute.usesLayout.tagName)); 82 | } 83 | routePairArray.elements.push(new ArrayLiteral(parts)); 84 | } 85 | 86 | // Add the routes as a static member to the router class 87 | routerComponent.staticFields!.get("r")!.value = routePairArray; 88 | } -------------------------------------------------------------------------------- /src/builders/template.ts: -------------------------------------------------------------------------------- 1 | import { IFinalPrismSettings } from "../settings"; 2 | import { flatElements, HTMLDocument, HTMLElement, Node } from "../chef/html/html"; 3 | import { fileBundle } from "../bundled-files"; 4 | import { NodeData } from "../templating/template"; 5 | import { assignToObjectMap } from "../helpers"; 6 | import { IRenderSettings } from "../chef/helpers"; 7 | import { join } from "path"; 8 | 9 | export interface IShellData { 10 | document: HTMLDocument, 11 | nodeData: WeakMap 12 | slots: Set 13 | } 14 | 15 | /** 16 | * Creates the underlining index document including references in the script to the script and style bundle. 17 | */ 18 | export function parseTemplateShell(settings: IFinalPrismSettings, jsName: string, cssName: string): IShellData { 19 | // Read the included template or one specified by settings 20 | let document: HTMLDocument; 21 | const nodeData: WeakMap = new WeakMap(), 22 | slots: Set = new Set(); 23 | if (settings.absoluteTemplatePath) { 24 | document = HTMLDocument.fromFile(settings.absoluteTemplatePath); 25 | } else { 26 | document = HTMLDocument.fromString(fileBundle.get("template.html")!, "template.html"); 27 | } 28 | 29 | for (const element of flatElements(document)) { 30 | if (element.tagName === "slot") { 31 | const slotFor = element.attributes?.get("for") ?? "content"; 32 | 33 | slots.add(element); 34 | assignToObjectMap(nodeData, element, "slotFor", slotFor); 35 | 36 | switch (slotFor) { 37 | case "content": 38 | // Wrap slot inside out router-component: 39 | let swapElement: HTMLElement = new HTMLElement("router-component", null, [], element.parent); 40 | element.parent!.children.splice(element.parent!.children.indexOf(element), 1, swapElement); 41 | swapElement.children.push(element); 42 | element.parent = swapElement; 43 | break; 44 | case "meta": 45 | // TODO manifest 46 | element.parent!.children.splice( 47 | element.parent!.children.indexOf(element), 48 | 0, 49 | new HTMLElement( 50 | "script", 51 | new Map([["type", "module"], ["src", settings.relativeBasePath + jsName]]), 52 | [], 53 | element.parent 54 | ), 55 | new HTMLElement( 56 | "link", 57 | new Map([["rel", "stylesheet"], ["href", settings.relativeBasePath + cssName]]), 58 | [], 59 | element.parent 60 | ) 61 | ); 62 | break; 63 | default: 64 | throw Error(`Unknown value for slot for. Expected "content" or "meta" received "${slotFor}"`); 65 | } 66 | } 67 | } 68 | 69 | return { document, nodeData, slots }; 70 | } 71 | 72 | /** 73 | Writes out a "index.html" or "shell.html" which is for using Prism as a csr spa 74 | */ 75 | export function writeIndexHTML( 76 | template: IShellData, 77 | settings: IFinalPrismSettings, 78 | clientRenderSettings: Partial 79 | ) { 80 | const indexHTMLPath = 81 | join(settings.absoluteOutputPath, settings.context === "client" ? "index.html" : "shell.html"); 82 | 83 | // Remove slot elements from the output 84 | const slotElements: Array<[HTMLElement | HTMLDocument, HTMLElement, number]> = []; 85 | for (const slotElem of template.slots) { 86 | const parent = slotElem.parent!; 87 | const indexOfSlotElement = parent.children.indexOf(slotElem); 88 | parent.children.splice(indexOfSlotElement, 1); 89 | slotElements.push([parent, slotElem, indexOfSlotElement]); 90 | } 91 | template.document.writeToFile(clientRenderSettings, indexHTMLPath); 92 | // Replace slot elements 93 | slotElements.forEach(([parent, slotElem, index]) => parent.children.splice(index, 0, slotElem)); 94 | } -------------------------------------------------------------------------------- /src/bundle/component.ts: -------------------------------------------------------------------------------- 1 | import { cOO } from "./observable"; 2 | 3 | /** 4 | * Adds reusable prism functionality on top of the base HTMLElement class 5 | */ 6 | export abstract class Component extends HTMLElement { 7 | // The private cached (un-proxied) component data 8 | _d: Partial = {}; 9 | // Proxied data 10 | _dP: Partial = {}; 11 | 12 | // Like isConnected but false until connectedCallback is finished 13 | _isR: boolean = false; 14 | 15 | // Caches for element lookup 16 | // TODO new Map could be lazy 17 | _eC: Map = new Map(); 18 | _ifEC: Map = new Map(); 19 | 20 | abstract layout: true | undefined; // Used by router to detect if SSR content is layout 21 | abstract _t: T | undefined; // The primary observable 22 | 23 | // A callback for when triggered from a router where params are a set of URL params 24 | abstract load?: (params?: Object) => Promise; 25 | 26 | // CSR component 27 | abstract render(); 28 | 29 | // Add and remove event bindings, a = add 30 | abstract handleEvents(a: boolean): void; 31 | 32 | // User defined lifecycle callbacks (which don't interfere with connectedCallback) 33 | abstract connected(): void | undefined; 34 | abstract disconnected(): void | undefined; 35 | 36 | // Used to retrieve elements inside the components DOM using a set class name by the Prism compiler 37 | // Also caches the element as to not call querySelector every time 38 | // Using query selector will work across csr and ssr component dom 39 | // TODO cache invalidation if element is not connected??? 40 | getElem(id: string) { 41 | if (this._eC.has(id)) { 42 | return this._eC.get(id); 43 | } else { 44 | const e = (this.shadowRoot ?? this).querySelector(`.${id}`); 45 | if (e) this._eC.set(id, e); 46 | return e; 47 | } 48 | } 49 | 50 | // Used to manually update the cache 51 | setElem(id: string, e: HTMLElement) { 52 | e.classList.add(id); 53 | this._eC.set(id, e); 54 | } 55 | 56 | // Returns reactivity state of the component. Deep changes will be reflected in the dom. Will only create observable once 57 | get data(): Partial { 58 | if (!this._isR) { 59 | return this._d; 60 | } 61 | if (!this._t) { 62 | // @ts-expect-error ._bindings does exist statically on derived class (abstract static) 63 | this._t = cOO.call(this, this.constructor._bindings, this._d, this._dP) 64 | } 65 | return this._t; 66 | } 67 | 68 | // Deeply assign values 69 | set data(value) { 70 | if (this._isR) { 71 | Object.assign(this.data, value) 72 | } else { 73 | this._d = value 74 | } 75 | } 76 | 77 | connectedCallback() { 78 | // If component has been sever side rendered 79 | if (this.hasAttribute("data-ssr")) { 80 | this._d = {}; 81 | this.handleEvents?.(true); 82 | } else { 83 | this.render() 84 | } 85 | this.connected?.(); 86 | this._isR = true; 87 | } 88 | 89 | disconnectedCallback() { 90 | this.disconnected?.(); 91 | this.handleEvents?.(false); 92 | this._isR = false; 93 | this._eC.clear() 94 | } 95 | } -------------------------------------------------------------------------------- /src/bundle/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from "./component"; 2 | 3 | /** 4 | * Utility function for swapping elements, used under #if cssu (client side state updates) 5 | * @param p A expression which if evaluates to truthy will sw 6 | * @param id 7 | * @param elementGenerator A function to generate the nodes. The element predicate value is aware of the value of the predicate. TODO could be sent value to not reevaluate 8 | */ 9 | export function conditionalSwap(this: Component, p: boolean, id: string, elementGenerator: () => HTMLElement): void { 10 | // (o)ldElem 11 | const oE: Element = this.getElem(id); 12 | // Don't change the element if the value of the predicate hasn't changed 13 | if (!!p === oE.hasAttribute("data-else")) { 14 | // this._ifSwapElemCache.get(id) will always return the prev discarded (if it was generated) 15 | const nE = this._ifEC.get(id) ?? elementGenerator.call(this, p); 16 | this.setElem(id, nE); // Manually update cache 17 | this._ifEC.set(id, oE); 18 | oE.replaceWith(nE); // Replace the element 19 | } 20 | } 21 | 22 | /** 23 | * Assign to e if it exists 24 | * @param e Component instance or CharacterData to assign to 25 | * @param v Value to attempt to assign 26 | * @param p Property to assign to (defaults to "data") 27 | */ 28 | export function tryAssignData(e: CharacterData | Component | null, v: any, p = "data") { 29 | if (e) Reflect.set(e, p, v); 30 | } 31 | 32 | /** 33 | * Given a element, cut out old children and for each old one call its remove function to remove it from the DOM. 34 | * This is when called by observable arrays 35 | * @param p Parent element (one with #for on) 36 | * @param l The target length for the parent.children 37 | */ 38 | export function setLength(p: HTMLElement | null, l: number) { 39 | if (p) Array.from(p.children).splice(l).forEach(e => e.remove()); 40 | } 41 | 42 | /** 43 | * Returns true if array has holes / undefined elements 44 | * @example `isArrayHoley([,,1]) -> true` 45 | * @param a Array 46 | */ 47 | export function isArrayHoley(a: Array): boolean { 48 | for (let i = 0; i < a.length; i++) { 49 | if (a[i] === undefined) return true; 50 | } 51 | return false; 52 | } 53 | 54 | /** 55 | * Generic function for adding or removing events to some element given a id, event name and callback 56 | * @param t the component to add to 57 | * @param id the id of the element 58 | * @param en the event name to bind to 59 | * @param cb the callback 60 | * @param enable to remove the "disabled" attribute 61 | */ 62 | export function changeEvent(t: Component, id: string, en: string, cb: any, add = true, enable = true) { 63 | const elem = t.getElem(id); 64 | if (!elem) return; 65 | if (add) { 66 | elem.addEventListener(en, cb); 67 | if (enable) elem.removeAttribute("disabled"); 68 | } else { 69 | elem.removeEventListener(en, cb); 70 | if (enable) elem.setAttribute("disabled", ""); 71 | } 72 | } -------------------------------------------------------------------------------- /src/bundle/index.prism: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/bundle/others.ts: -------------------------------------------------------------------------------- 1 | /** Contains alternative runtime function implementations */ 2 | 3 | import { Component } from "./component"; 4 | import { oE } from "./render"; 5 | 6 | /** 7 | * Minified h render function + no svg support 8 | * @param tn 9 | * @param a 10 | * @param v 11 | * @param c 12 | */ 13 | function h(tn: string, a: Object | 0 = 0, v: Object | 0 = 0, ...c: Array): HTMLElement { 14 | // (e)lement 15 | const e = document.createElement(tn); 16 | if (a) { 17 | oE(a, ([k, v]) => { 18 | if (k in e) { 19 | e[k] = v; 20 | } else { 21 | e.setAttribute(k, v); 22 | } 23 | }); 24 | } 25 | if (v) { 26 | oE(v, ([eN, h]) => { 27 | e.addEventListener(eN, h); 28 | }); 29 | } 30 | e.append(...c); 31 | return e; 32 | } 33 | 34 | // Minimal version of create observable object for non datetime, array or nested object based envs 35 | function cOO( 36 | this: Component, 37 | m: any, 38 | d: Partial, 39 | ): T { 40 | return new Proxy(d, { 41 | // target, prop, receiver 42 | get: (t, p, r) => { 43 | // Work around for JSON.stringify thing 44 | if (p === "toJSON") { 45 | return JSON.stringify( 46 | Object.assign(t, 47 | Object.fromEntries(Object.keys(r).map(k => [k, r[k]])))) 48 | } 49 | // Get the respective (c)hunk on the mapping tree 50 | if (!m[p]) return; 51 | return t[p]; 52 | }, 53 | // target, prop, value, receiver 54 | set: (t, p, v) => { 55 | // Try call set handlers 56 | m[p]?.set?.call?.(this, v) 57 | return Reflect.set(t, p, v) 58 | }, 59 | has(_, p) { 60 | return p in m; 61 | }, 62 | ownKeys() { 63 | return Object.keys(m) 64 | }, 65 | getOwnPropertyDescriptor() { 66 | return { configurable: true, enumerable: true, writable: true } 67 | } 68 | }) as T; 69 | } 70 | 71 | function connectedCallback() { 72 | this.render(); 73 | this.connected?.(); 74 | this._isR = true; 75 | } 76 | 77 | function disconnectedCallback() { 78 | this.disconnected?.(); 79 | this._isR = false; 80 | } -------------------------------------------------------------------------------- /src/bundle/render.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create Comment 3 | * Used for maintaining consistency of splitting text from SSR 4 | */ 5 | export function cC(comment: string = ""): Comment { 6 | return document.createComment(comment); 7 | } 8 | 9 | // TODO temp 10 | const svgElems = new Set(["svg", "g", "line", "rect", "path", "ellipse", "circle"]); 11 | export const oE = (a, b) => Object.entries(a).forEach(b); 12 | 13 | /** 14 | * JSX minified render function 15 | * O's is used as a falsy value if the element does not have any attribute or events 16 | * @param tN (tagName) 17 | * @param a (attributes) 18 | * @param v (events) 19 | * @param c (children) 20 | */ 21 | export function h(tn: string, a: Object | 0 = 0, v: Object | 0 = 0, ...c: Array): HTMLElement | SVGElement { 22 | const isSvg = svgElems.has(tn); 23 | const e = isSvg ? document.createElementNS("http://www.w3.org/2000/svg", tn) : document.createElement(tn); 24 | if (a) { 25 | oE(a, ([k, v]) => { 26 | // TODO temp, haven't figured the weird characteristics of IDL attributes and SVG 27 | if (k in e && !isSvg) { 28 | e[k] = v; 29 | } else { 30 | e.setAttribute(k, v); 31 | } 32 | }); 33 | } 34 | if (v) { 35 | oE(v, ([eN, h]) => { 36 | e.addEventListener(eN, h); 37 | }); 38 | } 39 | e.append(...c); 40 | return e; 41 | } -------------------------------------------------------------------------------- /src/bundle/router.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from "./component"; 2 | import { h } from "./render"; 3 | 4 | /** 5 | * Router used under Prism. Singleton 6 | */ 7 | export class Router extends HTMLElement { 8 | // this router 9 | static t: HTMLElement; 10 | // Routes are injected by prism compiler 11 | static r: Array<[RegExp, string, string]>; 12 | 13 | // TODO not needed under context=client 14 | // loaded component 15 | static lc: string | null = null; 16 | // loaded layout 17 | static ll: string | null = null; 18 | 19 | connectedCallback() { 20 | Router.t = this; 21 | } 22 | 23 | /* 24 | Called when whole application is loaded in, if goTo creates a element that has not been registered then it cannot find its goto method 25 | */ 26 | static init() { 27 | window.onpopstate = () => { 28 | Router.goTo(document.location.pathname); 29 | } 30 | const fc = Router.t.firstElementChild; 31 | if (!fc) { 32 | Router.goTo(window.location.pathname, false) 33 | } else { 34 | if ((fc as Component)?.layout) { 35 | Router.ll = (fc as Component).tagName.toLowerCase(); 36 | Router.lc = ((fc as Component).firstElementChild as Component).tagName.toLowerCase(); 37 | } else { 38 | Router.lc = (fc as Component).tagName.toLowerCase(); 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * Used to bind anchor tags to ignore default behavior and do client side routing 45 | */ 46 | static bind(event: MouseEvent) { 47 | // New tab clicks 48 | if (event.ctrlKey) return; 49 | Router.goTo((event.currentTarget as HTMLElement).getAttribute("href")); 50 | event.preventDefault(); 51 | } 52 | 53 | /** 54 | * Only reason its async is for awaiting page load 55 | * TODO explain 56 | * @param url 57 | * @param ps push state 58 | */ 59 | static async goTo(url: string, ps = true) { 60 | let r = this.t; 61 | if (ps) history.pushState({}, "", url) 62 | // pattern component layout 63 | for (const [p, comp, lay] of this.r) { 64 | // match 65 | const m = url.match(p); 66 | if (m) { 67 | if (this.lc === comp) { 68 | if (lay) { 69 | await (r.firstElementChild.firstElementChild as Component).load?.(m.groups); 70 | } else { 71 | await (r.firstElementChild as Component).load?.(m.groups); 72 | } 73 | } else { 74 | // Container 75 | let c = r; 76 | if (lay) { 77 | if (Router.ll === lay) { 78 | c = r.firstElementChild as Component; 79 | } else { 80 | const newLayout = h(lay); 81 | r.firstElementChild ? r.firstElementChild.replaceWith(newLayout) : r.append(newLayout); 82 | c = newLayout as HTMLElement; 83 | } 84 | } 85 | Router.lc = comp; 86 | // New Component 87 | const nC = h(comp) as Component; 88 | await nC.load?.(m.groups); 89 | // Rendering the component is deferred until till adding to dom which is next line 90 | c.firstElementChild ? c.firstElementChild.replaceWith(nC) : c.append(nC); 91 | } 92 | return; 93 | } 94 | } 95 | throw Error(`No match found for ${url}`); 96 | } 97 | } 98 | 99 | window.customElements.define("router-component", Router); -------------------------------------------------------------------------------- /src/bundle/server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Escapes HTML on the server. From: https://stackoverflow.com/a/6234804/10048799 3 | */ 4 | export function escape(unsafe: string | number | boolean | Date): string { 5 | return unsafe 6 | .toString() 7 | .replace(/&/g, "&") 8 | .replace(//g, ">") 10 | .replace(/"/g, """) 11 | .replace(/'/g, "'"); 12 | } -------------------------------------------------------------------------------- /src/bundle/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/chef/README.md: -------------------------------------------------------------------------------- 1 | # Chef 2 | 3 | Chef is a parser and renderer currently supporting HTML, JavaScript (inc most of TypeScript) and CSS. Chef is built on ES6 classes so it is very simple to construct AST's. Chef is written in TypeScript so building ASTs should be (mostly) type safe. Chef is used under the Prism compiler but does not contain any specific Prism syntax or traits. 4 | 5 | ```js 6 | > Expression.fromString("a < 2 || b(c.d)"); 7 | > Expression { 8 | lhs: Expression { 9 | lhs: VariableReference { name: "a" }, 10 | operation: Operation.LessThan, 11 | rhs: Value { value: "4", type: Type.number } 12 | }, 13 | operation: Operation.LogOr, 14 | rhs: Expression { 15 | lhs: VariableReference { name: "b" }, 16 | operation: Operation.Call, 17 | rhs: VariableReference { name: "d", parent: VariableReference { name: "c" } } 18 | } 19 | } 20 | ``` 21 | 22 | As well as parsing, Chef can generate and serialize nodes: 23 | 24 | ```js 25 | > const expr = new Expression({ 26 | lhs: new Value(Type.number, 4), 27 | operation: Operation.Add, 28 | rhs: new VariableReference("x") 29 | }); 30 | 31 | > expr.render(); 32 | > "4 + x" 33 | ``` 34 | 35 | For more examples of code generation view [tests](https://github.com/kaleidawave/prism/blob/main/tests/chef/javascript/javascript.render.test.ts) 36 | 37 | #### HTML parsing integration: 38 | 39 | As JS and CSS can be imbedded in HTML: 40 | 41 | ```html 42 | 43 |

Some text

44 | 47 | 52 | 53 | ``` 54 | 55 | ```js 56 | > const [h1, script, style] = HTMLElement.fromString(...).children; 57 | > h1.children[0].text 58 | > "Some Text" 59 | > script.module 60 | > Module {...} 61 | > style.stylesheet 62 | > Stylesheet {...} 63 | ``` 64 | 65 | #### Speed comparisons: 66 | 67 | Through some rough testing Chef with `@babel/parsing` and `@babel/generator` Chef appears 2x times faster parsing and 6x faster at serializing the AST to a string. 68 | 69 | #### JS coverage & testing: 70 | 71 | Chef is not built alongside the spec so there are several things that are missed. Notably less used language features with statements, blocks, label statements, deep destructuring. But the tests have a good coverage over most used syntax. Just like Prism, Chef is experimental so getting it working is a greater priority than producing a production stage parser and renderer. I am hoping to using a external test set (such as [test262](https://github.com/tc39/test262)) for Chef to be more stable. 72 | 73 | #### Utilities: 74 | 75 | There are several non parsing based utilities included in Chef. 76 | 77 | - `javascript/utils/variables.ts` 78 | - Finding variables: returns list of variables in a construct 79 | - Aliasing variables: prefixes variables. e.g. `x` ➡ `this.data.x` 80 | - Replacing variables: replaces variables with any `IValue`. Effectively substitution 81 | - `javascript/utils/types.ts` : Resolves types by following references. Will return a map of properties that a type has 82 | - `javascript/utils/reverse.ts` : Attempts to create a function that resolve the variables used to construct the final value 83 | 84 | #### TypeScript 85 | 86 | Chef includes a lot of TypeScript syntax such as interfaces, type signatures (variables, parameters, function return types), decorators and generic parameters. Type constraints and some other syntax is not implemented. Chef also does not do any type checking. 87 | 88 | Compiling a node with the `settings.scriptLanguage` value will only output JS. And as TypeScript is a superset of JS removing TypeScript syntax returns valid JS code. 89 | 90 | #### Nested CSS rules: 91 | 92 | The CSS parser can process nested rules similar to scss (inc `&` for referencing the current selector): 93 | 94 | ```scss 95 | div { 96 | h1 { 97 | color: red; 98 | } 99 | 100 | &.class1 { 101 | h2 { 102 | text-align: center; 103 | } 104 | } 105 | } 106 | ``` 107 | 108 | Will be compiled to: 109 | 110 | ```css 111 | div h1 { 112 | color: red; 113 | } 114 | 115 | div.class1 h2 { 116 | text-align: center; 117 | } 118 | ``` 119 | 120 | (expanding nested rules is done as parse time) 121 | 122 | #### Dynamic URLs: 123 | 124 | Chef also can process URL syntax with variables similar to that of express: Minor addition that Prism uses 125 | 126 | ```js 127 | > const dynamicUrl = stringToDynamicUrl("/users/:userID"); 128 | // dynamicUrlToRegexPattern will produce a RegExpr that will only match on valid URLs and will return a groups of arguments 129 | > dynamicUrlToRegexPattern(dynamicUrl) 130 | > RegExpLiteral { 131 | expression: "^\\/users\\/(?(.+?))$" 132 | } 133 | ``` -------------------------------------------------------------------------------- /src/chef/abstract-asts.ts: -------------------------------------------------------------------------------- 1 | import { IFile } from "./filesystem"; 2 | import { IRenderable, IRenderOptions, IRenderSettings } from "./helpers"; 3 | 4 | export interface IModule extends IFile, IRenderable { 5 | statements: Array; 6 | 7 | render(settings?: Partial, options?: Partial): string; 8 | 9 | writeToFile(settings?: Partial): void; 10 | 11 | addExport(exportable: IFunctionDeclaration | any): void; 12 | addImport(importName: any, from: string): void; 13 | } 14 | 15 | export interface IFunctionDeclaration extends IRenderable { 16 | statements: Array; 17 | 18 | buildArgumentListFromArgumentsMap(argumentMap: Map): any; 19 | actualName: string | null; 20 | } -------------------------------------------------------------------------------- /src/chef/css/rule.ts: -------------------------------------------------------------------------------- 1 | import { ISelector, renderSelector, parseSelectorsFromTokens, prefixSelector } from "./selectors"; 2 | import { CSSValue, renderValue, parseStylingDeclarations } from "./value"; 3 | import { IRenderSettings, defaultRenderSettings, TokenReader } from "../helpers"; 4 | import { CSSToken } from "./css"; 5 | 6 | /** 7 | * Polyfill for .flatMap 8 | * @param arr 9 | * @param func 10 | */ 11 | function flatMap(arr: Array, func: (v: T) => any) { 12 | if (typeof arr.flatMap === "function") { 13 | return arr.flatMap(func); 14 | } else { 15 | return arr.map(func).reduce((acc, val) => acc.concat(val), []); 16 | } 17 | } 18 | 19 | export class Rule { 20 | 21 | constructor( 22 | public selectors: Array = [], 23 | public declarations: Map = new Map(), 24 | ) { } 25 | 26 | render(settings: IRenderSettings = defaultRenderSettings): string { 27 | // Early return if no declarations 28 | if (this.declarations.size === 0 && settings.minify) return ""; 29 | 30 | let acc = ""; 31 | // Render selectors: 32 | acc += this.selectors 33 | .map(selector => renderSelector(selector, settings)) 34 | .join(settings.minify ? "," : ", "); 35 | 36 | if (!settings.minify) acc += " "; 37 | acc += "{"; 38 | if (!settings.minify && this.declarations.size > 0) acc += "\n"; 39 | let renderedDeclarations = 0; 40 | for (const [key, value] of this.declarations) { 41 | // Indent: 42 | if (!settings.minify) acc += " ".repeat(4); 43 | acc += key; 44 | acc += ":"; 45 | if (!settings.minify) acc += " "; 46 | acc += renderValue(value, settings); 47 | // Last declaration can skip semi colon under minification 48 | if (!settings.minify || ++renderedDeclarations < this.declarations.size) acc += ";"; 49 | if (!settings.minify) acc += "\n"; 50 | } 51 | acc += "}"; 52 | return acc; 53 | } 54 | 55 | /** 56 | * Will flatten out nested rules at parse time 57 | */ 58 | static fromTokens(reader: TokenReader): Array { 59 | // Parsing selector 60 | const selectors: Array = parseSelectorsFromTokens(reader); 61 | reader.expectNext(CSSToken.OpenCurly); 62 | // Parsing rules 63 | const [declarations, nestedRules] = parseStylingDeclarations(reader); 64 | reader.expectNext(CSSToken.CloseCurly); 65 | const allRules: Array = []; 66 | // Will not add the rule if it used as a wrapper for nesting rules 67 | if (!(nestedRules.length > 0 && declarations.length === 0)) { 68 | allRules.push(new Rule(selectors, new Map(declarations))); 69 | } 70 | for (const nestedRule of nestedRules) { 71 | if (nestedRule.declarations.size === 0) continue; 72 | 73 | // Modify the nestedRule selectors to be prefixed under the main rule 74 | nestedRule.selectors = 75 | flatMap(nestedRule.selectors, selector1 => selectors.map(selector2 => prefixSelector(selector1, selector2))); 76 | 77 | allRules.push(nestedRule); 78 | } 79 | return allRules; 80 | } 81 | } -------------------------------------------------------------------------------- /src/chef/css/stylesheet.ts: -------------------------------------------------------------------------------- 1 | import { TokenReader, IRenderSettings, makeRenderSettings } from "../helpers"; 2 | import { CSSToken, stringToTokens } from "./css"; 3 | import { Rule } from "./rule"; 4 | import { readFile, writeFile, IFile } from "../filesystem"; 5 | import { AtRule, AtRuleFromTokens } from "./at-rules"; 6 | 7 | export class Stylesheet implements IFile { 8 | 9 | constructor( 10 | public filename: string, 11 | public rules: Array = [] 12 | ) { } 13 | 14 | static fromTokens(reader: TokenReader, filename: string): Stylesheet { 15 | const rules: Array = []; 16 | while (reader.current.type !== CSSToken.EOF) { 17 | if (reader.current.type === CSSToken.Comment) { 18 | reader.move(); 19 | continue; 20 | } 21 | if (reader.current.type === CSSToken.At) { 22 | rules.push(AtRuleFromTokens(reader)); 23 | } else { 24 | rules.push(...Rule.fromTokens(reader)); 25 | } 26 | } 27 | return new Stylesheet(filename, rules); 28 | } 29 | 30 | render(partialSettings: Partial = {}): string { 31 | const settings = makeRenderSettings(partialSettings); 32 | let acc = ""; 33 | for (let i = 0; i < this.rules.length; i++) { 34 | acc += this.rules[i].render(settings); 35 | if (!settings.minify && i + 1 < this.rules.length) acc += "\n"; 36 | } 37 | return acc; 38 | } 39 | 40 | static fromString(content: string, filename: string = "anom.css", columnOffset?: number, lineOffset?: number): Stylesheet { 41 | const reader = stringToTokens(content, { 42 | file: filename, 43 | columnOffset, 44 | lineOffset 45 | }); 46 | 47 | const styleSheet = Stylesheet.fromTokens(reader, filename); 48 | reader.expect(CSSToken.EOF); 49 | return styleSheet; 50 | } 51 | 52 | static fromFile(filename: string): Stylesheet { 53 | return Stylesheet.fromString(readFile(filename), filename); 54 | } 55 | 56 | combine(stylesheet2: Stylesheet): void { 57 | this.rules.push(...stylesheet2.rules); 58 | } 59 | 60 | writeToFile(settings: Partial = {}, filename?: string) { 61 | writeFile(filename ?? this.filename!, this.render(settings)); 62 | } 63 | } -------------------------------------------------------------------------------- /src/chef/css/value.ts: -------------------------------------------------------------------------------- 1 | import { TokenReader, IRenderSettings, defaultRenderSettings } from "../helpers"; 2 | import { CSSToken, stringToTokens } from "./css"; 3 | import { Rule } from "./rule"; 4 | 5 | /* CSS Values: */ 6 | interface IFunctionCall { 7 | name: string, 8 | arguments: CSSValue 9 | } 10 | 11 | interface IValue { 12 | value: string, 13 | quotationWrapped?: boolean, 14 | unit?: string, 15 | } 16 | 17 | export type CSSValue = Array; 18 | 19 | export function parseValue(reader: TokenReader): CSSValue { 20 | // TODO "," | "/" are temp 21 | let values: Array = []; 22 | while (reader.current) { 23 | if (reader.peek()?.type === CSSToken.OpenBracket) { 24 | const name = reader.current.value!; 25 | reader.move(2); 26 | const args = parseValue(reader); 27 | reader.expectNext(CSSToken.CloseBracket); 28 | values.push({ name, arguments: args }); 29 | } else { 30 | if ( 31 | reader.current.type === CSSToken.NumberLiteral 32 | && reader.peek()?.type === CSSToken.Identifier 33 | && (reader.current.column + reader.current.value!.length) === reader.peek()?.column 34 | ) { 35 | const number = reader.current.value!; 36 | reader.move(); 37 | const unit = reader.current.value!; 38 | values.push({ value: number, unit }); 39 | reader.move(); 40 | } else if (reader.current.type === CSSToken.Hash) { 41 | reader.move(); 42 | values.push({ value: "#" + reader.current.value! }); 43 | reader.move(); 44 | } else if ( 45 | reader.current.type === CSSToken.NumberLiteral 46 | || reader.current.type === CSSToken.StringLiteral 47 | || reader.current.type === CSSToken.Identifier 48 | ) { 49 | const quotationWrapped = reader.current.type === CSSToken.StringLiteral; 50 | values.push({ value: reader.current.value!, quotationWrapped }); 51 | reader.move(); 52 | } else { 53 | reader.throwExpect("Expected value"); 54 | } 55 | } 56 | if ([CSSToken.CloseBracket, CSSToken.SemiColon, CSSToken.CloseCurly].includes(reader.current.type)) { 57 | break; 58 | } 59 | if (reader.current.type === CSSToken.Comma) { 60 | values.push(","); 61 | reader.move(); 62 | } else if (reader.current.type === CSSToken.ForwardSlash) { 63 | values.push("/"); 64 | reader.move(); 65 | } 66 | } 67 | return values; 68 | } 69 | 70 | // [key: value] 71 | export type Declaration = [string, CSSValue]; 72 | 73 | export function parseSingleDeclaration(reader: TokenReader): Declaration { 74 | reader.expect(CSSToken.Identifier); 75 | const key = reader.current.value!; 76 | reader.move(); 77 | reader.expectNext(CSSToken.Colon); 78 | const value = parseValue(reader); 79 | return [key, value]; 80 | } 81 | 82 | /** 83 | * @param reader 84 | * @param parseNestedRules 85 | */ 86 | export function parseStylingDeclarations(reader: TokenReader, parseNestedRules = true): [Array, Array] { 87 | const declarations: Array = []; 88 | const rules: Array = []; 89 | while (reader.current.type !== CSSToken.CloseCurly && reader.current.type !== CSSToken.EOF) { 90 | if (reader.current.type === CSSToken.Comment) { 91 | reader.move(); 92 | continue; 93 | } 94 | // Test whether it is a declaration or a nested rule 95 | const tokensToStopAt = new Set([CSSToken.OpenCurly, CSSToken.CloseCurly, CSSToken.SemiColon, CSSToken.EOF]) 96 | const [end] = reader.run((tokenType) => tokensToStopAt.has(tokenType)); 97 | 98 | // Parse it as a value: 99 | if (end === CSSToken.SemiColon || end === CSSToken.CloseCurly || end === CSSToken.EOF || !parseNestedRules) { 100 | const declaration = parseSingleDeclaration(reader); 101 | declarations.push(declaration); 102 | if (reader.current.type == CSSToken.SemiColon) { 103 | reader.move(); 104 | } else { 105 | break; // Last declaration can miss out semi colon so break 106 | } 107 | } 108 | // Parse it as a sub rule 109 | else { 110 | rules.push(...Rule.fromTokens(reader)); 111 | } 112 | } 113 | return [declarations, rules] 114 | } 115 | 116 | export function parseStylingDeclarationsFromString(string: string) { 117 | const reader = stringToTokens(string); 118 | const [declarations] = parseStylingDeclarations(reader, false); 119 | reader.expect(CSSToken.EOF); 120 | return declarations; 121 | } 122 | 123 | /** 124 | * Renders out a CSSValue 125 | * @param value 126 | * @param settings 127 | */ 128 | export function renderValue( 129 | value: CSSValue | IFunctionCall | IValue | "," | "/", 130 | settings: IRenderSettings = defaultRenderSettings 131 | ): string { 132 | if (typeof value === "string") { 133 | return value; 134 | } else if ("name" in value && "arguments" in value) { 135 | return `${value.name}(${renderValue(value.arguments, settings)})`; 136 | } else if ("value" in value) { 137 | if (value.quotationWrapped) return `"${value.value}"`; 138 | return value.value + (value.unit ?? ""); 139 | } else { 140 | let acc = ""; 141 | for (let i = 0; i < value.length; i++) { 142 | acc += renderValue(value[i], settings); 143 | if (!(value[i] === "," || value[i + 1] === ",") && value[i + 1]) { 144 | acc += " "; 145 | } 146 | } 147 | return acc; 148 | } 149 | } -------------------------------------------------------------------------------- /src/chef/dynamic-url.ts: -------------------------------------------------------------------------------- 1 | import { RegExpLiteral } from "./javascript/components/value/regex"; 2 | 3 | /* 4 | * /pages/:pageID 5 | * Slug = ^^^^^^^ 6 | */ 7 | interface Slug { 8 | name: string 9 | } 10 | 11 | export type DynamicUrl = Array; 12 | 13 | /** 14 | * Takes a dynamic url and forms a string representation of it in express routing format 15 | * @example TODO 16 | */ 17 | export function dynamicUrlToString(url: DynamicUrl): string { 18 | if (url.length === 0) { 19 | return "/" 20 | } 21 | let acc = ""; 22 | for (const part of url) { 23 | acc += "/"; 24 | if (typeof part === "string") { 25 | acc += part; 26 | } else { 27 | acc += ":" + part.name; 28 | } 29 | } 30 | return acc; 31 | } 32 | 33 | export function stringToDynamicUrl(url: string): DynamicUrl { 34 | const dynamicUrl: DynamicUrl = []; 35 | const parts = url.split(/\//g).filter(Boolean); 36 | for (const part of parts) { 37 | if (part[0] === ":") { 38 | dynamicUrl.push({ name: part.slice(1) }) 39 | } else { 40 | dynamicUrl.push(part); 41 | } 42 | } 43 | return dynamicUrl; 44 | } 45 | 46 | /** 47 | * Creates a regular expression with a pattern that matches urls 48 | * @param route A express style url pattern e.g. "/posts/:postID" 49 | */ 50 | export function dynamicUrlToRegexPattern(url: DynamicUrl): RegExpLiteral { 51 | if (url.length === 0) { 52 | return new RegExpLiteral("^\\/$") 53 | } 54 | let regexPattern = "^"; 55 | for (const part of url) { 56 | regexPattern += "\\/"; 57 | if (typeof part === "string") { 58 | regexPattern += part; // TODO escape characters like . 59 | } else { 60 | // Add a name capture group 61 | regexPattern += `(?<${part.name}>([^\/]+?))`; 62 | } 63 | } 64 | regexPattern += "$"; 65 | return new RegExpLiteral(regexPattern); 66 | } -------------------------------------------------------------------------------- /src/chef/filesystem.ts: -------------------------------------------------------------------------------- 1 | import { IRenderSettings } from "./helpers"; 2 | 3 | export interface IFile { 4 | filename: string; 5 | writeToFile(settings: Partial): void; 6 | } 7 | 8 | /** 9 | * Given a filename returns the result 10 | */ 11 | export type fsReadCallback = (filename: string) => string; 12 | 13 | let __fileSystemReadCallback: fsReadCallback | null = null; 14 | export function registerFSReadCallback(cb: fsReadCallback | null) { 15 | __fileSystemReadCallback = cb; 16 | } 17 | 18 | export type fsWriteCallback = (filename: string, content: string) => void; 19 | 20 | let __fileSystemWriteCallback: fsWriteCallback | null = null; 21 | export function registerFSWriteCallback(cb: fsWriteCallback | null) { 22 | __fileSystemWriteCallback = cb; 23 | } 24 | 25 | /** 26 | * TODO if Deno 27 | * @param filename 28 | */ 29 | export function readFile(filename: string): string { 30 | if (__fileSystemReadCallback) { 31 | return __fileSystemReadCallback(filename); 32 | } else { 33 | throw Error("Cannot read file without fs callback"); 34 | } 35 | } 36 | 37 | export function writeFile(filename: string, content: string): void { 38 | if (__fileSystemWriteCallback) { 39 | __fileSystemWriteCallback(filename, content); 40 | } else { 41 | throw Error("Cannot write file without fs callback"); 42 | } 43 | } -------------------------------------------------------------------------------- /src/chef/javascript/components/constructs/block.ts: -------------------------------------------------------------------------------- 1 | import { StatementTypes, parseStatement } from "../statements/statement"; 2 | import { TokenReader, IRenderSettings, defaultRenderSettings } from "../../../helpers"; 3 | import { JSToken } from "../../javascript"; 4 | import { ImportStatement, ExportStatement } from "../statements/import-export"; 5 | import { ClassDeclaration } from "./class"; 6 | import { FunctionDeclaration } from "./function"; 7 | 8 | const endingSwitchBlockTokens = new Set([JSToken.Default, JSToken.Case, JSToken.CloseCurly]) 9 | 10 | /** 11 | * Parses blocks from statements for, if, function etc 12 | * @param inSwitch If parsing a switch statement 13 | */ 14 | export function parseBlock(reader: TokenReader, inSwitch = false): Array { 15 | if (reader.current.type === JSToken.OpenCurly || inSwitch) { 16 | if (reader.current.type === JSToken.OpenCurly) reader.move(); 17 | const statements: Array = []; 18 | while (reader.current.type as JSToken !== JSToken.CloseCurly) { 19 | statements.push(parseStatement(reader)); 20 | if (reader.current.type as JSToken === JSToken.SemiColon) reader.move(); 21 | if (inSwitch && endingSwitchBlockTokens.has(reader.current.type)) return statements; 22 | } 23 | reader.move(); 24 | return statements; 25 | } else { 26 | // If using shorthand block (without {}) then just parse in a single expression 27 | const statement = parseStatement(reader); 28 | if (reader.current.type as JSToken === JSToken.SemiColon) reader.move(); 29 | return [statement]; 30 | } 31 | } 32 | 33 | /** 34 | * Renders out a "block" / list of statements 35 | * Handles indentation and pretty printing 36 | * DOES NOT INCLUDE SURROUNDING CURLY BRACES 37 | * @param indent whether to indent block members 38 | */ 39 | export function renderBlock(block: Array, settings: IRenderSettings = defaultRenderSettings, indent = true) { 40 | let acc = ""; 41 | for (let i = 0; i < block.length; i++) { 42 | const statement = block[i]; 43 | if (!statement) continue; // Not sure the case when the statement is falsy but handles this 44 | const serializedStatement = statement.render(settings); 45 | if (!serializedStatement) continue; // Handles "" from comment render if settings.comments is false 46 | 47 | if (settings.minify) { 48 | acc += serializedStatement; 49 | if (i + 1 < block.length) { 50 | // Minified statements have issues where cannot detect 51 | acc += ";"; 52 | } 53 | } else if (indent) { 54 | acc += ("\n" + serializedStatement).replace(/\n/g, "\n" + " ".repeat(settings.indent)); 55 | } else { 56 | acc += serializedStatement; 57 | 58 | if (i + 1 < block.length) { 59 | acc += "\n"; 60 | 61 | // Add padding around imports, classes and functions 62 | if (statement instanceof ImportStatement) { 63 | // If next statement is import statement 64 | if (block[i + 1] instanceof ImportStatement) continue; 65 | acc += "\n"; 66 | } else if (statement instanceof ClassDeclaration || statement instanceof FunctionDeclaration) { 67 | acc += "\n"; 68 | } else if (statement instanceof ExportStatement && ( 69 | statement.exported instanceof ClassDeclaration || 70 | statement.exported instanceof FunctionDeclaration) 71 | ) { 72 | acc += "\n"; 73 | } 74 | } 75 | } 76 | } 77 | if (block.length > 0 && !settings.minify && indent) acc += "\n"; 78 | return acc; 79 | } -------------------------------------------------------------------------------- /src/chef/javascript/components/statements/comments.ts: -------------------------------------------------------------------------------- 1 | import { IRenderSettings, ScriptLanguages, defaultRenderSettings } from "../../../helpers"; 2 | 3 | export class Comment { 4 | 5 | constructor( 6 | public comment: string, 7 | public multiline: boolean = false 8 | ) { } 9 | 10 | render(settings: IRenderSettings = defaultRenderSettings): string { 11 | // TODO source map comments should be kept during minification 12 | if (settings.minify || !settings.comments) { 13 | return ""; 14 | } 15 | 16 | if (settings.comments === "docstring") { 17 | if (!this.multiline || this.comment[0] !== "*") { 18 | return ""; 19 | } 20 | } 21 | if (settings.comments === "info") { 22 | if (this.comment.startsWith("TODO")) { 23 | return ""; 24 | } 25 | } 26 | 27 | // TODO "@ts-check" can be used in .js files 28 | if (this.comment.startsWith("@ts-") && settings.scriptLanguage !== ScriptLanguages.Typescript) { 29 | return ""; 30 | } 31 | 32 | if (this.multiline) { 33 | return `/*${this.comment} */`; 34 | } else { 35 | return `// ${this.comment}`; 36 | } 37 | } 38 | } 39 | 40 | interface IDocString { 41 | text: string, 42 | remarks: string, 43 | public: string, 44 | see: string, 45 | } 46 | 47 | /** 48 | * Generates a documentation comment in the tsdoc standard 49 | * #inception 50 | * @see [tsdoc](https://github.com/microsoft/tsdoc) 51 | */ 52 | export function GenerateDocString(content: Partial): Comment { 53 | let comment = "*\n"; 54 | for (const [name, value] of Object.entries(content)) { 55 | if (name === "text") { 56 | comment += ` * ${value}\n`; 57 | } else { 58 | comment += ` * @${name} ${value}\n`; 59 | } 60 | } 61 | return new Comment(comment, true); 62 | } -------------------------------------------------------------------------------- /src/chef/javascript/components/statements/for.ts: -------------------------------------------------------------------------------- 1 | import { JSToken, stringToTokens } from "../../javascript"; 2 | import { StatementTypes } from "./statement"; 3 | import { VariableDeclaration, VariableContext } from "../statements/variable"; 4 | import { Expression, Operation } from "../value/expression"; 5 | import { TokenReader, IRenderSettings, defaultRenderSettings, IRenderable } from "../../../helpers"; 6 | import { ValueTypes } from "../value/value"; 7 | import { renderBlock, parseBlock } from "../constructs/block"; 8 | 9 | // These tokens refer to object destructuring 10 | const openers = new Set([JSToken.OpenSquare, JSToken.OpenCurly]) 11 | const closers = new Set([JSToken.CloseSquare, JSToken.CloseCurly]) 12 | 13 | /** 14 | * @example `let x = 2; x < 5; x++;` 15 | */ 16 | export class ForStatementExpression implements IRenderable { 17 | 18 | constructor( 19 | public initializer: VariableDeclaration | null, 20 | public condition: Expression | null, 21 | public finalExpression: Expression | null 22 | ) { } 23 | 24 | static fromTokens(reader: TokenReader): ForStatementExpression { 25 | let condition: Expression | null = null, 26 | initialization: VariableDeclaration | null = null, 27 | finalExpression: Expression | null = null; 28 | 29 | if (reader.current.type !== JSToken.SemiColon) { 30 | initialization = VariableDeclaration.fromTokens(reader); 31 | if (!initialization.value) { 32 | throw Error("Expected variable in for loop to have initial value"); 33 | } 34 | reader.move(-1); 35 | } 36 | reader.expectNext(JSToken.SemiColon); 37 | if (reader.current.type !== JSToken.SemiColon) { 38 | condition = Expression.fromTokens(reader) as Expression; 39 | } 40 | reader.expectNext(JSToken.SemiColon); 41 | if (reader.current.type !== JSToken.CloseBracket) { 42 | finalExpression = Expression.fromTokens(reader) as Expression; 43 | } 44 | return new ForStatementExpression(initialization, condition, finalExpression); 45 | } 46 | 47 | render(settings: IRenderSettings = defaultRenderSettings): string { 48 | let acc = ""; 49 | if (this.initializer) acc += this.initializer.render(settings); 50 | acc += settings.minify ? ";" : "; "; 51 | if (this.condition) acc += this.condition.render(settings); 52 | acc += settings.minify ? ";" : "; "; 53 | if (this.finalExpression) acc += this.finalExpression.render(settings); 54 | return acc; 55 | } 56 | } 57 | 58 | const validIteratorExpressions = new Set([JSToken.Of, JSToken.In]); 59 | 60 | export type ForLoopExpression = ForIteratorExpression | ForStatementExpression; 61 | 62 | /** 63 | * Parses: `const|let|var ... of ...` 64 | * @example `const elem of elements` 65 | */ 66 | export class ForIteratorExpression { 67 | constructor( 68 | public variable: VariableDeclaration, // TODO allow string for utility 69 | public operation: Operation.Of | Operation.In, 70 | public subject: ValueTypes, 71 | ) { } 72 | 73 | static fromTokens(reader: TokenReader): ForIteratorExpression { 74 | const variable = VariableDeclaration.fromTokens(reader, { context: VariableContext.For }); 75 | if (!validIteratorExpressions.has(reader.current.type)) { 76 | reader.throwExpect(`Expected "of" or "in" expression in for statement`); 77 | } 78 | const operation = reader.current.type === JSToken.Of ? Operation.Of : Operation.In; 79 | reader.move(); 80 | const subject = Expression.fromTokens(reader); 81 | return new ForIteratorExpression(variable, operation, subject); 82 | } 83 | 84 | render(settings: IRenderSettings = defaultRenderSettings): string { 85 | let acc = ""; 86 | acc += this.variable.render(settings); 87 | acc += this.operation === Operation.Of ? " of " : " in "; 88 | acc += this.subject.render(settings); 89 | return acc; 90 | } 91 | } 92 | 93 | export class ForStatement implements IRenderable { 94 | 95 | constructor( 96 | public expression: ForLoopExpression, 97 | public statements: Array 98 | ) { } 99 | 100 | render(settings: IRenderSettings = defaultRenderSettings): string { 101 | let acc = "for ("; 102 | acc += this.expression.render(settings); 103 | acc += ") {"; 104 | acc += renderBlock(this.statements, settings); 105 | acc += "}"; 106 | return acc; 107 | } 108 | 109 | static fromTokens(reader: TokenReader): ForStatement { 110 | reader.expectNext(JSToken.For); 111 | reader.expectNext(JSToken.OpenBracket); 112 | const expression: ForLoopExpression = ForStatement.parseForParameterFromTokens(reader); 113 | reader.expectNext(JSToken.CloseBracket); 114 | const statements = parseBlock(reader); 115 | return new ForStatement(expression, statements); 116 | } 117 | 118 | static parseForParameter(string: string): ForLoopExpression { 119 | const reader = stringToTokens(string); 120 | const expression = ForStatement.parseForParameterFromTokens(reader); 121 | reader.expect(JSToken.EOF); 122 | return expression; 123 | } 124 | 125 | static parseForParameterFromTokens(reader: TokenReader): ForLoopExpression { 126 | // Backup is used to because run has to start at a certain position 127 | let backup = false; 128 | if (new Set([JSToken.Const, JSToken.Let, JSToken.Var]).has(reader.current.type)) { 129 | reader.move(); 130 | backup = true; 131 | } 132 | let bracketCount = 0; 133 | // Finds what is after the variable declaration 134 | const [operator] = reader.run((token) => { 135 | if (openers.has(token)) bracketCount++; 136 | else if (closers.has(token)) bracketCount--; 137 | if (bracketCount === 0) return true; 138 | else return false; 139 | }, true); 140 | 141 | if (backup) reader.move(-1); 142 | 143 | // If "of" or "in" do a iterator expression 144 | if (validIteratorExpressions.has(operator)) { 145 | return ForIteratorExpression.fromTokens(reader); 146 | } else { 147 | return ForStatementExpression.fromTokens(reader); 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /src/chef/javascript/components/statements/if.ts: -------------------------------------------------------------------------------- 1 | import { TokenReader, IRenderSettings, defaultRenderSettings, IRenderable } from "../../../helpers"; 2 | import { JSToken } from "../../javascript"; 3 | import { StatementTypes } from "./statement"; 4 | import { Expression } from "../value/expression"; 5 | import { ValueTypes } from "../value/value"; 6 | import { parseBlock, renderBlock } from "../constructs/block"; 7 | 8 | export class IfStatement implements IRenderable { 9 | constructor( 10 | public condition: ValueTypes, 11 | public statements: Array = [], 12 | public consequent: ElseStatement | null = null 13 | ) { } 14 | 15 | render(settings: IRenderSettings = defaultRenderSettings): string { 16 | let acc = "if"; 17 | acc += settings.minify ? "(" : " ("; 18 | acc += this.condition.render(settings); 19 | acc += settings.minify ? "){" : ") {"; 20 | // TODO if statements.length === 1 then can drop curly braces 21 | acc += renderBlock(this.statements, settings); 22 | acc += "}"; 23 | if (!this.consequent) { 24 | return acc; 25 | } else { 26 | if (!settings.minify) acc += " "; 27 | acc += this.consequent.render(settings); 28 | return acc; 29 | } 30 | } 31 | 32 | static fromTokens(reader: TokenReader): IfStatement { 33 | reader.expectNext(JSToken.If); 34 | reader.expectNext(JSToken.OpenBracket); 35 | // Parse condition 36 | const condition = Expression.fromTokens(reader); 37 | reader.expectNext(JSToken.CloseBracket); 38 | // Parse statements 39 | const statements = parseBlock(reader); 40 | let consequent: ElseStatement | null = null; 41 | if (reader.current.type === JSToken.Else) { 42 | consequent = ElseStatement.fromTokens(reader); 43 | } 44 | return new IfStatement(condition, statements, consequent); 45 | } 46 | } 47 | 48 | export class ElseStatement implements IRenderable { 49 | constructor( 50 | public condition: ValueTypes | null = null, 51 | public statements: Array, 52 | public consequent: ElseStatement | null = null 53 | ) {} 54 | 55 | render(settings: IRenderSettings = defaultRenderSettings): string { 56 | let acc = "else"; 57 | if (this.condition !== null || typeof this.condition === "undefined") { 58 | acc += settings.minify ? " if(" : " if ("; 59 | acc += this.condition.render(settings); 60 | acc += settings.minify ? "){" : ") {"; 61 | } else { 62 | acc += settings.minify ? "{" : " {"; 63 | } 64 | acc += renderBlock(this.statements, settings); 65 | acc += "}"; 66 | if (!this.consequent) { 67 | return acc; 68 | } else { 69 | if (!settings.minify) acc += " "; 70 | acc += this.consequent.render(settings); 71 | return acc; 72 | } 73 | } 74 | 75 | static fromTokens(reader: TokenReader): ElseStatement { 76 | reader.expectNext(JSToken.Else); 77 | let condition: ValueTypes | null = null; 78 | if (reader.current.type === JSToken.If) { 79 | reader.move(); 80 | reader.expectNext(JSToken.OpenBracket); 81 | condition = Expression.fromTokens(reader); 82 | reader.expectNext(JSToken.CloseBracket); 83 | } 84 | const statements = parseBlock(reader); 85 | let consequent: ElseStatement | null = null; 86 | if (reader.current.type === JSToken.Else) { 87 | consequent = ElseStatement.fromTokens(reader); 88 | } 89 | return new ElseStatement(condition, statements, consequent); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/chef/javascript/components/statements/switch.ts: -------------------------------------------------------------------------------- 1 | import { StatementTypes } from "./statement"; 2 | import { ValueTypes } from "../value/value"; 3 | import { IRenderSettings, TokenReader, defaultRenderSettings, IRenderable } from "../../../helpers"; 4 | import { JSToken } from "../../javascript"; 5 | import { Expression } from "../value/expression"; 6 | import { parseBlock, renderBlock } from "../constructs/block"; 7 | 8 | export class SwitchStatement implements IRenderable { 9 | constructor( 10 | public expression: ValueTypes, 11 | public cases: Array<[ValueTypes | null, Array]>, // TODO should LHS be of type IValue ??? 12 | ) { } 13 | 14 | render(settings: IRenderSettings = defaultRenderSettings): string { 15 | let acc = "switch"; 16 | acc += settings.minify ? "(" : " ("; 17 | acc += this.expression.render(settings); 18 | acc += settings.minify ? "){" : ") {"; 19 | let block = ""; 20 | for (const [case_, statements] of this.cases) { 21 | if (case_) { 22 | block += "case "; 23 | block += case_.render(settings); 24 | } else { 25 | block += "default"; 26 | } 27 | block += ":"; 28 | block += renderBlock(statements, settings); 29 | if (settings.minify) block += ";"; 30 | } 31 | if (settings.minify) { 32 | acc += block; 33 | } else { 34 | acc += ("\n" + block).replace(/\n/g, "\n" + " ".repeat(settings.indent)); 35 | // After indentation, acc contains a leading " ".repeat(settings.indent); 36 | acc = acc.substring(0, acc.length - settings.indent); 37 | } 38 | acc += "}"; 39 | return acc; 40 | } 41 | 42 | get defaultCase(): StatementTypes[] { 43 | return this.cases.find(([condition]) => condition === null)?.[1] || []; 44 | } 45 | 46 | static fromTokens(reader: TokenReader): SwitchStatement { 47 | reader.expectNext(JSToken.Switch); 48 | reader.expectNext(JSToken.OpenBracket); 49 | const expr = Expression.fromTokens(reader); 50 | reader.expectNext(JSToken.CloseBracket); 51 | reader.expectNext(JSToken.OpenCurly); 52 | const cases: Array<[ValueTypes | null, Array]> = []; 53 | while (reader.current.type !== JSToken.CloseCurly) { 54 | if (reader.current.type === JSToken.Case) { 55 | reader.move(); 56 | const condition = Expression.fromTokens(reader); 57 | reader.expectNext(JSToken.Colon); 58 | const statements = parseBlock(reader, true); 59 | cases.push([condition, statements]); 60 | } else if (reader.current.type === JSToken.Default) { 61 | reader.move(); 62 | reader.expectNext(JSToken.Colon); 63 | const statements = parseBlock(reader, true); 64 | cases.push([null, statements]); 65 | } else { 66 | reader.throwExpect("Expected case or default in switch statement"); 67 | } 68 | } 69 | reader.move(); 70 | return new SwitchStatement(expr, cases); 71 | } 72 | } -------------------------------------------------------------------------------- /src/chef/javascript/components/statements/try-catch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains declarations for "throw" and "try...catch...finally" 3 | */ 4 | 5 | import { StatementTypes } from "./statement"; 6 | import { TokenReader, IRenderSettings, defaultRenderSettings, IRenderable } from "../../../helpers"; 7 | import { JSToken } from "../../javascript"; 8 | import { parseBlock, renderBlock } from "../constructs/block"; 9 | import { ValueTypes } from "../value/value"; 10 | import { Expression } from "../value/expression"; 11 | import { VariableDeclaration, VariableContext } from "../statements/variable"; 12 | 13 | export class ThrowStatement implements IRenderable { 14 | 15 | constructor( 16 | public value: ValueTypes 17 | ) {} 18 | 19 | render(settings: IRenderSettings = defaultRenderSettings): string { 20 | return `throw ${this.value.render(settings)}`; 21 | } 22 | 23 | static fromTokens(reader: TokenReader) { 24 | reader.expectNext(JSToken.Throw); 25 | const valueToThrow = Expression.fromTokens(reader); 26 | if (reader.current.type === JSToken.SemiColon) reader.move(); 27 | return new ThrowStatement(valueToThrow); 28 | } 29 | } 30 | 31 | export class TryBlock implements IRenderable { 32 | 33 | constructor ( 34 | public statements: Array, 35 | public catchBlock: CatchBlock | null = null, 36 | public finallyBlock: FinallyBlock | null = null, 37 | ) {} 38 | 39 | render(settings: IRenderSettings = defaultRenderSettings): string { 40 | let acc = "try"; 41 | if (!settings.minify) acc += " "; 42 | acc += "{"; 43 | acc += renderBlock(this.statements, settings); 44 | acc += "}"; 45 | if (!settings.minify) acc += " "; 46 | if (this.catchBlock) acc += this.catchBlock.render(settings); 47 | if (this.finallyBlock) acc += this.finallyBlock!.render(settings); 48 | return acc; 49 | } 50 | 51 | static fromTokens(reader: TokenReader) { 52 | reader.expectNext(JSToken.Try); 53 | reader.expect(JSToken.OpenCurly); 54 | const statements = parseBlock(reader); 55 | let catchBlock: CatchBlock | null = null; 56 | if (reader.current.type === JSToken.Catch) { 57 | catchBlock = CatchBlock.fromTokens(reader); 58 | } 59 | let finallyBlock: FinallyBlock | null = null; 60 | if (reader.current.type === JSToken.Finally) { 61 | finallyBlock = FinallyBlock.fromTokens(reader); 62 | } 63 | if (!catchBlock && !finallyBlock) { 64 | reader.throwExpect("Expected either catch or finally block after try block"); 65 | } 66 | return new TryBlock(statements, catchBlock, finallyBlock); 67 | } 68 | } 69 | 70 | export class CatchBlock implements IRenderable { 71 | 72 | constructor ( 73 | public errorVariable: VariableDeclaration | null, 74 | public statements: Array, 75 | ) { 76 | if (errorVariable) errorVariable.context = VariableContext.Parameter; 77 | } 78 | 79 | render(settings: IRenderSettings = defaultRenderSettings): string { 80 | let acc = "catch"; 81 | if (!settings.minify) acc += " "; 82 | if (this.errorVariable) { 83 | acc += `(${this.errorVariable.render(settings)})` 84 | } 85 | if (!settings.minify) acc += " "; 86 | acc += "{"; 87 | acc += renderBlock(this.statements,settings); 88 | acc += "}"; 89 | return acc; 90 | } 91 | 92 | static fromTokens(reader: TokenReader): CatchBlock { 93 | reader.expectNext(JSToken.Catch); 94 | let errorVariable: VariableDeclaration | null = null; 95 | if (reader.current.type === JSToken.OpenBracket) { 96 | reader.move(); 97 | errorVariable = VariableDeclaration.fromTokens(reader, {context: VariableContext.Parameter}); 98 | reader.expectNext(JSToken.CloseBracket); 99 | } 100 | reader.expect(JSToken.OpenCurly); 101 | const statements = parseBlock(reader); 102 | return new CatchBlock(errorVariable, statements); 103 | } 104 | } 105 | 106 | export class FinallyBlock implements IRenderable { 107 | 108 | constructor ( 109 | public statements: Array, 110 | ) {} 111 | 112 | render(settings: IRenderSettings = defaultRenderSettings): string { 113 | let acc = "finally"; 114 | if (!settings.minify) acc += " "; 115 | acc += "{"; 116 | acc += renderBlock(this.statements, settings); 117 | acc += "}"; 118 | return acc; 119 | } 120 | 121 | static fromTokens(reader: TokenReader): FinallyBlock { 122 | reader.expectNext(JSToken.Finally); 123 | reader.expect(JSToken.OpenCurly); 124 | const statements = parseBlock(reader); 125 | return new FinallyBlock(statements); 126 | } 127 | } -------------------------------------------------------------------------------- /src/chef/javascript/components/statements/while.ts: -------------------------------------------------------------------------------- 1 | import { StatementTypes } from "./statement"; 2 | import { TokenReader, IRenderSettings, defaultRenderSettings, IRenderable } from "../../../helpers"; 3 | import { JSToken } from "../../javascript"; 4 | import { ValueTypes } from "../value/value"; 5 | import { Expression } from "../value/expression"; 6 | import { parseBlock, renderBlock } from "../constructs/block"; 7 | 8 | export class WhileStatement implements IRenderable { 9 | 10 | constructor( 11 | public expression: ValueTypes, 12 | public statements: Array 13 | ) { } 14 | 15 | render(settings: IRenderSettings = defaultRenderSettings): string { 16 | let acc = "while"; 17 | if (!settings.minify) acc += " "; 18 | acc += "("; 19 | acc += this.expression.render(settings); 20 | acc += ")"; 21 | if (!settings.minify) acc += " "; 22 | acc += "{"; 23 | acc += renderBlock(this.statements, settings); 24 | acc += "}"; 25 | return acc; 26 | } 27 | 28 | static fromTokens(reader: TokenReader): WhileStatement { 29 | reader.expectNext(JSToken.While); 30 | reader.expectNext(JSToken.OpenBracket); 31 | const condition = Expression.fromTokens(reader); 32 | reader.expectNext(JSToken.CloseBracket); 33 | const statements = parseBlock(reader); 34 | return new WhileStatement(condition, statements) 35 | } 36 | } 37 | 38 | export class DoWhileStatement implements IRenderable { 39 | 40 | constructor( 41 | public expression: ValueTypes, 42 | public statements: Array 43 | ) { } 44 | 45 | render(settings: IRenderSettings = defaultRenderSettings): string { 46 | let acc = "do"; 47 | if (!settings.minify) acc += " "; 48 | acc += "{"; 49 | acc += renderBlock(this.statements, settings); 50 | acc += "}"; 51 | if (!settings.minify) acc += " "; 52 | acc += "while" 53 | if (!settings.minify) acc += " "; 54 | acc += "("; 55 | acc += this.expression.render(settings); 56 | acc += ")"; 57 | return acc; 58 | } 59 | 60 | static fromTokens(reader: TokenReader): WhileStatement { 61 | reader.expectNext(JSToken.Do); 62 | const statements = parseBlock(reader); 63 | reader.expectNext(JSToken.While); 64 | reader.expectNext(JSToken.OpenBracket) 65 | const condition = Expression.fromTokens(reader); 66 | reader.expectNext(JSToken.CloseBracket); 67 | return new DoWhileStatement(condition, statements) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/chef/javascript/components/types/decorator.ts: -------------------------------------------------------------------------------- 1 | import { defaultRenderSettings, IRenderable, IRenderSettings, TokenReader } from "../../../helpers"; 2 | import { JSToken } from "../../javascript"; 3 | import { ArgumentList } from "../constructs/function"; 4 | import { ValueTypes } from "../value/value"; 5 | 6 | export class Decorator implements IRenderable { 7 | private _argumentList?: ArgumentList; // Arguments sent to decorator 8 | 9 | constructor( 10 | public name: string, 11 | args?: Array | ArgumentList 12 | ) { 13 | if (args) { 14 | if (args instanceof ArgumentList) { 15 | this._argumentList = args; 16 | } else { 17 | this._argumentList = new ArgumentList(args); 18 | } 19 | } 20 | } 21 | 22 | render(settings: IRenderSettings = defaultRenderSettings): string { 23 | let acc = "@" + this.name; 24 | if (this._argumentList) { 25 | acc += this._argumentList.render(settings); 26 | } 27 | return acc; 28 | } 29 | 30 | // Get arguments parsed to decorator function 31 | get args() { 32 | return this._argumentList?.args ?? []; 33 | } 34 | 35 | static fromTokens(reader: TokenReader) { 36 | reader.expect(JSToken.At); 37 | const { value: name } = reader.next(); 38 | reader.expectNext(JSToken.Identifier); 39 | if (reader.current.type === JSToken.OpenBracket) { 40 | return new Decorator(name!, ArgumentList.fromTokens(reader)); 41 | } else { 42 | return new Decorator(name!); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/chef/javascript/components/types/enum.ts: -------------------------------------------------------------------------------- 1 | import { TokenReader, IRenderSettings, defaultRenderSettings, ScriptLanguages, IRenderable } from "../../../helpers"; 2 | import { commentTokens, JSToken } from "../../javascript"; 3 | import { Value, Type } from "../value/value"; 4 | import { VariableDeclaration } from "../statements/variable"; 5 | import { ObjectLiteral } from "../value/object"; 6 | import { Expression, Operation, tokenAsIdent, VariableReference } from "../value/expression"; 7 | 8 | export class EnumDeclaration implements IRenderable { 9 | 10 | constructor( 11 | public name: string, 12 | public members: Map 13 | ) { } 14 | 15 | get actualName() { 16 | return this.name!; 17 | } 18 | 19 | render(settings: IRenderSettings = defaultRenderSettings): string { 20 | if (settings.scriptLanguage === ScriptLanguages.Typescript) { 21 | let acc = "enum "; 22 | acc += this.name; 23 | acc += " {"; 24 | acc += "\n"; 25 | let cur = 0; 26 | for (const [key, value] of this.members) { 27 | acc += " ".repeat(settings.indent) + key; 28 | if (!(value instanceof Value) || (cur++).toString() !== value.value) { 29 | acc += " = " + value.render(settings); 30 | } 31 | if (cur < this.members.size) acc += ","; 32 | acc += "\n"; 33 | } 34 | acc += "}"; 35 | return acc; 36 | } else { 37 | const enumAsObject = enumToFrozenObject(this); 38 | return enumAsObject.render(settings); 39 | } 40 | } 41 | 42 | static fromTokens(reader: TokenReader) { 43 | reader.expectNext(JSToken.Enum); 44 | const name = reader.current.value || tokenAsIdent(reader.current.type); 45 | reader.move(); 46 | reader.expectNext(JSToken.OpenCurly); 47 | const members = new Map(); 48 | let counter = 0; 49 | while (reader.current.type !== JSToken.CloseCurly) { 50 | if (commentTokens.includes(reader.current.type)) { 51 | reader.move(); 52 | continue; 53 | } 54 | let value: Value; 55 | const member = reader.current.value || tokenAsIdent(reader.current.type); 56 | reader.move(); 57 | if (reader.current.type === JSToken.Assign) { 58 | reader.move(); 59 | value = Value.fromTokens(reader); 60 | } else { 61 | value = new Value(Type.number, counter++); 62 | } 63 | members.set(member, value); 64 | 65 | if (reader.current.type === JSToken.CloseBracket) break; 66 | // Commas in interfaces are not necessary 67 | if (reader.current.type === JSToken.Comma) reader.move(); 68 | } 69 | reader.move(); 70 | return new EnumDeclaration(name, members); 71 | } 72 | } 73 | 74 | /** 75 | * De-sugars ts enum declarations 76 | * Frozen to prevent mutation during runtime 77 | * @example `enum X {Y, Z}` -> const X = Object.freeze({Y: 0, Z: 1}) 78 | */ 79 | function enumToFrozenObject(enum_: EnumDeclaration): VariableDeclaration { 80 | const obj = new ObjectLiteral(enum_.members); 81 | const frozenObj = new Expression({ 82 | lhs: VariableReference.fromChain("Object", "freeze"), 83 | operation: Operation.Call, 84 | rhs: obj 85 | }) 86 | return new VariableDeclaration(enum_.name, { value: frozenObj }); 87 | } -------------------------------------------------------------------------------- /src/chef/javascript/components/types/interface.ts: -------------------------------------------------------------------------------- 1 | import { TokenReader, IRenderSettings, ScriptLanguages, defaultRenderSettings } from "../../../helpers"; 2 | import { commentTokens, JSToken } from "../../javascript"; 3 | import { TypeSignature } from "./type-signature"; 4 | import { tokenAsIdent } from "../value/expression"; 5 | import { Decorator } from "./decorator"; 6 | 7 | export class InterfaceDeclaration { 8 | 9 | name: TypeSignature; 10 | public decorators?: Array; 11 | 12 | constructor( 13 | name: string | TypeSignature, 14 | public extendsType: TypeSignature | null, 15 | public members: Map = new Map(), 16 | public memberDecorators: Map = new Map(), // One decorator per member for now 17 | public optionalProperties: Set = new Set() 18 | ) { 19 | if (typeof name === "string") { 20 | this.name = new TypeSignature({ name }); 21 | } else { 22 | this.name = name; 23 | } 24 | } 25 | 26 | get actualName() { 27 | return this.name.name!; 28 | } 29 | 30 | render(settings: IRenderSettings = defaultRenderSettings): string { 31 | if (settings.scriptLanguage !== ScriptLanguages.Typescript) { 32 | return ""; 33 | } 34 | let acc = ""; 35 | if (this.decorators && settings.scriptLanguage === ScriptLanguages.Typescript) { 36 | const separator = settings.minify ? " " : "\n"; 37 | acc += this.decorators.map(decorator => decorator.render(settings)).join(separator) + separator; 38 | } 39 | acc += "interface "; 40 | acc += this.name.render(settings); 41 | if (this.extendsType) { 42 | acc += " extends "; 43 | acc += this.extendsType.render(settings); 44 | } 45 | acc += " {"; 46 | if (this.members.size > 0 && !settings.minify) acc += "\n"; 47 | const members = Array.from(this.members); 48 | for (let index = 0; index < members.length; index++) { 49 | const [key, value] = members[index]; 50 | acc += " ".repeat(settings.indent); 51 | acc += key; 52 | if (this.optionalProperties.has(key)) { 53 | acc += "?: "; 54 | } else { 55 | acc += ": "; 56 | } 57 | acc += value.render(settings); 58 | if (index + 1 < members.length) { 59 | acc += "," 60 | } 61 | if (!settings.minify) acc += "\n"; 62 | } 63 | if (!settings.minify) acc += "\n"; 64 | return acc; 65 | } 66 | 67 | static fromTokens(reader: TokenReader) { 68 | reader.expect(JSToken.Interface); 69 | reader.move(); 70 | reader.expect(JSToken.Identifier); 71 | const name = TypeSignature.fromTokens(reader); 72 | 73 | let extendsType: TypeSignature | null = null; 74 | if (reader.current.type === JSToken.Extends) { 75 | reader.move(); 76 | extendsType = TypeSignature.fromTokens(reader); 77 | } 78 | 79 | const members: Map = new Map(); 80 | const memberDecorators: Map = new Map(); 81 | // Could use Map but that seems to create to much objects 82 | const optionalKeys: Set = new Set(); 83 | reader.expectNext(JSToken.OpenCurly) 84 | while (reader.current.type !== JSToken.CloseCurly) { 85 | if (commentTokens.includes(reader.current.type)) { 86 | reader.move(); 87 | continue; 88 | } 89 | 90 | let decorator: Decorator | null = null; 91 | if (reader.current.type === JSToken.At) { 92 | decorator = Decorator.fromTokens(reader); 93 | } 94 | 95 | let key: string; 96 | if (reader.current.type === JSToken.OpenSquare) { 97 | reader.throwError("Not implemented - computed interface properties"); 98 | } else { 99 | try { 100 | key = reader.current.value || tokenAsIdent(reader.current.type); 101 | reader.move(); 102 | } catch { 103 | reader.throwExpect("Expected valid interface name") 104 | } 105 | } 106 | if (reader.current.type === JSToken.OptionalMember) { 107 | optionalKeys.add(key); 108 | reader.move(); 109 | } else { 110 | reader.expectNext(JSToken.Colon); 111 | } 112 | const typeSig = TypeSignature.fromTokens(reader); 113 | members.set(key, typeSig); 114 | 115 | if (decorator) memberDecorators.set(key, decorator); 116 | 117 | while (commentTokens.includes(reader.current.type)) { 118 | reader.move(); 119 | } 120 | 121 | if (reader.current.type as JSToken === JSToken.CloseCurly) break; 122 | // Here optional skip over commas as they are not required acc to ts spec 123 | if (reader.current.type === JSToken.Comma || reader.current.type === JSToken.SemiColon) reader.move(); 124 | } 125 | reader.move(); 126 | return new InterfaceDeclaration(name, extendsType, members, memberDecorators, optionalKeys); 127 | } 128 | } -------------------------------------------------------------------------------- /src/chef/javascript/components/types/statements.ts: -------------------------------------------------------------------------------- 1 | import { ValueTypes } from "../value/value"; 2 | import { TypeSignature } from "./type-signature"; 3 | import { IRenderSettings, defaultRenderSettings, ScriptLanguages, TokenReader, IRenderable } from "../../../helpers"; 4 | import { JSToken } from "../../javascript"; 5 | 6 | export class AsExpression implements IRenderable { 7 | constructor( 8 | public value: ValueTypes, 9 | public asType: TypeSignature 10 | ) { } 11 | 12 | render(settings: IRenderSettings = defaultRenderSettings): string { 13 | if (settings.scriptLanguage !== ScriptLanguages.Typescript) { 14 | return this.value.render(settings); 15 | } 16 | let acc = this.value.render(settings); 17 | acc += " as "; 18 | acc += this.asType.render(settings); 19 | return acc; 20 | } 21 | } 22 | 23 | export class TypeDeclaration implements IRenderable { 24 | constructor( 25 | public name: TypeSignature, 26 | public value: TypeSignature, 27 | ) { } 28 | 29 | get actualName() { 30 | return this.name.name!; 31 | } 32 | 33 | render(settings: IRenderSettings = defaultRenderSettings) { 34 | if (settings.scriptLanguage !== ScriptLanguages.Typescript) return ""; 35 | let acc = "type "; 36 | acc += this.name.render(settings); 37 | acc += " = "; 38 | acc += this.value.render(settings); 39 | acc += ";"; 40 | return acc; 41 | } 42 | 43 | static fromTokens(reader: TokenReader): TypeDeclaration { 44 | reader.expectNext(JSToken.Type); 45 | // LHS can have generics so parse it as so 46 | const name = TypeSignature.fromTokens(reader); 47 | // TODO catch type x | y = 2; etc 48 | reader.expectNext(JSToken.Assign); 49 | const value = TypeSignature.fromTokens(reader); 50 | return new TypeDeclaration(name, value); 51 | } 52 | } -------------------------------------------------------------------------------- /src/chef/javascript/components/value/array.ts: -------------------------------------------------------------------------------- 1 | import { ValueTypes } from "./value"; 2 | import { TokenReader, IRenderSettings, defaultRenderSettings, IRenderable } from "../../../helpers"; 3 | import { JSToken } from "../../javascript"; 4 | import { Expression } from "./expression"; 5 | 6 | /** 7 | * Represents a array literal 8 | * @example `[1,2,3]` 9 | */ 10 | export class ArrayLiteral implements IRenderable { 11 | 12 | constructor ( 13 | public elements: ValueTypes[] = [] 14 | ) {} 15 | 16 | render(settings: IRenderSettings = defaultRenderSettings): string { 17 | let acc = "["; 18 | // Multi dimension arrays are printed a little different 19 | const isMultiDimensionArray = this.elements.length > 0 && this.elements.every(element => element instanceof ArrayLiteral); 20 | if (!settings.minify && isMultiDimensionArray) acc += "\n"; 21 | for (let i = 0; i < this.elements.length; i++) { 22 | if (!settings.minify && isMultiDimensionArray) acc += " ".repeat(settings.indent); 23 | const element = this.elements[i]; 24 | acc += element.render(settings); 25 | if (i !== this.elements.length - 1) acc += settings.minify ? "," : ", "; 26 | if (!settings.minify && isMultiDimensionArray) acc += "\n"; 27 | } 28 | return acc + "]" 29 | } 30 | 31 | static fromTokens(reader: TokenReader): ValueTypes { 32 | const array = new ArrayLiteral(); 33 | reader.expectNext(JSToken.OpenSquare); 34 | while (reader.current.type !== JSToken.CloseSquare) { 35 | array.elements.push(Expression.fromTokens(reader)); 36 | if (reader.current.type as JSToken === JSToken.CloseSquare) break; 37 | reader.expectNext(JSToken.Comma); 38 | } 39 | reader.move(); 40 | return array; 41 | } 42 | } -------------------------------------------------------------------------------- /src/chef/javascript/components/value/group.ts: -------------------------------------------------------------------------------- 1 | import { ValueTypes } from "./value"; 2 | import { IRenderSettings, ScriptLanguages, IRenderable, defaultRenderSettings } from "../../../helpers"; 3 | import { AsExpression } from "../types/statements"; 4 | 5 | export class Group implements IRenderable { 6 | constructor( 7 | public value: ValueTypes 8 | ) { } 9 | 10 | render(settings: IRenderSettings = defaultRenderSettings): string { 11 | // If (x as Thing).doStuff ==> x.doStuff 12 | if (settings.scriptLanguage !== ScriptLanguages.Typescript && this.value instanceof AsExpression) { 13 | return this.value.value.render(settings); 14 | } 15 | return `(${this.value.render(settings)})`; 16 | } 17 | } -------------------------------------------------------------------------------- /src/chef/javascript/components/value/regex.ts: -------------------------------------------------------------------------------- 1 | import { ValueTypes } from "./value"; 2 | import { TokenReader, IRenderSettings, defaultRenderSettings, IRenderable } from "../../../helpers"; 3 | import { JSToken } from "../../javascript"; 4 | 5 | export enum RegExpressionFlags { 6 | CaseInsensitive, 7 | Global, 8 | Multiline, 9 | DotAll, 10 | Unicode, 11 | Sticky 12 | } 13 | 14 | const flagMap: Map = new Map([ 15 | ["i", RegExpressionFlags.CaseInsensitive], 16 | ["g", RegExpressionFlags.Global], 17 | ["m", RegExpressionFlags.Multiline], 18 | ["s", RegExpressionFlags.DotAll], 19 | ["u", RegExpressionFlags.Unicode], 20 | ["y", RegExpressionFlags.Sticky], 21 | ]); 22 | 23 | const mapFlag: Map = new Map(Array.from(flagMap).map(([s, f]) => [f, s])); 24 | 25 | export class RegExpLiteral implements IRenderable { 26 | 27 | constructor ( 28 | public expression: string, 29 | public flags?: Set 30 | ) {} 31 | 32 | render(settings: IRenderSettings = defaultRenderSettings): string { 33 | let regexp = `/${this.expression}/`; 34 | if (this.flags) { 35 | for (const flag of this.flags) { 36 | regexp += mapFlag.get(flag); 37 | } 38 | } 39 | return regexp; 40 | } 41 | 42 | static fromTokens(reader: TokenReader): RegExpLiteral { 43 | reader.expect(JSToken.RegexLiteral); 44 | const pattern = reader.current.value!; 45 | reader.move(); 46 | // Parse flags 47 | let flags: Set; 48 | if (reader.current.type === JSToken.Identifier) { 49 | flags = new Set(); 50 | for (const char of reader.current.value!) { 51 | if (!flagMap.has(char)) reader.throwError(`Expected valid flag but received "${char}"`) 52 | const respectiveFlag = flagMap.get(char)!; 53 | flags.add(respectiveFlag); 54 | } 55 | reader.move(); 56 | } 57 | return new RegExpLiteral(pattern, flags!); 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/chef/javascript/components/value/template-literal.ts: -------------------------------------------------------------------------------- 1 | import { ValueTypes } from "./value"; 2 | import { TokenReader, IRenderSettings, defaultRenderSettings, IRenderable } from "../../../helpers"; 3 | import { JSToken } from "../../javascript"; 4 | import { Expression } from "./expression"; 5 | 6 | export class TemplateLiteral implements IRenderable { 7 | 8 | entries: Array = [] 9 | 10 | constructor( 11 | entries: Array = [], 12 | public tag: string | null = null // TODO tagging as IValue ??? 13 | ) { 14 | this.entries = []; 15 | entries.forEach(entry => this.addEntry(entry)); 16 | } 17 | 18 | addEntry(...entries: Array): void { 19 | // Collapses strings 20 | for (const entry of entries) { 21 | if (typeof entry === "string" && typeof this.entries[this.entries.length - 1] === "string") { 22 | this.entries[this.entries.length - 1] += entry; 23 | } else if (entry instanceof TemplateLiteral) { 24 | this.entries = this.entries.concat(entry.entries); 25 | } else { 26 | this.entries.push(entry); 27 | } 28 | } 29 | } 30 | 31 | render(settings: IRenderSettings = defaultRenderSettings): string { 32 | let acc = `${this.tag || ""}\``; 33 | for (const entry of this.entries) { 34 | if (typeof entry === "string") { 35 | acc += entry; 36 | } else { 37 | acc += "${" + entry.render(settings) + "}"; 38 | } 39 | } 40 | return acc + "`"; 41 | } 42 | 43 | static fromTokens(reader: TokenReader): TemplateLiteral { 44 | let tag: string | null = null; 45 | // If has tag 46 | if (reader.current.type === JSToken.Identifier) { 47 | tag = reader.current.value!; 48 | reader.move(); 49 | } 50 | reader.expectNext(JSToken.TemplateLiteralStart); 51 | const entries: Array = []; 52 | while (reader.current.type !== JSToken.TemplateLiteralEnd) { 53 | if (reader.current.type === JSToken.TemplateLiteralString) { 54 | if (reader.current.value !== "") { 55 | entries.push(reader.current.value!); 56 | } 57 | reader.move(); 58 | } else { 59 | entries.push(Expression.fromTokens(reader)); 60 | } 61 | } 62 | reader.move(); 63 | return new TemplateLiteral(entries, tag); 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /src/chef/javascript/components/value/value.ts: -------------------------------------------------------------------------------- 1 | import { IRenderSettings, defaultRenderSettings, IRenderable, TokenReader } from "../../../helpers"; 2 | import type { TemplateLiteral } from "./template-literal"; 3 | import type { Expression, VariableReference } from "./expression"; 4 | import type { ObjectLiteral } from "./object"; 5 | import type { ArrayLiteral } from "./array"; 6 | import type { RegExpLiteral } from "./regex"; 7 | import type { Group } from "./group"; 8 | import type { FunctionDeclaration } from "../constructs/function"; 9 | import type { ClassDeclaration } from "../constructs/class"; 10 | import { JSToken } from "../../javascript"; 11 | 12 | // All constructs that can be used as values: 13 | export type ValueTypes = Value 14 | | TemplateLiteral 15 | | Expression 16 | | ObjectLiteral 17 | | ArrayLiteral 18 | | RegExpLiteral 19 | | Group 20 | | FunctionDeclaration 21 | | ClassDeclaration 22 | | VariableReference; 23 | 24 | export enum Type { 25 | undefined, 26 | boolean, 27 | number, 28 | string, 29 | bigint, 30 | symbol, 31 | object, 32 | function, 33 | } 34 | 35 | export const literalTypes = new Set([JSToken.NumberLiteral, JSToken.StringLiteral, JSToken.True, JSToken.False]); 36 | 37 | /** 38 | * Represents string literals, number literals (inc bigint), boolean literals, "null" and "undefined" 39 | */ 40 | export class Value implements IRenderable { 41 | value: string | null; // TODO value is null if value is undefined 42 | 43 | constructor( 44 | public type: Type, 45 | value?: string | number | boolean, 46 | ) { 47 | if (typeof value === "number" || typeof value === "boolean") { 48 | this.value = value.toString(); 49 | } else if (typeof value !== "undefined" && value !== null) { 50 | // Escape new line characters in string 51 | this.value = value!.replace(/\r?\n/g, "\\n"); 52 | } else { 53 | this.value = null; 54 | } 55 | } 56 | 57 | static fromTokens(reader: TokenReader) { 58 | let value: Value; 59 | if (reader.current.type === JSToken.NumberLiteral) { 60 | value = new Value(Type.number, reader.current.value!); 61 | } else if (reader.current.type === JSToken.StringLiteral) { 62 | value = new Value(Type.string, reader.current.value!); 63 | } else if (reader.current.type === JSToken.True || reader.current.type === JSToken.False) { 64 | value = new Value(Type.boolean, reader.current.value! === "true"); 65 | } else { 66 | throw reader.throwExpect("Expected literal value"); 67 | } 68 | reader.move(); 69 | return value; 70 | } 71 | 72 | render(settings: IRenderSettings = defaultRenderSettings): string { 73 | switch (this.type) { 74 | case Type.string: return `"${this.value?.replace?.(/"/g, "\\\"") ?? ""}"`; 75 | case Type.number: return this.value!; 76 | case Type.bigint: return this.value!; 77 | case Type.boolean: return this.value!; 78 | case Type.undefined: return "undefined"; 79 | case Type.object: return "null"; 80 | default: throw Error(`Cannot render value of type "${Type[this.type]}"`) 81 | } 82 | } 83 | } 84 | 85 | export const nullValue = Object.freeze(new Value(Type.object)); -------------------------------------------------------------------------------- /src/chef/javascript/utils/reverse.ts: -------------------------------------------------------------------------------- 1 | import { ValueTypes, Value, Type } from "../components/value/value"; 2 | import { TemplateLiteral } from "../components/value/template-literal"; 3 | import { Expression, Operation, VariableReference } from "../components/value/expression"; 4 | import { FunctionDeclaration, ArgumentList } from "../components/constructs/function"; 5 | import { ReturnStatement } from "../components/statements/statement"; 6 | import { Group } from "../components/value/group"; 7 | import { replaceVariables, cloneAST } from "./variables"; 8 | import { ObjectLiteral } from "../components/value/object"; 9 | 10 | /** 11 | * Attempts to build a function that given the evaluated value as the argument will return the variable 12 | * Used by Prism to build get bindings from non straight variables. 13 | * TODO Chef really needs a interpreter to do this. 14 | * TODO only accepts expression has one variables. Should consider other variables in the expression as arguments 15 | * @param expression 16 | */ 17 | export function buildReverseFunction(expression: ValueTypes, targetVariable: Array): FunctionDeclaration { 18 | const func = new FunctionDeclaration(null, ["value"], [], { bound: false }); 19 | const reverseExpression = reverseValue(cloneAST(expression), targetVariable); 20 | func.statements.push(new ReturnStatement(reverseExpression)); 21 | return func; 22 | } 23 | 24 | function arraysEqual(array1: Array, array2: Array): boolean { 25 | if (array1.length !== array2.length) { 26 | return false; 27 | } 28 | for (let i = 0; i < array1.length; i++) { 29 | if (array1[i] !== array2[i]) { 30 | return false; 31 | } 32 | } 33 | return true; 34 | } 35 | 36 | const value = new VariableReference("value"); 37 | 38 | export function reverseValue(expression: ValueTypes | ArgumentList, targetVariable: Array): ValueTypes { 39 | if (expression instanceof VariableReference) { 40 | if (arraysEqual(expression.toChain(), targetVariable)) { 41 | return value; 42 | } else { 43 | return expression; 44 | } 45 | } else if (expression instanceof Value) { 46 | return expression; 47 | } else if (expression instanceof TemplateLiteral) { 48 | return reverseTemplateLiteral(expression); 49 | } else if (expression instanceof Expression) { 50 | const lhs = reverseValue(expression.lhs, targetVariable); 51 | const rhs = expression.rhs ? reverseValue(expression.rhs, targetVariable) : null; 52 | let reversedOperation: Operation; 53 | switch (expression.operation) { 54 | case Operation.Add: reversedOperation = Operation.Subtract; break; // TODO this assumes + numbers and not from string concatenation 55 | case Operation.Subtract: reversedOperation = Operation.Add; break; 56 | case Operation.Multiply: reversedOperation = Operation.Divide; break; 57 | case Operation.Divide: reversedOperation = Operation.Multiply; break; 58 | case Operation.LogNot: reversedOperation = Operation.LogNot; break; // TODO this will only work for booleans 59 | default: throw Error(`Cannot reverse operation ${Operation[expression.operation]}`); 60 | } 61 | return new Expression({ lhs, rhs, operation: reversedOperation }); 62 | } else if (expression instanceof ObjectLiteral) { 63 | // TODO catch undefined here: 64 | // TODO what about nested object literals 65 | const firstKey = targetVariable[0]; 66 | const matchingValue = expression.values.get(firstKey!); 67 | const reversedValue = reverseValue(matchingValue!, targetVariable); 68 | return new VariableReference(firstKey!, reversedValue); 69 | } else { 70 | throw Error(`Cannot reverse expression of construct "${expression.constructor.name}"`); 71 | } 72 | } 73 | 74 | /** 75 | * Attempts to create a slice expression given a template literal implementation 76 | * Will fail on expressions with more than one value interpolated 77 | */ 78 | export function reverseTemplateLiteral(templateLiteral: TemplateLiteral): Expression { 79 | if (templateLiteral.entries.filter(entry => typeof entry !== "string").length !== 1) { 80 | throw Error("Cannot reverse value as has two or more interpolation points"); 81 | } else { 82 | let startIndex: number = 0; 83 | // string ${x} ... 84 | if (typeof templateLiteral.entries[0] === "string") { 85 | startIndex = templateLiteral.entries[0].length; 86 | } 87 | let endLength: number | undefined; 88 | // `${x} string` 89 | if (startIndex === 0 && typeof templateLiteral.entries[1] === "string") { 90 | endLength = templateLiteral.entries[1].length; 91 | } 92 | // `... ${x} string` 93 | else if (typeof templateLiteral.entries[2] === "string") { 94 | endLength = templateLiteral[2].length; 95 | } 96 | 97 | const sliceArguments = [startIndex]; 98 | if (typeof endLength !== "undefined") sliceArguments.push(endLength * -1); 99 | 100 | // Returns a slice expression 101 | // Slice: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice 102 | return new Expression({ 103 | lhs: VariableReference.fromChain("value", "slice"), 104 | operation: Operation.Call, 105 | rhs: new ArgumentList(sliceArguments.map(value => new Value(Type.number, value))) 106 | }); 107 | } 108 | } 109 | 110 | // TODO better place for these: 111 | export function isIIFE(value: ValueTypes) { 112 | return value instanceof Expression && 113 | value.operation === Operation.Call && 114 | value.lhs instanceof Group && 115 | value.lhs.value instanceof FunctionDeclaration; 116 | } 117 | 118 | /** 119 | * "Flattens" a "iife" 120 | * @example ((t) => t * 2)(8) -> 8 * 2 121 | * @param iife a IIFE 122 | */ 123 | export function compileIIFE(iife: Expression): ValueTypes { 124 | const func: FunctionDeclaration = (iife.lhs as Group).value as FunctionDeclaration; 125 | if (func.statements.length !== 1) { 126 | throw Error("Cannot compile IIFE"); 127 | } 128 | let statement: ValueTypes | null = cloneAST((func.statements[0] as ReturnStatement).returnValue!); 129 | if (!statement) throw Error("IIFE must have return value to be compiled"); 130 | for (const [index, value] of (iife.rhs as ArgumentList).args.entries()) { 131 | const targetArgument = func.parameters[index]; 132 | replaceVariables(statement, value, [targetArgument.toReference()]); 133 | } 134 | return statement; 135 | } -------------------------------------------------------------------------------- /src/chef/rust/dynamic-statement.ts: -------------------------------------------------------------------------------- 1 | import { IRenderable, IRenderOptions, IRenderSettings } from "../helpers"; 2 | 3 | /** 4 | * Represents unstructured rust code as parsing will not be implemented. 5 | */ 6 | export class DynamicStatement implements IRenderable { 7 | constructor ( 8 | public rawCode: string 9 | ) {} 10 | 11 | render(settings: IRenderSettings, options?: Partial): string { 12 | return this.rawCode; 13 | } 14 | } -------------------------------------------------------------------------------- /src/chef/rust/module.ts: -------------------------------------------------------------------------------- 1 | import { IModule } from "../abstract-asts"; 2 | import { writeFile } from "../filesystem"; 3 | import { IRenderSettings, IRenderOptions, makeRenderSettings, defaultRenderSettings } from "../helpers"; 4 | import { renderStatements, StatementTypes } from "./statements/block"; 5 | 6 | export class Module implements IModule { 7 | 8 | constructor(public filename: string, public statements: Array = []) { } 9 | 10 | render(partialSettings: Partial = defaultRenderSettings, options?: Partial): string { 11 | const settings = makeRenderSettings(partialSettings); 12 | return renderStatements(this.statements, settings, false); 13 | } 14 | 15 | addExport(exportable: any) { 16 | throw new Error("Method not implemented."); 17 | } 18 | 19 | addImport(importName: any, from: string) { 20 | throw new Error("Method not implemented."); 21 | } 22 | 23 | writeToFile(settings: Partial) { 24 | writeFile(this.filename, this.render(settings)); 25 | } 26 | } -------------------------------------------------------------------------------- /src/chef/rust/statements/block.ts: -------------------------------------------------------------------------------- 1 | import { VariableDeclaration } from "./variable"; 2 | import { UseStatement } from "./use"; 3 | import { Expression } from "../values/expression"; 4 | import { FunctionDeclaration, ReturnStatement } from "./function"; 5 | import { defaultRenderSettings } from "../../helpers"; 6 | import { StructStatement } from "./struct"; 7 | import { IfStatement } from "./if"; 8 | import { ForStatement } from "./for"; 9 | import { ModStatement } from "./mod"; 10 | import { DeriveStatement } from "./derive"; 11 | import { DynamicStatement } from "../dynamic-statement"; 12 | import { ValueTypes } from "../values/value"; 13 | 14 | export type StatementTypes = Expression | VariableDeclaration | FunctionDeclaration | ReturnStatement | UseStatement | StructStatement | IfStatement | ForStatement | ModStatement | DeriveStatement | DynamicStatement | ValueTypes; 15 | 16 | export function renderStatements(statements: Array, settings = defaultRenderSettings, doIndent: boolean = true): string { 17 | let acc = ""; 18 | for (const statement of statements) { 19 | const doNotAddSemiColon = 20 | statement instanceof FunctionDeclaration || 21 | statement instanceof StructStatement || 22 | statement instanceof ForStatement || 23 | statement instanceof IfStatement || 24 | statement instanceof DynamicStatement || 25 | statement instanceof DeriveStatement; 26 | if (doIndent) { 27 | acc += ("\n" + statement.render(settings) + (doNotAddSemiColon ? "" : ";")).replace(/\n/g, "\n" + " ".repeat(settings.indent)); 28 | } else { 29 | acc += statement.render(settings) + (doNotAddSemiColon ? "" : ";") + "\n"; 30 | } 31 | } 32 | if (statements.length > 0 && doIndent) acc += "\n"; 33 | return acc; 34 | } -------------------------------------------------------------------------------- /src/chef/rust/statements/derive.ts: -------------------------------------------------------------------------------- 1 | import { IRenderable, IRenderSettings, IRenderOptions } from "../../helpers"; 2 | 3 | /** TODO does not really exist */ 4 | export class DeriveStatement implements IRenderable { 5 | constructor ( 6 | public traits: Array 7 | ) {} 8 | 9 | render(settings: IRenderSettings, options?: Partial): string { 10 | return `#[derive(${this.traits.join(", ")})]`; 11 | } 12 | } -------------------------------------------------------------------------------- /src/chef/rust/statements/for.ts: -------------------------------------------------------------------------------- 1 | import { IRenderable, IRenderOptions, IRenderSettings } from "../../helpers"; 2 | import { ValueTypes } from "../values/value"; 3 | import { renderStatements, StatementTypes } from "./block"; 4 | 5 | export class ForStatement implements IRenderable { 6 | constructor ( 7 | public variable: string, 8 | public subject: ValueTypes, 9 | public statements: Array 10 | ) {} 11 | 12 | render(settings: IRenderSettings, options?: Partial): string { 13 | let acc = `for ${this.variable} in ${this.subject.render(settings)} `; 14 | acc += `{${renderStatements(this.statements, settings)}}`; 15 | return acc; 16 | } 17 | } -------------------------------------------------------------------------------- /src/chef/rust/statements/function.ts: -------------------------------------------------------------------------------- 1 | import { IFunctionDeclaration } from "../../abstract-asts"; 2 | import { IRenderSettings, IRenderOptions, IRenderable, defaultRenderSettings } from "../../helpers"; 3 | import { ValueTypes } from "../values/value"; 4 | import { renderStatements, StatementTypes } from "./block"; 5 | import { TypeSignature } from "./struct"; 6 | 7 | export class ArgumentList implements IRenderable { 8 | constructor ( 9 | public args: Array 10 | ) {} 11 | 12 | render(settings: IRenderSettings = defaultRenderSettings, options?: Partial): string { 13 | return "(" + this.args.map(arg => arg.render(settings)).join(", ") + ")"; 14 | } 15 | } 16 | 17 | export class FunctionDeclaration implements IFunctionDeclaration { 18 | constructor( 19 | public actualName: string, 20 | public parameters: Array<[string, TypeSignature]>, 21 | public returnType: TypeSignature | null, 22 | public statements: StatementTypes[], 23 | public isPublic: boolean = false, 24 | ) { } 25 | 26 | buildArgumentListFromArgumentsMap(argumentMap: Map): ArgumentList { 27 | const args = this.parameters.map(([paramName]) => { 28 | if (!argumentMap.has(paramName)) throw Error(`Missing argument for parameter "${paramName}"`); 29 | return argumentMap.get(paramName)!; 30 | }); 31 | return new ArgumentList(args); 32 | } 33 | 34 | render(settings: IRenderSettings = defaultRenderSettings, options?: Partial): string { 35 | let acc = ""; 36 | if (this.isPublic) { 37 | acc += "pub " 38 | } 39 | acc += "fn " + this.actualName + "("; 40 | acc += this.parameters.map(([name, typeSig]) => `${name}: ${typeSig.render(settings)}`).join(", "); 41 | acc += ") "; 42 | if (this.returnType) { 43 | acc += `-> ${this.returnType.render(settings)} `; 44 | } 45 | acc += "{"; 46 | acc += renderStatements(this.statements, settings); 47 | acc += "}"; 48 | return acc; 49 | } 50 | } 51 | 52 | export class ClosureExpression implements IRenderable { 53 | constructor ( 54 | public parameters: Array<[string, TypeSignature | null]>, 55 | public statements: Array, 56 | public captureEnv: boolean = false, 57 | ) {} 58 | 59 | render(settings: IRenderSettings, options?: Partial): string { 60 | let acc = ""; 61 | if (this.captureEnv) acc += "move "; 62 | acc += "|" + this.parameters.map(([name, type]) => `${name}${type ? ": " + type.render(settings) : ""}`).join(", ") + "| "; 63 | if (this.statements.length === 1 && this.statements[0] instanceof ReturnStatement) { 64 | acc += this.statements[0].value.render(settings); 65 | } else { 66 | acc += "{"; 67 | acc += renderStatements(this.statements, settings); 68 | acc += "}"; 69 | } 70 | return acc; 71 | } 72 | } 73 | 74 | export class ReturnStatement implements IRenderable { 75 | constructor (public value: ValueTypes) {} 76 | 77 | render(settings: IRenderSettings, options?: Partial): string { 78 | return `return ${this.value.render(settings)}`; 79 | } 80 | } -------------------------------------------------------------------------------- /src/chef/rust/statements/if.ts: -------------------------------------------------------------------------------- 1 | import { IRenderable, IRenderOptions, IRenderSettings } from "../../helpers"; 2 | import { ValueTypes } from "../values/value"; 3 | import { renderStatements, StatementTypes } from "./block"; 4 | 5 | export class IfStatement implements IRenderable { 6 | constructor( 7 | public condition: ValueTypes, 8 | public statements: Array, 9 | public elseStatement?: ElseStatement 10 | ) { } 11 | 12 | render(settings: IRenderSettings, options?: Partial): string { 13 | let acc = `if ${this.condition.render(settings)} `; 14 | acc += `{${renderStatements(this.statements, settings)}}`; 15 | if (this.elseStatement) acc += this.elseStatement.render(settings); 16 | return acc; 17 | } 18 | } 19 | 20 | export class ElseStatement implements IRenderable { 21 | constructor( 22 | public condition: ValueTypes | null, 23 | public statements: Array, 24 | public elseStatement?: ElseStatement 25 | ) { } 26 | 27 | render(settings: IRenderSettings, options?: Partial): string { 28 | let acc = " else "; 29 | if (this.condition) acc += `if ${this.condition.render(settings)} `; 30 | acc += `{${renderStatements(this.statements, settings)}} `; 31 | if (this.elseStatement) acc += this.elseStatement.render(settings); 32 | return acc; 33 | } 34 | } -------------------------------------------------------------------------------- /src/chef/rust/statements/mod.ts: -------------------------------------------------------------------------------- 1 | import { IRenderable, IRenderOptions, IRenderSettings } from "../../helpers"; 2 | 3 | export class ModStatement implements IRenderable { 4 | constructor ( 5 | public name: string, 6 | public isPublicCrate: boolean = false 7 | ) {} 8 | 9 | render(settings: IRenderSettings, options?: Partial): string { 10 | let acc = ""; 11 | if (this.isPublicCrate) acc += "pub(crate) "; 12 | return acc + "mod " + this.name; 13 | } 14 | } -------------------------------------------------------------------------------- /src/chef/rust/statements/struct.ts: -------------------------------------------------------------------------------- 1 | import { IRenderable, IRenderOptions, IRenderSettings } from "../../helpers"; 2 | import { DynamicStatement } from "../dynamic-statement"; 3 | import { DeriveStatement } from "./derive"; 4 | 5 | export class StructStatement implements IRenderable { 6 | constructor ( 7 | public name: TypeSignature, 8 | public members: Map, 9 | public memberAttributes: Map = new Map(), 10 | public isPublic: boolean = false, 11 | public privateMembers: Set = new Set() 12 | ) {} 13 | 14 | render(settings: IRenderSettings, options?: Partial): string { 15 | let acc = ""; 16 | if (this.isPublic) acc += "pub "; 17 | acc += `struct ${this.name.render(settings)} {`; 18 | if (this.members.size > 0) acc += "\n"; 19 | for (const [name, type] of this.members) { 20 | acc += " ".repeat(settings.indent); 21 | if (this.memberAttributes.has(name)) { 22 | acc += this.memberAttributes.get(name)!.render(settings); 23 | acc += "\n" + " ".repeat(settings.indent); 24 | } 25 | if (!this.privateMembers.has(name)) acc += "pub "; 26 | acc += `${name}: ${type.render(settings)},\n`; 27 | } 28 | acc += "}"; 29 | return acc; 30 | } 31 | } 32 | 33 | interface TypeSignatureOptions { 34 | typeArguments?: Array; 35 | lifeTime?: boolean; 36 | borrowed?: boolean; 37 | } 38 | 39 | export class TypeSignature implements IRenderable, TypeSignatureOptions { 40 | typeArguments?: Array; 41 | lifeTime?: boolean; 42 | borrowed?: boolean; 43 | 44 | constructor ( 45 | public name: string, 46 | options?: TypeSignatureOptions 47 | ) { 48 | for (const key in options) Reflect.set(this, key, options[key]); 49 | } 50 | 51 | render(settings: IRenderSettings, options?: Partial): string { 52 | let acc = this.name; 53 | if (this.typeArguments) { 54 | acc += "<" + this.typeArguments.map(typeArg => typeArg.render(settings)).join(", ") + ">"; 55 | } 56 | return acc; 57 | } 58 | } -------------------------------------------------------------------------------- /src/chef/rust/statements/use.ts: -------------------------------------------------------------------------------- 1 | import { IRenderable, IRenderOptions, IRenderSettings } from "../../helpers"; 2 | 3 | export class UseStatement implements IRenderable { 4 | constructor ( 5 | public path: Array> 6 | ) {} 7 | 8 | render(settings: IRenderSettings, options?: Partial): string { 9 | return "use " + this.path.map(part => Array.isArray(part) ? `{${part.join(", ")}}` : part).join("::"); 10 | } 11 | } -------------------------------------------------------------------------------- /src/chef/rust/statements/variable.ts: -------------------------------------------------------------------------------- 1 | import { defaultRenderSettings, IRenderable, IRenderOptions, IRenderSettings } from "../../helpers"; 2 | import { ValueTypes } from "../values/value"; 3 | 4 | export class VariableDeclaration implements IRenderable { 5 | constructor( 6 | public name: string, 7 | public isMutable: boolean = false, 8 | public value?: ValueTypes, 9 | ) {} 10 | 11 | render(settings: IRenderSettings = defaultRenderSettings, options?: Partial): string { 12 | let acc = "let "; 13 | if (this.isMutable) acc += "mut "; 14 | acc += this.name; 15 | if (this.value) acc += " = " + this.value.render(settings); 16 | return acc; 17 | } 18 | } -------------------------------------------------------------------------------- /src/chef/rust/values/expression.ts: -------------------------------------------------------------------------------- 1 | import { IRenderable, IRenderOptions, IRenderSettings } from "../../helpers"; 2 | import { ArgumentList } from "../statements/function"; 3 | import { ValueTypes } from "./value"; 4 | 5 | export enum Operation { 6 | Call, 7 | Borrow, // Not sure whether this is operator but... 8 | Not, 9 | And, Or, 10 | Equal, 11 | LessThan, LessThanEqual, GreaterThan, GreaterThanEqual 12 | } 13 | 14 | export class Expression implements IRenderable { 15 | 16 | constructor( 17 | public lhs: ValueTypes, 18 | public operation: Operation, 19 | public rhs?: ValueTypes | ArgumentList 20 | ) { 21 | if (operation === Operation.Call && !(rhs instanceof ArgumentList)) { 22 | this.rhs = new ArgumentList(rhs ? [rhs] : []); 23 | } 24 | } 25 | 26 | render(settings: IRenderSettings, options?: Partial): string { 27 | switch (this.operation) { 28 | case Operation.Call: 29 | return this.lhs.render(settings) + (this.rhs?.render?.(settings) ?? "()"); 30 | case Operation.Borrow: 31 | return "&" + this.lhs.render(settings); 32 | case Operation.Not: 33 | return "!" + this.lhs.render(settings); 34 | case Operation.And: 35 | return this.lhs.render(settings) + " && " + this.rhs!.render(settings); 36 | case Operation.Or: 37 | return this.lhs.render(settings) + " || " + this.rhs!.render(settings); 38 | case Operation.Equal: 39 | return this.lhs.render(settings) + " == " + this.rhs!.render(settings); 40 | case Operation.LessThan: 41 | return this.lhs.render(settings) + " < " + this.rhs!.render(settings); 42 | case Operation.LessThanEqual: 43 | return this.lhs.render(settings) + " <= " + this.rhs!.render(settings); 44 | case Operation.GreaterThan: 45 | return this.lhs.render(settings) + " > " + this.rhs!.render(settings); 46 | case Operation.GreaterThanEqual: 47 | return this.lhs.render(settings) + " >= " + this.rhs!.render(settings); 48 | default: 49 | throw Error(`Cannot render operation "${Operation[this.operation]}"`); 50 | } 51 | } 52 | } 53 | 54 | export class StructConstructor implements IRenderable { 55 | 56 | // TODO tuple structs 57 | constructor( 58 | public name: string, 59 | public values: Array<[string, ValueTypes]> 60 | ) { } 61 | 62 | render(settings: IRenderSettings, options?: Partial): string { 63 | let acc = this.name; 64 | acc += " {"; 65 | for (const [name, value] of this.values) { 66 | acc += "\n"; 67 | acc += " ".repeat(settings.indent); 68 | acc += name; 69 | acc += ": "; 70 | acc += value.render(settings, options); 71 | } 72 | acc += "\n}"; 73 | return acc; 74 | } 75 | } -------------------------------------------------------------------------------- /src/chef/rust/values/value.ts: -------------------------------------------------------------------------------- 1 | import { defaultRenderSettings, IRenderable, IRenderOptions, IRenderSettings } from "../../helpers"; 2 | import { ClosureExpression } from "../statements/function"; 3 | import { Expression } from "./expression"; 4 | import { VariableReference } from "./variable"; 5 | 6 | export type ValueTypes = Expression | VariableReference | Value | ClosureExpression; 7 | 8 | export enum Type { 9 | boolean, 10 | number, 11 | string, 12 | } 13 | 14 | export class Value implements IRenderable { 15 | constructor(public type: Type, public value: string) { 16 | if (type === Type.string) { 17 | this.value = this.value.replace(/\r?\n/g, "\\n"); 18 | } 19 | } 20 | 21 | render(settings: IRenderSettings = defaultRenderSettings, options?: Partial): string { 22 | switch (this.type) { 23 | case Type.boolean: return this.value; 24 | case Type.number: return this.value; 25 | case Type.string: return `"${this.value.replace(/"/g, "\\\"")}"`; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/chef/rust/values/variable.ts: -------------------------------------------------------------------------------- 1 | import { defaultRenderSettings, IRenderSettings } from "../../helpers"; 2 | import { ValueTypes } from "./value"; 3 | 4 | export class VariableReference { 5 | 6 | constructor( 7 | public name: string, 8 | public parent?: ValueTypes, 9 | public scoped: boolean = false 10 | ) { } 11 | 12 | render(settings: IRenderSettings = defaultRenderSettings): string { 13 | let acc = this.name; 14 | if (this.parent) { 15 | acc = this.parent.render(settings) + (this.scoped ? "::" : ".") + acc; 16 | } 17 | return acc; 18 | } 19 | } -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import { printHelpScreen, printInfoScreen, printWarningBanner } from "./others/banners"; 2 | import { createPrismTemplateApp } from "./others/actions"; 3 | // This node import also has side effects of setting callbacks to read from local filesystem 4 | import { registerSettings, compileApplication, compileComponent, runApplication } from "./node"; 5 | 6 | switch (process.argv[2] ?? "info") { 7 | case "version": 8 | case "info": 9 | printInfoScreen(); 10 | break; 11 | case "help": 12 | printHelpScreen(); 13 | break; 14 | case "init": 15 | createPrismTemplateApp(process.cwd()); 16 | break; 17 | case "compile-component": { 18 | const settings = registerSettings(process.cwd()); 19 | printWarningBanner(); 20 | if (settings.buildTimings) console.time("Building single component"); 21 | let componentTagName = compileComponent(process.cwd(), settings); 22 | if (settings.buildTimings) console.timeEnd("Building single component"); 23 | console.log(`Wrote out component.js and component.css to ${settings.outputPath}`); 24 | console.log(`Built web component, use with "<${componentTagName}>" or "document.createElement("${componentTagName}")"`); 25 | break; 26 | } 27 | case "compile-application": 28 | case "compile-app": { 29 | const settings = registerSettings(process.cwd()); 30 | printWarningBanner(); 31 | if (settings.buildTimings) console.time("Building application"); 32 | compileApplication(process.cwd(), settings, runApplication) 33 | if (settings.buildTimings) console.timeEnd("Building application"); 34 | break; 35 | } 36 | // Others 37 | case "run": { 38 | const settings = registerSettings(process.cwd()); 39 | const openBrowser = process.argv[3] === "--open"; 40 | runApplication(openBrowser, settings); 41 | break; 42 | } 43 | default: 44 | console.error(`Unknown action ${process.argv[2]}. Run 'prism help' for a list of functions`) 45 | break; 46 | } -------------------------------------------------------------------------------- /src/filesystem.ts: -------------------------------------------------------------------------------- 1 | import { 2 | fsReadCallback, fsWriteCallback, 3 | registerFSReadCallback as chefRegisterFSReadCallback, 4 | registerFSWriteCallback as chefRegisterFSWriteCallback 5 | } from "./chef/filesystem"; 6 | import type { Stats } from "fs"; 7 | 8 | /** Registering FS callbacks */ 9 | export let __fileSystemReadCallback: fsReadCallback | null = null; 10 | export function registerFSReadCallback(cb: fsReadCallback | null) { 11 | __fileSystemReadCallback = cb; 12 | chefRegisterFSReadCallback(cb); 13 | } 14 | 15 | export let __fileSystemWriteCallback: fsWriteCallback | null = null; 16 | export function registerFSWriteCallback(cb: fsWriteCallback | null) { 17 | __fileSystemWriteCallback = cb; 18 | chefRegisterFSWriteCallback(cb); 19 | } 20 | 21 | type fsCopyCallback = (from: string, to: string) => void; 22 | let __fileSystemCopyCallback: fsCopyCallback | null = null; 23 | export function registerFSCopyCallback(cb: fsCopyCallback) { 24 | __fileSystemCopyCallback = cb; 25 | } 26 | 27 | type fsExistsCallback = (path: string) => boolean; 28 | let __fileSystemExistsCallback: fsExistsCallback | null = null; 29 | export function registerFSExistsCallback(cb: fsExistsCallback) { 30 | __fileSystemExistsCallback = cb; 31 | } 32 | 33 | type fsReadDirectoryCallback = (path: string) => Array; 34 | let __fileSystemReadDirectoryCallback: fsReadDirectoryCallback | null = null; 35 | export function registerFSReadDirectoryCallback(cb: fsReadDirectoryCallback) { 36 | __fileSystemReadDirectoryCallback = cb; 37 | } 38 | 39 | type fsPathInfoCallback = (path: string) => Stats; 40 | let __fileSystemPathInfoCallback: fsPathInfoCallback | null = null; 41 | export function registerFSPathInfoCallback(cb: fsPathInfoCallback) { 42 | __fileSystemPathInfoCallback = cb; 43 | } 44 | 45 | /** FS access functions: */ 46 | export function copyFile(from: string, to: string) { 47 | if (!__fileSystemCopyCallback) throw Error("No file system copy file callback registered"); 48 | return __fileSystemCopyCallback(from, to); 49 | } 50 | 51 | export function exists(path: string): boolean { 52 | if (!__fileSystemExistsCallback) throw Error("No file system exists callback registered"); 53 | return __fileSystemExistsCallback(path); 54 | } 55 | 56 | export function readFile(filename: string): string { 57 | if (!__fileSystemReadCallback) throw Error("No file system read file callback registered"); 58 | return __fileSystemReadCallback(filename); 59 | } 60 | 61 | export function readDirectory(filename: string): Array { 62 | if (!__fileSystemReadDirectoryCallback) throw Error("No file system read directory callback registered"); 63 | return __fileSystemReadDirectoryCallback(filename); 64 | } 65 | 66 | export function pathInformation(filename: string): Stats { 67 | if (!__fileSystemPathInfoCallback) throw Error("No file system read file callback registered"); 68 | return __fileSystemPathInfoCallback(filename); 69 | } -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { readDirectory, pathInformation } from "./filesystem"; 2 | import { join } from "path"; 3 | 4 | /** 5 | * Parses command line arguments to map 6 | */ 7 | export function getArguments(args: string[]): Map { 8 | const argsMap = new Map(); 9 | let index = 0; 10 | while (index < args.length) { 11 | if (index + 1 >= args.length || args[index + 1].startsWith("--")) { 12 | argsMap.set(args[index].substring(2), null); 13 | index++; 14 | } else { 15 | let value: string | boolean | number = args[index + 1]; 16 | if (value === "true") value = true; 17 | else if (value === "false") value = false; 18 | // @ts-ignore 19 | else if (!isNaN(value)) value = parseInt(value); 20 | argsMap.set(args[index].substring(2), value); 21 | index += 2; 22 | } 23 | } 24 | return argsMap; 25 | } 26 | 27 | export interface IMetadata { 28 | title?: string, 29 | description?: string, 30 | website?: string, 31 | image?: string 32 | } 33 | 34 | export function isUppercase(char: string): boolean { 35 | const charCode = char.charCodeAt(0); 36 | return charCode >= 65 && charCode <= 90 37 | } 38 | 39 | /** 40 | * Recursively yields absolute file paths in a folder 41 | * @param folder 42 | * @yields full filepath 43 | */ 44 | export function filesInFolder(folder: string): Array { 45 | const files: Array = []; 46 | for (const member of readDirectory(folder)) { 47 | const memberPath = join(folder, member); 48 | if (pathInformation(memberPath).isDirectory()) { 49 | files.push(...filesInFolder(memberPath)); 50 | } else { 51 | files.push(memberPath); 52 | } 53 | } 54 | return files; 55 | } 56 | 57 | /** 58 | * like `arr.findIndex` but returns the rightmost index first. 59 | * @param arr Array to search 60 | * @param predicate Predicate to test against 61 | * @throws If cannot find element that matches `predicate` 62 | */ 63 | export function findLastIndex(arr: Array, predicate: (arg0: T) => boolean): number { 64 | for (let i = arr.length - 1; i >= 0; i--) { 65 | if (predicate(arr[i])) return i; 66 | } 67 | throw Error("Could not find element in arr that matched predicate"); 68 | } 69 | 70 | /** 71 | * Utility function for assigning data to objects without interfering their prototypes 72 | * @param map The `Map` or `WeakMap` 73 | * @param obj The 74 | * @param key 75 | * @param value 76 | */ 77 | export function assignToObjectMap( 78 | map: Map | WeakMap, 79 | obj: T, 80 | key: keyof U, 81 | value: U[typeof key] 82 | ) { 83 | if (map.has(obj)) { 84 | Reflect.set(map.get(obj)!, key, value); 85 | } else { 86 | // @ts-ignore issue around partials 87 | map.set(obj, {[key]: value}); 88 | } 89 | } -------------------------------------------------------------------------------- /src/metatags.ts: -------------------------------------------------------------------------------- 1 | import { HTMLElement, Node } from "./chef/html/html"; 2 | import { ValueTypes } from "./chef/javascript/components/value/value"; 3 | import { assignToObjectMap } from "./helpers"; 4 | import { NodeData } from "./templating/template"; 5 | 6 | /** 7 | * Helper for building meta tag with attributes 8 | * @param metadata a object equal to the 9 | */ 10 | function buildMetaTag(metadata: object, nodeData: WeakMap): HTMLElement { 11 | const tag = new HTMLElement("meta"); 12 | for (const key in metadata) { 13 | const attribute = metadata[key]; 14 | if (attribute === null) { 15 | if (!tag.attributes) { 16 | tag.attributes = new Map(); 17 | } 18 | tag.attributes.set(key, null); 19 | } else if (typeof attribute === "string") { 20 | if (!tag.attributes) { 21 | tag.attributes = new Map(); 22 | } 23 | tag.attributes.set(key, attribute); 24 | } else { 25 | const dynamicAttributes = nodeData.get(tag)?.dynamicAttributes; 26 | if (!dynamicAttributes) { 27 | assignToObjectMap(nodeData, tag, "dynamicAttributes", new Map([[key, attribute]])); 28 | } else { 29 | dynamicAttributes.set(key, attribute); 30 | } 31 | } 32 | } 33 | return tag; 34 | } 35 | 36 | /** 37 | * Creates a series of standard meta tags used for seo and 38 | * TODO stricter metadata types (possible string enums) 39 | * @param metadata 40 | */ 41 | export function buildMetaTags(metadata: Map): { 42 | metadataTags: Array, 43 | nodeData: WeakMap 44 | } { 45 | const metadataTags: Array = [], nodeData = new WeakMap(); 46 | for (const [key, value] of metadata) { 47 | switch (key) { 48 | case "title": 49 | metadataTags.push( 50 | buildMetaTag({ name: "title", content: value }, nodeData), 51 | buildMetaTag({ property: "og:title", content: value }, nodeData), 52 | buildMetaTag({ property: "twitter:title", content: value }, nodeData) 53 | ); 54 | break; 55 | case "description": 56 | metadataTags.push( 57 | buildMetaTag({ name: "description", content: value }, nodeData), 58 | buildMetaTag({ property: "og:description", content: value }, nodeData), 59 | buildMetaTag({ property: "twitter:description", content: value }, nodeData) 60 | ); 61 | break; 62 | case "image": 63 | metadataTags.push( 64 | buildMetaTag({ property: "og:image", content: value }, nodeData), 65 | buildMetaTag({ property: "twitter:image", content: value }, nodeData) 66 | ); 67 | break; 68 | case "website": 69 | metadataTags.push( 70 | buildMetaTag({ property: "og:website", content: value }, nodeData), 71 | buildMetaTag({ property: "twitter:website", content: value }, nodeData) 72 | ); 73 | break; 74 | default: 75 | // TODO dynamic tags other than 76 | metadataTags.push(buildMetaTag({ property: key, content: value }, nodeData)); 77 | break; 78 | } 79 | } 80 | 81 | metadataTags.push( 82 | buildMetaTag({ property: "og:type", content: "website" }, nodeData), 83 | buildMetaTag({ property: "twitter:card", content: "summary_large_image" }, nodeData), 84 | ); 85 | 86 | return { metadataTags, nodeData }; 87 | } -------------------------------------------------------------------------------- /src/node.ts: -------------------------------------------------------------------------------- 1 | import { 2 | registerFSCopyCallback, registerFSExistsCallback, 3 | registerFSPathInfoCallback, registerFSReadCallback, 4 | registerFSReadDirectoryCallback, registerFSWriteCallback, 5 | } from "./filesystem"; 6 | import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, readdirSync, lstatSync } from "fs"; 7 | import { getArguments } from "./helpers"; 8 | import { IPrismSettings, makePrismSettings } from "./settings"; 9 | import { createServer } from "http"; 10 | import { emitKeypressEvents } from "readline"; 11 | import { exec } from "child_process"; 12 | import { dirname, join, isAbsolute } from "path"; 13 | 14 | registerFSReadCallback(filename => readFileSync(filename).toString()); 15 | registerFSWriteCallback((filename, content) => { 16 | const dir = dirname(filename); 17 | if (!existsSync(dir)) { 18 | mkdirSync(dir, { recursive: true }); 19 | } 20 | writeFileSync(filename, content) 21 | }); 22 | registerFSCopyCallback((from, to) => { 23 | const dir = dirname(to); 24 | if (!existsSync(dir)) { 25 | mkdirSync(dir, { recursive: true }); 26 | } 27 | copyFileSync(from, to); 28 | }); 29 | registerFSExistsCallback(existsSync); 30 | registerFSReadDirectoryCallback(readdirSync); 31 | registerFSPathInfoCallback(lstatSync); 32 | 33 | // Re-export fs callbacks so module users can overwrite existing node fs behavior 34 | export { registerFSCopyCallback, registerFSExistsCallback, registerFSPathInfoCallback, registerFSReadCallback, registerFSReadDirectoryCallback, registerFSWriteCallback }; 35 | 36 | export { compileApplication } from "./builders/compile-app"; 37 | export { compileComponent, compileComponentFromFSMap, compileSingleComponentFromString, compileComponentFromFSObject } from "./builders/compile-component"; 38 | export { makePrismSettings } from "./settings"; 39 | export { prismVersion } from "./bundled-files"; 40 | 41 | export function registerSettings(cwd: string): Partial { 42 | let configFilePath: string; 43 | let startIndex = 3; 44 | if (process.argv[startIndex]?.endsWith("prism.config.json")) { 45 | if (isAbsolute(process.argv[startIndex])) { 46 | configFilePath = process.argv[startIndex]; 47 | } else { 48 | configFilePath = join(cwd, process.argv[startIndex]); 49 | } 50 | startIndex++; 51 | } else { 52 | configFilePath = join(cwd, "prism.config.json"); 53 | } 54 | 55 | let settings = {}; 56 | 57 | if (existsSync(join(configFilePath))) { 58 | Object.assign(settings, JSON.parse(readFileSync(configFilePath).toString())); 59 | } 60 | 61 | const args = process.argv.slice(startIndex); 62 | 63 | for (const [argument, value] of getArguments(args)) { 64 | if (value === null) { 65 | Reflect.set(settings, argument, true); 66 | } else { 67 | Reflect.set(settings, argument, value); 68 | } 69 | } 70 | 71 | return settings; 72 | } 73 | 74 | /** 75 | * Runs a client side prism application 76 | * @param openBrowser Whether to open the browser to the site 77 | */ 78 | export function runApplication(openBrowser: boolean = false, partialSettings: Partial): Promise { 79 | const settings = makePrismSettings(process.cwd(), partialSettings); 80 | const htmlShell = join(settings.absoluteOutputPath, settings.context === "client" ? "index.html" : "shell.html"); 81 | return new Promise((res, rej) => { 82 | const port = 8080 83 | const server = createServer(function (req, res) { 84 | const path = join(settings.absoluteOutputPath, req.url!); 85 | switch (path.split(".").pop()) { 86 | case "js": res.setHeader("Content-Type", "text/javascript"); break; 87 | case "css": res.setHeader("Content-Type", "text/css"); break; 88 | case "ico": res.setHeader("Content-Type", "image/vnd.microsoft.icon"); break; 89 | default: res.setHeader("Content-Type", "text/html"); break; 90 | } 91 | if (existsSync(path) && lstatSync(path).isFile()) { 92 | res.write(readFileSync(path)); 93 | } else { 94 | res.write(readFileSync(htmlShell)); 95 | } 96 | res.end(); 97 | }); 98 | 99 | server.addListener("error", rej); 100 | server.listen(port); 101 | 102 | emitKeypressEvents(process.stdin); 103 | process.stdin.setRawMode(true); 104 | const keyReader = process.stdin.on("keypress", (_, key) => { 105 | if (key.ctrl && key.name === "c") { 106 | console.log("Closing Server"); 107 | server.close(); 108 | keyReader.end(); 109 | keyReader.destroy(); 110 | res(); 111 | } 112 | }); 113 | 114 | const url = `http://localhost:${port}`; 115 | 116 | if (openBrowser) { 117 | let start: string; 118 | switch (process.platform) { 119 | case "darwin": start = "open"; break; 120 | case "win32": start = "start"; break; 121 | default: throw Error("Unknown Platform"); 122 | }; 123 | exec(`${start} ${url}`); 124 | } 125 | console.log(`Server started at ${url}, stop with ctrl+c`); 126 | }); 127 | } -------------------------------------------------------------------------------- /src/others/actions.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "../chef/javascript/components/module"; 2 | import { Stylesheet } from "../chef/css/stylesheet"; 3 | import { HTMLDocument } from "../chef/html/html"; 4 | import { fileBundle } from "../bundled-files"; 5 | import { ScriptLanguages } from "../chef/helpers"; 6 | import { writeFile } from "../chef/filesystem"; 7 | import { extname, isAbsolute, join } from "path"; 8 | 9 | export function minifyFile(targetFile: string, outputFile: string) { 10 | const absTargetFile = isAbsolute(targetFile) ? targetFile : join(process.cwd(), targetFile); 11 | const absOutputFile = isAbsolute(outputFile) ? outputFile : join(process.cwd(), outputFile); 12 | const extension = extname(targetFile); 13 | switch (extension) { 14 | case ".js": 15 | case ".ts": 16 | Module.fromFile(absTargetFile).writeToFile({ minify: true }, absOutputFile); 17 | break; 18 | case ".css": 19 | Stylesheet.fromFile(absTargetFile).writeToFile({ minify: true }, absOutputFile); 20 | break; 21 | case ".html": 22 | HTMLDocument.fromFile(absTargetFile).writeToFile({ minify: true }, absOutputFile); 23 | break; 24 | default: 25 | throw Error(`Files with extension "${extension}"`) 26 | } 27 | } 28 | 29 | export function createPrismTemplateApp(cwd: string) { 30 | HTMLDocument.fromString(fileBundle.get("index.prism")!, join(cwd, "views", "index.prism")) 31 | .writeToFile({ minify: false, scriptLanguage: ScriptLanguages.Typescript }); 32 | 33 | writeFile("prism.config.json", JSON.stringify({ context: "client", versioning: false }, undefined, 4)); 34 | 35 | console.log(`Wrote out "views/index.prism", run with "prism compile-app --run open"`); 36 | } -------------------------------------------------------------------------------- /src/others/banners.ts: -------------------------------------------------------------------------------- 1 | import { prismVersion } from "../bundled-files"; 2 | 3 | export function printWarningBanner(): void { 4 | const message = `Warning: Prism is an experimental, expect unexpected behavior to not be caught, not for use in production`; 5 | const leftOffset = Math.floor(Math.max(process.stdout.columns - message.length, 0) / 2); 6 | console.log("\n" + " ".repeat(leftOffset) + message + "\n"); 7 | } 8 | 9 | export function printInfoScreen(): void { 10 | const leftOffset = Math.floor(Math.max(process.stdout.columns - 76, 0) / 2); 11 | console.log( 12 | `${" ".repeat(leftOffset)} ______ ______ __ ______ __ __ 13 | ${" ".repeat(leftOffset)}/\\ == \\ /\\ == \\ /\\ \\ /\\ ___\\ /\\ "-./ \\ Prism Compiler 14 | ${" ".repeat(leftOffset)}\\ \\ _-/ \\ \\ __< \\ \\ \\ \\ \\___ \\ \\ \\ \\-./\\ \\ ${prismVersion} 15 | ${" ".repeat(leftOffset)} \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ \\ \\_\\ \\ \\_\\ @kaleidawave 16 | ${" ".repeat(leftOffset)} \\/_/ \\/_/ /_/ \\/_/ \\/_____/ \\/_/ \\/_/ 17 | `); 18 | } 19 | 20 | export function printHelpScreen(): void { 21 | console.log(`To compile application: 22 | 23 | prism compile-app 24 | 25 | To compile single component: 26 | 27 | prism compile-component --projectPath "./path/to/component" 28 | 29 | For complete documentation on templating syntax and configuration goto github.com/kaleidawave/prism`) 30 | } -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { join, isAbsolute, sep as separator } from "path"; 2 | 3 | export interface IPrismSettings { 4 | minify: boolean, // Removes whitespace for space saving in output 5 | comments: boolean, // Leave comments in TODO comment levels 6 | componentPath: string | null, // The path to the entry component 7 | projectPath: string, // The path to the components folder 8 | assetPath: string | null, // The path to the assets folder 9 | outputPath: string, // The path to the output folder 10 | serverOutputPath: string | null, // The path to the output folder 11 | templatePath: string | null, // The path to the output folder, null if default 12 | context: "client" | "isomorphic", // If client will not build server paths or add hydration logic to client bundle 13 | backendLanguage: "js" | "ts" | "rust", // The languages to output server templates in 14 | buildTimings: boolean, // Whether to print timings of the static build 15 | relativeBasePath: string, // Prefix all routes, used if index is not under "/" 16 | clientSideRouting: boolean, // Add router and do client side routing 17 | run: boolean | "open", // Whether to run output after build 18 | disableEventElements: boolean, // Add disable attribute to the SSR markup of all events which is then removed once event has been added 19 | versioning: boolean, // Whether to version bundles (Insert a unique id into path) 20 | // Whether to SSR the content of components with shadow dom https://web.dev/declarative-shadow-dom/ 21 | declarativeShadowDOM: boolean, 22 | deno: boolean, // Includes file extensions in imports on server output 23 | bundleOutput: boolean, // Concatenate output to single bundle 24 | outputTypeScript: boolean, // Whether to output components in typescript so that checking can be done 25 | includeCSSImports: boolean, // Include CSS imports in components 26 | } 27 | 28 | const defaultSettings: IPrismSettings = { 29 | minify: false, 30 | comments: false, 31 | declarativeShadowDOM: false, 32 | componentPath: null, 33 | projectPath: "./views", 34 | assetPath: null, 35 | outputPath: "./out", 36 | serverOutputPath: null, 37 | templatePath: null, 38 | context: "isomorphic", 39 | backendLanguage: "js", 40 | buildTimings: false, 41 | clientSideRouting: true, 42 | versioning: true, 43 | relativeBasePath: "/", 44 | disableEventElements: true, 45 | run: false, 46 | deno: false, 47 | bundleOutput: true, 48 | includeCSSImports: false, 49 | outputTypeScript: false, 50 | }; 51 | 52 | /** 53 | * Adds some getters because projectPath and outputPath can be relative 54 | * Relative to cwd 55 | * @example if projectPath = "../abc" then absoluteProjectPath ~ "C:/abc" 56 | */ 57 | export interface IFinalPrismSettings extends IPrismSettings { 58 | projectPath: string, 59 | absoluteComponentPath: string, // Entry component (TODO) 60 | pathSplitter: string, // Needed for rust things 61 | absoluteProjectPath: string, 62 | absoluteOutputPath: string, 63 | absoluteAssetPath: string, 64 | absoluteServerOutputPath: string, 65 | absoluteTemplatePath: string | null, 66 | } 67 | 68 | export function makePrismSettings( 69 | cwd: string, 70 | partialSettings: Partial = {} 71 | ): IFinalPrismSettings { 72 | const projectPath = partialSettings.projectPath ?? defaultSettings.projectPath; 73 | const outputPath = partialSettings.outputPath ?? defaultSettings.outputPath; 74 | const assetPath = partialSettings.assetPath ?? join(projectPath, "assets"); 75 | const serverOutputPath = partialSettings.serverOutputPath ?? join(outputPath, "server"); 76 | const templatePath = partialSettings.templatePath ?? defaultSettings.templatePath; 77 | const componentPath = partialSettings.componentPath ?? join(cwd, "index.prism"); 78 | return { 79 | ...defaultSettings, 80 | ...partialSettings, 81 | pathSplitter: separator, 82 | componentPath, 83 | absoluteComponentPath: isAbsolute(componentPath) ? componentPath : join(cwd, componentPath), 84 | absoluteProjectPath: isAbsolute(projectPath) ? projectPath : join(cwd, projectPath), 85 | absoluteOutputPath: isAbsolute(outputPath) ? outputPath : join(cwd, outputPath), 86 | absoluteAssetPath: isAbsolute(assetPath) ? assetPath : join(cwd, assetPath), 87 | absoluteServerOutputPath: isAbsolute(serverOutputPath) ? serverOutputPath : join(cwd, serverOutputPath), 88 | absoluteTemplatePath: templatePath ? isAbsolute(templatePath) ? templatePath : join(cwd, templatePath) : null, 89 | }; 90 | } -------------------------------------------------------------------------------- /src/templating/builders/server-event-bindings.ts: -------------------------------------------------------------------------------- 1 | import { FunctionDeclaration, ArgumentList } from "../../chef/javascript/components/constructs/function"; 2 | import { Expression, Operation, VariableReference } from "../../chef/javascript/components/value/expression"; 3 | import { Value, Type, ValueTypes } from "../../chef/javascript/components/value/value"; 4 | import { IEvent } from "../template"; 5 | 6 | const addVariable = new VariableReference("a"); 7 | 8 | function generateChangeEventCall( 9 | elementID: string, 10 | eventName: string, 11 | callback: ValueTypes, 12 | wasDisabled: boolean 13 | ): Expression { 14 | const changeEventArgs = new ArgumentList([ 15 | new VariableReference("this"), 16 | new Value(Type.string, elementID), 17 | new Value(Type.string, eventName), 18 | callback, 19 | addVariable, 20 | ]); 21 | 22 | if (wasDisabled) { 23 | changeEventArgs.args.push(new Value(Type.boolean, true)); 24 | } 25 | 26 | return new Expression({ 27 | lhs: new VariableReference("changeEvent"), 28 | operation: Operation.Call, 29 | rhs: changeEventArgs 30 | }); 31 | } 32 | 33 | /** 34 | * Creates functions for binding events to ssr content 35 | */ 36 | export function buildEventBindings( 37 | events: Array, 38 | disableEventElements: boolean 39 | ): FunctionDeclaration { 40 | const handleEventListenersFunction = new FunctionDeclaration("handleEvents", ["a"]); 41 | 42 | for (const event of events) { 43 | // Bind the cb to "this" so that the data variable exist, rather than being bound the event invoker 44 | let callback: ValueTypes; 45 | if (event.existsOnComponentClass) { 46 | callback = new Expression({ 47 | lhs: VariableReference.fromChain("this", event.callback.name, "bind"), 48 | operation: Operation.Call, 49 | rhs: new ArgumentList([new VariableReference("this")]) 50 | }); 51 | } else { 52 | callback = event.callback; 53 | } 54 | 55 | const eventChangeCall = generateChangeEventCall( 56 | event.nodeIdentifier, 57 | event.eventName, 58 | callback, 59 | event.required && disableEventElements 60 | ); 61 | 62 | handleEventListenersFunction.statements.push(eventChangeCall); 63 | } 64 | 65 | return handleEventListenersFunction; 66 | } -------------------------------------------------------------------------------- /src/templating/constructs/for.ts: -------------------------------------------------------------------------------- 1 | import { BindingAspect, Locals, VariableReferenceArray, PartialBinding, ITemplateData, ITemplateConfig } from "../template"; 2 | import { ForStatement, ForStatementExpression } from "../../chef/javascript/components/statements/for"; 3 | import { addIdentifierToElement, addBinding } from "../helpers"; 4 | import { VariableReference } from "../../chef/javascript/components/value/expression"; 5 | import { parseNode } from "../template"; 6 | import { HTMLComment, HTMLElement } from "../../chef/html/html"; 7 | import { assignToObjectMap } from "../../helpers"; 8 | 9 | export function parseForNode( 10 | element: HTMLElement, 11 | templateData: ITemplateData, 12 | templateConfig: ITemplateConfig, 13 | globals: Array, 14 | localData: Locals, 15 | nullable: boolean, 16 | multiple: boolean 17 | ) { 18 | const value = element.attributes!.get("#for"); 19 | if (!value) { 20 | throw Error("Expected value for #for construct") 21 | } 22 | 23 | const expression = ForStatement.parseForParameter(value); 24 | if (expression instanceof ForStatementExpression) { 25 | throw Error("#for construct only supports iterator expression"); 26 | } 27 | 28 | assignToObjectMap(templateData.nodeData, element, "iteratorExpression", expression); 29 | 30 | const subjectReference = (expression.subject as VariableReference).toChain(); 31 | 32 | // Parent identifier 33 | if (!multiple) { 34 | addIdentifierToElement(element, templateData.nodeData); 35 | } 36 | 37 | // Deals with nested arrays: 38 | let fromLocal: VariableReferenceArray = subjectReference; 39 | if (localData.some(local => local.name === (expression.subject as VariableReference).name)) { 40 | fromLocal = localData.find(local => local.name === (expression.subject as VariableReference).name)!.path; 41 | } 42 | 43 | if (element.children.filter(child => !(child instanceof HTMLComment)).length > 1) { 44 | throw Error("#for construct element must be single child"); 45 | } 46 | 47 | const newLocals: Locals = [ 48 | ...localData, 49 | { 50 | name: expression.variable.name!, 51 | path: [...fromLocal, { aspect: "*", alias: expression.variable.name, origin: element }] 52 | } 53 | ]; 54 | 55 | const binding: PartialBinding = { aspect: BindingAspect.Iterator, element, expression, } 56 | 57 | addBinding(binding, localData, globals, templateData.bindings); 58 | 59 | for (const child of element.children) { 60 | parseNode( 61 | child, 62 | templateData, 63 | templateConfig, 64 | globals, 65 | newLocals, 66 | nullable, 67 | true 68 | ); 69 | } 70 | element.attributes!.delete("#for"); 71 | } -------------------------------------------------------------------------------- /src/templating/constructs/if.ts: -------------------------------------------------------------------------------- 1 | import { BindingAspect, Locals, PartialBinding, ITemplateConfig, ITemplateData } from "../template"; 2 | import { Expression, VariableReference } from "../../chef/javascript/components/value/expression"; 3 | import { addIdentifierToElement, addBinding, createNullElseElement } from "../helpers"; 4 | import { parseNode } from "../template"; 5 | import { HTMLElement } from "../../chef/html/html"; 6 | import { assignToObjectMap } from "../../helpers"; 7 | 8 | export function parseIfNode( 9 | element: HTMLElement, 10 | templateData: ITemplateData, 11 | templateConfig: ITemplateConfig, 12 | globals: Array, 13 | locals: Locals, 14 | multiple: boolean, 15 | ) { 16 | if (multiple) { 17 | throw Error("Not implemented - #if node under a #for element") 18 | } 19 | 20 | const value = element.attributes!.get("#if"); 21 | if (!value) { 22 | throw Error("Expected value for #if construct") 23 | } 24 | 25 | const expression = Expression.fromString(value); 26 | assignToObjectMap(templateData.nodeData, element, "conditionalExpression", expression); 27 | 28 | const identifier = addIdentifierToElement(element, templateData.nodeData); 29 | assignToObjectMap(templateData.nodeData, element, "nullable", true); 30 | 31 | const binding: PartialBinding = { 32 | aspect: BindingAspect.Conditional, 33 | expression, 34 | element 35 | } 36 | 37 | addBinding(binding, locals, globals, templateData.bindings); 38 | 39 | for (const child of element.children) { 40 | parseNode( 41 | child, 42 | templateData, 43 | templateConfig, 44 | globals, 45 | locals, 46 | true, 47 | multiple 48 | ); 49 | } 50 | 51 | element.attributes!.delete("#if"); 52 | 53 | // Skip over comments in between #if and #else 54 | let elseElement: HTMLElement | null = element.next as HTMLElement; 55 | 56 | if (elseElement && elseElement instanceof HTMLElement && elseElement.attributes?.has("#else")) { 57 | elseElement.attributes.delete("#else"); 58 | parseNode(elseElement, templateData, templateConfig, globals, locals, true, multiple); 59 | elseElement.attributes.set("data-else", null); 60 | // Add a (possibly second) identifer to elseElement. It is the same identifer used for the #if element 61 | // and simplifies runtime by having a single id element to swap 62 | if (elseElement.attributes.has("class")) { 63 | elseElement.attributes.set("class", elseElement.attributes.get("class") + " " + identifier); 64 | } else { 65 | elseElement.attributes.set("class", identifier); 66 | } 67 | 68 | // Remove else node from being rendered normally as it will be created under the function 69 | elseElement.parent!.children.splice(elseElement.parent!.children.indexOf(elseElement), 1); 70 | } else { 71 | elseElement = createNullElseElement(identifier); 72 | assignToObjectMap(templateData.nodeData, elseElement, "nullable", true); 73 | } 74 | 75 | // TODO is elseElement used??? 76 | assignToObjectMap(templateData.nodeData, element, "elseElement", elseElement); 77 | } -------------------------------------------------------------------------------- /src/templating/template.ts: -------------------------------------------------------------------------------- 1 | import { TextNode, HTMLElement, Node } from "../chef/html/html"; 2 | import type { ValueTypes } from "../chef/javascript/components/value/value"; 3 | import type { VariableReference } from "../chef/javascript/components/value/expression"; 4 | import type { ForLoopExpression, ForIteratorExpression } from "../chef/javascript/components/statements/for"; 5 | import { Component } from "../component"; 6 | import { parseHTMLElement } from "./html-element"; 7 | import { parseTextNode } from "./text-node"; 8 | import { FunctionDeclaration } from "../chef/javascript/components/constructs/function"; 9 | 10 | /** 11 | * Represents a event 12 | */ 13 | export interface IEvent { 14 | nodeIdentifier: string, 15 | element: HTMLElement, 16 | eventName: string, 17 | callback: VariableReference, 18 | required: boolean, // If required for logic to work, if true will be disabled on ssr, 19 | existsOnComponentClass: boolean, // True if the callback points to a method on the component class 20 | } 21 | 22 | /** 23 | * Extends the HTMLElement interface adding new properties used in Prism template syntax 24 | */ 25 | interface FullNodeData { 26 | component: Component // Whether the element is a external component 27 | dynamicAttributes: Map // Attributes of an element which are linked to data 28 | events: Array // Events the element has 29 | identifier: string // A identifier used for lookup of the element 30 | slotFor: string // If slot the key of content that should be there 31 | nullable: boolean // True if the element is not certain to exist in the DOM 32 | multiple: boolean // If the element can exist multiple times in the DOM 33 | 34 | rawAttribute: ValueTypes, // A name of a variable that does
35 | rawInnerHTML: ValueTypes, // Raw (unescaped) innerHTML value 36 | 37 | // A expression that has to return a truthy value to render (also used for determine that it was a #if node) 38 | conditionalExpression: ValueTypes, 39 | // A expression that is used for iteration over children (also used for determine that it was a #for node) 40 | iteratorExpression: ForIteratorExpression, 41 | 42 | // A method that renders itself or its children, used for #if and #for node 43 | clientRenderMethod: FunctionDeclaration, 44 | 45 | elseElement: HTMLElement, // If #if points to the #else element 46 | 47 | // For TextNodes: 48 | textNodeValue: ValueTypes; // A expression value for its text content 49 | // For HTMLComments: 50 | isFragment: true // If the comment is used to break up text nodes for ssr hydration 51 | } 52 | 53 | export type NodeData = Partial; 54 | 55 | // Explains what a variable affects 56 | export enum BindingAspect { 57 | Attribute, // Affects a specific attribute of a node 58 | Data, // A components data 59 | InnerText, // Affects the inner text value of a node 60 | InnerHTML, // Affects raw inner HTML 61 | Iterator, // Affects the number of a children under a node / iterator 62 | Conditional, // Affects if a node is rendered TODO not visible but exists 63 | DocumentTitle, // Affects the document title 64 | SetHook, // Hook to method on a class 65 | Style, // A css style 66 | ServerParameter // Used in referencing the 67 | } 68 | 69 | // Represents a link between data and a element 70 | export interface IBinding { 71 | expression: ValueTypes | ForLoopExpression, // The expression that is the mutation of the variable 72 | aspect: BindingAspect, // The aspect the variable affects 73 | element?: HTMLElement, // The element that is around the binding. Used to see if the element is multiple or nullable 74 | fragmentIndex?: number, // The index of the fragment to edit 75 | attribute?: string, // If aspect is a attribute then the name of the attribute 76 | styleKey?: string, // 77 | referencesVariables: Array, 78 | } 79 | 80 | export type PartialBinding = Omit; 81 | 82 | export interface ForLoopVariable { 83 | aspect: "*", 84 | alias: string, 85 | origin: HTMLElement 86 | } 87 | 88 | export type VariableReferenceArray = Array; 89 | export type Locals = Array<{ name: string, path: VariableReferenceArray }>; 90 | 91 | export interface ITemplateData { 92 | slots: Map, 93 | nodeData: WeakMap 94 | bindings: Array, 95 | events: Array, 96 | hasSVG: boolean 97 | } 98 | 99 | export interface ITemplateConfig { 100 | staticSrc: string, 101 | ssrEnabled: boolean, 102 | tagNameToComponentMap: Map, 103 | doClientSideRouting: boolean 104 | } 105 | 106 | /** 107 | * Parse the