Reshape
:diamond_shape_with_a_dot_inside: transform html with javascript plugins
Install / Use
/learn @reshape/ReshapeREADME
Reshape is a tool for transforming HTML with JavaScript plugins. Reshape parses input HTML into an abstract syntax tree (AST). Plugins receive the AST, can transform it as they wish, and return it to be passed to the next plugin. When all plugins have finished, reshape transforms the AST into a JavaScript function which, when called, will produce a string of HTML.
Table of Contents
- Installation
- Basic Example
- Configuration Options
- The Reshape AST
- Writing Plugins
- Plugin Directory
- License, Contributing, Etc
Installation
Reshape can be installed through npm, and requires node v6 or higher.
npm i reshape --save
Reshape is a javascript library -- if you are looking for a CLI interface, check out reshape-cli!
Usage
Initialize reshape with some plugins you'd like to use and any other options, then call process with the HTML you'd like to process. For example:
const reshape = require('reshape')
const expressions = require('reshape-expressions')
const include = require('reshape-include')
let html = `
<section>
<h1>Reshape is so cool!</h1>
<p>Hello, {{ planet}}!</p>
<include src='_partial.html' />
</section>`
reshape({ plugins: [expressions(), include()] })
.process(html)
.then((result) => {
console.log(result.output({ planet: 'world' }))
// <section>
// <h1>Reshape is so cool!</h1>
// <p>Hello, world!</p>
// <p>Hello from a partial!</p>
// </section>
})
Reshape generates a JavaScript template as its output, which can be called (with optional locals) to produce a string. This means that reshape can generate static HTML as well as JavaScript templates for the front-end.
Options
None of the options are required, any of them may be skipped.
| Option | Description | Default |
| ------ | ----------- | ------- |
| plugins | Either a single plugin or an array of plugins to be used | []
| parser | Override the default parser | parser
| generator | Override the default code generator | code-gen
| parserOptions | Options to be passed to the parser |
| generatorOptions | Options to be passed to the code generator |
| runtime | A place to store functions executed at runtime | {}
| filename | Name of the file being processed, for debugging. |
A quick example, using sugarml, a jade-like, whitespace-based custom parser:
const reshape = require('reshape')
const sugarml = require('sugarml')
const html = `
#main
p hello world!
`
reshape({ parser: sugarml })
.process(html)
.then((result) => {
console.log(result.output())
// <div id='main'><p>hello world!</p></div>
})
Options can also be passed either to the reshape constructor as above, or to the process method. Options passed to reshape will persist between compiles, where options passed to process will only apply for that particular compile. Options passed to process will be deep-merged with existing options and take priority if there is a conflict. For example:
const ph = reshape({ plugins: [example(), anotherExample()] })
ph.process(someHtml, { filename: 'foo.html'})
ph.process(otherHtml, { filename: 'bar.html', plugins: [alternatePlugin()] })
ph.process(evenMoreHtml, { parser: someParser })
Here, the default plugins applied to ph at the top will apply to all compiles, except for the second, in which we override them locally. All other options will be merged in and applied only to their individual compiles.
Reshape AST
Plugins act on an abstract syntax tree which represents the HTML structure, but is easier to search and modify than plain text. It is a very simple recursive tree structure. Each node in the tree is represented by an object, which is required to have a type property. The default code generator supports three data types:
Text
A string of plain text. The content property contains the string.
{
type: 'text',
content: 'hello world!',
location: { line: 1, col: 1, startOffset: 1, endOffset: 12 }
}
Doctype
A document type declaration. The content property contains the full contents, same as a text node, but there is no entity escaping for a doctype node.
{
type: 'doctype',
content: '<!doctype html>',
location: { line: 1, col: 1, startOffset: 1, endOffset: 15 }
}
Tag
An HTML tag. Must have a name property with the tag name. Can optionally have an attrs property, which is an object with the key being a string, and the value being either a string or code type, or an array of multiple. Can also optionally have a content property, which can contain a full AST.
{
type: 'tag',
name: 'p',
attrs: {
class: [{ type: 'text', content: 'test', line: 1, col: 5 }],
'data-foo': [{ type: 'text', content: 'bar', line: 1, col: 18 }],
},
content: [/* full ast */],
location: { line: 1, col: 1, startOffset: 1, startInnerOffset: 31, endInnerOffset: 31, endOffset: 35 }
}
Code
A piece of code to be evaluated at runtime. Code can access any locals that the user has passed in to the function through the locals argument, and any runtime functions through the runtime object, which should be available in any scope that a template function is executed in. The name of the runtime object is configurable and can be accessed via this.options.runtimeName within any plugin. The code itself should be in the content attribute of the code node.
{
type: 'code',
content: 'locals.foo',
location: { line: 1, col: 1 }
}
Sometimes there's a situation where you want code to surround some HTML, in order to control or change its appearance, for example a conditional statement. When this is the case, a special helper can be used within your code so that you can avoid needing to manually run the code generator over the contained nodes. A quick example:
{
type: 'code',
content: `if (locals.show) {
__nodes[0]
} else {
__nodes[1]
}`,
nodes: [
{ type: 'text', content: 'shown!', location: { line: 1, col: 1 } },
{ type: 'text', content: 'hidden!', location: { line: 2, col: 1 } }
]
}
In this case, the code generator will parse the nodes in the nodes property and inject them at the appropriate locations in your code block. Nodes in the nodes property can be full ASTs, and even include more code nodes. Note that the nodes property is represented inside your code's content as __nodes to prevent any potential name conflicts.
Code should be expected to run in any JavaScript environment, from node to the browser, and in any version. As such, care should be taken to make code snippets as simple and widely-compatible as possible.
Additionally, all tree nodes should include information about their source, so that errors are clear, and source maps can be accurate. Each tree node must also have two additional properties under the location property:
line: the line in the original sourcecol: the column in the original sourcestartOffset: the zero-based first character index in the original sourceendOffset: the zero-based last character index in the original sourcestartInnerOffset: the zero-based first character index within an element in the original sourceendInnerOffset: the zero-based last character index within an element in the original sourceinnerHTML: the raw inner content of an element from the original sourceouterHTML: the raw outer content of an element from the original source
There is a strongly encouraged filename option available through the reshape options. This in combination with the line and col information can provide accurate debugging. However, if the original source comes from a different file, you can also provide a filename property on the tree node so that it is accurate. For example, if using reshape-include to include code from a different file, this would be necessary.
Example
For the following file:
<div id='main'>
<p>Hello {{ planet }}</p>
</div>
After processing by the reshape-expressions plugin, you would get the following tree:
[
{
"type": "tag",
"name": "div",
"location": {
"line": 1,
"col": 1,
"startOffset": 0,
"startInnerOffset": 15,
"endInnerOffset": 44,
"endOffset": 50,
"innerHTML": "\n <p>Hello {{ planet }}</p>\n",
"outerHTML": "<div id='main'>\n <p>Hello {{ planet }}</p>\n</div>"
},
"attrs": {
"id": [
{
"type": "text",
"content": "main",
"location": {
"line": 1,
"col": 6,
"startOf
