htmlincluder plugin for gulp
This allows you to insert a special markup into your files to do a number of things:
- insert files into other files
- wrap a file with another file
- clip off certain parts of a file
- inject data for use in adding text to files
- basic control flow (if and each) to use with data
This was written with the intention of working on static websites while still allowing for a component based development. It also was intended to let the individual components be testable by themselves by allowing the individual component files to include portions that would be removed on build.
It also can allow the inclusion of some dynamic content into these static sites, as long as you can wrangle said content into a json object and pass it in when you are building.
I'm sure this can be put to a much broader use than originally intended (and may do for a name change at some point, since there is no reason you couldn't include json files into json files, or do other things.)
Certain features use eval
. I have flagged these features below, but please use caution when using them so that you don't have any untrusted data that isn't unsanitized going into the system. It could lead to compromising the build environment or XSS attacks in the resulting html.
npm install --save-dev gulp gulp-htmlincluder
gulpfile.js
(Using gulp 4):
const { src, dest } = require('gulp');
const includer = require('gulp-htmlincluder');
// A simple method for making basic api requests.
const http = require('http');
const getApiData = url => new Promise((resolve, reject) => {
http.get(url, resp => {
let data = '';
resp.on('data', chunk => data += chunk)
resp.on('end', () => resolve(JSON.parse(data)))
})
})
exports.includer = function(path) {
// optional use of json data. options object is not required
const jsonInput = require('./input-file.json');
const rawJsonPlugins = { getApiData };
src('./src/**/*.html')
.pipe(includer({ jsonInput, rawJsonPlugins }))
.pipe(dest('./dist/'))
}
you can then run this with gulp includer
gulpfile.js
(using earlier versions of gulp):
var gulp = require('gulp'),
includer = require('gulp-htmlincluder');
gulp.task('htmlIncluder', function() {
// optional use of json data. options object is not required
var json = require('./input-file.json');
var options = {
jsonInput : json,
};
gulp.src('./src/**/*.html')
.pipe(includer(options))
.pipe(gulp.dest('./dist/'));
});
gulp.task('default', ['htmlIncluder']);
gulp.task('watch', function() {
gulp.watch(['files/*.html'], function(event) {
gulp.start('default');
});
});
htmlincluder requires files follow a particular naming convention.
Files that you want to include in other files begin with -
.
Files that you want to wrap around other files begin with _
.
Files that you want to use to build the resulting static pages can be named however you want, as long as they don't begin with -
or _
.
Right now this is necessary because the files that will ultimately exist in the build folder are those that don't start with -
or _
. I'd love to change this to determine this in another manner (for example, only files that aren't included are finally built).
It is probably best to learn from examples. These can be found in the ./test/html
folder of this repository. These can be run by cloning this repository, running npm install
and then npx gulp
. Check the gulpfile.js
for specifics if you just want to try building single files.
Basic tag structure
<!--#tagName attributeName="attributeValue" -->
Note: *
means required attribute
- Wrapping a file with another file
<!--#wrap path="*" jsonPath="" rawJson="" -->
<!--#middle -->
<-- content wrapped replaces themiddle
tag<!--#endwrap -->
- Inserting a file
<!--#insert path="*" jsonPath="" rawJson="" -->
- Inserting Json (passed in from parent component)
<!--#data jsonPath="*" default="" rawJson="" -->
- Inserting Json (from data input, or default value)
<!--#jsonInsert jsonPath="*" default="" -->
- Control Flow
- If
<!--#if jsonPath="*" rawJson="" -->
<!--#endif -->
- Each (repeater)
<!--#each count="*" jsonPath="" rawJson="" -->
<!--#endeach -->
- If
- Clipping (Ignores parts of file)
<!--#clipbefore -->
<!--#clipafter -->
<!--#clipbetween -->
<!--#endclipbetween -->
A relative (to current file) path to a file.
Example:
src
|- components
| |- -header.html
|- index.html
in index.html
the path
for -header.html
is:
<!--#tagName path="./components/-header.html" -->
A path to data in a json object.
Example:
{
a: {
b: {
c: 'hello world'
}
}
}
The path to c
is:
<!--#tagName jsonPath="a.b.c" -->
A string that, when converted to JavaScript and executed, produces a json object. You can access this data using your jsonPath attribute.
For example:
rawJson="{ sample : 'this is some data to use!' }"
You can pass in a self-invoking function you can invoke this function with the arguments json
and/or plugins
.
rawJson="((json, {}) => ({ title: 'generated object?'}))(json, plugins)"
json
represents the json object that you may pass in to the options for htmlincluder when invoking it in your gulp task. plugins
It would even be possible to use an async function to load data from a service or CMS:
rawJson="(async ({ fetch }) => { const response = await fetch('https://cms.com/api'); return response.json(); })(plugins)"
Warning
Internally eval
is used so beware. But also, you could technically write a script that generates an object if you need more complicated data.
Of course, if you're loading data from an api it could be compromised. If it contains something like:
<script>console.log('gotcha')</script>
You may have compromised your users. So beware!
On an #each you can tell it how many times to repeat the inner content with a count attribute
<!--#each count="3" -->hello world<!--#endeach -->
This is the simplest use case. Simply put the following html comment
<!--#insert path="<relative-path-to-file.ext>" jsonPath="" rawJson="" -->
file1.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<!--#insert path="-file2.html" -->
</body>
</html>
-file2.html
hello world
Results:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
hello world
</body>
</html>
If you want to use ssi includes along with this, and so have the insert string follow that format there is an argument to pass into the htmlincluder in gulp.
Thanks to theSLY for suggesting this!
var gulp = require('gulp'),
includer = require('gulp-htmlincluder');
gulp.task('htmlIncluder', function() {
var options = {
insertPattern: 'include virtual',
};
gulp.src('files/*.html')
//now looks for <!--#include virtual, instead of <!--#insert
.pipe(includer(options))
.pipe(gulp.dest('dist/'));
});
gulp.task('default', ['htmlIncluder']);
gulp.task('watch', function() {
gulp.watch(['files/*.html'], function(event) {
gulp.start('default');
});
});
<!--#wrap path="<relative-path-to-file.ext>" jsonPath="" rawJson="" -->
AND
<!--#endwrap -->
The middle tag must be placed in the wrap file so we know where to put the middle part of the other file
<!--#middle -->
file1.html
<!--#wrap path="_file2.html" -->
hello world
<!--#endwrap -->
_file2.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<!--#middle -->
</body>
</html>
Results:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
hello world
</body>
</html>
Inside of wrap and insert tags that you have used the jsonPath
attribute on you can use use this tag to print data that has been passed down.
<!--#data jsonPath="<path.to.json>" default="some text" rawJson="" -->
file1.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<!-- JSON path passed down from above -->
<!--#insert path="-insert.html" jsonPath="hello" -->
</body>
</html>
-insert.html
<div>
<!--#data jsonPath="world" -->
</div>
file.json
{
"hello" : {
"world" : "hello world"
}
}
Results
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div>
hello world
</div>
</body>
</html>
When using a json data file, you can pull data from it directly using this tag.
<!--#jsonInsert jsonPath="<path.to.json>" rawJson="" -->
file1.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<!--#jsonInsert jsonPath="hello.world" -->
</body>
</html>
file.json
{
"hello" : {
"world" : "hello world"
}
}
Results
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
hello world
</body>
</html>
Instead of using the data file, you can provide a raw json object
<!--#jsonInsert jsonPath="path.to.json" rawJson="{ path: { to: { json: "hello world" } } }" -->
Results
hello world
Each can be used to iterate over a json array, or just to repeat some data a number of times.
<!--#each count="5" -->
<div>hello world</div>
<!--#endeach -->
Results:
<div>hello world</div>
<div>hello world</div>
<div>hello world</div>
<div>hello world</div>
<div>hello world</div>
<!--#each count="5" -->
<!--#insert path="./-insert-hello-world.html" -->
<!--#endeach -->
Results:
<div>hello world</div>
<div>hello world</div>
<div>hello world</div>
<div>hello world</div>
<div>hello world</div>
<!--#each jsonPath="data" rawJson="{ data : [1, 2, 3, 4] }" -->
<div><!--#data --></div>
<!--#endeach -->
Results:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<!--#each rawJson="[1, 2, 3, 4]" -->
<div><!--#data jsonPath="this" --></div>
<!--#endeach -->
Results:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<!--#each jsonPath="this" rawJson="[1, 2, 3, 4]" count="2" -->
<div><!--#data jsonPath="this" --></div>
<!--#endeach -->
Results:
<div>1</div>
<div>2</div>
note: use single quotes in json objects, which means you'll need to escape
<!--#each jsonPath="this" rawJson="[
{ name: 'Tom', age: 33, bio: 'Bio for Tom' },
{ name: 'Dick', age: 55, bio: 'Dick\'s bio' },
{ name: 'Jane', age: 28, bio: 'Bio for Jane' },
{ name: 'Harry', age: 27 },
]" -->
<div>name: <!--#data jsonPath="name" --></div>
<div>age: <!--#data jsonPath="age" --></div>
<div>bio: <!--#data jsonPath="bio" default="nothing is known about them" --></div>
<!--#endeach -->
Results:
<div>name: Tom</div>
<div>age: 33</div>
<div>bio: Bio for Tom</div>
<div>name: Dick</div>
<div>age: 55</div>
<div>bio: Dick's bio</div>
<div>name: Jane</div>
<div>age: 28</div>
<div>bio: Bio for Jane</div>
<div>name: Harry</div>
<div>age: 27</div>
<div>bio: nothing is known about them</div>
<!--#clipbefore -->
<!--#clipafter -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<!--#clipbefore -->
blah
<!--#clipafter -->
</body>
</html>
Results:
blah
<!--#clipbetween -->
<!--#endclipbetween -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
something
<!--#clipbetween -->
a widget!
<!--#endclipbetween -->
something else
</body>
</html>
Results:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
something
something else
</body>
</html>
Look in ./test/html
- Build out more tests - consider more automated versions of them
- Implement some form of caching so only changed files need be processed