Git Product home page Git Product logo

micromustache's Introduction

Downloads GitHub stars Known Vulnerabilities GitHub license Version code style: prettier GitHub issues

micromustache

Logo

A secure, fast and lightweight template engine with some handy additions.

⛹ Check out the playground

Think of it as a sweet spot between plain text interpolation and mustache.js; Certainly not as logic-ful as Handlebars! Sometimes a stricter syntax is the right boundary to reduce potential errors and improve performance. This tool has a limited scope that doesn't attempt to solve everybody's use case, and instead do a specific thing well.

  • 🏃 Faster than MustacheJS (Micromustache is the fastest template engine that doesn't need pre-compilation and still works in CSP environments)
  • 🔒 Secure has limits for variable length, number of interpolations, nesting debth and common Javascript pitfalls (__proto__, constructor, getters/etc). Works in CSP environments (no usage of eval() or new Function()). Published only with 2FA.
  • Bracket notation support a[1]['foo'] accessors (mustache.js/handlebar syntax of a.1.foo is also supported).
  • 🎈 Lightweight less than 350 source lines of code, easy to audit, small API surface, easy to pick up
  • 🏳 No dependencies
  • 🐁 Small memory footprint sane caching strategy, no memory leak
  • 🚩 Meaningful errors to improve developer experience. All functions test their input contracts and throw meaningful errors to improve developer experience (DX).
  • TypeScript types included out of the box and updated with every version of the library
  • 🐇 Works in node (CommonJS) and Browser (UMD) and EcmaScript 6 Modules (ESM)
  • 🛠 Thoroughly tested (full test coverage over 120+ tests). Also tested to produce the same results as Mustache.js.
  • 📖 Full JSDoc documentation

If variable interpolation is all you need, micromustache is a drop-in replacement for MustacheJS (see its differences with Mustache.js)

Try it in your browser!

Getting started

Use directly with UNPKG:

import { render } from 'https://unpkg.com/browse/micromustache/dist/micromustache.mjs'
console.log(render('Hello {{name}}!', { name: 'world' }))
// Hello world!

Install:

$ npm i micromustache

Use:

const { render } = require('micromustache')
console.log(render('Hello {{name}}!', { name: 'world' }))
// Hello world!

Why not just use EcmaScript template literals?

Template literals work great when the template and the variables are in the same scope but not so well when the template is in another scope or is not known ahead of time. For example, suppose you had a function like this:

function greet(name) {
  return `Hi ${name}!`
}

After your function became successful and you got rich 🤑 you may decide to dominate the world and expand to new markets which speak other languages. You need to internationalize it. Adding one more language is easy:

function greet(name, lang) {
  // Note the lang parameter that contains a language code
  return lang === 'sv' ? `Hej ${name}!` : `Hi ${name}!`
}

But how about a bunch of them?

function greet(name, lang) {
  switch (lang) {
    case 'sv': return `Hej ${name}!`
    case 'es': return `Hola ${name}!`
    default:
    case 'en': return `Hi ${name}!`
  }
}

That doesn't scale well as you dominate country after country and need to support more languages! Besides, that's just one string! The main problem is that the content (the text) is coupled to the code (the variable interpolation). Template engines help you to move the content out of the function and let something else deal with that concern.

const { render } = require('micromustache')
// A very simplified i18n database
const db = {
  en: {
    greeting: 'Hi {{name}}!',
    // ...
  },
  sv: {
    greeting: 'Hej {{name}}!',
    // ...
  },
  // ...
}

function greet(name, lang) {
  return render(db[lang].greeting, { name } )
}

Now it's better! 😎 All the templates are together and they are easy to update and translate. By default, we use the popular syntax that encloses paths between double curly braces ({{ and }}) but you can customize micromustache if you prefer something else. Just like template literals, you can of course reference deep nested objects:

const { render } = require('micromustache')
const scope = {
  fruits: [
    { name: 'Apple', color: 'red' },
    { name: 'Banana', color: 'yellow' },
  ]
}
console.log(render('I like {{fruits[1].color}}!', scope))
// I like Bababa!

It worth to note that Mustache and Handlebars don't support fruits[1].color syntax and rather expect you to write it as fruits.1.color.

The real power of micromustache comes from letting you resolve a path using your own functions! To pass a resolver function, you can use renderFn() instead of render():

const { renderFn } = require('micromustache')
// Just converts the path to upper case
const up = str => str.toUpperCase()

console.log(renderFn('My name is {{Alex}}!', up))
// My name is ALEX!

The resolver gets the scope as its second parameter. If you want to lookup a value, there's a get() function as well:

const { renderFn, get } = require('micromustache')

// Looks up the value and converts it to stars
function star(path, scope) {
  // path comes from the template and is 'password' here
  // scope is { password: 'abc' }
  const value = get(scope, path) // value is 'abc'
  return '*'.repeat(value.length)
}

console.log(renderFn('My password is {{password}}!', star, { password: 'abc' }))
// My password is ***!

If you want to resolve a value asynchronously, we got you covered using the renderFnAsync() instead of renderFn(). For example the following code uses node-fetch to resolve a url.

const { renderFnAsync } = require('micromustache')
const fetch = require('node-fetch')

async function taskTitleFromUrl(url) {
  const response = await fetch(url)
  const obj = await response.json()
  return obj.title
}

console.log(await renderFnAsync('Got {{https://jsonplaceholder.typicode.com/todos/1}}!', fetch))
// Got delectus aut autem!

If you find yourself working on a particular template too often, you can compile() it once and cache the result so the future renders will be much faster. The compiler returns an object with render(), renderFn() and renderFnAsync() methods. The only difference is that they don't get the template and only need a scope:

const { compile } = require('micromustache')
const compiled = compile('Hello {{name}}! I am {{age}} years old!')
console.log(compiled.render({ name: 'world', age: 42 }))
// Hello world! I'm 42
// The methods are bound so you can use the destructed version for brevity
const { render } = compile
console.log(render({ name: 'world', age: 42 }))
// Hello world! I'm 42

If the compiled variable above is garbage collected, the cache is freed (unlike some other template engines that dearly keep hold of the compiled result in their cache which may leads to memory leaks or out of memory errors over longer usage).

Using the options you can do all sorts of fancy stuff. For example, here is an imitation of the C# string interpolation syntax:

const { render } = require('micromustache')
const $ = scope => strings => render(strings[0], scope, { tags: ['{', '}'] })

const name = 'Michael'
console.log($({ name })`Hello {name}!`)
// Hello Michael!

API

On Github pages

Examples

Check out the examples directory. Note that they need you to build the project locally.

FAQ

On wiki

Known issues

On wiki

License

MIT


Made in Sweden 🇸🇪 by @alexewerlof

micromustache's People

Contributors

alexewerlof avatar bripkens avatar dependabot[bot] avatar evanhahn avatar joebowbeer avatar onepagecraft avatar userpixel avatar victorwpbastos avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

micromustache's Issues

Improve the error messages by giving a context in the template

Template: hello {{name}}}}
[roughly] current error: Variable names cannot have "}}"
Desired error:

