@marko/compiler
Version:
Marko template to JS compiler.
666 lines (595 loc) • 18.9 kB
JavaScript
;exports.__esModule = true;exports.parseMarko = parseMarko;var _babelUtils = require("@marko/compiler/babel-utils");
var _babel = require("@marko/compiler/internal/babel");
var _htmljsParser = require("htmljs-parser");
var _buildCodeFrame = require("../util/build-code-frame");
var _mergeErrors = _interopRequireDefault(require("../util/merge-errors"));function _interopRequireDefault(e) {return e && e.__esModule ? e : { default: e };}
const noop = () => {};
const emptyRange = (part) => part.start === part.end;
const isAttrTag = (tag) => tag.name.value?.[0] === "@";
const isStatementTag = (tag) => tag.tagDef?.parseOptions?.statement;
const toBabelPosition = ({ line, character }) => ({
// Babel lines start at 1 and use "column" instead of "character".
line: line + 1,
column: character
});
function parseMarko(file) {
const { code } = file;
const { htmlParseOptions = {} } = file.markoOpts;
const { watchFiles } = file.metadata.marko;
const parseVisits = [];
let currentTag = file.path;
let currentBody = currentTag;
let currentAttr = undefined;
let currentShorthandId = undefined;
let currentShorthandClassNames = undefined;
let { preserveWhitespace } = htmlParseOptions;
let preservingWhitespaceUntil = preserveWhitespace;
let onNext = noop;
const positionAt = (index) => toBabelPosition(parser.positionAt(index));
const locationAt = (range) => {
const { start, end } = parser.locationAt(range);
return {
start: toBabelPosition(start),
end: toBabelPosition(end)
};
};
const withLoc = (node, range) => {
node.start = range.start;
node.end = range.end;
node.loc = locationAt(range);
return node;
};
const enterTag = (node) => {
if (isAttrTag(node)) {
if (currentTag === file.path) {
throw file.buildCodeFrameError(
node.name,
"@tags must be nested within another element."
);
}
let previousSiblingIndex = currentBody.length;
while (previousSiblingIndex) {
let previousSibling = currentBody[--previousSiblingIndex];
if (!_babel.types.isMarkoComment(previousSibling)) {
break;
}
currentTag.pushContainer("attributeTags", previousSibling.node);
currentBody.get("body").get(previousSiblingIndex).remove();
}
currentTag = currentTag.pushContainer("attributeTags", node)[0];
} else {
currentTag = currentBody.pushContainer("body", node)[0];
}
currentBody = currentTag.get("body");
onNext(node);
};
const pushContent = (node) => {
currentBody.node.body.push(node);
onNext(node);
};
const endAttr = () => {
if (currentAttr) {
currentAttr.loc = locationAt(currentAttr);
currentAttr = undefined;
}
};
const parseTemplateString = ({ quasis, expressions }) => {
switch (expressions.length) {
case 0:{
const [first] = quasis;
return withLoc(_babel.types.stringLiteral(parser.read(first)), first);
}
case 1:{
if (emptyRange(quasis[0]) && emptyRange(quasis[1])) {
const [{ value }] = expressions;
const result = (0, _babelUtils.parseExpression)(
file,
parser.read(value),
value.start,
value.end
);
if (_babel.types.isStringLiteral(result)) {
// convert to template literal just so that we don't mistake it for a native tag if this is a tag name.
return withLoc(
_babel.types.templateLiteral([templateElement(result.value, true)], []),
value
);
} else {
return result;
}
}
}
}
const [{ start }] = quasis;
const end = quasis[quasis.length - 1].end;
return (0, _babelUtils.parseTemplateLiteral)(file, parser.read({ start, end }), start, end);
};
const parser = (0, _htmljsParser.createParser)({
onError(part) {
const err = (0, _buildCodeFrame.buildCodeFrameError)(
file.opts.filename,
file.code,
locationAt(part),
part.message
);
if (!file.___hasParseErrors) {
throw err;
}
const errors = [];
_babel.types.traverseFast(file.path.node, (node) => {
if (node.type === "MarkoParseError") {
errors.push(
(0, _buildCodeFrame.buildCodeFrameError)(
file.opts.filename,
file.code,
node.errorLoc || node.loc,
node.label
)
);
}
});
errors.push(err);
(0, _mergeErrors.default)(errors);
},
onText(part) {
const rawValue = parser.read(part);
if (preservingWhitespaceUntil) {
pushContent(withLoc(_babel.types.markoText(rawValue), part));
return;
}
if (/^(?:[\n\r]\s*)?(?:[\n\r]\s*)?$/.test(rawValue)) return;
const { body } = currentBody.node;
let prev;
let prevIndex = body.length;
// Find previous non-scriptlet or comment.
while (prevIndex > 0) {
prev = body[--prevIndex];
if (_babel.types.isMarkoScriptlet(prev) || _babel.types.isMarkoComment(prev)) {
prev = undefined;
} else {
break;
}
}
let value = rawValue;
switch (prev?.type) {
case "MarkoPlaceholder":
break;
case "MarkoText":
if (/\s$/.test(prev.value)) {
value = value.replace(/^\s+/, "");
}
break;
case "MarkoTag":
if (isStatementTag(prev) || isAttrTag(prev)) {
value = value.replace(/^[\n\r]\s*/, "");
}
break;
default:
value = value.replace(/^[\n\r]\s*/, "");
break;
}
const node = _babel.types.markoText(value);
pushContent(node);
onNext = (next) => {
switch (next?.type) {
case "MarkoScriptlet":
case "MarkoComment":
return;
case "MarkoPlaceholder":
break;
case "MarkoText":
if (/^\s/.test(next.value)) {
value = value.replace(/\s+$/, "");
}
break;
case "MarkoTag":
if (isStatementTag(next) || isAttrTag(next)) {
value = value.replace(/[\n\r]\s*$/, "");
}
break;
default:
value = value.replace(/[\n\r]\s*$/, "");
break;
}
node.value = value.replace(/\s+/g, " ");
if (node.value) {
const trimmedStart = part.start + rawValue.indexOf(value);
withLoc(node, {
start: trimmedStart,
end: trimmedStart + rawValue.length
});
} else {
body.splice(body.indexOf(node), 1);
}
onNext = noop;
};
},
onCDATA(part) {
pushContent(withLoc(_babel.types.markoCDATA(parser.read(part.value)), part));
},
onDoctype(part) {
pushContent(withLoc(_babel.types.markoDocumentType(parser.read(part.value)), part));
},
onDeclaration(part) {
pushContent(withLoc(_babel.types.markoDeclaration(parser.read(part.value)), part));
},
onComment(part) {
pushContent(withLoc(_babel.types.markoComment(parser.read(part.value)), part));
},
onTagTypeArgs(part) {
currentTag.node.typeArguments = (0, _babelUtils.parseTypeArgs)(
file,
parser.read(part.value),
part.value.start,
part.value.end
);
},
onTagTypeParams(part) {
currentBody.node.typeParameters = (0, _babelUtils.parseTypeParams)(
file,
parser.read(part.value),
part.value.start,
part.value.end
);
},
onPlaceholder(part) {
pushContent(
withLoc(
_babel.types.markoPlaceholder(
(0, _babelUtils.parseExpression)(
file,
parser.read(part.value),
part.value.start,
part.value.end
),
part.escape
),
part
)
);
},
onScriptlet(part) {
pushContent(
withLoc(
_babel.types.markoScriptlet(
(0, _babelUtils.parseStatements)(
file,
parser.read(part.value),
part.value.start,
part.value.end
)
),
part
)
);
},
onOpenTagName(part) {
const tagName = parseTemplateString(part);
const node = _babel.types.markoTag(tagName, [], _babel.types.markoTagBody());
let parseType = _htmljsParser.TagType.html;
node.start =
part.start - (part.start && code[part.start - 1] === "<" ? 1 : 0); // Account for leading `<` in html mode.
node.end = part.end;
if (_babel.types.isStringLiteral(tagName)) {
const literalTagName = tagName.value || (tagName.value = "div");
if (literalTagName === "%") {
throw file.buildCodeFrameError(
tagName,
"<% scriptlets %> are no longer supported."
);
}
const parseOptions = (node.tagDef = (0, _babelUtils.getTagDefForTagName)(
file,
literalTagName
))?.parseOptions;
if (parseOptions) {
if (parseOptions.preserveWhitespace) {
preservingWhitespaceUntil = node;
}
if (parseOptions.statement) {
parseType = _htmljsParser.TagType.statement;
} else if (parseOptions.openTagOnly) {
parseType = _htmljsParser.TagType.void;
} else if (parseOptions.text) {
parseType = _htmljsParser.TagType.text;
}
}
}
enterTag(node);
return parseType;
},
onTagShorthandId(part) {
currentShorthandId = parseTemplateString(part);
},
onTagShorthandClass(part) {
if (currentShorthandClassNames) {
currentShorthandClassNames.push(parseTemplateString(part));
} else {
currentShorthandClassNames = [parseTemplateString(part)];
}
},
onTagVar({ value }) {
currentTag.node.var = (0, _babelUtils.parseVar)(
file,
parser.read(value),
value.start,
value.end
);
},
onTagParams({ value }) {
currentTag.node.body.params = (0, _babelUtils.parseParams)(
file,
parser.read(value),
value.start,
value.end
);
},
onTagArgs({ value }) {
currentTag.node.arguments = (0, _babelUtils.parseArgs)(
file,
parser.read(value),
value.start,
value.end
);
},
onAttrName(part) {
const [, name, modifier] = /^([^:]*)(?::(.*))?/.exec(parser.read(part));
endAttr();
currentTag.node.attributes.push(
currentAttr = _babel.types.markoAttribute(
name || "value",
_babel.types.booleanLiteral(true),
modifier,
undefined,
!name
)
);
currentAttr.start = part.start;
currentAttr.end = part.end;
},
onAttrArgs({ value, end }) {
currentAttr.arguments = (0, _babelUtils.parseArgs)(
file,
parser.read(value),
value.start,
value.end
);
currentAttr.end = end;
},
onAttrValue(part) {
currentAttr.end = part.end;
currentAttr.bound = part.bound;
currentAttr.value = (0, _babelUtils.parseExpression)(
file,
parser.read(part.value),
part.value.start
);
},
onAttrMethod(part) {
currentAttr.end = part.end;
currentAttr.value = withLoc(
_babel.types.functionExpression(
undefined,
(0, _babelUtils.parseParams)(
file,
parser.read(part.params.value),
part.params.value.start,
part.params.value.end
),
_babel.types.blockStatement(
(0, _babelUtils.parseStatements)(
file,
parser.read(part.body.value),
part.body.value.start,
part.body.value.end
)
)
),
part
);
},
onAttrSpread(part) {
endAttr();
currentTag.node.attributes.push(
withLoc(
_babel.types.markoSpreadAttribute(
(0, _babelUtils.parseExpression)(file, parser.read(part.value), part.value.start)
),
part
)
);
},
onOpenTagEnd(part) {
const { node } = currentTag;
const { attributes } = node;
const parseOptions = node.tagDef?.parseOptions;
endAttr();
if (currentShorthandClassNames) {
let foundClassAttr = false;
const classShorthandValue =
currentShorthandClassNames.length === 1 ?
currentShorthandClassNames[0] :
currentShorthandClassNames.every((expr) =>
_babel.types.isStringLiteral(expr)
) ?
withLoc(
_babel.types.stringLiteral(
currentShorthandClassNames.
map((node) => node.value).
join(" ")
),
{
start: currentShorthandClassNames[0].start,
end: currentShorthandClassNames[
currentShorthandClassNames.length - 1].
end
}
) :
_babel.types.arrayExpression(currentShorthandClassNames);
for (const attr of attributes) {
if (attr.name === "class") {
foundClassAttr = true;
if (
_babel.types.isStringLiteral(attr.value) &&
_babel.types.isStringLiteral(classShorthandValue))
{
attr.value = _babel.types.templateLiteral(
[
templateElement("", false),
templateElement(" ", false),
templateElement("", true)],
[classShorthandValue, attr.value]
);
} else {
attr.value = _babel.types.arrayExpression(
_babel.types.isArrayExpression(classShorthandValue) ?
classShorthandValue.elements.concat(
_babel.types.isArrayExpression(attr.value) ?
attr.value.elements :
attr.value
) :
_babel.types.isArrayExpression(attr.value) ?
[classShorthandValue].concat(attr.value.elements) :
[classShorthandValue, attr.value]
);
}
break;
}
}
if (!foundClassAttr) {
attributes.push(_babel.types.markoAttribute("class", classShorthandValue));
}
currentShorthandClassNames = undefined;
}
if (currentShorthandId) {
for (const attr of attributes) {
if (attr.name === "id") {
throw file.buildCodeFrameError(
attr,
"Cannot have shorthand id and id attribute."
);
}
}
attributes.push(_babel.types.markoAttribute("id", currentShorthandId));
currentShorthandId = undefined;
}
if (parseOptions) {
if (parseOptions.rawOpenTag) {
node.rawValue = parser.read({
start: node.name.start,
end: part.start
});
}
if (
part.selfClosed ||
parseOptions.statement ||
parseOptions.openTagOnly)
{
this.onCloseTagEnd(part);
}
} else if (part.selfClosed) {
this.onCloseTagEnd(part);
}
},
onCloseTagEnd(part) {
const { node } = currentTag;
const tagDef = node.tagDef;
const parserPlugin = tagDef?.parser;
if (preservingWhitespaceUntil === node) {
preservingWhitespaceUntil = undefined;
}
node.end = part.end;
node.loc = locationAt(node);
if (parserPlugin) {
const { hook } = parserPlugin;
if (parserPlugin.path) watchFiles.push(parserPlugin.path);
parseVisits.push(hook.default || hook, currentTag);
}
const parentTag = isAttrTag(node) ?
currentTag.parentPath :
currentTag.parentPath.parentPath;
const { attributeTags } = node;
if (attributeTags.length) {
const isControlFlow = tagDef?.parseOptions?.controlFlow;
if (node.body.body.length) {
const body = [];
// When we have a control flow with mixed body and attribute tag content
// we move any scriptlets, comments or empty nested control flow.
// This is because they initially ambiguous as to whether
// they are part of the body or the attributeTags.
// Otherwise we only move scriptlets.
for (const child of node.body.body) {
if (
_babel.types.isMarkoScriptlet(child) ||
isControlFlow && _babel.types.isMarkoComment(child))
{
attributeTags.push(child);
} else if (
isControlFlow &&
child.tagDef?.controlFlow &&
!child.body.body.length)
{
child.body.attributeTags = true;
attributeTags.push(child);
} else {
body.push(child);
}
}
if (isControlFlow) {
if (body.length) {
onNext();
throw file.buildCodeFrameError(
body[0],
"Cannot have attribute tags and body content under a control flow tag."
);
}
node.attributeTags = body;
node.body.body = attributeTags;
node.body.attributeTags = true;
} else {
node.body.body = body;
}
attributeTags.sort(sortByStart);
} else if (isControlFlow) {
node.attributeTags = [];
node.body.body = attributeTags;
node.body.attributeTags = true;
}
if (isControlFlow) {
currentTag.remove();
parentTag.pushContainer("attributeTags", node);
}
}
if (parentTag) {
currentTag = parentTag;
currentBody = currentTag.get("body");
} else {
currentTag = currentBody = file.path;
}
onNext();
}
});
parser.parse(code);
onNext();
for (let i = 0; i < parseVisits.length;) {
parseVisits[i++](parseVisits[i++]);
}
const { ast } = file;
const { program } = ast;
ast.start = program.start = 0;
ast.end = program.end = code.length - 1;
ast.loc = program.loc = {
start: { line: 1, column: 0 },
end: positionAt(ast.end)
};
}
function sortByStart(a, b) {
return a.start - b.start;
}
function templateElement(value, tail) {
return _babel.types.templateElement({
tail,
raw: value,
cooked: value
});
}