import normalize from './normalize';
import parseHTML from './parseHTML';
import treeAdapter from './parseHTMLTreeAdapter';
import Comment from './Comment';
import Doctype from './Doctype';
import Element from './Element';
import Fragment from './Fragment';
import Text from './Text';
import visit, { getVisitors } from './visit';
/** Return a new syntax tree {@link Result} from a processed input. */
class Result {
/**
* @param {string} html - HTML string to be parsed.
* @param {ProcessOptions} [processOptions] - Custom settings applied to the {@link Result}.
*/
constructor (html, processOptions) {
/** Type identifier of the Result
* @type {'result'} */
this.type = 'result';
/** Path to the HTML source file. You should always set from, because it is used in source map generation and syntax error messages
* @type {string} */
// the "to" and "from" locations are always string values
this.from = 'from' in Object(processOptions) && processOptions.from !== undefined && processOptions.from !== null
? String(processOptions.from)
: '';
/** Path to the HTML output file
* @type {string} */
this.to = 'to' in Object(processOptions) && processOptions.to !== undefined && processOptions.to !== null
? String(processOptions.to)
: this.from;
/** List of the elements that only have a start tag, as they cannot have any content
* @type {string[]} */
this.voidElements = 'voidElements' in Object(processOptions)
? [].concat(Object(processOptions).voidElements || [])
: Result.voidElements;
/** Normalized plugins and visitors
* @type {Object<string, Function[]>} */
// prepare visitors (which may be functions or visitors)
this.visitors = getVisitors(Object(processOptions).visitors);
/** List of the messages gathered during transformations
* @type {Message[]} */
this.messages = [];
// prepare the result object
this.input = { html: html, from: this.from, to: this.to };
// parse the html and transform it into nodes
const documentFragment = parseHTML(html, { voidElements: this.voidElements });
/** Root Node
* @type {Node} */
this.root = transform(documentFragment, this);
}
/** Current {@link Root} as a String. */
get html () {
return String(this.root);
}
/**
* Messages that are warnings.
* @returns {Array<MessageWarning>}
**/
get warnings () {
return /** @type Message[] */(this.messages).filter(
message => Object(message).type === 'warning'
);
}
/**
* Return a normalized node whose instances match the current {@link Result}.
* @param {Node} [node] - Node to be normalized.
* @example
* result.normalize(someNode)
*/
normalize (node) {
return normalize(node);
}
/**
* Current {@link Root} as an Object.
* @returns {Object}
*/
toJSON () {
return this.root.toJSON();
}
/**
* Transform the current {@link Node} and any descendants using visitors.
* @param {Node} node - {@link Node} to be visited.
* @param {Object} [overrideVisitors] - Alternative visitors to be used in place of {@link Result} visitors.
* @returns {ResultPromise}
* @example
* await result.visit(someNode)
* @example
* await result.visit() // visit using the root of the current result
* @example
* await result.visit(root, {
* Element () {
* // do something to an element
* }
* })
*/
visit (node, overrideVisitors) {
const nodeToUse = 0 in arguments ? node : this.root;
return visit(nodeToUse, this, overrideVisitors);
}
/**
* Add a warning to the current {@link Root}.
* @param {string} text - Message being sent as the warning.
* @param {Object} [opts] - Additional information about the warning.
* @example
* result.warn('Something went wrong')
* @example
* result.warn('Something went wrong', {
* node: someNode,
* plugin: somePlugin
* })
*/
warn (text, rawopts) {
const opts = Object(rawopts);
if (!opts.plugin) {
if (Object(this.currentPlugin).name) {
opts.plugin = this.currentPlugin.name
}
}
this.messages.push({ type: 'warning', text, opts });
}
static voidElements = [
'area',
'base',
'br',
'col',
'command',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr'
];
}
function transform (node, result) {
const hasSource = node.sourceCodeLocation === Object(node.sourceCodeLocation);
const source = hasSource
? {
startOffset: node.sourceCodeLocation.startOffset,
endOffset: node.sourceCodeLocation.endOffset,
startInnerOffset: Object(node.sourceCodeLocation.startTag).endOffset || node.sourceCodeLocation.startOffset,
endInnerOffset: Object(node.sourceCodeLocation.endTag).startOffset || node.sourceCodeLocation.endOffset,
input: result.input
}
: {
startOffset: 0,
startInnerOffset: 0,
endInnerOffset: result.input.html.length,
endOffset: result.input.html.length,
input: result.input
};
if (Object(node.sourceCodeLocation).startTag) {
source.before = result.input.html.slice(source.startOffset, source.startInnerOffset - 1).match(/\s*\/?$/)[0]
}
if (Object(node.sourceCodeLocation).endTag) {
source.after = result.input.html.slice(source.endInnerOffset + 2 + node.nodeName.length, source.endOffset - 1)
}
const $node = treeAdapter.isCommentNode(node)
? new Comment({ comment: node.data, source, result })
: treeAdapter.isDocumentTypeNode(node)
? new Doctype(Object.assign(node, { result, source: Object.assign({}, node.source, source) }))
: treeAdapter.isElementNode(node)
? new Element({
name: result.input.html.slice(source.startOffset + 1, source.startOffset + 1 + node.nodeName.length),
attrs: node.attrs.map(attr => attr.raw),
nodes: node.childNodes instanceof Array ? node.childNodes.map(child => transform(child, result)) : null,
isSelfClosing: /\//.test(source.before),
isWithoutEndTag: !Object(node.sourceCodeLocation).endTag,
isVoid: result.voidElements.includes(node.tagName),
result,
source
})
: treeAdapter.isTextNode(node)
? new Text({
data: hasSource ? source.input.html.slice(
source.startInnerOffset,
source.endInnerOffset
) : node.value,
result,
source
})
: new Fragment({
nodes: node.childNodes instanceof Array ? node.childNodes.map(child => transform(child, result)) : null,
result,
source
});
return $node;
}
export default Result;
/**
* Promise to return a syntax tree.
* @typedef {Promise<Result>} ResultPromise
* @example
* resultPromise.then(result => {
* // do something with the result
* })
*/
/**
* @typedef {Object} ProcessOptions - Custom settings applied to the {@link Result}.
* @property {string} [from] - Source input location.
* @property {string} [to] - Destination output location.
* @property {string[]} [voidElements] - Void elements.
*/
/**
* @typedef {Object} ResultType - Result of a pHTML transformation.
* @property {string} from - Path to the HTML source file. You should always set from, because it is used in source map generation and syntax error messages.
* @property {string} to - Path to the HTML output file.
* @property {Fragment} root - Object representing the parsed nodes of the HTML file.
* @property {Message[]} messages - List of the messages gathered during transformations.
* @property {string[]} voidElements - List of the elements that only have a start tag, as they cannot have any content.
*/
/**
* @typedef {Object} Message - Message gathered during transformations.
* @property {string} type - Type of message.
* @property {string} text - Message itself.
* @property {MessageInfo} [opts] - Information about the message.
*/
/**
* @typedef {Object} MessageInfo - Information about the message.
* @property {Plugin} [plugin] - Current plugin.
*/
/**
* @typedef {{ type: "warning" }} MessageWarning - Warning message gathered during transformations.
* @extends {Message}
*/