diff --git a/src/HtmlAgilityPack.Shared/HtmlNode.Encapsulator.cs b/src/HtmlAgilityPack.Shared/HtmlNode.Encapsulator.cs index af4e87bd..aaa90909 100644 --- a/src/HtmlAgilityPack.Shared/HtmlNode.Encapsulator.cs +++ b/src/HtmlAgilityPack.Shared/HtmlNode.Encapsulator.cs @@ -11,7 +11,6 @@ using System.Collections; using System.Collections.Generic; using System.Reflection; -using System.Xml; using System.Xml.XPath; namespace HtmlAgilityPack @@ -201,6 +200,7 @@ public object GetEncapsulatedData(Type targetType, HtmlDocument htmlDocument = n } else // It target attribute of HTMLTag { + ThrowIfNodeReturnTypeIsExplicitlySetWhenAttributeNameIsGiven(xPathAttribute); result = htmlNode.GetAttributeValue(xPathAttribute.AttributeName, null); } @@ -329,6 +329,8 @@ public object GetEncapsulatedData(Type targetType, HtmlDocument htmlDocument = n } else // It target attribute { + ThrowIfNodeReturnTypeIsExplicitlySetWhenAttributeNameIsGiven(xPathAttribute); + foreach (HtmlNode node in nodeCollection) { string nodeAttributeValue = node.GetAttributeValue(xPathAttribute.AttributeName, null); @@ -386,6 +388,15 @@ public object GetEncapsulatedData(Type targetType, HtmlDocument htmlDocument = n } #endregion targetObject_NOTDefined_XPath } + + + private static void ThrowIfNodeReturnTypeIsExplicitlySetWhenAttributeNameIsGiven(XPathAttribute xPathAttr) + { + if (xPathAttr.IsNodeReturnTypeExplicitlySet && !string.IsNullOrEmpty(xPathAttr.AttributeName)) + { + throw new InvalidNodeReturnTypeException("Specifying a ReturnType value not allowed for XPathAttribute annotations targeting element attributes"); + } + } } @@ -749,7 +760,7 @@ internal static string GetHtmlForEncapsulation(HtmlNode node, ReturnType returnT case ReturnType.OuterHtml: return node.OuterHtml; default: - throw new IndexOutOfRangeException("Unhandled ReturnType : " + returnType.ToString()); + throw new InvalidNodeReturnTypeException(string.Format("Invalid ReturnType value {0}", returnType)); }; } } @@ -852,20 +863,6 @@ public XPathAttribute(string xpathString, string attributeName) AttributeName = attributeName; _nodeReturnType = ReturnType.InnerText; } - - - /// - /// Specify Xpath and Attribute to find related Html Node and its attribute value. - /// - /// - /// - /// Specify you want the output include html text too. - public XPathAttribute(string xpathString, string attributeName, ReturnType nodeReturnType) - { - XPath = xpathString; - AttributeName = attributeName; - NodeReturnType = nodeReturnType; - } } @@ -955,6 +952,26 @@ public MissingXPathException(string message) : base(message) { } public MissingXPathException(string message, Exception inner) : base(message, inner) { } } + + /// + /// Exception that occurs when an XPathAttribute annotation has an invalid ReturnType specified. + /// + public class InvalidNodeReturnTypeException : Exception + { + /// + /// + /// + /// + public InvalidNodeReturnTypeException(string message) + : base(message) { } + + /// + /// + /// + /// + /// + public InvalidNodeReturnTypeException(string message, Exception inner) : base(message, inner) { } + } } #if FX20 diff --git a/src/Tests/HtmlAgilityPack.Tests.NetStandard2_0/HtmlNode_GetEncapsulatedData_NodeReturnType_Tests.cs b/src/Tests/HtmlAgilityPack.Tests.NetStandard2_0/HtmlNode_GetEncapsulatedData_NodeReturnType_Tests.cs new file mode 100644 index 00000000..b0d4a68a --- /dev/null +++ b/src/Tests/HtmlAgilityPack.Tests.NetStandard2_0/HtmlNode_GetEncapsulatedData_NodeReturnType_Tests.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace HtmlAgilityPack.Tests.NetStandard2_0 +{ + public class HtmlNode_GetEncapsulatedData_NodeReturnType_Tests + { + [Theory] + [InlineData(typeof(Model_InvalidReturnTypeOnElementWithStringProperty))] + [InlineData(typeof(Model_InvalidReturnTypeAsNamedArgOnElementWithStringProperty))] + public void InvalidReturnTypeOnElementWithStringProperty(Type modelType) + { + const string html = + @"
+