...hello {{name}}}}
               ^
Variable names cannot have "}}"

Maybe our syntax errors can even have an extra property indicating the position that the error happened.

break get to two functions

Current behavior

The signature varies. Currently it's very similar to lodash but it doesn't have to be.

Expected behavior

Simpler functions are preferred.
The "get" function that gets the value of a ref string can still be called get()
The part that looks up the value using the path can be called lookup()
An important key here is to use the same options structure and pass it to the path so it won't dig too deep unnecessarily when parsing a path that is going to end up being longer than maxDepth

Tips and tricks

Tips and tricks

To put in the wiki or in the code

Security

Template compilation and interpolation is a resource consuming task.
The algorithms in most template engines (including regular expression matching) runs in synchronous mode and can block the event loop. Blocking the event loops can lead to:

  • Janky UI in the frontend
  • Inaccessible server on the backend

Use worker threads if you know that your templates are going to be large or contain too many paths.

Parsing the template can be particularly resource consuming (even for a light library like Micromustache). There are multiple options available to put a limit on how the template is parsed.

Multi-level processing

This one is very important because one of the main use cases (and its original reason for existence) for MicroMustache is internationalization and it's common for i18n templates to require multi-level interpolation. Demo a real use case with the i18n.js example.

Performance

Use paths like a.b.c instead of a['b']['c'] because it is much faster to parse.

If it is possible compile the template once and reuse the results.

Don't allow huge varNames

due to the risk for DDoS put a limit on how long the var names can be.
A deeply nested very long var name can potentially DDoS the user's code.
Initially the limit can be 1000 characters which using the a.b.c.d... syntax can lead to a maximum depth of 500 lookups.

Throw an error for orphan close symbol

