├── src ├── Fizzler.snk ├── PublicAPI.Unshipped.txt ├── AssemblyInfo.cs ├── SerializableAttribute.cs ├── Selector.cs ├── UnreachableException.cs ├── SelectorsCachingCompiler.cs ├── Fizzler.csproj ├── TokenKind.cs ├── NamespacePrefix.cs ├── Either.cs ├── Reader.cs ├── Token.cs ├── HumanReadableSelectorGenerator.cs ├── IElementOps.cs ├── ISelectorGenerator.cs ├── SelectorGeneratorTee.cs ├── Tokener.cs ├── SelectorGenerator.cs └── PublicAPI.Shipped.txt ├── global.json ├── .config └── dotnet-tools.json ├── test.sh ├── .gitignore ├── test.cmd ├── tests ├── .editorconfig ├── Fizzler.Tests.csproj ├── Throws.cs ├── HumanReadableSelectorGeneratorTests.cs ├── NamespacePrefixTests.cs ├── ReaderTests.cs ├── TokenTests.cs ├── SelectorGeneratorTeeTests.cs ├── TokenerTests.cs └── ParserTests.cs ├── pack.cmd ├── pack.sh ├── .gitattributes ├── Directory.Build.props ├── appveyor.yml ├── Fizzler.sln ├── README.md └── .editorconfig /src/Fizzler.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atifaziz/Fizzler/HEAD/src/Fizzler.snk -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.302", 4 | "rollForward": "latestFeature" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/PublicAPI.Unshipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | Fizzler.HumanReadableSelectorGenerator.Class(string! clazz) -> void 3 | -------------------------------------------------------------------------------- /src/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | [assembly: System.Runtime.InteropServices.ComVisible(false)] 2 | [assembly: System.CLSCompliant(true)] 3 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "sourcelink": { 6 | "version": "3.1.1", 7 | "commands": [ 8 | "sourcelink" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | [[ -e test.sh ]] || { echo >&2 "Please cd into the script location before running it."; exit 1; } 3 | set -e 4 | ./build.sh 5 | for c in Debug Release; do 6 | dotnet test --no-build tests -c $c 7 | done 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | Bin/ 3 | *.dll 4 | *.pdb 5 | *.user 6 | *.cache 7 | *.suo 8 | *.docstates 9 | obj/ 10 | _ReSharper.* 11 | *.DotSettings 12 | dist/ 13 | pkg/*/ 14 | *.nupkg 15 | packages/ 16 | .vs/ 17 | .idea/ 18 | TestResults/ 19 | .vscode/ 20 | 21 | -------------------------------------------------------------------------------- /test.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | pushd "%~dp0" 3 | call :main %* 4 | popd 5 | goto :EOF 6 | 7 | :main 8 | call build ^ 9 | && call :test Debug ^ 10 | && call :test Release 11 | goto :EOF 12 | 13 | :test 14 | dotnet test --no-build tests -c %1 15 | goto :EOF 16 | -------------------------------------------------------------------------------- /tests/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | 3 | [*Tests.cs] 4 | 5 | # CA1707: Identifiers should not contain underscores 6 | dotnet_diagnostic.CA1707.severity = silent 7 | 8 | # CA1034: Nested types should not be visible 9 | dotnet_diagnostic.CA1034.severity = suggestion 10 | -------------------------------------------------------------------------------- /pack.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | pushd "%~dp0" 3 | call :main %* 4 | popd 5 | goto :EOF 6 | 7 | :main 8 | setlocal 9 | set VERSION_SUFFIX= 10 | if not "%~1"=="" set VERSION_SUFFIX=--version-suffix %~1 11 | call build && dotnet pack --no-build -c Release %VERSION_SUFFIX% src 12 | goto :EOF 13 | -------------------------------------------------------------------------------- /pack.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | [[ -e pack.sh ]] || { echo >&2 "Please cd into the script location before running it."; exit 1; } 3 | set -e 4 | if [ -n "$1" ]; then 5 | VERSION_SUFFIX="--version-suffix $1" 6 | else 7 | VERSION_SUFFIX= 8 | fi 9 | ./build.sh 10 | dotnet pack --no-build -c Release $VERSION_SUFFIX src 11 | -------------------------------------------------------------------------------- /src/SerializableAttribute.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD1_0 2 | 3 | namespace Fizzler 4 | { 5 | using Attribute = System.Attribute; 6 | using AttributeUsageAttribute = System.AttributeUsageAttribute; 7 | using static System.AttributeTargets; 8 | 9 | [AttributeUsage(Class | Struct | Enum | Delegate)] 10 | sealed class SerializableAttribute : Attribute {} 11 | } 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | *.sh eol=lf 5 | *.cmd eol=crlf 6 | 7 | # Custom for Visual Studio 8 | *.cs diff=csharp 9 | 10 | # Standard to msysgit 11 | *.doc diff=astextplain 12 | *.DOC diff=astextplain 13 | *.docx diff=astextplain 14 | *.DOCX diff=astextplain 15 | *.dot diff=astextplain 16 | *.DOT diff=astextplain 17 | *.pdf diff=astextplain 18 | *.PDF diff=astextplain 19 | *.rtf diff=astextplain 20 | *.RTF diff=astextplain 21 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 4 | enable 5 | true 6 | 8.0-all 7 | true 8 | 9 | EnableGenerateDocumentationFile 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/Fizzler.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | false 5 | 6 | 7 | 8 | 9 | 10 | all 11 | runtime; build; native; contentfiles; analyzers; buildtransitive 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Selector.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler 23 | { 24 | using System.Collections.Generic; 25 | 26 | /// 27 | /// Represents a selector implementation over an arbitrary type of elements. 28 | /// 29 | public delegate IEnumerable Selector(IEnumerable elements); 30 | } 31 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | image: 3 | - Visual Studio 2022 4 | - Ubuntu 5 | branches: 6 | only: 7 | - master 8 | - /\d+\.\d+(\.\d+)?/ 9 | except: 10 | - /.+[\-.]wip$/ 11 | - wip 12 | pull_requests: 13 | do_not_increment_build_number: true 14 | skip_commits: 15 | files: 16 | - '*.md' 17 | - '*.txt' 18 | - '.editorconfig' 19 | environment: 20 | DOTNET_CLI_TELEMETRY_OPTOUT: true 21 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 22 | install: 23 | - cmd: curl -OsSL https://dot.net/v1/dotnet-install.ps1 24 | - ps: if ($isWindows) { ./dotnet-install.ps1 -JsonFile global.json } 25 | - sh: curl -OsSL https://dot.net/v1/dotnet-install.sh 26 | - sh: chmod +x dotnet-install.sh 27 | - sh: ./dotnet-install.sh --jsonfile global.json 28 | - sh: export PATH="$HOME/.dotnet:$PATH" 29 | before_build: 30 | - dotnet --info 31 | - dotnet tool restore 32 | build_script: 33 | - ps: |- 34 | $id = ([datetimeoffset]$env:APPVEYOR_REPO_COMMIT_TIMESTAMP).ToUniversalTime().ToString('yyyyMMdd''t''HHmm') 35 | if ($isWindows) { .\pack.cmd ci-$id } else { ./pack.sh ci-$id } 36 | dotnet sourcelink test "$(dir dist\*.nupkg)" 37 | test_script: 38 | - cmd: test.cmd 39 | - sh: ./test.sh 40 | artifacts: 41 | - path: dist\*.nupkg 42 | deploy: 43 | - provider: NuGet 44 | server: https://www.myget.org/F/raboof/api/v2/package 45 | api_key: 46 | secure: fhGwXyO35FSshRzs5GWmF1LJTrd1sIqmS/jNCSfO2LfOciuYAKiXuFMYZFGiTAl+ 47 | symbol_server: https://www.myget.org/F/raboof/symbols/api/v2/package 48 | on: 49 | branch: master 50 | notifications: 51 | - provider: Email 52 | to: 53 | - raboof-ci@googlegroups.com 54 | on_build_success: true 55 | on_build_failure: true 56 | on_build_status_changed: false 57 | -------------------------------------------------------------------------------- /Fizzler.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.35013.160 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8FC1F69B-FAFC-484C-984B-78FA7E224BCB}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | appveyor.yml = appveyor.yml 10 | build.cmd = build.cmd 11 | build.sh = build.sh 12 | COPYING.txt = COPYING.txt 13 | Directory.Build.props = Directory.Build.props 14 | global.json = global.json 15 | pack.cmd = pack.cmd 16 | pack.sh = pack.sh 17 | README.md = README.md 18 | test.cmd = test.cmd 19 | test.sh = test.sh 20 | EndProjectSection 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fizzler", "src\Fizzler.csproj", "{939036D6-29FD-46E4-B6CD-52618F51081B}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fizzler.Tests", "tests\Fizzler.Tests.csproj", "{3C0C905E-47D4-4616-8B09-C3CECB12598F}" 25 | EndProject 26 | Global 27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 28 | Debug|Any CPU = Debug|Any CPU 29 | Release|Any CPU = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {939036D6-29FD-46E4-B6CD-52618F51081B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {939036D6-29FD-46E4-B6CD-52618F51081B}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {939036D6-29FD-46E4-B6CD-52618F51081B}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {939036D6-29FD-46E4-B6CD-52618F51081B}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {3C0C905E-47D4-4616-8B09-C3CECB12598F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {3C0C905E-47D4-4616-8B09-C3CECB12598F}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {3C0C905E-47D4-4616-8B09-C3CECB12598F}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {3C0C905E-47D4-4616-8B09-C3CECB12598F}.Release|Any CPU.Build.0 = Release|Any CPU 40 | EndGlobalSection 41 | GlobalSection(SolutionProperties) = preSolution 42 | HideSolutionNode = FALSE 43 | EndGlobalSection 44 | GlobalSection(ExtensibilityGlobals) = postSolution 45 | SolutionGuid = {5312DB20-C237-4BCC-82CD-FE82768BC363} 46 | EndGlobalSection 47 | EndGlobal 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fizzler: .NET CSS Selector Engine 2 | 3 | [![Build Status][build-badge]][builds] 4 | [![NuGet][nuget-badge]][nuget-pkg] 5 | [![MyGet][myget-badge]][edge-pkgs] 6 | 7 | Fizzler is a .NET Standard 1.0 library; it is a [W3C Selectors 8 | (Level 3)][w3cs3] parser and generic selector framework over document 9 | hierarchies. 10 | 11 | The [default implementation][fizzhap] is based on [HTMLAgilityPack][hap] and 12 | selects from HTML documents. The unit tests are based on the jQuery 13 | selector engine tests. 14 | 15 | Contributions are welcome in forms of: 16 | 17 | * Increased selector support 18 | * Implementation over an HTML-like hierarchical document model 19 | * Re-factorings 20 | * Improved tests 21 | 22 | ## Examples 23 | 24 | The following example uses [Fizzler.Systems.HtmlAgilityPack][fizzhap]: 25 | 26 | ```c# 27 | // Load the document using HTMLAgilityPack as normal 28 | var html = new HtmlDocument(); 29 | html.LoadHtml(@" 30 | 31 | 32 | 33 |
34 |

Fizzler

35 |

CSS Selector Engine

36 | 37 | "); 38 | 39 | // Fizzler for HtmlAgilityPack is implemented as the 40 | // QuerySelectorAll extension method on HtmlNode 41 | 42 | var document = html.DocumentNode; 43 | 44 | // yields: [

Fizzler

] 45 | document.QuerySelectorAll(".content"); 46 | 47 | // yields: [

Fizzler

,

CSS Selector Engine

] 48 | document.QuerySelectorAll("p"); 49 | 50 | // yields empty sequence 51 | document.QuerySelectorAll("body>p"); 52 | 53 | // yields [

Fizzler

,

CSS Selector Engine

] 54 | document.QuerySelectorAll("body p"); 55 | 56 | // yields [