Link 1

+
"; + + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + var ex = Assert.Throws( + () => doc.DocumentNode.GetEncapsulatedData(modelType) + ); + + Assert.Equal("Invalid ReturnType value 1234", ex.Message); + } + + [HasXPath] + class Model_InvalidReturnTypeOnElementWithStringProperty + { + [XPath("//div/a", (ReturnType) 1234)] + public string? StringValue { get; set; } + } + + [HasXPath] + class Model_InvalidReturnTypeAsNamedArgOnElementWithStringProperty + { + [XPath("//div/a", NodeReturnType = (ReturnType) 1234)] + public string? StringValue { get; set; } + } + + + [Fact] + public void ImplicitReturnTypeOnElementWithStringProperty() + { + const string html = + @"
+

Link 1

+
"; + + var doc = new HtmlDocument(); + doc.LoadHtml(html); + var model = doc.DocumentNode.GetEncapsulatedData(); + + Assert.NotNull(model); + Assert.Equal("Link 1", model.StringValue); + } + + [HasXPath] + class Model_ImplicitReturnTypeOnElementWithStringProperty + { + [XPath("//div/a")] + public string? StringValue { get; set; } + } + + + [Theory] + [InlineData(typeof(Model_ReturnTypeInnerTextOnElementWithStringProperty))] + [InlineData(typeof(Model_ReturnTypeInnerTextAsNamedArgOnElementWithStringProperty))] + public void ReturnTypeInnerTextOnElementWithStringProperty(Type modelType) + { + const string html = + @"
+

Link 1

+
"; + + var doc = new HtmlDocument(); + doc.LoadHtml(html); + var model = (IModelWithStringProperty) doc.DocumentNode.GetEncapsulatedData(modelType); + + Assert.NotNull(model); + Assert.Equal("Link 1", model.StringValue); + } + + [HasXPath] + class Model_ReturnTypeInnerTextOnElementWithStringProperty : IModelWithStringProperty + { + [XPath("//div/a", ReturnType.InnerText)] + public string? StringValue { get; set; } + } + + [HasXPath] + class Model_ReturnTypeInnerTextAsNamedArgOnElementWithStringProperty : IModelWithStringProperty + { + [XPath("//div/a", NodeReturnType = ReturnType.InnerText)] + public string? StringValue { get; set; } + } + + + [Theory] + [InlineData(typeof(Model_ReturnTypeInnerHTMLOnElementWithStringProperty))] + [InlineData(typeof(Model_ReturnTypeInnerHTMLAsNamedArgOnElementWithStringProperty))] + public void ReturnTypeInnerHTMLOnElementWithStringProperty(Type modelType) + { + const string html = + @"
+

Link 1

+
"; + + var doc = new HtmlDocument(); + doc.LoadHtml(html); + var model = (IModelWithStringProperty) doc.DocumentNode.GetEncapsulatedData(modelType); + + Assert.NotNull(model); + Assert.Equal("

Link 1

