diff --git a/README.md b/README.md index 66601546c..be90a5f4c 100644 --- a/README.md +++ b/README.md @@ -27,5 +27,4 @@ This project has dependencies on other open-source projects. These projects are |Project|Author|Sources|License| |--------|-----|---|---------| -|Fizzler|Atif Aziz (@atifaziz)|[GitHub](https://github.com/atifaziz/Fizzler)|[LGPL](https://github.com/atifaziz/Fizzler/blob/master/COPYING.txt)| |ExCSS|Tyler Brinks (@tylerbrinks)|[GitHub](https://github.com/TylerBrinks/ExCSS)|[MIT](https://github.com/TylerBrinks/ExCSS/blob/master/license.txt)| diff --git a/Source/Css/ExCssQuery.cs b/Source/Css/ExCssQuery.cs new file mode 100644 index 000000000..5c29fada8 --- /dev/null +++ b/Source/Css/ExCssQuery.cs @@ -0,0 +1,327 @@ +using System; +using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Reflection; + using ExCSS; + +namespace Svg.Css +{ + internal static class ExCssQuery + { + public static IEnumerable QuerySelectorAll(this SvgElement elem, ISelector selector, SvgElementFactory elementFactory) + { + var input = Enumerable.Repeat(elem, 1); + var ops = new ExSvgElementOps(elementFactory); + + var func = GetFunc(selector, ops, ops.Universal()); + var descendants = ops.Descendant(); + var func1 = func; + func = f => func1(descendants(f)); + return func(input).Distinct(); + } + + private static Func, IEnumerable> GetFunc( + CompoundSelector selector, + ExSvgElementOps ops, + Func, + IEnumerable> inFunc) + { + foreach (var it in selector) + { + inFunc = GetFunc(it, ops, inFunc); + } + + return inFunc; + } + + private static Func, IEnumerable> GetFunc( + FirstChildSelector selector, + ExSvgElementOps ops) + { + var step = selector.Step; + var offset = selector.Offset; + + if (offset == 0) + { + return ops.FirstChild(); + } + + return ops.NthChild(step, offset); + } + + private static Func, IEnumerable> GetFunc( + FirstTypeSelector selector, + ExSvgElementOps ops) + { + var step = selector.Step; + var offset = selector.Offset; + + return ops.NthType(step, offset); + } + + private static Func, IEnumerable> GetFunc( + LastTypeSelector selector, + ExSvgElementOps ops) + { + var step = selector.Step; + var offset = selector.Offset; + + return ops.NthLastType(step, offset); + } + + private static Func, IEnumerable> GetFunc( + LastChildSelector selector, + ExSvgElementOps ops) + { + var step = selector.Step; + var offset = selector.Offset; + + if (offset == 0) + { + return ops.LastChild(); + } + + return ops.NthLastChild(step, offset); + } + + private static Func, IEnumerable> GetFunc( + ListSelector listSelector, + ExSvgElementOps ops, + Func, + IEnumerable> inFunc) + { + List, IEnumerable>> results = new(); + + foreach (var selector in listSelector) + { + results.Add(GetFunc(selector, ops, null)); + } + + return f => + { + var svgElements = inFunc(f); + var nodes = results[0](svgElements); + for (int i = 1; i < results.Count; i++) + { + nodes = nodes.Union(results[i](svgElements)); + } + + return nodes; + }; + } + + private static Func, IEnumerable> GetFunc( + PseudoClassSelector selector, + ExSvgElementOps ops, + Func, + IEnumerable> inFunc) + { + Func, IEnumerable> pseudoFunc; + if (selector.Class == PseudoClassNames.FirstChild) + { + pseudoFunc = ops.FirstChild(); + } + else if (selector.Class == PseudoClassNames.LastChild) + { + pseudoFunc = ops.LastChild(); + } + else if (selector.Class == PseudoClassNames.Empty) + { + pseudoFunc = ops.Empty(); + } + else if (selector.Class == PseudoClassNames.OnlyChild) + { + pseudoFunc = ops.OnlyChild(); + } + else if (selector.Class == PseudoClassNames.Hover) + { + // Currently no Hover Property exists in SvgElement so ignoring it now and returning empty + pseudoFunc = ops.Empty(); + } + else + { + if (selector.Class.StartsWith(PseudoClassNames.Not)) + { + var sel = selector.Class.Substring(PseudoClassNames.Not.Length + 1, selector.Class.Length - 2 - PseudoClassNames.Not.Length); + var parser = new StylesheetParser(true, true, tolerateInvalidValues: true); + var styleSheet = parser.Parse(sel); + var newSelector = styleSheet.StyleRules.First().Selector; + var func = GetFunc(newSelector, ops, ops.Universal()); + var descendants = ops.Descendant(); + var func1 = func; + func = f => func1(descendants(f)); + HashSet notElements = null; + + pseudoFunc = f => f.Where(e => + { + notElements ??= func(f).ToHashSet(); + return !notElements.Contains(e); + }); + } + else if (selector.Class.StartsWith(PseudoClassNames.Lang)) + { + // Currently no Language Property exists in SvgElement so ignoring it now and returning empty + pseudoFunc = ops.Empty(); + } + else if (selector.Class.StartsWith(PseudoClassNames.Root)) + { + pseudoFunc = ops.Root(); + } + else + { + throw new NotImplementedException(); + } + } + + if (inFunc == null) + { + return pseudoFunc; + } + + return f => pseudoFunc(inFunc(f)); + } + + private static Func, IEnumerable> GetFunc( + ComplexSelector selector, + ExSvgElementOps ops, + Func, + IEnumerable> inFunc) + { + List, IEnumerable>> results = new(); + + + foreach (var it in selector) + { + results.Add(GetFunc(it.Selector, ops, null)); + + Func, IEnumerable> combinatorFunc; + if (it.Delimiter == Combinator.Child.Delimiter) + { + combinatorFunc = ops.Child(); + } + else if (it.Delimiter == Combinators.Descendent) + { + combinatorFunc = ops.Descendant(); + } + else if (it.Delimiter == Combinator.Deep.Delimiter) + { + throw new NotImplementedException(); + } + else if (it.Delimiter == Combinators.Adjacent) + { + combinatorFunc = ops.Adjacent(); + } + else if (it.Delimiter == Combinators.Sibling) + { + combinatorFunc = ops.GeneralSibling(); + } + else if (it.Delimiter == Combinators.Pipe) + { + // Namespace + throw new NotImplementedException(); + } + else if (it.Delimiter == Combinators.Column) + { + throw new NotImplementedException(); + } + else if (it.Delimiter == null) + { + combinatorFunc = null; + } + else + { + throw new NotImplementedException(); + } + + if (combinatorFunc != null) + { + results.Add(combinatorFunc); + } + } + + Func, IEnumerable> result = inFunc; + foreach (var it in results) + { + if (result == null) + { + result = it; + } + else + { + var temp = result; + result = f => it(temp(f)); + } + } + + return result; + } + + private static Func, IEnumerable> GetFunc(ISelector selector, ExSvgElementOps ops, Func, IEnumerable> inFunc) + { + var func = selector switch + { + AllSelector allSelector => ops.Universal(), + AttrAvailableSelector attrAvailableSelector => ops.AttributeExists(attrAvailableSelector.Attribute), + AttrBeginsSelector attrBeginsSelector => ops.AttributePrefixMatch(attrBeginsSelector.Attribute, attrBeginsSelector.Value), + AttrContainsSelector attrContainsSelector => ops.AttributeSubstring(attrContainsSelector.Attribute, attrContainsSelector.Value), + AttrEndsSelector attrEndsSelector => ops.AttributeSuffixMatch(attrEndsSelector.Attribute, attrEndsSelector.Value), + AttrHyphenSelector attrHyphenSelector => ops.AttributeDashMatch(attrHyphenSelector.Attribute, attrHyphenSelector.Value), + AttrListSelector attrListSelector => ops.AttributeIncludes(attrListSelector.Attribute, attrListSelector.Value), + AttrMatchSelector attrMatchSelector => ops.AttributeExact(attrMatchSelector.Attribute, attrMatchSelector.Value), + AttrNotMatchSelector attrNotMatchSelector => ops.AttributeNotMatch(attrNotMatchSelector.Attribute, attrNotMatchSelector.Value), + ClassSelector classSelector => ops.Class(classSelector.Class), + ComplexSelector complexSelector => GetFunc(complexSelector, ops, inFunc), + CompoundSelector compoundSelector => GetFunc(compoundSelector, ops, inFunc), + FirstChildSelector firstChildSelector => GetFunc(firstChildSelector, ops), + LastChildSelector lastChildSelector => GetFunc(lastChildSelector, ops), + FirstColumnSelector firstColumnSelector => throw new NotImplementedException(), + LastColumnSelector lastColumnSelector => throw new NotImplementedException(), + FirstTypeSelector firstTypeSelector => GetFunc(firstTypeSelector, ops), + LastTypeSelector lastTypeSelector => GetFunc(lastTypeSelector, ops), + ChildSelector childSelector => ops.Child(), + ListSelector listSelector => GetFunc(listSelector, ops, inFunc), + NamespaceSelector namespaceSelector => throw new NotImplementedException(), + PseudoClassSelector pseudoClassSelector => GetFunc(pseudoClassSelector, ops, inFunc), + PseudoElementSelector pseudoElementSelector => throw new NotImplementedException(), + TypeSelector typeSelector => ops.Type(typeSelector.Name), + UnknownSelector unknownSelector => throw new NotImplementedException(), + IdSelector idSelector => ops.Id(idSelector.Id), + PageSelector pageSelector => throw new NotImplementedException(), + _ => throw new NotImplementedException(), + }; + + if (inFunc == null) + { + return func; + } + + return f => func(inFunc(f)); + } + +#if NETSTANDARD2_0 || NET462_OR_GREATER + private static HashSet ToHashSet(this IEnumerable enumarable) + { + var result = new HashSet(); + foreach (var it in enumarable) + { + result.Add(it); + } + + return result; + } +#endif + + public static int GetSpecificity(this ISelector selector) + { + var specificity = 0x0; + // ID selector + specificity |= (1 << 12) * selector.Specificity.Ids; + // class selector + specificity |= (1 << 8) * selector.Specificity.Classes; + // element selector + specificity |= (1 << 4) * selector.Specificity.Tags; + return specificity; + } + } +} diff --git a/Source/Css/ExSvgElementOps.cs b/Source/Css/ExSvgElementOps.cs new file mode 100644 index 000000000..a685de955 --- /dev/null +++ b/Source/Css/ExSvgElementOps.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Svg.Css +{ + internal class ExSvgElementOps : IExCssSelectorOps + { + private readonly SvgElementFactory _elementFactory; + + public ExSvgElementOps(SvgElementFactory elementFactory) + { + _elementFactory = elementFactory; + } + + public Func, IEnumerable> Type(string name) + { + if (_elementFactory.AvailableElementsDictionary.TryGetValue(name, out var types)) + { + return nodes => nodes.Where(n => types.Contains(n.GetType())); + } + return nodes => Enumerable.Empty(); + } + + public Func, IEnumerable> Universal() + { + return nodes => nodes; + } + + public Func, IEnumerable> Id(string id) + { + return nodes => nodes.Where(n => n.ID == id); + } + + public Func, IEnumerable> Class(string clazz) + { + return AttributeIncludes("class", clazz); + } + + public Func, IEnumerable> AttributeExists(string name) + { + return nodes => nodes.Where(n => n.ContainsAttribute(name)); + } + + public Func, IEnumerable> AttributeExact(string name, string value) + { + return nodes => nodes.Where(n => (n.TryGetAttribute(name, out var val) && val == value)); + } + + public Func, IEnumerable> AttributeNotMatch(string name, string value) + { + return nodes => nodes.Where(n => (n.TryGetAttribute(name, out var val) && val != value)); + } + + public Func, IEnumerable> NthType(int step, int offset) + { + return nodes => nodes.Where(n => n.Parent != null && GetByTypes(n.Parent.Children, step, offset).Contains(n)); + } + + public Func, IEnumerable> NthLastType(int step, int offset) + { + return nodes => nodes.Where(n => n.Parent != null && GetByTypes(n.Parent.Children.Reverse(), step, offset).Contains(n)); + } + + public Func, IEnumerable> AttributeIncludes(string name, string value) + { + return nodes => nodes.Where(n => (n.TryGetAttribute(name, out var val) && val.Split(' ').Contains(value))); + } + + public Func, IEnumerable> AttributeDashMatch(string name, string value) + { + return string.IsNullOrEmpty(value) + ? (Func, IEnumerable>)(nodes => Enumerable.Empty()) + : (nodes => nodes.Where(n => (n.TryGetAttribute(name, out var val) && val.Split('-').Contains(value)))); + } + + public Func, IEnumerable> AttributePrefixMatch(string name, string value) + { + return string.IsNullOrEmpty(value) + ? (Func, IEnumerable>)(nodes => Enumerable.Empty()) + : (nodes => nodes.Where(n => (n.TryGetAttribute(name, out var val) && val.StartsWith(value)))); + } + + public Func, IEnumerable> AttributeSuffixMatch(string name, string value) + { + return string.IsNullOrEmpty(value) + ? (Func, IEnumerable>)(nodes => Enumerable.Empty()) + : (nodes => nodes.Where(n => (n.TryGetAttribute(name, out var val) && val.EndsWith(value)))); + } + + public Func, IEnumerable> AttributeSubstring(string name, string value) + { + return string.IsNullOrEmpty(value) + ? (Func, IEnumerable>)(nodes => Enumerable.Empty()) + : (nodes => nodes.Where(n => (n.TryGetAttribute(name, out var val) && val.Contains(value)))); + } + + public Func, IEnumerable> FirstChild() + { + return nodes => nodes.Where(n => n.Parent == null || n.Parent.Children.First() == n); + } + + public Func, IEnumerable> LastChild() + { + return nodes => nodes.Where(n => n.Parent == null || n.Parent.Children.Last() == n); + } + + private IEnumerable GetByIds(IList items, IEnumerable indices) + { + foreach (var i in indices) + { + if (i >= 0 && i < items.Count) yield return items[i]; + } + } + + private IEnumerable GetByTypes(IEnumerable items, int step, int offset) + { + Dictionary counter = new(); + + foreach (var it in items) + { + var type = it.ElementName; + counter.TryGetValue(type, out var count); + + if (offset == count) + { + yield return it; + } + else if (offset > count) + { + if (step != 0) + { + if ((count - offset) % step == 0) + { + yield return it; + } + } + } + + count++; + counter[type] = count; + } + } + + private IEnumerable GetByIdsReverse(IList items, IEnumerable indices) + { + foreach (var i in indices) + { + if (i >= 0 && i < items.Count) yield return items[items.Count - 1 - i]; + } + } + + public Func, IEnumerable> NthChild(int step, int offset) + { + return nodes => nodes.Where(n => n.Parent != null && GetByIds(n.Parent.Children, step == 0 ? new[]{offset} : (from i in Enumerable.Range(0, n.Parent.Children.Count / step) select step * i + offset)).Contains(n)); + } + + public Func, IEnumerable> OnlyChild() + { + return nodes => nodes.Where(n => n.Parent == null || n.Parent.Children.Count == 1); + } + + public Func, IEnumerable> Empty() + { + return nodes => nodes.Where(n => n.Children.Count == 0); + } + + public Func, IEnumerable> Child() + { + return nodes => nodes.SelectMany(n => n.Children); + } + + public Func, IEnumerable> Descendant() + { + return nodes => nodes.SelectMany(Descendants); + } + + private IEnumerable Descendants(SvgElement elem) + { + foreach (var child in elem.Children) + { + yield return child; + foreach (var descendant in child.Descendants()) + { + yield return descendant; + } + } + } + + public Func, IEnumerable> Adjacent() + { + return nodes => nodes.SelectMany(n => ElementsAfterSelf(n).Take(1)); + } + + public Func, IEnumerable> GeneralSibling() + { + return nodes => nodes.SelectMany(ElementsAfterSelf); + } + + private IEnumerable ElementsAfterSelf(SvgElement self) + { + return (self.Parent == null ? Enumerable.Empty() : self.Parent.Children.Skip(self.Parent.Children.IndexOf(self) + 1)); + } + + public Func, IEnumerable> NthLastChild(int step, int offset) + { + return nodes => nodes.Where(n => n.Parent != null && GetByIdsReverse(n.Parent.Children, step == 0 ? new[]{offset} : (from i in Enumerable.Range(0, n.Parent.Children.Count / step) select step * i + offset)).Contains(n)); + } + + public Func, IEnumerable> Root() + { + return nodes => + { + var node = nodes.FirstOrDefault(); + if (node == null) + { + return Enumerable.Empty(); + } + + var root = node; + while (root.Parent != null) + { + root = root.Parent; + } + + return new List { root }; + }; + } + } +} diff --git a/Source/Css/IExCssSelectorOps.cs b/Source/Css/IExCssSelectorOps.cs new file mode 100644 index 000000000..36bcf3002 --- /dev/null +++ b/Source/Css/IExCssSelectorOps.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace Svg.Css; + +internal interface IExCssSelectorOps +{ + Func, IEnumerable> Type(string name); + Func, IEnumerable> Universal(); + Func, IEnumerable> Id(string id); + Func, IEnumerable> Class(string clazz); + Func, IEnumerable> AttributeExists(string name); + Func, IEnumerable> AttributeExact(string name, string value); + Func, IEnumerable> AttributeIncludes(string name, string value); + Func, IEnumerable> AttributeDashMatch(string name, string value); + Func, IEnumerable> AttributePrefixMatch(string name, string value); + Func, IEnumerable> AttributeSuffixMatch(string name, string value); + Func, IEnumerable> AttributeSubstring(string name, string value); + Func, IEnumerable> FirstChild(); + Func, IEnumerable> LastChild(); + Func, IEnumerable> NthChild(int step, int offset); + Func, IEnumerable> OnlyChild(); + Func, IEnumerable> Empty(); + Func, IEnumerable> Child(); + Func, IEnumerable> Descendant(); + Func, IEnumerable> Adjacent(); + Func, IEnumerable> GeneralSibling(); + Func, IEnumerable> NthLastChild(int step, int offset); + Func, IEnumerable> AttributeNotMatch(string name, string value); + Func, IEnumerable> NthType(int step, int offset); + Func, IEnumerable> NthLastType(int step, int offset); +} diff --git a/Source/Painting/SvgMarker.Drawing.cs b/Source/Painting/SvgMarker.Drawing.cs index 12dfe52c0..3807418c3 100644 --- a/Source/Painting/SvgMarker.Drawing.cs +++ b/Source/Painting/SvgMarker.Drawing.cs @@ -1,4 +1,4 @@ -#if !NO_SDC +#if !NO_SDC using System; using System.Drawing; using System.Drawing.Drawing2D; diff --git a/Source/Painting/SvgMarker.cs b/Source/Painting/SvgMarker.cs index d4cda97ed..4b7e7e8c3 100644 --- a/Source/Painting/SvgMarker.cs +++ b/Source/Painting/SvgMarker.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Svg.DataTypes; namespace Svg diff --git a/Source/Properties/AssemblyInfo.cs b/Source/Properties/AssemblyInfo.cs index b8feca81a..451e79446 100644 --- a/Source/Properties/AssemblyInfo.cs +++ b/Source/Properties/AssemblyInfo.cs @@ -29,4 +29,11 @@ "e667607d1fca2c0f0cdcc1c1b926ae46669128282ecad43e6d0776497cd8289dca11e4479773d5" + "45fc4c557686de548aadbb8652fa550e21d4c402885fec4c1deebfa79e861adb966fc8f4e78235" + "79a535280ddd3a0168cb4d19522c7591b6693377058675da70e50c7bd6fdceae055cef085f02a0" + -"5a7f0cb4")] \ No newline at end of file +"5a7f0cb4")] + +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Svg.Benchmark,PublicKey=" + + "00240000048000009400000006020000002400005253413100040000010001008d4e723a8c76be" + + "e667607d1fca2c0f0cdcc1c1b926ae46669128282ecad43e6d0776497cd8289dca11e4479773d5" + + "45fc4c557686de548aadbb8652fa550e21d4c402885fec4c1deebfa79e861adb966fc8f4e78235" + + "79a535280ddd3a0168cb4d19522c7591b6693377058675da70e50c7bd6fdceae055cef085f02a0" + + "5a7f0cb4")] diff --git a/Source/Svg.csproj b/Source/Svg.csproj index a4c0f00c5..9f3fdaac7 100644 --- a/Source/Svg.csproj +++ b/Source/Svg.csproj @@ -87,8 +87,7 @@ - - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Source/Svg.sln.DotSettings b/Source/Svg.sln.DotSettings new file mode 100644 index 000000000..1874a930f --- /dev/null +++ b/Source/Svg.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/Source/SvgDocument.cs b/Source/SvgDocument.cs index 8e02d5829..77d2b8001 100644 --- a/Source/SvgDocument.cs +++ b/Source/SvgDocument.cs @@ -343,8 +343,52 @@ public static SvgDocument Open(string path) } } + private static T Create(XmlReader reader) where T : SvgDocument, new() { + var styles = new List(); + var elementFactory = new SvgElementFactory(); + + var svgDocument = Create(reader, elementFactory, styles); + + if (styles.Any()) + { + var cssTotal = string.Join(Environment.NewLine, styles.Select(s => s.Content).ToArray()); + var stylesheetParser = new StylesheetParser(true, true, tolerateInvalidValues: true); + var stylesheet = stylesheetParser.Parse(cssTotal); + + foreach (var rule in stylesheet.StyleRules) + try + { + var rootNode = new NonSvgElement(); + rootNode.Children.Add(svgDocument); + + var elemsToStyle = rootNode.QuerySelectorAll(rule.Selector, elementFactory); + foreach (var elem in elemsToStyle) + foreach (var declaration in rule.Style) + { + elem.AddStyle(declaration.Name, declaration.Original, rule.Selector.GetSpecificity()); + } + } + catch (Exception ex) + { + Trace.TraceWarning(ex.Message); + } + } + + svgDocument?.FlushStyles(true); + return svgDocument; + } + + /// Open Svg Document without applying Stylesheets. + /// SvgDocument to create + /// reader + /// elementFactory + /// read svg StyleSheets + /// Created Svg Document + internal static T Create(XmlReader reader, SvgElementFactory elementFactory, List styles) + where T : SvgDocument, new() + { #if !NO_SDC if (!SkipGdiPlusCapabilityCheck) { @@ -356,9 +400,6 @@ public static SvgDocument Open(string path) SvgElement element = null; SvgElement parent; T svgDocument = null; - var elementFactory = new SvgElementFactory(); - - var styles = new List(); while (reader.Read()) { @@ -441,32 +482,6 @@ public static SvgDocument Open(string path) } } - if (styles.Any()) - { - var cssTotal = string.Join(Environment.NewLine, styles.Select(s => s.Content).ToArray()); - var stylesheetParser = new StylesheetParser(true, true, tolerateInvalidValues: true); - var stylesheet = stylesheetParser.Parse(cssTotal); - - foreach (var rule in stylesheet.StyleRules) - try - { - var rootNode = new NonSvgElement(); - rootNode.Children.Add(svgDocument); - - var elemsToStyle = rootNode.QuerySelectorAll(rule.Selector.Text, elementFactory); - foreach (var elem in elemsToStyle) - foreach (var declaration in rule.Style) - { - elem.AddStyle(declaration.Name, declaration.Original, rule.Selector.GetSpecificity()); - } - } - catch (Exception ex) - { - Trace.TraceWarning(ex.Message); - } - } - - svgDocument?.FlushStyles(true); return svgDocument; } diff --git a/Source/SvgElementCollection.cs b/Source/SvgElementCollection.cs index 0660a3739..6bcd9e8f5 100644 --- a/Source/SvgElementCollection.cs +++ b/Source/SvgElementCollection.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -205,4 +205,4 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() return this._elements.GetEnumerator(); } } -} \ No newline at end of file +} diff --git a/Svg.Custom/Svg.Custom.csproj b/Svg.Custom/Svg.Custom.csproj index 3b5c4e9e0..20dd84f1e 100644 --- a/Svg.Custom/Svg.Custom.csproj +++ b/Svg.Custom/Svg.Custom.csproj @@ -55,8 +55,7 @@ - - + diff --git a/Tests/Svg.Benchmark/CssQueryPerformanceBenchmark.cs b/Tests/Svg.Benchmark/CssQueryPerformanceBenchmark.cs new file mode 100644 index 000000000..f75687108 --- /dev/null +++ b/Tests/Svg.Benchmark/CssQueryPerformanceBenchmark.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using BenchmarkDotNet.Attributes; +using ExCSS; +using Svg.Css; + +namespace Svg.Benchmark +{ + public class CssQueryPerformanceBenchmark + { + private readonly List _styles; + private readonly SvgDocument _svgDokument; + private readonly List _rules; + private readonly SvgElementFactory _svgElementFactory; + + private Stream Open(string name) => typeof(Program).Assembly.GetManifestResourceStream($"Svg.Benchmark.Assets.{name}"); + + public CssQueryPerformanceBenchmark() + { + using var stream = Open("struct-use-11-f.svg"); + _styles = new List(); + _svgElementFactory = new SvgElementFactory(); + using (var xmlTextReader = new XmlTextReader(stream)) + { + _svgDokument = SvgDocument.Create(xmlTextReader, _svgElementFactory, _styles); + } + + var cssTotal = string.Join(Environment.NewLine, _styles.Select(s => s.Content).ToArray()); + var stylesheetParser = new StylesheetParser(true, true, tolerateInvalidValues: true); + var stylesheet = stylesheetParser.Parse(cssTotal); + _rules = stylesheet.StyleRules.ToList(); + } + + [Benchmark] + public void SelectorPerformanceExCss() + { + var rootNode = new NonSvgElement(); + rootNode.Children.Add(_svgDokument); + + foreach (var rule in _rules) + { + rootNode.QuerySelectorAll(rule.Selector, _svgElementFactory); + } + } + + [Benchmark] + public void SelectorPerformanceFizzler() + { + var rootNode = new NonSvgElement(); + rootNode.Children.Add(_svgDokument); + + foreach (var rule in _rules) + { + rootNode.QuerySelectorAll(rule.Selector.Text, _svgElementFactory); + } + } + } +} diff --git a/Tests/Svg.Benchmark/Svg.Benchmark.csproj b/Tests/Svg.Benchmark/Svg.Benchmark.csproj index cc80b0d46..3929d8b33 100644 --- a/Tests/Svg.Benchmark/Svg.Benchmark.csproj +++ b/Tests/Svg.Benchmark/Svg.Benchmark.csproj @@ -4,6 +4,8 @@ net6.0;netcoreapp3.1;net462 false 8.0 + True + ..\Svg.UnitTests\svgkey.snk @@ -29,6 +31,7 @@ + diff --git a/Source/Css/CssQuery.cs b/Tests/Svg.UnitTests/Css/CssQuery.cs similarity index 100% rename from Source/Css/CssQuery.cs rename to Tests/Svg.UnitTests/Css/CssQuery.cs diff --git a/Source/Css/SvgElementOps.cs b/Tests/Svg.UnitTests/Css/SvgElementOps.cs similarity index 93% rename from Source/Css/SvgElementOps.cs rename to Tests/Svg.UnitTests/Css/SvgElementOps.cs index 87cdfdf00..b3902e9f6 100644 --- a/Source/Css/SvgElementOps.cs +++ b/Tests/Svg.UnitTests/Css/SvgElementOps.cs @@ -123,6 +123,14 @@ private IEnumerable GetByIds(IList items, IEnumerable indices) } } + private IEnumerable GetByIdsReverse(IList items, IEnumerable indices) + { + foreach (var i in indices) + { + if (i >= 0 && i < items.Count) yield return items[items.Count - 1 -i]; + } + } + public Selector NthChild(int a, int b) { return nodes => nodes.Where(n => n.Parent != null && GetByIds(n.Parent.Children, (from i in Enumerable.Range(0, n.Parent.Children.Count / a) select a * i + b)).Contains(n)); @@ -177,7 +185,7 @@ private IEnumerable ElementsAfterSelf(SvgElement self) public Selector NthLastChild(int a, int b) { - throw new NotImplementedException(); + return nodes => nodes.Where(n => n.Parent != null && GetByIdsReverse(n.Parent.Children, (from i in Enumerable.Range(0, n.Parent.Children.Count / a) select a * i + b)).Contains(n)); } } } diff --git a/Tests/Svg.UnitTests/ExCssSelectorTests.cs b/Tests/Svg.UnitTests/ExCssSelectorTests.cs new file mode 100644 index 000000000..72dfba5a0 --- /dev/null +++ b/Tests/Svg.UnitTests/ExCssSelectorTests.cs @@ -0,0 +1,376 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Xml; +using ExCSS; +using Fizzler; +using Svg.Css; + +namespace Svg.UnitTests +{ + [TestFixture] + public class ExCssSelectorTests + { + [Test] + [TestCase("struct-use-11-f")] + [TestCase("struct-use-10-f")] + [TestCase("styling-css-03-b")] + [TestCase("__issue-280-01")] + [TestCase("styling-css-01-b")] + [TestCase("__issue-034-02")] + public void RunAllSelectorTests(string baseName) + { + RunAllSelectorTests(baseName, null); + } + + public void RunAllSelectorTests(string baseName, string folder) + { + var elementFactory = new SvgElementFactory(); + var testSuite = Path.Combine(ImageTestDataSource.SuiteTestsFolder, "W3CTestSuite"); + string basePath = folder ?? Path.Combine(testSuite, "svg"); + var svgPath = Path.Combine(basePath, baseName + ".svg"); + + var styles = new List(); + using (var xmlFragment = File.Open(svgPath, FileMode.Open)) + { + using (var xmlTextReader = new XmlTextReader(xmlFragment)) + { + var svgDocument = SvgDocument.Create(xmlTextReader, elementFactory, styles); + + var rootNode = new NonSvgElement(); + rootNode.Children.Add(svgDocument); + + if (styles.Any()) + { + var cssTotal = string.Join(Environment.NewLine, styles.Select(s => s.Content).ToArray()); + var stylesheetParser = new StylesheetParser(true, true); + var stylesheet = stylesheetParser.Parse(cssTotal); + + foreach (var selector in stylesheet.StyleRules) + { + TestSelector(selector.Selector.Text, rootNode, elementFactory); + } + } + } + } + } + + [Test] + [TestCaseSource(typeof(TestSvg), nameof(TestSvg.AllSvgs))] + [TestCaseSource(typeof(TestSvg), nameof(TestSvg.AllImageSvgs))] + public void RunSelectorsOnSvgTests(TestSvg svg) + { + RunAllSelectorTests(svg.BaseName, svg.Folder); + } + + [Test] + [TestCase("#testId.test1", "struct-use-11-f")] + [TestCase("*.test2", "struct-use-11-f")] + [TestCase("circle.test3", "struct-use-11-f")] + [TestCase(".descendant circle.test4", "struct-use-11-f")] + [TestCase(".child", "struct-use-11-f")] + [TestCase("circle.test5", "struct-use-11-f")] + [TestCase(".child > circle.test5", "struct-use-11-f")] + [TestCase(".test6:first-child", "struct-use-11-f")] + [TestCase(".sibling + circle.test7", "struct-use-11-f")] + [TestCase("circle[cx].test8", "struct-use-11-f")] + [TestCase("circle[cx=\"50\"].test9", "struct-use-11-f")] + [TestCase("circle[foo~=\"warning1\"].test10", "struct-use-11-f")] + [TestCase("circle[lang|=\"en\"].test11", "struct-use-11-f")] + [TestCase(".test12", "struct-use-11-f")] + [TestCase(".twochildren:first-child", "struct-use-11-f")] + [TestCase("defs > rect", "struct-use-10-f")] + [TestCase(".testclass1", "struct-use-10-f")] + [TestCase("#testid1 .testclass1", "struct-use-10-f")] + [TestCase("g .testClass1", "struct-use-10-f")] + [TestCase("#g1 .testclass2", "struct-use-10-f")] + [TestCase("g#g1", "struct-use-10-f")] + [TestCase("#testid2", "struct-use-10-f")] + [TestCase("g#g2", "struct-use-10-f")] + [TestCase(".testclass3 > rect", "struct-use-10-f")] + [TestCase("#testid3 rect", "struct-use-10-f")] + [TestCase("#testid3 rect#testrect3", "struct-use-10-f")] + [TestCase(".mummy", "styling-css-03-b")] + [TestCase(".mummy rect", "styling-css-03-b")] + [TestCase(".mummy > .thischild", "styling-css-03-b")] + [TestCase(".child", "styling-css-03-b")] + [TestCase(".gap > .thischild", "styling-css-03-b")] + [TestCase(".daddy", "styling-css-03-b")] + [TestCase(".daddy > .tertius", "styling-css-03-b")] + [TestCase(".primus + .secundus", "styling-css-03-b")] + [TestCase(".daddy :first-child", "styling-css-03-b")] + [TestCase(".st1", "__issue-280-01")] + [TestCase(".st2", "__issue-280-01")] + [TestCase(".st3", "__issue-280-01")] + [TestCase(".st4", "__issue-280-01")] + [TestCase(".st5", "__issue-280-01")] + [TestCase(".st6", "__issue-280-01")] + [TestCase(".st7", "__issue-280-01")] + [TestCase(".st8", "__issue-280-01")] + [TestCase(".st9", "__issue-280-01")] + [TestCase("rect","styling-css-01-b")] + [TestCase(".warning", "styling-css-01-b")] + [TestCase(".bar","styling-css-01-b")] + [TestCase(".line","__issue-034-02")] + [TestCase(".bold-line","__issue-034-02")] + [TestCase(".thin-line","__issue-034-02")] + [TestCase(".filled","__issue-034-02")] + [TestCase("text.terminal","__issue-034-02")] + [TestCase("text.nonterminal","__issue-034-02")] + [TestCase("text.regexp","__issue-034-02")] + [TestCase("rect, circle, polygon","__issue-034-02")] + [TestCase("rect.terminal","__issue-034-02")] + [TestCase("rect.nonterminal","__issue-034-02")] + [TestCase("rect.text","__issue-034-02")] + [TestCase("polygon.regexp","__issue-034-02")] + [TestCase("path:hover","__issue-315-01")] + [TestCase("path","__issue-315-01")] + [TestCase("text","styling-css-04-f")] + [TestCase("rect","styling-css-04-f")] + [TestCase("#test-frame","styling-css-04-f")] + [TestCase("g#alpha","styling-css-04-f")] + [TestCase("a#alpha","styling-css-04-f")] + [TestCase("#alpha","styling-css-04-f")] + [TestCase("#alpha-2 > rect","styling-css-04-f")] + [TestCase("#beta rect","styling-css-04-f")] + [TestCase("g#gamma * g * * rect","styling-css-04-f")] + [TestCase("g#gamma * * rect","styling-css-04-f")] + [TestCase("[stroke-width=\"1.0001\"]","styling-css-04-f")] + [TestCase("g#delta rect[stroke-width=\"1.0002\"]","styling-css-04-f")] + [TestCase("g#delta > rect[stroke-width=\"1.0003\"]","styling-css-04-f")] + [TestCase("#delta + g > *","styling-css-04-f")] + [TestCase("g#delta + g > rect + rect","styling-css-04-f")] + [TestCase("#delta + g#epsilon * rect:first-child","styling-css-04-f")] + [TestCase("#zeta [cursor]","styling-css-04-f")] + [TestCase("g#zeta [cursor=\"help\"]","styling-css-04-f")] + [TestCase("g#zeta [rx~=\"3E\"]","styling-css-04-f")] + [TestCase("g#epsilon + g [stroke-dasharray|=\"3.1415926\"]","styling-css-04-f")] + [TestCase("g#epsilon + g > rect.hello","styling-css-04-f")] + [TestCase("g#eta rect:first-child","styling-css-04-f")] + public void RunSelectorTests(string selector, string baseName) + { + var elementFactory = new SvgElementFactory(); + var testSuite = Path.Combine(ImageTestDataSource.SuiteTestsFolder, "W3CTestSuite"); + string basePath = testSuite; + var svgPath = Path.Combine(basePath, "svg", baseName + ".svg"); + var styles = new List(); + using (var xmlFragment = File.Open(svgPath, FileMode.Open)) + { + using (var xmlTextReader = new XmlTextReader(xmlFragment)) + { + var svgDocument = SvgDocument.Create(xmlTextReader, elementFactory, styles); + + var rootNode = new NonSvgElement(); + rootNode.Children.Add(svgDocument); + + TestSelector(selector, rootNode, elementFactory); + } + } + } + + [Test] + [TestCase("*:first-child")] + [TestCase("p:first-child")] + [TestCase("*:last-child")] + [TestCase("p:last-child")] + [TestCase("*:only-child")] + [TestCase("p:only-child")] + [TestCase("*:empty")] + [TestCase(":nth-child(2)")] + [TestCase("*:nth-child(2)")] + [TestCase("p:nth-child(2)")] + [TestCase(":nth-last-child(2)")] + [TestCase("#myDiv :nth-last-child(2)")] + [TestCase("span:nth-last-child(3)")] + [TestCase("span:nth-last-child(2)")] + [TestCase("p.hiclass,a")] + [TestCase("p.hiclass, a")] + [TestCase("p.hiclass , a")] + [TestCase("p.hiclass ,a")] + [TestCase("#myDiv")] + [TestCase("div#myDiv")] + [TestCase("#theBody #myDiv")] + [TestCase("#theBody #whatwhatwhat")] + [TestCase("#whatwhatwhat #someOtherDiv")] + [TestCase("#myDiv *")] + [TestCase("div#myDiv")] + [TestCase("#theBody #myDiv")] + [TestCase("#theBody #whatwhatwhat")] + [TestCase("#whatwhatwhat #someOtherDiv")] + [TestCase("#myDiv *")] + [TestCase("#theBody>#myDiv")] + [TestCase("#theBody>#someOtherDiv")] + [TestCase("#myDiv>*")] + [TestCase("#someOtherDiv>*")] + [TestCase("*")] + [TestCase("body")] + [TestCase("p")] + [TestCase("head p")] + [TestCase("div p")] + [TestCase("div a")] + [TestCase("div p a")] + [TestCase("div p a")] + [TestCase("div div")] + [TestCase("form input")] + [TestCase(".checkit")] + [TestCase(".omg.ohyeah")] + [TestCase("p.ohyeah")] + [TestCase("p.ohyeah")] + [TestCase("div .ohyeah")] + [TestCase("div > p")] + [TestCase("div> p")] + [TestCase("div >p")] + [TestCase("div>p")] + [TestCase("div > p.ohyeah")] + [TestCase("p > *")] + [TestCase("div > * > *")] + [TestCase("a + span")] + [TestCase("a+ span")] + [TestCase("a +span")] + [TestCase("a+span")] + [TestCase("a + span, div > p")] + [TestCase("div ~ form")] + [TestCase("div[id]")] + [TestCase("div[id=\"someOtherDiv\"]")] + [TestCase("p[class~=\"ohyeah\"]")] + [TestCase("p[class~='']")] + [TestCase("span[class|=\"separated\"]")] + [TestCase("[class=\"checkit\"]")] + [TestCase("*[class=\"checkit\"]")] + [TestCase("*[class=\"checkit\"]")] + [TestCase("*[class^=check]")] + [TestCase("*[class^='']")] + [TestCase("*[class$=it]")] + [TestCase("*[class$='']")] + [TestCase("*[class*=heck]")] + [TestCase("*[class*='']")] + [TestCase("p[class!='hiclass']")] + [TestCase(":nth-of-type(2)")] + [TestCase(":nth-last-of-type(2)")] + [TestCase(":not(p)")] + [TestCase(":root")] + public void RunSelectorTests(string selector) + { + string baseName = "struct-use-11-f"; + var elementFactory = new SvgElementFactory(); + var testSuite = Path.Combine(ImageTestDataSource.SuiteTestsFolder, "W3CTestSuite"); + string basePath = testSuite; + var svgPath = Path.Combine(basePath, "svg", baseName + ".svg"); + var styles = new List(); + using (var xmlFragment = File.Open(svgPath, FileMode.Open)) + { + using (var xmlTextReader = new XmlTextReader(xmlFragment)) + { + var svgDocument = SvgDocument.Create(xmlTextReader, elementFactory, styles); + + var rootNode = new NonSvgElement(); + rootNode.Children.Add(svgDocument); + + TestSelector(selector, rootNode, elementFactory); + } + } + } + + private void TestSelector(string selector, NonSvgElement rootNode, SvgElementFactory elementFactory) + { + ////SvgElementOps.NodeDebug = SvgElementOps.NodeDebug = string.Empty; // nameof(SvgElementOpsFunc.Child); + bool fizzler = !(selector.Contains("nth-child") || selector.Contains("nth-last-child")); + + List fizzlerElements = null; + Debug.WriteLine(Environment.NewLine); + try + { + Debug.WriteLine("Fizzler:\r\n"); + fizzlerElements = QuerySelectorFizzlerAll(rootNode, selector, elementFactory).OrderBy(f => f.ElementName).ToList(); + Debug.WriteLine(Environment.NewLine); + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); + fizzler = false; + } + + Debug.WriteLine("ExCss:\r\n"); + var exCssElements = QuerySelectorExCssAll(rootNode, selector, elementFactory).OrderBy(f => f.ElementName).ToList(); + Debug.WriteLine(Environment.NewLine); + + if (fizzler) + { + var areEqualFizzler = fizzlerElements.SequenceEqual(exCssElements); + Assert.IsTrue(areEqualFizzler, "should select the same elements"); + } + else + { + Assert.Inconclusive("Fizzler can't handle this selector"); + } + } + + private IEnumerable QuerySelectorExCssAll(NonSvgElement elem, string selector, SvgElementFactory elementFactory) + { + var stylesheetParser = new StylesheetParser(true, true); + var stylesheet = stylesheetParser.Parse(selector + " {color:black}"); + var exCssSelector = stylesheet.StyleRules.First().Selector; + return elem.QuerySelectorAll(exCssSelector, elementFactory); + } + + private IEnumerable QuerySelectorFizzlerAll(NonSvgElement elem, string selector, SvgElementFactory elementFactory) + { + var generator = new SelectorGenerator(new SvgElementOps(elementFactory)); + Fizzler.Parser.Parse(selector, generator); + return generator.Selector(Enumerable.Repeat(elem, 1)); + } + } + + public class TestSvg + { + private readonly string _baseName; + private readonly string _folder; + + private TestSvg(string baseName, string folder) + { + _baseName = baseName; + _folder = folder; + } + + public override string ToString() + { + return $"TestSvg - {BaseName}"; + } + + public string BaseName => _baseName; + public string Folder => _folder; + + public static IEnumerable AllSvgs() + { + var basePath = ImageTestDataSource.SuiteTestsFolder; + var testSuite = Path.Combine(basePath, "W3CTestSuite", "svg"); + // Enumerate all Test Svgs + foreach(var baseName in Directory.EnumerateFiles(testSuite)) + { + if (Path.GetExtension(baseName) == ".svg") + { + yield return new TestSvg(Path.GetFileNameWithoutExtension(baseName), testSuite); + } + } + } + + public static IEnumerable AllImageSvgs() + { + var basePath = ImageTestDataSource.SuiteTestsFolder; + var testSuite = Path.Combine(basePath, "W3CTestSuite", "images"); + // Enumerate all Test Svgs + foreach(var baseName in Directory.EnumerateFiles(testSuite)) + { + if (Path.GetExtension(baseName) == ".svg") + { + yield return new TestSvg( + Path.GetFileNameWithoutExtension(baseName), + testSuite); + } + } + } + } +} diff --git a/Tests/Svg.UnitTests/Properties/AssemblyInfo.cs b/Tests/Svg.UnitTests/Properties/AssemblyInfo.cs index c89bd3002..df90ebd6d 100644 --- a/Tests/Svg.UnitTests/Properties/AssemblyInfo.cs +++ b/Tests/Svg.UnitTests/Properties/AssemblyInfo.cs @@ -32,3 +32,10 @@ // by using the '*' as shown below: [assembly: AssemblyVersion("2.4.5.0")] [assembly: AssemblyFileVersion("2.4.5.0")] + +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Svg.Benchmark,PublicKey=" + + "00240000048000009400000006020000002400005253413100040000010001008d4e723a8c76be" + + "e667607d1fca2c0f0cdcc1c1b926ae46669128282ecad43e6d0776497cd8289dca11e4479773d5" + + "45fc4c557686de548aadbb8652fa550e21d4c402885fec4c1deebfa79e861adb966fc8f4e78235" + + "79a535280ddd3a0168cb4d19522c7591b6693377058675da70e50c7bd6fdceae055cef085f02a0" + + "5a7f0cb4")] diff --git a/Tests/Svg.UnitTests/Svg.UnitTests.csproj b/Tests/Svg.UnitTests/Svg.UnitTests.csproj index a93f4d3eb..37801f65d 100644 --- a/Tests/Svg.UnitTests/Svg.UnitTests.csproj +++ b/Tests/Svg.UnitTests/Svg.UnitTests.csproj @@ -36,11 +36,12 @@ - + +