Skip to content

Commit

Permalink
Merge pull request #2 from agailloty/multi-xpaths
Browse files Browse the repository at this point in the history
Adding the ability to evaluate multiple xpath expressions
  • Loading branch information
agailloty authored Feb 8, 2025
2 parents 5b82545 + 497d329 commit 3a3a5e4
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 62 deletions.
9 changes: 9 additions & 0 deletions XpathRunner/ResultModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Collections.Generic;

namespace XpathRunner;

public class ResultModel
{
public string ColumnName { get; set; }

Check warning on line 7 in XpathRunner/ResultModel.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Non-nullable property 'ColumnName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 7 in XpathRunner/ResultModel.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Non-nullable property 'ColumnName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public List<string> Rows { get; set; } = new();
}
14 changes: 11 additions & 3 deletions XpathRunner/Service/DialogService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
Expand Down Expand Up @@ -30,10 +32,16 @@ public class DialogService
return fileList;
}

public async Task SaveResultsToCsvAsync(string[] content)
public async Task SaveResultsToCsvAsync(IEnumerable<IList<string>> content)
{
var result = string.Join(Environment.NewLine, content);
await ExportFile(result);
StringBuilder lines = new();
foreach (var line in content)
{
var result = string.Join(",", line);
lines.AppendLine(result);
}

await ExportFile(lines.ToString());
}

private async Task ExportFile(string content)
Expand Down
70 changes: 43 additions & 27 deletions XpathRunner/Service/XpathService.cs
Original file line number Diff line number Diff line change
@@ -1,46 +1,62 @@
using System.Collections.Generic;
using System.Linq;
using HtmlAgilityPack;

namespace XpathRunner.Service;

public class XpathService
{
public IList<string> ExtractHtmlContent(string filepath, string xpath)
public IList<ResultModel> ExtractMultipleHtmlContent(string[] filepaths, string[] xpaths)
{
if (string.IsNullOrEmpty(filepath) || string.IsNullOrEmpty(xpath))
return new List<string>();
// keep only the paths and xpaths that are not empty
var paths = filepaths.Where(path => !string.IsNullOrEmpty(path)).ToArray();
var xpathsList = xpaths.Where(xpath => !string.IsNullOrEmpty(xpath)).ToArray();

var content = new List<string>();
var doc = new HtmlDocument();
doc.Load(filepath);

var results = doc.DocumentNode.SelectNodes(xpath);
if (results == null)
return content;

foreach (var result in results)
content.Add(result.InnerText.Trim());
return content;
}

public IList<string> ExtractHtmlContent(string[] filepaths, string xpath)
{
if (filepaths == null || filepaths.Length == 0 || string.IsNullOrEmpty(xpath))
return new List<string>();
// if there are no paths or xpaths, return an empty list
if (paths.Length == 0 || xpathsList.Length == 0)
return new List<ResultModel>();

var content = new List<string>();
foreach (var filepath in filepaths)
var results = new List<ResultModel>();

foreach (var filepath in paths)
{
var doc = new HtmlDocument();
doc.Load(filepath);
var content = EvaluateMultipleXpaths(doc, xpathsList);
results.AddRange(content);
}

// Get unique xpath expressions
var uniqueXpaths = results.Select(x => x.ColumnName).Distinct().ToList();
var uniqueResults = new List<ResultModel>();
foreach (var xpath in uniqueXpaths)
{
var rows = results.Where(r => r.ColumnName == xpath).SelectMany(r => r.Rows).ToList();
uniqueResults.Add(new ResultModel { ColumnName = xpath, Rows = rows });

}

return uniqueResults;
}

var results = doc.DocumentNode.SelectNodes(xpath);
if (results == null)
public IList<ResultModel> EvaluateMultipleXpaths(HtmlDocument doc, IEnumerable<string> xpaths)
{
var results = new List<ResultModel>();
foreach (var xpath in xpaths)
{
var content = new ResultModel( ) { ColumnName = xpath };
var resultsNode = doc.DocumentNode.SelectNodes(xpath);
if (resultsNode == null)
{
results.Add(content);
continue;
}

foreach (var result in results)
content.Add(result.InnerText.Trim());
foreach (var result in resultsNode)
content.Rows.Add(result.InnerText.Trim());
results.Add(content);
}
return content;
return results;
}

}
73 changes: 51 additions & 22 deletions XpathRunner/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
Expand All @@ -23,10 +24,17 @@ public class MainWindowViewModel : ObservableObject
private int _xpathResultCount;
private ObservableCollection<FileInfo>? _selectedFiles;
private string _selectedFileLabel;
private ObservableCollection<XpathExpressionItem> _xpathExpressions;
private readonly XpathService _xpathService;

