Skip to main content

3 posts tagged with "JavaScript"

View All Tags

How to Use Moonpad on Your Website

· 11 min read

cover.png

Both the MoonBit homepage and Language Tour feature a component that allows you to write MoonBit code directly in the browser and compile it in real-time. This component is Moonpad, which we developed and has now been released on npm. This blog post will guide you on how to use Moonpad on your own website.

All the code referenced in this post has been uploaded to GitHub, and you can view it at https://github.com/moonbit-community/moonpad-blog-examples.

What is Moonpad?

MoonBit plugin provides many features that greatly benefit developers. In addition to supporting syntax highlighting, autocompletion, and error diagnostics for MoonBit syntax, it also provides an out-of-the-box debugger, real-time value tracing, testing, and a built-in MoonBit AI assistant, all of which help reduce the workload outside of core development, creating a highly efficient and smooth development environment. If you haven't experienced the complete MoonBit plugin, you can install our VS Code plugin.

Moonpad, as described in this tutorial, is an online MoonBit editor based on the Monaco editor. It supports syntax highlighting, autocompletion, and error diagnostics and more for MoonBit. Moreover, it can compile MoonBit code in real-time in the browser, making it essentially a simplified MoonBit plugin running in the browser. In this tutorial, we'll walk you through how to implement Moonpad, which you might have tried it in the MoonBit homepage and language tour.

How to Use Moonpad

Setup

First, create a new JS project:

mkdir moonpad
cd moonpad
npm init -y

Install dependencies:

npm i @moonbit/moonpad-monaco esbuild monaco-editor-core

Here’s a brief explanation of each dependency:

  • @moonbit/moonpad-monaco is a Monaco editor plugin that provides syntax highlighting, autocompletion, error diagnostics, and compilation features for MoonBit.
  • esbuild is a fast JavaScript bundling tool used for packaging source code.
  • monaco-editor-core is the core library for the Monaco editor. We use this library instead of monaco-editor because it doesn’t include unnecessary syntax highlighting and semantic support for languages like HTML, CSS, and JS, making the bundled output smaller.

Writing Code

Next, we need to write code for using Moonpad and Monaco editor along with a build script.

The following code is written in the moonpad directory and contains extensive comments to help you better understand it.

Here’s the code for index.js:

import * as moonbitMode from "@moonbit/moonpad-monaco";
import * as monaco from "monaco-editor-core";

// Monaco editor requires global variables; for more details: https://github.com/microsoft/monaco-editor
self.MonacoEnvironment = {
getWorkerUrl: function () {
return "/moonpad/editor.worker.js";
},
};

// moonbitMode is an extension of Monaco Editor, which we call Moonpad.
// moonbitMode.init initializes various MoonBit functionalities and returns a simple MoonBit build system for compiling and running MoonBit code.
const moon = moonbitMode.init({
// A URL string that can request onig.wasm
// URL for onig.wasm (the WASM version of oniguruma for MoonBit syntax highlighting)
onigWasmUrl: new URL("./onig.wasm", import.meta.url).toString(),
// A worker running the MoonBit LSP server for language support
lspWorker: new Worker("/moonpad/lsp-server.js"),
// A factory function for creating a worker to compile MoonBit code in the browser
mooncWorkerFactory: () => new Worker("/moonpad/moonc-worker.js"),
// A configuration function to filter which codeLens should be displayed
// Return false to disable any codeLens for simplicity
codeLensFilter() {
return false;
},
});

// Note: All paths are hardcoded here, so later when writing the build script, we need to ensure these paths are correct.

// Mount Moonpad

// Create an editor model and specify its languageId as "moonbit". We can initialize the code content here.
// Moonpad only provides LSP services for models with languageId "moonbit".

const model = monaco.editor.createModel(
`fn main {
println("hello")
}
`,
"moonbit",
);

// Create a Monaco editor and mount it to the `app` div element, displaying the model we just created.
monaco.editor.create(document.getElementById("app"), {
model,
// Moonpad provides its own theme with better syntax highlighting, compared to the default Monaco theme
// Moonpad also provides a dark theme "dark-plus", which you can try replacing with "dark-plus"
theme: "light-plus",
});

