fix: native translate improvements and taglib cleanup

This commit is contained in:
dpiercey 2025-12-04 09:00:01 -07:00 committed by Dylan Piercey
parent 17c4ba7edf
commit fdc46fb3e7
21 changed files with 167 additions and 483 deletions

View File

@ -0,0 +1,5 @@
---
"marko": patch
---
Move body tag transform logic into translate.

View File

@ -0,0 +1,5 @@
---
"@marko/compiler": patch
---
Default title tag to be text only for Marko 6.

View File

@ -0,0 +1,5 @@
---
"@marko/runtime-tags": patch
---
Move title tag logic into native tag translator.

View File

@ -0,0 +1,7 @@
---
"marko": patch
"@marko/runtime-tags": patch
"@marko/compiler": patch
---
Normalize taglib ids to be consistent with register ids and across Marko 5/6.

View File

@ -0,0 +1,5 @@
---
"marko": patch
---
Merge migrate taglib into core taglib.

View File

@ -19,9 +19,9 @@ const registeredTaglibs = [];
const loadedTranslatorsTaglibs = new Map();
let lookupCache = Object.create(null);
register("marko/html", markoHTMLTaglib);
register("marko/svg", markoSVGTaglib);
register("marko/math", markoMathTaglib);
register(markoHTMLTaglib["taglib-id"], markoHTMLTaglib);
register(markoSVGTaglib["taglib-id"], markoSVGTaglib);
register(markoMathTaglib["taglib-id"], markoMathTaglib);
export function buildLookup(dirname, requestedTranslator, onError) {
const translator = tryLoadTranslator(requestedTranslator);

View File

@ -873,7 +873,10 @@
},
"<title>": {
"html": true,
"attribute-groups": ["html-attributes"]
"attribute-groups": ["html-attributes"],
"parse-options": {
"text": true
}
},
"<tr>": {
"html": true,

View File

@ -57,7 +57,29 @@ export default {
moveIgnoredAttrTags(path);
}
if (isDynamicTag(path) || !(isMacroTag(path) || isNativeTag(path))) {
if (isNativeTag(path)) {
if (tagDef && tagDef.name === "body") {
path
.get("body")
.pushContainer("body", [
t.markoTag(
t.stringLiteral("init-components"),
[],
t.markoTagBody(),
),
t.markoTag(
t.stringLiteral("await-reorderer"),
[],
t.markoTagBody(),
),
t.markoTag(
t.stringLiteral("_preferred-script-location"),
[],
t.markoTagBody(),
),
]);
}
} else if (!isMacroTag(path)) {
analyzeAttributeTags(path);
}

View File

@ -3,6 +3,7 @@ import * as translateElseIf from "./conditional/translate-else-if";
import * as translateIf from "./conditional/translate-if";
import * as parseMacro from "./macro/parse";
import * as translateMacro from "./macro/translate";
import migrate from "./migrate";
import * as parseClass from "./parse-class";
import * as parseExport from "./parse-export";
import * as parseImport from "./parse-import";
@ -18,7 +19,8 @@ import * as translateServerOnly from "./translate-server-only";
import * as translateWhile from "./translate-while";
export default {
"taglib-id": "marko-default-core",
taglibId: "marko-core",
migrate,
"<import>": {
"node-factory": parseImport,
"parse-options": {
@ -285,9 +287,6 @@ export default {
"code-generator": translateServerOnly,
renderer: "marko/src/core-tags/components/preferred-script-location-tag.js",
},
"<body>": {
transformer: transformBody,
},
"<await>": {
renderer: "marko/src/core-tags/core/await/renderer.js",
types: "marko/src/core-tags/core/await/index.d.marko",

View File

@ -14,5 +14,5 @@ export default function (path) {
body = body[0].body;
}
path.replaceWith(t.MarkoScriptlet(body, true));
path.replaceWith(t.markoScriptlet(body, true));
}

View File

@ -1,7 +1,6 @@
import coreTaglib from "./core";
import migrateTaglib from "./migrate";
export const optionalTaglibs = ["marko-widgets", "@marko/compat-v4"];
export default [
["marko/core", coreTaglib],
["marko/migrate", migrateTaglib],
["marko-html-title", { "<title>": { parseOptions: { text: false } } }], // In Marko 5 the title tag parses as html even though only text is really allowed.
[coreTaglib.taglibId, coreTaglib],
];

View File

@ -1,5 +0,0 @@
import * as migrateAllTemplates from "./all-templates";
export default {
"taglib-id": "marko-default-migrate",
migrator: migrateAllTemplates,
};

View File

@ -3,18 +3,13 @@ const _marko_componentType = "M__dLOJ",
_marko_template = _t(_marko_componentType);
export default _marko_template;
import _marko_constElement from "marko/dist/runtime/vdom/helpers/const-element.js";
const _marko_node = _marko_constElement("head", null, 1).e("title", null, 1).t("Title of the document");
const _marko_node = _marko_constElement("html", null, 2).e("head", null, 1).e("title", null, 1).t("Title of the document").e("body", null, 1).t("The content of the document......");
import _marko_renderer from "marko/dist/runtime/components/renderer.js";
import { r as _marko_registerComponent } from "marko/dist/runtime/components/registry.js";
_marko_registerComponent(_marko_componentType, () => _marko_template);
const _marko_component = {};
_marko_template._ = _marko_renderer(function (input, out, _componentDef, _component, state, $global) {
out.be("html", null, "0", _component, null, 0);
out.n(_marko_node, _component);
out.be("body", null, "3", _component, null, 0);
out.t("The content of the document......", _component);
out.ee();
out.ee();
}, {
t: _marko_componentType,
i: true

View File

@ -22,7 +22,6 @@ import ScriptTag from "./script";
import ServerTag from "./server";
import StaticTag from "./static";
import StyleTag from "./style";
import TitleTag from "./title";
import TryTag from "./try";
export default {
@ -52,6 +51,5 @@ export default {
"<server>": ServerTag,
"<static>": StaticTag,
"<style>": StyleTag,
"<title>": TitleTag,
"<try>": TryTag,
};

View File

@ -1,421 +0,0 @@
// TODO: this shares a bunch of logic with the native tag translator.
// we should probably attempt to share that logic where possible.
// Also need to ensure it stays in sync.
import { types as t } from "@marko/compiler";
import {
assertNoArgs,
assertNoParams,
getProgram,
type Tag,
} from "@marko/compiler/babel-utils";
import { getEventHandlerName, isEventHandler } from "../../common/helpers";
import { WalkCode } from "../../common/types";
import { bodyToTextLiteral } from "../util/body-to-text-literal";
import evaluate from "../util/evaluate";
import { isOutputHTML } from "../util/marko-config";
import { type Opt, push } from "../util/optional";
import {
type Binding,
BindingType,
createBinding,
dropReferences,
getScopeAccessorLiteral,
mergeReferences,
trackDomVarReferences,
} from "../util/references";
import { callRuntime, getHTMLRuntime } from "../util/runtime";
import { createScopeReadExpression } from "../util/scope-read";
import {
getOrCreateSection,
getScopeIdIdentifier,
getSection,
} from "../util/sections";
import {
addSerializeExpr,
getSerializeReason,
} from "../util/serialize-reasons";
import { addHTMLEffectCall, addStatement } from "../util/signals";
import { toObjectProperty } from "../util/to-property-name";
import { propsToExpression } from "../util/translate-attrs";
import { translateDomVar } from "../util/translate-var";
import * as walks from "../util/walks";
import * as writer from "../util/writer";
import { scopeIdentifier } from "../visitors/program";
const kNodeBinding = Symbol("title tag node binding");
declare module "@marko/compiler/dist/types" {
export interface NodeExtra {
[kNodeBinding]?: Binding;
}
}
export default {
analyze(tag) {
assertNoArgs(tag);
assertNoParams(tag);
const { node } = tag;
if (node.var && !t.isIdentifier(node.var)) {
throw tag
.get("var")
.buildCodeFrameError(
"Tag variables on native elements cannot be destructured.",
);
}
const seen: Record<string, t.MarkoAttribute> = {};
const { attributes } = tag.node;
let spreadReferenceNodes: t.Node[] | undefined;
let exprExtras: Opt<t.NodeExtra>;
let hasEventHandlers = false;
let hasDynamicAttributes = false;
for (let i = attributes.length; i--; ) {
const attr = attributes[i];
const valueExtra = (attr.value.extra ??= {});
if (t.isMarkoAttribute(attr)) {
if (seen[attr.name]) {
// drop references for duplicated attributes.
dropReferences(attr.value);
continue;
}
seen[attr.name] = attr;
if (isEventHandler(attr.name)) {
valueExtra.isEffect = true;
hasEventHandlers = true;
} else if (!evaluate(attr.value).confident) {
hasDynamicAttributes = true;
}
} else if (t.isMarkoSpreadAttribute(attr)) {
valueExtra.isEffect = true;
hasEventHandlers = true;
hasDynamicAttributes = true;
}
if (spreadReferenceNodes) {
spreadReferenceNodes.push(attr.value);
} else if (t.isMarkoSpreadAttribute(attr)) {
spreadReferenceNodes = [attr.value];
} else {
exprExtras = push(exprExtras, valueExtra);
}
}
const bodyPlaceholderNodes: t.Node[] = [];
let hasBodyPlaceholders = false;
for (const child of tag.node.body.body) {
if (t.isMarkoPlaceholder(child)) {
bodyPlaceholderNodes.push(child.value);
hasBodyPlaceholders = true;
} else if (!t.isMarkoText(child)) {
throw tag.hub.buildError(
child,
"Invalid child. Only text is allowed inside a `<title>`.",
);
}
}
if (
node.var ||
hasEventHandlers ||
hasDynamicAttributes ||
hasBodyPlaceholders
) {
const tagExtra = (node.extra ??= {});
const tagSection = getOrCreateSection(tag);
const nodeBinding = (tagExtra[kNodeBinding] = createBinding(
"#title",
BindingType.dom,
tagSection,
));
if (hasEventHandlers) {
getProgram().node.extra.isInteractive = true;
}
if (spreadReferenceNodes) {
mergeReferences(tagSection, tag.node, spreadReferenceNodes);
}
if (hasBodyPlaceholders) {
exprExtras = push(
exprExtras,
bodyPlaceholderNodes.length === 1
? (bodyPlaceholderNodes[0].extra ??= {})
: mergeReferences(
tagSection,
bodyPlaceholderNodes[0],
bodyPlaceholderNodes.slice(1),
),
);
}
trackDomVarReferences(tag, nodeBinding);
addSerializeExpr(
tagSection,
!!(node.var || hasEventHandlers),
nodeBinding,
);
addSerializeExpr(tagSection, push(exprExtras, tagExtra), nodeBinding);
}
},
translate: {
enter(tag) {
const tagExtra = tag.node.extra!;
const nodeBinding = tagExtra[kNodeBinding];
const isHTML = isOutputHTML();
const write = writer.writeTo(tag);
const tagSection = getSection(tag);
if (isHTML) {
translateDomVar(tag, nodeBinding);
}
if (nodeBinding) {
walks.visit(tag, WalkCode.Get);
}
write`<title`;
const usedAttrs = getUsedAttrs(tag.node);
const { staticAttrs, skipExpression, spreadExpression } = usedAttrs;
for (const attr of staticAttrs) {
const { name, value } = attr;
const { confident, computed } = value.extra || {};
const valueReferences = value.extra?.referencedBindings;
switch (name) {
case "class":
case "style": {
const helper = `_attr_${name}` as const;
if (confident) {
write`${getHTMLRuntime()[helper](computed)}`;
} else if (isHTML) {
write`${callRuntime(helper, value)}`;
} else {
addStatement(
"render",
tagSection,
valueReferences,
t.expressionStatement(
callRuntime(
helper,
createScopeReadExpression(nodeBinding!),
value,
),
),
);
}
break;
}
default:
if (confident) {
write`${getHTMLRuntime()._attr(name, computed)}`;
} else if (isHTML) {
if (isEventHandler(name)) {
addHTMLEffectCall(tagSection, valueReferences);
} else {
write`${callRuntime("_attr", t.stringLiteral(name), value)}`;
}
} else if (isEventHandler(name)) {
addStatement(
"effect",
tagSection,
valueReferences,
t.expressionStatement(
callRuntime(
"_on",
createScopeReadExpression(nodeBinding!),
t.stringLiteral(getEventHandlerName(name)),
value,
),
),
);
} else {
addStatement(
"render",
tagSection,
valueReferences,
t.expressionStatement(
callRuntime(
"_attr",
createScopeReadExpression(nodeBinding!),
t.stringLiteral(name),
value,
),
),
);
}
break;
}
}
if (spreadExpression) {
const visitAccessor = getScopeAccessorLiteral(nodeBinding!);
if (isHTML) {
addHTMLEffectCall(tagSection, tagExtra.referencedBindings);
if (skipExpression) {
write`${callRuntime("_attrs_partial", spreadExpression, skipExpression, visitAccessor, getScopeIdIdentifier(tagSection), t.stringLiteral("title"))}`;
} else {
write`${callRuntime("_attrs", spreadExpression, visitAccessor, getScopeIdIdentifier(tagSection), t.stringLiteral("title"))}`;
}
} else {
if (skipExpression) {
addStatement(
"render",
tagSection,
tagExtra.referencedBindings,
t.expressionStatement(
callRuntime(
"_attrs_partial",
scopeIdentifier,
visitAccessor,
spreadExpression,
skipExpression,
),
),
);
} else {
addStatement(
"render",
tagSection,
tagExtra.referencedBindings,
t.expressionStatement(
callRuntime(
"_attrs",
scopeIdentifier,
visitAccessor,
spreadExpression,
),
),
);
}
addStatement(
"effect",
tagSection,
tagExtra.referencedBindings,
t.expressionStatement(
callRuntime("_attrs_script", scopeIdentifier, visitAccessor),
),
false,
);
}
}
write`>`;
walks.enter(tag);
},
exit(tag) {
const tagSection = getSection(tag);
const tagExtra = tag.node.extra!;
const nodeBinding = tagExtra[kNodeBinding];
const write = writer.writeTo(tag);
if (isOutputHTML()) {
for (const child of tag.node.body.body) {
if (t.isMarkoText(child)) {
write`${child.value}`;
} else if (t.isMarkoPlaceholder(child)) {
write`${callRuntime("_to_text", child.value)}`;
}
}
} else {
const textLiteral = bodyToTextLiteral(tag.node.body);
if (t.isStringLiteral(textLiteral)) {
write`${textLiteral}`;
} else {
addStatement(
"render",
getSection(tag),
textLiteral.extra?.referencedBindings,
t.expressionStatement(
callRuntime(
"_text_content",
createScopeReadExpression(nodeBinding!),
textLiteral,
),
),
);
}
}
write`</title>`;
if (nodeBinding) {
writer.markNode(
tag,
nodeBinding,
getSerializeReason(tagSection, nodeBinding),
);
}
walks.exit(tag);
tag.remove();
},
},
parseOptions: {
text: true,
},
} as Tag;
function getUsedAttrs(tag: t.MarkoTag) {
const seen: Record<string, t.MarkoAttribute> = {};
const { attributes } = tag;
const maybeStaticAttrs = new Set<t.MarkoAttribute>();
let spreadExpression: undefined | t.Expression;
let skipExpression: undefined | t.Expression;
let spreadProps: undefined | t.ObjectExpression["properties"];
let skipProps: undefined | t.ObjectExpression["properties"];
for (let i = attributes.length; i--; ) {
const attr = attributes[i];
const { value } = attr;
if (t.isMarkoSpreadAttribute(attr)) {
if (!spreadProps) {
spreadProps = [];
}
spreadProps.push(t.spreadElement(value));
} else if (!seen[attr.name]) {
seen[attr.name] = attr;
if (spreadProps) {
spreadProps.push(toObjectProperty(attr.name, attr.value));
} else {
maybeStaticAttrs.add(attr);
}
}
}
const staticAttrs = [...maybeStaticAttrs].reverse();
if (spreadProps) {
spreadProps.reverse();
for (const { name } of staticAttrs) {
(skipProps ||= []).push(toObjectProperty(name, t.numericLiteral(1)));
}
if (skipProps) {
skipExpression = t.objectExpression(skipProps);
}
spreadExpression = propsToExpression(spreadProps);
}
return {
staticAttrs,
spreadExpression,
skipExpression,
};
}

View File

@ -37,7 +37,7 @@ export const preferAPI = "tags";
export const { transform, analyze, translate } = visitors;
export const taglibs = [
[
__dirname,
coreTagLib.taglibId,
{
...coreTagLib,
migrate: visitors.migrate,

View File

@ -15,13 +15,7 @@ export function isCoreTag(
if (tagDef) {
switch (tagDef.taglibId) {
case taglibId:
return true;
case interopTaglibId:
switch (tagDef.name) {
// The body tag is registered in the v5 translator, without this it'd be seen as a core tag.
case "body":
return false;
}
return true;
case htmlTaglibId:
switch (tagDef.name) {

View File

@ -1,4 +1,5 @@
import { types as t } from "@marko/compiler";
import { getTagDef } from "@marko/compiler/babel-utils";
import { isCoreTag } from "./is-core-tag";
@ -7,16 +8,29 @@ export function isNonHTMLText(
) {
const parentTag =
placeholder.parentPath.isMarkoTagBody() &&
placeholder.parentPath.parentPath;
if (parentTag && isCoreTag(parentTag)) {
switch (parentTag.node.name.value) {
case "html-comment":
case "html-script":
case "html-style":
case "title":
return true;
(placeholder.parentPath.parentPath as t.NodePath<t.MarkoTag>);
if (parentTag) {
if (isCoreTag(parentTag)) {
switch (parentTag.node.name.value) {
case "html-comment":
case "html-script":
case "html-style":
return true;
}
} else if (isTextOnlyNativeTag(parentTag)) {
return true;
}
}
return false;
}
export function isTextOnlyNativeTag(tag: t.NodePath<t.MarkoTag>) {
const def = getTagDef(tag);
// Have to special case `title` here for the compat with v5 which does not treat title as a text only tag.
return !!(
def &&
def.html &&
(def.name === "title" || def.parseOptions?.text)
);
}

View File

@ -289,7 +289,6 @@ export function getNodeContentType(
return ContentType.Comment;
case "html-script":
case "html-style":
case "title":
return ContentType.Tag;
case "for":
case "if":
@ -486,7 +485,6 @@ function isNativeNode(tag: t.NodePath<t.MarkoTag>) {
case "html-comment":
case "html-script":
case "html-style":
case "title":
return true;
default:
return false;

View File

@ -10,9 +10,11 @@ import {
import { assertExclusiveAttrs } from "../../../common/errors";
import { getEventHandlerName, isEventHandler } from "../../../common/helpers";
import { WalkCode } from "../../../common/types";
import { bodyToTextLiteral } from "../../util/body-to-text-literal";
import evaluate from "../../util/evaluate";
import { generateUidIdentifier } from "../../util/generate-uid";
import { getTagName } from "../../util/get-tag-name";
import { isTextOnlyNativeTag } from "../../util/is-non-html-text";
import { type Opt, push } from "../../util/optional";
import {
type Binding,
@ -81,13 +83,14 @@ export default {
}
const tagName = getTagName(tag)!;
const textOnly = isTextOnlyNativeTag(tag);
const seen: Record<string, t.MarkoAttribute> = {};
const { attributes } = tag.node;
let hasDynamicAttributes = false;
let hasEventHandlers = false;
let relatedControllable: RelatedControllable;
let spreadReferenceNodes: t.Node[] | undefined;
let attrExprExtras: Opt<t.NodeExtra>;
let exprExtras: Opt<t.NodeExtra>;
for (let i = attributes.length; i--; ) {
const attr = attributes[i];
@ -123,7 +126,7 @@ export default {
spreadReferenceNodes = [attr.value];
relatedControllable = getRelatedControllable(tagName, seen);
} else {
attrExprExtras = push(attrExprExtras, valueExtra);
exprExtras = push(exprExtras, valueExtra);
}
}
@ -131,10 +134,25 @@ export default {
throw tag.get("name").buildCodeFrameError(msg);
});
let textPlaceholders: undefined | t.Node[];
if (textOnly) {
for (const child of tag.node.body.body) {
if (t.isMarkoPlaceholder(child)) {
(textPlaceholders ||= []).push(child.value);
} else if (!t.isMarkoText(child)) {
throw tag.hub.buildError(
child,
`Only text is allowed inside a \`<${tagName}>\`.`,
);
}
}
}
if (
node.var ||
hasEventHandlers ||
hasDynamicAttributes ||
textPlaceholders ||
getRelatedControllable(tagName, seen)?.special
) {
const tagExtra = (node.extra ??= {});
@ -177,6 +195,19 @@ export default {
);
}
if (textPlaceholders) {
exprExtras = push(
exprExtras,
textPlaceholders.length === 1
? (textPlaceholders[0].extra ??= {})
: mergeReferences(
tagSection,
textPlaceholders[0],
textPlaceholders.slice(1),
),
);
}
addSerializeExpr(
tagSection,
!!(node.var || hasEventHandlers),
@ -185,11 +216,7 @@ export default {
trackDomVarReferences(tag, nodeBinding);
addSerializeExpr(
tagSection,
push(attrExprExtras, tagExtra),
nodeBinding,
);
addSerializeExpr(tagSection, push(exprExtras, tagExtra), nodeBinding);
}
},
},
@ -426,9 +453,18 @@ export default {
const tagExtra = tag.node.extra!;
const nodeBinding = tagExtra[kNativeTagBinding];
const openTagOnly = getTagDef(tag)?.parseOptions?.openTagOnly;
const textOnly = isTextOnlyNativeTag(tag);
const selectArgs = htmlSelectArgs.get(tag.node);
const tagName = getTagName(tag);
const tagSection = getSection(tag);
const markerSerializeReason =
!tagExtra[kSkipEndTag] &&
nodeBinding &&
getSerializeReason(tagSection, nodeBinding);
const write = writer.writeTo(
tag,
!markerSerializeReason && (tagName === "html" || tagName === "body"),
);
if (tagExtra[kTagContentAttr]) {
writer.flushBefore(tag);
@ -440,7 +476,7 @@ export default {
if (selectArgs) {
if (!tagExtra[kSkipEndTag]) {
writer.writeTo(tag)`</${tag.node.name}>`;
write`</${tag.node.name}>`;
}
writer.flushInto(tag);
@ -459,21 +495,20 @@ export default {
),
),
);
} else if (textOnly) {
for (const child of tag.node.body.body) {
if (t.isMarkoText(child)) {
write`${child.value}`;
} else if (t.isMarkoPlaceholder(child)) {
write`${callRuntime("_to_text", child.value)}`;
}
}
} else {
tag.insertBefore(tag.node.body.body).forEach((child) => child.skip());
}
const markerSerializeReason =
!tagExtra[kSkipEndTag] &&
nodeBinding &&
getSerializeReason(tagSection, nodeBinding);
if (!tagExtra[kSkipEndTag] && !openTagOnly && !selectArgs) {
writer.writeTo(
tag,
!markerSerializeReason &&
(tagName === "html" || tagName === "body"),
)`</${tag.node.name}>`;
write`</${tag.node.name}>`;
}
// dynamic tag stuff
@ -734,10 +769,36 @@ export default {
walks.enter(tag);
},
exit(tag) {
const tagExtra = tag.node.extra!;
const nodeBinding = tagExtra[kNativeTagBinding];
const openTagOnly = getTagDef(tag)?.parseOptions?.openTagOnly;
tag.insertBefore(tag.node.body.body).forEach((child) => child.skip());
const textOnly = isTextOnlyNativeTag(tag);
if (!openTagOnly) {
if (textOnly) {
const textLiteral = bodyToTextLiteral(tag.node.body);
if (t.isStringLiteral(textLiteral)) {
writer.writeTo(tag)`${textLiteral}`;
} else {
addStatement(
"render",
getSection(tag),
textLiteral.extra?.referencedBindings,
t.expressionStatement(
callRuntime(
"_text_content",
createScopeReadExpression(nodeBinding!),
textLiteral,
),
),
);
}
} else {
tag
.insertBefore(tag.node.body.body)
.forEach((child) => child.skip());
}
writer.writeTo(tag)`</${tag.node.name}>`;
}