Current behavior

Hello {{name.a}} {{! throws
Hello {{name.a}} }}! doesn't. Neither does Hello }} {{name.a}}!

Expected behavior

Check the string before adding to the result array.

Rename npm run docs to npm run build:docs

Current behavior

npm run docs generates the documentation while build:ts builds the TypeScript files and

Expected behavior

Update the script and wherever it is used (travis, documentation, etc.)

Support function and class types for view

There shouldn't be any obligation for the view to be an object. Let it be a class or function as well. This may complicate the call since view and generalValueFn are both optional.

Support for async custom resolver

I'd like to be able to use async functions in my custom resolver. How feasible/easy is it to add an async version of the render function?

E.g.,

async customResolver(varName, view) {
  return view[varName] || await lookUpVar(varName);
}
async bar() {
  await micromustache.renderAsync("{{foo}}", {}, customResolver);
}

When looking up the object only support own props

Changing this behavior will break looking up properties from objects that are instantiated from classes:

class A { get z() { return 2}}
const b = new A
b.z // 2
b.hasOwnProperty('z') // false

Dealing with the __proto__ pollution vulnerability is outside the scope of this lib:

f = { foo: 'f' }
g = {__proto__: f, bar: 'g'}
g.foo // "f"
g.bar // "g"
g.__proto__ = { cux: 'c' }
g.cux // "c"
g.foo // undefined

Can no longer import micromustache v6 using ECMAScript Modules

Prior to v6, you could use ECMAScript Modules with Node and this package.

For some reason, there isn't the export for render in the current build. I am using this package inside a bigger project and this is the only time I've come across an error with a Node package.

The code I am trying to run:

import {render} from 'micromustache';
console.log("Hello, world!");

The result when running with node --es-module-specifier-resolution=node --experimental-modules index.mjs:

import {render} from 'micromustache';
        ^^^^^^
SyntaxError: The requested module 'micromustache' does not provide an export named 'render'
    at ModuleJob._instantiate (internal/modules/esm/module_job.js:91:21)
    at async ModuleJob.run (internal/modules/esm/module_job.js:106:20)
    at async Loader.import (internal/modules/esm/loader.js:132:24)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! [email protected] start: `node --es-module-specifier-resolution=node --experimental-modules index.mjs`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the [email protected] start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

Sample repro: https://github.com/gda-gp/micromustache-demo

Hooks

Current behaviour

The renderFn() and renderFnAsync() seem to be mainly underused if I am to trust Github search.
This feature is one of the killer features of micromustache, so let's rethink how to make it more intuitive.

Expected behavior

Add the possibility to process paths before and after they are resolved using get(). This can allow HTML escaping for example. Also see #50

The code (and its usage) will be simpler if we have some steps for resolving and allow hooks to be implemented to modify the default behaviour:

  1. before the path is parsed
  2. after the path has turned into a ref but before that ref is resolved from the scope
  3. after the value is resolved from the scope and before it is being interpolated

At each step, a hook can modify its input, throw, or cancel the rest of the chain

Also it should be possible to disable a step (say get() for example)

That way we can have only two methods: render() and renderAsync() and the hooks can be passed as options.

maxVarNameLength range should be inclusive

Current behavior

Have a template like {{person.name}} and give maxVarNameLength=11 as an option.
It'll throw saying "SyntaxError: The variable name is longer than expected. Max: 11, Got: 11"

Expected behavior

It shouldn't be an error.
Also the correct error type for this kind of error is RangeError not SyntaxError.

A key cannot be 'null' or 'undefined'

  it('can lookup a key that is literally "null"', () => {
    const obj = {
      null: 'some value for null'
    }
    expect(get(obj, 'foo')).to.equal(obj.null)
  })

  it('can lookup a key that is literally "undefined"', () => {
    const obj = {
      undefined: 'some value for undefined'
    }
    expect(get(obj, 'foo')).to.equal(obj.undefined)
  })

Improve DX with a better error

Current behavior

Template: {{ndd}}
options.tags: ['{', '}}']
options.maxVarNameLength: 4
Error: SyntaxError: The variable name is longer than expected. Max: 4, Got: 4

Technically what's happenning is that due to a different open tag, we are seeing {ndd as a variable name. But the error doesn't give a clear picture of what's going on.

Expected behavior

Let's improve the DX and show the variable name in the error as well.

Address a few edge cases

Current behavior

