tempura

repository·master·Indexed 19 days ago

https://github.com/lukeed/tempura

A lightweight, high-performance template engine (v0.4.1) featuring a Handlebars-like syntax. It supports both build-time transformation via Rollup and esbuild plugins and runtime rendering using `compile()`. Key features include custom directives (blocks), variable expectations with `#expect`, inline variables with `#var`, and conditional logic. It provides a `transform()` function to convert templates into ESM or CJS strings and a `tempura.esc()` utility for HTML escaping.

Tokens
8.6K
Snippets
39
Records
41
Agent score
65%

What's inside tempura

  1. Tempura template syntax overview

    master

    Tempura uses a syntax similar to Handlebars, supporting features like:

    • Expectations: Define expected properties using {{#expect prop1, prop2 }}.
    • Variables: Define inline variables using {{#var name = value }}.
    • Conditionals: Use {{#if condition}}, {{#elif condition}}, and {{#else}}.
    • Iteration: Use {{#each collection as item}} to loop over arrays.
    • Escaping: Standard interpolation is escaped; use triple braces {{{ value }}} for unescaped output.

    For a full list of syntax rules, refer to the Syntax Cheatsheet.

    {{#expect title, items }}
    
    {{#var count = items.length }}
    
    {{#if count == 0}}
      <p>Done!</p>
    {{#else}}
      <p>You have {{{ count }}} items!</p>
      <ul>
        {{#each items as todo}}
          <li>{{ todo.text }}</li>
        {{/each}}
      </ul>
    {{/if}}
    {{/expect}}
  2. Use `tempura.compile` results as custom blocks

    master

    Because the output of tempura.compile shares the same Compiler interface as a custom block—(data, blocks?) => Promise<string> | string—you can use compiled templates as custom blocks. This allows for powerful composition of complex template logic.

    // Define a block using a compiled template
    let blocks = {
      bar: tempura.compile(`
        {{#expect name}}
        <bar>{{ name }}</bar>
      `),
    };
    
    // Define another block that uses the 'bar' block
    blocks.foo = tempura.compile(`
      {{#expect other}}
      <foo>
        {{#if other}}
          {{#bar name=other }}
        {{/if}}
      </foo>
    `, { blocks });
    
    let render = tempura.compile('{{#foo other=123}} – {{#bar name="Alice"}}', { blocks });
    render(); //=> "<foo><bar>123</bar></foo> – <bar>Alice</bar>"
  3. Handle HTML escaping for expressions

    master

    By default, all expressions wrapped in {{ }} are HTML-escaped (e.g., < becomes &lt;).

    To disable escaping and print a raw value, use the triple-curly syntax: {{{ value }}}. This is useful when you want to output pre-constructed HTML or values you know are safe.

    {{! input }}
    escaped: {{ 'a & b " c < d' }}
    raw: {{{ 'a & b " c < d' }}}

    // Output: // escaped: a & b " c < d // raw: a & b " c < d

  4. Invoke one custom block from another

    master

    Custom blocks can reference other custom blocks directly without needing external helper functions. To do this, use the second argument provided to the block definition function, which is the blocks object (the same object passed in options.blocks).

    When calling another block via the blocks object, you must manually construct an object containing the arguments that the target block expects.

    let options = {
      blocks: {
        foo(args, blocks) {
          let output = '<foo>';
          if (args.other) {
            // Call the 'bar' block directly by passing its expected argument shape
            output += blocks.bar({ name: args.other });
          }
          return output + '</foo>';
        },
        bar(args, blocks) {
          return `<bar>${args.name}</bar>`;
        }
      }
    };
    
    let render = tempura.compile('{{#foo other=123}} – {{#bar name="Alice"}}', options);
    render(); //=> "<foo><bar>123</bar></foo> – <bar>Alice</bar>"
  5. Add comments to templates

    master

    Use {{! comment }} for template comments. These are removed during rendering and will not appear in the output. Standard HTML comments (e.g., <!-- comment -->) are preserved and will appear in the rendered output.

    <!-- HTML comments are kept in output -->
    {{! template comments are removed }}
    {{! 
      template comments 
      can also be multi-line 
    !}}
    <p>hello world</p>
  6. Implement recursive Compiler Blocks

    master

    To create a recursive block using tempura.compile, you must handle a circular dependency in the options.blocks object.

    The Pattern:

    1. Initialize a blocks object with a placeholder (e.g., null) for the block that will call itself.
    2. Define the block by calling tempura.compile and passing that same blocks object in the options.
    3. Assign the resulting compiler to the placeholder key in the blocks object.

    This ensures that when the block is parsed, the key for the recursive call already exists in the blocks object, even if the functional definition hasn't been assigned yet.

    let blocks = {
      // 1. Placeholder for the recursive block
      loop: null,
    };
    
    // 2. Define the block and pass the blocks object into its own options
    blocks.loop = tempura.compile(`
      {{#expect value }}
    
      {{ value }}
      {{#if value-- }}
        ~> {{#loop value=value }}
      {{/if}}
    `, { blocks });
    
    // 3. Use it
    let render = tempura.compile('{{#loop value=3 }}', { blocks });
    render(); //=> "3 ~> 2 ~> 1 ~> 0"
  7. Define custom blocks in Tempura

    master

    Custom blocks are stateless functions used as template helpers. They must be defined in the options.blocks object passed to tempura.compile or tempura.transform.

    Key Requirements:

    • Each block function receives an args object containing parsed template arguments.
    • Blocks must return a string.
    • If a template references a block (e.g., {{#foo}}) that is not defined in options.blocks.foo, a parsing error will be thrown.
    • To use async blocks, you must set options.async to true in your configuration.
    let options = {
      blocks: {
        script(args) {
          let { src, defer, type } = args;
          let output = `<script src="${src}"`;
          if (type) output += ` type="${type}"`;
          if (defer) output += ' defer';
          output += '></script>';
          return output;
        }
      }
    };
    
    // Usage in template:
    // {{#script src="main.js" defer=true }}
  8. Use tempura with Rollup or esbuild

    master

    Tempura can be integrated into any environment that requires a build step (such as Cloudflare Workers) using its Rollup or esbuild plugins. These plugins transform *.hbs template files into JavaScript functions during the build process.

    Crucially, this approach ensures that no tempura runtime code is included in your final built output, with the exception of tempura.esc (189 bytes) if you utilize HTML-escaped sequences.

  9. Use expressions to print values

    master

    You can print any value (strings, objects, arrays, etc.) by wrapping it in double curly braces {{ value }}. These expressions are replaced by the value during evaluation. For example, {{ items.length }} will print the length of an array provided in the input object.

    {{#expect name}}
    <p>Hello, {{ name }}!</p>

    // Input: { name: 'world' } // Output: <p>Hello, world!</p>