public MainWindowViewModel()

Check warning on line 30 in XpathRunner/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Non-nullable field '_filePath' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 30 in XpathRunner/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Non-nullable field '_xpathExpression' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 30 in XpathRunner/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Non-nullable field '_selectedFileLabel' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 30 in XpathRunner/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Non-nullable field '_filePath' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 30 in XpathRunner/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Non-nullable field '_xpathExpression' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 30 in XpathRunner/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Non-nullable field '_selectedFileLabel' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 30 in XpathRunner/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Non-nullable field '_filePath' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 30 in XpathRunner/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Non-nullable field '_xpathExpression' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 30 in XpathRunner/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Non-nullable field '_selectedFileLabel' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 30 in XpathRunner/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Non-nullable field '_xpathExpressions' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.
{
_xpathService = new XpathService();
IsXpathResultsEmpty = true;
XpathExpressions =
[
new XpathExpressionItem()
];
FilePickerCommand = new RelayCommand(async () =>
{
IsBusy = true;
Expand All @@ -35,24 +43,7 @@ public MainWindowViewModel()
IsBusy = false;
});

GetXpathResultsCommand = new RelayCommand(() =>
{
IsBusy = true;
var xpathService = new XpathService();
var filePaths = SelectedFiles?.Select(file => file.FullName).ToArray();
if (filePaths != null)
{
var results = xpathService.ExtractHtmlContent(filePaths, XpathExpression);
XpathResults.Clear();
foreach (var result in results)
{
XpathResults.Add(result);
}
XpathResultsCount = XpathResults.Count;
IsXpathResultsEmpty = XpathResultsCount == 0;
}
IsBusy = false;
});
GetXpathResultsCommand = new RelayCommand(GetXpathResults);

AddFilesFileCommand = new RelayCommand(async () => await AddFiles());
RemoveFileCommand = new RelayCommand<FileInfo>(RemoveFile);
Expand All @@ -73,10 +64,15 @@ public MainWindowViewModel()
ExportResultsCommand = new RelayCommand(async () =>

Check warning on line 64 in XpathRunner/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
IsBusy = true;
var content = XpathResults.ToArray();
await _dialogService.SaveResultsToCsvAsync(content);
var content = XpathResults.ToList();
//await _dialogService.SaveResultsToCsvAsync(content);
IsBusy = false;
});

AddXpathCommand = new RelayCommand(() =>
{
XpathExpressions.Add(new XpathExpressionItem());
});
}

#region Public properties
Expand Down Expand Up @@ -117,7 +113,8 @@ public string XpathExpression
set => SetProperty(ref _xpathExpression, value);
}

public ObservableCollection<string> XpathResults { get; } = new();
public event PropertyChangedEventHandler? ResultsChanged;
public ObservableCollection<ResultModel> XpathResults { get; } = new();

public bool IsXpathResultsEmpty
{
Expand Down Expand Up @@ -148,13 +145,22 @@ public string SelectedFilesLabel
public ICommand AddFilesFileCommand { get; }
public ICommand RemoveFileCommand { get; }
public ICommand ExportResultsCommand { get; }

public ICommand AddXpathCommand { get; }

public ObservableCollection<FileInfo>? SelectedFiles
{
get => _selectedFiles;
set => SetProperty(ref _selectedFiles, value);
}

public ObservableCollection<XpathExpressionItem> XpathExpressions
{
get => _xpathExpressions;
set => SetProperty(ref _xpathExpressions, value);
}


#endregion

#region Private methods
Expand Down Expand Up @@ -186,7 +192,7 @@ private void RemoveFile(FileInfo? file)

private void UpdateSelectedFilesLabel()
{
if (SelectedFiles.Count == 1)
if (SelectedFiles?.Count == 1)
{
SelectedFilesLabel = $"Selected file : {FilePath}";
}
Expand All @@ -195,5 +201,28 @@ private void UpdateSelectedFilesLabel()
SelectedFilesLabel = $"Number of selected files : {SelectedFiles.Count}";
}
}

