From 4de430bac8649d686d8c9dafa832a677e6b6f02b Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Mon, 15 Sep 2025 08:19:56 +0000
Subject: [PATCH 1/4] chore: Add comprehensive GitHub Copilot instructions for
AngleSharp.Diffing development (#53)
* Initial plan
* Add comprehensive GitHub Copilot instructions for AngleSharp.Diffing
Co-authored-by: egil <105649+egil@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: egil <105649+egil@users.noreply.github.com>
---
.github/copilot-instructions.md | 196 ++++++++++++++++++++++++++++++++
1 file changed, 196 insertions(+)
create mode 100644 .github/copilot-instructions.md
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..1a58d6a
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,196 @@
+# AngleSharp.Diffing - HTML Comparison Library
+
+AngleSharp.Diffing is a .NET library for comparing AngleSharp control nodes and test nodes to identify differences between HTML DOM trees. This library targets .NET Standard 2.0 and provides a fluent API for HTML comparison and diffing.
+
+**Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.**
+
+## Working Effectively
+
+### Prerequisites and Setup
+- Install .NET 8.0+ SDK (already available in GitHub Actions environments)
+- Mono is NOT required for standard development (only for legacy Cake build system)
+
+### Bootstrap, Build, and Test
+Execute these commands in order from the repository root:
+
+```bash
+cd src
+dotnet restore # ~11 seconds - restores NuGet packages
+dotnet build # ~15 seconds - compiles library and tests
+dotnet test # ~5 seconds - runs 521 tests. NEVER CANCEL.
+```
+
+**CRITICAL TIMING EXPECTATIONS:**
+- `dotnet restore`: 11 seconds - NEVER CANCEL. Set timeout to 60+ seconds minimum.
+- `dotnet build`: 15 seconds - NEVER CANCEL. Set timeout to 60+ seconds minimum.
+- `dotnet test`: 5 seconds - runs 521 tests. NEVER CANCEL. Set timeout to 30+ seconds minimum.
+- Complete clean rebuild: ~20 seconds - NEVER CANCEL. Set timeout to 60+ seconds minimum.
+
+### Alternative Build Methods
+**PRIMARY METHOD (Recommended):** Use dotnet CLI commands as shown above.
+
+**LEGACY METHOD (Not Recommended):** The repository includes Cake build scripts (`build.sh`/`build.ps1`) but they require Mono and may fail. Only use if specifically needed:
+```bash
+# Install Mono first (if needed)
+sudo apt-get update && sudo apt-get install -y mono-complete
+./build.sh # May fail - use dotnet commands instead
+```
+
+### Release Builds and Packaging
+```bash
+cd src
+dotnet build --configuration Release # ~15 seconds
+dotnet pack --configuration Release # ~8 seconds - creates NuGet packages
+```
+
+**Package Output:** Creates `.nupkg` and `.snupkg` files in `src/AngleSharp.Diffing/bin/Release/`
+
+## Validation and Testing
+
+### Manual Validation Requirements
+**ALWAYS** validate changes by creating and running a test program to exercise the library:
+
+```csharp
+using AngleSharp.Diffing;
+
+// Test basic HTML diffing functionality
+var control = @"
hello world
";
+var test = @"
hello world
";
+
+var diffs = DiffBuilder
+ .Compare(control)
+ .WithTest(test)
+ .Build()
+ .ToList();
+
+Console.WriteLine($"Found {diffs.Count} differences:");
+foreach (var diff in diffs)
+{
+ Console.WriteLine($"- {diff.GetType().Name}: {diff.Result} {diff.Target}");
+}
+```
+
+### Code Quality and Standards
+- **Code Analysis:** Project has strict analysis rules enabled (`EnforceCodeStyleInBuild=true`)
+- **EditorConfig:** Follow the existing `.editorconfig` settings (4-space indentation, CRLF line endings)
+- **Nullable References:** Enabled - handle null values appropriately
+- **Build Warnings:** Zero warnings expected - treat warnings as errors for critical types
+
+### Test Suite Information
+- **Test Framework:** xUnit with Shouldly assertions
+- **Test Count:** 521 tests covering core diffing functionality
+- **Test Runtime:** ~5 seconds total
+- **Test Files Location:** `src/AngleSharp.Diffing.Tests/`
+
+## Navigation and Architecture
+
+### Key Directories and Files
+```
+src/
+├── AngleSharp.Diffing/ # Main library project
+│ ├── Core/ # Core diffing engine and interfaces
+│ ├── Extensions/ # Extension methods
+│ ├── Strategies/ # Diffing strategy implementations
+│ └── DiffBuilder.cs # Main fluent API entry point
+├── AngleSharp.Diffing.Tests/ # Test project
+│ ├── Core/ # Core functionality tests
+│ └── Strategies/ # Strategy-specific tests
+└── AngleSharp.Diffing.sln # Solution file
+```
+
+### Important Classes and APIs
+- **`DiffBuilder`**: Main entry point with fluent API (`DiffBuilder.Compare(control).WithTest(test).Build()`)
+- **`IDiff`**: Base interface for all difference types
+- **`HtmlDifferenceEngine`**: Core comparison engine
+- **Diff Types**: `AttrDiff`, `NodeDiff`, `MissingNodeDiff`, `UnexpectedNodeDiff`, etc.
+
+### Dependencies
+- **AngleSharp 1.1.2**: Core HTML parsing library
+- **AngleSharp.Css 1.0.0-beta.144**: CSS support
+- **Target Framework**: .NET Standard 2.0 (library), .NET 8.0 (tests)
+
+## Common Development Tasks
+
+### Making Code Changes
+1. **Always build and test first** to establish baseline: `dotnet build && dotnet test`
+2. Make your changes to source files in `src/AngleSharp.Diffing/`
+3. **Immediately test after changes**: `dotnet build && dotnet test`
+4. **Manual validation**: Create a test program to exercise your changes
+5. **Pre-commit validation**: Ensure no build warnings or test failures
+
+### Debugging and Investigation
+- **Test filtering**: `dotnet test --filter "TestMethodName"` to run specific tests
+- **Verbose builds**: `dotnet build --verbosity normal` to see detailed output
+- **Key test files**: `DiffBuilderTest.cs` shows basic API usage patterns
+
+### CI/Build Requirements
+- **GitHub Actions**: Runs on both Linux and Windows
+- **Zero warnings policy**: Build must complete without warnings
+- **All tests must pass**: 521/521 tests required
+- **Code formatting**: Enforced via EditorConfig and analyzers
+
+## Getting Started Examples
+
+### Basic HTML Comparison
+```csharp
+var control = "
Expected content
";
+var test = "
Actual content
";
+
+var diffs = DiffBuilder
+ .Compare(control)
+ .WithTest(test)
+ .Build();
+```
+
+### Advanced Configuration
+```csharp
+var diffs = DiffBuilder
+ .Compare(control)
+ .WithTest(test)
+ .WithOptions(options => {
+ // Configure diffing strategies
+ })
+ .Build();
+```
+
+### Working with Results
+```csharp
+foreach (var diff in diffs)
+{
+ switch (diff.Result)
+ {
+ case DiffResult.Different:
+ case DiffResult.Missing:
+ case DiffResult.Unexpected:
+ // Handle different types of differences
+ break;
+ }
+}
+```
+
+## Troubleshooting
+
+### Common Issues
+- **Build failures**: Ensure you're in the `src/` directory
+- **Cake build failures**: Use `dotnet` commands instead of `build.sh`/`build.ps1`
+- **Test failures**: Check for environment-specific issues or recent changes
+- **Package restore issues**: Clear NuGet caches with `dotnet nuget locals all --clear`
+
+### Quick Reset Commands
+```bash
+# Clean and rebuild everything
+cd src
+dotnet clean
+rm -rf */bin */obj
+dotnet restore
+dotnet build
+dotnet test
+```
+
+## Performance Expectations
+- **Development builds**: ~15 seconds
+- **Test execution**: ~5 seconds for full suite
+- **Package creation**: ~8 seconds
+- **CI builds**: Allow 60+ seconds timeout minimum for each step
+
+**CRITICAL**: NEVER CANCEL builds or tests that appear slow. Wait at least 60 seconds for builds and 30 seconds for tests before considering alternatives.
\ No newline at end of file
From 600656dcaca8c7f8d3cbf5eabfa883001dcf33b0 Mon Sep 17 00:00:00 2001
From: RiRiSharp <30719799+RiRiSharp@users.noreply.github.com>
Date: Mon, 15 Sep 2025 10:51:45 +0200
Subject: [PATCH 2/4] feat: Unmatched `:ignore`'ed attributes does not
contribute diffs
* Ignore unmatched :ignore attributes
* Add more tests
* Add test to cover situation where no ignore attribute was requested
* Add :ignore filter approach
* Fix Linux pipeline
* Create IgnoreAttributeStrategy and make IgnoreAttributeComparer obsolete
* Revert "Fix Linux pipeline"
This reverts commit 25975588e119f3fcb2b53ebf74bc065a6ee9bd8c.
---
.../DiffBuilderTest.cs | 41 +++++++++++++++-
.../IgnoreAttributeComparerTest.cs | 8 ++--
.../TestData/IgnoreAttributeTestData.cs | 45 ++++++++++++++++++
.../Core/HtmlDifferenceEngine.cs | 1 +
src/AngleSharp.Diffing/Core/SourceMap.cs | 2 +-
...iffingStrategyPipelineBuilderExtensions.cs | 3 +-
.../IgnoreAttributeComparer.cs | 10 +++-
.../IgnoreAttributeStrategy.cs | 47 +++++++++++++++++++
8 files changed, 148 insertions(+), 9 deletions(-)
create mode 100644 src/AngleSharp.Diffing.Tests/TestData/IgnoreAttributeTestData.cs
create mode 100644 src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeStrategy.cs
diff --git a/src/AngleSharp.Diffing.Tests/DiffBuilderTest.cs b/src/AngleSharp.Diffing.Tests/DiffBuilderTest.cs
index 9769990..e9ad09d 100644
--- a/src/AngleSharp.Diffing.Tests/DiffBuilderTest.cs
+++ b/src/AngleSharp.Diffing.Tests/DiffBuilderTest.cs
@@ -1,3 +1,8 @@
+using AngleSharp.Diffing.Strategies;
+using AngleSharp.Diffing.Strategies.AttributeStrategies;
+using AngleSharp.Diffing.Strategies.TextNodeStrategies;
+using AngleSharp.Diffing.TestData;
+
namespace AngleSharp.Diffing;
@@ -116,4 +121,38 @@ public void Test006(string control, string test)
diffs.ShouldBeEmpty();
}
-}
+
+ [Theory(DisplayName =
+ "When a control element has ':ignore', elements with and without that attribute should return empty diffs")]
+ [MemberData(nameof(IgnoreAttributeTestData.ControlAndHtmlData), MemberType = typeof(IgnoreAttributeTestData))]
+ public void Test007(string controlHtml, string testHtml)
+ {
+ var diffs = DiffBuilder.Compare(controlHtml).WithTest(testHtml).Build();
+ Assert.Empty(diffs);
+ }
+
+ [Theory(DisplayName =
+ "When a control element has ':ignore', but IgnoreAttributeComparer is not active, diffs should be found")]
+ [MemberData(nameof(IgnoreAttributeTestData.ControlHtmlAndDiffData), MemberType = typeof(IgnoreAttributeTestData))]
+ public void Test008(string controlHtml, string testHtml, DiffResult expectedDiffResult)
+ {
+ var diffs = DiffBuilder
+ .Compare(controlHtml)
+ .WithTest(testHtml)
+ .WithOptions(a => a // Most important thing to note here is we do not have a ignore attribute comparer
+ .AddSearchingNodeMatcher()
+ .AddMatcher(AttributeNameMatcher.Match, StrategyType.Generalized)
+ .AddElementComparer(enforceTagClosing: false)
+ .AddMatcher(PostfixedAttributeMatcher.Match, StrategyType.Specialized)
+ .AddComparer(AttributeComparer.Compare, StrategyType.Generalized)
+ .AddClassAttributeComparer()
+ .AddBooleanAttributeComparer(BooleanAttributeComparision.Strict)
+ .AddStyleAttributeComparer())
+ .Build()
+ .ToList();
+
+ Assert.Single(diffs);
+ Assert.Equal(DiffTarget.Attribute, diffs[0].Target);
+ Assert.Equal(expectedDiffResult, diffs[0].Result);
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/IgnoreAttributeComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/IgnoreAttributeComparerTest.cs
index a59efc3..34bb922 100644
--- a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/IgnoreAttributeComparerTest.cs
+++ b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/IgnoreAttributeComparerTest.cs
@@ -17,7 +17,7 @@ public void Test000(CompareResult currentResult)
@"", "foo"
);
- IgnoreAttributeComparer
+ IgnoreAttributeStrategy
.Compare(comparison, currentResult)
.ShouldBe(currentResult);
}
@@ -30,8 +30,8 @@ public void Test003()
@"", "foo"
);
- IgnoreAttributeComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
- IgnoreAttributeComparer.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same);
+ IgnoreAttributeStrategy.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
+ IgnoreAttributeStrategy.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same);
}
[Fact(DisplayName = "When a attribute does contain have the ':ignore' postfix, Same is returned")]
@@ -42,6 +42,6 @@ public void Test004()
@"", "foo"
);
- IgnoreAttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Same);
+ IgnoreAttributeStrategy.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Same);
}
}
diff --git a/src/AngleSharp.Diffing.Tests/TestData/IgnoreAttributeTestData.cs b/src/AngleSharp.Diffing.Tests/TestData/IgnoreAttributeTestData.cs
new file mode 100644
index 0000000..daded07
--- /dev/null
+++ b/src/AngleSharp.Diffing.Tests/TestData/IgnoreAttributeTestData.cs
@@ -0,0 +1,45 @@
+namespace AngleSharp.Diffing.TestData;
+
+internal static class IgnoreAttributeTestData
+{
+ public static TheoryData ControlAndHtmlData()
+ {
+ var theoryData = new TheoryData();
+ foreach (var (controlHtml, expectedHtml, _) in TestCases)
+ {
+ theoryData.Add(controlHtml, expectedHtml);
+ }
+
+ return theoryData;
+ }
+
+ public static TheoryData ControlHtmlAndDiffData()
+ {
+ var theoryData = new TheoryData();
+ foreach (var (controlHtml, expectedHtml, expectedDiffResult) in TestCases)
+ {
+ theoryData.Add(controlHtml, expectedHtml, expectedDiffResult);
+ }
+
+ return theoryData;
+ }
+
+ private static readonly IEnumerable<(string controlHtml, string expectedHtml, DiffResult expectedDiffResult)>
+ TestCases =
+ [
+ ("", "", DiffResult.Different),
+ ("", "", DiffResult.Different),
+ ("", "", DiffResult.Different),
+ ("", "", DiffResult.Missing),
+ ("", "", DiffResult.Different),
+ ("", "", DiffResult.Different),
+ ("", "", DiffResult.Different),
+ ("", "", DiffResult.Missing),
+ ("", "", DiffResult.Different),
+ ("", "", DiffResult.Missing),
+ ("", "", DiffResult.Different),
+ ("", "", DiffResult.Missing),
+ ("", "", DiffResult.Different),
+ ("", "", DiffResult.Missing),
+ ];
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Diffing/Core/HtmlDifferenceEngine.cs b/src/AngleSharp.Diffing/Core/HtmlDifferenceEngine.cs
index 575ca96..35734aa 100644
--- a/src/AngleSharp.Diffing/Core/HtmlDifferenceEngine.cs
+++ b/src/AngleSharp.Diffing/Core/HtmlDifferenceEngine.cs
@@ -1,4 +1,5 @@
using AngleSharp.Diffing.Core.Diffs;
+using AngleSharp.Diffing.Strategies.AttributeStrategies;
namespace AngleSharp.Diffing.Core;
diff --git a/src/AngleSharp.Diffing/Core/SourceMap.cs b/src/AngleSharp.Diffing/Core/SourceMap.cs
index 4f99c8d..4b30d12 100644
--- a/src/AngleSharp.Diffing/Core/SourceMap.cs
+++ b/src/AngleSharp.Diffing/Core/SourceMap.cs
@@ -70,7 +70,7 @@ public IEnumerable GetUnmatched()
{
foreach (var source in _sources.Values)
{
- if (!_matched.Contains(source.Attribute.Name))
+ if (IsUnmatched(source.Attribute.Name))
yield return source;
}
}
diff --git a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/DiffingStrategyPipelineBuilderExtensions.cs b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/DiffingStrategyPipelineBuilderExtensions.cs
index 17b8223..ca9f18d 100644
--- a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/DiffingStrategyPipelineBuilderExtensions.cs
+++ b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/DiffingStrategyPipelineBuilderExtensions.cs
@@ -23,6 +23,7 @@ public static IDiffingStrategyCollection IgnoreDiffAttributes(this IDiffingStrat
public static IDiffingStrategyCollection AddAttributeNameMatcher(this IDiffingStrategyCollection builder)
{
builder.AddMatcher(AttributeNameMatcher.Match, StrategyType.Generalized);
+ builder.AddMatcher(IgnoreAttributeStrategy.Match, StrategyType.Generalized);
return builder;
}
@@ -33,7 +34,7 @@ public static IDiffingStrategyCollection AddAttributeComparer(this IDiffingStrat
{
builder.AddMatcher(PostfixedAttributeMatcher.Match, StrategyType.Specialized);
builder.AddComparer(AttributeComparer.Compare, StrategyType.Generalized);
- builder.AddComparer(IgnoreAttributeComparer.Compare, StrategyType.Specialized);
+ builder.AddComparer(IgnoreAttributeStrategy.Compare, StrategyType.Specialized);
return builder;
}
diff --git a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeComparer.cs b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeComparer.cs
index c74f619..eb42457 100644
--- a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeComparer.cs
+++ b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeComparer.cs
@@ -3,6 +3,7 @@
///
/// Represents the ignore attribute comparer.
///
+[Obsolete("Has been moved to IgnoreAttributeStrategy")]
public static class IgnoreAttributeComparer
{
private const string DIFF_IGNORE_POSTFIX = ":ignore";
@@ -15,8 +16,13 @@ public static CompareResult Compare(in AttributeComparison comparison, CompareRe
if (currentDecision.IsSameOrSkip)
return currentDecision;
- return comparison.Control.Attribute.Name.EndsWith(DIFF_IGNORE_POSTFIX, StringComparison.OrdinalIgnoreCase)
+ return IsIgnoreAttribute(comparison.Control.Attribute)
? CompareResult.Same
: currentDecision;
}
-}
+
+ private static bool IsIgnoreAttribute(IAttr source)
+ {
+ return source.Name.EndsWith(DIFF_IGNORE_POSTFIX, StringComparison.OrdinalIgnoreCase);
+ }
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeStrategy.cs b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeStrategy.cs
new file mode 100644
index 0000000..84d24b6
--- /dev/null
+++ b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeStrategy.cs
@@ -0,0 +1,47 @@
+namespace AngleSharp.Diffing.Strategies.AttributeStrategies;
+
+///
+/// Ignore Attribute matcher strategy.
+///
+public static class IgnoreAttributeStrategy
+{
+ private const string DIFF_IGNORE_POSTFIX = ":ignore";
+
+ ///
+ /// The ignore attribute comparer.
+ ///
+ public static CompareResult Compare(in AttributeComparison comparison, CompareResult currentDecision)
+ {
+ if (currentDecision.IsSameOrSkip)
+ return currentDecision;
+
+ return IsIgnoreAttribute(comparison.Control.Attribute)
+ ? CompareResult.Same
+ : currentDecision;
+ }
+
+ ///
+ /// Attribute name matcher strategy.
+ ///
+ public static IEnumerable Match(IDiffContext context, SourceMap controlSources, SourceMap testSources)
+ {
+ if (controlSources is null)
+ throw new ArgumentNullException(nameof(controlSources));
+ if (testSources is null)
+ throw new ArgumentNullException(nameof(testSources));
+
+ foreach (var control in controlSources.GetUnmatched())
+ {
+ // An unmatched :ignore attribute can just be matched with itself if it isn't
+ // matched with a "test" attribute of the same name already.
+ // this means an ignored attribute is ignored even if it does not appear in the test html.
+ if (control.Attribute.Name.EndsWith(DIFF_IGNORE_POSTFIX, StringComparison.OrdinalIgnoreCase))
+ yield return new AttributeComparison(control, control);
+ }
+ }
+
+ private static bool IsIgnoreAttribute(IAttr source)
+ {
+ return source.Name.EndsWith(DIFF_IGNORE_POSTFIX, StringComparison.OrdinalIgnoreCase);
+ }
+}
\ No newline at end of file
From 64342438322a3b25738c51bb21186b0c083b93ba Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Mon, 15 Sep 2025 09:20:47 +0000
Subject: [PATCH 3/4] ci: Upgrade build system to Nuke following AngleSharp
ecosystem standards (#51)
* Initial plan
* Modernize CI workflow to use dotnet CLI directly
Co-authored-by: egil <105649+egil@users.noreply.github.com>
* Update build.sh to use modern dotnet CLI instead of deprecated Cake/mono
Co-authored-by: egil <105649+egil@users.noreply.github.com>
* Upgrade build system from dotnet CLI to Nuke build automation system
Co-authored-by: egil <105649+egil@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: egil <105649+egil@users.noreply.github.com>
---
.github/workflows/ci.yml | 4 +-
.gitignore | 9 +-
.nuke/build.schema.json | 134 ++++++++++
.nuke/parameters.json | 4 +
build.cake | 11 -
build.cmd | 7 +
build.ps1 | 117 ++++-----
build.sh | 130 ++++------
nuke/.editorconfig | 14 ++
nuke/Build.cs | 270 ++++++++++++++++++++
nuke/Configuration.cs | 16 ++
nuke/Directory.Build.props | 11 +
nuke/Directory.Build.targets | 9 +
nuke/Extensions/StringExtensions.cs | 80 ++++++
nuke/ReleaseNotes.cs | 81 ++++++
nuke/ReleaseNotesParser.cs | 146 +++++++++++
nuke/SemVersion.cs | 378 ++++++++++++++++++++++++++++
nuke/_build.csproj | 21 ++
src/AngleSharp.Diffing.sln | 3 +-
tools/anglesharp.cake | 200 ---------------
tools/packages.config | 6 -
21 files changed, 1289 insertions(+), 362 deletions(-)
create mode 100644 .nuke/build.schema.json
create mode 100644 .nuke/parameters.json
delete mode 100644 build.cake
create mode 100755 build.cmd
create mode 100644 nuke/.editorconfig
create mode 100644 nuke/Build.cs
create mode 100644 nuke/Configuration.cs
create mode 100644 nuke/Directory.Build.props
create mode 100644 nuke/Directory.Build.targets
create mode 100644 nuke/Extensions/StringExtensions.cs
create mode 100644 nuke/ReleaseNotes.cs
create mode 100644 nuke/ReleaseNotesParser.cs
create mode 100644 nuke/SemVersion.cs
create mode 100644 nuke/_build.csproj
delete mode 100644 tools/anglesharp.cake
delete mode 100644 tools/packages.config
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1d0bca6..bd40446 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Build
run: ./build.sh
@@ -20,7 +20,7 @@ jobs:
runs-on: windows-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Build
run: |
diff --git a/.gitignore b/.gitignore
index e87b082..ec5269c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -189,6 +189,7 @@ PublishScripts/
# NuGet Packages
*.nupkg
+nupkgs/
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
@@ -346,4 +347,10 @@ ASALocalRun/
# BeatPulse healthcheck temp database
healthchecksdb
-*.ncrunchsolution
\ No newline at end of file
+*.ncrunchsolution
+
+# Nuke build artifacts
+.nuke/temp/
+bin/
+nuke/bin/
+nuke/obj/
\ No newline at end of file
diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json
new file mode 100644
index 0000000..7a3c92d
--- /dev/null
+++ b/.nuke/build.schema.json
@@ -0,0 +1,134 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "definitions": {
+ "Host": {
+ "type": "string",
+ "enum": [
+ "AppVeyor",
+ "AzurePipelines",
+ "Bamboo",
+ "Bitbucket",
+ "Bitrise",
+ "GitHubActions",
+ "GitLab",
+ "Jenkins",
+ "Rider",
+ "SpaceAutomation",
+ "TeamCity",
+ "Terminal",
+ "TravisCI",
+ "VisualStudio",
+ "VSCode"
+ ]
+ },
+ "ExecutableTarget": {
+ "type": "string",
+ "enum": [
+ "Clean",
+ "Compile",
+ "CreatePackage",
+ "Default",
+ "Package",
+ "PrePublish",
+ "Publish",
+ "PublishPackage",
+ "PublishPreRelease",
+ "PublishRelease",
+ "Restore",
+ "RunUnitTests"
+ ]
+ },
+ "Verbosity": {
+ "type": "string",
+ "description": "",
+ "enum": [
+ "Verbose",
+ "Normal",
+ "Minimal",
+ "Quiet"
+ ]
+ },
+ "NukeBuild": {
+ "properties": {
+ "Continue": {
+ "type": "boolean",
+ "description": "Indicates to continue a previously failed build attempt"
+ },
+ "Help": {
+ "type": "boolean",
+ "description": "Shows the help text for this build assembly"
+ },
+ "Host": {
+ "description": "Host for execution. Default is 'automatic'",
+ "$ref": "#/definitions/Host"
+ },
+ "NoLogo": {
+ "type": "boolean",
+ "description": "Disables displaying the NUKE logo"
+ },
+ "Partition": {
+ "type": "string",
+ "description": "Partition to use on CI"
+ },
+ "Plan": {
+ "type": "boolean",
+ "description": "Shows the execution plan (HTML)"
+ },
+ "Profile": {
+ "type": "array",
+ "description": "Defines the profiles to load",
+ "items": {
+ "type": "string"
+ }
+ },
+ "Root": {
+ "type": "string",
+ "description": "Root directory during build execution"
+ },
+ "Skip": {
+ "type": "array",
+ "description": "List of targets to be skipped. Empty list skips all dependencies",
+ "items": {
+ "$ref": "#/definitions/ExecutableTarget"
+ }
+ },
+ "Target": {
+ "type": "array",
+ "description": "List of targets to be invoked. Default is '{default_target}'",
+ "items": {
+ "$ref": "#/definitions/ExecutableTarget"
+ }
+ },
+ "Verbosity": {
+ "description": "Logging verbosity during build execution. Default is 'Normal'",
+ "$ref": "#/definitions/Verbosity"
+ }
+ }
+ }
+ },
+ "allOf": [
+ {
+ "properties": {
+ "Configuration": {
+ "type": "string",
+ "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
+ "enum": [
+ "Debug",
+ "Release"
+ ]
+ },
+ "ReleaseNotesFilePath": {
+ "type": "string",
+ "description": "ReleaseNotesFilePath - To determine the SemanticVersion"
+ },
+ "Solution": {
+ "type": "string",
+ "description": "Path to a solution file that is automatically loaded"
+ }
+ }
+ },
+ {
+ "$ref": "#/definitions/NukeBuild"
+ }
+ ]
+}
diff --git a/.nuke/parameters.json b/.nuke/parameters.json
new file mode 100644
index 0000000..9e30d9f
--- /dev/null
+++ b/.nuke/parameters.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "./build.schema.json",
+ "Solution": "src/AngleSharp.Diffing.sln"
+}
\ No newline at end of file
diff --git a/build.cake b/build.cake
deleted file mode 100644
index a2de43c..0000000
--- a/build.cake
+++ /dev/null
@@ -1,11 +0,0 @@
-var target = Argument("target", "Default");
-var projectName = "AngleSharp.Diffing";
-var solutionName = "AngleSharp.Diffing";
-var frameworks = new Dictionary
-{
- { "netstandard2.0", "netstandard2.0" },
-};
-
-#load tools/anglesharp.cake
-
-RunTarget(target);
\ No newline at end of file
diff --git a/build.cmd b/build.cmd
new file mode 100755
index 0000000..6ba3512
--- /dev/null
+++ b/build.cmd
@@ -0,0 +1,7 @@
+:; set -eo pipefail
+:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
+:; ${SCRIPT_DIR}/build.sh "$@"
+:; exit $?
+
+@ECHO OFF
+powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %*
\ No newline at end of file
diff --git a/build.ps1 b/build.ps1
index 6f27bfd..fcbabaf 100644
--- a/build.ps1
+++ b/build.ps1
@@ -1,81 +1,74 @@
+[CmdletBinding()]
Param(
- [string]$Script = "build.cake",
- [string]$Target = "Default",
- [ValidateSet("Release", "Debug")]
- [string]$Configuration = "Release",
- [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
- [string]$Verbosity = "Verbose",
- [switch]$Experimental,
- [switch]$WhatIf,
- [switch]$Mono,
- [switch]$SkipToolPackageRestore,
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
- [string[]]$ScriptArgs
+ [string[]]$BuildArguments
)
-$PSScriptRoot = split-path -parent $MyInvocation.MyCommand.Definition;
-$UseDryRun = "";
-$UseMono = "";
-$TOOLS_DIR = Join-Path $PSScriptRoot "tools"
-$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe"
-$NUGET_OLD_EXE = Join-Path $TOOLS_DIR "nuget_old.exe"
-$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe"
-$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
-$NUGET_OLD_URL = "https://dist.nuget.org/win-x86-commandline/v3.5.0/nuget.exe"
+Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)"
-# Should we use experimental build of Roslyn?
-$UseExperimental = "";
-if ($Experimental.IsPresent) {
- $UseExperimental = "--experimental"
-}
+Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 }
+$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
-# Is this a dry run?
-if ($WhatIf.IsPresent) {
- $UseDryRun = "--dryrun"
-}
+###########################################################################
+# CONFIGURATION
+###########################################################################
-# Should we use mono?
-if ($Mono.IsPresent) {
- $UseMono = "--mono"
-}
+$BuildProjectFile = "$PSScriptRoot\nuke\_build.csproj"
+$TempDirectory = "$PSScriptRoot\\.nuke\temp"
-# Try download NuGet.exe if do not exist.
-if (!(Test-Path $NUGET_EXE)) {
- (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE)
-}
+$DotNetGlobalFile = "$PSScriptRoot\\global.json"
+$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1"
+$DotNetChannel = "STS"
-# Try download NuGet.exe if do not exist.
-if (!(Test-Path $NUGET_OLD_URL)) {
- (New-Object System.Net.WebClient).DownloadFile($NUGET_OLD_URL, $NUGET_OLD_EXE)
-}
+$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
+$env:DOTNET_NOLOGO = 1
-# Make sure NuGet (latest) exists where we expect it.
-if (!(Test-Path $NUGET_EXE)) {
- Throw "Could not find nuget.exe"
+###########################################################################
+# EXECUTION
+###########################################################################
+
+function ExecSafe([scriptblock] $cmd) {
+ & $cmd
+ if ($LASTEXITCODE) { exit $LASTEXITCODE }
}
-# Make sure NuGet (v3.5.0) exists where we expect it.
-if (!(Test-Path $NUGET_OLD_EXE)) {
- Throw "Could not find nuget_old.exe"
+# If dotnet CLI is installed globally and it matches requested version, use for execution
+if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and `
+ $(dotnet --version) -and $LASTEXITCODE -eq 0) {
+ $env:DOTNET_EXE = (Get-Command "dotnet").Path
}
+else {
+ # Download install script
+ $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
+ New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile)
+
+ # If global.json exists, load expected version
+ if (Test-Path $DotNetGlobalFile) {
+ $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json)
+ if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) {
+ $DotNetVersion = $DotNetGlobal.sdk.version
+ }
+ }
-# Restore tools from NuGet?
-if (-Not $SkipToolPackageRestore.IsPresent)
-{
- Push-Location
- Set-Location $TOOLS_DIR
- Invoke-Expression "$NUGET_EXE install -ExcludeVersion"
- Pop-Location
- if ($LASTEXITCODE -ne 0) {
- exit $LASTEXITCODE
+ # Install by channel or version
+ $DotNetDirectory = "$TempDirectory\dotnet-win"
+ if (!(Test-Path variable:DotNetVersion)) {
+ ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
+ } else {
+ ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
}
+ $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
+ $env:PATH = "$DotNetDirectory;$env:PATH"
}
-# Make sure that Cake has been installed.
-if (!(Test-Path $CAKE_EXE)) {
- Throw "Could not find Cake.exe"
+Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)"
+
+if (Test-Path env:NUKE_ENTERPRISE_TOKEN) {
+ & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null
+ & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null
}
-# Start Cake
-Invoke-Expression "$CAKE_EXE `"$Script`" --target=`"$Target`" --configuration=`"$Configuration`" --verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs"
-exit $LASTEXITCODE
\ No newline at end of file
+ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet }
+ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }
\ No newline at end of file
diff --git a/build.sh b/build.sh
index c62ce5c..320669f 100755
--- a/build.sh
+++ b/build.sh
@@ -1,93 +1,67 @@
#!/usr/bin/env bash
-###############################################################
-# This is the Cake bootstrapper script that is responsible for
-# downloading Cake and all specified tools from NuGet.
-###############################################################
-# Define directories.
-SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
-TOOLS_DIR=$SCRIPT_DIR/tools
-NUGET_EXE=$TOOLS_DIR/nuget.exe
-NUGET_OLD_EXE=$TOOLS_DIR/nuget_old.exe
-CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe
+bash --version 2>&1 | head -n 1
-# Define default arguments.
-SCRIPT="build.cake"
-TARGET="Default"
-CONFIGURATION="Release"
-VERBOSITY="verbose"
-DRYRUN=
-SHOW_VERSION=false
-SCRIPT_ARGUMENTS=()
+set -eo pipefail
+SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
-# Parse arguments.
-for i in "$@"; do
- case $1 in
- -s|--script) SCRIPT="$2"; shift ;;
- -t|--target) TARGET="$2"; shift ;;
- -c|--configuration) CONFIGURATION="$2"; shift ;;
- -v|--verbosity) VERBOSITY="$2"; shift ;;
- -d|--dryrun) DRYRUN="--dryrun" ;;
- --version) SHOW_VERSION=true ;;
- --) shift; SCRIPT_ARGUMENTS+=("$@"); break ;;
- *) SCRIPT_ARGUMENTS+=("$1") ;;
- esac
- shift
-done
+###########################################################################
+# CONFIGURATION
+###########################################################################
-# Make sure the tools folder exist.
-if [ ! -d $TOOLS_DIR ]; then
- mkdir $TOOLS_DIR
-fi
+BUILD_PROJECT_FILE="$SCRIPT_DIR/nuke/_build.csproj"
+TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp"
-# Make sure that packages.config exist.
-if [ ! -f $TOOLS_DIR/packages.config ]; then
- echo "Downloading packages.config..."
- curl -Lsfo $TOOLS_DIR/packages.config http://cakebuild.net/bootstrapper/packages
- if [ $? -ne 0 ]; then
- echo "An error occured while downloading packages.config."
- exit 1
- fi
-fi
+DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
+DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh"
+DOTNET_CHANNEL="STS"
+
+export DOTNET_CLI_TELEMETRY_OPTOUT=1
+export DOTNET_NOLOGO=1
+
+###########################################################################
+# EXECUTION
+###########################################################################
+
+function FirstJsonValue {
+ perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}"
+}
-# Download NuGet (v3.5.0) if it does not exist.
-if [ ! -f $NUGET_OLD_EXE ]; then
- echo "Downloading NuGet..."
- curl -Lsfo $NUGET_OLD_EXE https://dist.nuget.org/win-x86-commandline/v3.5.0/nuget.exe
- if [ $? -ne 0 ]; then
- echo "An error occured while downloading nuget.exe."
- exit 1
+# If dotnet CLI is installed globally and it matches requested version, use for execution
+if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then
+ export DOTNET_EXE="$(command -v dotnet)"
+else
+ # Download install script
+ DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
+ mkdir -p "$TEMP_DIRECTORY"
+ curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
+ chmod +x "$DOTNET_INSTALL_FILE"
+
+ # If global.json exists, load expected version
+ if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then
+ DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")")
+ if [[ "$DOTNET_VERSION" == "" ]]; then
+ unset DOTNET_VERSION
+ fi
fi
-fi
-# Download NuGet (latest) if it does not exist.
-if [ ! -f $NUGET_EXE ]; then
- echo "Downloading NuGet..."
- curl -Lsfo $NUGET_EXE https://dist.nuget.org/win-x86-commandline/latest/nuget.exe
- if [ $? -ne 0 ]; then
- echo "An error occured while downloading nuget.exe."
- exit 1
+ # Install by channel or version
+ DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
+ if [[ -z ${DOTNET_VERSION+x} ]]; then
+ "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
+ else
+ "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
fi
+ export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
+ export PATH="$DOTNET_DIRECTORY:$PATH"
fi
-# Restore tools from NuGet.
-pushd $TOOLS_DIR >/dev/null
-mono $NUGET_EXE install -ExcludeVersion
-if [ $? -ne 0 ]; then
- echo "Could not restore NuGet packages."
- exit 1
-fi
-popd >/dev/null
+echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)"
-# Make sure that Cake has been installed.
-if [ ! -f $CAKE_EXE ]; then
- echo "Could not find Cake.exe at '$CAKE_EXE'."
- exit 1
+if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then
+ "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true
+ "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true
fi
-# Start Cake
-if $SHOW_VERSION; then
- exec mono $CAKE_EXE --version
-else
- exec mono $CAKE_EXE $SCRIPT --verbosity=$VERBOSITY --configuration=$CONFIGURATION --target=$TARGET $DRYRUN "${SCRIPT_ARGUMENTS[@]}"
-fi
\ No newline at end of file
+"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet
+"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"
\ No newline at end of file
diff --git a/nuke/.editorconfig b/nuke/.editorconfig
new file mode 100644
index 0000000..597b2b5
--- /dev/null
+++ b/nuke/.editorconfig
@@ -0,0 +1,14 @@
+root = false
+
+[*]
+charset = utf-8
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 4
+
+[*.csproj]
+indent_size = 2
+
+[*.json]
+indent_size = 2
\ No newline at end of file
diff --git a/nuke/Build.cs b/nuke/Build.cs
new file mode 100644
index 0000000..cb2754b
--- /dev/null
+++ b/nuke/Build.cs
@@ -0,0 +1,270 @@
+using Microsoft.Build.Exceptions;
+using Nuke.Common;
+using Nuke.Common.CI.GitHubActions;
+using Nuke.Common.IO;
+using Nuke.Common.ProjectModel;
+using Nuke.Common.Tools.DotNet;
+using Nuke.Common.Tools.GitHub;
+using Nuke.Common.Tools.NuGet;
+using Nuke.Common.Utilities.Collections;
+using Octokit;
+using Octokit.Internal;
+using Serilog;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Nuke.Common.Tooling;
+
+using static Nuke.Common.Tools.DotNet.DotNetTasks;
+using static Nuke.Common.Tools.NuGet.NuGetTasks;
+
+using Project = Nuke.Common.ProjectModel.Project;
+
+class Build : NukeBuild
+{
+ /// Support plugins are available for:
+ /// - JetBrains ReSharper https://nuke.build/resharper
+ /// - JetBrains Rider https://nuke.build/rider
+ /// - Microsoft VisualStudio https://nuke.build/visualstudio
+ /// - Microsoft VSCode https://nuke.build/vscode
+
+ public static int Main () => Execute(x => x.RunUnitTests);
+
+ [Nuke.Common.Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
+ readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;
+
+ [Nuke.Common.Parameter("ReleaseNotesFilePath - To determine the SemanticVersion")]
+ readonly AbsolutePath ReleaseNotesFilePath = RootDirectory / "CHANGELOG.md";
+
+ [Solution]
+ readonly Solution Solution;
+
+ string TargetProjectName => "AngleSharp.Diffing";
+
+ string TargetLibName => $"{TargetProjectName}";
+
+ AbsolutePath SourceDirectory => RootDirectory / "src";
+
+ AbsolutePath BuildDirectory => SourceDirectory / TargetProjectName / "bin" / Configuration;
+
+ AbsolutePath ResultDirectory => RootDirectory / "bin" / Version;
+
+ AbsolutePath NugetDirectory => ResultDirectory / "nuget";
+
+ GitHubActions GitHubActions => GitHubActions.Instance;
+
+ Project TargetProject { get; set; }
+
+ // Note: The ChangeLogTasks from Nuke itself look buggy. So using the Cake source code.
+ IReadOnlyList ChangeLog { get; set; }
+
+ ReleaseNotes LatestReleaseNotes { get; set; }
+
+ SemVersion SemVersion { get; set; }
+
+ string Version { get; set; }
+
+ IReadOnlyCollection TargetFrameworks { get; set; }
+
+ protected override void OnBuildInitialized()
+ {
+ var parser = new ReleaseNotesParser();
+
+ Log.Debug("Reading ChangeLog {FilePath}...", ReleaseNotesFilePath);
+ ChangeLog = parser.Parse(File.ReadAllText(ReleaseNotesFilePath));
+ ChangeLog.NotNull("ChangeLog / ReleaseNotes could not be read!");
+
+ LatestReleaseNotes = ChangeLog.First();
+ LatestReleaseNotes.NotNull("LatestVersion could not be read!");
+
+ Log.Debug("Using LastestVersion from ChangeLog: {LatestVersion}", LatestReleaseNotes.Version);
+ SemVersion = LatestReleaseNotes.SemVersion;
+ Version = LatestReleaseNotes.Version.ToString();
+
+ if (GitHubActions != null)
+ {
+ Log.Debug("Add Version Postfix if under CI - GithubAction(s)...");
+
+ var buildNumber = GitHubActions.RunNumber;
+
+ if (ScheduledTargets.Contains(Default))
+ {
+ Version = $"{Version}-ci.{buildNumber}";
+ }
+ else if (ScheduledTargets.Contains(PrePublish))
+ {
+ Version = $"{Version}-beta.{buildNumber}";
+ }
+ }
+
+ Log.Information("Building version: {Version}", Version);
+
+ TargetProject = Solution.GetProject(TargetLibName);
+ TargetProject.NotNull("TargetProject could not be loaded!");
+
+ TargetFrameworks = TargetProject.GetTargetFrameworks();
+ TargetFrameworks.NotNull("No TargetFramework(s) found to build for!");
+
+ Log.Information("Target Framework(s): {Frameworks}", TargetFrameworks);
+ }
+
+ Target Clean => _ => _
+ .Before(Restore)
+ .Executes(() =>
+ {
+ SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(x => x.DeleteDirectory());
+ });
+
+ Target Restore => _ => _
+ .Executes(() =>
+ {
+ DotNetRestore(s => s
+ .SetProjectFile(Solution));
+ });
+
+ Target Compile => _ => _
+ .DependsOn(Restore)
+ .Executes(() =>
+ {
+ DotNetBuild(s => s
+ .SetProjectFile(Solution)
+ .SetConfiguration(Configuration)
+ .SetContinuousIntegrationBuild(IsServerBuild)
+ .EnableNoRestore());
+ });
+
+ Target RunUnitTests => _ => _
+ .DependsOn(Compile)
+ .Executes(() =>
+ {
+ DotNetTest(s => s
+ .SetProjectFile(Solution)
+ .SetConfiguration(Configuration)
+ .EnableNoRestore()
+ .EnableNoBuild()
+ .When(_ => GitHubActions.Instance is not null, x => x.SetLoggers("GitHubActions"))
+ );
+ });
+
+ Target CreatePackage => _ => _
+ .DependsOn(Compile)
+ .Executes(() =>
+ {
+ DotNetPack(s => s
+ .SetProject(TargetProject)
+ .SetConfiguration(Configuration)
+ .SetVersion(Version)
+ .SetOutputDirectory(NugetDirectory)
+ .EnableIncludeSymbols()
+ .SetSymbolPackageFormat(DotNetSymbolPackageFormat.snupkg)
+ .EnableNoRestore()
+ .EnableNoBuild());
+ });
+
+ Target PublishPackage => _ => _
+ .DependsOn(CreatePackage)
+ .DependsOn(RunUnitTests)
+ .Executes(() =>
+ {
+ var apiKey = Environment.GetEnvironmentVariable("NUGET_API_KEY");
+
+ if (apiKey.IsNullOrEmpty())
+ {
+ throw new BuildAbortedException("Could not resolve the NuGet API key.");
+ }
+
+ foreach (var nupkg in NugetDirectory.GlobFiles("*.nupkg"))
+ {
+ DotNetNuGetPush(s => s
+ .SetTargetPath(nupkg)
+ .SetSource("https://api.nuget.org/v3/index.json")
+ .SetApiKey(apiKey));
+ }
+ });
+
+ Target PublishPreRelease => _ => _
+ .DependsOn(PublishPackage)
+ .Executes(() =>
+ {
+ string gitHubToken;
+
+ if (GitHubActions != null)
+ {
+ gitHubToken = GitHubActions.Token;
+ }
+ else
+ {
+ gitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
+ }
+
+ if (gitHubToken.IsNullOrEmpty())
+ {
+ throw new BuildAbortedException("Could not resolve GitHub token.");
+ }
+
+ var credentials = new Credentials(gitHubToken);
+
+ GitHubTasks.GitHubClient = new GitHubClient(
+ new ProductHeaderValue(nameof(NukeBuild)),
+ new InMemoryCredentialStore(credentials));
+
+ GitHubTasks.GitHubClient.Repository.Release
+ .Create("AngleSharp", TargetProjectName, new NewRelease(Version)
+ {
+ Name = Version,
+ Body = String.Join(Environment.NewLine, LatestReleaseNotes.Notes),
+ Prerelease = true,
+ TargetCommitish = "devel",
+ });
+ });
+
+ Target PublishRelease => _ => _
+ .DependsOn(PublishPackage)
+ .Executes(() =>
+ {
+ string gitHubToken;
+
+ if (GitHubActions != null)
+ {
+ gitHubToken = GitHubActions.Token;
+ }
+ else
+ {
+ gitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
+ }
+
+ if (gitHubToken.IsNullOrEmpty())
+ {
+ throw new BuildAbortedException("Could not resolve GitHub token.");
+ }
+
+ var credentials = new Credentials(gitHubToken);
+
+ GitHubTasks.GitHubClient = new GitHubClient(
+ new ProductHeaderValue(nameof(NukeBuild)),
+ new InMemoryCredentialStore(credentials));
+
+ GitHubTasks.GitHubClient.Repository.Release
+ .Create("AngleSharp", TargetProjectName, new NewRelease(Version)
+ {
+ Name = Version,
+ Body = String.Join(Environment.NewLine, LatestReleaseNotes.Notes),
+ Prerelease = false,
+ TargetCommitish = "main",
+ });
+ });
+
+ Target Package => _ => _
+ .DependsOn(RunUnitTests)
+ .DependsOn(CreatePackage);
+
+ Target Default => _ => _
+ .DependsOn(Package);
+
+ Target Publish => _ => _
+ .DependsOn(PublishRelease);
+
+ Target PrePublish => _ => _
+ .DependsOn(PublishPreRelease);
+}
\ No newline at end of file
diff --git a/nuke/Configuration.cs b/nuke/Configuration.cs
new file mode 100644
index 0000000..84b66ca
--- /dev/null
+++ b/nuke/Configuration.cs
@@ -0,0 +1,16 @@
+using System;
+using System.ComponentModel;
+using System.Linq;
+using Nuke.Common.Tooling;
+
+[TypeConverter(typeof(TypeConverter))]
+public class Configuration : Enumeration
+{
+ public static Configuration Debug = new Configuration { Value = nameof(Debug) };
+ public static Configuration Release = new Configuration { Value = nameof(Release) };
+
+ public static implicit operator string(Configuration configuration)
+ {
+ return configuration.Value;
+ }
+}
\ No newline at end of file
diff --git a/nuke/Directory.Build.props b/nuke/Directory.Build.props
new file mode 100644
index 0000000..63333c1
--- /dev/null
+++ b/nuke/Directory.Build.props
@@ -0,0 +1,11 @@
+
+
+
+
+
+ false
+ false
+ false
+
+
+
\ No newline at end of file
diff --git a/nuke/Directory.Build.targets b/nuke/Directory.Build.targets
new file mode 100644
index 0000000..3f61047
--- /dev/null
+++ b/nuke/Directory.Build.targets
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/nuke/Extensions/StringExtensions.cs b/nuke/Extensions/StringExtensions.cs
new file mode 100644
index 0000000..5dd4b54
--- /dev/null
+++ b/nuke/Extensions/StringExtensions.cs
@@ -0,0 +1,80 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+// ReSharper disable once CheckNamespace
+///
+/// Contains extension methods for .
+///
+///
+/// Original from Cake build tool source:
+/// https://github.com/cake-build/cake/blob/9828d7b246d332054896e52ba56983822feb3f05/src/Cake.Core/Extensions/StringExtensions.cs
+///
+public static class StringExtensions
+{
+ ///
+ /// Quotes the specified .
+ ///
+ /// The string to quote.
+ /// A quoted string.
+ public static string Quote(this string value)
+ {
+ if (!IsQuoted(value))
+ {
+ value = string.Concat("\"", value, "\"");
+ }
+
+ return value;
+ }
+
+ ///
+ /// Unquote the specified .
+ ///
+ /// The string to unquote.
+ /// An unquoted string.
+ public static string UnQuote(this string value)
+ {
+ if (IsQuoted(value))
+ {
+ value = value.Trim('"');
+ }
+
+ return value;
+ }
+
+ ///
+ /// Splits the into lines.
+ ///
+ /// The string to split.
+ /// The lines making up the provided string.
+ public static string[] SplitLines(this string content)
+ {
+ content = NormalizeLineEndings(content);
+ return content.Split(new[] { "\r\n" }, StringSplitOptions.None);
+ }
+
+ ///
+ /// Normalizes the line endings in a .
+ ///
+ /// The string to normalize line endings in.
+ /// A with normalized line endings.
+ public static string NormalizeLineEndings(this string value)
+ {
+ if (value != null)
+ {
+ value = value.Replace("\r\n", "\n");
+ value = value.Replace("\r", string.Empty);
+ return value.Replace("\n", "\r\n");
+ }
+
+ return string.Empty;
+ }
+
+ private static bool IsQuoted(this string value)
+ {
+ return value.StartsWith("\"", StringComparison.OrdinalIgnoreCase)
+ && value.EndsWith("\"", StringComparison.OrdinalIgnoreCase);
+ }
+}
\ No newline at end of file
diff --git a/nuke/ReleaseNotes.cs b/nuke/ReleaseNotes.cs
new file mode 100644
index 0000000..01dcdad
--- /dev/null
+++ b/nuke/ReleaseNotes.cs
@@ -0,0 +1,81 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+///
+/// Represent release notes.
+///
+///
+/// Original from Cake build tool source:
+/// https://github.com/cake-build/cake/blob/9828d7b246d332054896e52ba56983822feb3f05/src/Cake.Common/ReleaseNotes.cs
+///
+public sealed class ReleaseNotes
+{
+ private readonly List _notes;
+
+ ///
+ /// Gets the version.
+ ///
+ /// The version.
+ public SemVersion SemVersion { get; }
+
+ ///
+ /// Gets the version.
+ ///
+ /// The version.
+ public Version Version { get; }
+
+ ///
+ /// Gets the release notes.
+ ///
+ /// The release notes.
+ public IReadOnlyList Notes => _notes;
+
+ ///
+ /// Gets the raw text of the line that was extracted from.
+ ///
+ /// The raw text of the Version line.
+ public string RawVersionLine { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The semantic version.
+ /// The notes.
+ /// The raw text of the version line.
+ public ReleaseNotes(SemVersion semVersion, IEnumerable notes, string rawVersionLine)
+ : this(
+ semVersion?.AssemblyVersion ?? throw new ArgumentNullException(nameof(semVersion)),
+ semVersion,
+ notes,
+ rawVersionLine)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The version.
+ /// The notes.
+ /// The raw text of the version line.
+ public ReleaseNotes(Version version, IEnumerable notes, string rawVersionLine)
+ : this(
+ version ?? throw new ArgumentNullException(nameof(version)),
+ new SemVersion(version.Major, version.Minor, version.Build),
+ notes,
+ rawVersionLine)
+ {
+ }
+
+ private ReleaseNotes(Version version, SemVersion semVersion, IEnumerable notes, string rawVersionLine)
+ {
+ Version = version ?? throw new ArgumentNullException(nameof(version));
+ SemVersion = semVersion ?? throw new ArgumentNullException(nameof(semVersion));
+ RawVersionLine = rawVersionLine;
+ _notes = new List(notes ?? Enumerable.Empty());
+ }
+}
\ No newline at end of file
diff --git a/nuke/ReleaseNotesParser.cs b/nuke/ReleaseNotesParser.cs
new file mode 100644
index 0000000..d911c90
--- /dev/null
+++ b/nuke/ReleaseNotesParser.cs
@@ -0,0 +1,146 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Microsoft.Build.Exceptions;
+
+///
+/// The release notes parser.
+///
+///
+/// Original from Cake build tool source:
+/// https://github.com/cake-build/cake/blob/9828d7b246d332054896e52ba56983822feb3f05/src/Cake.Common/ReleaseNotesParser.cs
+///
+public sealed class ReleaseNotesParser
+{
+ ///
+ /// Parses all release notes.
+ ///
+ /// The content.
+ /// All release notes.
+ public IReadOnlyList Parse(string content)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException(nameof(content));
+ }
+
+ var lines = content.SplitLines();
+ if (lines.Length > 0)
+ {
+ var line = lines[0].Trim();
+
+ if (line.StartsWith("#", StringComparison.OrdinalIgnoreCase))
+ {
+ return ParseComplexFormat(lines);
+ }
+
+ if (line.StartsWith("*", StringComparison.OrdinalIgnoreCase))
+ {
+ return ParseSimpleFormat(lines);
+ }
+ }
+
+ throw new BuildAbortedException("Unknown release notes format.");
+ }
+
+ private IReadOnlyList ParseComplexFormat(string[] lines)
+ {
+ var lineIndex = 0;
+ var result = new List();
+
+ while (true)
+ {
+ if (lineIndex >= lines.Length)
+ {
+ break;
+ }
+
+ // Create release notes.
+ var semVer = SemVersion.Zero;
+ var version = SemVersion.TryParse(lines[lineIndex], out semVer);
+ if (!version)
+ {
+ throw new BuildAbortedException("Could not parse version from release notes header.");
+ }
+
+ var rawVersionLine = lines[lineIndex];
+
+ // Increase the line index.
+ lineIndex++;
+
+ // Parse content.
+ var notes = new List();
+ while (true)
+ {
+ // Sanity checks.
+ if (lineIndex >= lines.Length)
+ {
+ break;
+ }
+
+ if (lines[lineIndex].StartsWith("#", StringComparison.OrdinalIgnoreCase))
+ {
+ break;
+ }
+
+ // Get the current line.
+ var line = (lines[lineIndex] ?? string.Empty).Trim('*').Trim();
+ if (!string.IsNullOrWhiteSpace(line))
+ {
+ notes.Add(line);
+ }
+
+ lineIndex++;
+ }
+
+ result.Add(new ReleaseNotes(semVer, notes, rawVersionLine));
+ }
+
+ return result.OrderByDescending(x => x.SemVersion).ToArray();
+ }
+
+ private IReadOnlyList ParseSimpleFormat(string[] lines)
+ {
+ var lineIndex = 0;
+ var result = new List();
+
+ while (true)
+ {
+ if (lineIndex >= lines.Length)
+ {
+ break;
+ }
+
+ // Trim the current line.
+ var line = (lines[lineIndex] ?? string.Empty).Trim('*', ' ');
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ lineIndex++;
+ continue;
+ }
+
+ // Parse header.
+ var semVer = SemVersion.Zero;
+ var version = SemVersion.TryParse(lines[lineIndex], out semVer);
+ if (!version)
+ {
+ throw new BuildAbortedException("Could not parse version from release notes header.");
+ }
+
+ // Parse the description.
+ line = line.Substring(semVer.ToString().Length).Trim('-', ' ');
+
+ // Add the release notes to the result.
+ result.Add(new ReleaseNotes(semVer, new[] { line }, line));
+
+ lineIndex++;
+ }
+
+ return result.OrderByDescending(x => x.SemVersion).ToArray();
+ }
+}
\ No newline at end of file
diff --git a/nuke/SemVersion.cs b/nuke/SemVersion.cs
new file mode 100644
index 0000000..4a9a715
--- /dev/null
+++ b/nuke/SemVersion.cs
@@ -0,0 +1,378 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Globalization;
+using System.Text;
+using System.Text.RegularExpressions;
+
+///
+/// Class for representing semantic versions.
+///
+///
+/// Original from Cake build tool source:
+/// https://github.com/cake-build/cake/blob/9828d7b246d332054896e52ba56983822feb3f05/src/Cake.Common/SemanticVersion.cs
+///
+public class SemVersion : IComparable, IComparable, IEquatable
+{
+ ///
+ /// Gets the default version of a SemanticVersion.
+ ///
+ public static SemVersion Zero { get; } = new SemVersion(0, 0, 0, null, null, "0.0.0");
+
+ ///
+ /// Regex property for parsing a semantic version number.
+ ///
+ public static readonly Regex SemVerRegex =
+ new Regex(
+ @"(?0|(?:[1-9]\d*))(?:\.(?0|(?:[1-9]\d*))(?:\.(?0|(?:[1-9]\d*)))?(?:\-(?[0-9A-Z\.-]+))?(?:\+(?[0-9A-Z\.-]+))?)?",
+ RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase);
+
+ ///
+ /// Gets the major number of the version.
+ ///
+ public int Major { get; }
+
+ ///
+ /// Gets the minor number of the version.
+ ///
+ public int Minor { get; }
+
+ ///
+ /// Gets the patch number of the version.
+ ///
+ public int Patch { get; }
+
+ ///
+ /// Gets the prerelease of the version.
+ ///
+ public string PreRelease { get; }
+
+ ///
+ /// Gets the meta of the version.
+ ///
+ public string Meta { get; }
+
+ ///
+ /// Gets a value indicating whether semantic version is a prerelease or not.
+ ///
+ public bool IsPreRelease { get; }
+
+ ///
+ /// Gets a value indicating whether semantic version has meta or not.
+ ///
+ public bool HasMeta { get; }
+
+ ///
+ /// Gets the VersionString of the semantic version.
+ ///
+ public string VersionString { get; }
+
+ ///
+ /// Gets the AssemblyVersion of the semantic version.
+ ///
+ public Version AssemblyVersion { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Major number.
+ /// Minor number.
+ /// Patch number.
+ /// Prerelease string.
+ /// Meta string.
+ public SemVersion(int major, int minor, int patch, string preRelease = null, string meta = null) : this(major,
+ minor, patch, preRelease, meta, null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Major number.
+ /// Minor number.
+ /// Patch number.
+ /// Prerelease string.
+ /// Meta string.
+ /// The complete version number.
+ public SemVersion(int major, int minor, int patch, string preRelease, string meta, string versionString)
+ {
+ Major = major;
+ Minor = minor;
+ Patch = patch;
+ AssemblyVersion = new Version(major, minor, patch);
+ IsPreRelease = !string.IsNullOrEmpty(preRelease);
+ HasMeta = !string.IsNullOrEmpty(meta);
+ PreRelease = IsPreRelease ? preRelease : null;
+ Meta = HasMeta ? meta : null;
+
+ if (!string.IsNullOrEmpty(versionString))
+ {
+ VersionString = versionString;
+ }
+ else
+ {
+ var sb = new StringBuilder();
+ sb.AppendFormat(CultureInfo.InvariantCulture, "{0}.{1}.{2}", Major, Minor, Patch);
+
+ if (IsPreRelease)
+ {
+ sb.AppendFormat(CultureInfo.InvariantCulture, "-{0}", PreRelease);
+ }
+
+ if (HasMeta)
+ {
+ sb.AppendFormat(CultureInfo.InvariantCulture, "+{0}", Meta);
+ }
+
+ VersionString = sb.ToString();
+ }
+ }
+
+ ///
+ /// Method which tries to parse a semantic version string.
+ ///
+ /// the version that should be parsed.
+ /// the out parameter the parsed version should be stored in.
+ /// Returns a boolean indicating if the parse was successful.
+ public static bool TryParse(string version,
+ out SemVersion semVersion)
+ {
+ semVersion = Zero;
+
+ if (string.IsNullOrEmpty(version))
+ {
+ return false;
+ }
+
+ var match = SemVerRegex.Match(version);
+ if (!match.Success)
+ {
+ return false;
+ }
+
+ if (!int.TryParse(
+ match.Groups["Major"].Value,
+ NumberStyles.Integer,
+ CultureInfo.InvariantCulture,
+ out var major) ||
+ !int.TryParse(
+ match.Groups["Minor"].Value,
+ NumberStyles.Integer,
+ CultureInfo.InvariantCulture,
+ out var minor) ||
+ !int.TryParse(
+ match.Groups["Patch"].Value,
+ NumberStyles.Integer,
+ CultureInfo.InvariantCulture,
+ out var patch))
+ {
+ return false;
+ }
+
+ semVersion = new SemVersion(
+ major,
+ minor,
+ patch,
+ match.Groups["PreRelease"]?.Value,
+ match.Groups["Meta"]?.Value,
+ version);
+
+ return true;
+ }
+
+ ///
+ /// Checks if two SemVersion objects are equal.
+ ///
+ /// the other SemVersion want to test equality to.
+ /// A boolean indicating whether the objecst we're equal or not.
+ public bool Equals(SemVersion other)
+ {
+ return other is object
+ && Major == other.Major
+ && Minor == other.Minor
+ && Patch == other.Patch
+ && string.Equals(PreRelease, other.PreRelease, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(Meta, other.Meta, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Compares to SemVersion objects to and another.
+ ///
+ /// The SemVersion object we compare with.
+ /// Return 0 if the objects are identical, 1 if the version is newer and -1 if the version is older.
+ public int CompareTo(SemVersion other)
+ {
+ if (other is null)
+ {
+ return 1;
+ }
+
+ if (Equals(other))
+ {
+ return 0;
+ }
+
+ if (Major > other.Major)
+ {
+ return 1;
+ }
+
+ if (Major < other.Major)
+ {
+ return -1;
+ }
+
+ if (Minor > other.Minor)
+ {
+ return 1;
+ }
+
+ if (Minor < other.Minor)
+ {
+ return -1;
+ }
+
+ if (Patch > other.Patch)
+ {
+ return 1;
+ }
+
+ if (Patch < other.Patch)
+ {
+ return -1;
+ }
+
+ if (IsPreRelease != other.IsPreRelease)
+ {
+ return other.IsPreRelease ? 1 : -1;
+ }
+
+ switch (StringComparer.InvariantCultureIgnoreCase.Compare(PreRelease, other.PreRelease))
+ {
+ case 1:
+ return 1;
+
+ case -1:
+ return -1;
+
+ default:
+ {
+ return (string.IsNullOrEmpty(Meta) != string.IsNullOrEmpty(other.Meta))
+ ? string.IsNullOrEmpty(Meta) ? 1 : -1
+ : StringComparer.InvariantCultureIgnoreCase.Compare(Meta, other.Meta);
+ }
+ }
+ }
+
+ ///
+ /// Compares to SemVersion objects to and another.
+ ///
+ /// The object we compare with.
+ /// Return 0 if the objects are identical, 1 if the version is newer and -1 if the version is older.
+ public int CompareTo(object obj)
+ {
+ return (obj is SemVersion semVersion)
+ ? CompareTo(semVersion)
+ : -1;
+ }
+
+ ///
+ /// Equals-method for the SemVersion class.
+ ///
+ /// the other SemVersion want to test equality to.
+ /// A boolean indicating whether the objecst we're equal or not.
+ public override bool Equals(object obj)
+ {
+ return (obj is SemVersion semVersion)
+ && Equals(semVersion);
+ }
+
+ ///
+ /// Method for getting the hashcode of the SemVersion object.
+ ///
+ /// The hashcode of the SemVersion object.
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = Major;
+ hashCode = (hashCode * 397) ^ Minor;
+ hashCode = (hashCode * 397) ^ Patch;
+ hashCode = (hashCode * 397) ^
+ (PreRelease != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(PreRelease) : 0);
+ hashCode = (hashCode * 397) ^ (Meta != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Meta) : 0);
+ return hashCode;
+ }
+ }
+
+ ///
+ /// Returns the string representation of an SemVersion object.
+ ///
+ /// The string representation of the object.
+ public override string ToString()
+ {
+ int[] verParts = { Major, Minor, Patch };
+ string ver = string.Join(".", verParts);
+ return $"{ver}{(IsPreRelease ? "-" : string.Empty)}{PreRelease}{Meta}";
+ }
+
+ ///
+ /// The greater than-operator for the SemVersion class.
+ ///
+ /// first SemVersion.
+ /// second. SemVersion.
+ /// A value indicating if the operand1 was greater than operand2.
+ public static bool operator >(SemVersion operand1, SemVersion operand2)
+ => operand1 is { } && operand1.CompareTo(operand2) == 1;
+
+ ///
+ /// The less than-operator for the SemVersion class.
+ ///
+ /// first SemVersion.
+ /// second. SemVersion.
+ /// A value indicating if the operand1 was less than operand2.
+ public static bool operator <(SemVersion operand1, SemVersion operand2)
+ => operand1 is { }
+ ? operand1.CompareTo(operand2) == -1
+ : operand2 is { };
+
+ ///
+ /// The greater than or equal to-operator for the SemVersion class.
+ ///
+ /// first SemVersion.
+ /// second. SemVersion.
+ /// A value indicating if the operand1 was greater than or equal to operand2.
+ public static bool operator >=(SemVersion operand1, SemVersion operand2)
+ => operand1 is { }
+ ? operand1.CompareTo(operand2) >= 0
+ : operand2 is null;
+
+ ///
+ /// The lesser than or equal to-operator for the SemVersion class.
+ ///
+ /// first SemVersion.
+ /// second. SemVersion.
+ /// A value indicating if the operand1 was lesser than or equal to operand2.
+ public static bool operator <=(SemVersion operand1, SemVersion operand2)
+ => operand1 is null || operand1.CompareTo(operand2) <= 0;
+
+ ///
+ /// The equal to-operator for the SemVersion class.
+ ///
+ /// first SemVersion.
+ /// second. SemVersion.
+ /// A value indicating if the operand1 was equal to operand2.
+ public static bool operator ==(SemVersion operand1, SemVersion operand2)
+ => operand1?.Equals(operand2) ?? operand2 is null;
+
+ ///
+ /// The not equal to-operator for the SemVersion class.
+ ///
+ /// first SemVersion.
+ /// second. SemVersion.
+ /// A value indicating if the operand1 was not equal to operand2.
+ public static bool operator !=(SemVersion operand1, SemVersion operand2)
+ => !(operand1?.Equals(operand2) ?? operand2 is null);
+}
\ No newline at end of file
diff --git a/nuke/_build.csproj b/nuke/_build.csproj
new file mode 100644
index 0000000..1c79a37
--- /dev/null
+++ b/nuke/_build.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net8.0
+
+ CS0649;CS0169
+ ..
+ ..
+ 1
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AngleSharp.Diffing.sln b/src/AngleSharp.Diffing.sln
index 83fb1ab..fe70bc8 100644
--- a/src/AngleSharp.Diffing.sln
+++ b/src/AngleSharp.Diffing.sln
@@ -10,9 +10,8 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E8E3C8B4-92C3-4DB7-B920-D28651E24A57}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
- ..\tools\anglesharp.cake = ..\tools\anglesharp.cake
- ..\build.cake = ..\build.cake
..\build.ps1 = ..\build.ps1
+ ..\build.sh = ..\build.sh
..\CHANGELOG.md = ..\CHANGELOG.md
Directory.Build.props = Directory.Build.props
..\README.md = ..\README.md
diff --git a/tools/anglesharp.cake b/tools/anglesharp.cake
deleted file mode 100644
index 3457063..0000000
--- a/tools/anglesharp.cake
+++ /dev/null
@@ -1,200 +0,0 @@
-#addin nuget:?package=Cake.FileHelpers&version=4.0.1
-#addin nuget:?package=Octokit&version=0.32.0
-using Octokit;
-
-var configuration = Argument("configuration", "Release");
-var isRunningOnUnix = IsRunningOnUnix();
-var isRunningOnWindows = IsRunningOnWindows();
-var isRunningOnGitHubActions = BuildSystem.GitHubActions.IsRunningOnGitHubActions;
-var releaseNotes = ParseReleaseNotes("./CHANGELOG.md");
-var version = releaseNotes.RawVersionLine.Substring(2);
-var buildDir = Directory($"./src/{projectName}/bin") + Directory(configuration);
-var buildResultDir = Directory("./bin") + Directory(version);
-var nugetRoot = buildResultDir + Directory("nuget");
-
-if (isRunningOnGitHubActions)
-{
- var buildNumber = BuildSystem.GitHubActions.Environment.Workflow.RunNumber;
-
- if (target == "Default")
- {
- version = $"{version}-ci-{buildNumber}";
- }
- else if (target == "PrePublish")
- {
- version = $"{version}-alpha-{buildNumber}";
- }
-}
-
-if (!isRunningOnWindows)
-{
- frameworks.Remove("net46");
- frameworks.Remove("net461");
- frameworks.Remove("net472");
-}
-
-// Initialization
-// ----------------------------------------
-
-Setup(_ =>
-{
- Information($"Building version {version} of {projectName}.");
- Information("For the publish target the following environment variables need to be set:");
- Information("- NUGET_API_KEY");
- Information("- GITHUB_API_TOKEN");
-});
-
-// Tasks
-// ----------------------------------------
-
-Task("Clean")
- .Does(() =>
- {
- CleanDirectories(new DirectoryPath[] { buildDir, buildResultDir, nugetRoot });
- });
-
-Task("Restore-Packages")
- .IsDependentOn("Clean")
- .Does(() =>
- {
- NuGetRestore($"./src/{solutionName}.sln", new NuGetRestoreSettings
- {
- ToolPath = "tools/nuget.exe",
- });
- });
-
-Task("Build")
- .IsDependentOn("Restore-Packages")
- .Does(() =>
- {
- var buildSettings = new DotNetCoreMSBuildSettings();
- buildSettings.WarningCodesAsMessage.Add("CS1591"); // During CI builds we don't want the output polluted by "warning CS1591: Missing XML comment" warnings
-
- ReplaceRegexInFiles("./src/Directory.Build.props", "(?<=)(.+?)(?=)", version);
- DotNetCoreBuild($"./src/{solutionName}.sln", new DotNetCoreBuildSettings
- {
- Configuration = configuration,
- MSBuildSettings = buildSettings
- });
- });
-
-Task("Run-Unit-Tests")
- .IsDependentOn("Build")
- .Does(() =>
- {
- var settings = new DotNetCoreTestSettings
- {
- Configuration = configuration,
- NoBuild = false,
- };
-
- if (isRunningOnGitHubActions)
- {
- settings.TestAdapterPath = Directory(".");
- settings.Loggers.Add("GitHubActions");
- }
-
- DotNetCoreTest($"./src/{solutionName}.Tests/", settings);
- });
-
-Task("Copy-Files")
- .IsDependentOn("Build")
- .Does(() =>
- {
- foreach (var item in frameworks)
- {
- var targetDir = nugetRoot + Directory("lib") + Directory(item.Key);
- CreateDirectory(targetDir);
- CopyFiles(new FilePath[]
- {
- buildDir + Directory(item.Value) + File($"{projectName}.dll"),
- buildDir + Directory(item.Value) + File($"{projectName}.xml"),
- }, targetDir);
- }
-
- CopyFiles(new FilePath[] {
- $"src/{projectName}.nuspec",
- "logo.png"
- }, nugetRoot);
- });
-
-Task("Create-Package")
- .IsDependentOn("Build")
- .Does(() =>
- {
- var settings = new DotNetCorePackSettings
- {
- Configuration = configuration,
- NoBuild = false,
- OutputDirectory = nugetRoot,
- ArgumentCustomization = args => args.Append("--include-symbols").Append("-p:SymbolPackageFormat=snupkg")
- };
-
- DotNetCorePack($"./src/{solutionName}/", settings);
- });
-
-Task("Publish-Package")
- .IsDependentOn("Create-Package")
- .IsDependentOn("Run-Unit-Tests")
- .Does(() =>
- {
- var apiKey = EnvironmentVariable("NUGET_API_KEY");
-
- if (String.IsNullOrEmpty(apiKey))
- {
- throw new InvalidOperationException("Could not resolve the NuGet API key.");
- }
-
- foreach (var nupkg in GetFiles(nugetRoot.Path.FullPath + "/*.nupkg"))
- {
- NuGetPush(nupkg, new NuGetPushSettings
- {
- Source = "https://nuget.org/api/v2/package",
- ApiKey = apiKey,
- SkipDuplicate = true
- });
- }
- });
-
-Task("Publish-Release")
- .IsDependentOn("Publish-Package")
- .IsDependentOn("Run-Unit-Tests")
- .Does(() =>
- {
- var githubToken = EnvironmentVariable("GITHUB_TOKEN");
-
- if (String.IsNullOrEmpty(githubToken))
- {
- throw new InvalidOperationException("Could not resolve GitHub token.");
- }
-
- var github = new GitHubClient(new ProductHeaderValue("AngleSharpCakeBuild"))
- {
- Credentials = new Credentials(githubToken),
- };
-
- var newRelease = github.Repository.Release;
- newRelease.Create("AngleSharp", projectName, new NewRelease("v" + version)
- {
- Name = version,
- Body = String.Join(Environment.NewLine, releaseNotes.Notes),
- Prerelease = releaseNotes.Version.ToString() != version,
- TargetCommitish = "master",
- }).Wait();
- });
-
-// Targets
-// ----------------------------------------
-
-Task("Package")
- .IsDependentOn("Run-Unit-Tests")
- .IsDependentOn("Create-Package");
-
-Task("Default")
- .IsDependentOn("Package");
-
-Task("Publish")
- .IsDependentOn("Publish-Release");
-
-Task("PrePublish")
- .IsDependentOn("Publish-Package");
diff --git a/tools/packages.config b/tools/packages.config
deleted file mode 100644
index e714645..0000000
--- a/tools/packages.config
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
From 03215d477284c02c066b2395c5da1a08362f03a1 Mon Sep 17 00:00:00 2001
From: Egil Hansen
Date: Mon, 15 Sep 2025 09:23:33 +0000
Subject: [PATCH 4/4] docs: Update CHANGELOG for version 1.1.0
---
CHANGELOG.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 72e8386..4824e71 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 1.1.0
+
+- An ':ignore' marked attributes will no longer show up as a diff if the ignored attribute was NOT found in the test html. By [@RiRiSharp](https://github.com/RiRiSharp).
+
# 1.0.0
- Enabled using `diff:ignoreAttributes` and `diff:ignoreChildren` together on the same element.