Create an esbuild.js file and enter the following code for our build script that bundles index.js:

const esbuild = require("esbuild");
const fs = require("fs");

// The dist folder is our output directory. Here we clear it to ensure esbuild always starts from scratch.
fs.rmSync("./dist", { recursive: true, force: true });

esbuild.buildSync({
entryPoints: [
"./index.js",
// Bundle Monaco Editor worker required for providing editing services, which is also the return value of MonacoEnvironment.getWorkerUrl in `index.js`.
// As previously mentioned, all paths are hardcoded, so we use `entryNames` to ensure the worker is named `editor.worker.js`.
"./node_modules/monaco-editor-core/esm/vs/editor/editor.worker.js",
],
bundle: true,
minify: true,
format: "esm",
// The output directory corresponds to the hardcoded paths in `index.js`.
outdir: "./dist/moonpad",
entryNames: "[name]",
loader: {
".ttf": "file",
".woff2": "file",
},
});

fs.copyFileSync("./index.html", "./dist/index.html");

// Copy various worker files needed to initialize Moonpad, since they are already bundled, we don’t need to bundle them again with esbuild.
fs.copyFileSync(
"./node_modules/@moonbit/moonpad-monaco/dist/lsp-server.js",
"./dist/moonpad/lsp-server.js"
);
fs.copyFileSync(
"./node_modules/@moonbit/moonpad-monaco/dist/moonc-worker.js",
"./dist/moonpad/moonc-worker.js"
);
fs.copyFileSync(
"./node_modules/@moonbit/moonpad-monaco/dist/onig.wasm",
"./dist/moonpad/onig.wasm"
);

Finally, create a simple index.html file:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Moonpad Demo</title>
<link rel="stylesheet" href="/moonpad/index.css" />
</head>
<body>
<div id="app" style="height: 500px"></div>
<script type="module" src="/moonpad/index.js"></script>
</body>
</html>

Build and Start the Server

Run the build script:

node esbuild.js

Your dist folder should now contain:

dist/
├── index.html
└── moonpad
├── codicon-37A3DWZT.ttf
├── editor.worker.js
├── index.css
├── index.js
├── lsp-server.js
├── moonc-worker.js
└── onig.wasm

2 directories, 8 files

The codicon-37A3DWZT.ttf file hash at the end may not match exactly in your case, but that's not a problem.

Now, start a simple HTTP server in the dist folder::

python3 -m http.server 8081 -d ./dist

Open http://localhost:8081 in your browser, and you should see Moonpad successfully rendered.

Moonpad

Using Moonpad on Your Website

Let’s see how to integrate Moonpad into your website, using Jekyll and Marp as examples.

Principle

In the previous section, we created a moonpad folder containing all the files necessary to use Moonpad. Any webpage can import moonpad/index.js and moonpad/index.css and modify the mount logic in index.js to use Moonpad.

To use Moonpad on your webpage, you need to:

  1. Modify the mount logic in moonpad/index.js based on your website framework.

  2. Import moonpad/index.js and moonpad/index.css on the page where you want to use Moonpad.

  3. Place the moonpad folder in the website’s static resource directory and ensure its path is /moonpad.

Using Moonpad in Jekyll

Jekyll is a simple, blog-aware static site generator. It uses Markdown or Textile, along with the Liquid template engine, to generate static web pages. Jekyll combines content with templates to generate static files that can be directly deployed to any web server. It is particularly well-suited for GitHub Pages, allowing users to easily create and maintain blogs or websites.

Observing the Structure of Code Blocks Rendered by Jekyll

For the following Markdown code block in Jekyll:

```moonbit
fn main {
println("hello, moonbit")
}
```

It generates the following HTML structure:

<pre><code class="language-moonbit">fn main {
println("hello, moonbit")
}
</code></pre>

If we want all MoonBit code blocks in Jekyll to be rendered in Moonpad, we need to replace the generated pre element with a div and mount Moonpad on this div.

Here is the implementation of this logic:

