├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── assets ├── asbind-0-1-0.wat ├── asbind-debug-0-1-0.wat ├── asbind.gif ├── asbind.webm ├── markdown-parser-0-1-0.wat └── mdParserScreenshot.png ├── examples ├── browser-sdk │ └── index.html ├── markdown-parser │ ├── assembly │ │ ├── as-mocks-banner.js │ │ ├── code-generator │ │ │ └── code-generator.ts │ │ ├── index.ts │ │ ├── parser │ │ │ ├── ast-node-type.ts │ │ │ ├── ast.ts │ │ │ └── parser.ts │ │ ├── tokenizer │ │ │ ├── token-type.ts │ │ │ ├── token.ts │ │ │ └── tokenizer.ts │ │ ├── tsconfig.json │ │ └── util.ts │ ├── index.css │ ├── index.html │ └── index.js └── quickstart │ ├── browser-puppeteer.js │ ├── browser.html │ ├── browser.js │ ├── nodejs.js │ ├── package-lock.json │ ├── package.json │ ├── path-to-my-wasm.wasm │ └── your-entryfile.ts ├── index.js ├── lib ├── asbind-instance │ ├── asbind-instance.ts │ ├── bind-function.ts │ ├── instantiate.ts │ ├── reserved-export-keys.ts │ └── type-converters.ts ├── lib.ts └── types.ts ├── package-lock.json ├── package.json ├── rollup.config.js ├── rollup.lib.js ├── rollup.markdown-parser.js ├── test ├── .gitignore ├── test-runner.html ├── test-runner.js └── tests │ ├── array │ ├── asc.ts │ └── test.js │ ├── arraybuffer │ ├── asc.ts │ └── test.js │ ├── arraybufferview │ ├── asc.ts │ └── test.js │ ├── custom-type │ ├── asc.ts │ └── test.js │ ├── import-on-prototype │ ├── asc.ts │ └── test.js │ ├── instantiateSync │ ├── asc.ts │ └── test.js │ ├── multifile │ ├── asc.ts │ ├── exports.ts │ ├── imports.ts │ └── test.js │ ├── namespace-import │ ├── asc.ts │ └── test.js │ ├── namespace │ ├── asc.ts │ └── test.js │ ├── pinning │ ├── asc.ts │ ├── config.js │ └── test.js │ ├── strings │ ├── asc.ts │ └── test.js │ ├── tsconfig.json │ ├── unexpected-import │ ├── asc.ts │ └── test.js │ ├── unknown-type │ ├── asc.ts │ └── test.js │ └── unused-import │ ├── asc.ts │ └── test.js ├── transform.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Project 2 | build/ 3 | dist/ 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # next.js build output 65 | .next 66 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | test/tests/namespace-import/asc.ts 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "semi": true, 4 | "arrowParens": "avoid", 5 | "printWidth": 80, 6 | "bracketSpacing": true 7 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | email: false 3 | language: node_js 4 | node_js: 5 | - "node" 6 | install: 7 | - npm install 8 | script: 9 | - npm run lint:ci 10 | - npm run build 11 | - npm run test 12 | # Commenting for now due to small AS/TS Compiler difference 13 | # Opening a PR as I write this to fix it haha! 14 | # - npm run md:build 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Aaron Turner 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # as-bind 2 | 3 | 4 | 5 | [![Build Status](https://travis-ci.org/torch2424/as-bind.svg?branch=master)](https://travis-ci.org/torch2424/as-bind) 6 | ![npm bundle size (minified)](https://img.shields.io/bundlephobia/min/as-bind.svg) 7 | ![npm](https://img.shields.io/npm/dt/as-bind.svg) 8 | ![npm version](https://img.shields.io/npm/v/as-bind.svg) 9 | ![GitHub](https://img.shields.io/github/license/torch2424/as-bind.svg) 10 | 11 | Isomorphic library to handle passing high-level data structures between AssemblyScript and JavaScript. 🤝🚀 12 | 13 | [Markdown Parser Demo](https://torch2424.github.io/as-bind/) 14 | 15 | ![Asbind Markdown Parser Demo Gif](./assets/asbind.gif) 16 | 17 | ## Table of Contents 18 | 19 | - [Features](#features) 20 | - [Installation](#installation) 21 | - [Quick Start](#quick-start) 22 | - [Additional Examples](#additional-examples) 23 | - [Supported Data Types](#supported-data-types) 24 | - [Supported AssemblyScript Runtime Variants](#supported-assemblyscript-runtime-variants) 25 | - [Reference API](#reference-api) 26 | - [Motivation](#motivation) 27 | - [Performance](#performance) 28 | - [Production](#production) 29 | - [Projects using as-bind](#projects-using-as-bind) 30 | - [Contributing](#contributing) 31 | - [License](#license) 32 | 33 | ## Features 34 | 35 | - The library is isomorphic. Meaning it supports both the Browser, and Node! And has ESM, AMD, CommonJS, and IIFE bundles! 🌐 36 | - Wraps around the [AssemblyScript Loader](https://github.com/AssemblyScript/assemblyscript/tree/master/lib/loader). The loader handles all the heavy-lifting of passing data into WebAssembly linear memory. 💪 37 | - Wraps around imported JavaScript functions, and exported AssemblyScript functions of the AssemblyScript Wasm Module. This allows high-level data types to be passed directly to exported AssemblyScript functions! 🤯 38 | - Moves a lot of work to compile-time using [AssemblyScript Transforms](https://www.assemblyscript.org/transforms.html#transforms) and completely avoids module-specific “glue code”. 🏃 39 | - Installable from package managers (npm), with a modern JavaScript API syntax. 📦 40 | - The library is [< 4KB (minified and gzip'd)](https://bundlephobia.com/result?p=as-bind), _including_ the AssemblyScript Loader ! 🌲 41 | - This library is currently (as of January, 2020) the [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) of AssemblyScript. 😀 42 | 43 | ## Installation 44 | 45 | You can install as-bind in your project by running the following: 46 | 47 | `npm install --save as-bind` 48 | 49 | ## Quick Start 50 | 51 | **1. Compiling your Assemblyscript** 52 | 53 | To enable as-bind for your AssemblyScript Wasm modules, add the as-bind transform when compiling your module: 54 | 55 | `asc your-entryfile.ts --exportRuntime --transform as-bind [...other cli options...]` 56 | 57 | The things to notice are: 58 | 59 | - `--transform as-bind` - This is the as-bind transform that runs at compile time. It embeds all the required type information into the WebAssembly Module. 60 | - `--exportRuntime` - This is required for the AssemblyScript Loader to work properly. It [exposes functions](https://www.assemblyscript.org/garbage-collection.html#runtime-interface) on the module to allocate memory from JavaScript. 61 | 62 | For **optional testing purposes** , let's export an example function we can try in `your-entryfile.ts`: 63 | 64 | ```typescript 65 | export function myExportedFunctionThatTakesAString(value: string): string { 66 | return "AsBind: " + value; 67 | } 68 | ``` 69 | 70 | **2. In your Javascript** 71 | 72 | For **browser** JavaScript. We can do the following: 73 | 74 | ```javascript 75 | // If you are using a Javascript bundler, use the ESM bundle with import syntax 76 | import * as AsBind from "as-bind"; 77 | 78 | // If you are not using a bundler add a 10 | 11 | 12 | 74 |

See the browser console!

