diff --git a/src/ExCSS.Tests/FontPaletteTests.cs b/src/ExCSS.Tests/FontPaletteTests.cs new file mode 100644 index 00000000..5f1ba48f --- /dev/null +++ b/src/ExCSS.Tests/FontPaletteTests.cs @@ -0,0 +1,121 @@ +using System.Linq; +using Xunit; + +namespace ExCSS.Tests +{ + public class FontPaletteTests : CssConstructionFunctions + { + private string FontPalette(string value) + { + var sheet = ParseStyleSheet($".x {{ font-palette: {value}; }}"); + var rule = sheet.Rules.OfType().Single(); + return rule.Style.GetPropertyValue("font-palette"); + } + + [Theory] + [InlineData("normal")] + [InlineData("light")] + [InlineData("dark")] + [InlineData("--my-palette")] + public void FontPaletteAcceptsValidValues(string value) + { + Assert.Equal(value, FontPalette(value)); + } + + [Theory] + [InlineData("5px")] + [InlineData("#fff")] + [InlineData("rgb(0,0,0)")] + public void FontPaletteDropsInvalidValues(string value) + { + Assert.Equal(string.Empty, FontPalette(value)); + } + + [Fact] + public void FontPaletteRoundTrips() + { + var sheet = ParseStyleSheet(".x { font-palette: --my-palette; }"); + var rule = sheet.Rules.OfType().Single(); + + Assert.Equal("--my-palette", rule.Style.GetPropertyValue("font-palette")); + Assert.Contains("font-palette: --my-palette", rule.ToCss()); + } + + [Fact] + public void FontPaletteValuesParsesNameAndDescriptors() + { + var src = "@font-palette-values --brand { font-family: \"Nabla\"; base-palette: 2; override-colors: 0 #ff0000, 1 rgb(0, 255, 0); }"; + var sheet = ParseStyleSheet(src); + + var rule = sheet.Rules.OfType().Single(); + Assert.Equal(RuleType.FontPaletteValues, rule.Type); + Assert.Equal("--brand", rule.Name); + Assert.Contains("Nabla", rule.Family); + Assert.Equal("2", rule.BasePalette); + Assert.Contains("#ff0000", rule.OverrideColors); + Assert.Contains("rgb(0, 255, 0)", rule.OverrideColors); + } + + [Fact] + public void FontPaletteValuesLightDarkBasePalette() + { + var sheet = ParseStyleSheet("@font-palette-values --d { font-family: Nabla; base-palette: dark; }"); + var rule = sheet.Rules.OfType().Single(); + Assert.Equal("dark", rule.BasePalette); + } + + [Fact] + public void FontPaletteValuesRoundTrips() + { + var src = "@font-palette-values --p { font-family: Nabla; base-palette: 1; }"; + var sheet = ParseStyleSheet(src); + var rule = sheet.Rules.OfType().Single(); + + var css = rule.ToCss(); + Assert.StartsWith("@font-palette-values --p {", css); + Assert.Contains("font-family: Nabla", css); + Assert.Contains("base-palette: 1", css); + } + + [Fact] + public void FontPaletteValuesIsExposedAsIFontPaletteValuesRule() + { + var sheet = ParseStyleSheet("@font-palette-values --c { font-family: Nabla; base-palette: 1; }"); + var rule = Assert.IsAssignableFrom(sheet.Rules.First()); + + Assert.Equal("--c", rule.Name); + } + + [Fact] + public void FontPaletteValuesAmongOtherRules() + { + var src = ".a { color: red } @font-palette-values --p { font-family: Nabla; base-palette: 1; } .b { color: blue }"; + var sheet = ParseStyleSheet(src); + + Assert.Equal(3, sheet.Rules.Length); + Assert.IsType(sheet.Rules[0]); + Assert.IsType(sheet.Rules[1]); + Assert.IsType(sheet.Rules[2]); + } + + [Fact] + public void FontPaletteValuesDoesNotDerailFollowingRules() + { + var src = "@font-palette-values --p { font-family: Nabla; base-palette: 1; } .after { color: red; }"; + var sheet = ParseStyleSheet(src); + + Assert.Single(sheet.Rules.OfType()); + var styleRule = sheet.Rules.OfType().Single(); + Assert.Equal(".after", styleRule.SelectorText); + Assert.Equal("rgb(255, 0, 0)", styleRule.Style.GetPropertyValue("color")); + } + + [Fact] + public void FontPaletteValuesNoDeclarationBlockDoesNotCrashAndFollowingRuleApplies() + { + var sheet = ParseStyleSheet("@font-palette-values --x; .after { color: red; }"); + var styleRule = sheet.Rules.OfType().Single(); + Assert.Equal(".after", styleRule.SelectorText); + } + } +} diff --git a/src/ExCSS/Enumerations/PropertyNames.cs b/src/ExCSS/Enumerations/PropertyNames.cs index 3156b5aa..013101e0 100644 --- a/src/ExCSS/Enumerations/PropertyNames.cs +++ b/src/ExCSS/Enumerations/PropertyNames.cs @@ -118,6 +118,7 @@ public static class PropertyNames public static readonly string Float = "float"; public static readonly string FontFamily = "font-family"; public static readonly string FontFeatureSettings = "font-feature-settings"; + public static readonly string FontPalette = "font-palette"; public static readonly string FontSize = "font-size"; public static readonly string FontSizeAdjust = "font-size-adjust"; public static readonly string FontStyle = "font-style"; @@ -244,6 +245,8 @@ public static class PropertyNames public static readonly string Syntax = "syntax"; public static readonly string InitialValue = "initial-value"; public static readonly string Inherits = "inherits"; + public static readonly string BasePalette = "base-palette"; + public static readonly string OverrideColors = "override-colors"; public static readonly string ObjectFit = "object-fit"; // CSS Paged Media 3: the "page" property assigns a box to a named page; "size" is an @page descriptor. public static readonly string PageName = "page"; diff --git a/src/ExCSS/Enumerations/RuleNames.cs b/src/ExCSS/Enumerations/RuleNames.cs index 06934227..be6dc8e4 100644 --- a/src/ExCSS/Enumerations/RuleNames.cs +++ b/src/ExCSS/Enumerations/RuleNames.cs @@ -14,5 +14,6 @@ public static class RuleNames public static readonly string Page = "page"; public static readonly string Container = "container"; public static readonly string Property = "property"; + public static readonly string FontPaletteValues = "font-palette-values"; } } \ No newline at end of file diff --git a/src/ExCSS/Enumerations/RuleType.cs b/src/ExCSS/Enumerations/RuleType.cs index 358e8df6..5d39a6a0 100644 --- a/src/ExCSS/Enumerations/RuleType.cs +++ b/src/ExCSS/Enumerations/RuleType.cs @@ -20,6 +20,7 @@ public enum RuleType : byte Viewport, RegionStyle, Container, - Property + Property, + FontPaletteValues } } \ No newline at end of file diff --git a/src/ExCSS/Factories/PropertyFactory.cs b/src/ExCSS/Factories/PropertyFactory.cs index 20c0a6a5..c5e5b67d 100644 --- a/src/ExCSS/Factories/PropertyFactory.cs +++ b/src/ExCSS/Factories/PropertyFactory.cs @@ -216,6 +216,7 @@ private PropertyFactory() AddLonghand(PropertyNames.FontVariant, () => new FontVariantProperty(), false, true); AddLonghand(PropertyNames.FontWeight, () => new FontWeightProperty(), true, true); AddLonghand(PropertyNames.FontStretch, () => new FontStretchProperty(), true, true); + AddLonghand(PropertyNames.FontPalette, () => new FontPaletteProperty()); AddShorthand(PropertyNames.Gap, () => new GapProperty(), PropertyNames.RowGap, @@ -387,6 +388,19 @@ private static bool IsPropertyDescriptor(string name) name.Is(PropertyNames.Inherits); } + // The @font-palette-values descriptors (font-family / base-palette / override-colors). Their values + // are stored raw via an UnknownProperty (Converters.Any); the resolver re-tokenizes them later. + public Property CreateFontPaletteDescriptor(string name) + { + return IsFontPaletteDescriptor(name) ? new UnknownProperty(name) : null; + } + + private static bool IsFontPaletteDescriptor(string name) + { + return name.Is(PropertyNames.FontFamily) || name.Is(PropertyNames.BasePalette) || + name.Is(PropertyNames.OverrideColors); + } + public Property CreateViewport(string name) { var feature = MediaFeatureFactory.Instance.Create(name); diff --git a/src/ExCSS/Parser/StylesheetComposer.cs b/src/ExCSS/Parser/StylesheetComposer.cs index 5ab8cce6..f1eb1882 100644 --- a/src/ExCSS/Parser/StylesheetComposer.cs +++ b/src/ExCSS/Parser/StylesheetComposer.cs @@ -41,6 +41,8 @@ public Rule CreateAtRule(Token token) if (token.Data.Is(RuleNames.Property)) return CreateProperty(token); + if (token.Data.Is(RuleNames.FontPaletteValues)) return CreateFontPaletteValues(token); + return token.Data.Is(RuleNames.Document) ? CreateDocument(token) : CreateUnknown(token); } @@ -171,6 +173,28 @@ public Rule CreateProperty(Token current) return SkipDeclarations(token); } + public Rule CreateFontPaletteValues(Token current) + { + var rule = new FontPaletteValuesRule(_parser); + var start = current.Position; + var token = NextToken(); + _nodes.Push(rule); + ParseComments(ref token); + rule.Name = GetRuleName(ref token); + ParseComments(ref token); + + if (token.Type == TokenType.CurlyBracketOpen) + { + var end = FillDeclarations(rule, PropertyFactory.Instance.CreateFontPaletteDescriptor); + rule.StylesheetText = CreateView(start, end); + _nodes.Pop(); + return rule; + } + + _nodes.Pop(); + return SkipDeclarations(token); + } + public Rule CreateImport(Token current) { var rule = new ImportRule(_parser); diff --git a/src/ExCSS/Rules/FontPaletteValuesRule.cs b/src/ExCSS/Rules/FontPaletteValuesRule.cs new file mode 100644 index 00000000..70feef59 --- /dev/null +++ b/src/ExCSS/Rules/FontPaletteValuesRule.cs @@ -0,0 +1,50 @@ +using System.IO; +using System.Linq; + +namespace ExCSS +{ + /// + /// A @font-palette-values at-rule (CSS Fonts Module Level 4): associates a named palette override + /// (a dashed-ident such as --my-palette) with a font family, selecting a base-palette and + /// optionally overriding individual colors (override-colors). The descriptor block is stored like + /// ; the name is captured from the prelude like . + /// + internal sealed class FontPaletteValuesRule : DeclarationRule, IFontPaletteValuesRule + { + internal FontPaletteValuesRule(StylesheetParser parser) + : base(RuleType.FontPaletteValues, RuleNames.FontPaletteValues, parser) + { + } + + protected override Property CreateNewProperty(string name) + { + return PropertyFactory.Instance.CreateFontPaletteDescriptor(name); + } + + public string Name { get; set; } + + public string Family + { + get => GetValue(PropertyNames.FontFamily); + set => SetValue(PropertyNames.FontFamily, value); + } + + public string BasePalette + { + get => GetValue(PropertyNames.BasePalette); + set => SetValue(PropertyNames.BasePalette, value); + } + + public string OverrideColors + { + get => GetValue(PropertyNames.OverrideColors); + set => SetValue(PropertyNames.OverrideColors, value); + } + + public override void ToCss(TextWriter writer, IStyleFormatter formatter) + { + var declarations = formatter.Declarations(Declarations.Where(d => d.HasValue).Select(d => d.ToCss(formatter))); + writer.Write(string.Concat("@font-palette-values ", Name, " { ", declarations, " }")); + } + } +} diff --git a/src/ExCSS/Rules/IFontPaletteValuesRule.cs b/src/ExCSS/Rules/IFontPaletteValuesRule.cs new file mode 100644 index 00000000..84f79067 --- /dev/null +++ b/src/ExCSS/Rules/IFontPaletteValuesRule.cs @@ -0,0 +1,21 @@ +namespace ExCSS +{ + /// + /// A palette override declared with a @font-palette-values at-rule (CSS Fonts Module Level 4). + /// Exposes the prelude name and the three descriptors. + /// + public interface IFontPaletteValuesRule : IRule, IProperties + { + /// The palette name, a dashed-ident referenced by font-palette: <dashed-ident>, e.g. --my-palette. + string Name { get; set; } + + /// The raw font-family descriptor value (the family this palette applies to). + string Family { get; set; } + + /// The raw base-palette descriptor value (<integer> | light | dark). + string BasePalette { get; set; } + + /// The raw override-colors descriptor value (a comma list of <index> <color> pairs). + string OverrideColors { get; set; } + } +} diff --git a/src/ExCSS/StyleProperties/Font/FontPaletteProperty.cs b/src/ExCSS/StyleProperties/Font/FontPaletteProperty.cs new file mode 100644 index 00000000..c6554a1c --- /dev/null +++ b/src/ExCSS/StyleProperties/Font/FontPaletteProperty.cs @@ -0,0 +1,19 @@ +namespace ExCSS +{ + /// + /// The font-palette property (CSS Fonts Module Level 4): selects a color palette for a COLR/CPAL + /// color font. Inherited. The value grammar (normal | light | dark | <dashed-ident>) is + /// validated by ; the authored text is preserved verbatim. + /// + internal sealed class FontPaletteProperty : Property + { + private static readonly IValueConverter StyleConverter = new FontPaletteValueConverter().OrDefault(); + + internal FontPaletteProperty() + : base(PropertyNames.FontPalette, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/src/ExCSS/ValueConverters/FontPaletteValueConverter.cs b/src/ExCSS/ValueConverters/FontPaletteValueConverter.cs new file mode 100644 index 00000000..0e2c6783 --- /dev/null +++ b/src/ExCSS/ValueConverters/FontPaletteValueConverter.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ExCSS +{ + /// + /// Validates a font-palette value (CSS Fonts Module Level 4): the keywords normal/ + /// light/dark, or a <dashed-ident> naming an @font-palette-values rule. + /// Anything else is dropped at parse time. The authored value text is preserved verbatim, mirroring + /// . (The palette-mix() form of the grammar is not supported.) + /// + internal sealed class FontPaletteValueConverter : IValueConverter + { + public IPropertyValue Convert(IEnumerable value) + { + var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (tokens.Length == 1 && tokens[0].Type == TokenType.Ident && + IsKeywordOrDashedIdent(tokens[0].Data)) + { + return new FontPaletteValue(value); + } + + return null; + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + + private static bool IsKeywordOrDashedIdent(string ident) => + ident.Isi(Keywords.Normal) || ident.Isi("light") || ident.Isi("dark") || + ident.StartsWith("--", StringComparison.Ordinal); + + private sealed class FontPaletteValue : IPropertyValue + { + public FontPaletteValue(IEnumerable tokens) + { + Original = new TokenValue(tokens); + } + + public string CssText => Original.Text; + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) => Original; + } + } +}