// Loop through all moonbit code blocks
for (const pre of document.querySelectorAll("pre:has(code.language-moonbit)")) {
// Get the content of the code block
const code = pre.textContent;
// Create a div element, which will serve as the mount point for Monaco editor
const div = document.createElement("div");
// Set the div's height based on the code content
const height = code.split("\n").length * 20;
div.style.height = `${height}px`;
// Replace the code block with the div
pre.replaceWith(div);
// Create a Monaco editor model using the obtained code content
const model = monaco.editor.createModel(code, "moonbit");
// Create Monaco editor and mount it to the div, displaying the previously created model
monaco.editor.create(div, {
model,
theme: "light-plus",
});
}

Simply replace the mounting logic in index.js with the above code.

Importing Moonpad

Jekyll supports using HTML directly in Markdown, so we can import Moonpad directly in the Markdown. Just add the following code at the end of the Markdown file where Moonpad is needed.

<link rel="stylesheet" href="/moonpad/index.css" />
<script type="module" src="/moonpad/index.js"></script>

Place the Moonpad Folder in Jekyll’s Static Directory

After running jekyll build, Jekyll will place all static assets in the _site directory. We only need to copy the moonpad folder into the _site directory.

The directory structure of the _site folder should look like this after copying:

_site/
├── ... # Other static assets
└── moonpad
├── codicon-37A3DWZT.ttf
├── editor.worker.js
├── index.css
├── index.js
├── lsp-server.js
├── moonc-worker.js
└── onig.wasm

Result

After completing the above steps, you can use Moonpad in Jekyll. The result will look like this:

Using Moonpad in Marp

Marp is a Markdown conversion tool that can convert Markdown files into slides. It is based on the Marpit framework, supports custom themes, and allows you to create and design slides using simple Markdown syntax, making it ideal for technical presentations.

Observing the Structure of Code Blocks Rendered by Marp

For the same code block as in the previous Jekyll section, Marp renders the HTML structure as follows:

<marp-pre>
...
<code class="language-moonbit">fn main { println("hello, moonbit") } </code>
</marp-pre>

Clearly, if we want all MoonBit code blocks to use Moonpad for rendering, we need to replace marp-pre with div and mount Moonpad on this div.

Here is the implementation of this logic, which is very similar to the Jekyll example:

for (const pre of document.querySelectorAll(
"marp-pre:has(code.language-moonbit)",
)) {
const code = pre.querySelector("code.language-moonbit").textContent;
const div = document.createElement("div");
const height = code.split("\n").length * 20;
div.style.height = `${height}px`;
pre.replaceWith(div);
const model = monaco.editor.createModel(code, "moonbit");
monaco.editor.create(div, {
model,
theme: "light-plus",
});
}

Just replace the mounting logic in index.js with the above code.

Importing Moonpad

Marp also supports using HTML in Markdown, but this feature must be explicitly enabled. Add the following code at the end of the Markdown file where Moonpad is needed:

<link rel="stylesheet" href="/moonpad/index.css" />
<script type="module" src="/moonpad/index.js"></script>

And enable the html option when building:

marp --html

Place the Moonpad Folder in Marp’s Static Directory

There are two common ways to preview slides in Marp: one is by using the built-in server feature, and the other is by exporting the Markdown file as HTML and setting up your own server.

For the built-in server feature, place the moonpad folder in the folder specified by the marp --server command.

For the case where you export Markdown as HTML, ensure that the moonpad folder and the exported HTML file are in the same directory.

Result

After completing the above steps, you can use Moonpad in Marp. The result will look like this:

Unfortunately, in Marp, the hover tooltip position in Monaco editor is not correct. We are currently unsure how to resolve this issue.

How to Use Moonpad to Compile MoonBit Code

As mentioned earlier, moonbitMode.init returns a simple build system, which we can use to compile and run MoonBit code. It exposes two methods: compile and run, which are used to compile and run MoonBit code, respectively. For example:

// compile also accepts more configuration via parameters, such as compiling files with tests. Here, we only compile a single file.
const result = await moon.compile({ libInputs: [["a.mbt", model.getValue()]] });
switch (result.kind) {
case "success":
// If compilation is successful, the JavaScript code compiled by the backend compiler will be returned.
const js = result.js;
// You can use the run method to execute the compiled JS and get the standard output stream.
// Note that the smallest unit of this stream is not characters, but each line of string output from the standard output.
const stream = moon.run(js);
// Collect the content from the stream into a buffer and output it to the console.
let buffer = "";
await stream.pipeTo(
new WritableStream({
write(chunk) {
buffer += `${chunk}\n`;
},
}),
);
console.log(buffer);
break;
case "error":
break;
}