The following variables are invalid javascript paths but the tokenizer lets them through (even with { validateVarNames: true }):

  • {{name[a].}} is a valid path
  • {{name,}}
  • {{name;}}

(they are considered prop names like xxx['name,'])

Expected behavior

Go with POLA.

Vulnerability Assessment

This issue is an ongoing work and will be completed as I find time to investigate more

This activity assesses the risks of identified threats and deviations in system behavior.

Vulnerabilities

The weaknesses that may be exploited by a threat to create loss.

  1. Low performance (eg. hurting response time when used in server code or time to first meaningful paint when used browser side)
  2. Out of memory error
  3. Crash the application
  4. Run unwanted code (if the adversary finds a way to hack/modify the render functions)
  5. Access information from outside the scope
  6. Change/override the behavior of the lib to affect the rest of the user code.

Threats

  1. Malicious templates (including variable names)
  2. Malicious resolver functions (developer or third party software)
  3. Malicious options that may lead to vulnerability (developer)

Attack vectors

  1. The template
  2. The scope object
  3. The variable values (when using resolve() or get())
  4. The resolveFn() and resolveFnAsync() functions

Typo in the error

Current behavior

This line:

${propName} is not defined in the scope (${String(scope)}) at path: "${propNamesAsStr()}"}

Expected behavior

This error should not end in }
Also, since scope is an object most of the times, there's no point just printing it.

Applying function to specific elements from scope, passing custom arguments to renderFn

Hey @userpixel,

Kudos for making an awesome library while focusing on security! I really appreciate it! 🏆

Are you considering implementing the ability to apply custom functions to just specific elements from the scope? eg.

Apply truncate function to just 1st and 3rd element and not the 2nd one?

const template = "My name is {{ truncate name }}, I am {{ age }} old. I come from {{ truncate country }}.

Also, are you considering adding support for custom arguments to be passed to function? eg. in the mentioned case {{ truncate name 3 }}, so 3 could be passed to the function definition?

Thanks ❤️ and keep up the awesome work!

Rename Renderer to Generator

Current behavior

Too many "render" functions. It's a bit confusing to have a class named "Renderer" as well.

Expected behavior

In compiler lingo, the task of this class is done in "generation" step. So let's call it Generator.

Make an example directory

With lots of real world use cases and make sure that the API supports an intuitive and easy solution for them.
The examples should somehow run in the standard unit tests as well.

Make an example for path validation

You can use the renderFn() to validate varNames and then render() to do the quick render.
also you can set that flag upon compilation that validates the names (or set it to false to let you do it)

Use Jest table tests

Current behavior

After converting tests from Mocha to Jest, this awesome feature of Jest is underused.

Expected behavior

The code looks better with this easy refactoring.

Add an option so that strinfigy to JSON.stringify objects

Current behavior

If the value is an object or array, [object Object] will be put into the template. This might not be very helpful for debugging (and sometimes this is exactly what you would want, which forces you to write a transformation).

Expected behavior

Add an option like stringifyObjectValues or equivalent in the stringify options and let it put a nicer presentation of objects in the output.

Out of scope

One may want to give special treatment to arrays or set the indentation as well but I'm not aware of any strong use case so let's not add that now.

Change sloc to count the distributed lines

Current behavior

We count the lines of code from src which is a bit misleading because that's not exactly what is distributed and matters.

Expected behavior

Count the lines of the dist folder. As a bonus 360+ SLOC of TS leads to 230~ SLOC of ES6!

"sloc": "npm run build:ts && sloc dist -e .*spec.* -k source --format cli-table",

Break the perf inspect to use a util

Current behavior

The two perf scripts are similar but don't share code.

Expected behavior

Put the common code in a file and let the two scripts be:

  • against mustachejs
  • render vs compile

Document the security features on the main readme

Current behavior

We have added a bunch of security-related boundries and checks but they are not super obvious although they are the main reason this library should be used (which also has had performance costs, so they are important)

Expected behavior

List all the security features in one section in the main readme. Give it focus and dominance.

Refactor the get* functions

Current behavior

So far it's decided that v9 is going to have two functions for getting (one for Ref and one for Path).

Expected behavior

Let's call them pathGet and refGet and have the path or ref as their first argument. It's good to have their signature like the render* functions.

Simplify the util functions

Current behavior

We have a bit of redundancy there.

Expected behavior

Now that a full featured JTY lib is created, let's keep the utils to bare minimum to have smaller and faster code.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.