diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/HtmlEntitiesInAttributes/TestComponent.cshtml b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/HtmlEntitiesInAttributes/TestComponent.cshtml new file mode 100644 index 00000000000..465673e10a7 --- /dev/null +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/HtmlEntitiesInAttributes/TestComponent.cshtml @@ -0,0 +1,3 @@ +

A

+

B @DateTime.Now

+
Test
diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentMarkupEncodingPass.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentMarkupEncodingPass.cs index f338c6b1e3f..590baa56471 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentMarkupEncodingPass.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentMarkupEncodingPass.cs @@ -156,6 +156,47 @@ public override void VisitHtml(HtmlContentIntermediateNode node) } } + public override void VisitHtmlAttributeValue(HtmlAttributeValueIntermediateNode node) + { + // Decode HTML entities in attribute values to ensure consistency between + // static attributes (e.g.,

) and + // mixed attributes (e.g.,

). + // + // For static string literals, we decode entities at compile time. + // This matches the behavior when the HTML parser processes pure static markup. + // + // Note: We don't skip encoding based on _avoidEncodingContent like VisitHtml does, + // because attribute values don't use the HasEncodedContent flag and are always + // written as string literals in the generated code. The entities should always be + // decoded at compile time for consistency. + + foreach (var child in node.Children) + { + if (child is not HtmlIntermediateToken token || token.Content.IsNullOrEmpty()) + { + // We only care about Html tokens. + continue; + } + + // Check if there are any ampersands (potential entities) + if (token.Content.IndexOf('&') >= 0) + { + // Try to decode HTML entities + if (TryDecodeHtmlEntities(token.Content.AsMemory(), out var decoded)) + { + token.UpdateContent(decoded); + } + // If decoding fails (invalid/unknown entity), keep the original content. + // This differs from VisitHtml which sets node.HasEncodedContent=true on failure. + // HtmlAttributeValueIntermediateNode doesn't have a HasEncodedContent property + // because attributes are always output as string literals in the generated code. + } + } + + // Continue walking the tree + base.VisitHtmlAttributeValue(node); + } + private static bool TryDecodeHtmlEntities(ReadOnlyMemory content, [NotNullWhen(true)] out string? decoded) { decoded = null;