Open the console, and you will see the output: hello.

For more usage of the compile function and how to display the code output on the page, refer to the language tour code: moonbit-docs/moonbit-tour/src/editor.ts#L28-L61

New to MoonBit?

Profiling MoonBit-Generated Wasm using Chrome

· 7 min read

cover

In one of our previous blog posts, we have introduced how to consume a MoonBit-powered Wasm library, Cmark, within frontend JavaScript. In this post, we will explore how to profile the same library directly from the Chrome browser. Hopefully, this will help you gain insights and eventually achieve better overall performance with MoonBit in similar use cases.

For this blog post in particular, we will focus on making minimal changes to our previous "Cmark for frontend" example, so that we may apply Chrome's built-in V8 profiler to Cmark's Wasm code.

Profiling the Cmark library

We can easily refactor the original frontend application to include a new navigation bar with two links, "Demo" and "Profiling". Clicking the first will tell the browser to render the HTML for the original A Tour of MoonBit for Beginners example, and clicking the second will lead it to our new document for profiling. (If you are curious about the actual implementation, you can find a link pointing to the final code towards the end of this post.)

Now we are ready to write some code to actually implement Wasm profiling. Are there any particularities involved in terms of profiling Wasm compared to JavaScript?

As it turns out, we can use the very same APIs for profiling Wasm as we do for JavaScript code. There is an article in the Chromium documentation that describes them in more detail, but in short:

  • When we call console.profile(), the V8 profiler will start recording a CPU performance profile;
  • After that, we can call the performance-critical function we would like to analyze;
  • Finally, when we call console.profileEnd(), the profiler will stop the recording and then visualizes the resulting data in Chrome's Performance Tab.

With that in mind, let's have a look at the actual implementation of our profiling functionality:

async function profileView() {
const docText = await fetchDocText("../public/spec.md");
console.profile();
const res = cmarkWASM(docText);
console.profileEnd();
return (
`<h2>Note</h2>
<p>
<strong
>Open your browser's
<a
href="https://developer.chrome.com/docs/devtools/performance"
rel="nofollow"
>Performance Tab</a
>
and refresh this page to see the CPU profile.</strong
>
</p>` + res
);
}

As you can see, we have to minimize the scope of the code being executed when the profiler is activated. Thus, the code is written so that the call to the cmarkWASM() function is the only thing within that scope.

On the other hand, we have chosen the 0.31.2 version of the CommonMark Specification (a.k.a. spec.md in the code above) as the input document for the profiling mode. This is notably because of the document's richness particularly in the employment of different Markdown features, in addition to its sheer length which can cause trouble for many Markdown parsers:

> wc -l spec.md  # line count
9756 spec.md
> wc -w spec.md # word count
25412 spec.md

We have reorganized our frontend application so that clicking on the "Profiling" link in the navigation bar will trigger the profileView() function above, giving the following:

profile-tab-stripped

If you have ever dug into performance optimization, this flame graph above should look pretty familiar...

Wait, what are wasm-function[679], wasm-function[367] and so on? How are we supposed to know which function corresponds to which number?

It turns out we need to retain some debug information when building our Wasm artifact. After all, we have been using the following command to build our MoonBit project:

> moon -C cmarkwrap build --release --target=wasm-gc

... and stripping is the standard behavior of moon when producing a release build.

Fortunately, there is an extra flag we can use to keep the symbol information without having to resort to a slow debug build: --no-strip. Let's rebuild our project with it:

> moon -C cmarkwrap build --release --no-strip --target=wasm-gc

Note

Similarly, if we would like to use wasm-opt on the resulting Wasm artifact, we can use the --debuginfo (or -g) flag of wasm-opt to preserve the function names in the optimized output.

With the function names retained, we can finally see what is really going on in the Performance Tab!

profile-tab

Analyzing the Flame Graph

