2018-10-13 08:55:38 +03:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
const {
|
2018-11-04 18:03:07 +03:00
|
|
|
canHaveInterpolation,
|
2018-10-13 08:55:38 +03:00
|
|
|
getNodeCssStyleDisplay,
|
|
|
|
isDanglingSpaceSensitiveNode,
|
2018-11-04 18:03:07 +03:00
|
|
|
isIndentationSensitiveNode,
|
2018-10-13 08:55:38 +03:00
|
|
|
isLeadingSpaceSensitiveNode,
|
|
|
|
isTrailingSpaceSensitiveNode,
|
2018-11-04 18:03:07 +03:00
|
|
|
isWhitespaceSensitiveNode
|
2018-10-13 08:55:38 +03:00
|
|
|
} = require("./utils");
|
|
|
|
|
|
|
|
const PREPROCESS_PIPELINE = [
|
2018-11-04 18:03:07 +03:00
|
|
|
removeIgnorableFirstLf,
|
2018-11-15 07:35:28 +03:00
|
|
|
mergeIeConditonalStartEndCommentIntoElementOpeningTag,
|
2018-11-04 18:03:07 +03:00
|
|
|
mergeCdataIntoText,
|
|
|
|
extractInterpolation,
|
2018-10-13 08:55:38 +03:00
|
|
|
extractWhitespaces,
|
|
|
|
addCssDisplay,
|
2018-11-04 18:03:07 +03:00
|
|
|
addIsSelfClosing,
|
2018-11-29 06:03:22 +03:00
|
|
|
addHasHtmComponentClosingTag,
|
2018-10-13 08:55:38 +03:00
|
|
|
addIsSpaceSensitive,
|
2018-11-04 18:03:07 +03:00
|
|
|
mergeSimpleElementIntoText
|
2018-10-13 08:55:38 +03:00
|
|
|
];
|
|
|
|
|
|
|
|
function preprocess(ast, options) {
|
|
|
|
for (const fn of PREPROCESS_PIPELINE) {
|
|
|
|
ast = fn(ast, options);
|
|
|
|
}
|
|
|
|
return ast;
|
|
|
|
}
|
|
|
|
|
2018-11-04 18:03:07 +03:00
|
|
|
function removeIgnorableFirstLf(ast /*, options */) {
|
|
|
|
return ast.map(node => {
|
|
|
|
if (
|
|
|
|
node.type === "element" &&
|
|
|
|
node.tagDefinition.ignoreFirstLf &&
|
|
|
|
node.children.length !== 0 &&
|
|
|
|
node.children[0].type === "text" &&
|
|
|
|
node.children[0].value[0] === "\n"
|
|
|
|
) {
|
|
|
|
const text = node.children[0];
|
|
|
|
return node.clone({
|
|
|
|
children:
|
|
|
|
text.value.length === 1
|
|
|
|
? node.children.slice(1)
|
|
|
|
: [].concat(
|
|
|
|
text.clone({ value: text.value.slice(1) }),
|
|
|
|
node.children.slice(1)
|
|
|
|
)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return node;
|
2018-10-13 08:55:38 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-11-15 07:35:28 +03:00
|
|
|
function mergeIeConditonalStartEndCommentIntoElementOpeningTag(
|
|
|
|
ast /*, options */
|
|
|
|
) {
|
|
|
|
/**
|
|
|
|
* <!--[if ...]><!--><target><!--<![endif]-->
|
|
|
|
*/
|
|
|
|
const isTarget = node =>
|
|
|
|
node.type === "element" &&
|
|
|
|
node.prev &&
|
|
|
|
node.prev.type === "ieConditionalStartComment" &&
|
|
|
|
node.prev.sourceSpan.end.offset === node.startSourceSpan.start.offset &&
|
|
|
|
node.firstChild &&
|
|
|
|
node.firstChild.type === "ieConditionalEndComment" &&
|
|
|
|
node.firstChild.sourceSpan.start.offset === node.startSourceSpan.end.offset;
|
|
|
|
return ast.map(node => {
|
|
|
|
if (node.children) {
|
|
|
|
const isTargetResults = node.children.map(isTarget);
|
|
|
|
if (isTargetResults.some(Boolean)) {
|
|
|
|
const newChildren = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < node.children.length; i++) {
|
|
|
|
const child = node.children[i];
|
|
|
|
|
|
|
|
if (isTargetResults[i + 1]) {
|
|
|
|
// ieConditionalStartComment
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isTargetResults[i]) {
|
|
|
|
const ieConditionalStartComment = child.prev;
|
|
|
|
const ieConditionalEndComment = child.firstChild;
|
|
|
|
|
|
|
|
const ParseSourceSpan = child.sourceSpan.constructor;
|
|
|
|
const startSourceSpan = new ParseSourceSpan(
|
|
|
|
ieConditionalStartComment.sourceSpan.start,
|
|
|
|
ieConditionalEndComment.sourceSpan.end
|
|
|
|
);
|
|
|
|
const sourceSpan = new ParseSourceSpan(
|
|
|
|
startSourceSpan.start,
|
|
|
|
child.sourceSpan.end
|
|
|
|
);
|
|
|
|
|
|
|
|
newChildren.push(
|
|
|
|
child.clone({
|
|
|
|
condition: ieConditionalStartComment.condition,
|
|
|
|
sourceSpan,
|
|
|
|
startSourceSpan,
|
|
|
|
children: child.children.slice(1)
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
newChildren.push(child);
|
|
|
|
}
|
|
|
|
|
|
|
|
return node.clone({ children: newChildren });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return node;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-11-04 18:03:07 +03:00
|
|
|
function mergeNodeIntoText(ast, shouldMerge, getValue) {
|
|
|
|
return ast.map(node => {
|
|
|
|
if (node.children) {
|
|
|
|
const shouldMergeResults = node.children.map(shouldMerge);
|
|
|
|
if (shouldMergeResults.some(Boolean)) {
|
|
|
|
const newChildren = [];
|
|
|
|
for (let i = 0; i < node.children.length; i++) {
|
|
|
|
const child = node.children[i];
|
|
|
|
|
|
|
|
if (child.type !== "text" && !shouldMergeResults[i]) {
|
|
|
|
newChildren.push(child);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const newChild =
|
|
|
|
child.type === "text"
|
|
|
|
? child
|
|
|
|
: child.clone({ type: "text", value: getValue(child) });
|
|
|
|
|
|
|
|
if (
|
|
|
|
newChildren.length === 0 ||
|
|
|
|
newChildren[newChildren.length - 1].type !== "text"
|
|
|
|
) {
|
|
|
|
newChildren.push(newChild);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const lastChild = newChildren.pop();
|
|
|
|
const ParseSourceSpan = lastChild.sourceSpan.constructor;
|
|
|
|
newChildren.push(
|
|
|
|
lastChild.clone({
|
|
|
|
value: lastChild.value + newChild.value,
|
|
|
|
sourceSpan: new ParseSourceSpan(
|
|
|
|
lastChild.sourceSpan.start,
|
|
|
|
newChild.sourceSpan.end
|
|
|
|
)
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return node.clone({ children: newChildren });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return node;
|
2018-10-13 08:55:38 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-11-04 18:03:07 +03:00
|
|
|
function mergeCdataIntoText(ast /*, options */) {
|
|
|
|
return mergeNodeIntoText(
|
|
|
|
ast,
|
|
|
|
node => node.type === "cdata",
|
|
|
|
node => `<![CDATA[${node.value}]]>`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function mergeSimpleElementIntoText(ast /*, options */) {
|
|
|
|
const isSimpleElement = node =>
|
|
|
|
node.type === "element" &&
|
|
|
|
node.attrs.length === 0 &&
|
|
|
|
node.children.length === 1 &&
|
|
|
|
node.firstChild.type === "text" &&
|
|
|
|
// \xA0: non-breaking whitespace
|
|
|
|
!/[^\S\xA0]/.test(node.children[0].value) &&
|
|
|
|
!node.firstChild.hasLeadingSpaces &&
|
|
|
|
!node.firstChild.hasTrailingSpaces &&
|
|
|
|
node.isLeadingSpaceSensitive &&
|
|
|
|
!node.hasLeadingSpaces &&
|
|
|
|
node.isTrailingSpaceSensitive &&
|
|
|
|
!node.hasTrailingSpaces &&
|
|
|
|
node.prev &&
|
|
|
|
node.prev.type === "text" &&
|
|
|
|
node.next &&
|
|
|
|
node.next.type === "text";
|
|
|
|
return ast.map(node => {
|
|
|
|
if (node.children) {
|
|
|
|
const isSimpleElementResults = node.children.map(isSimpleElement);
|
|
|
|
if (isSimpleElementResults.some(Boolean)) {
|
|
|
|
const newChildren = [];
|
|
|
|
for (let i = 0; i < node.children.length; i++) {
|
|
|
|
const child = node.children[i];
|
|
|
|
if (isSimpleElementResults[i]) {
|
|
|
|
const lastChild = newChildren.pop();
|
|
|
|
const nextChild = node.children[++i];
|
|
|
|
const ParseSourceSpan = node.sourceSpan.constructor;
|
|
|
|
const { isTrailingSpaceSensitive, hasTrailingSpaces } = nextChild;
|
|
|
|
newChildren.push(
|
|
|
|
lastChild.clone({
|
|
|
|
value:
|
|
|
|
lastChild.value +
|
|
|
|
`<${child.rawName}>` +
|
|
|
|
child.firstChild.value +
|
|
|
|
`</${child.rawName}>` +
|
|
|
|
nextChild.value,
|
|
|
|
sourceSpan: new ParseSourceSpan(
|
|
|
|
lastChild.sourceSpan.start,
|
|
|
|
nextChild.sourceSpan.end
|
|
|
|
),
|
|
|
|
isTrailingSpaceSensitive,
|
|
|
|
hasTrailingSpaces
|
|
|
|
})
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
newChildren.push(child);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return node.clone({ children: newChildren });
|
|
|
|
}
|
2018-10-13 08:55:38 +03:00
|
|
|
}
|
|
|
|
return node;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-11-04 18:03:07 +03:00
|
|
|
function extractInterpolation(ast, options) {
|
|
|
|
if (options.parser === "html") {
|
|
|
|
return ast;
|
|
|
|
}
|
|
|
|
|
|
|
|
const interpolationRegex = /\{\{([\s\S]+?)\}\}/g;
|
|
|
|
return ast.map(node => {
|
|
|
|
if (!canHaveInterpolation(node)) {
|
2018-10-13 08:55:38 +03:00
|
|
|
return node;
|
|
|
|
}
|
|
|
|
|
2018-11-04 18:03:07 +03:00
|
|
|
const newChildren = [];
|
|
|
|
|
|
|
|
for (const child of node.children) {
|
|
|
|
if (child.type !== "text") {
|
|
|
|
newChildren.push(child);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const ParseSourceSpan = child.sourceSpan.constructor;
|
|
|
|
|
|
|
|
let startSourceSpan = child.sourceSpan.start;
|
|
|
|
let endSourceSpan = null;
|
|
|
|
const components = child.value.split(interpolationRegex);
|
|
|
|
for (
|
|
|
|
let i = 0;
|
|
|
|
i < components.length;
|
|
|
|
i++, startSourceSpan = endSourceSpan
|
|
|
|
) {
|
|
|
|
const value = components[i];
|
|
|
|
|
|
|
|
if (i % 2 === 0) {
|
|
|
|
endSourceSpan = startSourceSpan.moveBy(value.length);
|
|
|
|
if (value.length !== 0) {
|
|
|
|
newChildren.push({
|
|
|
|
type: "text",
|
|
|
|
value,
|
|
|
|
sourceSpan: new ParseSourceSpan(startSourceSpan, endSourceSpan)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
endSourceSpan = startSourceSpan.moveBy(value.length + 4); // `{{` + `}}`
|
|
|
|
newChildren.push({
|
|
|
|
type: "interpolation",
|
|
|
|
sourceSpan: new ParseSourceSpan(startSourceSpan, endSourceSpan),
|
|
|
|
children:
|
|
|
|
value.length === 0
|
|
|
|
? []
|
|
|
|
: [
|
|
|
|
{
|
|
|
|
type: "text",
|
|
|
|
value,
|
|
|
|
sourceSpan: new ParseSourceSpan(
|
|
|
|
startSourceSpan.moveBy(2),
|
|
|
|
endSourceSpan.moveBy(-2)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return node.clone({ children: newChildren });
|
2018-10-13 08:55:38 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* - add `hasLeadingSpaces` field
|
|
|
|
* - add `hasTrailingSpaces` field
|
|
|
|
* - add `hasDanglingSpaces` field for parent nodes
|
2018-11-04 18:03:07 +03:00
|
|
|
* - add `isWhitespaceSensitive`, `isIndentationSensitive` field for text nodes
|
2018-10-13 08:55:38 +03:00
|
|
|
* - remove insensitive whitespaces
|
|
|
|
*/
|
|
|
|
function extractWhitespaces(ast /*, options*/) {
|
|
|
|
const TYPE_WHITESPACE = "whitespace";
|
2018-11-04 18:03:07 +03:00
|
|
|
return ast.map(node => {
|
2018-10-13 08:55:38 +03:00
|
|
|
if (!node.children) {
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
node.children.length === 0 ||
|
|
|
|
(node.children.length === 1 &&
|
|
|
|
node.children[0].type === "text" &&
|
2018-11-04 18:03:07 +03:00
|
|
|
node.children[0].value.trim().length === 0)
|
2018-10-13 08:55:38 +03:00
|
|
|
) {
|
2018-11-04 18:03:07 +03:00
|
|
|
return node.clone({
|
2018-10-13 08:55:38 +03:00
|
|
|
children: [],
|
|
|
|
hasDanglingSpaces: node.children.length !== 0
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-11-04 18:03:07 +03:00
|
|
|
const isWhitespaceSensitive = isWhitespaceSensitiveNode(node);
|
|
|
|
const isIndentationSensitive = isIndentationSensitiveNode(node);
|
2018-10-13 08:55:38 +03:00
|
|
|
|
2018-11-04 18:03:07 +03:00
|
|
|
return node.clone({
|
2018-11-15 05:34:42 +03:00
|
|
|
isWhitespaceSensitive,
|
|
|
|
isIndentationSensitive,
|
2018-10-13 08:55:38 +03:00
|
|
|
children: node.children
|
|
|
|
// extract whitespace nodes
|
|
|
|
.reduce((newChildren, child) => {
|
2018-11-15 05:34:42 +03:00
|
|
|
if (child.type !== "text" || isWhitespaceSensitive) {
|
2018-10-13 08:55:38 +03:00
|
|
|
return newChildren.concat(child);
|
|
|
|
}
|
|
|
|
|
|
|
|
const localChildren = [];
|
|
|
|
|
2018-11-04 18:03:07 +03:00
|
|
|
const [, leadingSpaces, text, trailingSpaces] = child.value.match(
|
2018-10-13 08:55:38 +03:00
|
|
|
/^(\s*)([\s\S]*?)(\s*)$/
|
|
|
|
);
|
|
|
|
|
|
|
|
if (leadingSpaces) {
|
|
|
|
localChildren.push({ type: TYPE_WHITESPACE });
|
|
|
|
}
|
|
|
|
|
2018-11-04 18:03:07 +03:00
|
|
|
const ParseSourceSpan = child.sourceSpan.constructor;
|
|
|
|
|
2018-10-13 08:55:38 +03:00
|
|
|
if (text) {
|
|
|
|
localChildren.push({
|
|
|
|
type: "text",
|
2018-11-04 18:03:07 +03:00
|
|
|
value: text,
|
|
|
|
sourceSpan: new ParseSourceSpan(
|
|
|
|
child.sourceSpan.start.moveBy(leadingSpaces.length),
|
|
|
|
child.sourceSpan.end.moveBy(-trailingSpaces.length)
|
|
|
|
)
|
2018-10-13 08:55:38 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (trailingSpaces) {
|
|
|
|
localChildren.push({ type: TYPE_WHITESPACE });
|
|
|
|
}
|
|
|
|
|
|
|
|
return newChildren.concat(localChildren);
|
|
|
|
}, [])
|
|
|
|
// set hasLeadingSpaces/hasTrailingSpaces and filter whitespace nodes
|
|
|
|
.reduce((newChildren, child, i, children) => {
|
|
|
|
if (child.type === TYPE_WHITESPACE) {
|
|
|
|
return newChildren;
|
|
|
|
}
|
|
|
|
|
|
|
|
const hasLeadingSpaces =
|
|
|
|
i !== 0 && children[i - 1].type === TYPE_WHITESPACE;
|
|
|
|
const hasTrailingSpaces =
|
|
|
|
i !== children.length - 1 &&
|
|
|
|
children[i + 1].type === TYPE_WHITESPACE;
|
|
|
|
|
|
|
|
return newChildren.concat(
|
|
|
|
Object.assign({}, child, {
|
|
|
|
hasLeadingSpaces,
|
|
|
|
hasTrailingSpaces
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}, [])
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-11-04 18:03:07 +03:00
|
|
|
function addIsSelfClosing(ast /*, options */) {
|
|
|
|
return ast.map(node =>
|
|
|
|
Object.assign(node, {
|
|
|
|
isSelfClosing:
|
|
|
|
!node.children ||
|
|
|
|
(node.type === "element" &&
|
|
|
|
(node.tagDefinition.isVoid ||
|
|
|
|
// self-closing
|
|
|
|
node.startSourceSpan === node.endSourceSpan))
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-11-29 06:03:22 +03:00
|
|
|
function addHasHtmComponentClosingTag(ast, options) {
|
|
|
|
return ast.map(node =>
|
|
|
|
node.type !== "element"
|
|
|
|
? node
|
|
|
|
: Object.assign(node, {
|
|
|
|
hasHtmComponentClosingTag:
|
|
|
|
node.endSourceSpan &&
|
|
|
|
/^<\s*\/\s*\/\s*>$/.test(
|
|
|
|
options.originalText.slice(
|
|
|
|
node.endSourceSpan.start.offset,
|
|
|
|
node.endSourceSpan.end.offset
|
|
|
|
)
|
|
|
|
)
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-10-13 08:55:38 +03:00
|
|
|
function addCssDisplay(ast, options) {
|
2018-11-04 18:03:07 +03:00
|
|
|
return ast.map(node =>
|
|
|
|
Object.assign(node, { cssDisplay: getNodeCssStyleDisplay(node, options) })
|
|
|
|
);
|
2018-10-13 08:55:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* - add `isLeadingSpaceSensitive` field
|
|
|
|
* - add `isTrailingSpaceSensitive` field
|
|
|
|
* - add `isDanglingSpaceSensitive` field for parent nodes
|
|
|
|
*/
|
|
|
|
function addIsSpaceSensitive(ast /*, options */) {
|
2018-11-04 18:03:07 +03:00
|
|
|
return ast.map(node => {
|
2018-10-13 08:55:38 +03:00
|
|
|
if (!node.children) {
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node.children.length === 0) {
|
2018-11-04 18:03:07 +03:00
|
|
|
return node.clone({
|
2018-10-13 08:55:38 +03:00
|
|
|
isDanglingSpaceSensitive: isDanglingSpaceSensitiveNode(node)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-11-04 18:03:07 +03:00
|
|
|
return node.clone({
|
2018-10-13 08:55:38 +03:00
|
|
|
children: node.children
|
2018-11-04 18:03:07 +03:00
|
|
|
.map(child => {
|
2018-10-13 08:55:38 +03:00
|
|
|
return Object.assign({}, child, {
|
2018-11-04 18:03:07 +03:00
|
|
|
isLeadingSpaceSensitive: isLeadingSpaceSensitiveNode(child),
|
|
|
|
isTrailingSpaceSensitive: isTrailingSpaceSensitiveNode(child)
|
2018-10-13 08:55:38 +03:00
|
|
|
});
|
|
|
|
})
|
2018-11-04 18:03:07 +03:00
|
|
|
.map((child, index, children) =>
|
|
|
|
Object.assign({}, child, {
|
|
|
|
isLeadingSpaceSensitive:
|
|
|
|
index === 0
|
|
|
|
? child.isLeadingSpaceSensitive
|
|
|
|
: children[index - 1].isTrailingSpaceSensitive &&
|
|
|
|
child.isLeadingSpaceSensitive,
|
|
|
|
isTrailingSpaceSensitive:
|
|
|
|
index === children.length - 1
|
|
|
|
? child.isTrailingSpaceSensitive
|
|
|
|
: children[index + 1].isLeadingSpaceSensitive &&
|
|
|
|
child.isTrailingSpaceSensitive
|
|
|
|
})
|
|
|
|
)
|
2018-10-13 08:55:38 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = preprocess;
|