", model.StringValue); + } + + [HasXPath] + class Model_ReturnTypeInnerHTMLOnElementWithStringProperty : IModelWithStringProperty + { + [XPath("//div/a", ReturnType.InnerHtml)] + public string? StringValue { get; set; } + } + + [HasXPath] + class Model_ReturnTypeInnerHTMLAsNamedArgOnElementWithStringProperty : IModelWithStringProperty + { + [XPath("//div/a", NodeReturnType = ReturnType.InnerHtml)] + public string? StringValue { get; set; } + } + + + [Theory] + [InlineData(typeof(Model_ReturnTypeOuterHTMLOnElementWithStringProperty))] + [InlineData(typeof(Model_ReturnTypeOuterHTMLAsNamedArgOnElementWithStringProperty))] + public void ReturnTypeOuterHTMLOnElementWithStringProperty(Type modelType) + { + const string html = + @"
+

Link 1

+
"; + + var doc = new HtmlDocument(); + doc.LoadHtml(html); + var model = (IModelWithStringProperty) doc.DocumentNode.GetEncapsulatedData(modelType); + + Assert.NotNull(model); + Assert.Equal("

Link 1

", model.StringValue); + } + + [HasXPath] + class Model_ReturnTypeOuterHTMLOnElementWithStringProperty : IModelWithStringProperty + { + [XPath("//div/a", ReturnType.OuterHtml)] + public string? StringValue { get; set; } + } + + [HasXPath] + class Model_ReturnTypeOuterHTMLAsNamedArgOnElementWithStringProperty : IModelWithStringProperty + { + [XPath("//div/a", NodeReturnType = ReturnType.OuterHtml)] + public string? StringValue { get; set; } + } + + + [Fact] + public void ImplicitReturnTypeOnAttributeWithStringProperty() + { + const string html = + @"
+

Link 1

+
"; + + var doc = new HtmlDocument(); + doc.LoadHtml(html); + var model = doc.DocumentNode.GetEncapsulatedData(); + + Assert.NotNull(model); + Assert.Equal("link1.html", model.Value); + } + + [HasXPath] + class Model_ImplicitReturnTypeOnAttributeWithStringProperty + { + [XPath("//div/a", "href")] + public string? Value { get; set; } + } + + + [Theory] + [InlineData(typeof(Model_ExplicitReturnTypeOnAttributeWithStringProperty))] + [InlineData(typeof(Model_ExplicitInvalidReturnTypeOnAttributeWithStringProperty))] + public void ExplicitReturnTypeOnAttributeWithStringProperty(Type modelType) + { + const string html = + @"
+

Link 1

+
"; + + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + var ex = Assert.Throws( + () => doc.DocumentNode.GetEncapsulatedData(modelType) + ); + + Assert.Equal("Specifying a ReturnType value not allowed for XPathAttribute annotations targeting element attributes", ex.Message); + } + + [HasXPath] + class Model_ExplicitReturnTypeOnAttributeWithStringProperty + { + [XPath("//div/a", "href", NodeReturnType = ReturnType.InnerText)] + public string? StringValue { get; set; } + } + + [HasXPath] + class Model_ExplicitInvalidReturnTypeOnAttributeWithStringProperty + { + [XPath("//div/a", "href", NodeReturnType = (ReturnType)567)] + public string? StringValue { get; set; } + } + + + [Fact] + public void ImplicitReturnTypeOnElementSequenceWithStringListProperty() + { + const string html = + @"
+

Link 1

+

Link 2

+

Link 3

+
"; + + + var doc = new HtmlDocument(); + doc.LoadHtml(html); + var model = doc.DocumentNode.GetEncapsulatedData(); + + Assert.NotNull(model); + + string[] expected = new[] { "Link 1", "Link 2", "Link 3" }; + Assert.Equal(expected, model.StringListValue); + } + + [HasXPath] + class Model_ImplicitReturnTypeOnElementSequenceWithStringListProperty + { + [XPath("//div/a")] + public List? StringListValue { get; set; } + } + + + [Theory] + [InlineData(typeof(Model_ReturnTypeInnerTextOnElementSequenceWithStringListProperty))] + [InlineData(typeof(Model_ReturnTypeInnerTextAsNamedArgOnElementSequenceWithStringListProperty))] + public void ReturnTypeInnerTextOnElementSequenceWithStringListProperty(Type modelType) + { + const string html = + @"
+