private void GetXpathResults()
{
IsBusy = true;
var filePaths = SelectedFiles?.Select(file => file.FullName).ToArray();
if (filePaths != null)
{
var nonEmptyXpaths = XpathExpressions.Where(expression => !string.IsNullOrEmpty(expression.XpathExpression))
.Select(x => x.XpathExpression).ToArray();
var results = _xpathService.ExtractMultipleHtmlContent(filePaths, nonEmptyXpaths);
XpathResults.Clear();
foreach (var result in results)
{
XpathResults.Add(result);
}

XpathResultsCount = XpathResults
.Select(x => x.Rows.Count).Sum();
IsXpathResultsEmpty = XpathResultsCount == 0;
}
IsBusy = false;
}

#endregion
}
14 changes: 14 additions & 0 deletions XpathRunner/ViewModels/XpathExpressionItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using CommunityToolkit.Mvvm.ComponentModel;

namespace XpathRunner.ViewModels;

public class XpathExpressionItem : ObservableObject
{
private string _xpathExpression;

Check warning on line 7 in XpathRunner/ViewModels/XpathExpressionItem.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Non-nullable field '_xpathExpression' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 7 in XpathRunner/ViewModels/XpathExpressionItem.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Non-nullable field '_xpathExpression' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

public string XpathExpression
{
get => _xpathExpression;
set => SetProperty(ref _xpathExpression, value);
}
}
14 changes: 11 additions & 3 deletions XpathRunner/Views/MainView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
VerticalAlignment="Top" HorizontalAlignment="Right">
<Button.ContentTemplate>
<DataTemplate>
<Image Margin="0" Width="15" Height="15" VerticalAlignment="Top"
<Image Margin="0 0 0 5" Width="15" Height="15" VerticalAlignment="Top"
HorizontalAlignment="Right" Source="/Assets/deletebtn.png" />
</DataTemplate>
</Button.ContentTemplate>
Expand All @@ -60,8 +60,16 @@
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center" Grid.Column="0" FontWeight="Bold" FontSize="15" Text="Xpath: "/>
<TextBox Grid.Column="1" Text="{Binding XpathExpression}" TextWrapping="Wrap" FontSize="15" MaxHeight="100"/>
<Button Grid.Column="0" Content="Add Xpath" Command="{Binding AddXpathCommand}"></Button>
<ScrollViewer Grid.Column="1" MaxHeight="150">
<ItemsControl ItemsSource="{Binding XpathExpressions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Margin="2" Text="{Binding XpathExpression, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap" FontSize="15" MaxHeight="100"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Button Grid.Column="2" Margin="10 0 0 0" ClickMode="Press" Command="{Binding GetXpathResultsCommand}">Search</Button>
</Grid>

Expand Down
24 changes: 21 additions & 3 deletions XpathRunner/Views/XpathResults.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,29 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:vm="using:XpathRunner.ViewModels"
xmlns:xpathRunner="clr-namespace:XpathRunner"
x:DataType="vm:MainWindowViewModel"
x:Class="XpathRunner.Views.XpathResults">
x:Class="XpathRunner.Views.XpathResults"
>
<Border>
<ScrollViewer>
<ListBox ItemsSource="{Binding XpathResults}" />
<ScrollViewer HorizontalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding XpathResults}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Expander MinWidth="100" BorderBrush="Transparent" Background="Transparent" IsExpanded="True" Header="{Binding ColumnName}">
<StackPanel>
<StackPanel Margin="0 10 10 10" Orientation="Horizontal">
<TextBlock FontWeight="Bold" FontSize="16" Text="{Binding ColumnName}" />
<TextBlock FontWeight="Bold" FontSize="16" Text="{Binding Rows.Count, StringFormat=' (Count : {0} )'}" />
</StackPanel>
<ListBox Background="Transparent" ItemsSource="{Binding Rows}" />
</StackPanel>
</Expander>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</UserControl>
2 changes: 2 additions & 0 deletions XpathRunner/Views/XpathResults.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.ComponentModel;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using XpathRunner.ViewModels;

namespace XpathRunner.Views;

Expand Down
4 changes: 0 additions & 4 deletions XpathRunner/XpathRunner.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>

<PropertyGroup>
<DebugType>none</DebugType>
</PropertyGroup>

<ItemGroup>
<None Remove="Assets\html_tag.png" />
Expand Down

0 comments on commit 3a3a5e4

Please sign in to comment.