🇺🇸 English | 🇷🇺 Русский язык
Gulp plugin for ejs with steroids. The project is inspired by ejs-locals
- Thanks
- Why this plugin was created?
- Example of using the plugin
- gulpEjsMonster()
- locals API
- Project Info
First of all, we want to express our gratitude to the people who led us to use the template engine ejs
and create on its basis gulp-ejs-monster
:
- Matthew Eernisse (mde) - for creating and supporting
ejs
(http://ejs.co), and the community ofejs
, which helps him in this - Tom Carden (RandomEtc) - for creating a project
ejs-locals
, from which we took the idea of realizationgulp-ejs-monster
- Ryan Zimmerman (RyanZim) -
EJS-Lint
- Ariya Hidayat (ariya) -
jquery/esprima
- Corey Hart (codenothing) -
jsonlint
ejs
(http://ejs.co) - is a universal template engine that allows you to create any markup of any complexity. The better your knowledge of JavaScript - the more opportunities you have with ejs
.
There are already many other plugins for ejs
. But we also decided to create own, as an add-on to the ejs
+ pumping it with a small set of "steroids" ))).
Also, the main focus of the plugin gulp-ejs-monster
- was made on optimization and rendering speed.
By default, ejs
uses the JavaScript construction with (expression)
to add scope - this gives its advantages for working with the template engine, but has its own price - the speed of searching for variables increases - which affects the rendering speed of the pages. This is especially noticeable on large projects.
Therefore gulp-ejs-monster
forcibly turns off the native ejs
parameters in order to work in strict mode. That gives a significant gain to the rendering speed.
List of constant values from gulp-ejs-monster
for ejs
:
{
"strict": true,
"_with": false,
"debug": false,
"rmWhitespace": false,
"client": false
}
This approach also has its price - now only one global object is available for you, without any "proxy" properties (which the with
design used to imitate).
If this approach to working with template engine
ejs
does not suit you, you may no read further and do not create PRs, since we do not intend to change it)))
npm i --save-dev gulp-ejs-monster
# or yarn cli
yarn add --dev gulp-ejs-monster
const gulp = require('gulp');
const gulpEjsMonster = require('gulp-ejs-monster');
gulp.task('ejs', function() {
return gulp.src('./src/ejs/*.ejs')
.pipe(gulpEjsMonster({/* plugin options */}))
.pipe(gulp.dest('./dist/'));
});
Example of a project structure
ejs/
layouts/
base.ejs
widgets/
news-list.ejs
includes/
critical.css
requires/
news-list.json
index.ejs
news.ejs
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><%- locals.blocks.title %></title>
<style><%- locals.include('includes/critical.css') %></style>
</head>
<body>
<%- locals.blocks.header %>
<%- locals.body %>
</body>
</html>
<% locals.setLayout('layouts/base.ejs') -%>
<% locals.block('title', 'Index view') -%>
<h1>Index view</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
<hr>
<% locals.setLayout('layouts/base.ejs') -%>
<% locals.block('title', 'Last News') -%>
<% let newsList = locals.require('requires/news-list.json') -%>
<h1>Last News</h1>
<%- locals.widget('widgets/news-list.ejs', {list: newsList}) %>
<hr>
[
{
"title": "News title 1",
"description": "Lorem ipsum dolor sit ....",
"href": "news-page.html"
}, {
"title": "News title 2",
"description": "Lorem ipsum dolor sit ....",
"href": "news-page.html"
}, {
"title": "News title 3",
"description": "Lorem ipsum dolor sit ....",
"href": "news-page.html"
}
]
html{font-family:sans-serif}
body{margin:0}
h1{color:red}
<%
let {
list = []
} = locals.entry;
if (!list.length) {
return 'No news yet :((';
}
-%>
<ul class="news-list">
<% list.forEach(item => { -%>
<li class="news-list__item">
<div class="news-item">
<div class="news-item__title"><%- item.title %></div>
<div class="news-item__description">
<p><%- item.description %></p>
<p><a href="<%- item.href %>">Read more</a></p>
</div>
</div>
</li>
<% }); -%>
</ul>
Plugin name.
The method which, on error, calls the end
event to prevent the current process gulp
from falling out of the task.
const gulp = require('gulp');
const gulpEjsMonster = require('gulp-ejs-monster');
gulp.task('ejs', function() {
return gulp.src('./src/ejs/*.ejs')
.pipe(gulpEjsMonster({/* plugin options */}).on('error', gulpEjsMonster.preventCrash))
.pipe(gulp.dest('./dist/'));
});
A little bit of advice - in order to speed up the processing and preparation of the plugin's parameters - use the created object with saving to a variable, which you can then specify when you call.
In this case, by storing references to an external object, the parameters will not be re-prepared. And also it is possible to save the received data (in the object locals
) from the previous page of the render to the next.
const gulp = require('gulp');
const gulpEjsMonster = require('gulp-ejs-monster');
const options = {/* plugin options */}; // save as variable
gulp.task('ejs', function() {
return gulp.src('./src/ejs/*.ejs')
.pipe(gulpEjsMonster(options).on('error', gulpEjsMonster.preventCrash))
.pipe(gulp.dest('./dist/'));
});
Then you can see a list of all available options.
data type string
|
default process.cwd()
The relative path from the current working directory to the directory with layouts.
data type string
|
default process.cwd()
The relative path from the current working directory to the directory with widgets.
data type string
|
default process.cwd()
Relative path from the current working directory to the directory with js/json files, which you can connect as executable files, using the CommonJS export modules.
data type string
|
default process.cwd()
The relative path from the current working directory to the directory c with any text files from which you can connect the content as it is.
data type string
|
default '.html'
Extend the resulting render files.
It is allowed not to specify. (dot) at the beginning of the value, example 'php' => '.php'
data type string
|
default '%'
|
допустимые значения ['%', '&', '$', '?']
Symbol for use with angle brackets for opening / closing.
If the specified property does not match the valid value, the value default will be set!
data type string
|
default 'locals'
The name that will be used for the object that holds the local variables. You can replace this value with your own and later use it inside the template.
The corresponding value must have a valid JavaScript variable name!
data type Object
|
default {}
Sending your own values to an object that stores local variables that will be available to you inside the template in the locals
object (or under the name you could specify in the localsName
property)
It is important to know that the plug-in already has a certain set of properties and methods that will be added to this object. So that there are no conflicts and overwriting - check out the locals API.
data type boolean
|
default false
When disabled, debugging tools are not compiled, which allows you to speed up the render process a little. The specified value will be converted to Boolean.
It is important to know that if an error occurs, the plugin will not be able to give an explanation of the failure if the value is false
. Therefore, in case of an error, the plug-in will render the current page render again with the parameter enabled, in order to find out what went wrong and to output the maximum report on the errors found.
If you use the watch task for the render - after correcting the error, the parameter will again have the same value.
In very specific situations, the re-renderer may not correctly detect errors,
because of a repeated pass, in which, for example, some value can be overridden, and so on.
If this happens - run the task immediately with the enabled parametercompileDebug
data type boolean
|
default false
Displays the render history after completing work with each page.
data type boolean
|
default false
Displays the render history on error.
data type function
|
default undefined
Name | Type | Description |
---|---|---|
markup |
string |
Markup inside the structure |
Your own escaping function used with the <% =
construct, which must return a string.
data type function
|
default undefined
The method that will be called after the page renderer with its layouts.
Name | Type | Description |
---|---|---|
markup |
string |
Final page markup |
file |
Object |
Current renderer file |
sources |
Array.<string> |
The paths of all the connected files during the rendering of the current file, including the path to the current page (the first one in the list) |
Using the afterRender
method, you can change the markup, for example, format with js-beautify
and return a new result using return
or use the method to set watches on dependent files for each page separately.
const gulp = require('gulp');
const gulpEjsMonster = require('gulp-ejs-monster');
const jsBeautify = require('js-beautify').html;
const options = {
afterRender (markup) {
return jsBeautify.html(markup, /* jsBeautify options */);
}
};
gulp.task('ejs', function() {
return gulp.src('./src/ejs/*.ejs')
.pipe(gulpEjsMonster(options).on('error', gulpEjsMonster.preventCrash))
.pipe(gulp.dest('./dist/'));
});
const gulp = require('gulp');
const gulpEjsMonster = require('gulp-ejs-monster');
const gulpWatchAndTouch = require('gulp-watch-and-touch');
const ejsFileWatcher = gulpWatchAndTouch(gulp);
const watchTask = true;
const options = {
afterRender (markup, file, sources) {
if (watchTask) {
let filePath = sources.shift(); // remove path of current view
let newImports = ejsFileWatcher(filePath, filePath, sources);
if (newImports) {
console.log(`${file.stem} has new imports`);
}
}
}
};
gulp.task('ejs', function() {
return gulp.src('./src/ejs/*.ejs')
.pipe(gulpEjsMonster(options).on('error', gulpEjsMonster.preventCrash))
.pipe(gulp.dest('./dist/'));
});
gulp.task('ejs-watch', function() {
gulp.watch('./src/ejs/*.ejs', gulp.series('ejs')); // gulp#4.x
});
We also emphasized the output of the maximum error reports that can occur when rendering pages so you can understand what went wrong.
If you fail, you will receive a report group:
The rendering history of the current page, with the help of which you can track the sequence of plug-in actions
Note! since 3.1.0
The history is displayed only when the showHistoryOnCrash parameter is turned on
Render history:
Start
render view - C:\Wezom\NodeModules\gulp-ejs-monster\examples\src\index.ejs
> set layout - C:\Wezom\NodeModules\gulp-ejs-monster\examples\src\_layouts\base.ejs
> render widget - C:\Wezom\NodeModules\gulp-ejs-monster\examples\src\_widgets\demo.ejs
caching new file content
√ file changed
! render file content
> render widget - C:\Wezom\NodeModules\gulp-ejs-monster\examples\src\_widgets\demo.ejs
getting file content from cache
! file not changed
! render file content
> require node module "lodash"
caching new file content
> require file - C:\Wezom\NodeModules\gulp-ejs-monster\examples\src\_requires\data.js
√ file changed
caching new file content
> require file - C:\Wezom\NodeModules\gulp-ejs-monster\examples\src\_requires\component.js
→ CRASH...
The native ejs
report on the error found
The availability of the following reports will depend on the error itself and the file in which it occurred
If the file you are looking for was not found.
If the error is in the * .ejs
file - it will lint file by EJS-Lint
, to detect possible errors.
If the error is in the * .js
file - it will test file by esprima
, to detect possible errors.
If the error is in the * .json
file - it will lint file by json-lint
, to detect possible errors.
locals
- is a single global object that contains local values, which will be available inside ejs
.
data type string
Content of the current page, for insertion inside the layouts.
Accordingly, the property is available only within the layouts!
<!-- view index.ejs -->
<% locals.setLayout('base.ejs') %>
<h1>Index view</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
<!-- layout base.ejs -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<%- locals.body %>
</body>
</html>
data type Object
List of assembled blocks that are created using the method locals.block()
<!-- view index.ejs -->
<% locals.setLayout('base.ejs') %>
<% locals.block('title', 'Index view') %>
<% locals.block('header', '<h1>Index view header</h1>') %>
<!-- view news.ejs -->
<% locals.setLayout('base.ejs') %>
<% locals.block('title', 'Last News') %>
<% locals.block('header', '<h1>News view header</h1>') %>
<!-- layout base.ejs -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><%- locals.blocks.title %></title>
</head>
<body>
<%- locals.blocks.header %>
<%- locals.body %>
</body>
</html>
data type Object
Incoming properties for the current widget.
Accordingly, the property is available only inside the widgets!
<!-- view index.ejs -->
<%- locals.widget('test.ejs') %>
<%- locals.widget('test.ejs', {title: 'Custom title'}) %>
<!-- widget test.ejs -->
<% let title = locals.entry.title || 'Default title'; %>
<h1><%- title) %></h1>
<!-- widget test.ejs -->
<% let {title = 'Default title'} = locals.entry; %>
<h1><%- title) %></h1>
data type string
The name of the current rendering page (without the extension).
Regardless of the current widget, the include, layout and so on.
data type string
Absolute path to the current render page in your file system.
Regardless of the current widget, the include, layout and so on.
data type boolean
Flag, whether the file changed after the last access to it.
The property is available inside the widgets. On the main render pages and their layouts, the property is also available, but for them it will always be true
.
Sets the path to the layout for the current page.
Name | Type | Description |
---|---|---|
filePath |
string |
The path to the file (with the extension) relative to the directory specified in the parameterlayouts |
Connecting the markup widget.
Name | Type | Attributes | Default | Description |
---|---|---|---|---|
filePath |
string |
The path to the file (with the extension) relative to the directory specified in the parameter widgets | ||
relativeFolderPath |
string |
<optional> | The relative path from which to connect the specified file, ignoring the widgets, If the parameter is not equal to a string, then it is perceived as entry |
|
entry |
Object |
<optional> | {} |
Incoming data that is passed to the widget, If the parameter relativeFolderPath is not equal to a string and the third parameter is equal to the logical value, then it is perceived as cacheRenderResult |
cacheRenderResult |
boolean |
<optional> | false |
Cache the result of the renderer. |
- data type:
string
- description: rendering ejs markup
Inside the widget, you can accept incoming data from the locals.entry
.
Caching the result of the renderer will allow you to store the received string as ready static markup and insert it on subsequent calls on the page without compilation. To do this in the next widgets, you must also specify cacheRenderResult
. Otherwise, the render will be performed again for the current call.
If you change the file of the widget itself (change the modification date), the cache will be reset.
This approach can also be used for several pages in the overall rendering task.
For example - the first page, index.ejs
, render the block of code, a news.ejs
, which goes after, will already take the cached result.
<!-- cache at first render -->
<%- locals.widget('big-rendering-markup.ejs', {/*data*/}, true) %>
<!-- get cached render result from first render -->
<%- locals.widget('big-rendering-markup.ejs', {/*data*/}, true) %>
<!-- new render result -->
<%- locals.widget('big-rendering-markup.ejs', {/*data*/}) %>
<!-- get cached render result from first render -->
<%- locals.widget('big-rendering-markup.ejs', {/*data*/}, true) %>
Connecting modules from installed node_modules
Name | Type | Description |
---|---|---|
moduleName |
string |
Module name |
- data type:
*
- description: Connected module
<%
let lodash = locals.requireNodeModule('lodash');
lodash.cloneDeep(options);
lodash.isPlainObject(data);
%>
Connect your own executable js/json files with CommonJS support for export.
Name | Type | Attributes | Default | Description |
---|---|---|---|---|
filePath |
string |
The path to the file (with the extension) relative to the directory specified in the parameter requires | ||
relativeFolderPath |
string |
<optional> | The relative path from which to connect the specified file, ignoring the requires |
- data type:
*
- description: Connected file
Inside such files, the locals
object is not available. You can transfer it to the file, for example, if you export a method:
Variant 1. Bind the context for the method
<%
let component = locals.require('component.js').bind(locals);
component('Hello');
%>
// component.js
function component (message) {
console.log(this);
console.log(message);
// require another components and files
let anotherComponent = this.require('another-component.js').bind(this);
let data = this.require('config.json');
// ...
}
module.exports = component;
Variant 2. Use currying
<%
let component = locals.require('component.js')(locals);
component('Hello');
%>
// component.js
// Function wrapper
function componentWrapper (locals) {
// component
function component (message) {
console.log(locals);
console.log(message);
// require another components and files
let anotherComponent = locals.require('another-component.js')(locals);
let data = this.require('config.json');
// ...
}
return component;
}
module.exports = componentWrapper;
Includes the text content of the file in your markup as is.
Name | Type | Attributes | Default | Description |
---|---|---|---|---|
filePath |
string |
The path to the file (with the extension) relative to the directory specified in the parameter includes | ||
relativeFolderPath |
string |
<optional> | The relative path from which to connect the specified file, ignoring the includes |
- data type:
Object
- description: The object has a set of properties
changed
- flag, if the file is changed.mtime
- The date of the last modification of the filecontent
- the content of the filetoString()
- own method of casting to a string that returnsthis.content
, so if you execute the method in the context of the insertion in the markup - the result will immediately be the content of the file.
<!-- include css file -->
<style><%- locals.include('critical.css') %></style>
// requires/components/md2html.js
function createMd2HtmlComponent (locals) {
const marked = locals.requireNodeModule('marked');
const lodash = locals.requireNodeModule('lodash');
const defaultOptions = {
render: false,
gfm: true,
tables: true,
breaks: true,
pedantic: false,
sanitize: false,
smartLists: true,
smartypants: true
};
/**
* Convert md 2 html
* @param {string} filePath
* @param {Object} [options={}]
* @returns {string} converted html markup
*/
function md2html (filePath, options = {}) {
let mdFile = locals.include(filePath);
if (mdFile.changed) {
let markedOptions = lodash.merge({}, defaultOptions, options);
// rewrite cached file content until it not changed
mdFile.content = marked(mdFile.content, markedOptions);
}
return mdFile;
}
return md2html;
}
module.exports = createMd2HtmlComponent;
// requires/extend-locals.js
function extendLocals (locals) {
if (!locals.hasOwnProperty('com')) {
locals.com = {};
}
locals.com.md2html = locals.com.md2html || locals.require('components/md2html.js')(locals);
// set other components inside render
// ...
}
module.exports = extendLocals;
# icludes/about-us.md
[Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet)
# H1
## H2
### H3
#### H4
##### H5
###### H6
Alternatively, for H1 and H2, an underline-ish style:
Alt-H1
======
Alt-H2
------
<!-- view index.ejs -->
<% locals.setLayout('base.ejs'); -%>
<% locals.require('extend-locals.js')(locals); -%>
<div class="container">
<article class="wysiwyg">
<%- locals.com.md2html('about-us.md') %>
</article>
</div>
Specify the markup block that will be available in the block list.
Name | Type | Attributes | Default | Description |
---|---|---|---|---|
blockName |
string |
The name of the block that can be accessed in the block list | ||
markup |
string |
Value of the block | ||
mtd |
string |
<optional> | 'replace' |
Method for specifying a value for a block. |
- data type:
string
- description: Value of the block
When specifying a value for a block, an array is formed, which, when printed, is joined to a string.
Methods for specifying a value for a block:
'replace'
- replace the previous value if it was. If not then just assign a new value.'append'
- add a new value to the end of the array.'prepend'
- add a new value to the beginning of the array.
<% locals.block(headers, '<h2>Ipsum</h2>') %>
...
<% locals.block(headers, '<h3>Dolor</h3>', 'append') %>
...
<% locals.block(headers, '<h1>Lorem</h1>', 'prepend') %>
...
<%- locals.blocks.header %> // => ['<h1>Lorem</h1>', '<h2>Ipsum</h2>', '<h3>Dolor</h3>'].join('\n');