Implement AngleSharp.XPath breaking changes

Bump of B warranted, more in the release notes
This commit is contained in:
JustArchi 2022-08-06 18:51:32 +02:00
parent 5c6ca3fee2
commit f3229fa45f
No known key found for this signature in database
GPG key ID: 6B138B4C64555AEA
10 changed files with 64 additions and 48 deletions

View file

@ -19,12 +19,6 @@
"allowedVersions": "<= 3.1",
"matchManagers": [ "nuget" ],
"matchPackageNames": [ "Microsoft.Extensions.Configuration.Json", "Microsoft.Extensions.Logging.Configuration" ]
},
{
// TODO: https://github.com/AngleSharp/AngleSharp.XPath/issues/40
"allowedVersions": "< 2.0",
"matchManagers": [ "nuget" ],
"matchPackageNames": [ "AngleSharp.XPath" ]
}
]
}

View file

@ -189,24 +189,45 @@ public static class Utilities {
}
[PublicAPI]
public static IEnumerable<IElement> SelectNodes(this IDocument document, string xpath) {
public static IList<INode> SelectNodes(this IDocument document, string xpath) {
ArgumentNullException.ThrowIfNull(document);
return document.Body.SelectNodes(xpath).OfType<IElement>();
return document.Body.SelectNodes(xpath);
}
[PublicAPI]
public static IElement? SelectSingleElementNode(this IElement element, string xpath) {
public static IEnumerable<T> SelectNodes<T>(this IDocument document, string xpath) where T : class, INode {
ArgumentNullException.ThrowIfNull(document);
return document.SelectNodes(xpath).OfType<T>();
}
[PublicAPI]
public static IEnumerable<T> SelectNodes<T>(this IElement element, string xpath) where T : class, INode {
ArgumentNullException.ThrowIfNull(element);
return (IElement?) element.SelectSingleNode(xpath);
return element.SelectNodes(xpath).OfType<T>();
}
[PublicAPI]
public static IElement? SelectSingleNode(this IDocument document, string xpath) {
public static INode? SelectSingleNode(this IDocument document, string xpath) {
ArgumentNullException.ThrowIfNull(document);
return (IElement?) document.Body.SelectSingleNode(xpath);
return document.Body.SelectSingleNode(xpath);
}
[PublicAPI]
public static T? SelectSingleNode<T>(this IDocument document, string xpath) where T : class, INode {
ArgumentNullException.ThrowIfNull(document);
return document.Body.SelectSingleNode(xpath) as T;
}
[PublicAPI]
public static T? SelectSingleNode<T>(this IElement element, string xpath) where T : class, INode {
ArgumentNullException.ThrowIfNull(element);
return element.SelectSingleNode(xpath) as T;
}
[PublicAPI]

View file

@ -651,7 +651,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
}
byte maxPages = 1;
IElement? htmlNode = badgePage.SelectSingleNode("(//a[@class='pagelink'])[last()]");
INode? htmlNode = badgePage.SelectSingleNode("(//a[@class='pagelink'])[last()]");
if (htmlNode != null) {
string lastPage = htmlNode.TextContent;
@ -1882,11 +1882,11 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
// We select badges that are ready to craft, as well as those that are already crafted to a maximum level, as those will not display with a craft button
// Level 5 is maximum level for card badges according to https://steamcommunity.com/tradingcards/faq
IEnumerable<IElement> linkElements = badgePage.SelectNodes("//a[@class='badge_craft_button'] | //div[@class='badges_sheet']/div[contains(@class, 'badge_row') and .//div[@class='badge_info_description']/div[contains(text(), 'Level 5')]]/a[@class='badge_row_overlay']");
IEnumerable<IAttr> linkElements = badgePage.SelectNodes<IAttr>("//a[@class='badge_craft_button'] | //div[@class='badges_sheet']/div[contains(@class, 'badge_row') and .//div[@class='badge_info_description']/div[contains(text(), 'Level 5')]]/a[@class='badge_row_overlay']/@href");
HashSet<uint> result = new();
foreach (string? badgeUri in linkElements.Select(static htmlNode => htmlNode.GetAttribute("href"))) {
foreach (string? badgeUri in linkElements.Select(static htmlNode => htmlNode.Value)) {
if (string.IsNullOrEmpty(badgeUri)) {
ArchiLogger.LogNullError(badgeUri);

View file

@ -29,6 +29,7 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using AngleSharp.Dom;
using AngleSharp.XPath;
using ArchiSteamFarm.Collections;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Localization;
@ -451,20 +452,20 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
ArgumentNullException.ThrowIfNull(htmlDocument);
ArgumentNullException.ThrowIfNull(parsedAppIDs);
IEnumerable<IElement> htmlNodes = htmlDocument.SelectNodes("//div[@class='badge_row_inner']");
IEnumerable<IElement> htmlNodes = htmlDocument.SelectNodes<IElement>("//div[@class='badge_row_inner']");
HashSet<Task>? backgroundTasks = null;
foreach (IElement htmlNode in htmlNodes) {
IElement? statsNode = htmlNode.SelectSingleElementNode(".//div[@class='badge_title_stats_content']");
IElement? appIDNode = statsNode?.SelectSingleElementNode(".//div[@class='card_drop_info_dialog']");
IElement? statsNode = htmlNode.SelectSingleNode<IElement>(".//div[@class='badge_title_stats_content']");
IAttr? appIDNode = statsNode?.SelectSingleNode<IAttr>(".//div[@class='card_drop_info_dialog']/@id");
if (appIDNode == null) {
// It's just a badge, nothing more
continue;
}
string? appIDText = appIDNode.GetAttribute("id");
string appIDText = appIDNode.Value;
if (string.IsNullOrEmpty(appIDText)) {
Bot.ArchiLogger.LogNullError(appIDText);
@ -522,7 +523,7 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
}
// Cards
IElement? progressNode = statsNode?.SelectSingleElementNode(".//span[@class='progress_info_bold']");
INode? progressNode = statsNode?.SelectSingleNode(".//span[@class='progress_info_bold']");
if (progressNode == null) {
Bot.ArchiLogger.LogNullError(progressNode);
@ -561,7 +562,7 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
}
// To save us on extra work, check cards earned so far first
IElement? cardsEarnedNode = statsNode?.SelectSingleElementNode(".//div[@class='card_drop_info_header']");
INode? cardsEarnedNode = statsNode?.SelectSingleNode(".//div[@class='card_drop_info_header']");
if (cardsEarnedNode == null) {
Bot.ArchiLogger.LogNullError(cardsEarnedNode);
@ -606,7 +607,7 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
}
// Hours
IElement? timeNode = statsNode?.SelectSingleElementNode(".//div[@class='badge_title_stats_playtime']");
INode? timeNode = statsNode?.SelectSingleNode(".//div[@class='badge_title_stats_playtime']");
if (timeNode == null) {
Bot.ArchiLogger.LogNullError(timeNode);
@ -635,7 +636,7 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
}
// Names
IElement? nameNode = statsNode?.SelectSingleElementNode("(.//div[@class='card_drop_info_body'])[last()]");
INode? nameNode = statsNode?.SelectSingleNode("(.//div[@class='card_drop_info_body'])[last()]");
if (nameNode == null) {
Bot.ArchiLogger.LogNullError(nameNode);
@ -687,7 +688,7 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
// Levels
byte badgeLevel = 0;
IElement? levelNode = htmlNode.SelectSingleElementNode(".//div[@class='badge_info_description']/div[2]");
INode? levelNode = htmlNode.SelectSingleNode(".//div[@class='badge_info_description']/div[2]");
if (levelNode != null) {
// There is no levelNode if we didn't craft that badge yet (level 0)
@ -1009,7 +1010,7 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
using IDocument? htmlDocument = await Bot.ArchiWebHandler.GetGameCardsPage(appID).ConfigureAwait(false);
IElement? progressNode = htmlDocument?.SelectSingleNode("//span[@class='progress_info_bold']");
INode? progressNode = htmlDocument?.SelectSingleNode("//span[@class='progress_info_bold']");
if (progressNode == null) {
return null;
@ -1052,7 +1053,7 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable {
byte maxPages = 1;
IElement? htmlNode = htmlDocument.SelectSingleNode("(//a[@class='pagelink'])[last()]");
INode? htmlNode = htmlDocument.SelectSingleNode("(//a[@class='pagelink'])[last()]");
if (htmlNode != null) {
string lastPage = htmlNode.TextContent;

View file

@ -1695,16 +1695,16 @@ public sealed class ArchiWebHandler : IDisposable {
return 0;
}
IEnumerable<IElement> htmlNodes = htmlDocument.SelectNodes("//div[@class='badge_card_set_cards']/div[starts-with(@class, 'badge_card_set_card')]");
IList<INode> htmlNodes = htmlDocument.SelectNodes("//div[@class='badge_card_set_cards']/div[starts-with(@class, 'badge_card_set_card')]");
result = (byte) htmlNodes.Count();
if (result == 0) {
Bot.ArchiLogger.LogNullError(result);
if (htmlNodes.Count == 0) {
Bot.ArchiLogger.LogNullError(htmlNodes);
return 0;
}
result = (byte) htmlNodes.Count;
ASF.GlobalDatabase?.CardCountsPerGame.TryAdd(appID, result);
return result;
@ -1818,11 +1818,11 @@ public sealed class ArchiWebHandler : IDisposable {
return null;
}
IEnumerable<IElement> htmlNodes = response.Content.SelectNodes("//div[@class='pending_gift']/div[starts-with(@id, 'pending_gift_')][count(div[@class='pending_giftcard_leftcol']) > 0]/@id");
IEnumerable<IAttr> htmlNodes = response.Content.SelectNodes<IAttr>("//div[@class='pending_gift']/div[starts-with(@id, 'pending_gift_')][count(div[@class='pending_giftcard_leftcol']) > 0]/@id");
HashSet<ulong> results = new();
foreach (string? giftCardIDText in htmlNodes.Select(static node => node.GetAttribute("id"))) {
foreach (string? giftCardIDText in htmlNodes.Select(static node => node.Value)) {
if (string.IsNullOrEmpty(giftCardIDText)) {
Bot.ArchiLogger.LogNullError(giftCardIDText);
@ -1865,11 +1865,11 @@ public sealed class ArchiWebHandler : IDisposable {
return null;
}
IEnumerable<IElement> htmlNodes = response.Content.SelectNodes("(//table[@class='accountTable'])[2]//a/@data-miniprofile");
IEnumerable<IAttr> htmlNodes = response.Content.SelectNodes<IAttr>("(//table[@class='accountTable'])[2]//a/@data-miniprofile");
HashSet<ulong> result = new();
foreach (string? miniProfile in htmlNodes.Select(static htmlNode => htmlNode.GetAttribute("data-miniprofile"))) {
foreach (string? miniProfile in htmlNodes.Select(static htmlNode => htmlNode.Value)) {
if (string.IsNullOrEmpty(miniProfile)) {
Bot.ArchiLogger.LogNullError(miniProfile);
@ -1953,7 +1953,7 @@ public sealed class ArchiWebHandler : IDisposable {
using HtmlDocumentResponse? response = await UrlGetToHtmlDocumentWithSession(request, checkSessionPreemptively: false).ConfigureAwait(false);
IElement? htmlNode = response?.Content?.SelectSingleNode("//div[@class='pagecontent']/script");
INode? htmlNode = response?.Content?.SelectSingleNode("//div[@class='pagecontent']/script");
if (htmlNode == null) {
// Trade can be no longer valid
@ -2345,7 +2345,7 @@ public sealed class ArchiWebHandler : IDisposable {
return (ESteamApiKeyState.Timeout, null);
}
IElement? titleNode = response.Content.SelectSingleNode("//div[@id='mainContents']/h2");
INode? titleNode = response.Content.SelectSingleNode("//div[@id='mainContents']/h2");
if (titleNode == null) {
Bot.ArchiLogger.LogNullError(titleNode);
@ -2365,7 +2365,7 @@ public sealed class ArchiWebHandler : IDisposable {
return (ESteamApiKeyState.AccessDenied, null);
}
IElement? htmlNode = response.Content.SelectSingleNode("//div[@id='bodyContents_ex']/p");
INode? htmlNode = response.Content.SelectSingleNode("//div[@id='bodyContents_ex']/p");
if (htmlNode == null) {
Bot.ArchiLogger.LogNullError(htmlNode);

View file

@ -93,7 +93,7 @@ internal sealed class SteamSaleEvent : IAsyncDisposable, IDisposable {
return null;
}
IElement? htmlNode = htmlDocument.SelectSingleNode("//div[@class='subtext']");
INode? htmlNode = htmlDocument.SelectSingleNode("//div[@class='subtext']");
if (htmlNode == null) {
// Valid, no cards for exploring the queue available

View file

@ -121,7 +121,7 @@ public sealed class MobileAuthenticator : IDisposable {
return null;
}
IEnumerable<IElement> confirmationNodes = htmlDocument.SelectNodes("//div[@class='mobileconf_list_entry']");
IEnumerable<IElement> confirmationNodes = htmlDocument.SelectNodes<IElement>("//div[@class='mobileconf_list_entry']");
HashSet<Confirmation> result = new();

View file

@ -86,12 +86,12 @@ internal static class GitHub {
return null;
}
IEnumerable<IElement> revisionNodes = response.Content.SelectNodes("//li[contains(@class, 'wiki-history-revision')]");
IEnumerable<IElement> revisionNodes = response.Content.SelectNodes<IElement>("//li[contains(@class, 'wiki-history-revision')]");
Dictionary<string, DateTime> result = new();
foreach (IElement revisionNode in revisionNodes) {
IElement? versionNode = revisionNode.SelectSingleElementNode(".//input/@value");
IAttr? versionNode = revisionNode.SelectSingleNode<IAttr>(".//input/@value");
if (versionNode == null) {
ASF.ArchiLogger.LogNullError(versionNode);
@ -99,7 +99,7 @@ internal static class GitHub {
return null;
}
string? versionText = versionNode.GetAttribute("value");
string versionText = versionNode.Value;
if (string.IsNullOrEmpty(versionText)) {
ASF.ArchiLogger.LogNullError(versionText);
@ -107,7 +107,7 @@ internal static class GitHub {
return null;
}
IElement? dateTimeNode = revisionNode.SelectSingleElementNode(".//relative-time/@datetime");
IAttr? dateTimeNode = revisionNode.SelectSingleNode<IAttr>(".//relative-time/@datetime");
if (dateTimeNode == null) {
ASF.ArchiLogger.LogNullError(dateTimeNode);
@ -115,7 +115,7 @@ internal static class GitHub {
return null;
}
string? dateTimeText = dateTimeNode.GetAttribute("datetime");
string dateTimeText = dateTimeNode.Value;
if (string.IsNullOrEmpty(dateTimeText)) {
ASF.ArchiLogger.LogNullError(dateTimeText);
@ -153,7 +153,7 @@ internal static class GitHub {
return null;
}
IElement? markdownBodyNode = response.Content.SelectSingleNode("//div[@class='markdown-body']");
IElement? markdownBodyNode = response.Content.SelectSingleNode<IElement>("//div[@class='markdown-body']");
return markdownBodyNode?.InnerHtml.Trim() ?? "";
}

View file

@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>5.2.9.0</Version>
<Version>5.3.0.0</Version>
</PropertyGroup>
<PropertyGroup>

View file

@ -1,6 +1,6 @@
<Project>
<ItemGroup>
<PackageVersion Include="AngleSharp.XPath" Version="1.1.7" />
<PackageVersion Include="AngleSharp.XPath" Version="2.0.1" />
<PackageVersion Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1" />
<PackageVersion Include="CryptSharpStandard" Version="1.0.0" />
<PackageVersion Include="Humanizer" Version="2.14.1" />