The flame graph as shown in the previous screenshot can provide a nice summary of function calls and their respective execution times in our code. In case you are not familiar with it, the main ideas behind it are as follows:

  • The Y-axis represents the call stack, with the topmost function being the one that was called first;
  • The X-axis represents the time spent in execution, with the width of each box-shaped node corresponding to the total time spent in a certain function call and its children.

Since we are investigating the performance of the Cmark library in particular, we should move downwards and concentrate on the node for @rami3l/cmark/cmark_html.render(). Here, for example, we can clearly see that the execution of render() is divided into two main parts, as represented by two children nodes on the graph:

  • @rami3l/cmark/cmark.Doc::from_string(), which stands for the conversion of the input Markdown document into a syntax tree;
  • @rami3l/cmark/cmark_html.from_doc(), which stands for the rendering of the syntax tree into the final HTML document.

To get a better view, let's highlight the render() node in the flame graph with a single click. This will tell Chrome to update the "Bottom-up" view to show only the functions that are transitively called by render(), and we will get something like the following:

profile-tab-focused

After sorting the items by self time (i.e. total time excluding child time) in the "Bottom-up" view, we can easily find out the functions that have consumed the most time on their own. This suggests that their implementation might be worth a closer look. Meanwhile, we would also want to try eliminating deep call stacks when possible, which can be found by looking for the long vertical bars in the flame graph.

Achieving High Performance

During its development process, Cmark has been profiled hundreds of times using the very method we have seen above in pursuit of satisfactory performance, but how does it actually perform against popular JavaScript Markdown libraries?

For this test, we have chosen Micromark and Remark---two well-known Markdown libraries in the JavaScript ecosystem---as our reference. We have used a recent build of Chrome 133 in this test as our JS and Wasm runtimes, and have imported Tinybench to measure the average throughput of each library.

Below is the average throughput of these libraries converting a single copy of the CommonMark spec to HTML on a MacBook M1 Pro:

Test (Fastest to Slowest)SamplesAverage/Hz±/%
cmark.mbt (WASM-GC + wasm-opt)21203.663.60
cmark.mbt (WASM-GC)19188.463.84
micromark1015.482.07
remark1014.283.16

The results are quite clear: thanks to the continuous profiling-and-optimizing process, Cmark is now significantly faster than both JavaScript-based libraries by a factor of about 12x for Micromark and 13x for Remark. Furthermore, wasm-opt's extra optimization passes can give Cmark another performance boost, bringing these factors up to about 13x and 14x respectively.

In conclusion, the performance of Cmark is a testament to the power of MoonBit in providing visible efficiency improvements in an actual frontend development scenario.

If you are interested in the details of this demo, you may check out the final code on GitHub. The code based used in benchmarking is also available here.

New to MoonBit?

Consuming a High Performance Wasm Library in MoonBit from JavaScript

· 5 min read

cover

In one of our previous blog posts, we have already started exploring the use of JavaScript strings directly within MoonBit's Wasm GC backend. As we have previously seen, not only is it possible to write a JavaScript-compatible string-manipulating API in MoonBit, but once compiled to Wasm, the resulting artifact is impressively tiny in size.

In the meantime, however, you might have wondered what it will look like in a more realistic use case. That is why we are presenting today a more realistic setting of rendering a Markdown document on a JavaScript-powered web application, with the help of the MoonBit library Cmark and Wasm's JS String Builtins Proposal.

Motivation

Cmark is a new MoonBit library for Markdown document processing, which makes it possible to parse both vanilla CommonMark and various common Markdown syntax extensions (task lists, footnotes, tables, etc.) in pure MoonBit. Furthermore, open to external renderers since its early days, it comes with a ready-to-use official HTML renderer implementation known as cmark_html.

Given Markdown's ubiquitous presence in today's cyberspace and the web world in particular, a conversion pipeline from Markdown to HTML remains an important tool in virtually every JavaScript developer's toolbox. As such, it also constitutes a perfect scenario for showcasing the use of MoonBit's Wasm GC APIs in frontend JavaScript.

Wrapping over Cmark

For the sake of this demo, let's start with a new project directory:

> mkdir cmark-frontend-example

