Skip to content

Commit

Permalink
refactor: attribute tags now on MarkoTag AST
Browse files Browse the repository at this point in the history
  • Loading branch information
DylanPiercey committed Oct 28, 2024
1 parent 4652400 commit be9a9ef
Show file tree
Hide file tree
Showing 57 changed files with 620 additions and 482 deletions.
9 changes: 9 additions & 0 deletions .changeset/famous-buttons-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@marko/translator-default": patch
"@marko/translator-tags": patch
"@marko/babel-utils": patch
"@marko/compiler": patch
"marko": patch
---

Make attribute tags a property on the MarkoTag AST and refactor how attribute tags are translated.
15 changes: 11 additions & 4 deletions packages/babel-utils/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export interface Tag {
rootOnly?: boolean;
rawOpenTag?: boolean;
openTagOnly?: boolean;
controlFlow?: boolean;
ignoreAttributes?: boolean;
relaxRequireCommas?: boolean;
state?: "html" | "static-text" | "parsed-text" | "cdata";
Expand All @@ -145,7 +146,7 @@ export type FunctionPlugin<T = any> = (
path: t.NodePath<T>,
types: typeof t,
) => void;
type EnterExitPlugin<T = any> = {
export type EnterExitPlugin<T = any> = {
enter?(path: t.NodePath<T>, types: typeof t): void;
exit?(path: t.NodePath<T>, types: typeof t): void;
};
Expand Down Expand Up @@ -174,9 +175,15 @@ export function assertAttributesOrSingleArg(path: t.NodePath<t.MarkoTag>): void;
export function isNativeTag(path: t.NodePath<t.MarkoTag>): boolean;
export function isMacroTag(path: t.NodePath<t.MarkoTag>): boolean;
export function isDynamicTag(path: t.NodePath<t.MarkoTag>): boolean;
export function isAttributeTag(path: t.NodePath<t.MarkoTag>): boolean;
export function isTransparentTag(path: t.NodePath<t.MarkoTag>): boolean;
export function isLoopTag(path: t.NodePath<t.MarkoTag>): boolean;
export function isAttributeTag(
path: t.NodePath<t.MarkoTag>,
): path is t.NodePath<t.MarkoTag & { name: t.StringLiteral }>;
export function isTransparentTag(
path: t.NodePath<t.MarkoTag>,
): path is t.NodePath<t.MarkoTag & { name: t.StringLiteral }>;
export function isLoopTag(
path: t.NodePath<t.MarkoTag>,
): path is t.NodePath<t.MarkoTag & { name: t.StringLiteral }>;

export function registerMacro(path: t.NodePath<any>, name: string): void;
export function hasMacro(path: t.NodePath<any>, name: string): boolean;
Expand Down
86 changes: 50 additions & 36 deletions packages/babel-utils/src/assert.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
export function assertAllowedAttributes(path, allowed) {
const { node } = path;
node.attributes.forEach((attr, i) => {
if (!allowed.includes(attr.name)) {
throw path
.get(`attributes.${i}`)
.buildCodeFrameError(
`Invalid "${node.name.value}" tag attribute: "${attr.name}".`,
);
let i = 0;
for (const attr of path.node.attributes) {
if (attr.type === "MarkoSpreadAttribute") {
throw path.hub.buildError(
attr,
`Tag does not support spread attributes.`,
);

Check warning on line 8 in packages/babel-utils/src/assert.js

View check run for this annotation

Codecov / codecov/patch

packages/babel-utils/src/assert.js#L5-L8

Added lines #L5 - L8 were not covered by tests
} else if (!allowed.includes(attr.name)) {
throw path.hub.buildError(
attr,
`Tag does not support the \`${attr.name}\` attribute.`,
);

Check warning on line 13 in packages/babel-utils/src/assert.js

View check run for this annotation

Codecov / codecov/patch

packages/babel-utils/src/assert.js#L10-L13

Added lines #L10 - L13 were not covered by tests
}
});

i++;
}
}

export function assertNoAttributes(path) {
assertAllowedAttributes(path, []);
const { attributes } = path.node;
if (attributes.length) {
const start = attributes[0].loc.start;
const end = attributes[attributes.length - 1].loc.end;
throw path.hub.buildError(
{ loc: { start, end } },
"Tag does not support attributes.",
);
}
}

export function assertNoParams(path) {
Expand All @@ -28,41 +42,42 @@ export function assertNoParams(path) {
}

export function assertNoAttributeTags(path) {
const exampleAttributeTag = path.get("exampleAttributeTag");
if (exampleAttributeTag.node) {
throw exampleAttributeTag
.get("name")
.buildCodeFrameError("@tags must be within a custom element.");
if (path.node.attributeTags.length) {
throw path.hub.buildError(
path.node.attributeTags[0],
"Tag not support nested attribute tags.",
);

Check warning on line 49 in packages/babel-utils/src/assert.js

View check run for this annotation

Codecov / codecov/patch

packages/babel-utils/src/assert.js#L46-L49

Added lines #L46 - L49 were not covered by tests
}
}

export function assertNoArgs(path) {
const { hub } = path;
const args = path.get("arguments");
if (args.length) {
const start = args[0].node.loc.start;
const end = args[args.length - 1].node.loc.end;
throw hub.buildError(
const args = path.node.arguments;
if (args && args.length) {
const start = args[0].loc.start;
const end = args[args.length - 1].loc.end;
throw path.hub.buildError(
{ loc: { start, end } },
"Tag does not support arguments.",
);
}
}

export function assertNoVar(path) {
const tagVar = path.get("var");
if (tagVar.node) {
throw tagVar.buildCodeFrameError("Tag does not support a variable.");
if (path.node.var) {
throw path.hub.buildError(
path.node.var,
"Tag does not support a variable.",
);
}
}

export function assertAttributesOrArgs(path) {
const { hub, node } = path;
const args = path.get("arguments");
if (args.length && (node.attributes.length > 0 || node.body.length)) {
const start = args[0].node.loc.start;
const end = args[args.length - 1].node.loc.end;
throw hub.buildError(
const { node } = path;
const args = node.arguments;
if (args && args.length && (node.attributes.length > 0 || node.body.length)) {
const start = args[0].loc.start;
const end = args[args.length - 1].loc.end;
throw path.hub.buildError(
{ loc: { start, end } },
"Tag does not support arguments when attributes or body present.",
);
Expand All @@ -71,12 +86,11 @@ export function assertAttributesOrArgs(path) {

export function assertAttributesOrSingleArg(path) {
assertAttributesOrArgs(path);
const { hub } = path;
const args = path.get("arguments");
if (args.length > 1) {
const start = args[1].node.loc.start;
const end = args[args.length - 1].node.loc.end;
throw hub.buildError(
const args = path.node.arguments;
if (args && args.length > 1) {
const start = args[1].loc.start;
const end = args[args.length - 1].loc.end;
throw path.hub.buildError(
{ loc: { start, end } },
"Tag does not support multiple arguments.",
);
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-utils/src/tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export function findParentTag(path) {
}

export function findAttributeTags(path, attributeTags = []) {
path.get("body.body").forEach((child) => {
path.get("attributeTags").forEach((child) => {

Check warning on line 216 in packages/babel-utils/src/tags.js

View check run for this annotation

Codecov / codecov/patch

packages/babel-utils/src/tags.js#L216

Added line #L216 was not covered by tests
if (isAttributeTag(child)) {
attributeTags.push(child);
} else if (isTransparentTag(child)) {
Expand Down
79 changes: 75 additions & 4 deletions packages/compiler/src/babel-plugin/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,28 @@ export function parseMarko(file) {
return node;
};
const enterTag = (node) => {
currentTag = currentBody.pushContainer("body", node)[0];
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 (!t.isMarkoComment(previousSibling)) {
break;
}
currentTag.pushContainer("attributeTags", previousSibling.node);
currentBody.get("body").get(previousSiblingIndex).remove();
}

Check warning on line 68 in packages/compiler/src/babel-plugin/parser.js

View check run for this annotation

Codecov / codecov/patch

packages/compiler/src/babel-plugin/parser.js#L62-L68

Added lines #L62 - L68 were not covered by tests

currentTag = currentTag.pushContainer("attributeTags", node)[0];
} else {
currentTag = currentBody.pushContainer("body", node)[0];
}
currentBody = currentTag.get("body");
onNext(node);
};
Expand Down Expand Up @@ -496,7 +517,8 @@ export function parseMarko(file) {
},
onCloseTagEnd(part) {
const { node } = currentTag;
const parserPlugin = node.tagDef?.parser;
const tagDef = node.tagDef;
const parserPlugin = tagDef?.parser;
if (preservingWhitespaceUntil === node) {
preservingWhitespaceUntil = undefined;
}
Expand All @@ -510,9 +532,54 @@ export function parseMarko(file) {
(hook.default || hook)(currentTag, t);
}

currentTag = currentTag.parentPath.parentPath;
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 = [];
for (const child of node.body.body) {
if (
t.isMarkoScriptlet(child) ||
(isControlFlow &&
(t.isMarkoComment(child) ||
(child.tagDef?.controlFlow && !child.body.body.length)))
) {
// 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.
attributeTags.push(child);
} else {
body.push(child);
}
}

if (isControlFlow && body.length) {
onNext();
throw file.buildCodeFrameError(
body[0],
"Cannot have attribute tags and body content under a control flow tag.",
);
} else {
node.body.body = body;
}

attributeTags.sort(sortByStart);
}

if (isControlFlow) {
currentTag.remove();
parentTag.pushContainer("attributeTags", node);
}
}

if (currentTag) {
if (parentTag) {
currentTag = parentTag;
currentBody = currentTag.get("body");
} else {
currentTag = currentBody = file.path;
Expand All @@ -534,3 +601,7 @@ export function parseMarko(file) {
end: positionAt(ast.end),
};
}

function sortByStart(a, b) {
return a.start - b.start;
}
72 changes: 67 additions & 5 deletions packages/compiler/src/babel-types/generator/patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Object.assign(Printer.prototype, {
},
MarkoPlaceholder(node, parent) {
if (parent) {
const parentBody = parent.body;
const parentBody = getBody(parent);
const prev = parentBody[parentBody.indexOf(node) - 1];

if (prev && (t.isMarkoText(prev) || t.isMarkoPlaceholder(prev))) {
Expand All @@ -45,7 +45,7 @@ Object.assign(Printer.prototype, {
MarkoScriptlet(node, parent) {
this.removeTrailingNewline();

if (!(t.isProgram(parent) && parent.body.indexOf(node) === 0)) {
if (!(t.isProgram(parent) && getBody(parent)[0] === node)) {
this.token("\n");
}

Expand Down Expand Up @@ -109,7 +109,7 @@ Object.assign(Printer.prototype, {
printWithParansIfNeeded.call(this, node.value);
},
MarkoText(node, parent) {
const parentBody = parent.body;
const parentBody = getBody(parent);
const prev = parentBody[parentBody.indexOf(node) - 1];
const concatToPrev = prev && t.isMarkoPlaceholder(prev);
let { value } = node;
Expand Down Expand Up @@ -213,14 +213,14 @@ Object.assign(Printer.prototype, {
if (SELF_CLOSING.voidElements.includes(tagName)) {
this.token(">");
} else if (
!node.body.body.length ||
!(node.body.body.length || node.attributeTags.length) ||
SELF_CLOSING.svgElements.includes(tagName)
) {
this.token("/>");
} else {
this.token(">");
this.newline();
this.print(node.body);
this.printSequence(zipAttributeTagsAndBody(node), { indent: true });
this.token("</");
if (!isDynamicTag) {
this.token(tagName);
Expand Down Expand Up @@ -270,3 +270,65 @@ function statementCouldHaveUnenclosedNewline(node) {
return false;
}
}

function zipAttributeTagsAndBody(tag) {
const {
attributeTags,
body: { body },
} = tag;
const bodyLen = body.length;
const attributeTagsLen = attributeTags.length;
if (!attributeTagsLen) return body;
if (!bodyLen) return attributeTags;

const result = [];
let i = 0;
let j = 0;

while (i < bodyLen && j < attributeTagsLen) {
const bodyNode = body[i];
const attributeTag = attributeTags[j];

if (bodyNode.loc != null && attributeTag.loc != null) {
if (compareStartLoc(bodyNode, attributeTag) < 0) {
result.push(bodyNode);
i++;
} else {
result.push(attributeTag);
j++;
}
} else if (j < attributeTagsLen) {
result.push(attributeTag);
j++;
} else {
result.push(bodyNode);
i++;
}

Check warning on line 306 in packages/compiler/src/babel-types/generator/patch.js

View check run for this annotation

Codecov / codecov/patch

packages/compiler/src/babel-types/generator/patch.js#L301-L306

Added lines #L301 - L306 were not covered by tests
}

while (j < attributeTagsLen) {
result.push(attributeTags[j++]);
}

while (i < bodyLen) {
result.push(body[i++]);
}

Check warning on line 315 in packages/compiler/src/babel-types/generator/patch.js

View check run for this annotation

Codecov / codecov/patch

packages/compiler/src/babel-types/generator/patch.js#L314-L315

Added lines #L314 - L315 were not covered by tests

return result;
}

function getBody(parent) {
switch (parent.type) {
case "MarkoTag":
return parent.body.body;
default:
return parent.body;
}
}

function compareStartLoc(a, b) {
return (
a.loc.start.line - b.loc.start.line ||
a.loc.start.column - b.loc.start.column

Check warning on line 332 in packages/compiler/src/babel-types/generator/patch.js

View check run for this annotation

Codecov / codecov/patch

packages/compiler/src/babel-types/generator/patch.js#L332

Added line #L332 was not covered by tests
);
}
Loading

0 comments on commit be9a9ef

Please sign in to comment.