Skip to content

Commit

Permalink
Merge pull request #183 from cwall-dev/feature/containerqueries
Browse files Browse the repository at this point in the history
Added support for @container queries and comparison operators
  • Loading branch information
TylerBrinks authored Oct 22, 2024
2 parents 8f13bbb + 964aea2 commit 7ee5c37
Show file tree
Hide file tree
Showing 28 changed files with 403 additions and 29 deletions.
103 changes: 103 additions & 0 deletions src/ExCSS.Tests/Container.cs
Original file line number Diff line number Diff line change
@@ -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<StyleRule>().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<StyleRule>().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<StyleRule>().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<StyleRule>().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());
}
}
}
41 changes: 34 additions & 7 deletions src/ExCSS.Tests/MediaList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using ExCSS;
using Xunit;
using System;

public class CssMediaListTests : CssConstructionFunctions
{
[Fact]
Expand Down Expand Up @@ -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());
}
}
}
9 changes: 9 additions & 0 deletions src/ExCSS/Enumerations/ContainerType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ExCSS
{
public enum ContainerType : byte
{
Normal,
Size,
InlineSize
}
}
2 changes: 2 additions & 0 deletions src/ExCSS/Enumerations/FeatureNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
2 changes: 2 additions & 0 deletions src/ExCSS/Enumerations/Keywords.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
2 changes: 2 additions & 0 deletions src/ExCSS/Enumerations/PropertyNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
1 change: 1 addition & 0 deletions src/ExCSS/Enumerations/RuleNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
3 changes: 2 additions & 1 deletion src/ExCSS/Enumerations/RuleType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public enum RuleType : byte
Document,
FontFeatureValues,
Viewport,
RegionStyle
RegionStyle,
Container
}
}
7 changes: 6 additions & 1 deletion src/ExCSS/Enumerations/TokenType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ internal enum TokenType : byte
Comma,
Semicolon,
Whitespace,
EndOfFile
EndOfFile,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
Equal
}
}
4 changes: 3 additions & 1 deletion src/ExCSS/Factories/MediaFeatureFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/ExCSS/Factories/PropertyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
4 changes: 2 additions & 2 deletions src/ExCSS/Formatting/CompressedStyleFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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, ")");
}

Expand Down
28 changes: 25 additions & 3 deletions src/ExCSS/MediaFeatures/MediaFeature.cs
Original file line number Diff line number Diff line change
@@ -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)
{
Expand All @@ -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;

Expand All @@ -42,6 +62,8 @@ internal bool TrySetValue(TokenValue tokenValue)

if (result) _tokenValue = tokenValue;

_constraintDelimiter = constraintDelimiter;

return result;
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/ExCSS/MediaFeatures/SizeMediaFeature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace ExCSS
{
internal sealed class SizeMediaFeature : MediaFeature
{
public SizeMediaFeature(string name) : base(name)
{
}

internal override IValueConverter Converter => Converters.LengthConverter;
}
}
1 change: 1 addition & 0 deletions src/ExCSS/Model/Converters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,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();
Expand Down
2 changes: 1 addition & 1 deletion src/ExCSS/Model/IStyleFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public interface IStyleFormatter
string Declaration(string name, string value, bool important);
string Declarations(IEnumerable<string> declarations);
string Medium(bool exclusive, bool inverse, string type, IEnumerable<IStyleFormattable> 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);
Expand Down
8 changes: 8 additions & 0 deletions src/ExCSS/Model/Map.cs
Original file line number Diff line number Diff line change
Expand Up @@ -592,5 +592,13 @@ internal static class Map
{ Keywords.SelfEnd, AlignItem.SelfEnd },
{ Keywords.Baseline, AlignItem.Baseline },
};

public static readonly Dictionary<string, ContainerType> ContainerTypes =
new(StringComparer.OrdinalIgnoreCase)
{
{Keywords.Normal, ContainerType.Normal},
{Keywords.Size, ContainerType.Size},
{Keywords.InlineSize, ContainerType.InlineSize}
};
}
}
2 changes: 2 additions & 0 deletions src/ExCSS/Model/ParserExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions src/ExCSS/Model/StyleDeclaration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 7ee5c37

Please sign in to comment.