In that very directory, we will first create a MoonBit library cmarkwrap that wraps Cmark:

> cd cmark-frontend-example && moon new cmarkwrap

This extra project cmarkwrap is required mostly because:

  • Cmark in itself doesn't expose any API across the FFI boundary, which is the common case for most MoonBit libraries;
  • We will need to fetch the Cmark project from the mooncakes.io repository and compile it locally to Wasm GC anyway.

cmarkwrap's structure is simple enough:

  • cmark-frontend-example/cmarkwrap/src/lib/moon.pkg.json:

    {
    "import": ["rami3l/cmark/cmark_html"],
    "link": {
    "wasm-gc": {
    "exports": ["render", "result_unwrap", "result_is_ok"],
    "use-js-builtin-string": true
    }
    }
    }

    This setup is pretty much identical to the one we have seen in the previous blog, with the use-js-builtin-string flag enabled for the Wasm GC target, and the relevant wrapper functions exported.

  • cmark-frontend-example/cmarkwrap/src/lib/wrap.mbt:

    ///|
    typealias RenderResult = Result[String, Error]

    ///|
    pub fn render(md : String) -> RenderResult {
    @cmark_html.render?(md)
    }

    ///|
    pub fn result_unwrap(res : RenderResult) -> String {
    match res {
    Ok(s) => s
    Err(_) => ""
    }
    }

    ///|
    pub fn result_is_ok(res : RenderResult) -> Bool {
    res.is_ok()
    }

    This is where things start to get interesting. The render() function is a wrapper of the underlying @cmark_html.render() function that, instead of being a throwing function, returns a RenderResult type.

    Unfortunately, being a Wasm object (instead of a number or a string), a RenderResult is opaque to JavaScript, and thus cannot be directly consumed by our JavaScript caller. As a result, we also need to provide means to destruct that very type from within MoonBit as well: the result_unwrap() and result_is_ok() functions are there exactly for this purpose, and that is why they accept a RenderResult input.

Integrating with JavaScript

Now is the time to write the web part of this project. At this point, you are basically free to choose any framework or bundler you prefer. This demo in particular has chosen to initialize a minimal project skeleton under the cmark-frontend-example directory with no extra runtime dependencies. For now, let's focus on the HTML and JS parts of the project:

  • cmark-frontend-example/index.html:

    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Cmark.mbt + JS</title>
    </head>
    <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
    <link rel="stylesheet" href="/src/style.css" />
    </body>
    </html>

    This simple HTML file includes a single div of id="app", which will become the target to which we render the Markdown document later.

  • cmark-frontend-example/src/main.js:

    const cmarkwrapWASM = await WebAssembly.instantiateStreaming(
    fetch("../cmarkwrap/target/wasm-gc/release/build/lib/lib.wasm"),
    {},
    {
    builtins: ["js-string"],
    importedStringConstants: "_",
    },
    );
    const { render, result_is_ok, result_unwrap } =
    cmarkwrapWASM.instance.exports;

    function cmarkWASM(md) {
    const res = render(md);
    if (!result_is_ok(res)) {
    throw new Error("cmarkWASM failed to render");
    }
    return result_unwrap(res);
    }

    async function docHTML() {
    const doc = await fetch("../public/tour.md");
    const docText = await doc.text();
    return cmarkWASM(docText);
    }

    document.getElementById("app").innerHTML = await docHTML();

    As it turns out, integrating cmarkwrap into JavaScript is fairly straightforward. After fetching and loading the Wasm artifact, we can call the wrapper functions right away. The result_is_ok() function helps us identify if we are on the happy path: if we are, we can unwrap the HTML result across the FFI boundary with result_unwrap(); otherwise, we can throw a JavaScript error. If everything goes as well, we can finally use the aforementioned <div id="app"></div> as our output target.

We can now compile the MoonBit Wasm GC artifact and launch the development server:

> moon -C cmarkwrap build --release --target=wasm-gc
> python3 -m http.server

And voilà! You can now read A Tour of MoonBit for Beginners rendered by the Cmark MoonBit library in a JavaScript frontend application at http://localhost:8000.

demo

You may find the code of this demo on GitHub.

New to MoonBit?