regve

A fast and dynamic view engine similar to handlebars.

Usage no npm install needed!

<script type="module">
  import regve from 'https://cdn.skypack.dev/regve';
</script>

README

Regex View Engine

npm Libraries.io dependency status for latest release GitHub top language NPM

npm npm

paypal

A fast and dynamic view engine similar to handlebars.

This module is focused on being a fast, and easy to use view engine for nodejs. Running most things in regex allows for a lightweight, fast, and dynamic html template engine.

This view engine avoids throwing errors on undefined values. The engine instead, simply hides undefined (or falsy) values. This means, you don't get that annoying crash when you simply don't what to show a value, instead the engine assumes you want to use the value only if it exists. It can handle nested objects without crashing even if the parent (or grandparent) object is undefined. This module also auto closes html tags (apart from those that should not close), and removes html comments unless they start with ! or @, or they include copyright, (c), license, or license.

The view engine runs mainly through regex functions, to try and gain improved speed and performance. Additionally, regex has the benefit of recognizing patters, which allows for an easy dynamic template engine.

The syntax of this view engine is similar to handlebars, so getting started should be easy. This is to start you off with a pattern that you may be familiar with already. Although, this is not completely the same as handlebars, and .hbs files will complain about errors, which do not exist in this template engine. In this template engine, those errors do not exist, because there handled dynamically and common mistakes are recognized and corrected by the engine.

To help with crash resistance, the file is read, not run. This view engine is running on javascript regex functions, so a file is simply read as a string.

This view engine has some (optional) basic markdown like features, by using regex to replace markdown with html. You can add variables and use if and each statements. You can also import other views into the current view. You can choose any html tag name, and have it automatically moved to a different location.

The if statements support & (and) | (or) operators, as well as the ! (not) operator. If statements also support < = > and you can check if a var is equal to a 'string'.

There are also some shortened methods for doing common tasks in a simpler way.

What's New

  • Extended Markdown Support
  • Added Components

Installation

npm install @aspiesoft/regve

Setup

// express

const express = require('express');
const regve = require('@aspiesoft/regve');

const app = express();

app.engine('html', regve({
  /* global options */
  opts: {default: 'some default options for res.render'}
}));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'html');

app.use(function(req, res, next){
    res.render('index', {title: 'example', content: "<h2>Hello, World!</h2>"});
});


// render from string

const regve = require('@aspiesoft/regve');

regve({/* global options */});

let html = '#Hello, {{name}}!';

html = regve.render(html, {name: 'World'});

console.log(html);

You can also define a template you want to use

regve({template: 'layout'});

Other options you might need if Not using express

// if your Not using express, you can still define the the views path and file type in another way
path = require('path');
regve({
  dir: path.join(__dirname, 'views'),
  type: 'html'
});

Usage

// to disable everything, and send raw html, just set the raw option to true
// note: cache will still run if set
app.engine('html', regve({raw: true}));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'html');
<!-- add vars -->
{{title}}

<!--add vars with objects-->
{{title.default}}
{{title.1}}

<!-- add html -->
<!-- {{{3}}} allows html and {{2}} escapes html -->
{{{content}}}

<!-- set attributes if the value exists -->
<script src="/script.js" {{nonce=nonce_key}}></script>
<!-- or if the attribute is named after the var -->
<script src="/script.js" {{=nonce}}></script>
<!-- or if your ide auto adds quotes -->
<script src="/script.js" {{nonce="nonce_key"}}></script>
<script src="/script.js" {{="nonce"}}></script>
<!-- for all of those, the result html will look something like this -->
<script src="/script.js" nonce="12345"></script>


