diff --git a/.changeset/chilly-plants-attack.md b/.changeset/chilly-plants-attack.md new file mode 100644 index 000000000..767f50605 --- /dev/null +++ b/.changeset/chilly-plants-attack.md @@ -0,0 +1,5 @@ +--- +"marko": patch +--- + +Move body tag transform logic into translate. diff --git a/.changeset/fancy-mammals-punch.md b/.changeset/fancy-mammals-punch.md new file mode 100644 index 000000000..64e3f959c --- /dev/null +++ b/.changeset/fancy-mammals-punch.md @@ -0,0 +1,5 @@ +--- +"@marko/compiler": patch +--- + +Default title tag to be text only for Marko 6. diff --git a/.changeset/funky-clowns-shop.md b/.changeset/funky-clowns-shop.md new file mode 100644 index 000000000..18b397ac8 --- /dev/null +++ b/.changeset/funky-clowns-shop.md @@ -0,0 +1,5 @@ +--- +"@marko/runtime-tags": patch +--- + +Move title tag logic into native tag translator. diff --git a/.changeset/lemon-carpets-hunt.md b/.changeset/lemon-carpets-hunt.md new file mode 100644 index 000000000..00a8cc1e5 --- /dev/null +++ b/.changeset/lemon-carpets-hunt.md @@ -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. diff --git a/.changeset/stale-buttons-lead.md b/.changeset/stale-buttons-lead.md new file mode 100644 index 000000000..b1c616377 --- /dev/null +++ b/.changeset/stale-buttons-lead.md @@ -0,0 +1,5 @@ +--- +"marko": patch +--- + +Merge migrate taglib into core taglib. diff --git a/packages/compiler/src/taglib/index.js b/packages/compiler/src/taglib/index.js index d6b580ce9..9b046ce56 100644 --- a/packages/compiler/src/taglib/index.js +++ b/packages/compiler/src/taglib/index.js @@ -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); diff --git a/packages/compiler/src/taglib/marko-html.json b/packages/compiler/src/taglib/marko-html.json index 9c9d6424a..399ad7b1b 100644 --- a/packages/compiler/src/taglib/marko-html.json +++ b/packages/compiler/src/taglib/marko-html.json @@ -873,7 +873,10 @@ }, "": { "html": true, - "attribute-groups": ["html-attributes"] + "attribute-groups": ["html-attributes"], + "parse-options": { + "text": true + } }, "<tr>": { "html": true, diff --git a/packages/runtime-class/src/translator/tag/index.js b/packages/runtime-class/src/translator/tag/index.js index 46e873055..a282c2866 100644 --- a/packages/runtime-class/src/translator/tag/index.js +++ b/packages/runtime-class/src/translator/tag/index.js @@ -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); } diff --git a/packages/runtime-class/src/translator/taglib/core/index.js b/packages/runtime-class/src/translator/taglib/core/index.js index cd095a4ee..b3abf8ac7 100644 --- a/packages/runtime-class/src/translator/taglib/core/index.js +++ b/packages/runtime-class/src/translator/taglib/core/index.js @@ -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", diff --git a/packages/runtime-class/src/translator/taglib/migrate/all-templates.js b/packages/runtime-class/src/translator/taglib/core/migrate.js similarity index 100% rename from packages/runtime-class/src/translator/taglib/migrate/all-templates.js rename to packages/runtime-class/src/translator/taglib/core/migrate.js diff --git a/packages/runtime-class/src/translator/taglib/core/parse-static.js b/packages/runtime-class/src/translator/taglib/core/parse-static.js index f826bc8fc..0946427fc 100644 --- a/packages/runtime-class/src/translator/taglib/core/parse-static.js +++ b/packages/runtime-class/src/translator/taglib/core/parse-static.js @@ -14,5 +14,5 @@ export default function (path) { body = body[0].body; } - path.replaceWith(t.MarkoScriptlet(body, true)); + path.replaceWith(t.markoScriptlet(body, true)); } diff --git a/packages/runtime-class/src/translator/taglib/index.js b/packages/runtime-class/src/translator/taglib/index.js index 6d553a04c..e5c4cacd1 100644 --- a/packages/runtime-class/src/translator/taglib/index.js +++ b/packages/runtime-class/src/translator/taglib/index.js @@ -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], ]; diff --git a/packages/runtime-class/src/translator/taglib/migrate/index.js b/packages/runtime-class/src/translator/taglib/migrate/index.js deleted file mode 100644 index a0e30f289..000000000 --- a/packages/runtime-class/src/translator/taglib/migrate/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import * as migrateAllTemplates from "./all-templates"; -export default { - "taglib-id": "marko-default-migrate", - migrator: migrateAllTemplates, -}; diff --git a/packages/runtime-class/test/translator/fixtures/doctype/snapshots/vdomProduction-expected.js b/packages/runtime-class/test/translator/fixtures/doctype/snapshots/vdomProduction-expected.js index 96d9a3f65..ad3f8a78a 100644 --- a/packages/runtime-class/test/translator/fixtures/doctype/snapshots/vdomProduction-expected.js +++ b/packages/runtime-class/test/translator/fixtures/doctype/snapshots/vdomProduction-expected.js @@ -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 diff --git a/packages/runtime-tags/src/translator/core/index.ts b/packages/runtime-tags/src/translator/core/index.ts index 6e83f4ef8..8ce88937f 100644 --- a/packages/runtime-tags/src/translator/core/index.ts +++ b/packages/runtime-tags/src/translator/core/index.ts @@ -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, }; diff --git a/packages/runtime-tags/src/translator/core/title.ts b/packages/runtime-tags/src/translator/core/title.ts deleted file mode 100644 index 9bd2adc89..000000000 --- a/packages/runtime-tags/src/translator/core/title.ts +++ /dev/null @@ -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``; - - 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 = {}; - const { attributes } = tag; - const maybeStaticAttrs = new Set(); - 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, - }; -} diff --git a/packages/runtime-tags/src/translator/index.ts b/packages/runtime-tags/src/translator/index.ts index 852282ce1..e6576adcb 100644 --- a/packages/runtime-tags/src/translator/index.ts +++ b/packages/runtime-tags/src/translator/index.ts @@ -37,7 +37,7 @@ export const preferAPI = "tags"; export const { transform, analyze, translate } = visitors; export const taglibs = [ [ - __dirname, + coreTagLib.taglibId, { ...coreTagLib, migrate: visitors.migrate, diff --git a/packages/runtime-tags/src/translator/util/is-core-tag.ts b/packages/runtime-tags/src/translator/util/is-core-tag.ts index ddce1f3bf..10a3122b8 100644 --- a/packages/runtime-tags/src/translator/util/is-core-tag.ts +++ b/packages/runtime-tags/src/translator/util/is-core-tag.ts @@ -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) { diff --git a/packages/runtime-tags/src/translator/util/is-non-html-text.ts b/packages/runtime-tags/src/translator/util/is-non-html-text.ts index edb0764aa..da8c6455f 100644 --- a/packages/runtime-tags/src/translator/util/is-non-html-text.ts +++ b/packages/runtime-tags/src/translator/util/is-non-html-text.ts @@ -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); + 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) { + 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) + ); +} diff --git a/packages/runtime-tags/src/translator/util/sections.ts b/packages/runtime-tags/src/translator/util/sections.ts index 6d0bc9293..570db5da4 100644 --- a/packages/runtime-tags/src/translator/util/sections.ts +++ b/packages/runtime-tags/src/translator/util/sections.ts @@ -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) { case "html-comment": case "html-script": case "html-style": - case "title": return true; default: return false; diff --git a/packages/runtime-tags/src/translator/visitors/tag/native-tag.ts b/packages/runtime-tags/src/translator/visitors/tag/native-tag.ts index 323bb0bb0..04d130e4f 100644 --- a/packages/runtime-tags/src/translator/visitors/tag/native-tag.ts +++ b/packages/runtime-tags/src/translator/visitors/tag/native-tag.ts @@ -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 = {}; const { attributes } = tag.node; let hasDynamicAttributes = false; let hasEventHandlers = false; let relatedControllable: RelatedControllable; let spreadReferenceNodes: t.Node[] | undefined; - let attrExprExtras: Opt; + let exprExtras: Opt; 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)``; + write``; } 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"), - )``; + write``; } // 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)``; }