Fizzler

] 57 | document.QuerySelectorAll("p:first-child"); 58 | ``` 59 | 60 | 61 | [build-badge]: https://img.shields.io/appveyor/ci/raboof/fizzler/master.svg?label=windows 62 | [builds]: https://ci.appveyor.com/project/raboof/fizzler 63 | [myget-badge]: https://img.shields.io/myget/raboof/vpre/Fizzler.svg?label=myget 64 | [edge-pkgs]: https://www.myget.org/feed/raboof/package/nuget/Fizzler 65 | [nuget-badge]: https://img.shields.io/nuget/v/Fizzler.svg 66 | [nuget-pkg]: https://www.nuget.org/packages/Fizzler 67 | 68 | [w3cs3]: https://www.w3.org/TR/selectors-3/ 69 | [fizzhap]: http://www.nuget.org/packages/Fizzler.Systems.HtmlAgilityPack/ 70 | [hap]: http://html-agility-pack.net/ 71 | -------------------------------------------------------------------------------- /tests/Throws.cs: -------------------------------------------------------------------------------- 1 | #region License and Terms 2 | // MoreLINQ - Extensions to LINQ to Objects 3 | // Copyright (c) 2022 Atif Aziz. All rights reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | #endregion 17 | 18 | // Modifications: 19 | // - Namespace changed 20 | // - Unused members removed 21 | // - Refactored "ObjectDisposedException" 22 | 23 | namespace Fizzler.Tests 24 | { 25 | using System; 26 | using NUnit.Framework.Constraints; 27 | 28 | static class Throws 29 | { 30 | public static ExactTypeConstraint InvalidOperationException => NUnit.Framework.Throws.InvalidOperationException; 31 | 32 | public static EqualConstraint ObjectDisposedException(string objectName) => 33 | ObjectDisposedException() 34 | .And.Property(nameof(System.ObjectDisposedException.ObjectName)) 35 | .EqualTo(objectName); 36 | 37 | public static ExactTypeConstraint ObjectDisposedException() => 38 | TypeOf(); 39 | 40 | public static ExactTypeConstraint TypeOf() 41 | where T : Exception => 42 | NUnit.Framework.Throws.TypeOf(); 43 | 44 | public static EqualConstraint ArgumentException(string expectedParamName) => 45 | NUnit.Framework.Throws.ArgumentException.With.ParamName(expectedParamName); 46 | 47 | public static EqualConstraint ArgumentNullException(string expectedParamName) => 48 | NUnit.Framework.Throws.ArgumentNullException.With.ParamName(expectedParamName); 49 | 50 | public static EqualConstraint ParamName(this ConstraintExpression constraint, string expectedParamName) => 51 | constraint.ParamName().EqualTo(expectedParamName); 52 | 53 | static ResolvableConstraintExpression ParamName(this ConstraintExpression constraint) => 54 | constraint.Property(nameof(System.ArgumentException.ParamName)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/UnreachableException.cs: -------------------------------------------------------------------------------- 1 | #region License and Terms 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) .NET Foundation and Contributors 5 | // 6 | // All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | #endregion 26 | 27 | #if NET7_0_OR_GREATER 28 | 29 | global using UnreachableException = System.Diagnostics.UnreachableException; 30 | 31 | #else 32 | 33 | namespace Fizzler 34 | { 35 | using System; 36 | 37 | // Source: https://github.com/dotnet/runtime/blob/v7.0.2/src/libraries/System.Private.CoreLib/src/System/Diagnostics/UnreachableException.cs 38 | 39 | /// 40 | /// Exception thrown when the program executes an instruction that was thought to be unreachable. 41 | /// 42 | 43 | #if !NETSTANDARD1_0 44 | [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] 45 | #endif 46 | #pragma warning disable CA1064 // Exceptions should be public 47 | sealed class UnreachableException : Exception 48 | #pragma warning restore CA1064 // Exceptions should be public 49 | { 50 | public UnreachableException() : 51 | this(null) { } 52 | 53 | public UnreachableException(string? message) : 54 | base(message, null) { } 55 | 56 | public UnreachableException(string? message, Exception? innerException) : 57 | base(message ?? "The program executed an instruction that was thought to be unreachable.", 58 | innerException) { } 59 | } 60 | } 61 | 62 | #endif // NET7_0_OR_GREATER 63 | -------------------------------------------------------------------------------- /src/SelectorsCachingCompiler.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler 23 | { 24 | #region Imports 25 | 26 | using System; 27 | using System.Collections.Generic; 28 | 29 | #endregion 30 | 31 | /// 32 | /// Implementation for a selectors compiler that supports caching. 33 | /// 34 | /// 35 | /// This class is primarily targeted for developers of selection 36 | /// over an arbitrary document model. 37 | /// 38 | public static class SelectorsCachingCompiler 39 | { 40 | /// 41 | /// Creates a caching selectors compiler on top on an existing compiler. 42 | /// 43 | public static Func Create(Func compiler) => 44 | Create(compiler, null); 45 | 46 | /// 47 | /// Creates a caching selectors compiler on top on an existing compiler. 48 | /// An addition parameter specified a dictionary to use as the cache. 49 | /// 50 | /// 51 | /// If is null then this method uses a 52 | /// the implementation with an 53 | /// ordinally case-insensitive selectors text comparer. 54 | /// 55 | public static Func Create(Func compiler, IDictionary? cache) 56 | { 57 | if(compiler == null) throw new ArgumentNullException(nameof(compiler)); 58 | return CreateImpl(compiler, cache ?? new Dictionary(StringComparer.OrdinalIgnoreCase)); 59 | } 60 | 61 | static Func CreateImpl(Func compiler, IDictionary cache) => 62 | selector => cache.TryGetValue(selector, out var compiled) 63 | ? compiled 64 | : cache[selector] = compiler(selector); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Fizzler.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard1.0;netstandard2.0 5 | 12 6 | enable 7 | true 8 | true 9 | Fizzler.snk 10 | 1.4.0 11 | true 12 | Atif Aziz, Colin Ramsay 13 | Fizzler is a W3C Selectors parser and generic selector framework for document hierarchies. 14 | LGPL-3.0-or-later 15 | README.md 16 | https://github.com/atifaziz/Fizzler 17 | https://github.com/atifaziz/Fizzler 18 | Git 19 | selectors w3c 20 | 21 | ..\dist 22 | Copyright © 2009 Atif Aziz, Colin Ramsay. All rights reserved. Portions Copyright © 2008 Novell, Inc. 23 | true 24 | True 25 | 26 | 27 | 28 | ..\bin\Debug\ 29 | 30 | 31 | 32 | ..\bin\Release\ 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | runtime; build; native; contentfiles; analyzers; buildtransitive 47 | all 48 | 49 | 50 | runtime; build; native; contentfiles; analyzers; buildtransitive 51 | all 52 | 53 | 54 | all 55 | runtime; build; native; contentfiles; analyzers; buildtransitive 56 | 57 | 58 | 59 | 60 | 61 | System.Index; 62 | System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute; 63 | System.Diagnostics.CodeAnalysis.NotNullWhenAttribute; 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/TokenKind.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | #pragma warning disable CA1720 // Identifier contains type name (by-design) 23 | 24 | namespace Fizzler 25 | { 26 | /// 27 | /// Represents the classification of a token. 28 | /// 29 | public enum TokenKind 30 | { 31 | /// 32 | /// Represents end of input/file/stream 33 | /// 34 | Eoi, 35 | 36 | /// 37 | /// Represents {ident} 38 | /// 39 | Ident, 40 | 41 | /// 42 | /// Represents "#" {name} 43 | /// 44 | Hash, 45 | 46 | /// 47 | /// Represents "~=" 48 | /// 49 | Includes, 50 | 51 | /// 52 | /// Represents "|=" 53 | /// 54 | DashMatch, 55 | 56 | /// 57 | /// Represents "^=" 58 | /// 59 | PrefixMatch, 60 | 61 | /// 62 | /// Represents "$=" 63 | /// 64 | SuffixMatch, 65 | 66 | /// 67 | /// Represents "*=" 68 | /// 69 | SubstringMatch, 70 | 71 | /// 72 | /// Represents {string} 73 | /// 74 | String, 75 | 76 | /// 77 | /// Represents S* "+" 78 | /// 79 | Plus, 80 | 81 | /// 82 | /// Represents S* ">" 83 | /// 84 | Greater, 85 | 86 | /// 87 | /// Represents [ \t\r\n\f]+ 88 | /// 89 | WhiteSpace, 90 | 91 | /// 92 | /// Represents {ident} "(" 93 | /// 94 | Function, 95 | 96 | /// 97 | /// Represents ":"{N}{O}{T}"(" 98 | /// 99 | Not, 100 | 101 | /// 102 | /// Represents [0-9]+ 103 | /// 104 | Integer, 105 | 106 | /// 107 | /// Represents S* "~" 108 | /// 109 | Tilde, 110 | 111 | /// 112 | /// Represents an arbitrary character 113 | /// 114 | Char, 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/NamespacePrefix.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler 23 | { 24 | using System; 25 | using System.Diagnostics.Contracts; 26 | 27 | /// 28 | /// Represent a type or attribute name. 29 | /// 30 | [Serializable] 31 | public readonly record struct NamespacePrefix 32 | { 33 | /// 34 | /// Represents a name from either the default or any namespace 35 | /// in a target document, depending on whether a default namespace is 36 | /// in effect or not. 37 | /// 38 | public static readonly NamespacePrefix None = new(null); 39 | 40 | /// 41 | /// Represents an empty namespace. 42 | /// 43 | public static readonly NamespacePrefix Empty = new(string.Empty); 44 | 45 | /// 46 | /// Represents any namespace. 47 | /// 48 | public static readonly NamespacePrefix Any = new("*"); 49 | 50 | /// 51 | /// Initializes an instance with a namespace prefix specification. 52 | /// 53 | public NamespacePrefix(string? text) : this() => Text = text; 54 | 55 | /// 56 | /// Gets the raw text value of this instance. 57 | /// 58 | public string? Text { get; } 59 | 60 | /// 61 | /// Indicates whether this instance represents a name 62 | /// from either the default or any namespace in a target 63 | /// document, depending on whether a default namespace is 64 | /// in effect or not. 65 | /// 66 | public bool IsNone => Text is null; 67 | 68 | /// 69 | /// Indicates whether this instance represents a name 70 | /// from any namespace (including one without one) 71 | /// in a target document. 72 | /// 73 | public bool IsAny => Text is ['*']; 74 | 75 | /// 76 | /// Indicates whether this instance represents a name 77 | /// without a namespace in a target document. 78 | /// 79 | public bool IsEmpty => Text is { Length: 0 }; 80 | 81 | /// 82 | /// Indicates whether this instance represents a name from a 83 | /// specific namespace or not. 84 | /// 85 | public bool IsSpecific => !IsNone && !IsAny; 86 | 87 | /// 88 | /// Returns a string representation of this instance. 89 | /// 90 | public override string ToString() => Text ?? "(none)"; 91 | 92 | /// 93 | /// Formats this namespace together with a name. 94 | /// 95 | [Pure] public string Format(string name) 96 | { 97 | if (name == null) throw new ArgumentNullException(nameof(name)); 98 | if (name.Length == 0) throw new ArgumentException(null, nameof(name)); 99 | 100 | return Text + (IsNone ? null : "|") + name; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,props,targets}] 11 | indent_size = 2 12 | 13 | [*.{sln}] 14 | indent_style = tab 15 | 16 | [*.{json,yml}] 17 | indent_size = 2 18 | 19 | [*.{cs,tt}] 20 | charset = utf-8 21 | indent_style = space 22 | indent_size = 4 23 | max_line_length = 100 24 | 25 | [*.cs] 26 | dotnet_analyzer_diagnostic.category-Style.severity = warning 27 | 28 | # IDE0022: Use expression/block body for methods 29 | dotnet_diagnostic.IDE0022.severity = suggestion 30 | 31 | # IDE1006: Naming rule violation 32 | dotnet_diagnostic.IDE1006.severity = warning 33 | 34 | # Naming capitalization styles 35 | dotnet_naming_style.camel_case_style.capitalization = camel_case 36 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 37 | 38 | # Naming rule that private instance fields must use camel case 39 | dotnet_naming_symbols.private_fields.applicable_kinds = field 40 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private 41 | dotnet_naming_rule.camel_case_private_fields.severity = warning 42 | dotnet_naming_rule.camel_case_private_fields.symbols = private_fields 43 | dotnet_naming_rule.camel_case_private_fields.style = camel_case_style 44 | 45 | # Naming rule that static read-only fields must use Pascal case 46 | dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field 47 | dotnet_naming_symbols.static_readonly_fields.applicable_accessibilities = * 48 | dotnet_naming_symbols.static_readonly_fields.required_modifiers = readonly, static 49 | dotnet_naming_rule.pascal_case_static_readonly_fields.severity = warning 50 | dotnet_naming_rule.pascal_case_static_readonly_fields.symbols = static_readonly_fields 51 | dotnet_naming_rule.pascal_case_static_readonly_fields.style = pascal_case_style 52 | 53 | # Naming rule that const fields must use Pascal case 54 | dotnet_naming_symbols.const_fields.applicable_kinds = field 55 | dotnet_naming_symbols.const_fields.applicable_accessibilities = * 56 | dotnet_naming_symbols.const_fields.required_modifiers = const 57 | dotnet_naming_rule.pascal_case_const_fields.severity = warning 58 | dotnet_naming_rule.pascal_case_const_fields.symbols = const_fields 59 | dotnet_naming_rule.pascal_case_const_fields.style = pascal_case_style 60 | 61 | # this. preferences 62 | dotnet_style_qualification_for_event = false 63 | dotnet_style_qualification_for_field = true 64 | dotnet_style_qualification_for_method = false 65 | dotnet_style_qualification_for_property = false 66 | 67 | # Prefer "var" everywhere 68 | csharp_style_var_for_built_in_types = true 69 | csharp_style_var_when_type_is_apparent = true 70 | csharp_style_var_elsewhere = true 71 | 72 | # Prefer method-like constructs to have a block body 73 | csharp_style_expression_bodied_methods = true 74 | csharp_style_expression_bodied_constructors = true 75 | csharp_style_expression_bodied_operators = true 76 | 77 | # Prefer property-like constructs to have an expression-body 78 | csharp_style_expression_bodied_properties = true 79 | csharp_style_expression_bodied_indexers = true 80 | csharp_style_expression_bodied_accessors = true 81 | 82 | # Suggest more modern language features when available 83 | csharp_style_pattern_matching_over_is_with_cast_check = true 84 | csharp_style_pattern_matching_over_as_with_null_check = true 85 | csharp_style_inlined_variable_declaration = true 86 | csharp_style_throw_expression = true 87 | csharp_style_conditional_delegate_call = true 88 | csharp_prefer_simple_default_expression = true 89 | 90 | # Spacing 91 | csharp_space_after_cast = false 92 | csharp_space_after_keywords_in_control_flow_statements = true 93 | csharp_space_between_method_declaration_parameter_list_parentheses = false 94 | 95 | # Wrapping 96 | csharp_preserve_single_line_statements = true 97 | csharp_preserve_single_line_blocks = true 98 | 99 | # Indentation 100 | csharp_indent_case_contents_when_block = false 101 | 102 | # Modifier preferences 103 | dotnet_style_require_accessibility_modifiers = omit_if_default 104 | 105 | # IDE0011: Add braces 106 | csharp_prefer_braces = when_multiline 107 | 108 | # IDE0061: Use block body for local functions 109 | csharp_style_expression_bodied_local_functions = true 110 | 111 | # IDE0065: Misplaced using directive 112 | csharp_using_directive_placement = inside_namespace 113 | 114 | # IDE0048: Add parentheses for clarity 115 | dotnet_diagnostic.IDE0048.severity = suggestion 116 | 117 | # IDE0055: Fix formatting 118 | dotnet_diagnostic.IDE0055.severity = suggestion 119 | 120 | # IDE0046: Convert to conditional expression 121 | dotnet_diagnostic.IDE0046.severity = suggestion 122 | -------------------------------------------------------------------------------- /src/Either.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2008 Novell, Inc. (http://www.novell.com) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining 5 | // a copy of this software and associated documentation files (the 6 | // "Software"), to deal in the Software without restriction, including 7 | // without limitation the rights to use, copy, modify, merge, publish, 8 | // distribute, sublicense, and/or sell copies of the Software, and to 9 | // permit persons to whom the Software is furnished to do so, subject to 10 | // the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be 13 | // included in all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | // 23 | 24 | namespace Fizzler 25 | { 26 | #region Imports 27 | 28 | using System; 29 | using System.Collections.Generic; 30 | 31 | #endregion 32 | 33 | // Adapted from Mono Rocks 34 | 35 | abstract class Either 36 | : IEquatable> 37 | { 38 | Either() {} 39 | 40 | public static Either A(TA value) => new AImpl(value); 41 | public static Either B(TB value) => new BImpl(value); 42 | 43 | public abstract override bool Equals(object? obj); 44 | public abstract bool Equals(Either obj); 45 | public abstract override int GetHashCode(); 46 | public abstract override string ToString(); 47 | public abstract TResult Fold(Func a, Func b); 48 | public abstract TResult Fold(T arg, Func a, Func b); 49 | 50 | sealed class AImpl(TA value) : Either 51 | { 52 | readonly TA value = value; 53 | 54 | public override int GetHashCode() => 55 | EqualityComparer.Default.GetHashCode(this.value); 56 | 57 | public override bool Equals(object? obj) => Equals(obj as AImpl); 58 | 59 | public override bool Equals(Either? obj) => 60 | obj is AImpl a 61 | && EqualityComparer.Default.Equals(this.value, a.value); 62 | 63 | public override TResult Fold(Func a, Func b) 64 | { 65 | if (a == null) throw new ArgumentNullException(nameof(a)); 66 | if (b == null) throw new ArgumentNullException(nameof(b)); 67 | return a(this.value); 68 | } 69 | 70 | public override TResult Fold(T arg, Func a, Func b) 71 | { 72 | if (a == null) throw new ArgumentNullException(nameof(a)); 73 | if (b == null) throw new ArgumentNullException(nameof(b)); 74 | return a(arg, this.value); 75 | } 76 | 77 | public override string ToString() => 78 | this.value?.ToString() ?? string.Empty; 79 | } 80 | 81 | sealed class BImpl(TB value) : Either 82 | { 83 | readonly TB value = value; 84 | 85 | public override int GetHashCode() => 86 | EqualityComparer.Default.GetHashCode(this.value); 87 | 88 | public override bool Equals(object? obj) => Equals(obj as BImpl); 89 | 90 | public override bool Equals(Either? obj) => 91 | obj is BImpl b 92 | && EqualityComparer.Default.Equals(this.value, b.value); 93 | 94 | public override TResult Fold(Func a, Func b) 95 | { 96 | if (a == null) throw new ArgumentNullException(nameof(a)); 97 | if (b == null) throw new ArgumentNullException(nameof(b)); 98 | return b(this.value); 99 | } 100 | 101 | public override TResult Fold(T arg, Func a, Func b) 102 | { 103 | if (a == null) throw new ArgumentNullException(nameof(a)); 104 | if (b == null) throw new ArgumentNullException(nameof(b)); 105 | return b(arg, this.value); 106 | } 107 | 108 | public override string ToString() => 109 | this.value?.ToString() ?? string.Empty; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Reader.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler 23 | { 24 | #region Imports 25 | 26 | using System; 27 | using System.Collections; 28 | using System.Collections.Generic; 29 | 30 | #endregion 31 | 32 | /// 33 | /// Adds reading semantics to a base with the 34 | /// option to un-read and insert new elements while consuming the source. 35 | /// 36 | public sealed class Reader : IDisposable, IEnumerable 37 | { 38 | IEnumerator? enumerator; 39 | Stack? buffer; 40 | 41 | /// 42 | /// Initialize a new with a base 43 | /// object. 44 | /// 45 | public Reader(IEnumerable e) : 46 | this(e is { } some ? some.GetEnumerator() : throw new ArgumentNullException(nameof(e))) { } 47 | 48 | /// 49 | /// Initialize a new with a base 50 | /// object. 51 | /// 52 | public Reader(IEnumerator e) 53 | { 54 | this.enumerator = e ?? throw new ArgumentNullException(nameof(e)); 55 | this.buffer = new Stack(); 56 | RealRead(); 57 | } 58 | 59 | ObjectDisposedException ObjectDisposedException() => new(GetType().Name); 60 | 61 | /// 62 | /// Indicates whether there is, at least, one value waiting to be read or not. 63 | /// 64 | public bool HasMore => this.buffer is { Count: var count } 65 | ? count > 0 66 | : throw ObjectDisposedException(); 67 | 68 | /// 69 | /// Pushes back a new value that will be returned on the next read. 70 | /// 71 | public void Unread(T value) 72 | { 73 | if (this.buffer is not { } someBuffer) 74 | throw ObjectDisposedException(); 75 | someBuffer.Push(value); 76 | } 77 | 78 | /// 79 | /// Reads and returns the next value. 80 | /// 81 | public T Read() 82 | { 83 | switch (this.buffer) 84 | { 85 | case null: 86 | throw ObjectDisposedException(); 87 | case { Count: 0 }: 88 | throw new InvalidOperationException(); 89 | case var someBuffer: 90 | var value = someBuffer.Pop(); 91 | if (someBuffer.Count == 0) 92 | RealRead(); 93 | return value; 94 | } 95 | } 96 | 97 | /// 98 | /// Peeks the next value waiting to be read. 99 | /// 100 | /// 101 | /// Thrown if there is no value waiting to be read. 102 | /// 103 | public T Peek() => this.buffer switch 104 | { 105 | null => throw ObjectDisposedException(), 106 | { Count: 0 } => throw new InvalidOperationException(), 107 | var buffer => buffer.Peek() 108 | }; 109 | 110 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 111 | 112 | /// 113 | /// Returns an enumerator that iterates through the remaining 114 | /// values to be read. 115 | /// 116 | public IEnumerator GetEnumerator() 117 | { 118 | return this.enumerator is not null ? Iterator(this) : throw ObjectDisposedException(); 119 | 120 | static IEnumerator Iterator(Reader reader) 121 | { 122 | while (reader.HasMore) 123 | yield return reader.Read(); 124 | } 125 | } 126 | 127 | void RealRead() 128 | { 129 | if (this.enumerator is not { } someEnumerator) 130 | throw ObjectDisposedException(); 131 | 132 | if (someEnumerator.MoveNext()) 133 | Unread(someEnumerator.Current); 134 | } 135 | 136 | /// 137 | /// Disposes the enumerator used to initialize this object 138 | /// if that enumerator supports . 139 | /// 140 | public void Close() => Dispose(); 141 | 142 | #pragma warning disable CA1063 // Implement IDisposable correctly (false negative) 143 | void IDisposable.Dispose() => Dispose(); 144 | #pragma warning restore CA1063 // Implement IDisposable correctly 145 | 146 | void Dispose() 147 | { 148 | if(this.enumerator == null) 149 | return; 150 | this.enumerator.Dispose(); 151 | this.enumerator = null; 152 | this.buffer = null; 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /tests/HumanReadableSelectorGeneratorTests.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler.Tests 23 | { 24 | using NUnit.Framework; 25 | 26 | [TestFixture] 27 | public class HumanReadableSelectorGeneratorTests 28 | { 29 | public class TestHumanReadableSelectorGenerator : HumanReadableSelectorGenerator 30 | { 31 | public new void Add(string selector) 32 | { 33 | base.Add(selector); 34 | } 35 | } 36 | 37 | [Test] 38 | public void Null_Selector() 39 | { 40 | var generator = new TestHumanReadableSelectorGenerator(); 41 | Assert.That(() => generator.Add(null!), Throws.ArgumentNullException("selector")); 42 | } 43 | 44 | [Test] 45 | public void All_Elements() 46 | { 47 | Run("*", "Take all elements and select them."); 48 | } 49 | 50 | [Test] 51 | public void Tag() 52 | { 53 | Run("p", "Take all

elements and select them."); 54 | } 55 | 56 | [Test] 57 | public void Descendant() 58 | { 59 | Run("p a", "Take all

elements, then take their descendants which are elements and select them."); 60 | } 61 | 62 | [Test] 63 | public void Three_Levels_Of_Descendant() 64 | { 65 | Run("p a img", "Take all

elements, then take their descendants which are elements. With those, take only their descendants which are elements and select them."); 66 | } 67 | 68 | [Test] 69 | public void Attribute() 70 | { 71 | Run("a[href]", "Take all elements which have attribute href defined and select them."); 72 | } 73 | 74 | [Test] 75 | public void Adjacent() 76 | { 77 | Run("a + span", "Take all elements, then take their immediate siblings which are elements and select them."); 78 | } 79 | 80 | [Test] 81 | public void Id() 82 | { 83 | Run("#nodeId", "Take all elements with an ID of 'nodeId' and select them."); 84 | } 85 | 86 | [Test] 87 | public void SelectorGroup() 88 | { 89 | Run("a, span", "Take all elements and select them. Combined with previous, take all elements and select them."); 90 | } 91 | 92 | [Test] 93 | public void GeneralSibling() 94 | { 95 | Run("div ~ p", "Take all

elements, then take their siblings which are

elements and select them."); 96 | } 97 | 98 | [Test] 99 | public void Empty() 100 | { 101 | Run("*:empty", "Take all elements where the element is empty and select them."); 102 | } 103 | 104 | [Test] 105 | public void FirstChild() 106 | { 107 | Run("*:first-child", "Take all elements which are the first child of their parent and select them."); 108 | } 109 | 110 | [Test] 111 | public void Child() 112 | { 113 | Run("* > p", "Take all elements, then take their immediate children which are

elements and select them."); 114 | } 115 | 116 | [Test] public void Class() 117 | { 118 | Run(".myclass", "Take all elements with a class of 'myclass' and select them."); 119 | } 120 | 121 | [Test] public void LastChild() 122 | { 123 | Run("*:last-child", "Take all elements which are the last child of their parent and select them."); 124 | } 125 | 126 | [Test] 127 | public void NthChild() 128 | { 129 | Run("*:nth-child(2)", "Take all elements where the element has 1n+2-1 sibling before it and select them."); 130 | } 131 | 132 | [Test] 133 | public void OnlyChild() 134 | { 135 | Run("*:only-child", "Take all elements where the element is the only child and select them."); 136 | } 137 | 138 | [Test] public void AttributeDashMatch() 139 | { 140 | Run("*[lang|='en']", "Take all elements which have attribute lang with a hyphen separated value matching 'en' and select them."); 141 | } 142 | [Test] public void AttributeExact() 143 | { 144 | Run("*[title='hithere']", "Take all elements which have attribute title with a value of 'hithere' and select them."); 145 | } 146 | [Test] public void AttributeIncludes() 147 | { 148 | Run("*[title~='hithere']", "Take all elements which have attribute title that includes the word 'hithere' and select them."); 149 | } 150 | [Test] public void AttributeSubstring() 151 | { 152 | Run("*[title*='hithere']", "Take all elements which have attribute title whose value contains 'hithere' and select them."); 153 | } 154 | [Test] public void AttributeSuffixMatch() 155 | { 156 | Run("*[title$='hithere']", "Take all elements which have attribute title whose value ends with 'hithere' and select them."); 157 | } 158 | 159 | [Test] 160 | public void AttributePrefixMatch() 161 | { 162 | Run("*[title^='hithere']", "Take all elements which have attribute title whose value begins with 'hithere' and select them."); 163 | } 164 | 165 | static void Run(string selector, string message) 166 | { 167 | var generator = Parser.Parse(selector, new HumanReadableSelectorGenerator()); 168 | Assert.That(generator.Text, Is.EqualTo(message)); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /tests/NamespacePrefixTests.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler.Tests 23 | { 24 | using NUnit.Framework; 25 | 26 | [TestFixture] 27 | public class NamespacePrefixTests 28 | { 29 | [Test] 30 | public void Initialization() 31 | { 32 | Assert.That(new NamespacePrefix("foo").Text, Is.EqualTo("foo")); 33 | } 34 | 35 | [Test] 36 | public void NoneText() 37 | { 38 | Assert.That(NamespacePrefix.None.Text, Is.Null); 39 | } 40 | 41 | [Test] 42 | public void NoneIsNone() 43 | { 44 | Assert.That(NamespacePrefix.None.IsNone, Is.True); 45 | } 46 | 47 | [Test] 48 | public void NoneIsNotAny() 49 | { 50 | Assert.That(NamespacePrefix.None.IsAny, Is.False); 51 | } 52 | 53 | [Test] 54 | public void NoneIsNotEmpty() 55 | { 56 | Assert.That(NamespacePrefix.None.IsEmpty, Is.False); 57 | } 58 | 59 | [Test] 60 | public void NoneIsNotSpecific() 61 | { 62 | Assert.That(NamespacePrefix.None.IsSpecific, Is.False); 63 | } 64 | 65 | [Test] 66 | public void AnyText() 67 | { 68 | Assert.That(NamespacePrefix.Any.Text, Is.EqualTo("*")); 69 | } 70 | 71 | [Test] 72 | public void AnyIsNotNone() 73 | { 74 | Assert.That(NamespacePrefix.Any.IsNone, Is.False); 75 | } 76 | 77 | [Test] 78 | public void AnyIsAny() 79 | { 80 | Assert.That(NamespacePrefix.Any.IsAny, Is.True); 81 | } 82 | 83 | [Test] 84 | public void AnyIsNotEmpty() 85 | { 86 | Assert.That(NamespacePrefix.Any.IsEmpty, Is.False); 87 | } 88 | 89 | [Test] 90 | public void AnyIsNotSpecific() 91 | { 92 | Assert.That(NamespacePrefix.Any.IsSpecific, Is.False); 93 | } 94 | 95 | [Test] 96 | public void EmptyText() 97 | { 98 | Assert.That(NamespacePrefix.Empty.Text, Is.EqualTo(string.Empty)); 99 | } 100 | 101 | [Test] 102 | public void EmptyIsNotNone() 103 | { 104 | Assert.That(NamespacePrefix.Empty.IsNone, Is.False); 105 | } 106 | 107 | [Test] 108 | public void EmptyIsNotAny() 109 | { 110 | Assert.That(NamespacePrefix.Empty.IsAny, Is.False); 111 | } 112 | 113 | [Test] 114 | public void EmptyIsEmpty() 115 | { 116 | Assert.That(NamespacePrefix.Empty.IsEmpty, Is.True); 117 | } 118 | 119 | [Test] 120 | public void EmptyIsSpecific() 121 | { 122 | Assert.That(NamespacePrefix.Empty.IsSpecific, Is.True); 123 | } 124 | 125 | [Test] 126 | public void Equality() 127 | { 128 | Assert.That(NamespacePrefix.None.Equals(NamespacePrefix.None), Is.True); 129 | Assert.That(NamespacePrefix.Any.Equals(NamespacePrefix.Any), Is.True); 130 | Assert.That(NamespacePrefix.Empty.Equals(NamespacePrefix.Empty), Is.True); 131 | var foo = new NamespacePrefix("foo"); 132 | Assert.That(foo.Equals(foo), Is.True); 133 | Assert.That(foo.Equals((object)foo), Is.True); 134 | } 135 | 136 | [Test] 137 | public void Inequality() 138 | { 139 | var foo = new NamespacePrefix("foo"); 140 | var bar = new NamespacePrefix("bar"); 141 | Assert.That(foo.Equals(bar), Is.False); 142 | Assert.That(foo.Equals((object)bar), Is.False); 143 | } 144 | 145 | [Test] 146 | public void TypeEquality() 147 | { 148 | var foo = new NamespacePrefix("foo"); 149 | Assert.That(foo, Is.Not.EqualTo(null)); 150 | Assert.That(foo, Is.Not.EqualTo(123)); 151 | } 152 | 153 | [Test] 154 | public void NoneHashCode() 155 | { 156 | Assert.That(0, Is.EqualTo(NamespacePrefix.None.GetHashCode())); 157 | } 158 | 159 | [Test] 160 | public void HashCode() 161 | { 162 | var foo = new NamespacePrefix("foo"); 163 | var bar = new NamespacePrefix("bar"); 164 | Assert.That(foo.GetHashCode() == bar.GetHashCode(), Is.False); 165 | } 166 | 167 | [Test] 168 | public void NoneStringRepresentation() 169 | { 170 | Assert.That(NamespacePrefix.None.ToString(), Is.EqualTo("(none)")); 171 | } 172 | 173 | [Test] 174 | public void EmptyStringRepresentation() 175 | { 176 | Assert.That(NamespacePrefix.Empty.ToString(), Is.EqualTo(string.Empty)); 177 | } 178 | 179 | [Test] 180 | public void AnyStringRepresentation() 181 | { 182 | Assert.That(NamespacePrefix.Any.ToString(), Is.EqualTo("*")); 183 | } 184 | 185 | [Test] 186 | public void StringRepresentation() 187 | { 188 | Assert.That(new NamespacePrefix("foo").ToString(), Is.EqualTo("foo")); 189 | } 190 | 191 | [Test] 192 | public void FormatNone() 193 | { 194 | Assert.That(NamespacePrefix.None.Format("name"), Is.EqualTo("name")); 195 | } 196 | 197 | [Test] 198 | public void FormatAny() 199 | { 200 | Assert.That(NamespacePrefix.Any.Format("name"), Is.EqualTo("*|name")); 201 | } 202 | 203 | [Test] 204 | public void FormatEmpty() 205 | { 206 | Assert.That(NamespacePrefix.Empty.Format("name"), Is.EqualTo("|name")); 207 | } 208 | 209 | [Test] 210 | public void Format() 211 | { 212 | Assert.That(new NamespacePrefix("foo").Format("bar"), Is.EqualTo("foo|bar")); 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /tests/ReaderTests.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler.Tests 23 | { 24 | #region Imports 25 | 26 | using System; 27 | using System.Collections; 28 | using System.Collections.Generic; 29 | using NUnit.Framework; 30 | 31 | #endregion 32 | 33 | [TestFixture] 34 | public class ReaderTests 35 | { 36 | [Test] 37 | public void NullEnumeratorInitialization() 38 | { 39 | Assert.That(() => new Reader((IEnumerator)null!), 40 | Throws.ArgumentNullException("e")); 41 | } 42 | 43 | [Test] 44 | public void NullEnumerableInitialization() 45 | { 46 | Assert.That(() => new Reader((IEnumerable)null!), 47 | Throws.ArgumentNullException("e")); 48 | } 49 | 50 | [Test] 51 | public void HasMoreWhenEmpty() 52 | { 53 | using var reader = new Reader([]); 54 | Assert.That(reader.HasMore, Is.False); 55 | } 56 | 57 | [Test] 58 | public void HasMoreWhenNotEmpty() 59 | { 60 | using var reader = new Reader(new int[1]); 61 | Assert.That(reader.HasMore, Is.True); 62 | } 63 | 64 | [Test] 65 | public void ReadEmpty() 66 | { 67 | using var reader = new Reader([]); 68 | Assert.That(reader.Read, Throws.InvalidOperationException); 69 | } 70 | 71 | [Test] 72 | public void Unreading() 73 | { 74 | using var reader = new Reader([78, 910]); 75 | reader.Unread(56); 76 | reader.Unread(34); 77 | reader.Unread(12); 78 | Assert.That(reader.Read(), Is.EqualTo(12)); 79 | Assert.That(reader.Read(), Is.EqualTo(34)); 80 | Assert.That(reader.Read(), Is.EqualTo(56)); 81 | Assert.That(reader.Read(), Is.EqualTo(78)); 82 | Assert.That(reader.Read(), Is.EqualTo(910)); 83 | } 84 | 85 | [Test] 86 | public void PeekEmpty() 87 | { 88 | using var reader = new Reader([]); 89 | Assert.That(reader.Peek, Throws.InvalidOperationException); 90 | } 91 | 92 | [Test] 93 | public void PeekNonEmpty() 94 | { 95 | using var reader = new Reader([12, 34, 56]); 96 | Assert.That(reader.Peek(), Is.EqualTo(12)); 97 | Assert.That(reader.Read(), Is.EqualTo(12)); 98 | Assert.That(reader.Peek(), Is.EqualTo(34)); 99 | Assert.That(reader.Read(), Is.EqualTo(34)); 100 | Assert.That(reader.Peek(), Is.EqualTo(56)); 101 | Assert.That(reader.Read(), Is.EqualTo(56)); 102 | } 103 | 104 | [Test] 105 | public void Enumeration() 106 | { 107 | using var reader = new Reader([12, 34, 56]); 108 | using var e = reader.GetEnumerator(); 109 | Assert.That(e.MoveNext(), Is.True); 110 | Assert.That(e.Current, Is.EqualTo(12)); 111 | Assert.That(e.MoveNext(), Is.True); 112 | Assert.That(e.Current, Is.EqualTo(34)); 113 | Assert.That(e.MoveNext(), Is.True); 114 | Assert.That(e.Current, Is.EqualTo(56)); 115 | Assert.That(e.MoveNext(), Is.False); 116 | } 117 | 118 | [Test] 119 | public void EnumerationNonGeneric() 120 | { 121 | using var reader = new Reader([12, 34, 56]); 122 | var e = ((IEnumerable)reader).GetEnumerator(); 123 | Assert.That(e.MoveNext(), Is.True); 124 | Assert.That(e.Current, Is.EqualTo(12)); 125 | Assert.That(e.MoveNext(), Is.True); 126 | Assert.That(e.Current, Is.EqualTo(34)); 127 | Assert.That(e.MoveNext(), Is.True); 128 | Assert.That(e.Current, Is.EqualTo(56)); 129 | Assert.That(e.MoveNext(), Is.False); 130 | } 131 | 132 | [Test] 133 | public void CloseDisposes() 134 | { 135 | using var e = new TestEnumerator(); 136 | Assert.That(e.Disposed, Is.False); 137 | new Reader(e).Close(); 138 | Assert.That(e.Disposed, Is.True); 139 | } 140 | 141 | [Test] 142 | public void DisposeDisposes() 143 | { 144 | using var e = new TestEnumerator(); 145 | Assert.That(e.Disposed, Is.False); 146 | ((IDisposable)new Reader(e)).Dispose(); 147 | Assert.That(e.Disposed, Is.True); 148 | } 149 | 150 | [Test] 151 | public void DisposeDisposesOnce() 152 | { 153 | using var e = new TestEnumerator(); 154 | Assert.That(e.Disposed, Is.False); 155 | IDisposable disposable = new Reader(e); 156 | disposable.Dispose(); 157 | Assert.That(e.DisposeCallCount, Is.EqualTo(1)); 158 | disposable.Dispose(); 159 | Assert.That(e.DisposeCallCount, Is.EqualTo(1)); 160 | } 161 | 162 | [Test] 163 | public void HasMoreDisposed() 164 | { 165 | Assert.That(() => _ = CreateDisposedReader().HasMore, 166 | Throws.ObjectDisposedException(typeof(Reader<>).Name)); 167 | } 168 | 169 | [Test] 170 | public void ReadDisposed() 171 | { 172 | Assert.That(() => _ = CreateDisposedReader().Read(), 173 | Throws.ObjectDisposedException(typeof(Reader<>).Name)); 174 | } 175 | 176 | [Test] 177 | public void UnreadDisposed() 178 | { 179 | Assert.That(() => CreateDisposedReader().Unread(42), 180 | Throws.ObjectDisposedException(typeof(Reader<>).Name)); 181 | } 182 | 183 | [Test] 184 | public void PeekDisposed() 185 | { 186 | Assert.That(() => CreateDisposedReader().Peek(), 187 | Throws.ObjectDisposedException(typeof(Reader<>).Name)); 188 | } 189 | 190 | [Test] 191 | public void EnumerateDisposed() 192 | { 193 | Assert.That(() => CreateDisposedReader().GetEnumerator(), 194 | Throws.ObjectDisposedException(typeof(Reader<>).Name)); 195 | } 196 | 197 | static Reader CreateDisposedReader() 198 | { 199 | var reader = new Reader([]); 200 | reader.Close(); 201 | return reader; 202 | } 203 | 204 | sealed class TestEnumerator : IEnumerator 205 | { 206 | public int DisposeCallCount { get; private set; } 207 | public bool Disposed => DisposeCallCount > 0; 208 | 209 | public void Dispose() => DisposeCallCount++; 210 | public bool MoveNext() => false; 211 | public void Reset() => throw new NotImplementedException(); 212 | public T Current => throw new NotImplementedException(); 213 | object? IEnumerator.Current => Current; 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/Token.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler 23 | { 24 | using System; 25 | 26 | /// 27 | /// Represent a token and optionally any text associated with it. 28 | /// 29 | public readonly record struct Token 30 | { 31 | /// 32 | /// Gets the kind/type/class of the token. 33 | /// 34 | public TokenKind Kind { get; } 35 | 36 | /// 37 | /// Gets text, if any, associated with the token. 38 | /// 39 | public string? Text { get; } 40 | 41 | internal string SomeText => Text ?? throw new InvalidOperationException(); 42 | 43 | Token(TokenKind kind, string? text = null) : this() 44 | { 45 | Kind = kind; 46 | Text = text; 47 | } 48 | 49 | /// 50 | /// Creates an end-of-input token. 51 | /// 52 | public static Token Eoi() => new(TokenKind.Eoi); 53 | 54 | static readonly Token StarToken = Char('*'); 55 | static readonly Token DotToken = Char('.'); 56 | static readonly Token ColonToken = Char(':'); 57 | static readonly Token CommaToken = Char(','); 58 | static readonly Token RightParenthesisToken = Char(')'); 59 | static readonly Token EqualsToken = Char('='); 60 | static readonly Token PipeToken = Char('|'); 61 | static readonly Token LeftBracketToken = Char('['); 62 | static readonly Token RightBracketToken = Char(']'); 63 | 64 | /// 65 | /// Creates a star token. 66 | /// 67 | public static Token Star() => StarToken; 68 | 69 | /// 70 | /// Creates a dot token. 71 | /// 72 | public static Token Dot() => DotToken; 73 | 74 | /// 75 | /// Creates a colon token. 76 | /// 77 | public static Token Colon() => ColonToken; 78 | 79 | /// 80 | /// Creates a comma token. 81 | /// 82 | public static Token Comma() => CommaToken; 83 | 84 | /// 85 | /// Creates a right parenthesis token. 86 | /// 87 | public static Token RightParenthesis() => RightParenthesisToken; 88 | 89 | /// 90 | /// Creates an equals token. 91 | /// 92 | public static Token Equals() => EqualsToken; 93 | 94 | /// 95 | /// Creates a left bracket token. 96 | /// 97 | public static Token LeftBracket() => LeftBracketToken; 98 | 99 | /// 100 | /// Creates a right bracket token. 101 | /// 102 | public static Token RightBracket() => RightBracketToken; 103 | 104 | /// 105 | /// Creates a pipe (vertical line) token. 106 | /// 107 | public static Token Pipe() => PipeToken; 108 | 109 | /// 110 | /// Creates a plus token. 111 | /// 112 | public static Token Plus() => new(TokenKind.Plus); 113 | 114 | /// 115 | /// Creates a greater token. 116 | /// 117 | public static Token Greater() => new(TokenKind.Greater); 118 | 119 | /// 120 | /// Creates an includes token. 121 | /// 122 | public static Token Includes() => new(TokenKind.Includes); 123 | 124 | /// 125 | /// Creates a dash-match token. 126 | /// 127 | public static Token DashMatch() => new(TokenKind.DashMatch); 128 | 129 | /// 130 | /// Creates a prefix-match token. 131 | /// 132 | public static Token PrefixMatch() => new(TokenKind.PrefixMatch); 133 | 134 | /// 135 | /// Creates a suffix-match token. 136 | /// 137 | public static Token SuffixMatch() => new(TokenKind.SuffixMatch); 138 | 139 | /// 140 | /// Creates a substring-match token. 141 | /// 142 | public static Token SubstringMatch() => new(TokenKind.SubstringMatch); 143 | 144 | /// 145 | /// Creates a general sibling token. 146 | /// 147 | public static Token Tilde() => new(TokenKind.Tilde); 148 | 149 | /// 150 | /// Creates an identifier token. 151 | /// 152 | public static Token Ident(string text) 153 | { 154 | ValidateTextArgument(text); 155 | return new Token(TokenKind.Ident, text); 156 | } 157 | 158 | /// 159 | /// Creates an integer token. 160 | /// 161 | #pragma warning disable CA1720 // Identifier contains type name (by-design) 162 | public static Token Integer(string text) 163 | #pragma warning restore CA1720 // Identifier contains type name 164 | { 165 | ValidateTextArgument(text); 166 | return new Token(TokenKind.Integer, text); 167 | } 168 | 169 | /// 170 | /// Creates a hash-name token. 171 | /// 172 | public static Token Hash(string text) 173 | { 174 | ValidateTextArgument(text); 175 | return new Token(TokenKind.Hash, text); 176 | } 177 | 178 | /// 179 | /// Creates a white-space token. 180 | /// 181 | public static Token WhiteSpace(string space) 182 | { 183 | ValidateTextArgument(space); 184 | return new Token(TokenKind.WhiteSpace, space); 185 | } 186 | 187 | /// 188 | /// Creates a string token. 189 | /// 190 | #pragma warning disable CA1720 // Identifier contains type name (by-design) 191 | public static Token String(string? text) => 192 | new(TokenKind.String, text ?? string.Empty); 193 | #pragma warning restore CA1720 // Identifier contains type name 194 | 195 | /// 196 | /// Creates a function token. 197 | /// 198 | public static Token Function(string text) 199 | { 200 | ValidateTextArgument(text); 201 | return new Token(TokenKind.Function, text); 202 | } 203 | 204 | /// 205 | /// Creates a not token. 206 | /// 207 | public static Token Not() => new(TokenKind.Not); 208 | 209 | /// 210 | /// Creates an arbitrary character token. 211 | /// 212 | #pragma warning disable CA1720 // Identifier contains type name (by-design) 213 | public static Token Char(char ch) => new(TokenKind.Char, ch.ToString()); 214 | #pragma warning restore CA1720 // Identifier contains type name 215 | 216 | /// 217 | /// Gets a string representation of the token. 218 | /// 219 | public override string ToString() => 220 | Text is { } text ? Kind + ": " + text : Kind.ToString(); 221 | 222 | static void ValidateTextArgument(string text) 223 | { 224 | if (text == null) throw new ArgumentNullException(nameof(text)); 225 | if (text.Length == 0) throw new ArgumentException(null, nameof(text)); 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/HumanReadableSelectorGenerator.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler 23 | { 24 | using System; 25 | 26 | /// 27 | /// An implementation that generates 28 | /// human-readable description of the selector. 29 | /// 30 | public class HumanReadableSelectorGenerator : ISelectorGenerator 31 | { 32 | int chainCount; 33 | string? text; 34 | 35 | /// 36 | /// Initializes the text. 37 | /// 38 | public virtual void OnInit() => this.text = null; 39 | 40 | /// 41 | /// Gets the generated human-readable description text. 42 | /// 43 | public string Text => this.text ?? throw new InvalidOperationException(); 44 | 45 | /// 46 | /// Generates human-readable for a selector in a group. 47 | /// 48 | public virtual void OnSelector() 49 | { 50 | if (string.IsNullOrEmpty(this.text)) 51 | this.text = "Take all"; 52 | else 53 | this.text += " and select them. Combined with previous, take all"; 54 | } 55 | 56 | /// 57 | /// Concludes the text. 58 | /// 59 | public virtual void OnClose() 60 | { 61 | if (this.text is not { } someText) 62 | throw new InvalidOperationException(); 63 | 64 | this.text = $"{someText.Trim()} and select them."; 65 | } 66 | 67 | /// 68 | /// Adds to the generated human-readable text. 69 | /// 70 | protected void Add(string selector) => 71 | this.text += selector ?? throw new ArgumentNullException(nameof(selector)); 72 | 73 | /// 74 | /// Generates human-readable text of this type selector. 75 | /// 76 | #pragma warning disable CA1725 // Parameter names should match base declaration (compatibility) 77 | public void Type(NamespacePrefix prefix, string type) => 78 | #pragma warning restore CA1725 // Parameter names should match base declaration 79 | Add($" <{type}> elements"); 80 | 81 | /// 82 | /// Generates human-readable text of this universal selector. 83 | /// 84 | public void Universal(NamespacePrefix prefix) => 85 | Add(" elements"); 86 | 87 | /// 88 | /// Generates human-readable text of this ID selector. 89 | /// 90 | public void Id(string id) => 91 | Add($" with an ID of '{id}'"); 92 | 93 | /// 94 | /// Generates human-readable text of this class selector. 95 | /// 96 | public void Class(string clazz) => 97 | Add($" with a class of '{clazz}'"); 98 | 99 | /// 100 | /// Generates human-readable text of this attribute selector. 101 | /// 102 | public void AttributeExists(NamespacePrefix prefix, string name) => 103 | Add($" which have attribute {name} defined"); 104 | 105 | /// 106 | /// Generates human-readable text of this attribute selector. 107 | /// 108 | public void AttributeExact(NamespacePrefix prefix, string name, string value) => 109 | Add($" which have attribute {name} with a value of '{value}'"); 110 | 111 | /// 112 | /// Generates human-readable text of this attribute selector. 113 | /// 114 | public void AttributeIncludes(NamespacePrefix prefix, string name, string value) => 115 | Add($" which have attribute {name} that includes the word '{value}'"); 116 | 117 | /// 118 | /// Generates human-readable text of this attribute selector. 119 | /// 120 | public void AttributeDashMatch(NamespacePrefix prefix, string name, string value) => 121 | Add($" which have attribute {name} with a hyphen separated value matching '{value}'"); 122 | 123 | /// 124 | /// Generates human-readable text of this attribute selector. 125 | /// 126 | public void AttributePrefixMatch(NamespacePrefix prefix, string name, string value) => 127 | Add($" which have attribute {name} whose value begins with '{value}'"); 128 | 129 | /// 130 | /// Generates human-readable text of this attribute selector. 131 | /// 132 | public void AttributeSuffixMatch(NamespacePrefix prefix, string name, string value) => 133 | Add($" which have attribute {name} whose value ends with '{value}'"); 134 | 135 | /// 136 | /// Generates human-readable text of this attribute selector. 137 | /// 138 | public void AttributeSubstring(NamespacePrefix prefix, string name, string value) => 139 | Add($" which have attribute {name} whose value contains '{value}'"); 140 | 141 | /// 142 | /// Generates human-readable text of this pseudo-class selector. 143 | /// 144 | public void FirstChild() => 145 | Add(" which are the first child of their parent"); 146 | 147 | /// 148 | /// Generates human-readable text of this pseudo-class selector. 149 | /// 150 | public void LastChild() => 151 | Add(" which are the last child of their parent"); 152 | 153 | /// 154 | /// Generates human-readable text of this pseudo-class selector. 155 | /// 156 | public void NthChild(int a, int b) => 157 | Add($" where the element has {a}n+{b}-1 sibling before it"); 158 | 159 | /// 160 | /// Generates human-readable text of this pseudo-class selector. 161 | /// 162 | public void OnlyChild() => 163 | Add(" where the element is the only child"); 164 | 165 | /// 166 | /// Generates human-readable text of this pseudo-class selector. 167 | /// 168 | public void Empty() => 169 | Add(" where the element is empty"); 170 | 171 | /// 172 | /// Generates human-readable text of this combinator. 173 | /// 174 | public void Child() => 175 | Add(", then take their immediate children which are"); 176 | 177 | /// 178 | /// Generates human-readable text of this combinator. 179 | /// 180 | public void Descendant() 181 | { 182 | if (this.chainCount > 0) 183 | { 184 | Add(". With those, take only their descendants which are"); 185 | } 186 | else 187 | { 188 | Add(", then take their descendants which are"); 189 | this.chainCount++; 190 | } 191 | } 192 | 193 | /// 194 | /// Generates human-readable text of this combinator. 195 | /// 196 | public void Adjacent() => 197 | Add(", then take their immediate siblings which are"); 198 | 199 | /// 200 | /// Generates a combinator, 201 | /// which separates two sequences of simple selectors. The elements represented 202 | /// by the two sequences share the same parent in the document tree and the 203 | /// element represented by the first sequence precedes (not necessarily 204 | /// immediately) the element represented by the second one. 205 | /// 206 | public void GeneralSibling() => 207 | Add(", then take their siblings which are"); 208 | 209 | /// 210 | /// Generates human-readable text of this combinator. 211 | /// 212 | public void NthLastChild(int a, int b) => 213 | Add($" where the element has {a}n+{b}-1 sibling after it"); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/IElementOps.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler 23 | { 24 | /// 25 | /// Represents a selectors implementation for an arbitrary document/node system. 26 | /// 27 | public interface IElementOps 28 | { 29 | // 30 | // Selectors 31 | // 32 | 33 | /// 34 | /// Generates a type selector, 35 | /// which represents an instance of the element type in the document tree. 36 | /// 37 | Selector Type(NamespacePrefix prefix, string name); 38 | 39 | /// 40 | /// Generates a universal selector, 41 | /// any single element in the document tree in any namespace 42 | /// (including those without a namespace) if no default namespace 43 | /// has been specified for selectors. 44 | /// 45 | Selector Universal(NamespacePrefix prefix); 46 | 47 | /// 48 | /// Generates a ID selector, 49 | /// which represents an element instance that has an identifier that 50 | /// matches the identifier in the ID selector. 51 | /// 52 | Selector Id(string id); 53 | 54 | /// 55 | /// Generates a class selector, 56 | /// which is an alternative when 57 | /// representing the class attribute. 58 | /// 59 | #pragma warning disable CA1716 // Identifiers should not match keywords (by-design) 60 | Selector Class(string clazz); 61 | #pragma warning restore CA1716 // Identifiers should not match keywords 62 | 63 | // 64 | // Attribute selectors 65 | // 66 | 67 | /// 68 | /// Generates an attribute selector 69 | /// that represents an element with the given attribute 70 | /// whatever the values of the attribute. 71 | /// 72 | Selector AttributeExists(NamespacePrefix prefix, string name); 73 | 74 | /// 75 | /// Generates an attribute selector 76 | /// that represents an element with the given attribute 77 | /// and whose value is exactly . 78 | /// 79 | Selector AttributeExact(NamespacePrefix prefix, string name, string value); 80 | 81 | /// 82 | /// Generates an attribute selector 83 | /// that represents an element with the given attribute 84 | /// and whose value is a whitespace-separated list of words, one of 85 | /// which is exactly . 86 | /// 87 | Selector AttributeIncludes(NamespacePrefix prefix, string name, string value); 88 | 89 | /// 90 | /// Generates an attribute selector 91 | /// that represents an element with the given attribute , 92 | /// its value either being exactly or beginning 93 | /// with immediately followed by "-" (U+002D). 94 | /// 95 | Selector AttributeDashMatch(NamespacePrefix prefix, string name, string value); 96 | 97 | /// 98 | /// Generates an attribute selector 99 | /// that represents an element with the attribute 100 | /// whose value begins with the prefix . 101 | /// 102 | Selector AttributePrefixMatch(NamespacePrefix prefix, string name, string value); 103 | 104 | /// 105 | /// Generates an attribute selector 106 | /// that represents an element with the attribute 107 | /// whose value ends with the suffix . 108 | /// 109 | Selector AttributeSuffixMatch(NamespacePrefix prefix, string name, string value); 110 | 111 | /// 112 | /// Generates an attribute selector 113 | /// that represents an element with the attribute 114 | /// whose value contains at least one instance of the substring . 115 | /// 116 | Selector AttributeSubstring(NamespacePrefix prefix, string name, string value); 117 | 118 | // 119 | // Pseudo-class selectors 120 | // 121 | 122 | /// 123 | /// Generates a pseudo-class selector, 124 | /// which represents an element that is the first child of some other element. 125 | /// 126 | Selector FirstChild(); 127 | 128 | /// 129 | /// Generates a pseudo-class selector, 130 | /// which represents an element that is the last child of some other element. 131 | /// 132 | Selector LastChild(); 133 | 134 | /// 135 | /// Generates a pseudo-class selector, 136 | /// which represents an element that is the N-th child of some other element. 137 | /// 138 | Selector NthChild(int a, int b); 139 | 140 | /// 141 | /// Generates a pseudo-class selector, 142 | /// which represents an element that has a parent element and whose parent 143 | /// element has no other element children. 144 | /// 145 | Selector OnlyChild(); 146 | 147 | /// 148 | /// Generates a pseudo-class selector, 149 | /// which represents an element that has no children at all. 150 | /// 151 | Selector Empty(); 152 | 153 | // 154 | // Combinators 155 | // 156 | 157 | /// 158 | /// Generates a combinator, 159 | /// which represents a childhood relationship between two elements. 160 | /// 161 | Selector Child(); 162 | 163 | /// 164 | /// Generates a combinator, 165 | /// which represents a relationship between two elements where one element is an 166 | /// arbitrary descendant of some ancestor element. 167 | /// 168 | Selector Descendant(); 169 | 170 | /// 171 | /// Generates a combinator, 172 | /// which represents elements that share the same parent in the document tree and 173 | /// where the first element immediately precedes the second element. 174 | /// 175 | Selector Adjacent(); 176 | 177 | /// 178 | /// Generates a combinator, 179 | /// which separates two sequences of simple selectors. The elements represented 180 | /// by the two sequences share the same parent in the document tree and the 181 | /// element represented by the first sequence precedes (not necessarily 182 | /// immediately) the element represented by the second one. 183 | /// 184 | Selector GeneralSibling(); 185 | 186 | /// 187 | /// Generates a pseudo-class selector, 188 | /// which represents an element that is the N-th child from bottom up of some other element. 189 | /// 190 | Selector NthLastChild(int a, int b); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /tests/TokenTests.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler.Tests 23 | { 24 | #region Imports 25 | 26 | using NUnit.Framework; 27 | 28 | #endregion 29 | 30 | [TestFixture] 31 | public class TokenTests 32 | { 33 | [Test] 34 | public void DefaultState() 35 | { 36 | AssertToken(TokenKind.Eoi, new Token()); 37 | } 38 | 39 | [Test] 40 | public void Star() 41 | { 42 | AssertToken(TokenKind.Char, "*", Token.Star()); 43 | } 44 | 45 | [Test] 46 | public void Dot() 47 | { 48 | AssertToken(TokenKind.Char, ".", Token.Dot()); 49 | } 50 | 51 | [Test] 52 | public void Colon() 53 | { 54 | AssertToken(TokenKind.Char, ":", Token.Colon()); 55 | } 56 | 57 | [Test] 58 | public void Comma() 59 | { 60 | AssertToken(TokenKind.Char, ",", Token.Comma()); 61 | 62 | } 63 | 64 | [Test] 65 | public void Equals() 66 | { 67 | AssertToken(TokenKind.Char, "=", Token.Equals()); 68 | 69 | } 70 | 71 | [Test] 72 | public void LeftBracket() 73 | { 74 | AssertToken(TokenKind.Char, "[", Token.LeftBracket()); 75 | 76 | } 77 | 78 | [Test] 79 | public void RightBracket() 80 | { 81 | AssertToken(TokenKind.Char, "]", Token.RightBracket()); 82 | } 83 | 84 | [Test] 85 | public void Plus() 86 | { 87 | AssertToken(TokenKind.Plus, Token.Plus()); 88 | } 89 | 90 | [Test] 91 | public void Greater() 92 | { 93 | AssertToken(TokenKind.Greater, Token.Greater()); 94 | } 95 | 96 | [Test] 97 | public void RightParenthesis() 98 | { 99 | AssertToken(TokenKind.Char, ")", Token.RightParenthesis()); 100 | } 101 | 102 | [Test] 103 | public void Eoi() 104 | { 105 | AssertToken(TokenKind.Eoi, Token.Eoi()); 106 | } 107 | 108 | [Test] 109 | public void Includes() 110 | { 111 | AssertToken(TokenKind.Includes, Token.Includes()); 112 | } 113 | 114 | [Test] 115 | public void DashMatch() 116 | { 117 | AssertToken(TokenKind.DashMatch, Token.DashMatch()); 118 | } 119 | 120 | [Test] 121 | public void Ident() 122 | { 123 | AssertToken(TokenKind.Ident, "foo", Token.Ident("foo")); 124 | } 125 | 126 | [Test] 127 | public void IdentNullText() 128 | { 129 | Assert.That(() => Token.Ident(null!), Throws.ArgumentNullException("text")); 130 | } 131 | 132 | [Test] 133 | public void IdentEmptyText() 134 | { 135 | Assert.That(() => Token.Ident(string.Empty), Throws.ArgumentException("text")); 136 | } 137 | 138 | [Test] 139 | public void Hash() 140 | { 141 | AssertToken(TokenKind.Hash, "foo", Token.Hash("foo")); 142 | } 143 | 144 | [Test] 145 | public void HashNullText() 146 | { 147 | Assert.That(() => Token.Hash(null!), Throws.ArgumentNullException("text")); 148 | } 149 | 150 | [Test] 151 | public void HashEmptyText() 152 | { 153 | Assert.That(() => Token.Hash(string.Empty), Throws.ArgumentException("text")); 154 | } 155 | 156 | [Test] 157 | #pragma warning disable CA1720 // Identifier contains type name (matches subject) 158 | public void String() 159 | #pragma warning restore CA1720 // Identifier contains type name 160 | { 161 | AssertToken(TokenKind.String, "foo", Token.String("foo")); 162 | } 163 | 164 | [Test] 165 | public void StringNullText() 166 | { 167 | _ = Token.String(null); 168 | } 169 | 170 | [Test] 171 | public void StringEmptyText() 172 | { 173 | AssertToken(TokenKind.String, string.Empty, Token.String(string.Empty)); 174 | } 175 | 176 | [Test] 177 | public void Function() 178 | { 179 | AssertToken(TokenKind.Function, "foo", Token.Function("foo")); 180 | } 181 | 182 | [Test] 183 | public void FunctionNullText() 184 | { 185 | Assert.That(() => Token.Function(null!), Throws.ArgumentNullException("text")); 186 | } 187 | 188 | [Test] 189 | public void FunctionEmptyText() 190 | { 191 | Assert.That(() => Token.Function(string.Empty), Throws.ArgumentException("text")); 192 | } 193 | 194 | [Test] 195 | public void Not() 196 | { 197 | AssertToken(TokenKind.Not, Token.Not()); 198 | } 199 | 200 | [Test] 201 | public void WhiteSpace() 202 | { 203 | AssertToken(TokenKind.WhiteSpace, " \n ", Token.WhiteSpace(" \n ")); 204 | } 205 | 206 | [Test] 207 | public void WhiteSpaceNullText() 208 | { 209 | Assert.That(() => Token.WhiteSpace(null!), Throws.ArgumentNullException("text")); 210 | } 211 | 212 | [Test] 213 | public void WhiteSpaceEmptyText() 214 | { 215 | Assert.That(() => Token.WhiteSpace(string.Empty), Throws.ArgumentException("text")); 216 | } 217 | 218 | [Test] 219 | #pragma warning disable CA1720 // Identifier contains type name (matches subject) 220 | public void Integer() 221 | #pragma warning restore CA1720 // Identifier contains type name 222 | { 223 | AssertToken(TokenKind.Integer, "123", Token.Integer("123")); 224 | } 225 | 226 | [Test] 227 | public void IntegerNullText() 228 | { 229 | Assert.That(() => Token.Integer(null!), Throws.ArgumentNullException("text")); 230 | } 231 | 232 | [Test] 233 | public void IntegerEmptyText() 234 | { 235 | Assert.That(() => Token.Integer(string.Empty), Throws.ArgumentException("text")); 236 | 237 | } 238 | 239 | static void AssertToken(TokenKind kindExpected, Token token) 240 | { 241 | AssertToken(kindExpected, null, token); 242 | } 243 | 244 | static void AssertToken(TokenKind expectedKind, string? expectedText, Token token) 245 | { 246 | Assert.That(token.Kind, Is.EqualTo(expectedKind)); 247 | if (expectedText == null) 248 | Assert.That(token.Text, Is.Null); 249 | else 250 | Assert.That(token.Text, Is.EqualTo(expectedText)); 251 | } 252 | 253 | [Test] 254 | public void PrefixMatch() 255 | { 256 | AssertToken(TokenKind.PrefixMatch, Token.PrefixMatch()); 257 | } 258 | 259 | [Test] 260 | public void SuffixMatch() 261 | { 262 | AssertToken(TokenKind.SuffixMatch, Token.SuffixMatch()); 263 | } 264 | 265 | [Test] 266 | public void SubstringMatch() 267 | { 268 | AssertToken(TokenKind.SubstringMatch, Token.SubstringMatch()); 269 | } 270 | 271 | [Test] 272 | public void GeneralSibling() 273 | { 274 | AssertToken(TokenKind.Tilde, Token.Tilde()); 275 | } 276 | 277 | [Test] 278 | public void Pipe() 279 | { 280 | var pipe = Token.Pipe(); 281 | Assert.That(pipe.Kind, Is.EqualTo(TokenKind.Char)); 282 | Assert.That(pipe.Text, Is.EqualTo("|")); 283 | } 284 | 285 | [Test] 286 | public void StringRepresentations() 287 | { 288 | Assert.That(Token.Eoi().ToString(), Is.EqualTo("Eoi")); 289 | Assert.That(Token.Ident("foo").ToString(), Is.EqualTo("Ident: foo")); 290 | Assert.That(Token.Hash("foo").ToString(), Is.EqualTo("Hash: foo")); 291 | Assert.That(Token.Includes().ToString(), Is.EqualTo("Includes")); 292 | Assert.That(Token.DashMatch().ToString(), Is.EqualTo("DashMatch")); 293 | Assert.That(Token.PrefixMatch().ToString(), Is.EqualTo("PrefixMatch")); 294 | Assert.That(Token.SuffixMatch().ToString(), Is.EqualTo("SuffixMatch")); 295 | Assert.That(Token.SubstringMatch().ToString(), Is.EqualTo("SubstringMatch")); 296 | Assert.That(Token.String("foo").ToString(), Is.EqualTo("String: foo")); 297 | Assert.That(Token.Plus().ToString(), Is.EqualTo("Plus")); 298 | Assert.That(Token.Greater().ToString(), Is.EqualTo("Greater")); 299 | Assert.That(Token.WhiteSpace(" ").ToString(), Is.EqualTo("WhiteSpace: ")); 300 | Assert.That(Token.Function("foo").ToString(), Is.EqualTo("Function: foo")); 301 | Assert.That(Token.Integer("42").ToString(), Is.EqualTo("Integer: 42")); 302 | Assert.That(Token.Tilde().ToString(), Is.EqualTo("Tilde")); 303 | Assert.That(Token.Star().ToString(), Is.EqualTo("Char: *")); 304 | Assert.That(Token.Dot().ToString(), Is.EqualTo("Char: .")); 305 | Assert.That(Token.Colon().ToString(), Is.EqualTo("Char: :")); 306 | Assert.That(Token.Comma().ToString(), Is.EqualTo("Char: ,")); 307 | Assert.That(Token.Equals().ToString(), Is.EqualTo("Char: =")); 308 | Assert.That(Token.LeftBracket().ToString(), Is.EqualTo("Char: [")); 309 | Assert.That(Token.RightBracket().ToString(), Is.EqualTo("Char: ]")); 310 | Assert.That(Token.RightParenthesis().ToString(), Is.EqualTo("Char: )")); 311 | Assert.That(Token.Pipe().ToString(), Is.EqualTo("Char: |")); 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/ISelectorGenerator.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler 23 | { 24 | /// 25 | /// Represent an implementation that is responsible for generating 26 | /// an implementation for a selector. 27 | /// 28 | public interface ISelectorGenerator 29 | { 30 | /// 31 | /// Delimits the initialization of a generation. 32 | /// 33 | void OnInit(); 34 | 35 | /// 36 | /// Delimits the closing/conclusion of a generation. 37 | /// 38 | void OnClose(); 39 | 40 | /// 41 | /// Delimits a selector generation in a group of selectors. 42 | /// 43 | void OnSelector(); 44 | 45 | // 46 | // Selectors 47 | // 48 | 49 | /// 50 | /// Generates a type selector, 51 | /// which represents an instance of the element type in the document tree. 52 | /// 53 | void Type(NamespacePrefix prefix, string name); 54 | 55 | /// 56 | /// Generates a universal selector, 57 | /// any single element in the document tree in any namespace 58 | /// (including those without a namespace) if no default namespace 59 | /// has been specified for selectors. 60 | /// 61 | void Universal(NamespacePrefix prefix); 62 | 63 | /// 64 | /// Generates a ID selector, 65 | /// which represents an element instance that has an identifier that 66 | /// matches the identifier in the ID selector. 67 | /// 68 | void Id(string id); 69 | 70 | /// 71 | /// Generates a class selector, 72 | /// which is an alternative when 73 | /// representing the class attribute. 74 | /// 75 | #pragma warning disable CA1716 // Identifiers should not match keywords (by-design) 76 | void Class(string clazz); 77 | #pragma warning restore CA1716 // Identifiers should not match keywords 78 | 79 | // 80 | // Attribute selectors 81 | // 82 | 83 | /// 84 | /// Generates an attribute selector 85 | /// that represents an element with the given attribute 86 | /// whatever the values of the attribute. 87 | /// 88 | void AttributeExists(NamespacePrefix prefix, string name); 89 | 90 | /// 91 | /// Generates an attribute selector 92 | /// that represents an element with the given attribute 93 | /// and whose value is exactly . 94 | /// 95 | void AttributeExact(NamespacePrefix prefix, string name, string value); 96 | 97 | /// 98 | /// Generates an attribute selector 99 | /// that represents an element with the given attribute 100 | /// and whose value is a whitespace-separated list of words, one of 101 | /// which is exactly . 102 | /// 103 | void AttributeIncludes(NamespacePrefix prefix, string name, string value); 104 | 105 | /// 106 | /// Generates an attribute selector 107 | /// that represents an element with the given attribute , 108 | /// its value either being exactly or beginning 109 | /// with immediately followed by "-" (U+002D). 110 | /// 111 | void AttributeDashMatch(NamespacePrefix prefix, string name, string value); 112 | 113 | /// 114 | /// Generates an attribute selector 115 | /// that represents an element with the attribute 116 | /// whose value begins with the prefix . 117 | /// 118 | void AttributePrefixMatch(NamespacePrefix prefix, string name, string value); 119 | 120 | /// 121 | /// Generates an attribute selector 122 | /// that represents an element with the attribute 123 | /// whose value ends with the suffix . 124 | /// 125 | void AttributeSuffixMatch(NamespacePrefix prefix, string name, string value); 126 | 127 | /// 128 | /// Generates an attribute selector 129 | /// that represents an element with the attribute 130 | /// whose value contains at least one instance of the substring . 131 | /// 132 | void AttributeSubstring(NamespacePrefix prefix, string name, string value); 133 | 134 | // 135 | // Pseudo-class selectors 136 | // 137 | 138 | /// 139 | /// Generates a pseudo-class selector, 140 | /// which represents an element that is the first child of some other element. 141 | /// 142 | void FirstChild(); 143 | 144 | /// 145 | /// Generates a pseudo-class selector, 146 | /// which represents an element that is the last child of some other element. 147 | /// 148 | void LastChild(); 149 | 150 | /// 151 | /// Generates a pseudo-class selector, 152 | /// which represents an element that is the N-th child of some other element. 153 | /// 154 | void NthChild(int a, int b); 155 | 156 | /// 157 | /// Generates a pseudo-class selector, 158 | /// which represents an element that has a parent element and whose parent 159 | /// element has no other element children. 160 | /// 161 | void OnlyChild(); 162 | 163 | /// 164 | /// Generates a pseudo-class selector, 165 | /// which represents an element that has no children at all. 166 | /// 167 | void Empty(); 168 | 169 | // 170 | // Combinators 171 | // 172 | 173 | /// 174 | /// Generates a combinator, 175 | /// which represents a childhood relationship between two elements. 176 | /// 177 | void Child(); 178 | 179 | /// 180 | /// Generates a combinator, 181 | /// which represents a relationship between two elements where one element is an 182 | /// arbitrary descendant of some ancestor element. 183 | /// 184 | void Descendant(); 185 | 186 | /// 187 | /// Generates a combinator, 188 | /// which represents elements that share the same parent in the document tree and 189 | /// where the first element immediately precedes the second element. 190 | /// 191 | void Adjacent(); 192 | 193 | /// 194 | /// Generates a combinator, 195 | /// which separates two sequences of simple selectors. The elements represented 196 | /// by the two sequences share the same parent in the document tree and the 197 | /// element represented by the first sequence precedes (not necessarily 198 | /// immediately) the element represented by the second one. 199 | /// 200 | void GeneralSibling(); 201 | 202 | /// 203 | /// Generates a pseudo-class selector, 204 | /// which represents an element that is the N-th child from bottom up of some other element. 205 | /// 206 | void NthLastChild(int a, int b); 207 | } 208 | 209 | /// 210 | /// Represent an implementation that is responsible for generating 211 | /// an implementation for a selector and supports the 212 | /// negation pseudo-class selector. 213 | /// 214 | public interface INegationSelectorGenerator : ISelectorGenerator 215 | { 216 | /// 217 | /// Starts a negation pseudo-class selector, 218 | /// which represents an element that is not represented by its argument. 219 | /// 220 | void BeginNegation(); 221 | 222 | /// 223 | /// Generates a negation pseudo-class selector, 224 | /// which represents an element that is not represented by its argument. 225 | /// 226 | void EndNegation(); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/SelectorGeneratorTee.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler 23 | { 24 | using System; 25 | 26 | /// 27 | /// An implementation that delegates 28 | /// to two other objects, which 29 | /// can be useful for doing work in a single pass. 30 | /// 31 | public sealed class SelectorGeneratorTee : INegationSelectorGenerator 32 | { 33 | /// 34 | /// Gets the first generator used to initialize this generator. 35 | /// 36 | public ISelectorGenerator Primary { get; } 37 | 38 | /// 39 | /// Gets the second generator used to initialize this generator. 40 | /// 41 | public ISelectorGenerator Secondary { get; } 42 | 43 | /// 44 | /// Initializes a new instance of 45 | /// with the two other objects 46 | /// it delegates to. 47 | /// 48 | #pragma warning disable IDE0290 // Use primary constructor (preserve doc) 49 | public SelectorGeneratorTee(ISelectorGenerator primary, ISelectorGenerator secondary) 50 | #pragma warning restore IDE0290 // Use primary constructor 51 | { 52 | Primary = primary ?? throw new ArgumentNullException(nameof(primary)); 53 | Secondary = secondary ?? throw new ArgumentNullException(nameof(secondary)); 54 | } 55 | 56 | /// 57 | /// Delegates to then generator. 58 | /// 59 | public void OnInit() 60 | { 61 | Primary.OnInit(); 62 | Secondary.OnInit(); 63 | } 64 | 65 | /// 66 | /// Delegates to then generator. 67 | /// 68 | public void OnClose() 69 | { 70 | Primary.OnClose(); 71 | Secondary.OnClose(); 72 | } 73 | 74 | /// 75 | /// Delegates to then generator. 76 | /// 77 | public void OnSelector() 78 | { 79 | Primary.OnSelector(); 80 | Secondary.OnSelector(); 81 | } 82 | 83 | /// 84 | /// Delegates to then generator. 85 | /// 86 | #pragma warning disable CA1725 // Parameter names should match base declaration (compatibility) 87 | public void Type(NamespacePrefix prefix, string type) 88 | #pragma warning restore CA1725 // Parameter names should match base declaration 89 | { 90 | Primary.Type(prefix, type); 91 | Secondary.Type(prefix, type); 92 | } 93 | 94 | /// 95 | /// Delegates to then generator. 96 | /// 97 | public void Universal(NamespacePrefix prefix) 98 | { 99 | Primary.Universal(prefix); 100 | Secondary.Universal(prefix); 101 | } 102 | 103 | /// 104 | /// Delegates to then generator. 105 | /// 106 | public void Id(string id) 107 | { 108 | Primary.Id(id); 109 | Secondary.Id(id); 110 | } 111 | 112 | /// 113 | /// Delegates to then generator. 114 | /// 115 | public void Class(string clazz) 116 | { 117 | Primary.Class(clazz); 118 | Secondary.Class(clazz); 119 | } 120 | 121 | /// 122 | /// Delegates to then generator. 123 | /// 124 | public void AttributeExists(NamespacePrefix prefix, string name) 125 | { 126 | Primary.AttributeExists(prefix, name); 127 | Secondary.AttributeExists(prefix, name); 128 | } 129 | 130 | /// 131 | /// Delegates to then generator. 132 | /// 133 | public void AttributeExact(NamespacePrefix prefix, string name, string value) 134 | { 135 | Primary.AttributeExact(prefix, name, value); 136 | Secondary.AttributeExact(prefix, name, value); 137 | } 138 | 139 | /// 140 | /// Delegates to then generator. 141 | /// 142 | public void AttributeIncludes(NamespacePrefix prefix, string name, string value) 143 | { 144 | Primary.AttributeIncludes(prefix, name, value); 145 | Secondary.AttributeIncludes(prefix, name, value); 146 | } 147 | 148 | /// 149 | /// Delegates to then generator. 150 | /// 151 | public void AttributeDashMatch(NamespacePrefix prefix, string name, string value) 152 | { 153 | Primary.AttributeDashMatch(prefix, name, value); 154 | Secondary.AttributeDashMatch(prefix, name, value); 155 | } 156 | 157 | /// 158 | /// Delegates to then generator. 159 | /// 160 | public void AttributePrefixMatch(NamespacePrefix prefix, string name, string value) 161 | { 162 | Primary.AttributePrefixMatch(prefix, name, value); 163 | Secondary.AttributePrefixMatch(prefix, name, value); 164 | } 165 | 166 | /// 167 | /// Delegates to then generator. 168 | /// 169 | public void AttributeSuffixMatch(NamespacePrefix prefix, string name, string value) 170 | { 171 | Primary.AttributeSuffixMatch(prefix, name, value); 172 | Secondary.AttributeSuffixMatch(prefix, name, value); 173 | } 174 | 175 | /// 176 | /// Delegates to then generator. 177 | /// 178 | public void AttributeSubstring(NamespacePrefix prefix, string name, string value) 179 | { 180 | Primary.AttributeSubstring(prefix, name, value); 181 | Secondary.AttributeSubstring(prefix, name, value); 182 | } 183 | 184 | /// 185 | /// Delegates to then generator. 186 | /// 187 | public void FirstChild() 188 | { 189 | Primary.FirstChild(); 190 | Secondary.FirstChild(); 191 | } 192 | 193 | /// 194 | /// Delegates to then generator. 195 | /// 196 | public void LastChild() 197 | { 198 | Primary.LastChild(); 199 | Secondary.LastChild(); 200 | } 201 | 202 | /// 203 | /// Delegates to then generator. 204 | /// 205 | public void NthChild(int a, int b) 206 | { 207 | Primary.NthChild(a, b); 208 | Secondary.NthChild(a, b); 209 | } 210 | 211 | /// 212 | /// Delegates to then generator. 213 | /// 214 | public void OnlyChild() 215 | { 216 | Primary.OnlyChild(); 217 | Secondary.OnlyChild(); 218 | } 219 | 220 | /// 221 | /// Delegates to then generator. 222 | /// 223 | public void Empty() 224 | { 225 | Primary.Empty(); 226 | Secondary.Empty(); 227 | } 228 | 229 | /// 230 | /// Delegates to then generator. 231 | /// 232 | public void BeginNegation() 233 | { 234 | ((INegationSelectorGenerator)Primary).BeginNegation(); 235 | ((INegationSelectorGenerator)Secondary).BeginNegation(); 236 | } 237 | 238 | /// 239 | /// Delegates to then generator. 240 | /// 241 | public void EndNegation() 242 | { 243 | ((INegationSelectorGenerator)Primary).EndNegation(); 244 | ((INegationSelectorGenerator)Secondary).EndNegation(); 245 | } 246 | 247 | /// 248 | /// Delegates to then generator. 249 | /// 250 | public void Child() 251 | { 252 | Primary.Child(); 253 | Secondary.Child(); 254 | } 255 | 256 | /// 257 | /// Delegates to then generator. 258 | /// 259 | public void Descendant() 260 | { 261 | Primary.Descendant(); 262 | Secondary.Descendant(); 263 | } 264 | 265 | /// 266 | /// Delegates to then generator. 267 | /// 268 | public void Adjacent() 269 | { 270 | Primary.Adjacent(); 271 | Secondary.Adjacent(); 272 | } 273 | 274 | /// 275 | /// Delegates to then generator. 276 | /// 277 | public void GeneralSibling() 278 | { 279 | Primary.GeneralSibling(); 280 | Secondary.GeneralSibling(); 281 | } 282 | 283 | /// 284 | /// Delegates to then generator. 285 | /// 286 | public void NthLastChild(int a, int b) 287 | { 288 | Primary.NthLastChild(a, b); 289 | Secondary.NthLastChild(a, b); 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /tests/SelectorGeneratorTeeTests.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler.Tests 23 | { 24 | #region Imports 25 | 26 | using System; 27 | using System.Collections.Generic; 28 | using System.Linq; 29 | using System.Reflection; 30 | using NUnit.Framework; 31 | 32 | #endregion 33 | 34 | /// 35 | /// Ensure that all actions on SelectorGeneratorTee are passed 36 | /// to the internal Primary and Secondary SelectorGenerators. 37 | /// 38 | [TestFixture] 39 | public class SelectorGeneratorTeeTests 40 | { 41 | #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. (Assigned during setup) 42 | SelectorGeneratorTee tee; 43 | FakeSelectorGenerator primary; 44 | FakeSelectorGenerator secondary; 45 | #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 46 | 47 | [SetUp] 48 | public void Setup() 49 | { 50 | this.primary = new FakeSelectorGenerator(); 51 | this.secondary = new FakeSelectorGenerator(); 52 | this.tee = new SelectorGeneratorTee(this.primary, this.secondary); 53 | } 54 | 55 | [Test] 56 | public void NullPrimary() 57 | { 58 | Assert.That(() => new SelectorGeneratorTee(null!, new FakeSelectorGenerator()), 59 | Throws.ArgumentNullException("primary")); 60 | } 61 | 62 | [Test] 63 | public void NullSecondary() 64 | { 65 | Assert.That(() => new SelectorGeneratorTee(new FakeSelectorGenerator(), null!), 66 | Throws.ArgumentNullException("secondary")); 67 | } 68 | 69 | [Test] 70 | public void OnInitTest() 71 | { 72 | Run(this.tee.OnInit); 73 | } 74 | 75 | [Test] 76 | public void OnCloseTest() 77 | { 78 | Run(this.tee.OnClose); 79 | } 80 | 81 | [Test] 82 | public void OnSelectorTest() 83 | { 84 | Run(this.tee.OnSelector); 85 | } 86 | 87 | [Test] 88 | public void TypeTest() 89 | { 90 | Run(this.tee.Type, NamespacePrefix.None, "go"); 91 | } 92 | 93 | [Test] 94 | public void UniversalTest() 95 | { 96 | Run(this.tee.Universal, NamespacePrefix.None); 97 | } 98 | 99 | [Test] 100 | public void IdTest() 101 | { 102 | Run(this.tee.Id, "hello"); 103 | } 104 | 105 | [Test] 106 | public void ClassTest() 107 | { 108 | Run(this.tee.Class, "hello"); 109 | } 110 | 111 | [Test] 112 | public void AttrExistsTest() 113 | { 114 | Run(this.tee.AttributeExists, NamespacePrefix.None, "hello"); 115 | } 116 | 117 | [Test] 118 | public void AttExactTest() 119 | { 120 | Run(this.tee.AttributeExact, NamespacePrefix.None, "hello", "there"); 121 | } 122 | 123 | [Test] 124 | public void AttrIncludesTest() 125 | { 126 | Run(this.tee.AttributeIncludes, NamespacePrefix.None, "hello", "there"); 127 | } 128 | 129 | [Test] 130 | public void AttrDashMatchTest() 131 | { 132 | Run(this.tee.AttributeDashMatch, NamespacePrefix.None, "hello", "there"); 133 | } 134 | 135 | [Test] 136 | public void AttrPrefixMatchTest() 137 | { 138 | Run(this.tee.AttributePrefixMatch,NamespacePrefix.None, "hello", "there"); 139 | } 140 | 141 | [Test] 142 | public void AttrSuffixMatchTest() 143 | { 144 | Run(this.tee.AttributeSuffixMatch, NamespacePrefix.None, "hello", "there"); 145 | } 146 | 147 | [Test] 148 | public void AttrSubstringTest() 149 | { 150 | Run(this.tee.AttributeSubstring, NamespacePrefix.None, "hello", "there"); 151 | } 152 | 153 | [Test] 154 | public void FirstChildTest() 155 | { 156 | Run(this.tee.FirstChild); 157 | } 158 | 159 | [Test] 160 | public void LastChildTest() 161 | { 162 | Run(this.tee.LastChild); 163 | } 164 | 165 | [Test] 166 | public void NthChildTest() 167 | { 168 | Run(this.tee.NthChild, 1, 2); 169 | } 170 | 171 | [Test] 172 | public void OnlyChildTest() 173 | { 174 | Run(this.tee.OnlyChild); 175 | } 176 | 177 | [Test] 178 | public void EmptyTest() 179 | { 180 | Run(this.tee.Empty); 181 | } 182 | 183 | [Test] 184 | public void ChildTest() 185 | { 186 | Run(this.tee.Child); 187 | } 188 | 189 | [Test] 190 | public void DescendantTest() 191 | { 192 | Run(this.tee.Descendant); 193 | } 194 | 195 | [Test] 196 | public void AdjacentTest() 197 | { 198 | Run(this.tee.Adjacent); 199 | } 200 | 201 | [Test] 202 | public void GeneralSiblingTest() 203 | { 204 | Run(this.tee.GeneralSibling); 205 | } 206 | 207 | void Run(Action action) 208 | { 209 | RunImpl(action.Method); 210 | } 211 | 212 | void Run(Action action, T arg) 213 | { 214 | RunImpl(action.Method, arg); 215 | } 216 | 217 | void Run(Action action, T1 arg1, T2 arg2) 218 | { 219 | RunImpl(action.Method, arg1, arg2); 220 | } 221 | 222 | void Run(Action action, T1 arg1, T2 arg2, T3 arg3) 223 | { 224 | RunImpl(action.Method, arg1, arg2, arg3); 225 | } 226 | 227 | /// 228 | /// Take the passed action, run it, and then check that the last method 229 | /// and last args are the same for pri and sec. 230 | /// 231 | void RunImpl(MethodBase action, params object?[] args) 232 | { 233 | var recordings = new Queue>(2); 234 | this.primary.Recorder = recordings.Enqueue; 235 | this.secondary.Recorder = recordings.Enqueue; 236 | 237 | _ = action.Invoke(this.tee, args); 238 | 239 | // Assert the fact that the primary and secondary methods were 240 | // both called with the same arguments and in the right order! 241 | 242 | var recording = recordings.Dequeue(); 243 | Assert.That(recording.Target, Is.SameAs(this.primary)); 244 | Assert.That(MapMethod(recording.Method).Name, Is.EqualTo(action.Name)); 245 | Assert.That(recording.Arguments, Is.EqualTo(args)); 246 | 247 | recording = recordings.Dequeue(); 248 | Assert.That(recording.Target, Is.SameAs(this.secondary)); 249 | Assert.That(MapMethod(recording.Method).Name, Is.EqualTo(action.Name)); 250 | Assert.That(recording.Arguments, Is.EqualTo(args)); 251 | } 252 | 253 | static MethodInfo MapMethod(MethodInfo method) where T : class 254 | { 255 | #pragma warning disable CA2201 // Do not raise reserved exception types (test code) 256 | var type = method.ReflectedType ?? throw new NullReferenceException(); 257 | #pragma warning restore CA2201 // Do not raise reserved exception types 258 | var mapping = type.GetInterfaceMap(typeof(T)); 259 | return mapping.InterfaceMethods 260 | .Select((m, i) => new { Source = m, Target = mapping.TargetMethods[i] }) 261 | .Single(m => m.Target == method).Source; 262 | } 263 | 264 | sealed class CallRecording(T target, MethodInfo method, object[] arguments) 265 | { 266 | public T Target { get; } = target; 267 | public MethodInfo Method { get; } = method; 268 | public object[] Arguments { get; } = arguments; 269 | } 270 | 271 | sealed class FakeSelectorGenerator : ISelectorGenerator 272 | { 273 | public Action>? Recorder { get; set; } 274 | 275 | public void OnInit() => 276 | OnInvoked(MethodBase.GetCurrentMethod()); 277 | 278 | public void OnClose() => 279 | OnInvoked(MethodBase.GetCurrentMethod()); 280 | 281 | public void OnSelector() => 282 | OnInvoked(MethodBase.GetCurrentMethod()); 283 | 284 | public void Type(NamespacePrefix prefix, string type) => 285 | OnInvoked(MethodBase.GetCurrentMethod(), prefix, type); 286 | 287 | public void Universal(NamespacePrefix prefix) => 288 | OnInvoked(MethodBase.GetCurrentMethod(), prefix); 289 | 290 | public void Id(string id) => 291 | OnInvoked(MethodBase.GetCurrentMethod(), id); 292 | 293 | public void Class(string clazz) => 294 | OnInvoked(MethodBase.GetCurrentMethod(), clazz); 295 | 296 | public void AttributeExists(NamespacePrefix prefix, string name) => 297 | OnInvoked(MethodBase.GetCurrentMethod(), prefix, name); 298 | 299 | public void AttributeExact(NamespacePrefix prefix, string name, string value) => 300 | OnInvoked(MethodBase.GetCurrentMethod(), prefix, name, value); 301 | 302 | public void AttributeIncludes(NamespacePrefix prefix, string name, string value) => 303 | OnInvoked(MethodBase.GetCurrentMethod(), prefix, name, value); 304 | 305 | public void AttributeDashMatch(NamespacePrefix prefix, string name, string value) => 306 | OnInvoked(MethodBase.GetCurrentMethod(), prefix, name, value); 307 | 308 | public void AttributePrefixMatch(NamespacePrefix prefix, string name, string value) => 309 | OnInvoked(MethodBase.GetCurrentMethod(), prefix, name, value); 310 | 311 | public void AttributeSuffixMatch(NamespacePrefix prefix, string name, string value) => 312 | OnInvoked(MethodBase.GetCurrentMethod(), prefix, name, value); 313 | 314 | public void AttributeSubstring(NamespacePrefix prefix, string name, string value) => 315 | OnInvoked(MethodBase.GetCurrentMethod(), prefix, name, value); 316 | 317 | public void FirstChild() => 318 | OnInvoked(MethodBase.GetCurrentMethod()); 319 | 320 | public void LastChild() => 321 | OnInvoked(MethodBase.GetCurrentMethod()); 322 | 323 | public void NthChild(int a, int b) => 324 | OnInvoked(MethodBase.GetCurrentMethod(), a, b); 325 | 326 | public void OnlyChild() => 327 | OnInvoked(MethodBase.GetCurrentMethod()); 328 | 329 | public void Empty() => 330 | OnInvoked(MethodBase.GetCurrentMethod()); 331 | 332 | public void Child() => 333 | OnInvoked(MethodBase.GetCurrentMethod()); 334 | 335 | public void Descendant() => 336 | OnInvoked(MethodBase.GetCurrentMethod()); 337 | 338 | public void Adjacent() => 339 | OnInvoked(MethodBase.GetCurrentMethod()); 340 | 341 | public void GeneralSibling() => 342 | OnInvoked(MethodBase.GetCurrentMethod()); 343 | 344 | public void NthLastChild(int a, int b) => 345 | OnInvoked(MethodBase.GetCurrentMethod(), a, b); 346 | 347 | void OnInvoked(MethodBase? method, params object[] args) 348 | { 349 | switch (method) 350 | { 351 | case null: throw new ArgumentNullException(nameof(method)); 352 | case MethodInfo info: Recorder?.Invoke(new CallRecording(this, info, args)); break; 353 | default: throw new ArgumentException(null, nameof(method)); 354 | } 355 | } 356 | } 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/Tokener.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler 23 | { 24 | #region Imports 25 | 26 | using System; 27 | using System.Collections.Generic; 28 | using System.IO; 29 | using System.Runtime.CompilerServices; 30 | using System.Text; 31 | 32 | #endregion 33 | 34 | /// 35 | /// Lexer for tokens in CSS selector grammar. 36 | /// 37 | public static class Tokener 38 | { 39 | /// 40 | /// Parses tokens from a given text source. 41 | /// 42 | public static IEnumerable Tokenize(TextReader reader) 43 | { 44 | if (reader == null) throw new ArgumentNullException(nameof(reader)); 45 | return Tokenize(reader.ReadToEnd()); 46 | } 47 | 48 | /// 49 | /// Parses tokens from a given string. 50 | /// 51 | public static IEnumerable Tokenize(string? input) 52 | { 53 | var reader = new Reader(input ?? string.Empty); 54 | StringBuilder? sb = null; 55 | 56 | while (reader.Read() is { } ch) 57 | { 58 | // 59 | // Identifier or function 60 | // 61 | if (ch == '-' || IsNmStart(ch)) 62 | { 63 | reader.Mark(); 64 | if (ch == '-') 65 | { 66 | if (reader.Read() is not { } n || !IsNmStart(n)) 67 | throw new FormatException($"Invalid identifier at position {reader.Position}."); 68 | } 69 | 70 | var r = reader.Read(); 71 | while (r is { } nm && IsNmChar(nm)) 72 | r = reader.Read(); 73 | 74 | if (r == '(') 75 | yield return Token.Function(reader.Marked()); 76 | else 77 | yield return Token.Ident(reader.MarkedWithUnread()); 78 | } 79 | // 80 | // Integer 81 | // 82 | else if (IsDigit(ch)) 83 | { 84 | reader.Mark(); 85 | do { /* NOP */ } while (reader.Read() is { } d && IsDigit(d)); 86 | yield return Token.Integer(reader.MarkedWithUnread()); 87 | } 88 | // 89 | // Whitespace, including that which is coupled with some punctuation 90 | // 91 | else if (IsS(ch)) 92 | { 93 | var space = ParseWhiteSpace(reader); 94 | switch (reader.Read()) 95 | { 96 | case ',': yield return Token.Comma(); break; 97 | case '+': yield return Token.Plus(); break; 98 | case '>': yield return Token.Greater(); break; 99 | case '~': yield return Token.Tilde(); break; 100 | 101 | default: 102 | reader.Unread(); 103 | yield return Token.WhiteSpace(space); 104 | break; 105 | } 106 | } 107 | #pragma warning disable IDE0011 // Add braces (save indentation for readability) 108 | else switch(ch) 109 | #pragma warning restore IDE0011 // Add braces 110 | { 111 | case '*': // * or *= 112 | case '~': // ~ or ~= 113 | case '|': // | or |= 114 | { 115 | if (reader.Read() == '=') 116 | { 117 | yield return ch == '*' 118 | ? Token.SubstringMatch() 119 | : ch == '|' ? Token.DashMatch() 120 | : Token.Includes(); 121 | } 122 | else 123 | { 124 | reader.Unread(); 125 | yield return ch is '*' or '|' 126 | ? Token.Char(ch) 127 | : Token.Tilde(); 128 | } 129 | break; 130 | } 131 | case '^': // ^= 132 | case '$': // $= 133 | { 134 | if (reader.Read() != '=') 135 | throw new FormatException($"Invalid character at position {reader.Position}."); 136 | 137 | #pragma warning disable IDE0010 // Add missing cases (handled by compiler) 138 | switch (ch) 139 | #pragma warning restore IDE0010 // Add missing cases 140 | { 141 | case '^': yield return Token.PrefixMatch(); break; 142 | case '$': yield return Token.SuffixMatch(); break; 143 | } 144 | break; 145 | } 146 | // 147 | // Single-character punctuation (mostly) 148 | // 149 | case '.': yield return Token.Dot(); break; 150 | case ',': yield return Token.Comma(); break; 151 | case '=': yield return Token.Equals(); break; 152 | case '[': yield return Token.LeftBracket(); break; 153 | case ']': yield return Token.RightBracket(); break; 154 | case ')': yield return Token.RightParenthesis(); break; 155 | case '+': yield return Token.Plus(); break; 156 | case '>': yield return Token.Greater(); break; 157 | case '#': yield return Token.Hash(ParseHash(reader)); break; 158 | case ':': 159 | { 160 | var pos = reader.Position; 161 | #pragma warning disable IDE0078 // Use pattern matching (may change code meaning) 162 | if (reader.Read() == 'n' && 163 | reader.Read() == 'o' && 164 | reader.Read() == 't' && 165 | reader.Read() == '(') 166 | #pragma warning restore IDE0078 // Use pattern matching 167 | { 168 | yield return Token.Not(); // ":"{N}{O}{T}"(" return NOT; 169 | break; 170 | } 171 | 172 | while (reader.Position > pos) 173 | reader.Unread(); 174 | 175 | yield return Token.Colon(); 176 | break; 177 | } 178 | // 179 | // Single- or double-quoted strings 180 | // 181 | case '\"': 182 | case '\'': yield return ParseString(reader, /* quote */ ch, ref sb); break; 183 | 184 | default: 185 | throw new FormatException($"Invalid character at position {reader.Position}."); 186 | } 187 | } 188 | yield return Token.Eoi(); 189 | } 190 | 191 | static string ParseWhiteSpace(Reader reader) 192 | { 193 | reader.Mark(); 194 | while (reader.Read() is { } ch && IsS(ch)) { /* NOP */ } 195 | return reader.MarkedWithUnread(); 196 | } 197 | 198 | static string ParseHash(Reader reader) 199 | { 200 | reader.MarkFromNext(); // skipping # 201 | while (reader.Read() is { } ch && IsNmChar(ch)) { /* NOP */ } 202 | var text = reader.MarkedWithUnread(); 203 | if (text.Length == 0) 204 | throw new FormatException($"Invalid hash at position {reader.Position}."); 205 | return text; 206 | } 207 | 208 | static Token ParseString(Reader reader, char quote, ref StringBuilder? sb) 209 | { 210 | // 211 | // TODO Support full string syntax! 212 | // 213 | // string {string1}|{string2} 214 | // string1 \"([^\n\r\f\\"]|\\{nl}|{nonascii}|{escape})*\" 215 | // string2 \'([^\n\r\f\\']|\\{nl}|{nonascii}|{escape})*\' 216 | // nonascii [^\0-\177] 217 | // escape {unicode}|\\[^\n\r\f0-9a-f] 218 | // unicode \\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])? 219 | // 220 | 221 | var strpos = reader.Position; 222 | reader.MarkFromNext(); // skipping quote 223 | 224 | _ = sb?.Clear(); 225 | 226 | for (var done = false; !done;) 227 | { 228 | #pragma warning disable IDE0010 // Add missing cases (handled by compiler) 229 | switch (reader.Read()) 230 | #pragma warning restore IDE0010 // Add missing cases 231 | { 232 | case null: 233 | throw new FormatException($"Unterminated string at position {strpos}."); 234 | 235 | case { } ch when ch == quote: 236 | done = true; 237 | break; 238 | 239 | case '\\': 240 | { 241 | // 242 | // NOTE: Only escaping of quote and backslash supported! 243 | // 244 | 245 | var esc = reader.Read(); 246 | if (esc != quote && esc != '\\') 247 | throw new FormatException($"Invalid escape sequence at position {reader.Position} in a string at position {strpos}."); 248 | 249 | sb ??= new StringBuilder(); 250 | _ = sb.Append(reader.MarkedExceptLast()); 251 | reader.Mark(); 252 | break; 253 | } 254 | } 255 | } 256 | 257 | var text = reader.Marked(); 258 | 259 | if (sb != null) 260 | text = sb.Append(text).ToString(); 261 | 262 | return Token.String(text); 263 | } 264 | 265 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 266 | static bool IsDigit(char ch) => // [0-9] 267 | ch is >= '0' and <= '9'; 268 | 269 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 270 | static bool IsS(char ch) => // [ \t\r\n\f] 271 | ch is ' ' or '\t' or '\r' or '\n' or '\f'; 272 | 273 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 274 | static bool IsNmStart(char ch) => // [_a-z]|{nonascii}|{escape} 275 | ch is '_' or >= 'a' and <= 'z' or >= 'A' and <= 'Z'; 276 | 277 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 278 | static bool IsNmChar(char ch) => // [_a-z0-9-]|{nonascii}|{escape} 279 | IsNmStart(ch) || ch is '-' or >= '0' and <= '9'; 280 | 281 | sealed class Reader(string input) 282 | { 283 | readonly string input = input; 284 | int index = -1; 285 | int start = -1; 286 | 287 | public int Position => this.index + 1; 288 | 289 | public void Mark() => this.start = this.index; 290 | public void MarkFromNext() => this.start = this.index + 1; 291 | public string Marked() => Marked(0); 292 | public string MarkedExceptLast() => Marked(-1); 293 | 294 | string Marked(int trim) 295 | { 296 | var start = this.start; 297 | return Math.Min(this.input.Length, this.index + trim) - start is var count and > 0 298 | ? this.input.Substring(start, count) 299 | : string.Empty; 300 | } 301 | 302 | public char? Read() 303 | { 304 | var input = this.input; 305 | 306 | var i = this.index = Position >= input.Length 307 | ? input.Length 308 | : this.index + 1; 309 | 310 | return i >= 0 && i < input.Length ? input[i] : null; 311 | } 312 | 313 | public void Unread() => this.index = Math.Max(-1, this.index - 1); 314 | 315 | public string MarkedWithUnread() 316 | { 317 | var text = Marked(); 318 | Unread(); 319 | return text; 320 | } 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /tests/TokenerTests.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler.Tests 23 | { 24 | #region Imports 25 | 26 | using System; 27 | using System.IO; 28 | using System.Linq; 29 | using NUnit.Framework; 30 | 31 | #endregion 32 | 33 | [TestFixture] 34 | public class TokenerTests 35 | { 36 | [Test] 37 | public void NullInput() 38 | { 39 | Assert.That(Tokener.Tokenize((string?)null).Single(), Is.EqualTo(Token.Eoi())); 40 | } 41 | 42 | [Test] 43 | public void EmptyInput() 44 | { 45 | Assert.That(Tokener.Tokenize(string.Empty).Single(), Is.EqualTo(Token.Eoi())); 46 | } 47 | 48 | [TestCase(" \r \n \f \t ", " \r \n \f \t ")] 49 | [TestCase(" \r \n \f \t ", " \r \n \f \t etc")] 50 | public void WhiteSpace(string ws, string input) 51 | { 52 | Assert.That(Tokener.Tokenize(input).First(), Is.EqualTo(Token.WhiteSpace(ws))); 53 | } 54 | 55 | [Test] 56 | public void Colon() 57 | { 58 | Assert.That(Tokener.Tokenize(":").First(), Is.EqualTo(Token.Colon())); 59 | } 60 | 61 | [Test] 62 | public void Comma() 63 | { 64 | Assert.That(Tokener.Tokenize(",").First(), Is.EqualTo(Token.Comma())); 65 | } 66 | 67 | [Test] 68 | public void CommaWhiteSpacePrepended() 69 | { 70 | Assert.That(Tokener.Tokenize(" ,").First(), Is.EqualTo(Token.Comma())); 71 | } 72 | 73 | [Test] 74 | public void Plus() 75 | { 76 | Assert.That(Tokener.Tokenize("+").First(), Is.EqualTo(Token.Plus())); 77 | } 78 | 79 | [Test] 80 | public void Equals() 81 | { 82 | Assert.That(Tokener.Tokenize("=").First(), Is.EqualTo(Token.Equals())); 83 | } 84 | 85 | [Test] 86 | public void LeftBracket() 87 | { 88 | Assert.That(Tokener.Tokenize("[").First(), Is.EqualTo(Token.LeftBracket())); 89 | } 90 | 91 | [Test] 92 | public void RightBracket() 93 | { 94 | Assert.That(Tokener.Tokenize("]").First(), Is.EqualTo(Token.RightBracket())); 95 | } 96 | 97 | [Test] 98 | public void PlusWhiteSpacePrepended() 99 | { 100 | Assert.That(Tokener.Tokenize(" +").First(), Is.EqualTo(Token.Plus())); 101 | } 102 | 103 | [Test] 104 | public void RightParenthesis() 105 | { 106 | Assert.That(Tokener.Tokenize(")").First(), Is.EqualTo(Token.RightParenthesis())); 107 | } 108 | 109 | [Test] 110 | public void Greater() 111 | { 112 | Assert.That(Tokener.Tokenize(">").First(), Is.EqualTo(Token.Greater())); 113 | } 114 | 115 | [Test] 116 | public void GreaterWhiteSpacePrepended() 117 | { 118 | Assert.That(Tokener.Tokenize(" >").First(), Is.EqualTo(Token.Greater())); 119 | } 120 | 121 | [Test] 122 | public void IdentifierLowerCaseOnly() 123 | { 124 | Assert.That(Tokener.Tokenize("foo").First(), Is.EqualTo(Token.Ident("foo"))); 125 | } 126 | 127 | [Test] 128 | public void IdentifierMixedCase() 129 | { 130 | Assert.That(Tokener.Tokenize("FoObAr").First(), Is.EqualTo(Token.Ident("FoObAr"))); 131 | } 132 | 133 | [Test] 134 | public void IdentifierIncludingDigits() 135 | { 136 | Assert.That(Tokener.Tokenize("foobar42").First(), Is.EqualTo(Token.Ident("foobar42"))); 137 | } 138 | 139 | [Test] 140 | public void IdentifierWithUnderscores() 141 | { 142 | Assert.That(Tokener.Tokenize("_foo_BAR_42_").First(), Is.EqualTo(Token.Ident("_foo_BAR_42_"))); 143 | } 144 | 145 | [Test] 146 | public void IdentifierWithHypens() 147 | { 148 | Assert.That(Tokener.Tokenize("foo-BAR-42").First(), Is.EqualTo(Token.Ident("foo-BAR-42"))); 149 | } 150 | 151 | [Test] 152 | public void IdentifierUsingVendorExtensionSyntax() 153 | { 154 | Assert.That(Tokener.Tokenize("-foo-BAR-42").First(), Is.EqualTo(Token.Ident("-foo-BAR-42"))); 155 | } 156 | 157 | [Test] 158 | public void IdentifierUsingVendorExtensionSyntaxCannotBeginWithDigit() 159 | { 160 | Assert.That(() => Tokener.Tokenize("-42").ToArray(), 161 | Throws.TypeOf()); 162 | } 163 | 164 | [Test] 165 | public void Hash() 166 | { 167 | Assert.That(Tokener.Tokenize("#foo_BAR-baz-42").First(), Is.EqualTo(Token.Hash("foo_BAR-baz-42"))); 168 | } 169 | 170 | [Test] 171 | public void Includes() 172 | { 173 | Assert.That(Tokener.Tokenize("~=").First().Kind, Is.EqualTo(TokenKind.Includes)); 174 | } 175 | 176 | [Test] 177 | public void TildeTilde() 178 | { 179 | Assert.That(Tokener.Tokenize("~~").Take(2).ToArray(), Is.EqualTo(new[] { Token.Tilde(), Token.Tilde() })); 180 | } 181 | 182 | [Test] 183 | public void DashMatch() 184 | { 185 | Assert.That(Tokener.Tokenize("|=").First().Kind, Is.EqualTo(TokenKind.DashMatch)); 186 | } 187 | 188 | [Test] 189 | public void Pipe() 190 | { 191 | Assert.That(Tokener.Tokenize("||").Take(2).ToArray(), 192 | Is.EqualTo(new[] { Token.Char('|'), Token.Char('|') })); 193 | } 194 | 195 | [TestCase("\"\"")] 196 | [TestCase("''")] 197 | public void EmptyString(string input) 198 | { 199 | Assert.That(Tokener.Tokenize(input).First(), Is.EqualTo(Token.String(string.Empty))); 200 | } 201 | 202 | [Test] 203 | public void StringSingleQuoted() 204 | { 205 | Assert.That(Tokener.Tokenize("'foo bar'").First(), Is.EqualTo(Token.String("foo bar"))); 206 | } 207 | 208 | [Test] 209 | public void StringDoubleQuoted() 210 | { 211 | Assert.That(Tokener.Tokenize("\"foo bar\"").First(), Is.EqualTo(Token.String("foo bar"))); 212 | } 213 | 214 | [Test] 215 | public void StringDoubleQuotedWithEscapedDoubleQuotes() 216 | { 217 | Assert.That(Tokener.Tokenize("\"foo \\\"bar\\\" baz\"").First(), Is.EqualTo(Token.String("foo \"bar\" baz"))); 218 | } 219 | 220 | [Test] 221 | public void StringSingleQuotedWithEscapedSingleQuotes() 222 | { 223 | Assert.That(Tokener.Tokenize(@"'foo \'bar\' baz'").First(), Is.EqualTo(Token.String("foo 'bar' baz"))); 224 | } 225 | 226 | [Test] 227 | public void StringDoubleQuotedWithEscapedBackslashes() 228 | { 229 | Assert.That(Tokener.Tokenize("\"foo \\\\bar\\\\ baz\"").First(), Is.EqualTo(Token.String(@"foo \bar\ baz"))); 230 | } 231 | 232 | [Test] 233 | public void StringSingleQuotedWithEscapedBackslashes() 234 | { 235 | Assert.That(Tokener.Tokenize(@"'foo \\bar\\ baz'").First(), Is.EqualTo(Token.String(@"foo \bar\ baz"))); 236 | } 237 | 238 | [Test] 239 | public void BracketedIdent() 240 | { 241 | var token = Tokener.Tokenize("[foo]").GetEnumerator(); 242 | Assert.That(token.MoveNext(), Is.True); Assert.That(token.Current, Is.EqualTo(Token.LeftBracket())); 243 | Assert.That(token.MoveNext(), Is.True); Assert.That(token.Current, Is.EqualTo(Token.Ident("foo"))); 244 | Assert.That(token.MoveNext(), Is.True); Assert.That(token.Current, Is.EqualTo(Token.RightBracket())); 245 | Assert.That(token.MoveNext(), Is.True); Assert.That(token.Current, Is.EqualTo(Token.Eoi())); 246 | Assert.That(token.MoveNext(), Is.False); 247 | } 248 | 249 | [Test] 250 | public void BadHash() 251 | { 252 | Assert.That(() => Tokener.Tokenize("#").ToArray(), Throws.TypeOf()); 253 | } 254 | 255 | [Test] 256 | public void HashDelimitedCorrectly() 257 | { 258 | var token = Tokener.Tokenize("#foo.").GetEnumerator(); 259 | Assert.That(token.MoveNext(), Is.True); Assert.That(token.Current, Is.EqualTo(Token.Hash("foo"))); 260 | Assert.That(token.MoveNext(), Is.True); Assert.That(token.Current, Is.EqualTo(Token.Dot())); 261 | Assert.That(token.MoveNext(), Is.True); Assert.That(token.Current, Is.EqualTo(Token.Eoi())); 262 | Assert.That(token.MoveNext(), Is.False); 263 | } 264 | 265 | [Test] 266 | public void Function() 267 | { 268 | Assert.That(Tokener.Tokenize("funky(").First(), Is.EqualTo(Token.Function("funky"))); 269 | } 270 | 271 | [Test] 272 | public void FunctionWithEnclosedIdent() 273 | { 274 | var token = Tokener.Tokenize("foo(bar)").GetEnumerator(); 275 | Assert.That(token.MoveNext(), Is.True); Assert.That(token.Current, Is.EqualTo(Token.Function("foo"))); 276 | Assert.That(token.MoveNext(), Is.True); Assert.That(token.Current, Is.EqualTo(Token.Ident("bar"))); 277 | Assert.That(token.MoveNext(), Is.True); Assert.That(token.Current, Is.EqualTo(Token.RightParenthesis())); 278 | Assert.That(token.MoveNext(), Is.True); Assert.That(token.Current, Is.EqualTo(Token.Eoi())); 279 | Assert.That(token.MoveNext(), Is.False); 280 | } 281 | 282 | [Test] 283 | #pragma warning disable CA1720 // Identifier contains type name (matches subject) 284 | public void Integer() 285 | #pragma warning restore CA1720 // Identifier contains type name 286 | { 287 | Assert.That(Tokener.Tokenize("42").First(), Is.EqualTo(Token.Integer("42"))); 288 | } 289 | 290 | [Test] 291 | public void IntegerEnclosed() 292 | { 293 | var token = Tokener.Tokenize("[42]").GetEnumerator(); 294 | Assert.That(token.MoveNext(), Is.True); Assert.That(token.Current, Is.EqualTo(Token.LeftBracket())); 295 | Assert.That(token.MoveNext(), Is.True); Assert.That(token.Current, Is.EqualTo(Token.Integer("42"))); 296 | Assert.That(token.MoveNext(), Is.True); Assert.That(token.Current, Is.EqualTo(Token.RightBracket())); 297 | Assert.That(token.MoveNext(), Is.True); Assert.That(token.Current, Is.EqualTo(Token.Eoi())); 298 | Assert.That(token.MoveNext(), Is.False); 299 | } 300 | 301 | [Test] 302 | public void SubstringMatch() 303 | { 304 | Assert.That(Tokener.Tokenize("*=").First().Kind, Is.EqualTo(TokenKind.SubstringMatch)); 305 | } 306 | 307 | [Test] 308 | public void Star() 309 | { 310 | Assert.That(Tokener.Tokenize("*").First(), Is.EqualTo(Token.Char('*'))); 311 | } 312 | 313 | [Test] 314 | public void StarStar() 315 | { 316 | Assert.That(Tokener.Tokenize("**").Take(2).ToArray(), 317 | Is.EqualTo(new[] { Token.Char('*'), Token.Char('*') })); 318 | } 319 | 320 | [Test] 321 | public void Tilde() 322 | { 323 | Assert.That(Tokener.Tokenize("~").First().Kind, Is.EqualTo(TokenKind.Tilde)); 324 | } 325 | 326 | [Test] 327 | public void TildeWhitespacePrepended() 328 | { 329 | Assert.That(Tokener.Tokenize(" ~").First().Kind, Is.EqualTo(TokenKind.Tilde)); 330 | } 331 | 332 | [Test] 333 | public void StringSingleQuoteUnterminated() 334 | { 335 | Assert.That(() => Tokener.Tokenize("'foo").ToArray(), Throws.TypeOf()); 336 | } 337 | 338 | [Test] 339 | public void StringDoubleQuoteUnterminated() 340 | { 341 | Assert.That(() => Tokener.Tokenize("\"foo").ToArray(), Throws.TypeOf()); 342 | } 343 | 344 | [TestCase(@"'f\oo")] 345 | [TestCase(@"'foo\")] 346 | public void StringInvalidEscaping(string input) 347 | { 348 | Assert.That(() => Tokener.Tokenize(input).ToArray(), Throws.TypeOf()); 349 | } 350 | 351 | [Test] 352 | public void NullReader() 353 | { 354 | Assert.That(() => Tokener.Tokenize((TextReader)null!), 355 | Throws.ArgumentNullException("reader")); 356 | 357 | } 358 | 359 | [Test] 360 | public void StringReader() 361 | { 362 | Assert.That(Tokener.Tokenize(new StringReader("123,*")).ToArray(), 363 | Is.EqualTo(new[] { Token.Integer("123"), Token.Comma(), Token.Char('*'), Token.Eoi() })); 364 | } 365 | 366 | [Test] 367 | public void InvalidChar() 368 | { 369 | Assert.That(() => Tokener.Tokenize("what?").ToArray(), Throws.TypeOf()); 370 | } 371 | 372 | [Test] 373 | public void Not() 374 | { 375 | Assert.That(Tokener.Tokenize(":not(").First(), Is.EqualTo(Token.Not())); 376 | } 377 | 378 | [Test] 379 | public void NotNotFunction() 380 | { 381 | Assert.That(Tokener.Tokenize(":notnot(").Take(2), 382 | Is.EqualTo(new[] { Token.Char(':'), Token.Function("notnot") })); 383 | } 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /src/SelectorGenerator.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler 23 | { 24 | #region Imports 25 | 26 | using System; 27 | using System.Collections.Generic; 28 | using System.Linq; 29 | 30 | #endregion 31 | 32 | /// 33 | /// A selector generator implementation for an arbitrary document/element system. 34 | /// 35 | public class SelectorGenerator : INegationSelectorGenerator 36 | { 37 | readonly IEqualityComparer equalityComparer; 38 | Selector? selector; 39 | readonly Stack> selectors = new(); 40 | Selector? negationSourceSelector; 41 | 42 | /// 43 | /// Initializes a new instance of this object with an instance 44 | /// of and the default equality 45 | /// comparer that is used for determining if two elements are equal. 46 | /// 47 | public SelectorGenerator(IElementOps ops) : this(ops, null) {} 48 | 49 | /// 50 | /// Initializes a new instance of this object with an instance 51 | /// of and an equality comparer 52 | /// used for determining if two elements are equal. 53 | /// 54 | #pragma warning disable IDE0290 // Use primary constructor (preserve doc) 55 | public SelectorGenerator(IElementOps ops, IEqualityComparer? equalityComparer) 56 | #pragma warning restore IDE0290 // Use primary constructor 57 | { 58 | Ops = ops ?? throw new ArgumentNullException(nameof(ops)); 59 | this.equalityComparer = equalityComparer ?? EqualityComparer.Default; 60 | } 61 | 62 | /// 63 | /// Gets the selector implementation. 64 | /// 65 | /// 66 | /// If the generation is not complete, this property returns the 67 | /// last generated selector. 68 | /// 69 | public Selector Selector => this.selector ?? throw new InvalidOperationException(); 70 | 71 | /// 72 | /// Gets the instance that this object 73 | /// was initialized with. 74 | /// 75 | public IElementOps Ops { get; } 76 | 77 | /// 78 | /// Returns the collection of selector implementations representing 79 | /// a group. 80 | /// 81 | /// 82 | /// If the generation is not complete, this method return the 83 | /// selectors generated so far in a group. 84 | /// 85 | public IEnumerable> GetSelectors() 86 | { 87 | var selectors = this.selectors; 88 | return this.selector is { } top 89 | ? selectors.Concat(Enumerable.Repeat(top, 1)) 90 | : selectors.Select(s => s); 91 | } 92 | 93 | /// 94 | /// Adds a generated selector. 95 | /// 96 | protected void Add(Selector selector) 97 | { 98 | if(selector == null) throw new ArgumentNullException(nameof(selector)); 99 | 100 | this.selector = this.selector is { } top ? (elements => selector(top(elements))) : selector; 101 | } 102 | 103 | /// 104 | /// Delimits the initialization of a generation. 105 | /// 106 | public virtual void OnInit() 107 | { 108 | this.selectors.Clear(); 109 | this.selector = null; 110 | } 111 | 112 | /// 113 | /// Delimits a selector generation in a group of selectors. 114 | /// 115 | public virtual void OnSelector() 116 | { 117 | if (this.selector is { } someSelector) 118 | this.selectors.Push(someSelector); 119 | this.selector = null; 120 | } 121 | 122 | /// 123 | /// Delimits the closing/conclusion of a generation. 124 | /// 125 | public virtual void OnClose() 126 | { 127 | var sum = GetSelectors().Aggregate((a, b) => elements => a(elements).Concat(b(elements))); 128 | var normalize = Ops.Descendant(); 129 | this.selector = elements => sum(normalize(elements)).Distinct(this.equalityComparer); 130 | this.selectors.Clear(); 131 | } 132 | 133 | /// 134 | /// Generates a ID selector, 135 | /// which represents an element instance that has an identifier that 136 | /// matches the identifier in the ID selector. 137 | /// 138 | public virtual void Id(string id) => 139 | Add(Ops.Id(id)); 140 | 141 | /// 142 | /// Generates a class selector, 143 | /// which is an alternative when 144 | /// representing the class attribute. 145 | /// 146 | #pragma warning disable CA1716 // Identifiers should not match keywords (inherited) 147 | public virtual void Class(string clazz) => 148 | Add(Ops.Class(clazz)); 149 | #pragma warning restore CA1716 // Identifiers should not match keywords 150 | 151 | /// 152 | /// Generates a type selector, 153 | /// which represents an instance of the element type in the document tree. 154 | /// 155 | #pragma warning disable CA1725 // Parameter names should match base declaration (compatibility) 156 | public virtual void Type(NamespacePrefix prefix, string type) => 157 | Add(Ops.Type(prefix, type)); 158 | #pragma warning restore CA1725 // Parameter names should match base declaration 159 | 160 | /// 161 | /// Generates a universal selector, 162 | /// any single element in the document tree in any namespace 163 | /// (including those without a namespace) if no default namespace 164 | /// has been specified for selectors. 165 | /// 166 | public virtual void Universal(NamespacePrefix prefix) => 167 | Add(Ops.Universal(prefix)); 168 | 169 | /// 170 | /// Generates an attribute selector 171 | /// that represents an element with the given attribute 172 | /// whatever the values of the attribute. 173 | /// 174 | public virtual void AttributeExists(NamespacePrefix prefix, string name) => 175 | Add(Ops.AttributeExists(prefix, name)); 176 | 177 | /// 178 | /// Generates an attribute selector 179 | /// that represents an element with the given attribute 180 | /// and whose value is exactly . 181 | /// 182 | public virtual void AttributeExact(NamespacePrefix prefix, string name, string value) => 183 | Add(Ops.AttributeExact(prefix, name, value)); 184 | 185 | /// 186 | /// Generates an attribute selector 187 | /// that represents an element with the given attribute 188 | /// and whose value is a whitespace-separated list of words, one of 189 | /// which is exactly . 190 | /// 191 | public virtual void AttributeIncludes(NamespacePrefix prefix, string name, string value) => 192 | Add(Ops.AttributeIncludes(prefix, name, value)); 193 | 194 | /// 195 | /// Generates an attribute selector 196 | /// that represents an element with the given attribute , 197 | /// its value either being exactly or beginning 198 | /// with immediately followed by "-" (U+002D). 199 | /// 200 | public virtual void AttributeDashMatch(NamespacePrefix prefix, string name, string value) => 201 | Add(Ops.AttributeDashMatch(prefix, name, value)); 202 | 203 | /// 204 | /// Generates an attribute selector 205 | /// that represents an element with the attribute 206 | /// whose value begins with the prefix . 207 | /// 208 | public void AttributePrefixMatch(NamespacePrefix prefix, string name, string value) => 209 | Add(Ops.AttributePrefixMatch(prefix, name, value)); 210 | 211 | /// 212 | /// Generates an attribute selector 213 | /// that represents an element with the attribute 214 | /// whose value ends with the suffix . 215 | /// 216 | public void AttributeSuffixMatch(NamespacePrefix prefix, string name, string value) => 217 | Add(Ops.AttributeSuffixMatch(prefix, name, value)); 218 | 219 | /// 220 | /// Generates an attribute selector 221 | /// that represents an element with the attribute 222 | /// whose value contains at least one instance of the substring . 223 | /// 224 | public void AttributeSubstring(NamespacePrefix prefix, string name, string value) => 225 | Add(Ops.AttributeSubstring(prefix, name, value)); 226 | 227 | /// 228 | /// Generates a pseudo-class selector, 229 | /// which represents an element that is the first child of some other element. 230 | /// 231 | public virtual void FirstChild() => 232 | Add(Ops.FirstChild()); 233 | 234 | /// 235 | /// Generates a pseudo-class selector, 236 | /// which represents an element that is the last child of some other element. 237 | /// 238 | public virtual void LastChild() => 239 | Add(Ops.LastChild()); 240 | 241 | /// 242 | /// Generates a pseudo-class selector, 243 | /// which represents an element that is the N-th child of some other element. 244 | /// 245 | public virtual void NthChild(int a, int b) => 246 | Add(Ops.NthChild(a, b)); 247 | 248 | /// 249 | /// Generates a pseudo-class selector, 250 | /// which represents an element that has a parent element and whose parent 251 | /// element has no other element children. 252 | /// 253 | public virtual void OnlyChild() => 254 | Add(Ops.OnlyChild()); 255 | 256 | /// 257 | /// Generates a pseudo-class selector, 258 | /// which represents an element that has no children at all. 259 | /// 260 | public virtual void Empty() => 261 | Add(Ops.Empty()); 262 | 263 | /// 264 | /// Generates a combinator, 265 | /// which represents a childhood relationship between two elements. 266 | /// 267 | public virtual void Child() => 268 | Add(Ops.Child()); 269 | 270 | /// 271 | /// Generates a combinator, 272 | /// which represents a relationship between two elements where one element is an 273 | /// arbitrary descendant of some ancestor element. 274 | /// 275 | public virtual void Descendant() => 276 | Add(Ops.Descendant()); 277 | 278 | /// 279 | /// Generates a combinator, 280 | /// which represents elements that share the same parent in the document tree and 281 | /// where the first element immediately precedes the second element. 282 | /// 283 | public virtual void Adjacent() => 284 | Add(Ops.Adjacent()); 285 | 286 | /// 287 | /// Generates a combinator, 288 | /// which separates two sequences of simple selectors. The elements represented 289 | /// by the two sequences share the same parent in the document tree and the 290 | /// element represented by the first sequence precedes (not necessarily 291 | /// immediately) the element represented by the second one. 292 | /// 293 | public virtual void GeneralSibling() => 294 | Add(Ops.GeneralSibling()); 295 | 296 | /// 297 | /// Generates a pseudo-class selector, 298 | /// which represents an element that is the N-th child from bottom up of some other element. 299 | /// 300 | public void NthLastChild(int a, int b) => 301 | Add(Ops.NthLastChild(a, b)); 302 | 303 | /// 304 | /// Starts a negation pseudo-class selector, 305 | /// which represents an element that is not represented by its argument. 306 | /// 307 | public void BeginNegation() 308 | { 309 | this.negationSourceSelector = Selector; 310 | this.selector = null; 311 | } 312 | 313 | /// 314 | /// Generates a negation pseudo-class selector, 315 | /// which represents an element that is not represented by its argument. 316 | /// 317 | public void EndNegation() 318 | { 319 | var negationSourceSelector = this.negationSourceSelector ?? throw new InvalidOperationException(); 320 | var selector = Selector; 321 | this.selector = nodes => negationSourceSelector(nodes).Except(selector(nodes), this.equalityComparer); 322 | this.negationSourceSelector = null; 323 | } 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/PublicAPI.Shipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | Fizzler.HumanReadableSelectorGenerator 3 | Fizzler.HumanReadableSelectorGenerator.Add(string! selector) -> void 4 | Fizzler.HumanReadableSelectorGenerator.Adjacent() -> void 5 | Fizzler.HumanReadableSelectorGenerator.AttributeDashMatch(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 6 | Fizzler.HumanReadableSelectorGenerator.AttributeExact(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 7 | Fizzler.HumanReadableSelectorGenerator.AttributeExists(Fizzler.NamespacePrefix prefix, string! name) -> void 8 | Fizzler.HumanReadableSelectorGenerator.AttributeIncludes(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 9 | Fizzler.HumanReadableSelectorGenerator.AttributePrefixMatch(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 10 | Fizzler.HumanReadableSelectorGenerator.AttributeSubstring(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 11 | Fizzler.HumanReadableSelectorGenerator.AttributeSuffixMatch(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 12 | Fizzler.HumanReadableSelectorGenerator.Child() -> void 13 | Fizzler.HumanReadableSelectorGenerator.Descendant() -> void 14 | Fizzler.HumanReadableSelectorGenerator.Empty() -> void 15 | Fizzler.HumanReadableSelectorGenerator.FirstChild() -> void 16 | Fizzler.HumanReadableSelectorGenerator.GeneralSibling() -> void 17 | Fizzler.HumanReadableSelectorGenerator.HumanReadableSelectorGenerator() -> void 18 | Fizzler.HumanReadableSelectorGenerator.Id(string! id) -> void 19 | Fizzler.HumanReadableSelectorGenerator.LastChild() -> void 20 | Fizzler.HumanReadableSelectorGenerator.NthChild(int a, int b) -> void 21 | Fizzler.HumanReadableSelectorGenerator.NthLastChild(int a, int b) -> void 22 | Fizzler.HumanReadableSelectorGenerator.OnlyChild() -> void 23 | Fizzler.HumanReadableSelectorGenerator.Text.get -> string! 24 | Fizzler.HumanReadableSelectorGenerator.Type(Fizzler.NamespacePrefix prefix, string! type) -> void 25 | Fizzler.HumanReadableSelectorGenerator.Universal(Fizzler.NamespacePrefix prefix) -> void 26 | Fizzler.IElementOps 27 | Fizzler.IElementOps.Adjacent() -> Fizzler.Selector! 28 | Fizzler.IElementOps.AttributeDashMatch(Fizzler.NamespacePrefix prefix, string! name, string! value) -> Fizzler.Selector! 29 | Fizzler.IElementOps.AttributeExact(Fizzler.NamespacePrefix prefix, string! name, string! value) -> Fizzler.Selector! 30 | Fizzler.IElementOps.AttributeExists(Fizzler.NamespacePrefix prefix, string! name) -> Fizzler.Selector! 31 | Fizzler.IElementOps.AttributeIncludes(Fizzler.NamespacePrefix prefix, string! name, string! value) -> Fizzler.Selector! 32 | Fizzler.IElementOps.AttributePrefixMatch(Fizzler.NamespacePrefix prefix, string! name, string! value) -> Fizzler.Selector! 33 | Fizzler.IElementOps.AttributeSubstring(Fizzler.NamespacePrefix prefix, string! name, string! value) -> Fizzler.Selector! 34 | Fizzler.IElementOps.AttributeSuffixMatch(Fizzler.NamespacePrefix prefix, string! name, string! value) -> Fizzler.Selector! 35 | Fizzler.IElementOps.Child() -> Fizzler.Selector! 36 | Fizzler.IElementOps.Class(string! clazz) -> Fizzler.Selector! 37 | Fizzler.IElementOps.Descendant() -> Fizzler.Selector! 38 | Fizzler.IElementOps.Empty() -> Fizzler.Selector! 39 | Fizzler.IElementOps.FirstChild() -> Fizzler.Selector! 40 | Fizzler.IElementOps.GeneralSibling() -> Fizzler.Selector! 41 | Fizzler.IElementOps.Id(string! id) -> Fizzler.Selector! 42 | Fizzler.IElementOps.LastChild() -> Fizzler.Selector! 43 | Fizzler.IElementOps.NthChild(int a, int b) -> Fizzler.Selector! 44 | Fizzler.IElementOps.NthLastChild(int a, int b) -> Fizzler.Selector! 45 | Fizzler.IElementOps.OnlyChild() -> Fizzler.Selector! 46 | Fizzler.IElementOps.Type(Fizzler.NamespacePrefix prefix, string! name) -> Fizzler.Selector! 47 | Fizzler.IElementOps.Universal(Fizzler.NamespacePrefix prefix) -> Fizzler.Selector! 48 | Fizzler.INegationSelectorGenerator 49 | Fizzler.INegationSelectorGenerator.BeginNegation() -> void 50 | Fizzler.INegationSelectorGenerator.EndNegation() -> void 51 | Fizzler.ISelectorGenerator 52 | Fizzler.ISelectorGenerator.Adjacent() -> void 53 | Fizzler.ISelectorGenerator.AttributeDashMatch(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 54 | Fizzler.ISelectorGenerator.AttributeExact(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 55 | Fizzler.ISelectorGenerator.AttributeExists(Fizzler.NamespacePrefix prefix, string! name) -> void 56 | Fizzler.ISelectorGenerator.AttributeIncludes(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 57 | Fizzler.ISelectorGenerator.AttributePrefixMatch(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 58 | Fizzler.ISelectorGenerator.AttributeSubstring(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 59 | Fizzler.ISelectorGenerator.AttributeSuffixMatch(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 60 | Fizzler.ISelectorGenerator.Child() -> void 61 | Fizzler.ISelectorGenerator.Class(string! clazz) -> void 62 | Fizzler.ISelectorGenerator.Descendant() -> void 63 | Fizzler.ISelectorGenerator.Empty() -> void 64 | Fizzler.ISelectorGenerator.FirstChild() -> void 65 | Fizzler.ISelectorGenerator.GeneralSibling() -> void 66 | Fizzler.ISelectorGenerator.Id(string! id) -> void 67 | Fizzler.ISelectorGenerator.LastChild() -> void 68 | Fizzler.ISelectorGenerator.NthChild(int a, int b) -> void 69 | Fizzler.ISelectorGenerator.NthLastChild(int a, int b) -> void 70 | Fizzler.ISelectorGenerator.OnClose() -> void 71 | Fizzler.ISelectorGenerator.OnInit() -> void 72 | Fizzler.ISelectorGenerator.OnlyChild() -> void 73 | Fizzler.ISelectorGenerator.OnSelector() -> void 74 | Fizzler.ISelectorGenerator.Type(Fizzler.NamespacePrefix prefix, string! name) -> void 75 | Fizzler.ISelectorGenerator.Universal(Fizzler.NamespacePrefix prefix) -> void 76 | Fizzler.NamespacePrefix 77 | Fizzler.NamespacePrefix.Format(string! name) -> string! 78 | Fizzler.NamespacePrefix.IsAny.get -> bool 79 | Fizzler.NamespacePrefix.IsEmpty.get -> bool 80 | Fizzler.NamespacePrefix.IsNone.get -> bool 81 | Fizzler.NamespacePrefix.IsSpecific.get -> bool 82 | Fizzler.NamespacePrefix.NamespacePrefix() -> void 83 | Fizzler.NamespacePrefix.NamespacePrefix(string? text) -> void 84 | Fizzler.NamespacePrefix.Text.get -> string? 85 | Fizzler.Parser 86 | Fizzler.Reader 87 | Fizzler.Reader.Close() -> void 88 | Fizzler.Reader.GetEnumerator() -> System.Collections.Generic.IEnumerator! 89 | Fizzler.Reader.HasMore.get -> bool 90 | Fizzler.Reader.Peek() -> T 91 | Fizzler.Reader.Read() -> T 92 | Fizzler.Reader.Reader(System.Collections.Generic.IEnumerable! e) -> void 93 | Fizzler.Reader.Reader(System.Collections.Generic.IEnumerator! e) -> void 94 | Fizzler.Reader.Unread(T value) -> void 95 | Fizzler.Selector 96 | Fizzler.SelectorGenerator 97 | Fizzler.SelectorGenerator.Add(Fizzler.Selector! selector) -> void 98 | Fizzler.SelectorGenerator.AttributePrefixMatch(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 99 | Fizzler.SelectorGenerator.AttributeSubstring(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 100 | Fizzler.SelectorGenerator.AttributeSuffixMatch(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 101 | Fizzler.SelectorGenerator.BeginNegation() -> void 102 | Fizzler.SelectorGenerator.EndNegation() -> void 103 | Fizzler.SelectorGenerator.GetSelectors() -> System.Collections.Generic.IEnumerable!>! 104 | Fizzler.SelectorGenerator.NthLastChild(int a, int b) -> void 105 | Fizzler.SelectorGenerator.Ops.get -> Fizzler.IElementOps! 106 | Fizzler.SelectorGenerator.Selector.get -> Fizzler.Selector! 107 | Fizzler.SelectorGenerator.SelectorGenerator(Fizzler.IElementOps! ops) -> void 108 | Fizzler.SelectorGenerator.SelectorGenerator(Fizzler.IElementOps! ops, System.Collections.Generic.IEqualityComparer? equalityComparer) -> void 109 | Fizzler.SelectorGeneratorTee 110 | Fizzler.SelectorGeneratorTee.Adjacent() -> void 111 | Fizzler.SelectorGeneratorTee.AttributeDashMatch(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 112 | Fizzler.SelectorGeneratorTee.AttributeExact(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 113 | Fizzler.SelectorGeneratorTee.AttributeExists(Fizzler.NamespacePrefix prefix, string! name) -> void 114 | Fizzler.SelectorGeneratorTee.AttributeIncludes(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 115 | Fizzler.SelectorGeneratorTee.AttributePrefixMatch(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 116 | Fizzler.SelectorGeneratorTee.AttributeSubstring(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 117 | Fizzler.SelectorGeneratorTee.AttributeSuffixMatch(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 118 | Fizzler.SelectorGeneratorTee.BeginNegation() -> void 119 | Fizzler.SelectorGeneratorTee.Child() -> void 120 | Fizzler.SelectorGeneratorTee.Class(string! clazz) -> void 121 | Fizzler.SelectorGeneratorTee.Descendant() -> void 122 | Fizzler.SelectorGeneratorTee.Empty() -> void 123 | Fizzler.SelectorGeneratorTee.EndNegation() -> void 124 | Fizzler.SelectorGeneratorTee.FirstChild() -> void 125 | Fizzler.SelectorGeneratorTee.GeneralSibling() -> void 126 | Fizzler.SelectorGeneratorTee.Id(string! id) -> void 127 | Fizzler.SelectorGeneratorTee.LastChild() -> void 128 | Fizzler.SelectorGeneratorTee.NthChild(int a, int b) -> void 129 | Fizzler.SelectorGeneratorTee.NthLastChild(int a, int b) -> void 130 | Fizzler.SelectorGeneratorTee.OnClose() -> void 131 | Fizzler.SelectorGeneratorTee.OnInit() -> void 132 | Fizzler.SelectorGeneratorTee.OnlyChild() -> void 133 | Fizzler.SelectorGeneratorTee.OnSelector() -> void 134 | Fizzler.SelectorGeneratorTee.Primary.get -> Fizzler.ISelectorGenerator! 135 | Fizzler.SelectorGeneratorTee.Secondary.get -> Fizzler.ISelectorGenerator! 136 | Fizzler.SelectorGeneratorTee.SelectorGeneratorTee(Fizzler.ISelectorGenerator! primary, Fizzler.ISelectorGenerator! secondary) -> void 137 | Fizzler.SelectorGeneratorTee.Type(Fizzler.NamespacePrefix prefix, string! type) -> void 138 | Fizzler.SelectorGeneratorTee.Universal(Fizzler.NamespacePrefix prefix) -> void 139 | Fizzler.SelectorsCachingCompiler 140 | Fizzler.Token 141 | Fizzler.Token.Kind.get -> Fizzler.TokenKind 142 | Fizzler.Token.Text.get -> string? 143 | Fizzler.Token.Token() -> void 144 | Fizzler.Tokener 145 | Fizzler.TokenKind 146 | Fizzler.TokenKind.Char = 16 -> Fizzler.TokenKind 147 | Fizzler.TokenKind.DashMatch = 4 -> Fizzler.TokenKind 148 | Fizzler.TokenKind.Eoi = 0 -> Fizzler.TokenKind 149 | Fizzler.TokenKind.Function = 12 -> Fizzler.TokenKind 150 | Fizzler.TokenKind.Greater = 10 -> Fizzler.TokenKind 151 | Fizzler.TokenKind.Hash = 2 -> Fizzler.TokenKind 152 | Fizzler.TokenKind.Ident = 1 -> Fizzler.TokenKind 153 | Fizzler.TokenKind.Includes = 3 -> Fizzler.TokenKind 154 | Fizzler.TokenKind.Integer = 14 -> Fizzler.TokenKind 155 | Fizzler.TokenKind.Not = 13 -> Fizzler.TokenKind 156 | Fizzler.TokenKind.Plus = 9 -> Fizzler.TokenKind 157 | Fizzler.TokenKind.PrefixMatch = 5 -> Fizzler.TokenKind 158 | Fizzler.TokenKind.String = 8 -> Fizzler.TokenKind 159 | Fizzler.TokenKind.SubstringMatch = 7 -> Fizzler.TokenKind 160 | Fizzler.TokenKind.SuffixMatch = 6 -> Fizzler.TokenKind 161 | Fizzler.TokenKind.Tilde = 15 -> Fizzler.TokenKind 162 | Fizzler.TokenKind.WhiteSpace = 11 -> Fizzler.TokenKind 163 | override Fizzler.NamespacePrefix.ToString() -> string! 164 | override Fizzler.Token.ToString() -> string! 165 | static Fizzler.Parser.Parse(string! selectors, TGenerator generator, System.Func! resultor) -> T 166 | static Fizzler.Parser.Parse(System.Collections.Generic.IEnumerable! tokens, TGenerator generator, System.Func! resultor) -> T 167 | static Fizzler.Parser.Parse(string! selectors, TGenerator generator) -> TGenerator 168 | static Fizzler.Parser.Parse(System.Collections.Generic.IEnumerable! tokens, TGenerator generator) -> TGenerator 169 | static Fizzler.SelectorsCachingCompiler.Create(System.Func! compiler) -> System.Func! 170 | static Fizzler.SelectorsCachingCompiler.Create(System.Func! compiler, System.Collections.Generic.IDictionary? cache) -> System.Func! 171 | static Fizzler.Token.Char(char ch) -> Fizzler.Token 172 | static Fizzler.Token.Colon() -> Fizzler.Token 173 | static Fizzler.Token.Comma() -> Fizzler.Token 174 | static Fizzler.Token.DashMatch() -> Fizzler.Token 175 | static Fizzler.Token.Dot() -> Fizzler.Token 176 | static Fizzler.Token.Eoi() -> Fizzler.Token 177 | static Fizzler.Token.Equals() -> Fizzler.Token 178 | static Fizzler.Token.Function(string! text) -> Fizzler.Token 179 | static Fizzler.Token.Greater() -> Fizzler.Token 180 | static Fizzler.Token.Hash(string! text) -> Fizzler.Token 181 | static Fizzler.Token.Ident(string! text) -> Fizzler.Token 182 | static Fizzler.Token.Includes() -> Fizzler.Token 183 | static Fizzler.Token.Integer(string! text) -> Fizzler.Token 184 | static Fizzler.Token.LeftBracket() -> Fizzler.Token 185 | static Fizzler.Token.Not() -> Fizzler.Token 186 | static Fizzler.Token.Pipe() -> Fizzler.Token 187 | static Fizzler.Token.Plus() -> Fizzler.Token 188 | static Fizzler.Token.PrefixMatch() -> Fizzler.Token 189 | static Fizzler.Token.RightBracket() -> Fizzler.Token 190 | static Fizzler.Token.RightParenthesis() -> Fizzler.Token 191 | static Fizzler.Token.Star() -> Fizzler.Token 192 | static Fizzler.Token.String(string? text) -> Fizzler.Token 193 | static Fizzler.Token.SubstringMatch() -> Fizzler.Token 194 | static Fizzler.Token.SuffixMatch() -> Fizzler.Token 195 | static Fizzler.Token.Tilde() -> Fizzler.Token 196 | static Fizzler.Token.WhiteSpace(string! space) -> Fizzler.Token 197 | static Fizzler.Tokener.Tokenize(string? input) -> System.Collections.Generic.IEnumerable! 198 | static Fizzler.Tokener.Tokenize(System.IO.TextReader! reader) -> System.Collections.Generic.IEnumerable! 199 | static readonly Fizzler.NamespacePrefix.Any -> Fizzler.NamespacePrefix 200 | static readonly Fizzler.NamespacePrefix.Empty -> Fizzler.NamespacePrefix 201 | static readonly Fizzler.NamespacePrefix.None -> Fizzler.NamespacePrefix 202 | virtual Fizzler.HumanReadableSelectorGenerator.OnClose() -> void 203 | virtual Fizzler.HumanReadableSelectorGenerator.OnInit() -> void 204 | virtual Fizzler.HumanReadableSelectorGenerator.OnSelector() -> void 205 | virtual Fizzler.SelectorGenerator.Adjacent() -> void 206 | virtual Fizzler.SelectorGenerator.AttributeDashMatch(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 207 | virtual Fizzler.SelectorGenerator.AttributeExact(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 208 | virtual Fizzler.SelectorGenerator.AttributeExists(Fizzler.NamespacePrefix prefix, string! name) -> void 209 | virtual Fizzler.SelectorGenerator.AttributeIncludes(Fizzler.NamespacePrefix prefix, string! name, string! value) -> void 210 | virtual Fizzler.SelectorGenerator.Child() -> void 211 | virtual Fizzler.SelectorGenerator.Class(string! clazz) -> void 212 | virtual Fizzler.SelectorGenerator.Descendant() -> void 213 | virtual Fizzler.SelectorGenerator.Empty() -> void 214 | virtual Fizzler.SelectorGenerator.FirstChild() -> void 215 | virtual Fizzler.SelectorGenerator.GeneralSibling() -> void 216 | virtual Fizzler.SelectorGenerator.Id(string! id) -> void 217 | virtual Fizzler.SelectorGenerator.LastChild() -> void 218 | virtual Fizzler.SelectorGenerator.NthChild(int a, int b) -> void 219 | virtual Fizzler.SelectorGenerator.OnClose() -> void 220 | virtual Fizzler.SelectorGenerator.OnInit() -> void 221 | virtual Fizzler.SelectorGenerator.OnlyChild() -> void 222 | virtual Fizzler.SelectorGenerator.OnSelector() -> void 223 | virtual Fizzler.SelectorGenerator.Type(Fizzler.NamespacePrefix prefix, string! type) -> void 224 | virtual Fizzler.SelectorGenerator.Universal(Fizzler.NamespacePrefix prefix) -> void 225 | -------------------------------------------------------------------------------- /tests/ParserTests.cs: -------------------------------------------------------------------------------- 1 | #region Copyright and License 2 | // 3 | // Fizzler - CSS Selector Engine for Microsoft .NET Framework 4 | // Copyright (c) 2009 Atif Aziz, Colin Ramsay. All rights reserved. 5 | // 6 | // This library is free software; you can redistribute it and/or modify it under 7 | // the terms of the GNU Lesser General Public License as published by the Free 8 | // Software Foundation; either version 3 of the License, or (at your option) 9 | // any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | // FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, write to the Free Software Foundation, Inc., 18 | // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | #endregion 21 | 22 | namespace Fizzler.Tests 23 | { 24 | #region Imports 25 | 26 | using System; 27 | using System.Collections.Generic; 28 | using NUnit.Framework; 29 | 30 | #endregion 31 | 32 | [TestFixture] 33 | public class ParserTests 34 | { 35 | [TestCase(":not(.foo.bar)")] 36 | [TestCase(":not(p.foo)")] 37 | [TestCase(":not(p div)")] 38 | [TestCase(":not(p, div)")] 39 | [TestCase(":not(#foo.bar)")] 40 | [TestCase(":not([foo][bar])")] 41 | public void Invalid(string selector) 42 | { 43 | Assert.That(() => Parser.Parse(Tokener.Tokenize(selector), new NoSelectorGenerator()), 44 | Throws.TypeOf()); 45 | } 46 | 47 | [Test] 48 | public void TypeNoNamespace() 49 | { 50 | Test("x", g => g.TypePrefix, NamespacePrefix.None, 51 | g => g.TypeName, "x"); 52 | } 53 | 54 | [Test] 55 | public void TypeEmptyNamespace() 56 | { 57 | Test("|x", g => g.TypePrefix, NamespacePrefix.Empty, 58 | g => g.TypeName, "x"); 59 | } 60 | 61 | [Test] 62 | public void TypeAnyNamespace() 63 | { 64 | Test("*|x", g => g.TypePrefix, NamespacePrefix.Any, 65 | g => g.TypeName, "x"); 66 | } 67 | 68 | [Test] 69 | public void NamespacedType() 70 | { 71 | Test("foo|bar", g => g.TypePrefix, new NamespacePrefix("foo"), 72 | g => g.TypeName, "bar"); 73 | } 74 | 75 | [Test] 76 | public void UniversalNoNamespace() 77 | { 78 | Test("*", g => g.UniversalPrefix, NamespacePrefix.None); 79 | } 80 | 81 | [Test] 82 | public void UniversalEmptyNamespace() 83 | { 84 | Test("|*", g => g.UniversalPrefix, NamespacePrefix.Empty); 85 | } 86 | 87 | [Test] 88 | public void UniversalAnyNamespace() 89 | { 90 | Test("*|*", g => g.UniversalPrefix, NamespacePrefix.Any); 91 | } 92 | 93 | [Test] 94 | public void NamespacedUniversal() 95 | { 96 | Test("foo|*", g => g.UniversalPrefix, new NamespacePrefix("foo")); 97 | } 98 | 99 | [Test] 100 | public void NoNamespaceAttribueExists() 101 | { 102 | Test("[foo]", g => g.AttributeExistsPrefix, NamespacePrefix.None, 103 | g => g.AttributeExistsName, "foo"); 104 | } 105 | 106 | [Test] 107 | public void EmptyNamespaceAttribueExists() 108 | { 109 | Test("[|a]", g => g.AttributeExistsPrefix, NamespacePrefix.Empty, 110 | g => g.AttributeExistsName, "a"); 111 | } 112 | 113 | [Test] 114 | public void AnyNamespaceAttribueExists() 115 | { 116 | Test("[*|a]", g => g.AttributeExistsPrefix, NamespacePrefix.Any, 117 | g => g.AttributeExistsName, "a"); 118 | } 119 | 120 | [Test] 121 | public void NamespacedAttribueExists() 122 | { 123 | Test("[ns|a]", g => g.AttributeExistsPrefix, new NamespacePrefix("ns"), 124 | g => g.AttributeExistsName, "a"); 125 | } 126 | 127 | [Test] 128 | public void NoNamespaceAttribueExact() 129 | { 130 | Test("[a=v]", g => g.AttributeExactPrefix, NamespacePrefix.None, 131 | g => g.AttributeExactName, "a", 132 | g => g.AttributeExactValue, "v"); 133 | } 134 | 135 | [Test] 136 | public void EmptyNamespaceAttribueExact() 137 | { 138 | Test("[|a=v]", g => g.AttributeExactPrefix, NamespacePrefix.Empty, 139 | g => g.AttributeExactName, "a", 140 | g => g.AttributeExactValue, "v"); 141 | } 142 | 143 | [Test] 144 | public void AnyNamespaceAttribueExact() 145 | { 146 | Test("[*|a=v]", g => g.AttributeExactPrefix, NamespacePrefix.Any, 147 | g => g.AttributeExactName, "a", 148 | g => g.AttributeExactValue, "v"); 149 | } 150 | 151 | [Test] 152 | public void NamespacedAttribueExact() 153 | { 154 | Test("[ns|a=v]", g => g.AttributeExactPrefix, new NamespacePrefix("ns"), 155 | g => g.AttributeExactName, "a", 156 | g => g.AttributeExactValue, "v"); 157 | } 158 | 159 | [Test] 160 | public void NoNamespaceAttribueIncludes() 161 | { 162 | Test("[a~=v]", g => g.AttributeIncludesPrefix, NamespacePrefix.None, 163 | g => g.AttributeIncludesName, "a", 164 | g => g.AttributeIncludesValue, "v"); 165 | } 166 | 167 | [Test] 168 | public void EmptyNamespaceAttribueIncludes() 169 | { 170 | Test("[|a~=v]", g => g.AttributeIncludesPrefix, NamespacePrefix.Empty, 171 | g => g.AttributeIncludesName, "a", 172 | g => g.AttributeIncludesValue, "v"); 173 | } 174 | 175 | [Test] 176 | public void AnyNamespaceAttribueIncludes() 177 | { 178 | Test("[*|a~=v]", g => g.AttributeIncludesPrefix, NamespacePrefix.Any, 179 | g => g.AttributeIncludesName, "a", 180 | g => g.AttributeIncludesValue, "v"); 181 | } 182 | 183 | [Test] 184 | public void NamespacedAttribueIncludes() 185 | { 186 | Test("[ns|a~=v]", g => g.AttributeIncludesPrefix, new NamespacePrefix("ns"), 187 | g => g.AttributeIncludesName, "a", 188 | g => g.AttributeIncludesValue, "v"); 189 | } 190 | 191 | [Test] 192 | public void NoNamespaceAttribueDashMatch() 193 | { 194 | Test("[a|=v]", g => g.AttributeDashMatchPrefix, NamespacePrefix.None, 195 | g => g.AttributeDashMatchName, "a", 196 | g => g.AttributeDashMatchValue, "v"); 197 | } 198 | 199 | [Test] 200 | public void EmptyNamespaceAttribueDashMatch() 201 | { 202 | Test("[|a|=v]", g => g.AttributeDashMatchPrefix, NamespacePrefix.Empty, 203 | g => g.AttributeDashMatchName, "a", 204 | g => g.AttributeDashMatchValue, "v"); 205 | } 206 | 207 | [Test] 208 | public void AnyNamespaceAttribueDashMatch() 209 | { 210 | Test("[*|a|=v]", g => g.AttributeDashMatchPrefix, NamespacePrefix.Any, 211 | g => g.AttributeDashMatchName, "a", 212 | g => g.AttributeDashMatchValue, "v"); 213 | } 214 | 215 | [Test] 216 | public void NamespacedAttribueDashMatch() 217 | { 218 | Test("[ns|a|=v]", g => g.AttributeDashMatchPrefix, new NamespacePrefix("ns"), 219 | g => g.AttributeDashMatchName, "a", 220 | g => g.AttributeDashMatchValue, "v"); 221 | } 222 | 223 | [Test] 224 | public void NoNamespaceAttribuePrefixMatch() 225 | { 226 | Test("[a^=v]", g => g.AttributePrefixMatchPrefix, NamespacePrefix.None, 227 | g => g.AttributePrefixMatchName, "a", 228 | g => g.AttributePrefixMatchValue, "v"); 229 | } 230 | 231 | [Test] 232 | public void EmptyNamespaceAttribuePrefixMatch() 233 | { 234 | Test("[|a^=v]", g => g.AttributePrefixMatchPrefix, NamespacePrefix.Empty, 235 | g => g.AttributePrefixMatchName, "a", 236 | g => g.AttributePrefixMatchValue, "v"); 237 | } 238 | 239 | [Test] 240 | public void AnyNamespaceAttribuePrefixMatch() 241 | { 242 | Test("[*|a^=v]", g => g.AttributePrefixMatchPrefix, NamespacePrefix.Any, 243 | g => g.AttributePrefixMatchName, "a", 244 | g => g.AttributePrefixMatchValue, "v"); 245 | } 246 | 247 | [Test] 248 | public void NamespacedAttribuePrefixMatch() 249 | { 250 | Test("[ns|a^=v]", g => g.AttributePrefixMatchPrefix, new NamespacePrefix("ns"), 251 | g => g.AttributePrefixMatchName, "a", 252 | g => g.AttributePrefixMatchValue, "v"); 253 | } 254 | 255 | [Test] 256 | public void NoNamespaceAttribueSuffixMatch() 257 | { 258 | Test("[a$=v]", g => g.AttributeSuffixMatchPrefix, NamespacePrefix.None, 259 | g => g.AttributeSuffixMatchName, "a", 260 | g => g.AttributeSuffixMatchValue, "v"); 261 | } 262 | 263 | [Test] 264 | public void EmptyNamespaceAttribueSuffixMatch() 265 | { 266 | Test("[|a$=v]", g => g.AttributeSuffixMatchPrefix, NamespacePrefix.Empty, 267 | g => g.AttributeSuffixMatchName, "a", 268 | g => g.AttributeSuffixMatchValue, "v"); 269 | } 270 | 271 | [Test] 272 | public void AnyNamespaceAttribueSuffixMatch() 273 | { 274 | Test("[*|a$=v]", g => g.AttributeSuffixMatchPrefix, NamespacePrefix.Any, 275 | g => g.AttributeSuffixMatchName, "a", 276 | g => g.AttributeSuffixMatchValue, "v"); 277 | } 278 | 279 | [Test] 280 | public void NamespacedAttribueSuffixMatch() 281 | { 282 | Test("[ns|a$=v]", g => g.AttributeSuffixMatchPrefix, new NamespacePrefix("ns"), 283 | g => g.AttributeSuffixMatchName, "a", 284 | g => g.AttributeSuffixMatchValue, "v"); 285 | } 286 | 287 | [Test] 288 | public void NoNamespaceAttribueSubstring() 289 | { 290 | Test("[a*=v]", g => g.AttributeSubstringPrefix, NamespacePrefix.None, 291 | g => g.AttributeSubstringName, "a", 292 | g => g.AttributeSubstringValue, "v"); 293 | } 294 | 295 | [Test] 296 | public void EmptyNamespaceAttribueSubstring() 297 | { 298 | Test("[|a*=v]", g => g.AttributeSubstringPrefix, NamespacePrefix.Empty, 299 | g => g.AttributeSubstringName, "a", 300 | g => g.AttributeSubstringValue, "v"); 301 | } 302 | 303 | [Test] 304 | public void AnyNamespaceAttribueSubstring() 305 | { 306 | Test("[*|a*=v]", g => g.AttributeSubstringPrefix, NamespacePrefix.Any, 307 | g => g.AttributeSubstringName, "a", 308 | g => g.AttributeSubstringValue, "v"); 309 | } 310 | 311 | [Test] 312 | public void NamespacedAttribueSubstring() 313 | { 314 | Test("[ns|a*=v]", g => g.AttributeSubstringPrefix, new NamespacePrefix("ns"), 315 | g => g.AttributeSubstringName, "a", 316 | g => g.AttributeSubstringValue, "v"); 317 | } 318 | 319 | static void Test(string input, Func actual1, T1 expected1) 320 | { 321 | Test(input, [g => actual1(g)], [expected1]); 322 | } 323 | 324 | static void Test(string input, 325 | Func actual1, T1 expected1, 326 | Func actual2, T2 expected2) 327 | { 328 | Test(input, 329 | [g => actual1(g), g => actual2(g)], 330 | [expected1, expected2]); 331 | } 332 | 333 | static void Test(string input, 334 | Func actual1, T1 expected1, 335 | Func actual2, T2 expected2, 336 | Func actual3, T2 expected3) 337 | { 338 | Test(input, 339 | [g => actual1(g), g => actual2(g), g => actual3(g)], 340 | [expected1, expected2, expected3]); 341 | } 342 | 343 | static void Test(string input, 344 | IEnumerable> actuals, 345 | IEnumerable expectations) 346 | { 347 | var generator = Parser.Parse(Tokener.Tokenize(input), new TestSelectorGenerator()); 348 | using var actual = actuals.GetEnumerator(); 349 | using var expected = expectations.GetEnumerator(); 350 | while(actual.MoveNext()) 351 | { 352 | Assert.That(expected.MoveNext(), Is.True, "Missing expectation"); 353 | Assert.That(actual.Current(generator), Is.EqualTo(expected.Current)); 354 | } 355 | Assert.That(expected.MoveNext(), Is.False, "Too many expectations"); 356 | } 357 | 358 | public class TestSelectorGenerator : ISelectorGenerator 359 | { 360 | public NamespacePrefix TypePrefix { get; private set; } 361 | public string? TypeName { get; private set; } 362 | 363 | public NamespacePrefix UniversalPrefix { get; private set; } 364 | 365 | public NamespacePrefix AttributeExistsPrefix { get; private set; } 366 | public string? AttributeExistsName { get; private set; } 367 | public NamespacePrefix AttributeExactPrefix { get; private set; } 368 | public string? AttributeExactName { get; private set; } 369 | public string? AttributeExactValue { get; private set; } 370 | public NamespacePrefix AttributeIncludesPrefix { get; private set; } 371 | public string? AttributeIncludesName { get; private set; } 372 | public string? AttributeIncludesValue { get; private set; } 373 | public NamespacePrefix AttributeDashMatchPrefix { get; private set; } 374 | public string? AttributeDashMatchName { get; private set; } 375 | public string? AttributeDashMatchValue { get; private set; } 376 | public NamespacePrefix AttributePrefixMatchPrefix { get; private set; } 377 | public string? AttributePrefixMatchName { get; private set; } 378 | public string? AttributePrefixMatchValue { get; private set; } 379 | public NamespacePrefix AttributeSuffixMatchPrefix { get; private set; } 380 | public string? AttributeSuffixMatchName { get; private set; } 381 | public string? AttributeSuffixMatchValue { get; private set; } 382 | public NamespacePrefix AttributeSubstringPrefix { get; private set; } 383 | public string? AttributeSubstringName { get; private set; } 384 | public string? AttributeSubstringValue { get; private set; } 385 | 386 | public void Type(NamespacePrefix prefix, string name) 387 | { 388 | TypePrefix = prefix; 389 | TypeName = name; 390 | } 391 | 392 | public void Universal(NamespacePrefix prefix) 393 | { 394 | UniversalPrefix = prefix; 395 | } 396 | 397 | public void AttributeExists(NamespacePrefix prefix, string name) 398 | { 399 | AttributeExistsPrefix = prefix; 400 | AttributeExistsName = name; 401 | } 402 | 403 | public void AttributeExact(NamespacePrefix prefix, string name, string value) 404 | { 405 | AttributeExactPrefix = prefix; 406 | AttributeExactName = name; 407 | AttributeExactValue = value; 408 | } 409 | 410 | public void AttributeIncludes(NamespacePrefix prefix, string name, string value) 411 | { 412 | AttributeIncludesPrefix = prefix; 413 | AttributeIncludesName = name; 414 | AttributeIncludesValue = value; 415 | } 416 | 417 | public void AttributeDashMatch(NamespacePrefix prefix, string name, string value) 418 | { 419 | AttributeDashMatchPrefix = prefix; 420 | AttributeDashMatchName = name; 421 | AttributeDashMatchValue = value; 422 | } 423 | 424 | public void AttributePrefixMatch(NamespacePrefix prefix, string name, string value) 425 | { 426 | AttributePrefixMatchPrefix = prefix; 427 | AttributePrefixMatchName = name; 428 | AttributePrefixMatchValue = value; 429 | } 430 | 431 | public void AttributeSuffixMatch(NamespacePrefix prefix, string name, string value) 432 | { 433 | AttributeSuffixMatchPrefix = prefix; 434 | AttributeSuffixMatchName = name; 435 | AttributeSuffixMatchValue = value; 436 | } 437 | 438 | public void AttributeSubstring(NamespacePrefix prefix, string name, string value) 439 | { 440 | AttributeSubstringPrefix = prefix; 441 | AttributeSubstringName = name; 442 | AttributeSubstringValue = value; 443 | } 444 | 445 | #region Unimplemented memebers 446 | 447 | public void OnInit() {} 448 | public void OnClose() {} 449 | public void OnSelector() {} 450 | public void Id(string id) => throw new NotImplementedException(); 451 | public void Class(string clazz) => throw new NotImplementedException(); 452 | public void FirstChild() => throw new NotImplementedException(); 453 | public void LastChild() => throw new NotImplementedException(); 454 | public void NthChild(int a, int b) => throw new NotImplementedException(); 455 | public void OnlyChild() => throw new NotImplementedException(); 456 | public void Empty() => throw new NotImplementedException(); 457 | public void Child() => throw new NotImplementedException(); 458 | public void Descendant() => throw new NotImplementedException(); 459 | public void Adjacent() => throw new NotImplementedException(); 460 | public void GeneralSibling() => throw new NotImplementedException(); 461 | public void NthLastChild(int a, int b) => throw new NotImplementedException(); 462 | 463 | #endregion 464 | } 465 | 466 | sealed class NoSelectorGenerator : INegationSelectorGenerator 467 | { 468 | public void OnInit() {} 469 | public void OnClose() {} 470 | public void OnSelector() {} 471 | public void Type(NamespacePrefix prefix, string name) {} 472 | public void Universal(NamespacePrefix prefix) {} 473 | public void Id(string id) {} 474 | public void Class(string clazz) {} 475 | public void AttributeExists(NamespacePrefix prefix, string name) {} 476 | public void AttributeExact(NamespacePrefix prefix, string name, string value) {} 477 | public void AttributeIncludes(NamespacePrefix prefix, string name, string value) {} 478 | public void AttributeDashMatch(NamespacePrefix prefix, string name, string value) {} 479 | public void AttributePrefixMatch(NamespacePrefix prefix, string name, string value) {} 480 | public void AttributeSuffixMatch(NamespacePrefix prefix, string name, string value) {} 481 | public void AttributeSubstring(NamespacePrefix prefix, string name, string value) {} 482 | public void FirstChild() {} 483 | public void LastChild() {} 484 | public void NthChild(int a, int b) {} 485 | public void OnlyChild() {} 486 | public void Empty() {} 487 | public void Child() {} 488 | public void Descendant() {} 489 | public void Adjacent() {} 490 | public void GeneralSibling() {} 491 | public void NthLastChild(int a, int b) {} 492 | public void BeginNegation() {} 493 | public void EndNegation() {} 494 | } 495 | } 496 | } 497 | --------------------------------------------------------------------------------