75 | 76 | 77 | -------------------------------------------------------------------------------- /examples/markdown-parser/assembly/as-mocks-banner.js: -------------------------------------------------------------------------------- 1 | let idofCounter = 0; 2 | const idof = () => idofCounter++; 3 | -------------------------------------------------------------------------------- /examples/markdown-parser/assembly/code-generator/code-generator.ts: -------------------------------------------------------------------------------- 1 | import { log } from "../util"; 2 | 3 | import { Token } from "../tokenizer/token"; 4 | import { TokenType } from "../tokenizer/token-type"; 5 | 6 | import { AstNode } from "../parser/ast"; 7 | import { AstNodeType } from "../parser/ast-node-type"; 8 | 9 | export function generateHtmlString(ast: Array): string { 10 | let htmlString: string = ""; 11 | 12 | for (let i = 0; i < ast.length; i++) { 13 | htmlString += generateHtmlStringForAstNode(ast[i], ast, i); 14 | } 15 | 16 | return htmlString; 17 | } 18 | 19 | function generateHtmlStringForAstNode( 20 | astNode: AstNode, 21 | ast: Array, 22 | index: i32 23 | ): string { 24 | let htmlString: string = ""; 25 | 26 | if (astNode.type == AstNodeType.NEWLINE) { 27 | htmlString = "
"; 28 | return htmlString; 29 | } 30 | 31 | // WhiteSpace value should be used 32 | 33 | if (astNode.type == AstNodeType.HEADER) { 34 | let headerTag = "h" + astNode.value; 35 | 36 | htmlString = "<" + headerTag + ">"; 37 | htmlString += generateHtmlString(astNode.childNodes); 38 | htmlString += ""; 39 | return htmlString; 40 | } 41 | 42 | if (astNode.type == AstNodeType.ITALICS) { 43 | htmlString = ""; 44 | htmlString += generateHtmlString(astNode.childNodes); 45 | htmlString += ""; 46 | return htmlString; 47 | } 48 | 49 | if (astNode.type == AstNodeType.BOLD) { 50 | htmlString = ""; 51 | htmlString += generateHtmlString(astNode.childNodes); 52 | htmlString += ""; 53 | return htmlString; 54 | } 55 | 56 | if (astNode.type == AstNodeType.STRIKETHROUGH) { 57 | htmlString = ""; 58 | htmlString += generateHtmlString(astNode.childNodes); 59 | htmlString += ""; 60 | return htmlString; 61 | } 62 | 63 | if (astNode.type == AstNodeType.UNORDERED_LIST) { 64 | htmlString = ""; 67 | return htmlString; 68 | } 69 | 70 | if (astNode.type == AstNodeType.ORDERED_LIST) { 71 | htmlString = "
    "; 72 | htmlString += generateHtmlString(astNode.childNodes); 73 | htmlString += "
"; 74 | return htmlString; 75 | } 76 | 77 | if (astNode.type == AstNodeType.LIST_ITEM) { 78 | htmlString = "
  • "; 79 | htmlString += generateHtmlString(astNode.childNodes); 80 | htmlString += "
  • "; 81 | return htmlString; 82 | } 83 | 84 | if (astNode.type == AstNodeType.IMAGE) { 85 | htmlString = "'; 95 | htmlString += astNode.childNodes[0].value; 96 | htmlString += ""; 97 | return htmlString; 98 | } 99 | 100 | if (astNode.type == AstNodeType.BLOCK_QUOTE) { 101 | htmlString = ""; 102 | htmlString += astNode.value; 103 | htmlString += ""; 104 | return htmlString; 105 | } 106 | 107 | if ( 108 | astNode.type == AstNodeType.CODE_BLOCK || 109 | astNode.type == AstNodeType.INLINE_CODE 110 | ) { 111 | htmlString = ""; 112 | htmlString += astNode.value; 113 | htmlString += ""; 114 | return htmlString; 115 | } 116 | 117 | if (astNode.type == AstNodeType.HORIZONTAL_LINE) { 118 | htmlString = "
    "; 119 | return htmlString; 120 | } 121 | 122 | // Character value should be used 123 | 124 | // It must be a node with a 1:1 value, return the value 125 | htmlString = astNode.value; 126 | 127 | return htmlString; 128 | } 129 | -------------------------------------------------------------------------------- /examples/markdown-parser/assembly/index.ts: -------------------------------------------------------------------------------- 1 | // Wasm module to do a basic markdown to HTML 2 | 3 | import { log } from "./util"; 4 | 5 | import { Token } from "./tokenizer/token"; 6 | import { markdownTokenizer } from "./tokenizer/tokenizer"; 7 | 8 | import { AstNode } from "./parser/ast"; 9 | import { markdownTokenParser } from "./parser/parser"; 10 | 11 | import { generateHtmlString } from "./code-generator/code-generator"; 12 | 13 | // https://www.geeksforgeeks.org/compiler-design-phases-compiler/ 14 | // https://github.com/jamiebuilds/the-super-tiny-compiler/blob/master/the-super-tiny-compiler.js 15 | 16 | // TODO: Performance: "also the AS module might grow memory multiple times, which also adds to the overall time, while js doesn't have to do this" - dcode 17 | // Maybe to a memory grow here? 18 | 19 | function printTokens(tokens: Array): void { 20 | log("Tokens lengths:" + tokens.length.toString()); 21 | for (let i = 0; i < tokens.length; i++) { 22 | log("Token Type: " + tokens[i].type); 23 | log("Token index: " + tokens[i].index.toString()); 24 | log("Token value: " + tokens[i].value); 25 | } 26 | } 27 | 28 | function printAst(ast: Array): void { 29 | log("AST length:" + ast.length.toString()); 30 | for (let i = 0; i < ast.length; i++) { 31 | log("ast type: " + ast[i].type); 32 | log("ast value: " + ast[i].value); 33 | log("ast number of children: " + ast[i].childNodes.length.toString()); 34 | if (ast[i].childNodes.length > 0) { 35 | log("printing childNodes..."); 36 | log("------"); 37 | printAst(ast[i].childNodes); 38 | log("------"); 39 | } 40 | } 41 | } 42 | 43 | export function convertMarkdownToHTML(markdown: string): string { 44 | // Turn the text into seperate tokens 45 | let tokens: Array = markdownTokenizer(markdown); 46 | // printTokens(tokens); 47 | 48 | // Parse the tokens into an AST 49 | let ast: Array = markdownTokenParser(tokens); 50 | // printAst(ast); 51 | 52 | // Generate code (HTML) from our AST 53 | let htmlString = generateHtmlString(ast); 54 | 55 | return htmlString; 56 | } 57 | -------------------------------------------------------------------------------- /examples/markdown-parser/assembly/parser/ast-node-type.ts: -------------------------------------------------------------------------------- 1 | export class AstNodeType { 2 | static readonly NEWLINE: string = "NewLine"; 3 | static readonly WHITESPACE: string = "Whitespace"; 4 | static readonly HEADER: string = "Header"; 5 | static readonly ITALICS: string = "Italics"; 6 | static readonly BOLD: string = "Bold"; 7 | static readonly STRIKETHROUGH: string = "Strikethrough"; 8 | static readonly UNORDERED_LIST: string = "UnorderedList"; 9 | static readonly ORDERED_LIST: string = "OrderedList"; 10 | static readonly LIST_ITEM: string = "ListItem"; 11 | static readonly IMAGE: string = "Image"; 12 | static readonly LINK: string = "Link"; 13 | static readonly BLOCK_QUOTE: string = "BlockQuote"; 14 | static readonly CODE_BLOCK: string = "CodeBlock"; 15 | static readonly INLINE_CODE: string = "InlineCode"; 16 | static readonly HORIZONTAL_LINE: string = "HorizontalLine"; 17 | static readonly CHARACTER: string = "Character"; 18 | } 19 | -------------------------------------------------------------------------------- /examples/markdown-parser/assembly/parser/ast.ts: -------------------------------------------------------------------------------- 1 | export class AstNode { 2 | type: string = ""; 3 | value: string = ""; 4 | childNodes: Array = new Array(); 5 | } 6 | -------------------------------------------------------------------------------- /examples/markdown-parser/assembly/parser/parser.ts: -------------------------------------------------------------------------------- 1 | import { log } from "../util"; 2 | 3 | import { Token } from "../tokenizer/token"; 4 | import { TokenType } from "../tokenizer/token-type"; 5 | 6 | import { AstNode } from "./ast"; 7 | import { AstNodeType } from "./ast-node-type"; 8 | 9 | function addTokensToAst(tokens: Array, ast: Array): void { 10 | for (let i: i32 = 0; i < tokens.length; i++) { 11 | let indexOffset = addAstNode(ast, tokens, i); 12 | i += indexOffset; 13 | } 14 | } 15 | 16 | export function markdownTokenParser(tokens: Array): Array { 17 | let ast = new Array(0); 18 | 19 | addTokensToAst(tokens, ast); 20 | 21 | return ast; 22 | } 23 | 24 | function getNewAstNode(): AstNode { 25 | // AS Bugfix: Make sure the child array for the AstNode is properly initialized 26 | // E.g, Make sure the child nodes of the ast is properly allocated as a new AST 27 | let astNode: AstNode = new AstNode(); 28 | astNode.childNodes = new Array(0); 29 | return astNode; 30 | } 31 | 32 | function addAstNode( 33 | ast: Array, 34 | tokens: Array, 35 | tokenIndex: i32 36 | ): i32 { 37 | let astNode: AstNode = getNewAstNode(); 38 | let token: Token = tokens[tokenIndex]; 39 | 40 | if (token.type == TokenType.NEWLINE) { 41 | astNode.type = AstNodeType.NEWLINE; 42 | astNode.value = token.value; 43 | ast.push(astNode); 44 | return 0; 45 | } 46 | 47 | if (token.type == TokenType.WHITESPACE) { 48 | astNode.type = AstNodeType.WHITESPACE; 49 | astNode.value = token.value; 50 | ast.push(astNode); 51 | return 0; 52 | } 53 | 54 | if (token.type == TokenType.HEADER) { 55 | // Initialize the offset for finding the header 56 | let indexOffset = 0; 57 | 58 | // Find the level of the header 59 | let headerLevel = 1; 60 | while ( 61 | tokenIndex + indexOffset + 1 < tokens.length - 1 && 62 | tokens[tokenIndex + indexOffset + 1].type === TokenType.HEADER 63 | ) { 64 | headerLevel += 1; 65 | indexOffset += 1; 66 | } 67 | 68 | // Check if the next value is whitespace 69 | if (tokens[tokenIndex + indexOffset + 1].type === TokenType.WHITESPACE) { 70 | // We have a header 71 | 72 | // Increase the offset once more 73 | indexOffset += 1; 74 | 75 | // Get the content of the header 76 | let contentTokens: Array = getAllTokensUntilTokenReached( 77 | tokens, 78 | tokenIndex + indexOffset + 1, 79 | TokenType.NEWLINE 80 | ); 81 | let content: string = getTokensAsString(contentTokens); 82 | let offsetTokenLength: i32 = contentTokens.length; 83 | 84 | astNode.type = AstNodeType.HEADER; 85 | astNode.value = headerLevel.toString(); 86 | 87 | indexOffset += offsetTokenLength; 88 | 89 | ast.push(astNode); 90 | 91 | // Go through the child tokens as well 92 | addTokensToAst(contentTokens, astNode.childNodes); 93 | 94 | return offsetTokenLength + headerLevel; 95 | } 96 | } 97 | 98 | if (token.type == TokenType.ITALICS) { 99 | if ( 100 | checkIfTypeIsFoundBeforeOtherType( 101 | tokens, 102 | tokenIndex + 1, 103 | TokenType.ITALICS, 104 | TokenType.NEWLINE 105 | ) 106 | ) { 107 | let contentTokens: Array = getAllTokensUntilTokenReached( 108 | tokens, 109 | tokenIndex + 1, 110 | TokenType.ITALICS 111 | ); 112 | let content: string = getTokensAsString(contentTokens); 113 | let offsetTokenLength: i32 = contentTokens.length; 114 | 115 | astNode.type = AstNodeType.ITALICS; 116 | 117 | // Go through the child tokens as well 118 | addTokensToAst(contentTokens, astNode.childNodes); 119 | 120 | ast.push(astNode); 121 | return offsetTokenLength + 1; 122 | } 123 | } 124 | 125 | if (token.type == TokenType.BOLD) { 126 | if ( 127 | checkIfTypeIsFoundBeforeOtherType( 128 | tokens, 129 | tokenIndex + 1, 130 | TokenType.BOLD, 131 | TokenType.NEWLINE 132 | ) 133 | ) { 134 | let contentTokens: Array = getAllTokensUntilTokenReached( 135 | tokens, 136 | tokenIndex + 1, 137 | TokenType.BOLD 138 | ); 139 | let content: string = getTokensAsString(contentTokens); 140 | let offsetTokenLength: i32 = contentTokens.length; 141 | 142 | astNode.type = AstNodeType.BOLD; 143 | 144 | // Go through the child tokens as well 145 | addTokensToAst(contentTokens, astNode.childNodes); 146 | 147 | ast.push(astNode); 148 | return offsetTokenLength + 1; 149 | } 150 | } 151 | 152 | if (token.type == TokenType.STRIKETHROUGH) { 153 | if ( 154 | checkIfTypeIsFoundBeforeOtherType( 155 | tokens, 156 | tokenIndex + 1, 157 | TokenType.STRIKETHROUGH, 158 | TokenType.NEWLINE 159 | ) 160 | ) { 161 | let contentTokens: Array = getAllTokensUntilTokenReached( 162 | tokens, 163 | tokenIndex + 1, 164 | TokenType.STRIKETHROUGH 165 | ); 166 | let content: string = getTokensAsString(contentTokens); 167 | let offsetTokenLength: i32 = contentTokens.length; 168 | 169 | astNode.type = AstNodeType.STRIKETHROUGH; 170 | 171 | // Go through the child tokens as well 172 | addTokensToAst(contentTokens, astNode.childNodes); 173 | 174 | ast.push(astNode); 175 | return offsetTokenLength + 1; 176 | } 177 | } 178 | 179 | // Doing lists as one, as they share list item logic 180 | if ( 181 | token.type == TokenType.UNORDERED_LIST_ITEM || 182 | token.type == TokenType.ORDERED_LIST_ITEM 183 | ) { 184 | // Set up the item type we are looking for 185 | let listItemType: string = ""; 186 | 187 | // Set our list node, and add it to the parent ast. 188 | if (token.type == TokenType.UNORDERED_LIST_ITEM) { 189 | astNode.type = AstNodeType.UNORDERED_LIST; 190 | listItemType = TokenType.UNORDERED_LIST_ITEM; 191 | } else { 192 | astNode.type = AstNodeType.ORDERED_LIST; 193 | listItemType = TokenType.ORDERED_LIST_ITEM; 194 | } 195 | ast.push(astNode); 196 | 197 | // Need to add all the list items to our list 198 | let tokensToSkip = 0; 199 | let tokensSkippedForWhitespace = 0; 200 | let listItemTokenIndex = tokenIndex; 201 | while ( 202 | listItemTokenIndex + tokensSkippedForWhitespace < tokens.length && 203 | tokens[listItemTokenIndex + tokensSkippedForWhitespace].type == 204 | listItemType 205 | ) { 206 | // Add the tokens we skipped for whitespace to our other skip/index values 207 | tokensToSkip += tokensSkippedForWhitespace; 208 | listItemTokenIndex += tokensSkippedForWhitespace; 209 | 210 | // Create the node for the list item 211 | let listItemAstNode: AstNode = getNewAstNode(); 212 | listItemAstNode.type = AstNodeType.LIST_ITEM; 213 | 214 | // Get its content tokens 215 | let contentTokens: Array = getAllTokensUntilTokenReached( 216 | tokens, 217 | listItemTokenIndex + 1, 218 | TokenType.NEWLINE 219 | ); 220 | let content: string = getTokensAsString(contentTokens); 221 | let offsetTokenLength: i32 = contentTokens.length; 222 | 223 | // Add the content to the list item 224 | addTokensToAst(contentTokens, listItemAstNode.childNodes); 225 | 226 | // Add the list item to the list 227 | astNode.childNodes.push(listItemAstNode); 228 | 229 | let nextTokenIndex = offsetTokenLength + 2; 230 | 231 | // Increase our values to keep looking through nodes! 232 | tokensToSkip += nextTokenIndex; 233 | listItemTokenIndex += nextTokenIndex; 234 | 235 | // Need to handle whitespace here for tabbing as well 236 | tokensSkippedForWhitespace = 0; 237 | while ( 238 | listItemTokenIndex + tokensSkippedForWhitespace < tokens.length && 239 | tokens[listItemTokenIndex + tokensSkippedForWhitespace].type == 240 | TokenType.WHITESPACE 241 | ) { 242 | tokensSkippedForWhitespace += 1; 243 | } 244 | } 245 | 246 | return tokensToSkip; 247 | } 248 | 249 | // Let's look for images 250 | if (token.type == TokenType.IMAGE_START) { 251 | let altTokens: Array = getAllTokensUntilTokenReached( 252 | tokens, 253 | tokenIndex + 1, 254 | TokenType.BRACKET_END 255 | ); 256 | let altText: string = getTokensAsString(altTokens); 257 | let altTextOffsetTokenLength: i32 = altTokens.length; 258 | 259 | let altTextAstNode = getNewAstNode(); 260 | altTextAstNode.type = "Alt"; 261 | altTextAstNode.value = altText; 262 | 263 | // We have the alt text, if this is an image 264 | // We need to check if this is immediately followed by a parentheses 265 | if ( 266 | tokens[tokenIndex + altTextOffsetTokenLength + 2].type == 267 | TokenType.PAREN_START 268 | ) { 269 | let imageTokens: Array = getAllTokensUntilTokenReached( 270 | tokens, 271 | tokenIndex + altTextOffsetTokenLength + 3, 272 | TokenType.PAREN_END 273 | ); 274 | let imageUrl: string = getTokensAsString(imageTokens); 275 | let imageUrlOffsetTokenLength: i32 = imageTokens.length; 276 | 277 | // Let's create the Ast Node for the image 278 | astNode.type = AstNodeType.IMAGE; 279 | astNode.value = imageUrl; 280 | astNode.childNodes.push(altTextAstNode); 281 | 282 | ast.push(astNode); 283 | return altTextOffsetTokenLength + imageUrlOffsetTokenLength + 4; 284 | } 285 | } 286 | 287 | // Let's look for links 288 | if (token.type == TokenType.BRACKET_START) { 289 | let linkTokens: Array = getAllTokensUntilTokenReached( 290 | tokens, 291 | tokenIndex + 1, 292 | TokenType.BRACKET_END 293 | ); 294 | let linkContent: string = getTokensAsString(linkTokens); 295 | let linkContentOffsetTokenLength: i32 = linkTokens.length; 296 | 297 | let linkContentAstNode = getNewAstNode(); 298 | linkContentAstNode.type = "Link Content"; 299 | linkContentAstNode.value = linkContent; 300 | 301 | // We have the link content, if this is an link 302 | // We need to check if this is immediately followed by a parentheses 303 | if ( 304 | tokens[tokenIndex + linkContentOffsetTokenLength + 2].type == 305 | TokenType.PAREN_START 306 | ) { 307 | let urlTokens: Array = getAllTokensUntilTokenReached( 308 | tokens, 309 | tokenIndex + linkContentOffsetTokenLength + 3, 310 | TokenType.PAREN_END 311 | ); 312 | let urlContent: string = getTokensAsString(urlTokens); 313 | let urlContentOffsetTokenLength: i32 = urlTokens.length; 314 | 315 | // Let's create the Ast Node for the image 316 | astNode.type = AstNodeType.LINK; 317 | astNode.value = urlContent; 318 | astNode.childNodes.push(linkContentAstNode); 319 | 320 | ast.push(astNode); 321 | return linkContentOffsetTokenLength + urlContentOffsetTokenLength + 4; 322 | } 323 | } 324 | 325 | if (token.type == TokenType.BLOCK_QUOTE) { 326 | let contentTokens: Array = getAllTokensUntilTokenReached( 327 | tokens, 328 | tokenIndex + 1, 329 | TokenType.NEWLINE 330 | ); 331 | let content: string = getTokensAsString(contentTokens); 332 | let offsetTokenLength: i32 = contentTokens.length; 333 | 334 | astNode.type = AstNodeType.BLOCK_QUOTE; 335 | astNode.value = content; 336 | 337 | ast.push(astNode); 338 | return offsetTokenLength + 1; 339 | } 340 | 341 | if (token.type == TokenType.CODE_BLOCK) { 342 | let contentTokens: Array = getAllTokensUntilTokenReached( 343 | tokens, 344 | tokenIndex + 1, 345 | TokenType.CODE_BLOCK 346 | ); 347 | let content: string = getTokensAsString(contentTokens); 348 | let offsetTokenLength: i32 = contentTokens.length; 349 | 350 | astNode.type = AstNodeType.CODE_BLOCK; 351 | astNode.value = content; 352 | 353 | ast.push(astNode); 354 | return offsetTokenLength + 1; 355 | } 356 | 357 | if (token.type == TokenType.INLINE_CODE) { 358 | if ( 359 | checkIfTypeIsFoundBeforeOtherType( 360 | tokens, 361 | tokenIndex + 1, 362 | TokenType.INLINE_CODE, 363 | TokenType.NEWLINE 364 | ) 365 | ) { 366 | let contentTokens: Array = getAllTokensUntilTokenReached( 367 | tokens, 368 | tokenIndex + 1, 369 | TokenType.INLINE_CODE 370 | ); 371 | let content: string = getTokensAsString(contentTokens); 372 | let offsetTokenLength: i32 = contentTokens.length; 373 | 374 | astNode.type = AstNodeType.INLINE_CODE; 375 | astNode.value = content; 376 | 377 | ast.push(astNode); 378 | return offsetTokenLength + 1; 379 | } 380 | } 381 | 382 | if ( 383 | token.type == TokenType.HORIZONTAL_LINE && 384 | tokens[tokenIndex + 1].type == TokenType.NEWLINE 385 | ) { 386 | astNode.type = AstNodeType.HORIZONTAL_LINE; 387 | ast.push(astNode); 388 | return 0; 389 | } 390 | 391 | // It did not match our cases, let's assume the node is for characters 392 | astNode.type = AstNodeType.CHARACTER; 393 | astNode.value = token.value; 394 | ast.push(astNode); 395 | return 0; 396 | } 397 | 398 | // Returns an array of strings where: 399 | // 0: is the content between the start, and the found ending token. 400 | // 1: is the number of tokens found before reaching the end. 401 | function getOffsetOfTokensUntilTokenReached( 402 | tokens: Array, 403 | startTokenIndex: i32, 404 | stopTokenType: string 405 | ): Array { 406 | let contentTokens: Array = getAllTokensUntilTokenReached( 407 | tokens, 408 | startTokenIndex, 409 | stopTokenType 410 | ); 411 | 412 | let content: string = ""; 413 | for (let i = 0; i < contentTokens.length; i++) { 414 | content += contentTokens[i].value; 415 | } 416 | 417 | let response = new Array(); 418 | 419 | response.push(content); 420 | response.push(contentTokens.length.toString()); 421 | return response; 422 | } 423 | 424 | // Function to return tokens as a string 425 | function getTokensAsString(tokens: Array): string { 426 | let content: string = ""; 427 | for (let i = 0; i < tokens.length; i++) { 428 | content += tokens[i].value; 429 | } 430 | return content; 431 | } 432 | 433 | function checkIfTypeIsFoundBeforeOtherType( 434 | tokens: Array, 435 | startTokenIndex: i32, 436 | checkTokenType: string, 437 | otherTokenType: string 438 | ): boolean { 439 | let checkTokens: Array = getAllTokensUntilTokenReached( 440 | tokens, 441 | startTokenIndex, 442 | checkTokenType 443 | ); 444 | let otherTokens: Array = getAllTokensUntilTokenReached( 445 | tokens, 446 | startTokenIndex, 447 | otherTokenType 448 | ); 449 | 450 | if (checkTokens.length < otherTokens.length) { 451 | return true; 452 | } else { 453 | return false; 454 | } 455 | } 456 | 457 | function getAllTokensUntilTokenReached( 458 | tokens: Array, 459 | startTokenIndex: i32, 460 | stopTokenType: string 461 | ): Array { 462 | let responseTokens = new Array(); 463 | 464 | for (let i = startTokenIndex; i < tokens.length; i++) { 465 | let token = tokens[i]; 466 | if (token.type == stopTokenType) { 467 | i = tokens.length; 468 | } else { 469 | responseTokens.push(token); 470 | } 471 | } 472 | 473 | return responseTokens; 474 | } 475 | -------------------------------------------------------------------------------- /examples/markdown-parser/assembly/tokenizer/token-type.ts: -------------------------------------------------------------------------------- 1 | export class TokenType { 2 | static readonly NEWLINE: string = "NewLine"; 3 | static readonly WHITESPACE: string = "Whitespace"; 4 | static readonly HEADER: string = "Header"; 5 | static readonly ITALICS: string = "Italics"; 6 | static readonly BOLD: string = "Bold"; 7 | static readonly STRIKETHROUGH: string = "Strikethrough"; 8 | static readonly UNORDERED_LIST_ITEM: string = "UnorderedListItem"; 9 | static readonly ORDERED_LIST_ITEM: string = "OrderedListItem"; 10 | static readonly IMAGE_START: string = "ImageStart"; 11 | static readonly BRACKET_START: string = "BracketStart"; 12 | static readonly BRACKET_END: string = "BracketEnd"; 13 | static readonly PAREN_START: string = "ParenStart"; 14 | static readonly PAREN_END: string = "ParenEnd"; 15 | static readonly BLOCK_QUOTE: string = "BlockQuote"; 16 | static readonly CODE_BLOCK: string = "CodeBlock"; 17 | static readonly INLINE_CODE: string = "InlineCode"; 18 | static readonly HORIZONTAL_LINE: string = "HorizontalLine"; 19 | static readonly CHARACTER: string = "Character"; 20 | } 21 | -------------------------------------------------------------------------------- /examples/markdown-parser/assembly/tokenizer/token.ts: -------------------------------------------------------------------------------- 1 | export class Token { 2 | index: i32 = 0; 3 | type: string = ""; 4 | value: string = ""; 5 | } 6 | -------------------------------------------------------------------------------- /examples/markdown-parser/assembly/tokenizer/tokenizer.ts: -------------------------------------------------------------------------------- 1 | import { log } from "../util"; 2 | 3 | import { Token } from "./token"; 4 | import { TokenType } from "./token-type"; 5 | 6 | let tokens = new Array(0); 7 | 8 | function isWhitespace(character: string): bool { 9 | return character.includes(" "); 10 | } 11 | 12 | function checkForTriplet( 13 | character: string, 14 | index: i32, 15 | markdown: string 16 | ): bool { 17 | return ( 18 | markdown.charAt(index + 0).includes(character) && 19 | markdown.charAt(index + 1).includes(character) && 20 | markdown.charAt(index + 2).includes(character) 21 | ); 22 | } 23 | 24 | function addToken(markdown: string, tokenIndex: i32, tokenValue: string): i32 { 25 | let token = new Token(); 26 | token.index = tokenIndex; 27 | token.value = tokenValue; 28 | 29 | // We care about newlines, as they specify blocks, and whether something is in the same newline 30 | if (tokenValue.includes("\n")) { 31 | token.type = TokenType.NEWLINE; 32 | 33 | tokens.push(token); 34 | return 0; 35 | } 36 | 37 | // Check for whitespace 38 | if (isWhitespace(tokenValue)) { 39 | token.type = TokenType.WHITESPACE; 40 | 41 | let tokenContinueLength = 0; 42 | tokenValue = ""; 43 | 44 | while ( 45 | tokenContinueLength < markdown.length - tokenIndex && 46 | isWhitespace(markdown.charAt(tokenIndex + tokenContinueLength)) 47 | ) { 48 | tokenValue += " "; 49 | tokenContinueLength += 1; 50 | } 51 | 52 | token.value = tokenValue; 53 | 54 | tokens.push(token); 55 | return tokenContinueLength - 1; 56 | } 57 | 58 | // Check for the # Headers in the beginning of a line 59 | if (tokenValue.includes("#")) { 60 | token.type = TokenType.HEADER; 61 | token.value = "#"; 62 | 63 | tokens.push(token); 64 | return 0; 65 | } 66 | 67 | // Check for Italics 68 | if ( 69 | tokenValue.includes("*") && 70 | markdown.charAt(tokenIndex + 1).includes("*") 71 | ) { 72 | token.type = TokenType.ITALICS; 73 | token.value = "**"; 74 | 75 | tokens.push(token); 76 | return 1; 77 | } 78 | 79 | // Check for bold 80 | if ( 81 | tokenValue.includes("_") && 82 | markdown.charAt(tokenIndex + 1).includes("_") 83 | ) { 84 | token.type = TokenType.BOLD; 85 | token.value = "__"; 86 | 87 | tokens.push(token); 88 | return 1; 89 | } 90 | 91 | // Check for strikethrough 92 | if ( 93 | tokenValue.includes("~") && 94 | markdown.charAt(tokenIndex + 1).includes("~") 95 | ) { 96 | token.type = TokenType.STRIKETHROUGH; 97 | token.value = "~~"; 98 | 99 | tokens.push(token); 100 | return 1; 101 | } 102 | 103 | // Check for Unordered List 104 | if ( 105 | tokenValue.includes("*") && 106 | isWhitespace(markdown.charAt(tokenIndex + 1)) 107 | ) { 108 | token.type = TokenType.UNORDERED_LIST_ITEM; 109 | token.value = "* "; 110 | 111 | tokens.push(token); 112 | return 1; 113 | } 114 | 115 | // Check for ordered list 116 | if ( 117 | tokenValue.includes("1") && 118 | markdown.charAt(tokenIndex + 1).includes(".") && 119 | isWhitespace(markdown.charAt(tokenIndex + 2)) 120 | ) { 121 | token.type = TokenType.ORDERED_LIST_ITEM; 122 | token.value = "1. "; 123 | 124 | tokens.push(token); 125 | return 1; 126 | } 127 | 128 | // Check for Images 129 | if ( 130 | tokenValue.includes("!") && 131 | markdown.charAt(tokenIndex + 1).includes("[") 132 | ) { 133 | token.type = TokenType.IMAGE_START; 134 | token.value = "!["; 135 | 136 | tokens.push(token); 137 | return 1; 138 | } 139 | 140 | // Check for Link Brackets 141 | if (tokenValue.includes("[")) { 142 | token.type = TokenType.BRACKET_START; 143 | token.value = "["; 144 | 145 | tokens.push(token); 146 | return 0; 147 | } 148 | 149 | if (tokenValue.includes("]")) { 150 | token.type = TokenType.BRACKET_END; 151 | token.value = "]"; 152 | 153 | tokens.push(token); 154 | return 0; 155 | } 156 | 157 | // Check for Link definitions 158 | if (tokenValue.includes("(")) { 159 | token.type = TokenType.PAREN_START; 160 | token.value = "("; 161 | 162 | tokens.push(token); 163 | return 0; 164 | } 165 | 166 | if (tokenValue.includes(")")) { 167 | token.type = TokenType.PAREN_END; 168 | token.value = ")"; 169 | 170 | tokens.push(token); 171 | return 0; 172 | } 173 | 174 | // Check for block quotes 175 | if ( 176 | tokenValue.includes(">") && 177 | isWhitespace(markdown.charAt(tokenIndex + 1)) 178 | ) { 179 | token.type = TokenType.BLOCK_QUOTE; 180 | token.value = ">"; 181 | 182 | tokens.push(token); 183 | return 1; 184 | } 185 | 186 | // Check for code blocks 187 | if (checkForTriplet("`", tokenIndex, markdown)) { 188 | token.type = TokenType.CODE_BLOCK; 189 | token.value = "```"; 190 | 191 | tokens.push(token); 192 | return 2; 193 | } 194 | 195 | // Check for inline code blocks 196 | if (tokenValue.includes("`")) { 197 | token.type = TokenType.INLINE_CODE; 198 | token.value = "`"; 199 | 200 | tokens.push(token); 201 | return 0; 202 | } 203 | 204 | // Check for horizontal lines 205 | if (checkForTriplet("-", tokenIndex, markdown)) { 206 | token.type = TokenType.HORIZONTAL_LINE; 207 | token.value = "---"; 208 | 209 | tokens.push(token); 210 | return 2; 211 | } 212 | 213 | if (checkForTriplet("=", tokenIndex, markdown)) { 214 | token.type = TokenType.HORIZONTAL_LINE; 215 | token.value = "==="; 216 | 217 | tokens.push(token); 218 | return 2; 219 | } 220 | 221 | // We forsure have a character token 222 | // Check if we should update the previous token 223 | if ( 224 | tokenIndex > 0 && 225 | tokens.length > 0 && 226 | tokens[tokens.length - 1].type.includes(TokenType.CHARACTER) 227 | ) { 228 | tokens[tokens.length - 1].value += tokenValue; 229 | return 0; 230 | } else { 231 | token.type = TokenType.CHARACTER; 232 | token.value = tokenValue; 233 | 234 | tokens.push(token); 235 | return 0; 236 | } 237 | } 238 | 239 | export function markdownTokenizer(markdown: string): Array { 240 | tokens = new Array(0); 241 | 242 | for (let i: i32 = 0; i < markdown.length; i++) { 243 | let tokenValue: string = markdown.charAt(i); 244 | 245 | let additionalIndex = addToken(markdown, i, tokenValue); 246 | i += additionalIndex; 247 | } 248 | 249 | return tokens; 250 | } 251 | -------------------------------------------------------------------------------- /examples/markdown-parser/assembly/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../node_modules/assemblyscript/std/assembly.json", 3 | "include": ["./**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/markdown-parser/assembly/util.ts: -------------------------------------------------------------------------------- 1 | // Console.log 2 | declare function consoleLog(message: string): void; 3 | export function log(message: string): void { 4 | consoleLog(message); 5 | } 6 | -------------------------------------------------------------------------------- /examples/markdown-parser/index.css: -------------------------------------------------------------------------------- 1 | @import "../../node_modules/normalize.css/normalize.css"; 2 | @import "../../node_modules/sakura.css/css/sakura.css"; 3 | 4 | html, 5 | body { 6 | padding: 0px; 7 | margin: 0px; 8 | height: 100vh; 9 | max-width: none; 10 | } 11 | 12 | textarea { 13 | background-color: #fff; 14 | color: #000; 15 | } 16 | 17 | #root, 18 | .app { 19 | width: 100%; 20 | height: 100%; 21 | } 22 | 23 | .app { 24 | display: flex; 25 | flex-direction: column; 26 | } 27 | 28 | .app > h1 { 29 | text-align: center; 30 | margin-bottom: 5px; 31 | } 32 | 33 | .description { 34 | width: 95%; 35 | max-width: 950px; 36 | 37 | margin-left: auto; 38 | margin-right: auto; 39 | text-align: center; 40 | 41 | margin-top: 5px; 42 | margin-bottom: 5px; 43 | } 44 | 45 | .link-row { 46 | margin: 5px; 47 | 48 | display: flex; 49 | flex-wrap: wrap; 50 | justify-content: center; 51 | align-items: center; 52 | } 53 | 54 | .editor-container { 55 | flex-grow: 1; 56 | overflow: hidden; 57 | } 58 | 59 | .editor { 60 | width: 100%; 61 | height: 100%; 62 | 63 | display: flex; 64 | flex-wrap: wrap; 65 | justify-content: space-around; 66 | align-items: center; 67 | } 68 | 69 | .markdown { 70 | min-width: 350px; 71 | width: 45%; 72 | height: 100%; 73 | 74 | display: flex; 75 | flex-direction: column; 76 | } 77 | 78 | .markdown > h2 { 79 | text-align: center; 80 | } 81 | 82 | .markdown textarea { 83 | flex-grow: 1; 84 | } 85 | 86 | .result { 87 | min-width: 350px; 88 | width: 45%; 89 | height: 100%; 90 | 91 | display: flex; 92 | flex-direction: column; 93 | } 94 | 95 | .result > h2 { 96 | text-align: center; 97 | } 98 | 99 | .result-html { 100 | flex-grow: 1; 101 | overflow: auto; 102 | 103 | margin-bottom: 10px; 104 | } 105 | 106 | code { 107 | display: block; 108 | white-space: pre-line; 109 | } 110 | 111 | .markdown textarea, 112 | .result .result-html { 113 | padding: 5px; 114 | border: 2px solid #000; 115 | } 116 | 117 | @media only screen and (max-width: 800px) { 118 | .editor { 119 | width: 100%; 120 | height: 100%; 121 | 122 | display: flex; 123 | flex-direction: column; 124 | justify-content: center; 125 | align-items: center; 126 | } 127 | 128 | .editor-container { 129 | overflow: initial; 130 | } 131 | 132 | .markdown, 133 | .result { 134 | min-width: 200px; 135 | width: 90%; 136 | height: 500px; 137 | 138 | margin: 5px; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /examples/markdown-parser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Asbind Markdown 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 |
    19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/markdown-parser/index.js: -------------------------------------------------------------------------------- 1 | import { h, render, Component } from "preact"; 2 | import * as asbind from "../../dist/as-bind.esm"; 3 | 4 | // Import our TypeScript equivalent 5 | import { convertMarkdownToHTML } from "../../dist/ts/index"; 6 | import "./index.css"; 7 | 8 | let testMarkdown = `# __as-bind__ ~~convert~~ **markdown** to html 9 | 10 | * Item 1 11 | * Item 2 12 | * Item 3 13 | 14 | === 15 | 16 | ## And now we are back! 17 | 18 | Here is a [link to the source code](https://github.com/torch2424/as-bind) 19 | 20 | And an image of the author! 21 | 22 | ![torch2424 avatar](https://avatars1.githubusercontent.com/u/1448289?s=460&v=4) 23 | 24 | --- 25 | 26 | ### More items 27 | 28 | 1. one 29 | 1. two 30 | 1. three 31 | 32 | === 33 | 34 | #### Let's write some quotes and code and stuff 35 | 36 | > WebAssembly is cool - Torch2424, 2019 37 | 38 | \`npm install as-bind\` 39 | 40 | \`\`\` 41 | const someCode = "hello world!"; 42 | // Yup we writing some code in here 43 | \`\`\` 44 | 45 | # The End! 46 | `; 47 | 48 | let asbindInstancePromise = asbind.instantiate(fetch("index.wasm"), { 49 | util: { 50 | consoleLog: message => { 51 | console.log(message); 52 | } 53 | }, 54 | env: { 55 | abort: () => { 56 | console.error("AssemblyScript Import Object Aborted!"); 57 | } 58 | } 59 | }); 60 | 61 | class App extends Component { 62 | constructor() { 63 | super(); 64 | 65 | this.state = { 66 | markdown: testMarkdown, 67 | html: "" 68 | }; 69 | } 70 | 71 | componentDidMount() { 72 | this.handleChange(); 73 | } 74 | 75 | async handleChange(event) { 76 | let markdown = this.state.markdown; 77 | if (event) { 78 | markdown = event.target.value; 79 | } 80 | const asbindInstance = await asbindInstancePromise; 81 | 82 | // Get the assemblyscript response 83 | let html = asbindInstance.exports.convertMarkdownToHTML(markdown); 84 | 85 | // Log the input and output to the console 86 | 87 | console.log(` 88 | Input Markdown: 89 | 90 | ${markdown} 91 | 92 | ------ 93 | 94 | as-bind response: 95 | 96 | ${html} 97 | `); 98 | 99 | this.setState({ 100 | markdown, 101 | html 102 | }); 103 | } 104 | 105 | render() { 106 | return ( 107 |
    108 |

    as-bind Markdown Parser Demo

    109 |

    110 | as-bind is a library to handle passing high-level data structures 111 | between AssemblyScript and JavaScript. This demo takes the input from 112 | the markdown text area, and passes the string directly to and exported 113 | function of the as-bind instantiated AssemblyScript module, and then 114 | binds the returned string to the result div. The input and response 115 | are logged into the JavaScript console. 116 |

    117 |

    118 | as-bind version: {asbind.version} 119 |

    120 | 127 |
    128 |
    129 |
    130 |

    Markdown

    131 | 135 |
    136 |
    137 |

    Result

    138 |
    144 |
    145 |
    146 |
    147 |
    148 | ); 149 | } 150 | } 151 | 152 | // render an instance of Clock into : 153 | render(, document.querySelector("#root")); 154 | -------------------------------------------------------------------------------- /examples/quickstart/browser-puppeteer.js: -------------------------------------------------------------------------------- 1 | // Use pupeteer to run in the browser 2 | const puppeteer = require("puppeteer"); 3 | 4 | // Require rollup to compile our browser.js 5 | const rollup = require("rollup"); 6 | const { nodeResolve } = require("@rollup/plugin-node-resolve"); 7 | 8 | // Get some native node libs, in order to host a static server 9 | const path = require("path"); 10 | const fs = require("fs"); 11 | const http = require("http"); 12 | 13 | // Host a static server of the local directory 14 | // https://nodejs.org/en/knowledge/HTTP/servers/how-to-serve-static-files/ 15 | http 16 | .createServer(function (req, res) { 17 | fs.readFile(__dirname + req.url, function (err, data) { 18 | if (err) { 19 | res.writeHead(404); 20 | res.end(JSON.stringify(err)); 21 | return; 22 | } 23 | res.writeHead(200); 24 | res.end(data); 25 | }); 26 | }) 27 | .listen(8000); 28 | 29 | (async () => { 30 | // Create a rollup bundle and get our compiled browser.js as a string 31 | const bundle = await rollup.rollup({ 32 | input: "./browser.js", 33 | plugins: [nodeResolve()] 34 | }); 35 | const { output } = await bundle.generate({ 36 | format: "iife" 37 | }); 38 | const browserQuickstartJs = output[0].code; 39 | 40 | // Launch the pupeteer browser and page 41 | const browser = await puppeteer.launch(); 42 | const page = await browser.newPage(); 43 | await page.goto("http://localhost:8000/browser.html"); 44 | 45 | // Create a promise that we will resolve or reject once we see any expected behavior 46 | let pageResolve; 47 | let pageReject; 48 | const pageResultPromise = new Promise((resolve, reject) => { 49 | pageResolve = resolve; 50 | pageReject = reject; 51 | }); 52 | 53 | // Listen to JS Console messages, log them, and resolve our promise on an expected message 54 | page.on("console", message => { 55 | console.log( 56 | `${message.type().substr(0, 3).toUpperCase()} ${message.text()}` 57 | ); 58 | 59 | if (message.text() === "AsBind: Hello World!") { 60 | pageResolve(); 61 | return; 62 | } 63 | }); 64 | 65 | // Listen to JS / Page errors, log them, and reject our promise 66 | page.on("pageerror", err => { 67 | theTempValue = err.toString(); 68 | console.log("Error: " + theTempValue); 69 | 70 | console.log("Browser Quickstart Failed."); 71 | pageReject(); 72 | return; 73 | }); 74 | 75 | // Run the compiled browser.js code 76 | await page.evaluate(browserQuickstartJs); 77 | 78 | // Wait for the promise, and then close everything out 79 | await pageResultPromise; 80 | await browser.close(); 81 | process.exit(); 82 | })(); 83 | -------------------------------------------------------------------------------- /examples/quickstart/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | title 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/quickstart/browser.js: -------------------------------------------------------------------------------- 1 | // If you are using a Javascript bundler, use the ESM bundle with import syntax 2 | import * as AsBind from "as-bind"; 3 | 4 | // If you are not using a bundler add a 9 | 10 | 48 | -------------------------------------------------------------------------------- /test/test-runner.js: -------------------------------------------------------------------------------- 1 | const { promisify } = require("util"); 2 | const fs = require("fs/promises"); 3 | const { dirname, join } = require("path"); 4 | 5 | const Express = require("express"); 6 | const Mocha = require("mocha"); 7 | const glob = promisify(require("glob")); 8 | const pptr = require("puppeteer"); 9 | 10 | const asc = require("assemblyscript/cli/asc"); 11 | 12 | globalThis.AsBind = require("../dist/as-bind.cjs.js"); 13 | 14 | async function main() { 15 | process.chdir(__dirname); 16 | await asc.ready; 17 | 18 | await compileAllAsc(); 19 | 20 | if ((await getNumFailingTestsInNode()) > 0) { 21 | process.exit(1); 22 | } 23 | if ((await getNumFailingTestsInPuppeteer()) > 0) { 24 | process.exit(1); 25 | } 26 | console.log("Passed node and browser tests"); 27 | return; 28 | } 29 | main(); 30 | 31 | async function compileAllAsc() { 32 | const ascFiles = await glob("./tests/**/asc.ts"); 33 | const transformFile = require.resolve("../dist/transform.cjs.js"); 34 | for (const ascFile of ascFiles) { 35 | const dir = dirname(ascFile); 36 | let config = { 37 | mangleCompilerParams() {} 38 | }; 39 | try { 40 | const configPath = require.resolve("./" + join(dir, "config.js")); 41 | const m = require(configPath); 42 | Object.assign(config, m); 43 | } catch (e) {} 44 | console.log(`Compiling ${ascFile}...`); 45 | const params = [ 46 | "--runtime", 47 | "stub", 48 | "--exportRuntime", 49 | "--transform", 50 | transformFile, 51 | "--binaryFile", 52 | ascFile.replace(/\.ts$/, ".wasm"), 53 | ascFile 54 | ]; 55 | config.mangleCompilerParams(params); 56 | await asc.main(params); 57 | } 58 | } 59 | 60 | async function getNumFailingTestsInNode() { 61 | const mocha = new Mocha(); 62 | 63 | const testFiles = await glob("./tests/**/test.js"); 64 | for (const testFile of testFiles) { 65 | mocha.addFile(testFile); 66 | } 67 | 68 | mocha.rootHooks({ 69 | async beforeEach() { 70 | const { file } = this.currentTest; 71 | const wasmFile = file.replace(/test\.js$/, "asc.wasm"); 72 | this.rawModule = await fs.readFile(wasmFile); 73 | } 74 | }); 75 | 76 | const numFailures = await runMochaAsync(mocha); 77 | return numFailures; 78 | } 79 | 80 | async function runMochaAsync(mocha) { 81 | await mocha.loadFilesAsync(); 82 | return new Promise(resolve => mocha.run(resolve)); 83 | } 84 | 85 | const PORT = process.env.PORT ?? 50123; 86 | const OPEN_DEVTOOLS = !!process.env.OPEN_DEVTOOLS; 87 | 88 | async function getNumFailingTestsInPuppeteer() { 89 | const testFiles = await glob("./tests/**/test.js"); 90 | const browser = await pptr.launch({ 91 | devtools: OPEN_DEVTOOLS, 92 | ...(process.env.PUPPETEER_EXECUTABLE_PATH?.length > 0 93 | ? { executablePath: process.env.PUPPETEER_EXECUTABLE_PATH } 94 | : {}) 95 | }); 96 | const page = await browser.newPage(); 97 | 98 | // Mocha’s JSON reporter doesn’t really give you access to the JSON report, 99 | // ironically. So I have to intercept console.log()s and detect which 100 | // one is the JSON resport string. `result` will contain the parsed JSON. 101 | let result; 102 | page.on("console", async msg => { 103 | const maybeResult = await maybeExtractMochaStatsDump(msg); 104 | if (maybeResult) { 105 | result = maybeResult; 106 | return; 107 | } 108 | }); 109 | 110 | // If we want DevTools open, wait for a second here so DevTools can load. 111 | // Otherwise we might run past `debugger` statements. 112 | if (OPEN_DEVTOOLS) { 113 | await new Promise(resolve => setTimeout(resolve, 1000)); 114 | } 115 | 116 | const app = Express(); 117 | app.use("/", Express.static("../")); 118 | const server = app.listen(PORT); 119 | await page.goto(`http://localhost:${PORT}/test/test-runner.html`); 120 | const numFailures = await page.evaluate(async testFiles => { 121 | for (const testFile of testFiles) { 122 | // Register the test 123 | await runScript(`/test/${testFile}`); 124 | // Save the test’s path. See `test-runner.html` for an explanation. 125 | runInlineScript(` 126 | suitePaths.push(${JSON.stringify(testFile)}); 127 | `); 128 | } 129 | 130 | // Create a promise that resolves once mocha is done running. 131 | // This way we can block this `evaluate` call until mocha is done. 132 | const script = document.createElement("script"); 133 | script.innerHTML = ` 134 | self.mochaRun = new Promise(resolve => mocha.run(resolve));`; 135 | document.body.append(script); 136 | return self.mochaRun; 137 | }, testFiles); 138 | await page.close(); 139 | await browser.close(); 140 | server.close(); 141 | 142 | console.log("\n\n=== Browser results"); 143 | for (const test of result.passes) { 144 | console.log(`✓ ${test.fullTitle}`); 145 | } 146 | console.log("\n\n"); 147 | for (const test of result.failures) { 148 | console.log(`X ${test.fullTitle}`); 149 | console.log(` ${test.err.message}`); 150 | } 151 | return numFailures; 152 | } 153 | 154 | async function maybeExtractMochaStatsDump(msg) { 155 | if (msg.args().length == 1 && msg.text().startsWith("{")) { 156 | const arg = await msg.args()[0].jsonValue(); 157 | let obj; 158 | try { 159 | obj = JSON.parse(arg); 160 | } catch (e) { 161 | return; 162 | } 163 | if (obj.hasOwnProperty("stats")) { 164 | return obj; 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /test/tests/array/asc.ts: -------------------------------------------------------------------------------- 1 | export function swapAndPad(a: Array, b: f64[]): Array { 2 | const result = swappedConcat(a, b); 3 | result.unshift(255); 4 | result.push(255); 5 | return result; 6 | } 7 | 8 | declare function swappedConcat(a: f64[], b: Array): f64[]; 9 | 10 | export function join(s: Array>): string { 11 | let result: string = ""; 12 | for (let outer = 0; outer < s.length; outer++) { 13 | for (let inner = 0; inner < s[outer].length; inner++) { 14 | result += s[outer][inner]; 15 | } 16 | } 17 | return result; 18 | } 19 | -------------------------------------------------------------------------------- /test/tests/array/test.js: -------------------------------------------------------------------------------- 1 | describe("as-bind", function () { 2 | it("should handle array", async function () { 3 | const instance = await AsBind.instantiate(this.rawModule, { 4 | asc: { 5 | swappedConcat(a, b) { 6 | return [].concat(b, a); 7 | } 8 | } 9 | }); 10 | assert( 11 | instance.exports.swapAndPad([1, 2, 3], [10, 11, 12]).join(",") === 12 | [255, 10, 11, 12, 1, 2, 3, 255].join(",") 13 | ); 14 | }); 15 | 16 | it("should handle nested array", async function () { 17 | const instance = await AsBind.instantiate(this.rawModule, { 18 | asc: { 19 | swappedConcat(a, b) { 20 | return [].concat(b, a); 21 | } 22 | } 23 | }); 24 | const data = [["a", "b", "c"], ["1"], ["w", "x", "y", "z"]]; 25 | assert(instance.exports.join(data) === data.map(s => s.join("")).join("")); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/tests/arraybuffer/asc.ts: -------------------------------------------------------------------------------- 1 | export function swapAndPad(a: ArrayBuffer, b: ArrayBuffer): ArrayBuffer { 2 | const intermediate = Uint8Array.wrap(swappedConcat(a, b)); 3 | const result = new Uint8Array(intermediate.length + 2); 4 | result.set(intermediate, 1); 5 | result[0] = 255; 6 | result[result.length - 1] = 255; 7 | return result.buffer; 8 | } 9 | 10 | declare function swappedConcat(a: ArrayBuffer, b: ArrayBuffer): ArrayBuffer; 11 | -------------------------------------------------------------------------------- /test/tests/arraybuffer/test.js: -------------------------------------------------------------------------------- 1 | describe("as-bind", function () { 2 | it("should handle ArrayBuffers", async function () { 3 | const instance = await AsBind.instantiate(this.rawModule, { 4 | asc: { 5 | swappedConcat(a, b) { 6 | a = new Uint8Array(a); 7 | b = new Uint8Array(b); 8 | const result = new Uint8Array(a.length + b.length); 9 | result.set(b, 0); 10 | result.set(a, b.length); 11 | return result.buffer; 12 | } 13 | } 14 | }); 15 | assert( 16 | new Uint8Array( 17 | instance.exports.swapAndPad( 18 | new Uint8Array([1, 2, 3]).buffer, 19 | new Uint8Array([10, 11, 12]).buffer 20 | ) 21 | ).join(",") === new Uint8Array([255, 10, 11, 12, 1, 2, 3, 255]).join(",") 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/tests/arraybufferview/asc.ts: -------------------------------------------------------------------------------- 1 | export function swapAndPad(a: Uint8Array, b: Uint8Array): Uint8Array { 2 | const intermediate = swappedConcat(a, b); 3 | const result = new Uint8Array(intermediate.length + 2); 4 | result.set(intermediate, 1); 5 | result[0] = 255; 6 | result[result.length - 1] = 255; 7 | return result; 8 | } 9 | 10 | export function testFloat32Array(): void { 11 | const data = new Float32Array(1); 12 | data[0] = 0.5; 13 | testF32Arr(data); 14 | } 15 | 16 | declare function swappedConcat(a: Uint8Array, b: Uint8Array): Uint8Array; 17 | declare function testF32Arr(data: Float32Array): void; 18 | -------------------------------------------------------------------------------- /test/tests/arraybufferview/test.js: -------------------------------------------------------------------------------- 1 | describe("as-bind", function () { 2 | it("should handle Uint8Arrays and Float32Array", async function () { 3 | const instance = await AsBind.instantiate(this.rawModule, { 4 | asc: { 5 | swappedConcat(a, b) { 6 | const result = new Uint8Array(a.length + b.length); 7 | result.set(b, 0); 8 | result.set(a, b.length); 9 | return result; 10 | }, 11 | testF32Arr(data) { 12 | assert(data instanceof Float32Array); 13 | assert(data[0] == 0.5); 14 | } 15 | } 16 | }); 17 | assert( 18 | instance.exports 19 | .swapAndPad(new Uint8Array([1, 2, 3]), new Uint8Array([10, 11, 12])) 20 | .join(",") === new Uint8Array([255, 10, 11, 12, 1, 2, 3, 255]).join(",") 21 | ); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/tests/custom-type/asc.ts: -------------------------------------------------------------------------------- 1 | class X { 2 | x: string; 3 | constructor(x: string) { 4 | this.x = x; 5 | } 6 | } 7 | export function makeAThing(v: string): X { 8 | return new X(v); 9 | } 10 | 11 | export function readAThing(v: X): u32 { 12 | return v.x.length; 13 | } 14 | -------------------------------------------------------------------------------- /test/tests/custom-type/test.js: -------------------------------------------------------------------------------- 1 | describe("as-bind", function () { 2 | it("should be extensible with custom type handlers", async function () { 3 | AsBind.converters.set(/^tests\/custom-type\/asc\/X$/, { 4 | ascToJs(asbindInstance, value, typeName) { 5 | const dv = new DataView(asbindInstance.exports.memory.buffer); 6 | const strPtr = dv.getUint32(value, true); 7 | return asbindInstance.exports.__getString(strPtr); 8 | }, 9 | jsToAsc(asbindInstance, value, typeName) { 10 | const ptr = asbindInstance.exports.__new( 11 | asbindInstance.getTypeId(typeName), 12 | asbindInstance.getTypeSize(typeName) 13 | ); 14 | const strPtr = asbindInstance.exports.__newString(value); 15 | const dv = new DataView(asbindInstance.exports.memory.buffer); 16 | dv.setUint32(ptr, strPtr, true); 17 | return ptr; 18 | } 19 | }); 20 | const instance = await AsBind.instantiate(this.rawModule); 21 | assert(instance.exports.makeAThing("hello") === "hello"); 22 | assert(instance.exports.readAThing("hello") === 5); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/tests/import-on-prototype/asc.ts: -------------------------------------------------------------------------------- 1 | declare function thing(): string; 2 | declare const otherThing: u32; 3 | 4 | export function add(): string { 5 | return otherThing.toString() + thing(); 6 | } 7 | -------------------------------------------------------------------------------- /test/tests/import-on-prototype/test.js: -------------------------------------------------------------------------------- 1 | describe("as-bind", function () { 2 | it("should handle the prototype chain on the imports object", async function () { 3 | debugger; 4 | const asc = { 5 | otherThing: new WebAssembly.Global({ value: "i32", mutable: false }, 4) 6 | }; 7 | // This function will be “seen” by the WebAssembly engine as an imported 8 | // function but is not visible via Object.keys() or similar. 9 | Object.setPrototypeOf(asc, { 10 | thing() { 11 | return "2"; 12 | } 13 | }); 14 | assert(asc.thing() === "2"); 15 | assert(Object.keys(asc).join(",") === "otherThing"); 16 | const instance = await AsBind.instantiate(this.rawModule, { asc }); 17 | assert(instance.exports.add() === "42"); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/tests/instantiateSync/asc.ts: -------------------------------------------------------------------------------- 1 | export function exclaim(a: string): string { 2 | return a + "!"; 3 | } 4 | -------------------------------------------------------------------------------- /test/tests/instantiateSync/test.js: -------------------------------------------------------------------------------- 1 | describe("as-bind", function () { 2 | it("works synchronously", function () { 3 | const instance = AsBind.instantiateSync(this.rawModule); 4 | assert(instance.exports.exclaim("a") === "a!"); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /test/tests/multifile/asc.ts: -------------------------------------------------------------------------------- 1 | export { swapAndPad } from "./exports"; 2 | -------------------------------------------------------------------------------- /test/tests/multifile/exports.ts: -------------------------------------------------------------------------------- 1 | import { swappedConcat } from "./imports"; 2 | 3 | export function addExclamations(s: string): string { 4 | return "!" + s + "!"; 5 | } 6 | 7 | export function swapAndPad(a: string, b: string): string { 8 | return addExclamations(swappedConcat(a, b)); 9 | } 10 | -------------------------------------------------------------------------------- /test/tests/multifile/imports.ts: -------------------------------------------------------------------------------- 1 | export declare function swappedConcat(a: string, b: string): string; 2 | -------------------------------------------------------------------------------- /test/tests/multifile/test.js: -------------------------------------------------------------------------------- 1 | describe("as-bind", function () { 2 | it("should handle multiple files correctly", async function () { 3 | const instance = await AsBind.instantiate(this.rawModule, { 4 | imports: { 5 | swappedConcat(a, b) { 6 | return b + a; 7 | } 8 | } 9 | }); 10 | assert(typeof instance.exports.addExclamations === "undefined"); 11 | assert(instance.exports.swapAndPad("a", "b") === "!ba!"); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/tests/namespace-import/asc.ts: -------------------------------------------------------------------------------- 1 | @external("my_module", "my_log") 2 | declare function log(str: string): void; 3 | 4 | @external("my_log2") 5 | declare function log2(str: string): void; 6 | 7 | export function fn(): void { 8 | log("ok"); 9 | log2("fine"); 10 | } 11 | -------------------------------------------------------------------------------- /test/tests/namespace-import/test.js: -------------------------------------------------------------------------------- 1 | describe("as-bind", function () { 2 | it("should support @external imports", async function () { 3 | let from_wasm; 4 | let from_wasm2; 5 | 6 | const instance = await AsBind.instantiate(this.rawModule, { 7 | my_module: { 8 | my_log(str) { 9 | from_wasm = str; 10 | } 11 | }, 12 | asc: { 13 | my_log2(str) { 14 | from_wasm2 = str; 15 | } 16 | } 17 | }); 18 | 19 | instance.exports.fn(); 20 | 21 | assert(from_wasm === "ok"); 22 | assert(from_wasm2 === "fine"); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/tests/namespace/asc.ts: -------------------------------------------------------------------------------- 1 | namespace console { 2 | export declare function log(str: string): void; 3 | } 4 | 5 | export function fn(): void { 6 | console.log("ok"); 7 | } 8 | -------------------------------------------------------------------------------- /test/tests/namespace/test.js: -------------------------------------------------------------------------------- 1 | describe("as-bind", function () { 2 | it("should support exported declare function in namespace", async function () { 3 | const instance = await AsBind.instantiate(this.rawModule, { 4 | asc: { 5 | "console.log"(str) { 6 | assert(str === "ok"); 7 | } 8 | } 9 | }); 10 | instance.exports.fn(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/tests/pinning/asc.ts: -------------------------------------------------------------------------------- 1 | // The entry file of your WebAssembly module. 2 | 3 | declare function string_log(str: string): void; 4 | 5 | class MemoryTrash { 6 | public t1: string; 7 | public t2: string; 8 | public t3: i32; 9 | 10 | constructor() { 11 | this.t1 = "trash1"; 12 | this.t2 = "trash1"; 13 | this.t3 = 42; 14 | } 15 | } 16 | 17 | export function trash(amount: i32): void { 18 | for (let i = 0; i < amount; i++) { 19 | let t = new MemoryTrash(); 20 | } 21 | } 22 | 23 | export function string_parameter( 24 | s1: string, 25 | s2: string, 26 | s3: string, 27 | s4: string 28 | ): void { 29 | string_log(s1); 30 | string_log(s2); 31 | string_log(s3); 32 | string_log(s4); 33 | } 34 | -------------------------------------------------------------------------------- /test/tests/pinning/config.js: -------------------------------------------------------------------------------- 1 | exports.mangleCompilerParams = params => { 2 | // Remove runtime parameter 3 | const idx = params.indexOf("stub"); 4 | params.splice(idx - 1, 2); 5 | // Add debug 6 | params.unshift("--target", "debug"); 7 | console.log({ params }); 8 | }; 9 | -------------------------------------------------------------------------------- /test/tests/pinning/test.js: -------------------------------------------------------------------------------- 1 | describe("as-bind", function () { 2 | // Shoutout to @RehkitzDev for the repro 3 | it("should not GC strings", function (done) { 4 | let num_logs = 0; 5 | function string_log(s) { 6 | num_logs += 1; 7 | assert(!/[^ABCD]/.test(s)); 8 | if (num_logs === 4) { 9 | done(); 10 | } 11 | } 12 | 13 | (async () => { 14 | const instance = await AsBind.instantiate(this.rawModule, { 15 | asc: { string_log } 16 | }); 17 | 18 | instance.exports.trash(10000); 19 | let a = "A".repeat(30); 20 | let b = "B".repeat(30); 21 | let c = "C".repeat(60); 22 | let d = "D".repeat(60); 23 | 24 | instance.exports.string_parameter(a, b, c, d); 25 | })(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/tests/strings/asc.ts: -------------------------------------------------------------------------------- 1 | export function swapAndPad(a: string, b: string): string { 2 | return "!" + swappedConcat(a, b) + "!"; 3 | } 4 | 5 | declare function swappedConcat(a: string, b: string): string; 6 | -------------------------------------------------------------------------------- /test/tests/strings/test.js: -------------------------------------------------------------------------------- 1 | describe("as-bind", function () { 2 | it("should handle strings", async function () { 3 | const instance = await AsBind.instantiate(this.rawModule, { 4 | asc: { 5 | swappedConcat(a, b) { 6 | return b + a; 7 | } 8 | } 9 | }); 10 | assert(instance.exports.swapAndPad("a", "b") === "!ba!"); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../node_modules/assemblyscript/std/assembly.json" 3 | } 4 | -------------------------------------------------------------------------------- /test/tests/unexpected-import/asc.ts: -------------------------------------------------------------------------------- 1 | export function thing(): i8 { 2 | return -1; 3 | } 4 | -------------------------------------------------------------------------------- /test/tests/unexpected-import/test.js: -------------------------------------------------------------------------------- 1 | describe("as-bind", function () { 2 | it("should handle unexpected imports gracefully", async function () { 3 | const instance = await AsBind.instantiate(this.rawModule, { 4 | env: { 5 | someFunc() {} 6 | } 7 | }); 8 | assert(instance.exports.thing() === -1); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/tests/unknown-type/asc.ts: -------------------------------------------------------------------------------- 1 | class X { 2 | x: u32; 3 | constructor(x: u32) { 4 | this.x = x; 5 | } 6 | } 7 | export function makeAThing(v: u32): X { 8 | return new X(v); 9 | } 10 | -------------------------------------------------------------------------------- /test/tests/unknown-type/test.js: -------------------------------------------------------------------------------- 1 | describe("as-bind", function () { 2 | it("should handle unknown types gracefully", async function () { 3 | const instance = await AsBind.instantiate(this.rawModule); 4 | assert(typeof instance.exports.makeAThing(43) === "number"); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /test/tests/unused-import/asc.ts: -------------------------------------------------------------------------------- 1 | declare function other(): void; 2 | 3 | export function thing(): i8 { 4 | return -1; 5 | } 6 | -------------------------------------------------------------------------------- /test/tests/unused-import/test.js: -------------------------------------------------------------------------------- 1 | describe("as-bind", function () { 2 | it("should handle unused imported functions gracefully", async function () { 3 | const instance = await AsBind.instantiate(this.rawModule); 4 | assert(instance.exports.thing() === -1); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /transform.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CommonFlags, 3 | NodeKind, 4 | ElementKind, 5 | Transform, 6 | IdentifierExpression, 7 | FunctionPrototype, 8 | StringLiteralExpression, 9 | Module, 10 | Function, 11 | DeclaredElement, 12 | Type 13 | } from "visitor-as/as"; 14 | import { TypeDef } from "./lib/types"; 15 | 16 | function isInternalElement(element: DeclaredElement) { 17 | return element.internalName.startsWith("~"); 18 | } 19 | 20 | function elementHasFlag(el: DeclaredElement, flag: number) { 21 | return (el.flags & flag) != 0; 22 | } 23 | 24 | function typeName(type: Type) { 25 | return type.getClass()?.internalName ?? type.toString(); 26 | } 27 | 28 | function containingModule(func: Function) { 29 | let container = func.parent; 30 | // Only a module is it’s own parent 31 | while (container !== container.parent) { 32 | container = container.parent; 33 | } 34 | return container; 35 | } 36 | 37 | function getFunctionTypeDescriptor(func: Function) { 38 | return { 39 | returnType: typeName(func.signature.returnType), 40 | parameters: func.signature.parameterTypes.map(parameter => 41 | typeName(parameter) 42 | ) 43 | }; 44 | } 45 | 46 | function extractTypeIds(type: Type) { 47 | const result = {}; 48 | const clazz = type.getClass?.(); 49 | if (!clazz) { 50 | return result; 51 | } 52 | result[clazz.internalName] = { 53 | id: clazz.id, 54 | byteSize: clazz.nextMemoryOffset 55 | }; 56 | if (clazz.typeArguments) { 57 | for (const subType of clazz.typeArguments) { 58 | Object.assign(result, extractTypeIds(subType)); 59 | } 60 | } 61 | return result; 62 | } 63 | 64 | function extractTypeIdsFromFunction(func: Function) { 65 | const result = {}; 66 | Object.assign(result, extractTypeIds(func.signature.returnType)); 67 | func.signature.parameterTypes.forEach(paramType => 68 | Object.assign(result, extractTypeIds(paramType)) 69 | ); 70 | return result; 71 | } 72 | 73 | const SECTION_NAME = "as-bind_bindings"; 74 | 75 | export default class AsBindTransform extends Transform { 76 | afterCompile(module: Module) { 77 | const flatExportedFunctions = [ 78 | ...this.program.elementsByDeclaration.values() 79 | ] 80 | .filter(el => elementHasFlag(el, CommonFlags.MODULE_EXPORT)) 81 | .filter(el => !isInternalElement(el)) 82 | .filter( 83 | el => el.declaration.kind === NodeKind.FUNCTIONDECLARATION 84 | ) as FunctionPrototype[]; 85 | const flatImportedFunctions = [ 86 | ...this.program.elementsByDeclaration.values() 87 | ] 88 | .filter(el => elementHasFlag(el, CommonFlags.DECLARE)) 89 | .filter(el => !isInternalElement(el)) 90 | .filter( 91 | v => v.declaration.kind === NodeKind.FUNCTIONDECLARATION 92 | ) as FunctionPrototype[]; 93 | 94 | const typeIds: TypeDef["typeIds"] = {}; 95 | const importedFunctions: TypeDef["importedFunctions"] = {}; 96 | for (let importedFunction of flatImportedFunctions) { 97 | // An imported function with no instances is an unused imported function. 98 | // Skip it. 99 | if (!importedFunction.instances) { 100 | continue; 101 | } 102 | if ( 103 | importedFunction.instances.size > 1 || 104 | !importedFunction.instances.has("") 105 | ) { 106 | throw Error(`Can’t import or export generic functions.`); 107 | } 108 | 109 | const iFunction = importedFunction.instances.get("")!; 110 | 111 | let external_module; 112 | let external_name; 113 | 114 | let decorators = iFunction.declaration.decorators; 115 | 116 | if (decorators) { 117 | for (let decorator of decorators) { 118 | if ((decorator.name as IdentifierExpression).text !== "external") 119 | continue; 120 | if (!decorator.args) continue; // sanity check 121 | 122 | if (decorator.args.length > 1) { 123 | external_module = (decorator.args[0] as StringLiteralExpression) 124 | .value; 125 | external_name = (decorator.args[1] as StringLiteralExpression) 126 | .value; 127 | } else { 128 | external_name = (decorator.args[0] as StringLiteralExpression) 129 | .value; 130 | } 131 | } 132 | } 133 | 134 | // To know under what module name an imported function will be expected, 135 | // we have to find the containing module of the given function, take the 136 | // internal name (which is effectively the file path without extension) 137 | // and only take the part after the last `/` 138 | // (i.e. the file name without extension). 139 | const moduleName = 140 | external_module || 141 | containingModule(iFunction).internalName.split("/").slice(-1)[0]; 142 | if (!importedFunctions.hasOwnProperty(moduleName)) { 143 | importedFunctions[moduleName] = {}; 144 | } 145 | let importedFunctionName = iFunction.name; 146 | if (external_name) { 147 | importedFunctionName = external_name; 148 | } else if ( 149 | iFunction.parent && 150 | iFunction.parent.kind === ElementKind.NAMESPACE 151 | ) { 152 | importedFunctionName = iFunction.parent.name + "." + iFunction.name; 153 | } 154 | importedFunctions[moduleName][importedFunctionName] = 155 | getFunctionTypeDescriptor(iFunction); 156 | Object.assign(typeIds, extractTypeIdsFromFunction(iFunction)); 157 | } 158 | const exportedFunctions = {}; 159 | for (let exportedFunction of flatExportedFunctions) { 160 | if ( 161 | exportedFunction.instances.size > 1 || 162 | !exportedFunction.instances.has("") 163 | ) { 164 | throw Error(`Can’t import or export generic functions.`); 165 | } 166 | const eFunction = exportedFunction.instances.get(""); 167 | exportedFunctions[eFunction.name] = getFunctionTypeDescriptor(eFunction); 168 | Object.assign(typeIds, extractTypeIdsFromFunction(eFunction)); 169 | } 170 | 171 | module.addCustomSection( 172 | SECTION_NAME, 173 | // @ts-ignore 174 | new TextEncoder("utf8").encode( 175 | JSON.stringify({ 176 | typeIds, 177 | importedFunctions, 178 | exportedFunctions 179 | }) 180 | ) 181 | ); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["lib/**/*.ts"], 3 | "files": ["transform.ts"], 4 | "compilerOptions": { 5 | "moduleResolution": "Node", 6 | "resolveJsonModule": true, 7 | "declaration": true, 8 | "declarationDir": "", 9 | "downlevelIteration": true, 10 | "allowSyntheticDefaultImports": true, 11 | "lib": ["ESNext", "DOM"] 12 | } 13 | } 14 | --------------------------------------------------------------------------------