<!-- if statements -->
{{#if title}}
    <title>{{title}}</title>
{{else}}
    <title>default title</title>
{{/if}}

<!-- the '!' (not) operator -->
<!-- this is used instead of an 'unless' function -->
{{#if !content}}
  no content
{{else}}
  {{{content}}}
{{/if}}

<!-- if statements also support else if -->
{{#if title1}}
  <title>{{title1}}</title>
{{else title2}}
  <title>{{title2}}</title>
{{else}}
  <title>Default Title</title>
{{/if}}

<!-- if statements also support '&' (and) '|' (or) operators -->
{{#if name & url}}
  <a {{href="url"}}>{{name}}</a>
{{else name | url}}
  <!-- unset tags are simply removed, and do Not throw an error -->
  {{name}} {{url}}
{{/if}}

<!-- you can also use the '|' operator in vars, similar to how you would in javascript -->
{{{myContent | myBackupContent}}}
<html {{lang="selected-lang|default-lang"}}></html>

<!-- you can use the '|' operator in first level each statements as well -->
<!-- note: you must Not include spaces in each statements -->
{{#each items|itemsDefault as item}}
  {{item}}
  <br>
{{/each}}

<!-- the '|' operator is also supported in any custom function (without spaces) -->

<!-- if equal operator -->
{{#if item = 'item1'}} <!-- if item === string -->
  this is the first item
{{else item = defaultItem}} <!-- if item === var -->
  this is {{defaultItem}}
{{else !item}}
  there is no item
{{/if}}

<!-- if not equal operator -->
{{#if item != 'item1'}} <!-- if item !== string (same with var) -->
  this is Not the first item
{{/if}}

<!-- note: number strings will be converted to numbers when checking equality ('1' = '01' & '1' = '1.0' output: true) -->

<!-- if < (or) > operators -->
{{#if '1' < '2'}}
  true
{{/if}}

{{#if '1' > '2'}}
  false
{{/if}}

{{#if '1' <= '1'}}
  true
{{/if}}
{{#if '1' < '1'}}
  false
{{/if}}

{{#if '2' >= '1'}}
  true
{{/if}}
{{#if '2' > '2'}}
  false
{{/if}}


<!-- | 'string' operator -->
<!-- you can use quotes to set a var | a fallback to a string, similar to how it works in javascript -->
{{test | 'default'}}

<!-- if you surround a var with quotes, it will be in quotes if it exists -->
{{"test"}}


<!-- each statements -->
<!-- in js {list: {userID1: 'username1', userID2: 'user2'}} -->
{{#each list as item of index}}
  {{index}} = {{item}}
{{/each}}

<!-- you can also include from object (outputs the name of the object your running on) -->
{{#each list as item of index from object}}
  {{object}} <!-- output: list -->
  {{index}} = {{item}}
{{/each}}

<!-- these attrs can be in any order (or undefined), as long as the object your running on is first -->
{{#each list from object of index as item}}
  {{object}}:
  {{index}} = {{item}}
{{/each}}

<!-- each statements with objects -->
<!-- in js {list: [{id: 'userID1', name: 'username1'}, {id: 'userID2', name: 'user2'}]} -->
{{#each list as item of index}}
  {{item.id}} = {{item.name}}
{{/each}}

<!-- each statements can have if statements inside -->
{{#each list as item}}
  {{#if item}}
    <a href="{{item.url}}">{{item.name}}</a>
  {{/if}}
{{/each}}

<!-- supports nested each statements -->
<!-- in js {menus: {main: [{url: '/', name: 'Home'}, {url: '/youtube', name: 'YouTube'}], youtube: [{url: '/youtube/video', name: 'Video'}]}} -->
{{#each menus as menu of type}}
  <div {{="type"}}>
    <!-- pulls reference from "as menu" -->
    {{#each menu as item}}
        <a {{href="item.url"}}>{{item.name}}</a>
    {{/each}}
  </div>
{{/each}}

<!-- you can run 2 or more each statements at the same time with the & (and) operator (no spaces) -->
{{#each list1&list2 as item of index from list}}
  {{list}}:
  {{index}} = {{item}}
  {{#if list = 'list1'}}
    this is the first list
  {{/if}}
  <br>
{{/each}}


<!-- creating vars -->
{{$myVar = 'a new var'}}
{{$myVar2 = menu|menus.0}}
<!-- or set in template -->
regve.render('index', {$: {myVar: 'a new var default'});

<!-- using vars -->
{{$myVar}} <!-- output: a new var -->
{{$myVar2}} <!-- output: menu (or) menus.0 (just like normal objects) -->

<!-- import another view -->
<!-- must be {{{3}}} to allow html -->
{{{#import header}}}
{{{#import page/header}}}
{{{#import some/file/path}}}


<!-- components work a lot like imports -->
{{{#module components/header var1:a var2: b str: "A String with spaces \"and\" escaped quotes" obj[ key:value item1: a item2: b item3: c ] }}}
  This module has content
{{{/module}}}

{{{#insert components/header desc: "a simple module without a body" body: "I can also define the body with a var if I want" test 'this \'test\' string uses single quotes'}}}


<!-- disable markdown -->
{{#no-markdown}}
  markdown will not run in here
{{/no-markdown}}

<!-- escape html -->
{{#no-html}}
  html should not run here
  Do Not rely on this for html security
  The purpose of this, is so an admin can display html without running it
{{/no-html}}

<!-- delete -->
{{#delete}}
  This text will be removed before rendering
  Do Not rely on this for security
  The purpose of this, is for the engine to remove the right content from if else statements in bulk
{{/delete}}


<!-- basic markdown support -->
<!-- note: markdown is not escaped in objects, but it is limited so your users can use it in comments -->
# h1
## h2
### h3
#### h4
##### h5
###### h6

--- = <hr>

<!-- auto clickable http and https links -->
https://example.com = <a href="https://example.com">https://example.com</a>
<!-- only runs if url is Not in "quotes" -->
<a href="https://example.com">example</a> = <a href="https://example.com">example</a>

`p tag` = <p>p tag</p>

pre tag also supported with 3 ` but this readme is written in markdown, so I can not display it.

*italic*
**bold**
***bold italic***
__underlined__
~~strike through~~

How to extract/move tags

//you can add any tag name. custom tags also work
//note: style tag also includes stylesheets. <link rel="stylesheet" href="/style.css">
app.engine('html', regve({extract: ['script', 'style', 'link', 'meta']}));
<!-- from: -->
<script src="script.js"></script>
<meta content="unwanted meta tag">
some other text
{{-script}}

<!-- to: -->
some other text
<script src="script.js"></script>

Cache views into memory

// this can help reduce the number of calls to fs.reaFile()
// note: the cache is also reset on server restart
// note: the cache value is case sensitive

app.engine('html', regve({cache: '2h'})); // 2 hours

// or if you have a small number of views that never change
app.engine('html', regve({cache: '1Y'})); // 1 Year

// or for a daily cache
app.engine('html', regve({cache: '1D'})); // 1 Day

// the cache only runs on production. to run the cache in development, set the cacheDev option
app.engine('html', regve({cache: '2m', cacheDev: true})); // 2 minutes (also runs in development)

run functions before and after render (express)

// in express, you can set a callback function, just before, or just after res.render() runs
// the data is the file data before or after rendered
// you can also return the data, with some modifications, to override the original data (must be type string)
// returning nothing will leave the result alone
// data will be returned as a buffer, so you may need to use data.toString() to modify it

regve({onBeforeRender: function(data){
  // this will run before res.render() runs
  return data;
}});

regve({onAfterRender: function(data){
  // this will run after res.render() runs
  return data;
}});

// you can also run these functions by setting options

res.render('index', {onBeforeRender: function(data){
  // this will run before res.render() runs
  return data;
}});

res.render('index', {onAfterRender: function(data){
  // this will run after res.render() runs
  return data;
}});

Other options

// to increase performance, you can globally skip some of the unused parts of the view engine
// you can also add these to res.render({/* options */});
app.engine('regve', regve({
  noImports: true,
  noEach: true,
  noMarkdown: true
}));

// by default, the view engine will remove any unused vars
// you can disable this feature by setting the keepInvalidVars option
app.engine('html', regve({keepInvalidVars: true}));

Creating your own template functions

let hasContent = true || false; // default = false
regve.addFunction('name', function(attrs, content, options){
  // attrs is an array of items added after the tag name, separated by spaces
  // example: {{#name attr1 attr2 attr3}}

  // content only exists if hasContent is true
  // if hasContent is true, than you will need to close the tag with {{/name}}
  // example: {{#name attrs}} content {{/name}}

  // options are the vars you set when adding the template
  // example: res.render('index', {/* options */});

  // when your done, you need to return the new content
  // if you return nothing, the content is simply removed
  return content;
}, hasContent);

Defining single type html elements

// this module automatically closes any open tags, but it defines the tags that should not close
// example: <div> should be closed with </div>, but <input> should Not be closed with </input>
// if you notice any tags that should Not close were missed, you can add them with this function
regve.defineSingleTagType('input');
regve.defineSingleTagType('img');
regve.defineSingleTagType('br');

Lazy Loading Pages

// lazy loading a page as the user scrolls down, is one of the more advanced options this view engine has to offer
// this option is disabled by default, and can be enabled per page render

// express example

const express = require('express');
const regve = require('regve');

const app = express();

// you may also need to set up a module to get post body, (or use optional get method)
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json({type: ['json']}));

app.engine('html', regve({/* global options */}));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'html');

app.post(function(req, res){
  // compatible with nonce script key (set with {{nonce}}) (even if random per click, will send original key in ajax call)
  // the nonce key (if set) is pulled from its own script tag to a temp 'let' variable when running an ajax call, to preserve the way google hides the key in console 'elements' on a script tag
  res.render('index', {lazyLoad: {tag: 'body', method: 'post', data: req.body.lazyLoadData}});
});

app.get(function(req, res){
  // method 'post' will use app.post when sending ajax request to server for next piece of the page
  // method 'get' is optional
  res.render('index', {lazyLoad: {tag: 'body', method: 'post'}});
});

app.get(function(req, res){
  // you can also set the 'lazyLoad.scrollElm' option and append to a different tag than you scrolled to
  // this is useful if you want to have a footer scroll with the content, and stay at the bottom when new content is added
  // 'scrollElm' can be a class, id, or tag name
  res.render('index', {lazyLoad: {tag: 'main', scrollElm: 'body'}});
});

app.get(function(req, res){
  // by default, the lazyLoad option will run before any variables are added or functions are run, allowing unused parts of the lazy loaded template to be removed before rendering
  // you can disable this if needed, by setting the 'afterVars' option to true
  // setting this can be useful if you have a variable that adds the {{#lazyload}} tag dynamically
  res.render('index', {lazyLoad: {afterVars: true}});
});

// if you only have specific vars that need to load early, you can set the 'earlyVars' option
// note: 'earlyVars' will only run on basic html var objects. This will skip attributes and escaped vars
res.render('index', {lazyLoad: {earlyVars: ['myContent', 'myScripts', 'myStyles.mainStyle']}});

// data parameter is required for the method that ajax requests to, so it can get the next piece of the page

inside the template, and the tag you choose, use {{#lazyload}} to separate lazy loaded instances

<!-- recommended, but Not dependent (optional) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<!-- if jQuery is not defined, XMLHttpRequest method will be used as a fallback -->

<body>
  <header>The Header</header>
  <main>
    <h2>Hello, World!</h2>
    <br>
    <div style="height: 120vh;">
      lazy load 1
    </div>
  {{#lazyload}}
    <div style="height: 120vh;">
      lazy load 2
    </div>
  {{#lazyload}}
    <div style="height: 120vh;">
      lazy load 3
    </div>
  {{#lazyload}}
    <div style="height: 120vh;">
      lazy load 4
    </div>
  {{#lazyload}}
    <div style="height: 120vh;">
      lazy load 5
    </div>
  ####No More Info To LazyLoad
  </main>
  <footer>The Footer</footer>
</body>

Lazy Load Event Listener

// client side javascript, there is a custom event listener you can use
// this event listener is triggered every time a new page (new content) is lazy loaded
document.addEventListener('onPageLazyLoad', function(e){
  // getPage() returns the numbered section loaded, based on separation between {{#lazyload}} tag occurrences
  console.log('page', e.detail.getPage());
});

Auto Ad Insert

// you can easily and automatically place your ads into your website
// this method will place ads by page distance, running on client side, and updating with scrolling for lazy load compatibility
// note: autoAdInsert 'tag' and 'scrollElm' are similar to lazyLoad 'tag' and 'scrollElm'
res.render('index', {autoAdInsert: {tag: 'body', scrollElm: 'window', distance: '120%', topPadding: 1, content: '<h3>My Ad</h3>'}});

// autoAdInsert 'distance' can be a percentage of the window height, or an absolute number
res.render('index', {autoAdInsert: {distance: '20%'}});
res.render('index', {autoAdInsert: {distance: 320}});

// you can embed html
res.render('index', {autoAdInsert: {content: '<iframe src="/my/ad/url"></iframe>'}});
// or run a script on insert
let onAdInsertScript = `
e.detail.insertAd('<iframe src="/my/ad/url?adNumber='+e.detail.getAdNumber()+'"></iframe>');
`;
res.render('index', {autoAdInsert: {onInsert: onAdInsertScript}});
// you can also add a client side event listener
document.addEventListener('onAdInsert', e => {
    e.detail.insertAd('<iframe src="/my/ad/url?adNumber='+e.detail.getAdNumber()+'"></iframe>');
});

// autoAdInsert 'topPadding' is used to decide how far down the page, before the first ad shows
// a 'topPadding' greater than 1 will try to wait for a specific scroll height before inserting the first ad
res.render('index', {autoAdInsert: {topPadding: 10}});
// a 'topPadding' of 0 will put the first ad at the top of the page
res.render('index', {autoAdInsert: {topPadding: 0}});

// when ads are placed, they go between each child element inside the 'tag' option you specify
// by default, the ads avoid going inside the child elements
// you can also scan the child elements content by setting the 'includeInnerChildren' option to true
res.render('index', {autoAdInsert: {includeInnerChildren: true}});