From 266572aaf5ec05ff3ba9699fa46dc7d2eeba5090 Mon Sep 17 00:00:00 2001 From: Pavel Bolekhan Date: Wed, 3 Jun 2026 11:03:12 +0000 Subject: [PATCH 1/2] Add Attributes dictionary to Reference for arbitrary tag attributes Adds IReadOnlyDictionary Attributes to Reference, populated with all parsed tag attributes. Enables consumers to read custom data-* attributes (e.g. data-sync-block-id). Constructor signatures are backward-compatible via optional parameter on the public Reference type. Co-Authored-By: Claude Sonnet 4.6 --- .../Html/Reference.cs | 10 +++- Engine/Quokka.Core/Html/Nodes/LinkBlock.cs | 7 ++- .../Html/Visitors/Html/HtmlBlockVisitor.cs | 13 +++-- .../Quokka.Tests/Helpers/ReferencesAssert.cs | 18 +++++++ .../Html/HtmlReferenceDiscoveryTests.cs | 54 +++++++++++++++++++ 5 files changed, 95 insertions(+), 7 deletions(-) diff --git a/Engine/Mindbox.Quokka.Abstractions/Html/Reference.cs b/Engine/Mindbox.Quokka.Abstractions/Html/Reference.cs index 004d09b..1030c7b 100644 --- a/Engine/Mindbox.Quokka.Abstractions/Html/Reference.cs +++ b/Engine/Mindbox.Quokka.Abstractions/Html/Reference.cs @@ -13,6 +13,7 @@ // // limitations under the License. using System; +using System.Collections.Generic; namespace Mindbox.Quokka.Html { @@ -22,13 +23,20 @@ public class Reference public string RedirectUrl { get; } public string Name { get; } public bool IsConstant { get; } + public IReadOnlyDictionary Attributes { get; } - public Reference(string redirectUrl, string name, Guid uniqueKey, bool isConstant) + public Reference( + string redirectUrl, + string name, + Guid uniqueKey, + bool isConstant, + IReadOnlyDictionary attributes = null) { RedirectUrl = redirectUrl; Name = name; UniqueKey = uniqueKey; IsConstant = isConstant; + Attributes = attributes ?? new Dictionary(StringComparer.InvariantCultureIgnoreCase); } } } diff --git a/Engine/Quokka.Core/Html/Nodes/LinkBlock.cs b/Engine/Quokka.Core/Html/Nodes/LinkBlock.cs index 49db010..427dd33 100644 --- a/Engine/Quokka.Core/Html/Nodes/LinkBlock.cs +++ b/Engine/Quokka.Core/Html/Nodes/LinkBlock.cs @@ -27,13 +27,15 @@ internal class LinkBlock : TemplateNodeBase, IStaticBlockPart private readonly AttributeValue hrefValue; private readonly AttributeValue nameValue; + private readonly IReadOnlyDictionary attributes; private readonly Guid uniqueKey; - public LinkBlock(AttributeValue hrefValue, AttributeValue nameValue) + public LinkBlock(AttributeValue hrefValue, AttributeValue nameValue, IReadOnlyDictionary attributes) { this.hrefValue = hrefValue; this.nameValue = nameValue; + this.attributes = attributes; uniqueKey = Guid.NewGuid(); } @@ -85,7 +87,8 @@ public override void CompileGrammarSpecificData(GrammarSpecificDataAnalysisConte hrefValue.Text, nameValue?.Text, uniqueKey, - isConstant: !hrefValue.TextComponents.OfType().Any())); + isConstant: !hrefValue.TextComponents.OfType().Any(), + attributes: attributes)); } public override void Accept(ITemplateVisitor treeVisitor) diff --git a/Engine/Quokka.Core/Html/Visitors/Html/HtmlBlockVisitor.cs b/Engine/Quokka.Core/Html/Visitors/Html/HtmlBlockVisitor.cs index d195b3f..c67625b 100644 --- a/Engine/Quokka.Core/Html/Visitors/Html/HtmlBlockVisitor.cs +++ b/Engine/Quokka.Core/Html/Visitors/Html/HtmlBlockVisitor.cs @@ -66,22 +66,27 @@ public override IStaticBlockPart VisitClosingTag(QuokkaHtml.ClosingTagContext co private IStaticBlockPart TryGetLinkNodeFromTagAttributes(IEnumerable attributes) { - var hrefAttributeValueVisitor = new AttributeValueVisitor(ParsingContext); + var attributeValueVisitor = new AttributeValueVisitor(ParsingContext); AttributeValue hrefValue = null; AttributeValue nameValue = null; + var allAttributes = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (var attribute in attributes) { var attributeName = attribute.TAG_NAME().GetText(); + var attributeValue = attribute.attributeValue()?.Accept(attributeValueVisitor); + if (attributeName.Equals("href", StringComparison.InvariantCultureIgnoreCase)) - hrefValue = attribute.attributeValue()?.Accept(hrefAttributeValueVisitor); + hrefValue = attributeValue; if (attributeName.Equals("data-name", StringComparison.InvariantCultureIgnoreCase)) - nameValue = attribute.attributeValue()?.Accept(hrefAttributeValueVisitor); + nameValue = attributeValue; + + allAttributes[attributeName] = attributeValue?.Text ?? string.Empty; } return hrefValue != null - ? new LinkBlock(hrefValue, nameValue) + ? new LinkBlock(hrefValue, nameValue, allAttributes) : null; } } diff --git a/Engine/Quokka.Tests/Helpers/ReferencesAssert.cs b/Engine/Quokka.Tests/Helpers/ReferencesAssert.cs index 7c3ef44..42ec3f5 100644 --- a/Engine/Quokka.Tests/Helpers/ReferencesAssert.cs +++ b/Engine/Quokka.Tests/Helpers/ReferencesAssert.cs @@ -40,5 +40,23 @@ public static void AreCollectionsEquivalent(IEnumerable expected, IRe Assert.AreEqual(expectedElement.IsConstant, actualElement.IsConstant); } } + + public static void ContainsAttribute(Reference reference, string attributeName, string expectedValue) + { + Assert.IsTrue( + reference.Attributes.ContainsKey(attributeName), + $"Reference does not contain attribute '{attributeName}'"); + Assert.AreEqual( + expectedValue, + reference.Attributes[attributeName], + $"Attribute '{attributeName}' has unexpected value"); + } + + public static void DoesNotContainAttribute(Reference reference, string attributeName) + { + Assert.IsFalse( + reference.Attributes.ContainsKey(attributeName), + $"Reference unexpectedly contains attribute '{attributeName}'"); + } } } diff --git a/Engine/Quokka.Tests/Html/HtmlReferenceDiscoveryTests.cs b/Engine/Quokka.Tests/Html/HtmlReferenceDiscoveryTests.cs index 02678d2..fcf492d 100644 --- a/Engine/Quokka.Tests/Html/HtmlReferenceDiscoveryTests.cs +++ b/Engine/Quokka.Tests/Html/HtmlReferenceDiscoveryTests.cs @@ -383,5 +383,59 @@ public void Html_ReferenceDiscovery_AHref_HtmlEncoded() }, references); } + + [TestMethod] + public void Html_ReferenceDiscovery_AHref_SingleDataAttribute_PopulatesAttributes() + { + var template = new HtmlTemplate( + "Test"); + var references = template.GetReferences(); + + Assert.AreEqual(1, references.Count); + ReferencesAssert.ContainsAttribute(references[0], "data-custom", "value-42"); + } + + [TestMethod] + public void Html_ReferenceDiscovery_AHref_MultipleDataAttributes_AllPopulated() + { + var template = new HtmlTemplate( + "Test"); + var references = template.GetReferences(); + + Assert.AreEqual(1, references.Count); + ReferencesAssert.ContainsAttribute(references[0], "data-foo", "bar"); + ReferencesAssert.ContainsAttribute(references[0], "data-name", "Link name"); + } + + [TestMethod] + public void Html_ReferenceDiscovery_AHref_AttributeWithoutValue_EmptyString() + { + var template = new HtmlTemplate( + "Test"); + var references = template.GetReferences(); + + Assert.AreEqual(1, references.Count); + ReferencesAssert.ContainsAttribute(references[0], "data-custom", string.Empty); + } + + [TestMethod] + public void Html_ReferenceDiscovery_AHref_NoExtraAttributes_AttributesEmpty() + { + var template = new HtmlTemplate("Test"); + var references = template.GetReferences(); + + Assert.AreEqual(1, references.Count); + ReferencesAssert.DoesNotContainAttribute(references[0], "data-custom"); + } + + [TestMethod] + public void Html_ReferenceDiscovery_AHref_HrefIsIncludedInAttributes() + { + var template = new HtmlTemplate("Test"); + var references = template.GetReferences(); + + Assert.AreEqual(1, references.Count); + ReferencesAssert.ContainsAttribute(references[0], "href", "http://example.com"); + } } } From 693bb0111c08d402389ea6fd0844a7065411a5e2 Mon Sep 17 00:00:00 2001 From: Pavel Bolekhan Date: Wed, 3 Jun 2026 11:27:30 +0000 Subject: [PATCH 2/2] Bump version to 8.3.0 Co-Authored-By: Claude Sonnet 4.6 --- Engine/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Engine/Directory.Build.props b/Engine/Directory.Build.props index 5df47ec..b0e18f5 100644 --- a/Engine/Directory.Build.props +++ b/Engine/Directory.Build.props @@ -16,6 +16,6 @@ - 8.2.0 + 8.3.0