Link 1

+

Link 2

+

Link 3

+
"; + + var doc = new HtmlDocument(); + doc.LoadHtml(html); + var model = (IModelWithStringListProperty) doc.DocumentNode.GetEncapsulatedData(modelType); + + Assert.NotNull(model); + + string[] expected = new[] { "Link 1", "Link 2", "Link 3" }; + Assert.Equal(expected, model.StringListValue); + } + + [HasXPath] + class Model_ReturnTypeInnerTextOnElementSequenceWithStringListProperty : IModelWithStringListProperty + { + [XPath("//div/a", ReturnType.InnerText)] + public List? StringListValue { get; set; } + } + + [HasXPath] + class Model_ReturnTypeInnerTextAsNamedArgOnElementSequenceWithStringListProperty : IModelWithStringListProperty + { + [XPath("//div/a", NodeReturnType = ReturnType.InnerText)] + public List? StringListValue { get; set; } + } + + + [Theory] + [InlineData(typeof(Model_ReturnTypeInnerHTMLOnElementSequenceWithStringListProperty))] + [InlineData(typeof(Model_ReturnTypeInnerHTMLAsNamedArgOnElementSequenceWithStringListProperty))] + public void ReturnTypeInnerHTMLOnElementSequenceWithStringListProperty(Type modelType) + { + const string html = + @"
+

Link 1

+

Link 2

+

Link 3

+
"; + + var doc = new HtmlDocument(); + doc.LoadHtml(html); + var model = (IModelWithStringListProperty) doc.DocumentNode.GetEncapsulatedData(modelType); + + Assert.NotNull(model); + + string[] expected = new[] + { + "

Link 1

", + "

Link 2

", + "

Link 3

" + }; + Assert.Equal(expected, model.StringListValue); + } + + [HasXPath] + class Model_ReturnTypeInnerHTMLOnElementSequenceWithStringListProperty : IModelWithStringListProperty + { + [XPath("//div/a", ReturnType.InnerHtml)] + public List? StringListValue { get; set; } + } + + [HasXPath] + class Model_ReturnTypeInnerHTMLAsNamedArgOnElementSequenceWithStringListProperty : IModelWithStringListProperty + { + [XPath("//div/a", NodeReturnType = ReturnType.InnerHtml)] + public List? StringListValue { get; set; } + } + + + [Theory] + [InlineData(typeof(Model_ReturnTypeOuterHTMLOnElementSequenceWithStringListProperty))] + [InlineData(typeof(Model_ReturnTypeOuterHTMLAsNamedArgOnElementSequenceWithStringListProperty))] + public void ReturnTypeOuterHTMLOnElementSequenceWithStringListProperty(Type modelType) + { + const string html = + @"
+

Link 1

+

Link 2

+

Link 3

+
"; + + var doc = new HtmlDocument(); + doc.LoadHtml(html); + var model = (IModelWithStringListProperty) doc.DocumentNode.GetEncapsulatedData(modelType); + + Assert.NotNull(model); + + string[] expected = new[] + { + "

Link 1

", + "

Link 2

", + "

Link 3

" + }; + Assert.Equal(expected, model.StringListValue); + } + + [HasXPath] + class Model_ReturnTypeOuterHTMLOnElementSequenceWithStringListProperty : IModelWithStringListProperty + { + [XPath("//div/a", ReturnType.OuterHtml)] + public List? StringListValue { get; set; } + } + + [HasXPath] + class Model_ReturnTypeOuterHTMLAsNamedArgOnElementSequenceWithStringListProperty : IModelWithStringListProperty + { + [XPath("//div/a", NodeReturnType = ReturnType.OuterHtml)] + public List? StringListValue { get; set; } + } + + + + interface IModelWithStringProperty + { + string? StringValue { get; set; } + } + + interface IModelWithStringListProperty + { + List? StringListValue { get; set; } + } + } +}