node-tailor
Version:
Tailor assembles a web page from multiple fragments
254 lines (240 loc) • 7.72 kB
JavaScript
'use strict';
const Serializer = require('parse5/lib/serializer');
/**
* CustomSerializer Class
*
* It uses the serializer from parse5 and overrides the serialize functions for handling
* the tags that are passed via handleTags and piping.
*
* All the default nodes like <div>, <head> go to parse5 serializer
* and the rest are handled in this class.
*
* Node - Represets DOM Tree
*/
module.exports = class CustomSerializer extends Serializer {
constructor(
node,
{ treeAdapter, fullRendering, slotMap, handleTags, pipeTags }
) {
super(node, { treeAdapter });
this.fullRendering = fullRendering;
this.slotMap = slotMap;
this.handleTags = handleTags;
this.pipeTags = pipeTags;
this.isPipeInserted = false;
this.lastChildInserted = false;
this.defaultSlotsInserted = false;
this.serializedList = [];
this._serializeNode = this._serializeNode.bind(this);
}
/**
* Push the serialized content in to the serializedList.
*
* this.html - serialized contents exposed by parse5
*/
pushBuffer() {
if (this.html !== '') {
this.serializedList.push(Buffer.from(this.html));
this.html = '';
}
}
/**
* Extract the serialized HTML content and reset the serialized buffer.
*
* @returns {String}
*/
getHtmlContent() {
let temp = '';
if (this.html !== '') {
temp = this.html;
this.html = '';
}
return temp;
}
/**
* Overidden the serialize function of parse5 Serializer
*
* this.startNode - Denotes the root node
* @returns {Array}
*/
serialize() {
this._serializeChildNodes(this.startNode);
this.pushBuffer();
return this.serializedList;
}
/**
* Checks if the node satifies the placeholder for `piping`
*
* @returns {Boolean}
*/
_isPipeNode(node) {
return this.pipeTags.includes(node.name);
}
/**
* Checks if the node is either slot node / script type=slot
*
* @returns {Boolean}
*/
_isSlotNode(node) {
const { attribs = {}, name } = node;
return (
name === 'slot' || (name === 'script' && attribs.type === 'slot')
);
}
/**
* Checks if the node is one of the nodes passed through handleTags
*
* @returns {Boolean}
*/
_isSpecialNode(node) {
const { attribs = {}, name } = node;
return (
this.handleTags.includes(name) ||
(name === 'script' && this.handleTags.includes(attribs.type))
);
}
/**
* Checks if the node is the lastChild of <body>
*
* @returns {Boolean}
*/
_isLastChildOfBody(node) {
const { parentNode: { name, lastChild } } = node;
return name === 'body' && Object.is(node, lastChild);
}
/**
* Serialize the nodes passed via handleTags
*
* @param {object} node
*
* // Input
* <fragment src="http://example.com" async primary></fragment>
*
* // Output
* {
* name: 'fragment',
* attributes: {
* async: '',
* primary: ''
* },
* }
*/
_serializeSpecial(node) {
this.pushBuffer();
let handledObj;
const { name, attribs: attributes } = node;
if (this.handleTags.includes(name)) {
handledObj = Object.assign({}, { name: name, attributes });
this.serializedList.push(handledObj);
this._serializeChildNodes(node);
this.pushBuffer();
} else {
// For handling the script type other than text/javascript
this._serializeChildNodes(node);
handledObj = Object.assign(
{},
{
name: attributes.type,
attributes,
textContent: this.getHtmlContent()
}
);
this.serializedList.push(handledObj);
}
this.serializedList.push({ closingTag: name });
}
/**
* Serialize the slot nodes from the slot map
*
* @param {object} node
*/
_serializeSlot(node) {
const slotName = node.attribs.name;
if (slotName) {
const childNodes = this.treeAdapter.getChildNodes(node);
const slots = this.slotMap.has(slotName)
? this.slotMap.get(slotName)
: childNodes;
slots && slots.forEach(this._serializeNode);
} else {
// Handling duplicate slots
if (this.defaultSlotsInserted) {
console.warn(
'Encountered duplicate Unnamed slots in the template - Skipping the node'
);
return;
}
const defaultSlots = this.slotMap.get('default');
this.defaultSlotsInserted = true;
defaultSlots && defaultSlots.forEach(this._serializeNode);
}
}
/**
* Insert the pipe placeholder and serialize the node
*
* @param {object} node
*/
_serializePipe(node) {
this.pushBuffer();
this.serializedList.push({ placeholder: 'pipe' });
this.isPipeInserted = true;
this._serializeNode(node);
}
/**
* Serialize the nodes in default slot from slot map and insert async placeholder.
*
* should happen before closing the body.
*/
_serializeRest() {
this.lastChildInserted = true;
if (!this.defaultSlotsInserted) {
const defaultSlots = this.slotMap.get('default');
defaultSlots && defaultSlots.forEach(this._serializeNode);
}
this.pushBuffer();
this.serializedList.push({ placeholder: 'async' });
}
/**
* Serialize all the children of a parent node.
*
* @param {object} parentNode
*/
_serializeChildNodes(parentNode) {
const childNodes = this.treeAdapter.getChildNodes(parentNode);
childNodes && childNodes.forEach(this._serializeNode);
}
/**
* Serialize the node based on their type
*
* @param {object} currentNode
*/
_serializeNode(currentNode) {
if (
this.fullRendering &&
!this.isPipeInserted &&
this._isPipeNode(currentNode)
) {
this._serializePipe(currentNode);
} else if (this._isSpecialNode(currentNode)) {
this._serializeSpecial(currentNode);
} else if (this._isSlotNode(currentNode)) {
this._serializeSlot(currentNode);
} else if (this.treeAdapter.isElementNode(currentNode)) {
this._serializeElement(currentNode);
} else if (this.treeAdapter.isTextNode(currentNode)) {
this._serializeTextNode(currentNode);
} else if (this.treeAdapter.isCommentNode(currentNode)) {
this._serializeCommentNode(currentNode);
} else if (this.treeAdapter.isDocumentTypeNode(currentNode)) {
this._serializeDocumentTypeNode(currentNode);
}
// Push default slots and async placeholder before body
if (
this.fullRendering &&
!this.lastChildInserted &&
this._isLastChildOfBody(currentNode)
) {
this._serializeRest();
}
}
};