npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

mancha

v0.10.0

Published

Javscript HTML rendering engine

Downloads

349

Readme

mancha

mancha is a simple HTML templating and reactivity library for simple people. It works on the browser or the server. It can be used as a command-line tool, or imported as a Javascript module.

Here's a small sample of the things that you can do with mancha:

<!-- Use the bundled file from `unkpg` and load a drop-in replacement for Tailwind CSS. -->
<script src="//unpkg.com/mancha" target="main" css="utils" init></script>

<!-- Scoped variables using the `:data` attribute. -->
<main class="p-4" :data="{count: 0, name: 'Stranger'}">
  <!-- Custom HTML tag element registration. -->
  <template is="counter">
    <div>
      <slot></slot>
      <button :on:click="count = count + 1">Counter: {{ count }}</button>
    </div>
  </template>

  <!-- Custom HTML tag element usage. -->
  <counter class="my-2">Click me:</counter>

  <!-- Reactive data binding. -->
  <p>Enter your name: <input type="text" :bind="name" /></p>
  <p>Hello, <span class="underline">{{ name }}</span>!</p>

  <!-- Include HTML partials. -->
  <footer class="text-xs">
    <include src="html/partial/footer.tpl.html"></include>
  </footer>
</main>

Why another front-end Javascript library?

There are plenty of other front-end Javascript libraries, many of them of great quality, including:

None of them have all the key features that make mancha unique:

| Feature | mancha | Svelte | React.js | Vue.js | petite-vue | Alpine.js | | --------------------- | ------ | ------ | -------- | ------ | ---------- | --------- | | Simple to learn | ✔️ | ❌ | ❌ | ❌ | ✔️ | ✔️ | | < 15kb compressed | ✔️ | ❌ | ❌ | ❌ | ✔️ | ❌ | | Custom web components | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | ❌ | | Client-side rendering | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | | Server-side rendering | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | ❌ |

mancha is great for:

  • prototyping, just plop a script tag in your HTML and off you go
  • testing, individual components can be rendered and tested outside the browser
  • progressive enhancement, from simple templating and basic reactivity to a full-blown app

A core benefit of using mancha is that it allows you to compartmentalize the complexity of front-end development. Whether you decide to break up your app into reusable partial sections via <include> or create custom web components, you can write HTML as if your mother was watching.

mancha implements its own reactivity engine, so the bundled browser module contains no external dependencies.

Preprocessing

As part of the rendering lifecycle, mancha first preprocesses the HTML. The two main stages of preprocessing consist of:

  • Resolution of <include> tags

    <!-- ./button.tpl.html -->
    <button>Click Me</button>
    
    <!-- ./index.html -->
    <div>
      <include src="button.tpl.html"></include>
    </div>
    
    <!-- Result after rendering `index.html`. -->
    <div>
      <button>Click Me</button>
    </div>
  • Registration and resolution of all custom web components

    <!-- Use <template is="my-component-name"> to register a component. -->
    <template is="my-red-button">
      <button style="background-color: red;">
        <slot></slot>
      </button>
    </template>
    
    <!-- Any node traversed after registration can use the component. -->
    <my-red-button :on:click="console.log('clicked')">
      <!-- The contents within will replace the `<slot></slot>` tag. -->
      Click Me
    </my-red-button>

Rendering

Once the HTML has been preprocessed, it is rendered by traversing every node in the DOM and applying a series of plugins. Each plugin is only applied if specific conditions are met such as the HTML element tag or attributes match a specific criteria. Here's the list of attributes handled:

  • :data provides scoped variables to all subnodes, evaluated using jexpr
    <div :data="{ name: 'Stranger' }"></div>
  • :for clones the node and repeats it
    <div :for="item in ['a', 'b', 'c']">{{ item }}</div>
  • :text sets the textContent value of a node
    <div :data="{foo: 'bar'}" :text="foo"></div>
  • :html sets the innerHTML value of a node
    <div :html="<span>Hello World</span>"></div>
  • :show toggles $elem.style.display to none
    <div :data="{foo: false}" :show="foo"></div>
  • :bind binds (two-way) a variable to the value or checked property of the element.
    <div :data="{ name: 'Stranger' }">
      <input type="text" :bind="name" />
    </div>
  • :on:{event} adds an event listener for event to the node
    <button :on:click="console.log('clicked')"></button>
  • :{attribute} sets the corresponding property for attribute in the node
    <a :href="buildUrl()"></a>
  • {{ value }} replaces value in text nodes
    <button :data="{label: 'Click Me'}">{{ label }}</button>

Evaluation

To avoid violation of Content Security Policy (CSP) that forbids the use of eval(), Mancha evaluates all expressions using jexpr. This means that only simple expressions are allowed, and spaces must be used to separate different expressions tokens. For example:

<!-- Valid expression: string concatenation -->
<body :data="{ pos: 1 }">
  <p :text="'you are number ' + pos + ' in the queue'"></p>
</body>

<!-- Valid expression: boolean logic -->
<body :data="{ pos: 1, finished: false }">
  <p :show="pos >= 1 && !finished">you are number {{ pos }} in the queue</p>
</body>

<!-- Valid expression: ternary operators -->
<body :data="{ pos: 1 }">
  <p :text="pos % 2 == 0 ? 'even' : 'odd'"></p>
</body>

<!-- Valid expression: function calling -->
<body :data="{ pos : 1 }">
  <p :text="buildQueueMessage()"></p>
  <script>
    const { $ } = Mancha;
    $.buildQueueMessage = function () {
      return "you are number " + this.pos + " in the queue";
    };
    // Alternatively, anonymous functions without `this`:
    // $.buildQueueMessage = () => 'you are number ' + $.pos + ' in the queue';
  </script>
