diff --git a/src/ExCSS.Tests/Container.cs b/src/ExCSS.Tests/Container.cs new file mode 100644 index 00000000..76d8e65b --- /dev/null +++ b/src/ExCSS.Tests/Container.cs @@ -0,0 +1,103 @@ +namespace ExCSS.Tests +{ + using ExCSS; + using Xunit; + using System.Linq; + + public class CssContainerTests : CssConstructionFunctions + { + [Fact] + public void SimpleContainer() + { + const string source = "@container tall (min-width: 500px) and (min-height: 300px) {h2 { line-height: 1.6; } }"; + var result = ParseStyleSheet(source); + Assert.Equal(source, result.StylesheetText.Text); + var rule = result.Rules[0] as ContainerRule; + Assert.NotNull(rule); + Assert.Equal("@container tall (min-width: 500px) and (min-height: 300px) { h2 { line-height: 1.6 } }", rule.Text); + Assert.Equal("tall", rule.Name); + Assert.Equal("(min-width: 500px) and (min-height: 300px)", rule.ConditionText); + var childRule = rule.Children.OfType().First(); + Assert.Equal("h2 { line-height: 1.6 }", childRule.ToCss()); + } + + [Fact] + public void ContainerWithoutName() + { + const string source = "@container (min-width: 500px) and (min-height: 300px) {h2 { line-height: 1.6; } }"; + var result = ParseStyleSheet(source); + Assert.Equal(source, result.StylesheetText.Text); + var rule = result.Rules[0] as ContainerRule; + Assert.NotNull(rule); + Assert.Equal("@container (min-width: 500px) and (min-height: 300px) { h2 { line-height: 1.6 } }", rule.Text); + Assert.Equal(string.Empty, rule.Name); + Assert.Equal("(min-width: 500px) and (min-height: 300px)", rule.ConditionText); + var childRule = rule.Children.OfType().First(); + Assert.Equal("h2 { line-height: 1.6 }", childRule.ToCss()); + } + + [Fact] + public void ContainerWithoutCondition() + { + const string source = "@container tall {h2 { line-height: 1.6; } }"; + var result = ParseStyleSheet(source); + Assert.Equal(source, result.StylesheetText.Text); + var rule = result.Rules[0] as ContainerRule; + Assert.NotNull(rule); + Assert.Equal("@container tall { h2 { line-height: 1.6 } }", rule.Text); + Assert.Equal("tall", rule.Name); + Assert.Equal(string.Empty, rule.ConditionText); + var childRule = rule.Children.OfType().First(); + Assert.Equal("h2 { line-height: 1.6 }", childRule.ToCss()); + } + + [Fact] + public void ContainerWithComparisonOperators() + { + const string source = "@container tall (width < 500px) and (height >= 300px) {h2 { line-height: 1.6; } }"; + var result = ParseStyleSheet(source); + Assert.Equal(source, result.StylesheetText.Text); + var rule = result.Rules[0] as ContainerRule; + Assert.NotNull(rule); + Assert.Equal("@container tall (width < 500px) and (height >= 300px) { h2 { line-height: 1.6 } }", rule.Text); + Assert.Equal("tall", rule.Name); + Assert.Equal("(width < 500px) and (height >= 300px)", rule.ConditionText); + var childRule = rule.Children.OfType().First(); + Assert.Equal("h2 { line-height: 1.6 }", childRule.ToCss()); + } + + [Fact] + public void CSSWithTwoContainers() + { + const string source = @"li { + container-type: inline-size; +} + +@container (min-width: 45ch) { + li span { + color: rgb(255, 0, 0); + font-size: 2rem !important; + } +} + +@container (min-width: 70ch) { + li span { + color: rgb(0, 0, 255); + font-size: 3rem !important; + } +}"; + var result = ParseStyleSheet(source); + Assert.Equal(source, result.StylesheetText.Text); + Assert.Equal(3, result.Rules.Length); + var rule1 = result.Rules[0] as StyleRule; + var rule2 = result.Rules[1] as ContainerRule; + var rule3 = result.Rules[2] as ContainerRule; + Assert.NotNull(rule1); + Assert.NotNull(rule2); + Assert.NotNull(rule3); + Assert.Equal("li { container-type: inline-size }", rule1.ToCss()); + Assert.Equal("@container (min-width: 45ch) { li span { color: rgb(255, 0, 0); font-size: 2rem !important } }", rule2.ToCss()); + Assert.Equal("@container (min-width: 70ch) { li span { color: rgb(0, 0, 255); font-size: 3rem !important } }", rule3.ToCss()); + } + } +} diff --git a/src/ExCSS.Tests/MediaList.cs b/src/ExCSS.Tests/MediaList.cs index 26746fd0..06b03c6b 100644 --- a/src/ExCSS.Tests/MediaList.cs +++ b/src/ExCSS.Tests/MediaList.cs @@ -3,7 +3,7 @@ using ExCSS; using Xunit; using System; - + public class CssMediaListTests : CssConstructionFunctions { [Fact] @@ -375,21 +375,48 @@ public void ImplicitAllFeatureMinResolutionAndMaxResolutionMediaList() [Fact] public void CssMediaListApiWithAppendDeleteAndTextShouldWork() { - var media = new [] { "handheld", "screen", "only screen and (max-device-width: 480px)" }; + var media = new[] { "handheld", "screen", "only screen and (max-device-width: 480px)" }; var p = new StylesheetParser(); - var m = new MediaList(p); + var m = new MediaList(p); Assert.Equal(0, m.Length); - m.Add(media[0]); - m.Add(media[1]); - m.Add(media[2]); + m.Add(media[0]); + m.Add(media[1]); + m.Add(media[2]); - m.Remove(media[1]); + m.Remove(media[1]); Assert.Equal(2, m.Length); Assert.Equal(media[0], m[0]); Assert.Equal(media[2], m[1]); Assert.Equal(String.Concat(media[0], ", ", media[2]), m.MediaText); } + + [Fact] + public void CombinedConditionMediaQueriesLevel4() + { + const string source = @"/* Traditionelle Syntax */ +@media (min-height: 500px) and (max-height: 800px) { + /* Styles */ + h1 { color: rgb(255, 0, 0); } +} + +/* Mit Vergleichsoperatoren */ +@media (height >= 500px) and (height <= 800px) { + /* Gleiche Styles */ + h1 { color: rgb(255, 0, 0); } +}"; + var result = ParseStyleSheet(source); + Assert.Equal(source, result.StylesheetText.Text); + Assert.Equal(2, result.Rules.Length); + var rule1 = result.Rules[0] as MediaRule; + var rule2 = result.Rules[1] as MediaRule; + Assert.NotNull(rule1); + Assert.NotNull(rule2); + Assert.Equal("(min-height: 500px) and (max-height: 800px)", rule1.ConditionText); + Assert.Equal("(height >= 500px) and (height <= 800px)", rule2.ConditionText); + Assert.Equal("@media (min-height: 500px) and (max-height: 800px) { h1 { color: rgb(255, 0, 0) } }", rule1.ToCss()); + Assert.Equal("@media (height >= 500px) and (height <= 800px) { h1 { color: rgb(255, 0, 0) } }", rule2.ToCss()); + } } } diff --git a/src/ExCSS/Enumerations/ContainerType.cs b/src/ExCSS/Enumerations/ContainerType.cs new file mode 100644 index 00000000..6a9c31b5 --- /dev/null +++ b/src/ExCSS/Enumerations/ContainerType.cs @@ -0,0 +1,9 @@ +namespace ExCSS +{ + public enum ContainerType : byte + { + Normal, + Size, + InlineSize + } +} diff --git a/src/ExCSS/Enumerations/FeatureNames.cs b/src/ExCSS/Enumerations/FeatureNames.cs index 2f447a01..cb264460 100644 --- a/src/ExCSS/Enumerations/FeatureNames.cs +++ b/src/ExCSS/Enumerations/FeatureNames.cs @@ -42,5 +42,7 @@ public static class FeatureNames public static readonly string Scripting = "scripting"; public static readonly string Pointer = "pointer"; public static readonly string Hover = "hover"; + public static readonly string BlockSize = "block-size"; + public static readonly string InlineSize = "inline-size"; } } \ No newline at end of file diff --git a/src/ExCSS/Enumerations/Keywords.cs b/src/ExCSS/Enumerations/Keywords.cs index d72bbb40..5bf39c9d 100644 --- a/src/ExCSS/Enumerations/Keywords.cs +++ b/src/ExCSS/Enumerations/Keywords.cs @@ -310,5 +310,7 @@ internal static class Keywords public static readonly string Last = "last"; public static readonly string SelfStart = "self-start"; public static readonly string SelfEnd = "self-end"; + public static readonly string Size = "size"; + public static readonly string InlineSize = "inline-size"; } } \ No newline at end of file diff --git a/src/ExCSS/Enumerations/PropertyNames.cs b/src/ExCSS/Enumerations/PropertyNames.cs index cea1107a..854654d9 100644 --- a/src/ExCSS/Enumerations/PropertyNames.cs +++ b/src/ExCSS/Enumerations/PropertyNames.cs @@ -91,6 +91,8 @@ public static class PropertyNames public static readonly string ClipRule = "clip-rule"; public static readonly string Color = "color"; public static readonly string ColorInterpolationFilters = "color-interpolation-filters"; + public static readonly string ContainerName= "container-name"; + public static readonly string ContainerType = "container-type"; public static readonly string Content = "content"; public static readonly string CounterIncrement = "counter-increment"; public static readonly string CounterReset = "counter-reset"; diff --git a/src/ExCSS/Enumerations/RuleNames.cs b/src/ExCSS/Enumerations/RuleNames.cs index b06f576c..c4d6bce2 100644 --- a/src/ExCSS/Enumerations/RuleNames.cs +++ b/src/ExCSS/Enumerations/RuleNames.cs @@ -12,5 +12,6 @@ public static class RuleNames public static readonly string Media = "media"; public static readonly string Namespace = "namespace"; public static readonly string Page = "page"; + public static readonly string Container = "container"; } } \ No newline at end of file diff --git a/src/ExCSS/Enumerations/RuleType.cs b/src/ExCSS/Enumerations/RuleType.cs index 8d4147aa..c24b9cbe 100644 --- a/src/ExCSS/Enumerations/RuleType.cs +++ b/src/ExCSS/Enumerations/RuleType.cs @@ -18,6 +18,7 @@ public enum RuleType : byte Document, FontFeatureValues, Viewport, - RegionStyle + RegionStyle, + Container } } \ No newline at end of file diff --git a/src/ExCSS/Enumerations/TokenType.cs b/src/ExCSS/Enumerations/TokenType.cs index db858723..581e437e 100644 --- a/src/ExCSS/Enumerations/TokenType.cs +++ b/src/ExCSS/Enumerations/TokenType.cs @@ -29,6 +29,11 @@ internal enum TokenType : byte Comma, Semicolon, Whitespace, - EndOfFile + EndOfFile, + GreaterThan, + GreaterThanOrEqual, + LessThan, + LessThanOrEqual, + Equal } } \ No newline at end of file diff --git a/src/ExCSS/Factories/MediaFeatureFactory.cs b/src/ExCSS/Factories/MediaFeatureFactory.cs index f7b4ba33..4c5bf9c4 100644 --- a/src/ExCSS/Factories/MediaFeatureFactory.cs +++ b/src/ExCSS/Factories/MediaFeatureFactory.cs @@ -60,7 +60,9 @@ internal sealed class MediaFeatureFactory {FeatureNames.UpdateFrequency, () => new UpdateFrequencyMediaFeature()}, {FeatureNames.Scripting, () => new ScriptingMediaFeature()}, {FeatureNames.Pointer, () => new PointerMediaFeature()}, - {FeatureNames.Hover, () => new HoverMediaFeature()} + {FeatureNames.Hover, () => new HoverMediaFeature()}, + {FeatureNames.InlineSize, () => new SizeMediaFeature(FeatureNames.InlineSize)}, + {FeatureNames.BlockSize, () => new SizeMediaFeature(FeatureNames.BlockSize)}, }; #endregion diff --git a/src/ExCSS/Factories/PropertyFactory.cs b/src/ExCSS/Factories/PropertyFactory.cs index fa42dd04..22316405 100644 --- a/src/ExCSS/Factories/PropertyFactory.cs +++ b/src/ExCSS/Factories/PropertyFactory.cs @@ -173,6 +173,8 @@ private PropertyFactory() AddLonghand(PropertyNames.Clear, () => new ClearProperty()); AddLonghand(PropertyNames.Clip, () => new ClipProperty(), true); AddLonghand(PropertyNames.Color, () => new ColorProperty(), true); + AddLonghand(PropertyNames.ContainerName, () => new ContainerNameProperty()); + AddLonghand(PropertyNames.ContainerType, () => new ContainerTypeProperty()); AddLonghand(PropertyNames.Content, () => new ContentProperty()); AddLonghand(PropertyNames.CounterIncrement, () => new CounterIncrementProperty()); AddLonghand(PropertyNames.CounterReset, () => new CounterResetProperty()); diff --git a/src/ExCSS/Formatting/CompressedStyleFormatter.cs b/src/ExCSS/Formatting/CompressedStyleFormatter.cs index bca9cc79..e27b32f5 100644 --- a/src/ExCSS/Formatting/CompressedStyleFormatter.cs +++ b/src/ExCSS/Formatting/CompressedStyleFormatter.cs @@ -15,9 +15,9 @@ string IStyleFormatter.Declaration(string name, string value, bool important) return string.Concat(name, ": ", rest); } - string IStyleFormatter.Constraint(string name, string value) + string IStyleFormatter.Constraint(string name, string value, string constraintDelimiter) { - var ending = value != null ? ": " + value : string.Empty; + var ending = value != null ? constraintDelimiter + value : string.Empty; return string.Concat("(", name, ending, ")"); } diff --git a/src/ExCSS/MediaFeatures/MediaFeature.cs b/src/ExCSS/MediaFeatures/MediaFeature.cs index e8302769..bd7f52ff 100644 --- a/src/ExCSS/MediaFeatures/MediaFeature.cs +++ b/src/ExCSS/MediaFeatures/MediaFeature.cs @@ -1,10 +1,12 @@ -using System.IO; +using System; +using System.IO; namespace ExCSS { public abstract class MediaFeature : StylesheetNode, IMediaFeature { private TokenValue _tokenValue; + private TokenType _constraintDelimiter; internal MediaFeature(string name) { @@ -27,11 +29,29 @@ internal MediaFeature(string name) public override void ToCss(TextWriter writer, IStyleFormatter formatter) { + var constraintDelimiter = GetConstraintDelimiter(); var value = HasValue ? Value : null; - writer.Write(formatter.Constraint(Name, value)); + writer.Write(formatter.Constraint(Name, value, GetConstraintDelimiter())); } - internal bool TrySetValue(TokenValue tokenValue) + private string GetConstraintDelimiter() + { + if (_constraintDelimiter == TokenType.Colon) + return ": "; + if (_constraintDelimiter == TokenType.GreaterThan) + return " > "; + if (_constraintDelimiter == TokenType.LessThan) + return " < "; + if (_constraintDelimiter == TokenType.Equal) + return " = "; + if (_constraintDelimiter == TokenType.GreaterThanOrEqual) + return " >= "; + if (_constraintDelimiter == TokenType.LessThanOrEqual) + return " <= "; + return ": "; + } + + internal bool TrySetValue(TokenValue tokenValue, TokenType constraintDelimiter) { bool result; @@ -42,6 +62,8 @@ internal bool TrySetValue(TokenValue tokenValue) if (result) _tokenValue = tokenValue; + _constraintDelimiter = constraintDelimiter; + return result; } } diff --git a/src/ExCSS/MediaFeatures/SizeMediaFeature.cs b/src/ExCSS/MediaFeatures/SizeMediaFeature.cs new file mode 100644 index 00000000..e61e1f0f --- /dev/null +++ b/src/ExCSS/MediaFeatures/SizeMediaFeature.cs @@ -0,0 +1,11 @@ +namespace ExCSS +{ + internal sealed class SizeMediaFeature : MediaFeature + { + public SizeMediaFeature(string name) : base(name) + { + } + + internal override IValueConverter Converter => Converters.LengthConverter; + } +} diff --git a/src/ExCSS/Model/Converters.cs b/src/ExCSS/Model/Converters.cs index 594de14d..d8cf729f 100644 --- a/src/ExCSS/Model/Converters.cs +++ b/src/ExCSS/Model/Converters.cs @@ -264,6 +264,7 @@ public static readonly IValueConverter public static readonly IValueConverter OverflowModeConverter = Map.OverflowModes.ToConverter(); public static readonly IValueConverter FloatingConverter = Map.FloatingModes.ToConverter(); public static readonly IValueConverter DisplayModeConverter = Map.DisplayModes.ToConverter(); + public static readonly IValueConverter ContainerTypeConverter = Map.ContainerTypes.ToConverter(); public static readonly IValueConverter ClearModeConverter = Map.ClearModes.ToConverter(); public static readonly IValueConverter FontStretchConverter = Map.FontStretches.ToConverter(); public static readonly IValueConverter FontStyleConverter = Map.FontStyles.ToConverter(); diff --git a/src/ExCSS/Model/IStyleFormatter.cs b/src/ExCSS/Model/IStyleFormatter.cs index 0d1bf4fd..5bc27d4b 100644 --- a/src/ExCSS/Model/IStyleFormatter.cs +++ b/src/ExCSS/Model/IStyleFormatter.cs @@ -9,7 +9,7 @@ public interface IStyleFormatter string Declaration(string name, string value, bool important); string Declarations(IEnumerable declarations); string Medium(bool exclusive, bool inverse, string type, IEnumerable constraints); - string Constraint(string name, string value); + string Constraint(string name, string value, string constraintDelimiter); string Rule(string name, string value); string Rule(string name, string prelude, string rules); string Style(string selector, IStyleFormattable rules); diff --git a/src/ExCSS/Model/Map.cs b/src/ExCSS/Model/Map.cs index aeb2118b..0db9d649 100644 --- a/src/ExCSS/Model/Map.cs +++ b/src/ExCSS/Model/Map.cs @@ -592,5 +592,13 @@ internal static class Map { Keywords.SelfEnd, AlignItem.SelfEnd }, { Keywords.Baseline, AlignItem.Baseline }, }; + + public static readonly Dictionary ContainerTypes = + new(StringComparer.OrdinalIgnoreCase) + { + {Keywords.Normal, ContainerType.Normal}, + {Keywords.Size, ContainerType.Size}, + {Keywords.InlineSize, ContainerType.InlineSize} + }; } } \ No newline at end of file diff --git a/src/ExCSS/Model/ParserExtensions.cs b/src/ExCSS/Model/ParserExtensions.cs index 3ac3b7d4..d5f6a87f 100644 --- a/src/ExCSS/Model/ParserExtensions.cs +++ b/src/ExCSS/Model/ParserExtensions.cs @@ -138,6 +138,8 @@ public static Rule CreateRule(this StylesheetParser parser, RuleType type) return new KeyframesRule(parser); case RuleType.Media: return new MediaRule(parser); + case RuleType.Container: + return new ContainerRule(parser); case RuleType.Namespace: return new NamespaceRule(parser); case RuleType.Page: diff --git a/src/ExCSS/Model/StyleDeclaration.cs b/src/ExCSS/Model/StyleDeclaration.cs index 5a4be073..34af1f15 100644 --- a/src/ExCSS/Model/StyleDeclaration.cs +++ b/src/ExCSS/Model/StyleDeclaration.cs @@ -842,6 +842,18 @@ public string ColumnWidth set => SetPropertyValue(PropertyNames.ColumnWidth, value); } + public string ContainerName + { + get => GetPropertyValue(PropertyNames.ContainerName); + set => SetPropertyValue(PropertyNames.ContainerName, value); + } + + public string ContainerType + { + get => GetPropertyValue(PropertyNames.ContainerType); + set => SetPropertyValue(PropertyNames.ContainerType, value); + } + public string Content { get => GetPropertyValue(PropertyNames.Content); diff --git a/src/ExCSS/Model/Stylesheet.cs b/src/ExCSS/Model/Stylesheet.cs index d0b0b711..feda757a 100644 --- a/src/ExCSS/Model/Stylesheet.cs +++ b/src/ExCSS/Model/Stylesheet.cs @@ -21,6 +21,7 @@ internal Stylesheet(StylesheetParser parser) public IEnumerable CharacterSetRules => Rules.Where(r => r is CharsetRule).Cast(); public IEnumerable FontfaceSetRules => Rules.Where(r => r is FontFaceRule).Cast(); public IEnumerable MediaRules => Rules.Where(r => r is MediaRule).Cast(); + public IEnumerable ContainerRules => Rules.Where(r => r is ContainerRule).Cast(); public IEnumerable ImportRules => Rules.Where(r => r is ImportRule).Cast(); public IEnumerable NamespaceRules => diff --git a/src/ExCSS/Model/ValueBuilder.cs b/src/ExCSS/Model/ValueBuilder.cs index 3f40c186..9315804b 100644 --- a/src/ExCSS/Model/ValueBuilder.cs +++ b/src/ExCSS/Model/ValueBuilder.cs @@ -55,6 +55,10 @@ public void Apply(Token token) case TokenType.Url: case TokenType.Number: case TokenType.Comma: + case TokenType.GreaterThan: + case TokenType.GreaterThanOrEqual: + case TokenType.LessThan: + case TokenType.LessThanOrEqual: Add(token); break; case TokenType.Comment: diff --git a/src/ExCSS/Parser/Lexer.cs b/src/ExCSS/Parser/Lexer.cs index d73a917d..367154ae 100644 --- a/src/ExCSS/Parser/Lexer.cs +++ b/src/ExCSS/Parser/Lexer.cs @@ -129,19 +129,29 @@ private Token Data(char current) return NewSemicolon(); case Symbols.LessThan: current = GetNext(); - if (current != Symbols.ExclamationMark) return NewDelimiter(GetPrevious()); - current = GetNext(); - if (current == Symbols.Minus) + if (current == Symbols.ExclamationMark) { current = GetNext(); - if (current == Symbols.Minus) return NewOpenComment(); - // ReSharper disable once RedundantAssignment - current = GetPrevious(); - } - - GetPrevious(); - return NewDelimiter(GetPrevious()); + if (current == Symbols.Minus) + { + current = GetNext(); + if (current == Symbols.Minus) return NewOpenComment(); + // ReSharper disable once RedundantAssignment + current = GetPrevious(); + } + GetPrevious(); + return NewDelimiter(GetPrevious()); + } + else if (current == Symbols.Equality) + { + Advance(); + return NewLessThanOrEqual(); + } + else + { + return NewLessThan(); + } case Symbols.At: return AtKeywordStart(); case Symbols.SquareBracketOpen: @@ -199,6 +209,13 @@ private Token Data(char current) return current == Symbols.Equality ? NewMatch(Combinators.Unlike) : NewDelimiter(GetPrevious()); + case Symbols.GreaterThan: + if (GetNext() == Symbols.Equality) + { + Advance(); + return NewGreaterThanOrEqual(); + } + return NewGreaterThan(); default: return current.IsNameStart() ? IdentStart(current) : NewDelimiter(current); } @@ -1081,6 +1098,13 @@ private Token NewEof() return new(TokenType.EndOfFile, string.Empty, _position); } + private Token NewGreaterThan() => new Token(TokenType.GreaterThan, ">", _position); + private Token NewGreaterThanOrEqual() => new Token(TokenType.GreaterThanOrEqual, ">=", _position); + private Token NewLessThan() => new Token(TokenType.LessThan, "<", _position); + private Token NewLessThanOrEqual() => new Token(TokenType.LessThanOrEqual, "<=", _position); + private Token NewEqual() => new Token(TokenType.Equal, "=", _position); + + private Token NumberExponential(char letter) { var current = GetNext(); diff --git a/src/ExCSS/Parser/SelectorConstructor.cs b/src/ExCSS/Parser/SelectorConstructor.cs index 35d7b635..c29678bf 100644 --- a/src/ExCSS/Parser/SelectorConstructor.cs +++ b/src/ExCSS/Parser/SelectorConstructor.cs @@ -176,6 +176,9 @@ private void OnData(Token token) case TokenType.Delim: OnDelim(token); break; + case TokenType.GreaterThan: + OnDelim(token); + break; case TokenType.Comma: InsertOr(); _ready = false; diff --git a/src/ExCSS/Parser/StylesheetComposer.cs b/src/ExCSS/Parser/StylesheetComposer.cs index 8313da80..e2aa61ae 100644 --- a/src/ExCSS/Parser/StylesheetComposer.cs +++ b/src/ExCSS/Parser/StylesheetComposer.cs @@ -37,6 +37,8 @@ public Rule CreateAtRule(Token token) if (token.Data.Is(RuleNames.ViewPort)) return CreateViewport(token); + if (token.Data.Is(RuleNames.Container)) return CreateContainer(token); + return token.Data.Is(RuleNames.Document) ? CreateDocument(token) : CreateUnknown(token); } @@ -220,6 +222,60 @@ public Rule CreateMedia(Token current) return rule; } + public Rule CreateContainer(Token current) + { + var rule = new ContainerRule(_parser); + var start = current.Position; + var token = NextToken(); + _nodes.Push(rule); + ParseComments(ref token); + rule.Name = GetRuleName(ref token); + ParseComments(ref token); + FillMediaList(rule.Media, TokenType.CurlyBracketOpen, ref token); + ParseComments(ref token); + + if (token.Type != TokenType.CurlyBracketOpen) + while (token.Type != TokenType.EndOfFile) + { + if (token.Type == TokenType.Semicolon) + { + _nodes.Pop(); + return null; + } + + if (token.Type == TokenType.CurlyBracketOpen) break; + + token = NextToken(); + } + + var end = FillRules(rule); + rule.StylesheetText = CreateView(start, end); + _nodes.Pop(); + return rule; + + //ParseComments(ref token); + //rule.Condition = AggregateCondition(ref token); + //ParseComments(ref token); + + //if (token.Type != TokenType.CurlyBracketOpen) + // while (token.Type != TokenType.EndOfFile) + // { + // if (token.Type == TokenType.Semicolon) + // { + // _nodes.Pop(); + // return null; + // } + + // if (token.Type == TokenType.CurlyBracketOpen) break; + + // token = NextToken(); + // } + + //var end = FillRules(rule); + //rule.StylesheetText = CreateView(start, end); + //_nodes.Pop(); + //return rule; + } public Rule CreateNamespace(Token current) { var rule = new NamespaceRule(_parser); @@ -1170,9 +1226,13 @@ private MediaFeature CreateFeature(ref Token token) : MediaFeatureFactory.Instance.Create(token.Data); token = NextToken(); - - if (token.Type == TokenType.Colon) + ParseComments(ref token); + var tokenDelimiter = TokenType.Colon; + if (token.Type == TokenType.Colon || + token.Type == TokenType.GreaterThan || token.Type == TokenType.LessThan || + token.Type == TokenType.GreaterThanOrEqual || token.Type == TokenType.LessThanOrEqual) { + tokenDelimiter = token.Type; var value = Pool.NewValueBuilder(); token = NextToken(); @@ -1189,7 +1249,7 @@ private MediaFeature CreateFeature(ref Token token) return null; } - if (feature != null && feature.TrySetValue(val)) + if (feature != null && feature.TrySetValue(val, tokenDelimiter)) { if (feature is StylesheetNode node) { diff --git a/src/ExCSS/Rules/ContainerRule.cs b/src/ExCSS/Rules/ContainerRule.cs new file mode 100644 index 00000000..aa60caee --- /dev/null +++ b/src/ExCSS/Rules/ContainerRule.cs @@ -0,0 +1,32 @@ +using System.IO; +using System.Linq; + +namespace ExCSS +{ + internal sealed class ContainerRule : ConditionRule, IContainerRule + { + internal ContainerRule(StylesheetParser parser) : base(RuleType.Container, parser) + { + AppendChild(new MediaList(parser)); + } + + public override void ToCss(TextWriter writer, IStyleFormatter formatter) + { + var rules = formatter.Block(Rules); + var name = "@container"; + if (!string.IsNullOrEmpty(Name)) + name = $"{name} {Name}"; + writer.Write(formatter.Rule(name, ConditionText, rules)); + } + + public MediaList Media => Children.OfType().FirstOrDefault(); + + public string Name { get; set; } + + public string ConditionText + { + get => Media.MediaText; + set => Media.MediaText = value; + } + } +} diff --git a/src/ExCSS/Rules/IContainer.cs b/src/ExCSS/Rules/IContainer.cs new file mode 100644 index 00000000..2238b9f2 --- /dev/null +++ b/src/ExCSS/Rules/IContainer.cs @@ -0,0 +1,8 @@ +namespace ExCSS +{ + public interface IContainerRule : IConditionRule + { + string Name { get; set; } + MediaList Media { get; } + } +} \ No newline at end of file diff --git a/src/ExCSS/StyleProperties/Container/ContainerNameProperty.cs b/src/ExCSS/StyleProperties/Container/ContainerNameProperty.cs new file mode 100644 index 00000000..e7a43336 --- /dev/null +++ b/src/ExCSS/StyleProperties/Container/ContainerNameProperty.cs @@ -0,0 +1,15 @@ +namespace ExCSS +{ + internal sealed class ContainerNameProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.StringConverter.OrDefault(); + + internal ContainerNameProperty() + : base(PropertyNames.ContainerName) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} \ No newline at end of file diff --git a/src/ExCSS/StyleProperties/Container/ContainerTypeProperty.cs b/src/ExCSS/StyleProperties/Container/ContainerTypeProperty.cs new file mode 100644 index 00000000..f072ad9a --- /dev/null +++ b/src/ExCSS/StyleProperties/Container/ContainerTypeProperty.cs @@ -0,0 +1,15 @@ +namespace ExCSS +{ + internal sealed class ContainerTypeProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.ContainerTypeConverter.OrDefault(Keywords.Normal); + + internal ContainerTypeProperty() + : base(PropertyNames.ContainerType) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} \ No newline at end of file