</body>

<!-- Valid expression: simple assignment -->
<body :data="{ pos: 1 }">
  <p :text="'you are number ' + pos + ' in the queue'"></p>
  <button :on:click="pos = pos + 1">Click to get there faster</button>
</body>

<!-- Invalid expression: missing spaces -->
<body :data="{ pos: 1 }">
  <p :text="'you are number '+pos+' in the queue'"></p>
</body>

<!-- Invalid expression: multiple statements -->
<button :on:click="console.log('yes'); answer = 'no'"></button>

<!-- Invalid expression: function definition -->
<body :data="{ foo: () => 'yes' }">
  <p :text="foo()"></p>
</body>

<!-- Invalid expression: complex assignment -->
<body :data="{ pos: 1 }">
  <p :text="'you are number ' + pos + ' in the queue'"></p>
  <button :on:click="pos++">Click to get there faster</button>
</body>

Scoping

Contents of the :data attribute are only available to subnodes in the HTML tree. This is better illustrated with an example:

<body :data="{ name: 'stranger' }">
  <!-- Hello, stranger -->
  <h1>Hello, {{ name }}</h1>

  <!-- undefined -->
  <span>{{ message }}</span>

  <!-- How are you, danger? The secret message is "secret" -->
  <p :data="{ name: 'danger', message: 'secret' }">
    How are you, {{ name }}? The secret message is: "{{ message }}".
  </p>
</body>

By default, the target root element is the body tag. So, any variables defined in the body's :data attribute are available to the main renderer.

In the example above, the variable message is only available to the <p> tag and all elements under that tag, if any. Since the variables are not accessible via the global object, you'll need to retrieve the renderer from the element's properties:

// Explicitly render the body, so we can await it and then modify variables.
const { $ } = Mancha;
await $.mount(document.body);

// This modifies the `name` variable in all the renderer contexts.
$.name = "world";

// This has no effect in the output, because the content of the `<p>` tag is
// bound to its local variable and `message` was undefined at rendering time.
$.message = "bandit";

// We extract the subrenderer from the element's properties. Only elements
// with `:data` attribute have a `renderer` property.
const subrenderer = document.querySelector("p").renderer;

// This modifies the `message` variable only in the `<p>` tag.
subrenderer.$.message = "banana";

Styling

Some basic styling rules are built into the library and can be optionally used. The styling component was designed to be used in the browser, and it's enabled by adding a css attribute to the <script> tag that loads mancha. The supported rulesets are:

  • basic: inspired by these rules, the full CSS can be found here.
  • utils: utility classes inspired by tailwindcss, the resulting CSS is a drop-in replacement for a subset of the classes provided by tailwindcss with the main exception of the color palette which is borrowed from material design.

Usage

Client Side Rendering (CSR)

To use mancha on the client (browser), use the mancha bundled file available via unpkg.

<body :data="{ name: 'John' }">
  <span>Hello, {{ name }}!</span>
</body>

<script src="//unpkg.com/mancha" target="body" css="basic+utils" init></script>

Script tag attributes:

  • init: whether to automatically render upon script load
  • target: document elements separated by + to render e.g. "body" or "head+body" (defaults to "body")
  • css: inject predefined CSS rulesets into the <head> element, see the styling section for more details.

For a more complete example, see examples/browser.

Compile Time Server Side Rendering (SSR)

To use mancha on the server at compile time, you can use the npx mancha command. For example, if this is your project structure:

src/
├─ components/
|  ├─ main.tpl.html
|  ├─ footer.tpl.html
├─ index.html
├─ vars.json

You can run the following command to compile the site into a public folder:

npx mancha --input="./src/index.html" --vars="$(cat vars.json)" --output="./public"

For a more complete example, see examples/compiled.

On Demand Server Side Rendering (SSR)

You can also use mancha as part of your server's request handling. Assuming a similar folder structure as described in the previous section, the following express node server would render the HTML code on demand for each incoming request:

import express from "express";
import { Renderer } from "mancha";
import vars from "./vars.json";

const app = express();

app.get("/", async (req, res) => {
  const name = req.query.name || "Stranger";
  // Instantiate a new renderer.
  const renderer = new Renderer({ name, ...vars });
  // Preprocess input HTML from a local file path.
  const fragment = await renderer.preprocessLocal("src/index.html");
  // Render and serialize output HTML.
  const html = renderer.serializeHTML(await renderer.renderNode(fragment));
  // Send it to the client.
  res.set("Content-Type", "text/html");
  res.send(html);
});

app.listen(process.env.PORT || 8080);

For a more complete example, see examples/express.

Web Worker Runtime Server Side Rendering (SSR)

For servers hosted as worker runtimes, such as Cloudflare Workers, you will need to import a stripped down version of mancha that does not have the ability to read local files.

import { Renderer } from "mancha/dist/worker";
import htmlIndex from "./index.html";
import vars from "./vars.json";

self.addEventListener("fetch", async (event) => {
  // Instantiate a new renderer.
  const renderer = new Renderer({ ...vars });
  // Preprocess input HTML from a string.
  const fragment = await renderer.preprocessString(htmlIndex);
  // Render and serialize output HTML.
  const html = renderer.serializeHTML(await renderer.renderNode(fragment));
  // Send it to the client.
  event.respondWith(new Response(content, { headers: { "Content-Type": "text/html" } }));
});

To meet the size requirements of popular worker runtimes, the worker version of mancha uses htmlparser2 instead of jsdom for the underlying HTML and DOM manipulation. This keeps the footprint of mancha and its dependencies under 100kb.

For a more complete example, see examples/wrangler.

Dependencies

The browser bundle contains a single external dependency, jexpr. The unbundled version can use htmlparser2, which is compatible with web workers, or jsdom.