├── src ├── Carbon.Css │ ├── Ast │ │ ├── Expressions │ │ │ ├── UnaryOperator.cs │ │ │ ├── UnaryExpression.cs │ │ │ ├── BinaryOperator.cs │ │ │ └── BinaryExpression.cs │ │ ├── Values │ │ │ ├── CssValueSeperator.cs │ │ │ ├── CssUrl.cs │ │ │ ├── CssInterpolatedString.cs │ │ │ ├── CssNumericType.cs │ │ │ ├── CssUndefined.cs │ │ │ ├── CssVariable.cs │ │ │ ├── CssAssignment.cs │ │ │ ├── CssReference.cs │ │ │ ├── CssBoolean.cs │ │ │ ├── CssFunction.cs │ │ │ ├── CssString.cs │ │ │ ├── CssColorSpace.cs │ │ │ ├── CssValueList.cs │ │ │ └── CssUrlValue.cs │ │ ├── Selectors │ │ │ ├── IdSelector.cs │ │ │ ├── ClassSelector.cs │ │ │ ├── PseudoElementSelector.cs │ │ │ ├── TagSelector.cs │ │ │ ├── CombinatorType.cs │ │ │ ├── CssSelectorType.cs │ │ │ ├── AttributeSelector.cs │ │ │ ├── PseudoClassSelector.cs │ │ │ ├── SelectorList.cs │ │ │ └── Selector.cs │ │ ├── Sass │ │ │ ├── WhileBlock.cs │ │ │ ├── EachBlock.cs │ │ │ ├── MixinNode.cs │ │ │ ├── IfBlock.cs │ │ │ └── ForBlock.cs │ │ ├── Rules │ │ │ ├── CssRule.cs │ │ │ ├── PageRule.cs │ │ │ ├── MediaRule.cs │ │ │ ├── ContainerRule.cs │ │ │ ├── UnknownRule.cs │ │ │ ├── SupportsRule.cs │ │ │ ├── CharsetRule.cs │ │ │ ├── StartingStyleRule.cs │ │ │ ├── KeyframesRule.cs │ │ │ ├── FontFaceRule.cs │ │ │ ├── RuleType.cs │ │ │ ├── ImportRule.cs │ │ │ └── StyleRule.cs │ │ ├── CssComment.cs │ │ ├── CssDirective.cs │ │ ├── ICssNumericValue.cs │ │ ├── IncludeNode.cs │ │ ├── Trivia.cs │ │ ├── CssNode.cs │ │ ├── CssRoot.cs │ │ ├── NodeKind.cs │ │ ├── CssDeclaration.cs │ │ └── CssBlock.cs │ ├── Model │ │ ├── CssUnitFlags.cs │ │ ├── CssFormatting.cs │ │ ├── CssBlockFlags.cs │ │ ├── ICssResolver.cs │ │ ├── CssParameter.cs │ │ ├── CssMap.cs │ │ ├── CssSelector.cs │ │ ├── CssScope.cs │ │ ├── CssContext.cs │ │ ├── CssUnitType.cs │ │ ├── ValueList.cs │ │ └── TokenList.cs │ ├── Writer │ │ └── WriterStyle.cs │ ├── Rewriters │ │ ├── ICssTransform.cs │ │ └── Rewriter.cs │ ├── Resolver │ │ ├── IStylesheet.cs │ │ └── Include.cs │ ├── Gradients │ │ ├── IGradient.cs │ │ ├── ConicGradient.cs │ │ ├── AngularColorStop.cs │ │ ├── LinearGradientDirection.cs │ │ ├── ColorStop.cs │ │ └── LinearGradientDirectionHelper.cs │ ├── Globals.cs │ ├── Comptability │ │ ├── BrowserPrefixKind.cs │ │ ├── BrowserType.cs │ │ ├── CompatibilityTable.cs │ │ ├── CursorCompatibility.cs │ │ ├── CssModuleType.cs │ │ ├── BrowserPrefix.cs │ │ ├── CssCompatibility.cs │ │ ├── CssCursor.cs │ │ └── BrowserInfo.cs │ ├── Patching │ │ ├── CssPatcher.cs │ │ ├── CssPatch.cs │ │ ├── PrefixNamePatcher.cs │ │ ├── PrefixNameAndValuePatcher.cs │ │ └── PatchFactory.cs │ ├── Parser │ │ ├── Exceptions │ │ │ ├── UnbalancedBlock.cs │ │ │ ├── UnexpectedModeChange.cs │ │ │ ├── SyntaxException.cs │ │ │ └── UnexpectedTokenException.cs │ │ ├── Tokenizer │ │ │ ├── LexicalMode.cs │ │ │ ├── LexicalModeContext.cs │ │ │ ├── CssTokenKind.cs │ │ │ └── CssToken.cs │ │ ├── Parser │ │ │ └── CssParser.Expressions.cs │ │ └── SourceReader.cs │ ├── Helpers │ │ ├── LineInfo.cs │ │ ├── ThrowHelper.cs │ │ ├── SourceLocation.cs │ │ ├── StringBuilderCache.cs │ │ ├── Base64Helper.cs │ │ ├── SourceHelper.cs │ │ ├── ReadOnlySpanExtensions.cs │ │ ├── StringSplitter.cs │ │ └── NumberHelper.cs │ ├── _ │ │ ├── CssVisibility.cs │ │ ├── CssTextTransform.cs │ │ ├── CssPosition.cs │ │ ├── FontSrcValue.cs │ │ ├── CssScale.cs │ │ ├── Tuple4.cs │ │ ├── BoxLayoutMode.cs │ │ ├── CssBoxAlignment.cs │ │ ├── CssBoxAlignmentExtensions.cs │ │ └── CssPlacement.cs │ ├── PseudoElementNames.cs │ ├── Carbon.Css.csproj │ ├── CssFunctionNames.cs │ ├── Serialization │ │ ├── CssGapConverter.cs │ │ ├── ThicknessConverter.cs │ │ └── CssUnitValueConverter.cs │ └── PseudoClassNames.cs ├── Carbon.Css.Tests │ ├── data │ │ ├── webcat │ │ │ ├── base │ │ │ │ ├── fonts.scss │ │ │ │ ├── clears.scss │ │ │ │ ├── type.scss │ │ │ │ ├── utility.scss │ │ │ │ ├── base.scss │ │ │ │ ├── forms.scss │ │ │ │ └── variables.scss │ │ │ ├── options │ │ │ │ ├── textAlignment.scss │ │ │ │ └── fontScheme.scss │ │ │ ├── components │ │ │ │ ├── zoomer.scss │ │ │ │ └── animations.scss │ │ │ ├── parts │ │ │ │ ├── nav.scss │ │ │ │ ├── footer.scss │ │ │ │ ├── post.scss │ │ │ │ ├── paginator.scss │ │ │ │ └── header.scss │ │ │ ├── all.scss │ │ │ └── pages │ │ │ │ └── about.scss │ │ ├── test1.css │ │ ├── fonts.css │ │ ├── nested.css │ │ ├── mixins.css │ │ └── test53.css │ ├── Extensions │ │ └── CssTokenExtensions.cs │ ├── CompatibilityTableTests.cs │ ├── Values │ │ ├── CssVisibilityTests.cs │ │ ├── CssPlacementTests.cs │ │ ├── CssBoxAlignmentTests.cs │ │ └── CssGapTests.cs │ ├── Helpers │ │ └── TestHelper.cs │ ├── DirectiveTests.cs │ ├── CssSelectorListTests.cs │ ├── Modules │ │ └── MaskingTests.cs │ ├── WriterTests.cs │ ├── SupportsTests.cs │ ├── Carbon.Css.Tests.csproj │ ├── CssFunctionTests.cs │ ├── AutoPrefixTests.cs │ ├── CssPropertyTests.cs │ ├── ResolverTests.cs │ ├── CssUnitNamesTests.cs │ ├── NativeFunctionTests.cs │ ├── PseudoElementTests.cs │ ├── Scss │ │ ├── EachTests.cs │ │ └── ForRuleTests.cs │ ├── CssUnitValueTests.cs │ ├── ContainerRuleTests.cs │ ├── StartingStyleRules.cs │ ├── CssUnitTests.cs │ ├── CssSelectorTests.cs │ ├── CssValueTests.cs │ ├── CssColorTests.cs │ ├── PlaceholderTests.cs │ └── CalcTests.cs ├── Carbon.Css.slnx └── Directory.Build.props ├── .gitignore ├── README.md ├── .github └── workflows │ └── dotnet.yml └── LICENSE /src/Carbon.Css/Ast/Expressions/UnaryOperator.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public enum UnaryOperator 4 | { 5 | Not = 1 // ! 6 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Model/CssUnitFlags.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public enum CssUnitFlags 4 | { 5 | None = 0, 6 | Relative = 1 7 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Values/CssValueSeperator.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public enum CssValueSeparator 4 | { 5 | Comma = 1, 6 | Space = 2 7 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Writer/WriterStyle.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public enum WriterStyle 4 | { 5 | Pretty = 1, 6 | OneRulePerLine = 2 7 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Model/CssFormatting.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public enum CssFormatting 4 | { 5 | Original = 1, 6 | Pretty = 2, 7 | None = 3 8 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Rewriters/ICssTransform.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public interface ICssRewriter 4 | { 5 | IEnumerable Rewrite(CssRule rule); 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | .DS_Store 3 | obj 4 | .vs 5 | Carbon.Color/bin 6 | bin 7 | obj 8 | bin 9 | packages 10 | *.xproj.user 11 | *.lock.json 12 | /build 13 | /release 14 | -------------------------------------------------------------------------------- /src/Carbon.Css/Resolver/IStylesheet.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Carbon.Css; 4 | 5 | public interface IStylesheet 6 | { 7 | void WriteTo(TextWriter writer); 8 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Gradients/IGradient.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Gradients; 2 | 3 | public interface IGradient 4 | { 5 | 6 | } 7 | 8 | // Linear 9 | // Radial 10 | // Conic -------------------------------------------------------------------------------- /src/Carbon.Css/Globals.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | 5 | [assembly:InternalsVisibleTo("Carbon.Css.Tests")] -------------------------------------------------------------------------------- /src/Carbon.Css/Comptability/BrowserPrefixKind.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | [Flags] 4 | public enum BrowserPrefixKind 5 | { 6 | Moz = 1, 7 | Ms = 2, 8 | Webkit = 4 9 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Selectors/IdSelector.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Selectors; 2 | 3 | public sealed class IdSelector : Selector 4 | { 5 | public IdSelector() 6 | : base(CssSelectorType.Id) { } 7 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Values/CssUrl.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | using Parser; 4 | 5 | public sealed class CssUrl(CssToken name, CssValue value) 6 | : CssFunction(name.Text, value) 7 | { 8 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Patching/CssPatcher.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public abstract class CssPatcher 4 | { 5 | public abstract CssPatch Patch(BrowserInfo browser, CssDeclaration declaration); 6 | } 7 | -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Sass/WhileBlock.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class WhileBlock(CssValue condition) 4 | : CssBlock(NodeKind.While) 5 | { 6 | public CssValue Condition { get; } = condition; 7 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Selectors/ClassSelector.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Selectors; 2 | 3 | public sealed class ClassSelector : Selector 4 | { 5 | public ClassSelector() 6 | : base(CssSelectorType.Class) { } 7 | } 8 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/base/fonts.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | @font-face { 4 | font-family: 'frontend'; 5 | src: url('https://static.cmcdn.net/kits/142/fonts/frontend/frontend.woff'); 6 | font-weight: normal; 7 | font-style: normal; 8 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Parser/Exceptions/UnbalancedBlock.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Parser; 2 | 3 | public sealed class UnbalancedBlock(CssToken startToken) 4 | : SyntaxException("The block is unclosed, '}' expected", startToken.Position) 5 | { 6 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Selectors/PseudoElementSelector.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Selectors; 2 | 3 | public sealed class PseudoElementSelector : Selector 4 | { 5 | public PseudoElementSelector() 6 | : base(CssSelectorType.PseudoElement) { } 7 | } 8 | -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Rules/CssRule.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public abstract class CssRule : CssBlock 4 | { 5 | public CssRule() 6 | : base(NodeKind.Rule) 7 | { } 8 | 9 | public abstract RuleType Type { get; } 10 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Helpers/LineInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Helpers; 2 | 3 | public readonly struct LineInfo(int number, string text) 4 | { 5 | public readonly int Number { get; } = number; 6 | 7 | public readonly string Text { get; } = text; 8 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Patching/CssPatch.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public readonly struct CssPatch(string name, CssValue value) 4 | { 5 | public readonly string Name { get; } = name; 6 | 7 | public readonly CssValue Value { get; } = value; 8 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Comptability/BrowserType.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | [Flags] 4 | public enum BrowserType 5 | { 6 | Unknown = 0, 7 | Chrome = 1, 8 | Edge = 2, 9 | Firefox = 4, // -moz 10 | Safari = 8 // -webkit 11 | } -------------------------------------------------------------------------------- /src/Carbon.Css.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Rules/PageRule.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class PageRule(CssSelector? selector = null) : CssRule 4 | { 5 | public override RuleType Type => RuleType.Page; 6 | 7 | public CssSelector? Selector { get; } = selector; 8 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Model/CssBlockFlags.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | [Flags] 4 | public enum CssBlockFlags 5 | { 6 | HasIncludes = 1 << 0, 7 | HasNestedAtRule = 1 << 1, 8 | HasChildBlocks = 1 << 2, 9 | IsComplex = 1 << 3 10 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Values/CssInterpolatedString.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | // #{$name} 4 | public sealed class CssInterpolatedString(CssValue expression) 5 | : CssValue(NodeKind.InterpolatedString) 6 | { 7 | public CssValue Expression { get; } = expression; 8 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Model/ICssResolver.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Carbon.Css; 4 | 5 | public interface ICssResolver 6 | { 7 | string ScopedPath { get; } // subpath ? 8 | 9 | Stream Open(string absolutePath); 10 | 11 | // TODO: GetStreamAsync 12 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Rules/MediaRule.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class MediaRule(TokenList queryList) : CssRule 4 | { 5 | public override RuleType Type => RuleType.Media; 6 | 7 | // QueryList? 8 | 9 | public TokenList Queries { get; } = queryList; 10 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/Extensions/CssTokenExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Parser.Tests; 2 | 3 | public static class CssTokenExtensions 4 | { 5 | public static (CssTokenKind kind, string text) AsTuple(this CssToken token) 6 | { 7 | return (token.Kind, token.Text); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Rules/ContainerRule.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class ContainerRule(TokenList queryList) : CssRule 4 | { 5 | public override RuleType Type => RuleType.Container; 6 | 7 | // QueryList? 8 | 9 | public TokenList Queries { get; } = queryList; 10 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Model/CssParameter.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public readonly struct CssParameter( 4 | string name, 5 | CssValue? defaultValue = null) 6 | { 7 | public string Name { get; } = name; 8 | 9 | public CssValue? DefaultValue { get; } = defaultValue; 10 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/options/textAlignment.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | @if $textAlignment == center { 4 | #projects .info { 5 | text-align: center; 6 | position: relative; 7 | left: -50px; 8 | } 9 | 10 | .moreLink:before, 11 | .moreLink:after { display: none; } 12 | } -------------------------------------------------------------------------------- /src/Carbon.Css/_/CssVisibility.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Carbon.Css; 4 | 5 | [JsonConverter(typeof(JsonStringEnumConverter))] 6 | public enum CssVisibility : byte 7 | { 8 | Visible = 1, 9 | Hidden = 2, 10 | Collapse = 3 11 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Parser/Exceptions/UnexpectedModeChange.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Parser; 2 | 3 | public sealed class UnexpectedModeChange(LexicalMode currentMode, LexicalMode leavingMode, int position) 4 | : SyntaxException($"Unexpected mode change. Expected '{leavingMode}'. Was {currentMode}.", position) 5 | { 6 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Sass/EachBlock.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class EachBlock(IReadOnlyList variables, CssValue enumerable) : CssBlock(NodeKind.Each) 4 | { 5 | public IReadOnlyList Variables { get; } = variables; 6 | 7 | public CssValue Enumerable { get; } = enumerable; 8 | } -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 4 | true 5 | enable 6 | $(MSBuildThisFileDirectory)/../artifacts 7 | IDE0130 8 | 9 | -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Expressions/UnaryExpression.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class UnaryExpression( 4 | UnaryOperator op, 5 | CssNode operand) : CssValue(NodeKind.Expression) 6 | { 7 | public UnaryOperator Operator { get; } = op; 8 | 9 | public CssNode Operand { get; } = operand; 10 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Rules/UnknownRule.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class UnknownRule(string name, TokenList? selector) : CssRule 4 | { 5 | public override RuleType Type => RuleType.Unknown; 6 | 7 | public string Name { get; } = name; 8 | 9 | public TokenList? Text { get; } = selector; 10 | } 11 | -------------------------------------------------------------------------------- /src/Carbon.Css/Patching/PrefixNamePatcher.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class PrefixNamePatcher : CssPatcher 4 | { 5 | public override CssPatch Patch(BrowserInfo browser, CssDeclaration declaration) => new ( 6 | name : browser.Prefix + declaration.Name, 7 | value : declaration.Value 8 | ); 9 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/CompatibilityTableTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class CompatibilityTableTests 4 | { 5 | [Fact] 6 | public void TestIsDefined() 7 | { 8 | Assert.False(default(CompatibilityTable).IsDefined); 9 | Assert.True(new CompatibilityTable(1, 2, 3, 4).IsDefined); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Rules/SupportsRule.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class SupportsRule(TokenList queryList) : CssRule 4 | { 5 | public override RuleType Type => RuleType.Supports; 6 | 7 | // | font-format() 8 | // | font-tech() 9 | // | selector 10 | 11 | public TokenList Queries { get; } = queryList; 12 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Gradients/ConicGradient.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Gradients; 2 | 3 | /* 4 | public struct ConicGradient : IGradient 5 | { 6 | } 7 | */ 8 | 9 | // conic-gradient(#f06, gold) 10 | // conic-gradient(at 50% 50%, #f06, gold); 11 | // conic-gradient(#f06 0deg, gold 1turn); 12 | // conic-gradient(hsl(0,0%,75%), hsl(0,0%,25%)); -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Selectors/TagSelector.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Carbon.Css.Selectors; 4 | 5 | public sealed class TagSelector : Selector 6 | { 7 | [SetsRequiredMembers] 8 | public TagSelector(string value) 9 | : base(CssSelectorType.Tag) 10 | { 11 | Text = value; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Selectors/CombinatorType.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Selectors; 2 | 3 | public enum CombinatorType : int 4 | { 5 | None = '\0', 6 | Descendant = ' ', // ' ' 7 | Child = '>', // '>' 8 | AdjacentSibling = '+', // '+' | DirectAdjacent 9 | SubsequentSibling = '~', // '~' | IndirectAdjacent 10 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/base/clears.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | .projectExpanded:after, 4 | .projectDetails:after, 5 | .projectMore:after, 6 | .aboutExtended:after, 7 | .row:after, 8 | li:after, 9 | .clear:after { 10 | display: block; 11 | content: "."; 12 | clear: both; 13 | font-size: 0; 14 | line-height: 0; 15 | height: 0; 16 | overflow: hidden; 17 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Patching/PrefixNameAndValuePatcher.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class PrefixNameAndValuePatcher : CssPatcher 4 | { 5 | public override CssPatch Patch(BrowserInfo browser, CssDeclaration declaration) => new ( 6 | name : browser.Prefix + declaration.Name, 7 | value : PatchFactory.PatchValue(declaration.Value, browser) 8 | ); 9 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/Values/CssVisibilityTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class CssVisibilityTests 4 | { 5 | [Fact] 6 | public void ValuesDontChange() 7 | { 8 | Assert.Equal(1, (byte)CssVisibility.Visible); 9 | Assert.Equal(2, (byte)CssVisibility.Hidden); 10 | Assert.Equal(3, (byte)CssVisibility.Collapse); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Carbon.Css/_/CssTextTransform.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Carbon.Css; 4 | 5 | [JsonConverter(typeof(JsonStringEnumConverter))] 6 | public enum CssTextTransform 7 | { 8 | None = 0, 9 | Capitalize = 1, 10 | LowerCase = 2, 11 | UpperCase = 3, 12 | FullWidth = 4, 13 | FullSizeKana = 5 14 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Rules/CharsetRule.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class CharsetRule(ReadOnlyMemory text) : CssRule 4 | { 5 | public override RuleType Type => RuleType.Charset; 6 | 7 | public ReadOnlyMemory Encoding { get; } = text; 8 | } 9 | 10 | // @charset "UTF-8"; 11 | 12 | // https://www.iana.org/assignments/character-sets/character-sets.xhtml -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Selectors/CssSelectorType.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Selectors; 2 | 3 | public enum CssSelectorType 4 | { 5 | None = 0, 6 | Tag, // AKA type 7 | Id, // #x 8 | Class, // .x 9 | Attribute, 10 | Universal, // * 11 | PseudoClass, 12 | PseudoElement, 13 | HasScope, // :has() 14 | NestingParent, // & 15 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Values/CssNumericType.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Carbon.Css.Ast.Values; 4 | 5 | [JsonConverter(typeof(JsonStringEnumConverter))] 6 | public enum CssNumericType 7 | { 8 | Length = 1, 9 | Angle = 2, 10 | Time = 3, 11 | Frequency = 4, 12 | Resolution = 5, 13 | Flex = 6, 14 | Percent = 7 15 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/CssComment.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class CssComment : CssNode 4 | { 5 | public CssComment(string text) 6 | : this(text.AsMemory()) { } 7 | 8 | public CssComment(ReadOnlyMemory text) 9 | : base(NodeKind.Comment) 10 | { 11 | Text = text; 12 | } 13 | 14 | public ReadOnlyMemory Text { get; } 15 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Values/CssUndefined.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class CssUndefined(string variableName) 4 | : CssValue(NodeKind.Undefined) 5 | { 6 | public string VariableName { get; } = variableName; 7 | 8 | public override CssUndefined CloneNode() => new(VariableName); 9 | 10 | public override string ToString() => $"/* ${VariableName} undefined */"; 11 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/Helpers/TestHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public static class TestHelper 4 | { 5 | public static FileInfo GetTestFile(string name) 6 | { 7 | string b = new DirectoryInfo(AppContext.BaseDirectory).Parent.Parent.Parent.Parent.FullName; 8 | 9 | return new FileInfo(Path.Combine(b, "src", "Carbon.Css.Tests", "data", name)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/CssDirective.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class CssDirective(string name, string? value) : CssNode(NodeKind.Directive) 4 | { 5 | public string Name { get; } = name; 6 | 7 | public string? Value { get; } = value; 8 | } 9 | 10 | /* 11 | EXAMPLES: 12 | //= support IE 11+ 13 | //= support Safari 5.1+ 14 | //= support Chrome 20+ 15 | //= inline < 1KiB 16 | */ 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Carbon.Css 2 | 3 | A general purpose CSS parser, auto-prefixer, and SCSS compiler for .NET Standard. 4 | 5 | 6 | # Usage 7 | 8 | ``` 9 | var css = StyleSheet.Parse(@" 10 | //= support Safari 5+ 11 | $backgroundColor: #000; 12 | 13 | html { 14 | background: $backgroundColor; 15 | font-size: 14px; 16 | } 17 | 18 | "); 19 | 20 | var writer = new StringWriter(); 21 | 22 | css.WriteTo(writer); 23 | 24 | ``` 25 | -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/ICssNumericValue.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Ast; 2 | 3 | public interface ICssNumericValue 4 | { 5 | // angle, flex, frequency, length, resolution, percent, percentHint, or time 6 | // public NodeKind Type { get; } 7 | 8 | public double Value { get; } 9 | 10 | // to Converts value into another one with the specified unit. 11 | 12 | } 13 | 14 | // https://drafts.css-houdini.org/css-typed-om/#dom-cssnumericvalue-type -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Sass/MixinNode.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class MixinNode( 4 | string name, 5 | IReadOnlyList parameters) : CssBlock(NodeKind.Mixin) 6 | { 7 | public string Name { get; } = name; 8 | 9 | public IReadOnlyList Parameters { get; } = parameters; 10 | } 11 | 12 | /* 13 | @mixin left($dist) { 14 | float: left; 15 | margin-left: $dist; 16 | } 17 | */ 18 | -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/IncludeNode.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class IncludeNode( 4 | string name, 5 | CssValue? args) : CssNode(NodeKind.Include) 6 | { 7 | public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); 8 | 9 | public CssValue? Args { get; } = args; 10 | 11 | public override IncludeNode CloneNode() => new(Name, Args); 12 | } 13 | 14 | // @include box-emboss(0.8, 0.05); -------------------------------------------------------------------------------- /src/Carbon.Css/_/CssPosition.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Carbon.Css; 4 | 5 | // https://www.w3.org/TR/css-typed-om-1/#csspositionvalue 6 | public readonly struct CssPosition(CssUnitValue x, CssUnitValue y) 7 | { 8 | // e.g. 100px 9 | [DataMember(Name = "x")] 10 | public readonly CssUnitValue X { get; } = x; 11 | 12 | [DataMember(Name = "y")] 13 | public readonly CssUnitValue Y { get; } = y; 14 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Helpers/ThrowHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Carbon.Css.Helpers; 4 | 5 | internal static class ThrowHelper 6 | { 7 | [DoesNotReturn] 8 | public static void RecursionDetected() 9 | { 10 | throw new Exception("Recursion detected"); 11 | } 12 | 13 | [DoesNotReturn] 14 | public static void SelfReferencing() 15 | { 16 | throw new Exception("Self referencing"); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/components/zoomer.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | #zoomer { 4 | transition: background 0.2s ease; 5 | background: $maskColor; 6 | 7 | &.closing { 8 | pointer-events: none; 9 | } 10 | 11 | &.closed { 12 | pointer-events: none; 13 | transition: background 0.1s ease, visibility 0 ease 0.2s; 14 | background: transparent; 15 | } 16 | 17 | user-select: none; 18 | } 19 | 20 | .zoomable { 21 | cursor: zoom-in; 22 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Sass/IfBlock.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class IfBlock(CssValue condition) : CssBlock(NodeKind.If) 4 | { 5 | public CssValue Condition { get; } = condition; 6 | 7 | public override IfBlock CloneNode() 8 | { 9 | var block = new IfBlock(Condition); 10 | 11 | foreach (var child in _children) 12 | { 13 | block.Add(child.CloneNode()); 14 | } 15 | 16 | return block; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Selectors/AttributeSelector.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Selectors; 2 | 3 | public sealed class AttributeSelector : Selector 4 | { 5 | public AttributeSelector() 6 | : base(CssSelectorType.Attribute) { } 7 | 8 | // todo: breakout attribute name, and value 9 | } 10 | 11 | /* 12 | public enum AttributeMatchType 13 | { 14 | Exact, 15 | Set, 16 | List, 17 | Hyphen, 18 | Contain, // E[foo*="bar"] 19 | Begin, // E[foo^="bar"] 20 | End // E[foo$="bar"] 21 | } 22 | */ -------------------------------------------------------------------------------- /src/Carbon.Css/Parser/Tokenizer/LexicalMode.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Parser; 2 | 3 | public enum LexicalMode 4 | { 5 | Unknown = 0, 6 | Rule = 1, 7 | Block = 2, 8 | Value = 3, 9 | Declaration = 4, 10 | Selector = 5, 11 | Assignment = 6, 12 | Function = 7, 13 | Symbol = 10, 14 | Unit = 11, 15 | InterpolatedString = 13, 16 | Mixin = 20 17 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Rules/StartingStyleRule.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class StartingStyleRule : CssRule 4 | { 5 | public override RuleType Type => RuleType.StartingStyle; 6 | 7 | public override StartingStyleRule CloneNode() 8 | { 9 | var clone = new StartingStyleRule() { 10 | Flags = Flags 11 | }; 12 | 13 | foreach (var child in Children) 14 | { 15 | clone.Add(child.CloneNode()); 16 | } 17 | 18 | return clone; 19 | } 20 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Helpers/SourceLocation.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Carbon.Css.Helpers; 4 | 5 | public readonly struct SourceLocation(int position, int line, int column) 6 | { 7 | // 0 based 8 | public int Position { get; } = position; 9 | 10 | // 1 based 11 | public int Line { get; } = line; 12 | 13 | // 1 based 14 | public int Column { get; } = column; 15 | 16 | public readonly override string ToString() => string.Create(CultureInfo.InvariantCulture, $"({Line},{Column})"); 17 | } -------------------------------------------------------------------------------- /src/Carbon.Css/_/FontSrcValue.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | internal readonly struct FontSrcValue(string url, string format) 4 | { 5 | public readonly string Url { get; } = url; 6 | 7 | public readonly string Format { get; } = format; 8 | 9 | public readonly override string ToString() => $"url('{Url}') format('{Format}')"; 10 | } 11 | 12 | /* 13 | src: url('../fonts/cm-billing-webfont.eot?#iefix') format('embedded-opentype'), 14 | url('../fonts/cm-billing-webfont.woff') format('woff'); 15 | */ 16 | -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Values/CssVariable.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | using Parser; 4 | 5 | public sealed class CssVariable(string text) : CssValue(NodeKind.Variable) 6 | { 7 | public CssVariable(CssToken token) 8 | : this(token.Text) 9 | { } 10 | 11 | public string Symbol { get; } = text; 12 | 13 | public override CssVariable CloneNode() => new(Symbol); 14 | 15 | public override string ToString() => "$" + Symbol; 16 | } 17 | 18 | // Variable (mathematics), a symbol that represents a quantity in a mathematical expression -------------------------------------------------------------------------------- /src/Carbon.Css/Gradients/AngularColorStop.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | using Carbon.Color; 4 | 5 | namespace Carbon.Css.Gradients; 6 | 7 | [method: JsonConstructor] 8 | public readonly struct AngularColorStop(Rgba32 color, double position, double angle = 0) 9 | { 10 | [JsonPropertyName("color")] 11 | public Rgba32 Color { get; } = color; 12 | 13 | [JsonPropertyName("position")] 14 | public double Position { get; } = position; 15 | 16 | [JsonPropertyName("angle")] 17 | public double Angle { get; } = angle; 18 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Rules/KeyframesRule.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class KeyframesRule(string name) : CssRule 4 | { 5 | public override RuleType Type => RuleType.Keyframes; 6 | 7 | public string Name { get; } = name; 8 | 9 | // TODO: Keyframes 10 | 11 | public override CssNode CloneNode() 12 | { 13 | var rule = new KeyframesRule(Name); 14 | 15 | foreach (var child in Children) 16 | { 17 | rule.Add(child.CloneNode()); 18 | } 19 | 20 | return rule; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Resolver/Include.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Carbon.Css.Resolver; 4 | 5 | internal readonly struct Include(FileInfo file) 6 | { 7 | private readonly FileInfo _file = file; 8 | 9 | public DateTime Modified => _file.LastWriteTime; 10 | 11 | public void WriteTo(TextWriter writer) 12 | { 13 | string? line; 14 | 15 | using var reader = _file.OpenText(); 16 | 17 | while ((line = reader.ReadLine()) is not null) 18 | { 19 | writer.WriteLine(line); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Parser/Exceptions/SyntaxException.cs: -------------------------------------------------------------------------------- 1 | using Carbon.Css.Helpers; 2 | 3 | namespace Carbon.Css.Parser; 4 | 5 | public class SyntaxException(string message, int position = 0) 6 | : Exception(message) 7 | { 8 | public int Position { get; } = position; 9 | 10 | public SourceLocation Location { get; set; } 11 | 12 | public IList? Lines { get; set; } 13 | 14 | public static SyntaxException UnexpectedEOF(string context) 15 | { 16 | return new SyntaxException($"Unexpected EOF reading '{context}'."); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/DirectiveTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class DirectiveTests 4 | { 5 | [Fact] 6 | public void ParsePartial() 7 | { 8 | var sheet = StyleSheet.Parse( 9 | """ 10 | //= partial 11 | div { color: blue; } 12 | 13 | """); // Prevents standalone compilation 14 | 15 | Assert.Equal( 16 | """ 17 | div { 18 | color: blue; 19 | } 20 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Values/CssAssignment.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | using Parser; 4 | 5 | public sealed class CssAssignment : CssNode 6 | { 7 | public CssAssignment(string name, CssValue value) 8 | : base(NodeKind.Assignment) 9 | { 10 | Name = name; 11 | Value = value; 12 | } 13 | 14 | public CssAssignment(CssToken name, CssValue value) 15 | : base(NodeKind.Assignment) 16 | { 17 | Name = name.Text; 18 | Value = value; 19 | } 20 | 21 | public string Name { get; } 22 | 23 | public CssValue Value { get; } 24 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/CssSelectorListTests.cs: -------------------------------------------------------------------------------- 1 | 2 | using Carbon.Css.Selectors; 3 | 4 | namespace Carbon.Css.Parser.Tests; 5 | 6 | public class CssSelectorListTests 7 | { 8 | [Fact] 9 | public void CanParse() 10 | { 11 | var selector = SelectorList.Parse("h1, h2, h3"); 12 | 13 | var h1 = selector[0]; 14 | var h2 = selector[1]; 15 | var h3 = selector[2]; 16 | 17 | Assert.Equal("h1", h1.ToString()); 18 | Assert.Equal("h2", h2.ToString()); 19 | Assert.Equal("h3", h3.ToString()); 20 | 21 | Assert.Equal("h1, h2, h3", selector.ToString()); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/base/type.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | p { margin: 0; } 4 | 5 | h1, h2, h3, h4, h5 { 6 | padding: 0; 7 | margin: 0; 8 | line-height: 1.3; 9 | } 10 | 11 | h1 { 12 | font-size: 33px; 13 | font-weight: 400; 14 | } 15 | 16 | h2 { 17 | font-size: 22px; 18 | font-weight: 400; 19 | } 20 | 21 | h3 { 22 | font-size: 20px; 23 | font-weight: 400; 24 | } 25 | 26 | h4 { 27 | margin: 0 0 15px; 28 | } 29 | 30 | blockquote { 31 | quotes: none; 32 | margin: 0; 33 | padding: 0; 34 | } 35 | 36 | blockquote:before, 37 | blockquote:after { 38 | content: ''; 39 | content: none; 40 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Expressions/BinaryOperator.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public enum BinaryOperator 4 | { 5 | // Logical 6 | And = 30, // && 7 | Or = 31, // || 8 | 9 | // Equality 10 | Eq = 40, // == 11 | NotEquals = 41, // != (<>) 12 | 13 | // Relational 14 | Gt = 50, // > 15 | Gte = 51, // >= 16 | Lt = 52, // < 17 | Lte = 53, // <= 18 | 19 | // Math 20 | Divide = 60, // / 21 | Multiply = 61, // * 22 | Add = 62, // + 23 | Subtract = 63, // - 24 | Mod = 64 // % 25 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Sass/ForBlock.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class ForBlock( 4 | CssVariable variable, 5 | CssValue start, 6 | CssValue end, 7 | bool isInclusive = true) : CssBlock(NodeKind.For) 8 | { 9 | public CssVariable Variable { get; } = variable; 10 | 11 | public CssValue Start { get; } = start; 12 | 13 | public CssValue End { get; } = end; 14 | 15 | // If through 16 | public bool IsInclusive { get; } = isInclusive; 17 | } 18 | 19 | // @for $var from through 20 | // @for $var from to 21 | 22 | // @for $i from 1 through 4 -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Rules/FontFaceRule.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class FontFaceRule : CssRule 4 | { 5 | public override RuleType Type => RuleType.FontFace; 6 | } 7 | 8 | 9 | /* 10 | Internet Explorer only supports EOT 11 | Mozilla browsers support OTF and TTF 12 | Safari and Opera support OTF, TTF and SVG 13 | Chrome supports TTF and SVG. 14 | */ 15 | 16 | /* 17 | "woff" WOFF (Web Open Font Format) .woff 18 | "truetype" TrueType .ttf 19 | "opentype" OpenType .ttf, .otf 20 | "embedded-opentype" Embedded OpenType .eot 21 | "svg" SVG Font .svg, .svgz 22 | */ 23 | -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Trivia.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | 5 | using Carbon.Css.Parser; 6 | 7 | namespace Carbon.Css; 8 | 9 | public sealed class Trivia : List 10 | { 11 | [SkipLocalsInit] 12 | public override string ToString() 13 | { 14 | var sb = new ValueStringBuilder(stackalloc char[32]); 15 | 16 | foreach (ref readonly CssToken token in CollectionsMarshal.AsSpan(this)) 17 | { 18 | sb.Append(token.Text); 19 | } 20 | 21 | return sb.ToString(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Rules/RuleType.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Carbon.Css; 4 | 5 | [JsonConverter(typeof(JsonStringEnumConverter))] 6 | public enum RuleType 7 | { 8 | Unknown = 0, 9 | Style = 1, 10 | Charset = 2, 11 | Import = 3, 12 | Media = 4, 13 | FontFace = 5, 14 | Page = 6, 15 | Keyframes = 7, 16 | Keyframe = 8, 17 | Namespace = 10, 18 | CounterStyle = 11, 19 | Supports = 12, 20 | Document = 13, 21 | FontFeatureValues = 14, 22 | Viewport = 15, 23 | Region = 16, 24 | Container = 17, 25 | StartingStyle = 18 26 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/base/utility.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | .utility { 4 | height: 100%; 5 | font-size: 18px !important; 6 | line-height: 1.65 !important; 7 | text-align: center !important; 8 | 9 | .centerWrap { 10 | display: table; 11 | width: 100%; 12 | height: 60%; 13 | } 14 | 15 | .centered { 16 | display: table-cell; 17 | vertical-align: middle; 18 | } 19 | } 20 | 21 | .utility #wrapper { 22 | position: absolute; 23 | top: 0; 24 | right: 0; 25 | bottom: 0; 26 | left: 0; 27 | } 28 | 29 | .utility h1 { 30 | font-size: 1.6em !important; 31 | margin-bottom: 1.2em !important; 32 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/CssNode.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Carbon.Css; 4 | 5 | public abstract class CssNode(NodeKind kind, CssNode? parent = null) 6 | { 7 | [JsonIgnore] 8 | public NodeKind Kind { get; } = kind; 9 | 10 | [JsonIgnore] 11 | public CssNode? Parent { get; set; } = parent; 12 | 13 | [JsonIgnore] 14 | internal Trivia? Leading { get; init; } 15 | 16 | [JsonIgnore] 17 | public Trivia? Trailing { get; set; } 18 | 19 | public virtual CssNode CloneNode() 20 | { 21 | throw new NotImplementedException($"{GetType().Name} does not implement Clone"); 22 | } 23 | } -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | defaults: 14 | run: 15 | working-directory: ./src 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v4 21 | with: 22 | dotnet-version: 9.0.x 23 | - name: Restore dependencies 24 | run: dotnet restore 25 | - name: Build 26 | run: dotnet build --no-restore 27 | - name: Test 28 | run: dotnet test --no-build --verbosity normal 29 | -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Expressions/BinaryExpression.cs: -------------------------------------------------------------------------------- 1 | using Carbon.Css.Parser; 2 | 3 | namespace Carbon.Css; 4 | 5 | public sealed class BinaryExpression( 6 | CssValue left, 7 | CssToken op, 8 | CssValue right) : CssValue(NodeKind.Expression) 9 | { 10 | public CssValue Left { get; } = left; 11 | 12 | public CssValue Right { get; } = right; 13 | 14 | public CssToken OperatorToken { get; } = op; 15 | 16 | public BinaryOperator Operator => (BinaryOperator)OperatorToken.Kind; 17 | 18 | public override BinaryExpression CloneNode() => new(Left, OperatorToken, Right); 19 | } 20 | 21 | // ||, &&, ==, != 22 | // +, -, *, /, % -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/Modules/MaskingTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class MaskingTests 4 | { 5 | [Fact] 6 | public void A() 7 | { 8 | var sheet = StyleSheet.Parse( 9 | """ 10 | //= support Safari >= 5 11 | 12 | div { 13 | mask-image: url('mask.svg'); 14 | } 15 | """); 16 | 17 | Assert.Equal( 18 | """ 19 | div { 20 | -webkit-mask-image: url('mask.svg'); 21 | mask-image: url('mask.svg'); 22 | } 23 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Selectors/PseudoClassSelector.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Selectors; 2 | 3 | public sealed class PseudoClassSelector : Selector 4 | { 5 | public PseudoClassSelector() 6 | : base(CssSelectorType.PseudoClass) { } 7 | } 8 | 9 | 10 | // CSSSelector::PseudoClass::FirstChild 11 | // CSSSelector::PseudoClass::FirstOfType 12 | // CSSSelector::PseudoClass::LastChild 13 | // CSSSelector::PseudoClass::LastOfType 14 | // CSSSelector::PseudoClass::OnlyChild 15 | // CSSSelector::PseudoClass::OnlyOfType 16 | // CSSSelector::PseudoClass::NthChild 17 | // CSSSelector::PseudoClass::NthOfType 18 | // CSSSelector::PseudoClass::NthLastChild 19 | // CSSSelector::PseudoClass::NthLastOfType; -------------------------------------------------------------------------------- /src/Carbon.Css/Comptability/CompatibilityTable.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Carbon.Css; 5 | 6 | public readonly struct CompatibilityTable( 7 | float chrome = 0, 8 | float edge = 0, 9 | float firefox = 0, 10 | float safari = 0) 11 | { 12 | public float Chrome { get; } = chrome; 13 | 14 | public float Edge { get; } = edge; 15 | 16 | public float Firefox { get; } = firefox; 17 | 18 | public float Safari { get; } = safari; 19 | 20 | public bool IsDefined => Unsafe.BitCast(this) != Vector4.Zero; // Chrome > 0 || Firefox > 0 || Edge > 0 || Safari > 0; 21 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Values/CssReference.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | using Parser; 4 | 5 | public sealed class CssReference : CssValue 6 | { 7 | public CssReference(string name) 8 | : base(NodeKind.Reference) 9 | { 10 | Name = name; 11 | } 12 | 13 | public CssReference(CssToken name) 14 | : base(NodeKind.Reference) 15 | { 16 | Name = name.Text; 17 | } 18 | 19 | public CssReference(string name, CssSequence value) 20 | : base(NodeKind.Reference) 21 | { 22 | Name = name; 23 | Value = value; 24 | } 25 | 26 | public string Name { get; } 27 | 28 | public CssSequence? Value { get; set; } 29 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Comptability/CursorCompatibility.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class CursorCompatibility : CssCompatibility 4 | { 5 | public CursorCompatibility() 6 | : base() { } 7 | 8 | public override bool HasPatch(CssDeclaration declaration, in BrowserInfo browser) 9 | { 10 | return CssCursor.NeedsPatch(declaration.Value.ToString()!, browser); 11 | } 12 | 13 | public override CssPatch GetPatch(CssDeclaration declaration, in BrowserInfo browser) 14 | { 15 | return new CssPatch(declaration.Name, new CssString(browser.Prefix + declaration.Value.ToString())); 16 | } 17 | 18 | public override bool HasPatches => true; 19 | } 20 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/options/fontScheme.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | input, 4 | textarea, 5 | button { font-family: $fontFamily; } 6 | 7 | carbon-player time { 8 | font-family: $fontFamily; 9 | } 10 | 11 | @if $fontScheme == sans { 12 | body { 13 | -webkit-font-smoothing: antialiased; 14 | font-weight: 400; 15 | } 16 | 17 | .contactInfo h3 { font-weight: 400; } 18 | 19 | nav > ul { letter-spacing: 0; } 20 | 21 | carbon-player time { font-weight: bold; } 22 | } 23 | 24 | @if $fontScheme == serif { 25 | .row h4 { font-size: 16px; } 26 | 27 | a { 28 | box-shadow: inset 0 -2px 0px $backgroundColor, inset 0 -3px 0px $linkUnderlineColor; 29 | border-bottom: none; 30 | } 31 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Model/CssMap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace Carbon.Css; 4 | 5 | public sealed class CssMap : CssValue, IEnumerable> 6 | { 7 | private readonly Dictionary _map = []; 8 | 9 | public CssMap() 10 | : base(NodeKind.Map) { } 11 | 12 | public void Add(string key, CssValue value) 13 | { 14 | _map.Add(key, value); 15 | } 16 | 17 | public CssValue this[string key] => _map[key]; 18 | 19 | public int Count => _map.Count; 20 | 21 | public IEnumerator> GetEnumerator() => _map.GetEnumerator(); 22 | 23 | IEnumerator IEnumerable.GetEnumerator() => _map.GetEnumerator(); 24 | } 25 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/WriterTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Carbon.Css.Tests; 4 | 5 | public class WriterTests 6 | { 7 | [Fact] 8 | public void WriteStyle() 9 | { 10 | var pieceStyle = new StyleRule("#piece_1") { 11 | { "max-width", "960px" } 12 | }; 13 | 14 | var sb = new StringBuilder(); 15 | 16 | using (var output = new StringWriter(sb)) 17 | { 18 | pieceStyle.WriteTo(output); 19 | } 20 | 21 | Assert.Equal( 22 | """ 23 | #piece_1 { 24 | max-width: 960px; 25 | } 26 | """.ReplaceLineEndings(Environment.NewLine), sb.ToString()); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Selectors/SelectorList.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | using Carbon.Css.Parser; 4 | 5 | namespace Carbon.Css.Selectors; 6 | 7 | internal sealed class SelectorList : List 8 | { 9 | public static SelectorList Parse(string text) 10 | { 11 | using var parser = new CssParser(text); 12 | 13 | return parser.ReadSelectorList(); 14 | } 15 | 16 | public override string ToString() 17 | { 18 | var sb = new ValueStringBuilder(128); 19 | 20 | for (int i = 0; i < Count; i++) 21 | { 22 | if (i > 0) 23 | { 24 | sb.Append(", "); 25 | } 26 | 27 | this[i].WriteTo(ref sb); 28 | } 29 | 30 | return sb.ToString(); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Carbon.Css/_/CssScale.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | internal readonly struct CssScale(CssValue x, CssValue y, CssValue z) : IEquatable 4 | { 5 | // e.g. 100px 6 | public CssValue X { get; } = x; 7 | 8 | public CssValue Y { get; } = y; 9 | 10 | public CssValue Z { get; } = z; 11 | 12 | public bool Equals(CssScale other) 13 | { 14 | return X.Equals(other.X) 15 | && Y.Equals(other.Y) 16 | && Z.Equals(other.Z); 17 | } 18 | 19 | public override bool Equals(object? obj) 20 | { 21 | return obj is CssScale other && Equals(other); 22 | } 23 | 24 | public override int GetHashCode() 25 | { 26 | return HashCode.Combine(X, Y, Z); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Values/CssBoolean.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Carbon.Css; 4 | 5 | public sealed class CssBoolean(bool value) : CssValue(NodeKind.Boolean) 6 | { 7 | public static readonly CssBoolean True = new(true); 8 | public static readonly CssBoolean False = new(false); 9 | 10 | public bool Value { get; } = value; 11 | 12 | public override CssBoolean CloneNode() => new(Value); 13 | 14 | public override string ToString() => Value ? "true" : "false"; 15 | 16 | internal override void WriteTo(scoped ref ValueStringBuilder sb) 17 | { 18 | sb.Append(Value ? "true" : "false"); 19 | } 20 | 21 | internal static CssBoolean Get(bool value) 22 | { 23 | return value ? True : False; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Helpers/StringBuilderCache.cs: -------------------------------------------------------------------------------- 1 | // Based on .NET Source code 2 | 3 | namespace System.Text; 4 | 5 | internal static class StringBuilderCache 6 | { 7 | [ThreadStatic] 8 | static StringBuilder? cachedInstance; 9 | 10 | public static StringBuilder Acquire() 11 | { 12 | var sb = cachedInstance; 13 | 14 | if (sb is null) 15 | { 16 | return new StringBuilder(256); 17 | } 18 | 19 | sb.Length = 0; 20 | 21 | cachedInstance = null; 22 | 23 | return sb; 24 | } 25 | 26 | public static string ExtractAndRelease(StringBuilder sb) 27 | { 28 | var text = sb.ToString(); 29 | 30 | cachedInstance = sb; 31 | 32 | return text; 33 | } 34 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Gradients/LinearGradientDirection.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Carbon.Css.Gradients; 4 | 5 | [JsonConverter(typeof(JsonStringEnumConverter))] 6 | public enum LinearGradientDirection 7 | { 8 | None = 0, 9 | 10 | Top = 1, // to top | 0deg 11 | Bottom = 2, // to bottom | 180deg 12 | Left = 3, // to left | 270deg 13 | Right = 4, // to right | 90deg 14 | 15 | // Specify the corner the line goes toward 16 | // The angle is calculated on the aspect ratio of the containing box 17 | TopLeft = 5, 18 | TopRight = 6, 19 | BottomLeft = 7, 20 | BottomRight = 8 21 | } 22 | 23 | // (top | bottom) (left | right) -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/parts/nav.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | nav { 4 | display: block; 5 | margin: 0 -5px; 6 | 7 | > ul { 8 | width: 100%; 9 | font-size: 14px; 10 | letter-spacing: 0.08em; 11 | line-height: 2.5; 12 | 13 | > li { 14 | position: relative; 15 | display: inline-block; 16 | margin: 0 5px 0; 17 | 18 | > a { 19 | display: inline-block; 20 | border: none; 21 | box-shadow: 0 0 0 1px $borderColor !important; 22 | height: 40px; 23 | line-height: if($fontScheme == sans, 40px, 43px); 24 | padding: 0 25px; 25 | font-weight: 400; 26 | transition: all 0.2s ease-in-out; 27 | 28 | &:hover { box-shadow: 0 0 0 1px rgba($accentColor, 0.5) !important; } 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Carbon.Css/PseudoElementNames.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Frozen; 2 | 3 | namespace Carbon.Css; 4 | 5 | public static class PseudoElementNames 6 | { 7 | private static readonly FrozenSet s_items = FrozenSet.ToFrozenSet([ 8 | "after", 9 | "backdrop", // Safari 15.4 10 | "before", 11 | "cue", 12 | "cue-region", 13 | "file-selector-button", 14 | "first-letter", 15 | "first-line", 16 | "grammar-error", 17 | "highlight", 18 | "marker", 19 | "part", 20 | "placeholder", 21 | "selection", 22 | "slotted", 23 | "spelling-error", 24 | "target-text", 25 | "view-transition", 26 | "view-transition-group" 27 | ]); 28 | 29 | public static bool Contains(string name) => s_items.Contains(name); 30 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Helpers/Base64Helper.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Carbon.Css.Helpers; 4 | 5 | internal static class Base64Helper 6 | { 7 | public static void WriteStreamAsBase64(Stream stream, TextWriter writer) 8 | { 9 | Span inputBuffer = stackalloc byte[3 * 256]; 10 | Span outputBuffer = stackalloc char[4 * 256]; 11 | 12 | while (true) 13 | { 14 | int bytesRead = stream.ReadAtLeast(inputBuffer, inputBuffer.Length, throwOnEndOfStream: false); 15 | 16 | if (bytesRead is 0) break; // nothing left to encode 17 | 18 | Convert.TryToBase64Chars(inputBuffer[..bytesRead], outputBuffer, out int charsWritten); 19 | writer.Write(outputBuffer[..charsWritten]); 20 | 21 | if (bytesRead < inputBuffer.Length) break; // final block 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Rewriters/Rewriter.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class RewriterCollection : List 4 | { 5 | public IEnumerable Rewrite(CssRule rule, int index = 0) 6 | { 7 | if (Count is 0) 8 | { 9 | yield return rule; 10 | 11 | yield break; 12 | } 13 | 14 | // TODO: Pass along in order 15 | 16 | // Chain 17 | 18 | foreach (var r in this[index].Rewrite(rule)) 19 | { 20 | if (Count > index + 1) 21 | { 22 | foreach (var r2 in Rewrite(r, ++index)) 23 | { 24 | yield return r2; 25 | } 26 | } 27 | else 28 | { 29 | yield return r; 30 | } 31 | } 32 | 33 | } 34 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/SupportsTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class SupportRuleTests 4 | { 5 | [Fact] 6 | public void A() 7 | { 8 | var sheet = StyleSheet.Parse( 9 | """ 10 | @supports (-moz-appearance: none) { 11 | .video-controls__progress { 12 | height: 4px; 13 | bottom: 1.9vw; 14 | background: rgba(#fff, 40%); 15 | } 16 | } 17 | """); 18 | 19 | Assert.Equal( 20 | """ 21 | @supports (-moz-appearance: none) { 22 | .video-controls__progress { 23 | height: 4px; 24 | bottom: 1.9vw; 25 | background: #ffffff66; 26 | } 27 | } 28 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/test1.css: -------------------------------------------------------------------------------- 1 | .world { 2 | position: relative; 3 | width: 670px; 4 | height: 650px; 5 | overflow: hidden; 6 | margin: auto; 7 | } 8 | 9 | .plane { 10 | position: absolute; 11 | left: 0; 12 | top: 0; 13 | width: 5000px; 14 | height: 650px; 15 | background-size: contain; 16 | } 17 | 18 | .plane.background { 19 | width: 5462px; 20 | height: 1121px; 21 | background-image: url('/onboard/images/background.png'); 22 | } 23 | 24 | .plane.midground { 25 | top: 50px; 26 | width: 7012px; 27 | height: 940px; 28 | background-image: url('/onboard/images/midground.png'); 29 | } 30 | 31 | .plane.foreground { 32 | width: 7052px; 33 | height: 966px; 34 | background-image: url('/onboard/images/foreground.png'); 35 | } 36 | -------------------------------------------------------------------------------- /src/Carbon.Css/Comptability/CssModuleType.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public enum CssModuleType 4 | { 5 | Animations = 1, 6 | BackgroundsAndBorders = 2, // http://www.w3.org/TR/css3-background/#background-position 7 | Core = 3, 8 | Color = 4, 9 | Columns = 5, 10 | Containment = 6, // https://www.w3.org/TR/css-contain-1/ 11 | Flexbox = 7, // https://www.w3.org/TR/css-flexbox-1/ 12 | Fonts = 8, // http://dev.w3.org/csswg/css3-fonts/ 13 | Masking = 9, // http://dev.w3.org/fxtf/masking/ 14 | Ruby = 10, 15 | Scrollbars = 11, // https://www.w3.org/TR/css-scrollbars-1/ 16 | UI = 12, 17 | Text = 13, 18 | Transitions = 14, 19 | Transforms = 15 20 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Parser/Tokenizer/LexicalModeContext.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Parser; 2 | 3 | public struct LexicalModeContext 4 | { 5 | private readonly Stack modes; 6 | 7 | public LexicalModeContext(LexicalMode start) 8 | { 9 | modes = new Stack(3); 10 | 11 | Current = start; 12 | 13 | modes.Push(start); 14 | } 15 | 16 | public void Enter(LexicalMode mode) 17 | { 18 | modes.Push(mode); 19 | 20 | Current = mode; 21 | } 22 | 23 | public void Leave(LexicalMode mode, int position = 0) 24 | { 25 | if (Current != mode) 26 | { 27 | throw new UnexpectedModeChange(Current, mode, position); 28 | } 29 | 30 | modes.Pop(); 31 | 32 | Current = modes.Peek(); 33 | } 34 | 35 | public LexicalMode Current { get; private set; } 36 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Carbon.Css.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Carbon.Css 5 | net9.0 6 | 0.76.1 7 | true 8 | true 9 | $(NoWarn),IDE0057,IDE0090 10 | 11 | 12 | iamcarbon 13 | Fast (S)CSS parser and auto-prefixer 14 | © 2012-2024 Jason Nelson 15 | css;scss;autoprefix 16 | MIT 17 | https://github.com/carbon/css 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Carbon.Css/Parser/Exceptions/UnexpectedTokenException.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Parser; 2 | 3 | public sealed class UnexpectedTokenException : SyntaxException 4 | { 5 | public UnexpectedTokenException(LexicalMode mode, CssToken token) 6 | : base($"Unexpected token reading {mode}. Was '{token.Kind}'.") 7 | { 8 | Token = token; 9 | } 10 | 11 | public UnexpectedTokenException(LexicalMode mode, CssTokenKind expectedKind, CssToken token) 12 | : base($"Unexpected token at {token.Position} reading {mode}. Expected '{expectedKind}'. Was '{token.Kind}'", token.Position) 13 | { 14 | Token = token; 15 | } 16 | 17 | public UnexpectedTokenException(CssToken token) 18 | : base($"Unexpected token. Was '{token.Kind}:{token.Text}'.", token.Position) 19 | { 20 | Token = token; 21 | } 22 | 23 | public CssToken Token { get; } 24 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/all.scss: -------------------------------------------------------------------------------- 1 | //= support Chrome 10+ 2 | //= support Firefox 30+ 3 | //= support IE 9+ 4 | //= support Safari 5+ 5 | 6 | @import 'base/variables'; 7 | @import 'base/fonts'; 8 | @import 'base/type'; 9 | 10 | // Components 11 | @import 'components/animations'; 12 | @import 'components/player'; 13 | @import 'components/zoomer'; 14 | 15 | @import 'base/base'; 16 | @import 'base/forms'; 17 | @import 'base/utility'; 18 | 19 | // Parts 20 | @import 'parts/header'; 21 | @import 'parts/nav'; 22 | @import 'parts/footer'; 23 | @import 'parts/paginator'; 24 | @import 'parts/post'; 25 | 26 | // Pages 27 | @import 'pages/mainInfo'; 28 | @import 'pages/about'; 29 | @import 'pages/contact'; 30 | @import 'pages/projects'; 31 | @import 'pages/project'; 32 | @import 'pages/privacy'; 33 | 34 | // Float clears 35 | @import 'base/clears'; 36 | 37 | // Options 38 | @import 'options/fontScheme'; 39 | @import 'options/textAlignment'; 40 | @import 'base/media'; 41 | -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/CssRoot.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public class CssRoot : CssNode 4 | { 5 | protected readonly List _children; 6 | 7 | public CssRoot() 8 | : this([]) 9 | { } 10 | 11 | public CssRoot(List children) 12 | : base(NodeKind.Document) 13 | { 14 | _children = children; 15 | } 16 | 17 | #region Children 18 | 19 | public void RemoveChild(CssNode node) 20 | { 21 | node.Parent = null; 22 | 23 | _children.Remove(node); 24 | } 25 | 26 | public void AddChild(CssNode node) 27 | { 28 | node.Parent = this; 29 | 30 | _children.Add(node); 31 | } 32 | 33 | public void InsertChild(int index, CssNode node) 34 | { 35 | node.Parent = this; 36 | 37 | _children.Insert(index, node); 38 | } 39 | 40 | public List Children => _children; 41 | 42 | #endregion 43 | } -------------------------------------------------------------------------------- /src/Carbon.Css/CssFunctionNames.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Frozen; 2 | 3 | namespace Carbon.Css; 4 | 5 | internal static class CssFunctionNames 6 | { 7 | private static readonly FrozenSet s_items = FrozenSet.ToFrozenSet([ 8 | "abs", 9 | "attr", 10 | "asin", 11 | "acos", 12 | "atan", 13 | "atan2", 14 | "calc", 15 | "clamp", 16 | "cross-fade", 17 | "cos", 18 | "cubic-bezier", 19 | "env", 20 | "exp", 21 | "hypot", 22 | "var", 23 | "round", 24 | "log", 25 | "max", 26 | "min", 27 | "mod", 28 | "pow", 29 | "rem", 30 | "sign", 31 | "sin", 32 | "sqrt", 33 | "tan", 34 | 35 | // color functions 36 | "color-mix", 37 | "color-contrast", 38 | "light-dark", 39 | ]); 40 | 41 | public static bool Contains(string name) => s_items.Contains(name); 42 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Serialization/CssGapConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Carbon.Css.Serialization; 6 | 7 | public sealed class CssGapConverter : JsonConverter 8 | { 9 | [SkipLocalsInit] 10 | public override CssGap Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 11 | { 12 | int length = reader.HasValueSequence 13 | ? checked((int)reader.ValueSequence.Length) 14 | : reader.ValueSpan.Length; 15 | 16 | scoped Span buffer = length <= 32 17 | ? stackalloc byte[32] 18 | : new byte[length]; 19 | 20 | ReadOnlySpan text = buffer.Slice(0, reader.CopyString(buffer)); 21 | 22 | return CssGap.Parse(text); 23 | } 24 | 25 | public override void Write(Utf8JsonWriter writer, CssGap value, JsonSerializerOptions options) 26 | { 27 | writer.WriteStringValue(value.ToString()); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/Carbon.Css.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | $(NoWarn),IDE0018,IDE0057 7 | annotations 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/CssFunctionTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class CssFunctionTests 4 | { 5 | [Fact] 6 | public void Unquote() 7 | { 8 | var sheet = StyleSheet.Parse( 9 | """ 10 | $t1: "calc((100% - 600px) / 2)"; 11 | 12 | div { padding: unquote($t1) } 13 | """); 14 | 15 | Assert.Equal( 16 | """ 17 | div { 18 | padding: calc((100% - 600px) / 2); 19 | } 20 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 21 | } 22 | 23 | [Fact] 24 | public void ParseFunctionWithLeadingWhitespaceInArgument() 25 | { 26 | var sheet = StyleSheet.Parse("div { background-color: rgba( 42, 45, 53, 0.7); }"); 27 | 28 | Assert.Equal( 29 | """ 30 | div { 31 | background-color: rgba(42, 45, 53, 0.7); 32 | } 33 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Serialization/ThicknessConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Carbon.Css.Serialization; 6 | 7 | public sealed class ThicknessJsonConverter : JsonConverter 8 | { 9 | [SkipLocalsInit] 10 | public override Thickness Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 11 | { 12 | int length = reader.HasValueSequence 13 | ? checked((int)reader.ValueSequence.Length) 14 | : reader.ValueSpan.Length; 15 | 16 | scoped Span buffer = length <= 32 17 | ? stackalloc char[32] 18 | : new char[length]; 19 | 20 | ReadOnlySpan text = buffer.Slice(0, reader.CopyString(buffer)); 21 | 22 | return Thickness.Parse(text); 23 | } 24 | 25 | public override void Write(Utf8JsonWriter writer, Thickness value, JsonSerializerOptions options) 26 | { 27 | writer.WriteStringValue(value.ToString()); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Serialization/CssUnitValueConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Carbon.Css.Serialization; 6 | 7 | public sealed class CssUnitValueConverter : JsonConverter 8 | { 9 | [SkipLocalsInit] 10 | public override CssUnitValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 11 | { 12 | int length = reader.HasValueSequence 13 | ? checked((int)reader.ValueSequence.Length) 14 | : reader.ValueSpan.Length; 15 | 16 | scoped Span buffer = length <= 32 17 | ? stackalloc byte[32] 18 | : new byte[length]; 19 | 20 | ReadOnlySpan text = buffer.Slice(0, reader.CopyString(buffer)); 21 | 22 | return CssUnitValue.Parse(text); 23 | } 24 | 25 | public override void Write(Utf8JsonWriter writer, CssUnitValue value, JsonSerializerOptions options) 26 | { 27 | writer.WriteStringValue(value.ToString()); 28 | } 29 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 - 2018 Jason Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies 10 | or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 15 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 16 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 17 | OR OTHER DEALINGS IN THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/AutoPrefixTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class AutoPrefixTests 4 | { 5 | [Fact] 6 | public void ZoomOut() 7 | { 8 | var sheet = StyleSheet.Parse( 9 | """ 10 | //= support Safari >= 7 11 | div { cursor: zoom-out } 12 | """); 13 | 14 | Assert.Equal( 15 | """ 16 | div { 17 | cursor: -webkit-zoom-out; 18 | cursor: zoom-out; 19 | } 20 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 21 | } 22 | 23 | [Fact] 24 | public void GrabSupport() 25 | { 26 | var sheet = StyleSheet.Parse( 27 | """ 28 | //= support Firefox >= 5 29 | //= support Safari >= 1 30 | div { cursor: grab } 31 | """); 32 | 33 | Assert.Equal( 34 | """ 35 | div { 36 | cursor: -moz-grab; 37 | cursor: -webkit-grab; 38 | cursor: grab; 39 | } 40 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Values/CssFunction.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Runtime.CompilerServices; 3 | using System.Text; 4 | 5 | namespace Carbon.Css; 6 | 7 | public class CssFunction(string name, CssValue arguments) 8 | : CssValue(NodeKind.Function) 9 | { 10 | public string Name { get; } = name; 11 | 12 | public CssValue Arguments { get; } = arguments; 13 | 14 | public override CssFunction CloneNode() => new(Name, Arguments); 15 | 16 | internal override void WriteTo(TextWriter writer) 17 | { 18 | writer.Write(Name); 19 | writer.Write('('); 20 | Arguments.WriteTo(writer); 21 | writer.Write(')'); 22 | } 23 | 24 | internal override void WriteTo(scoped ref ValueStringBuilder sb) 25 | { 26 | sb.Append(Name); 27 | sb.Append('('); 28 | Arguments.WriteTo(ref sb); 29 | sb.Append(')'); 30 | } 31 | 32 | [SkipLocalsInit] 33 | public override string ToString() 34 | { 35 | var sb = new ValueStringBuilder(stackalloc char[128]); 36 | 37 | WriteTo(ref sb); 38 | 39 | return sb.ToString(); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/parts/footer.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | footer { 4 | display: block; 5 | position: absolute; 6 | width: 100%; 7 | bottom: 0; 8 | left: 0; 9 | border-top: $border; 10 | margin: 0; 11 | 12 | a { transition: color .2s ease; } 13 | 14 | > .inner { 15 | display: block; 16 | max-width: 1000px; 17 | margin: 0 auto; 18 | padding: 0 10px; 19 | } 20 | 21 | .contentWrapper { 22 | padding: 20px 0; 23 | display: table; 24 | min-height: 80px; 25 | width: 100%; 26 | box-sizing: border-box; 27 | } 28 | 29 | carbon-branding, 30 | .footerBlurb { 31 | display: table-cell; 32 | vertical-align: middle; 33 | padding: 0 20px; 34 | } 35 | 36 | carbon-branding { 37 | > a { 38 | border-bottom: none !important; 39 | box-shadow: none; 40 | } 41 | 42 | carbon-glyph { 43 | position: relative; 44 | font: 28px/0 'frontend'; 45 | top: 7px; 46 | margin: 0 -2px 0 -5px; 47 | } 48 | 49 | span:first-of-type { display: none; } 50 | } 51 | 52 | .footerBlurb { text-align: right; } 53 | 54 | p, 55 | carbon-branding { 56 | font-size: 14px; 57 | line-height: 1.3; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Carbon.Css/Parser/Parser/CssParser.Expressions.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Parser; 2 | 3 | public sealed partial class CssParser : IDisposable 4 | { 5 | public CssValue ReadExpression() 6 | { 7 | // Literal (Number, Measurement, Variable, ... 8 | var left = ReadComponent(); 9 | 10 | return Current.IsBinaryOperator ? ReadExpressionFrom(left) : left; 11 | } 12 | 13 | public CssValue ReadExpressionFrom(CssValue left) 14 | { 15 | var operatorToken = Consume(); // Read operator 16 | 17 | ReadTrivia(); 18 | 19 | // This may be another expression... 20 | // TODO: Make recursive 21 | var right = ReadComponent(); 22 | 23 | return new BinaryExpression(left, operatorToken, right); 24 | } 25 | } 26 | 27 | /* 28 | // https://en.wikipedia.org/wiki/Shunting-yard_algorithm 29 | 30 | 1: Multiplicative : *, /, % 31 | 2: Additive : +, – 32 | 3: ? : ==, !=, >, >=, <, <= 33 | 4: Logical : &&, || 34 | 35 | | number | plus | number | equals | number | 36 | 5 + 10 == 5 37 | | BinaryExpression | 38 | | BinaryExpression | 39 | */ 40 | -------------------------------------------------------------------------------- /src/Carbon.Css/Model/CssSelector.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | using Carbon.Css.Parser; 4 | 5 | namespace Carbon.Css; 6 | 7 | public sealed class CssSelector(List items) : IEnumerable 8 | { 9 | private readonly List _items = items; // comma separated 10 | 11 | public int Count => _items.Count; 12 | 13 | public CssSequence this[int index] => _items[index]; 14 | 15 | public bool Contains(NodeKind kind) 16 | { 17 | foreach (var part in _items) 18 | { 19 | if (part.Contains(kind)) return true; 20 | } 21 | 22 | return false; 23 | } 24 | 25 | public override string ToString() => string.Join(", ", _items); 26 | 27 | public static CssSelector Parse(string text) 28 | { 29 | using var parser = new CssParser(text); 30 | 31 | return parser.ReadSelector(); 32 | } 33 | 34 | #region IEnumerator 35 | 36 | public IEnumerator GetEnumerator() => _items.GetEnumerator(); 37 | 38 | IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator(); 39 | 40 | #endregion 41 | } 42 | 43 | // a:hover 44 | // #id 45 | // .className 46 | // .className, .anotherName (multi-selector or group) -------------------------------------------------------------------------------- /src/Carbon.Css/_/Tuple4.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | internal ref struct Tuple4 4 | { 5 | public ReadOnlySpan _0 { get; private set; } 6 | 7 | public ReadOnlySpan _1 { get; private set; } 8 | 9 | public ReadOnlySpan _2 { get; private set; } 10 | 11 | public ReadOnlySpan _3 { get; private set; } 12 | 13 | public readonly ReadOnlySpan this[int i] => i switch 14 | { 15 | 0 => _0, 16 | 1 => _1, 17 | 2 => _2, 18 | 3 => _3, 19 | _ => throw new IndexOutOfRangeException() 20 | }; 21 | 22 | public int Length { get; private set; } 23 | 24 | public static Tuple4 Parse(ReadOnlySpan d) 25 | { 26 | var result = new Tuple4(); 27 | 28 | var splitter = new StringSplitter(d, ' '); 29 | 30 | while (splitter.TryGetNext(out var component)) 31 | { 32 | switch (result.Length) 33 | { 34 | case 0: result._0 = component; break; 35 | case 1: result._1 = component; break; 36 | case 2: result._2 = component; break; 37 | case 3: result._3 = component; break; 38 | } 39 | 40 | result.Length++; 41 | } 42 | 43 | return result; 44 | } 45 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Values/CssString.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | 4 | using Carbon.Css.Parser; 5 | 6 | namespace Carbon.Css; 7 | 8 | public sealed class CssString(string text) : CssValue(NodeKind.String) 9 | { 10 | public CssString(CssToken token) 11 | : this(token.Text) 12 | { } 13 | 14 | internal CssString(CssToken token, Trivia? trailing) 15 | : this(token.Text) 16 | { 17 | Trailing = trailing; 18 | } 19 | 20 | public string Text { get; } = text; 21 | 22 | public override CssString CloneNode() => new(Text); 23 | 24 | internal override void WriteTo(scoped ref ValueStringBuilder sb) 25 | { 26 | sb.Append(Text); 27 | } 28 | 29 | internal override void WriteTo(TextWriter writer) 30 | { 31 | writer.Write(Text); 32 | } 33 | 34 | public override string ToString() => Text; 35 | } 36 | 37 | /* 38 | math : calc S*; 39 | calc : "calc(" S* sum S* ")"; 40 | sum : product [ S+ [ "+" | "-" ] S+ product ]*; 41 | product : unit [ S* [ "*" S* unit | "/" S* NUMBER ] ]*; 42 | attr : "attr(" S* qname [ S+ type-keyword ]? S* [ "," [ unit | calc ] S* ]? ")"; 43 | unit : [ NUMBER | DIMENSION | PERCENTAGE | "(" S* sum S* ")" | calc | attr ]; 44 | */ 45 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/parts/post.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | .postWrapper { margin-top: 4em; } 4 | 5 | .tag:before { content: '#'; } 6 | 7 | article { 8 | display: block; 9 | position: relative; 10 | max-width: 700px; 11 | margin: 0 auto 6em; 12 | text-align: center; 13 | font-size: 0.75em; 14 | 15 | carbon-grid { 16 | position: relative; 17 | } 18 | 19 | > .body { font-size: 1rem; } 20 | 21 | > .header, 22 | > .body, 23 | > .tags { 24 | max-width: 460px; 25 | margin: 1.5em auto; 26 | } 27 | 28 | > .tags { 29 | > a { 30 | opacity: 0.6; 31 | margin: 0 .15em; 32 | border-bottom: none !important; 33 | transition: opacity .2s; 34 | &:hover { opacity: 1 } 35 | &.tag:before { opacity: 0.4;} 36 | } 37 | } 38 | 39 | // User styles? 40 | p, ul, ol { margin: 0 0 1em 0; } 41 | 42 | ul { 43 | list-style: disc; 44 | list-style-position: inside; 45 | padding-left: 0 !important; 46 | } 47 | 48 | ol { 49 | list-style: decimal; 50 | list-style-position: inside; 51 | padding-left: 0 !important; 52 | } 53 | 54 | .longPost { 55 | text-align: left; 56 | 57 | ul, ol { 58 | padding-left: 1.4em !important; 59 | list-style-position: outside; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/fonts.css: -------------------------------------------------------------------------------- 1 | i { 2 | font-style: normal; 3 | } 4 | 5 | h1, h2, h3, h4, h5, h6 { 6 | font-weight: 400; 7 | line-height: 1.1em; 8 | } 9 | 10 | 11 | h1 { 12 | color: #333; 13 | font-size: 2em; 14 | font-weight: 400; 15 | margin: 4% 6%; 16 | padding: 15px; 17 | @include transition(color, 1s, ease, .5s); 18 | @include transition(opacity, 1s, ease, .5s); 19 | opacity: 1; 20 | 21 | a { 22 | color: #333; 23 | text-decoration: none; 24 | border-bottom: none; 25 | @include transition(color, 0.3s, ease); 26 | } 27 | 28 | i { 29 | opacity: .3; 30 | } 31 | } 32 | 33 | h1 a:hover { 34 | color: #00e; 35 | } 36 | 37 | .leftAligned h1 { 38 | text-align: left; 39 | margin: 0 6% 10% 0; 40 | } 41 | 42 | 43 | h2 { 44 | border-bottom: 1px solid rgba(255, 255, 255, 0.5); 45 | display: inline; 46 | margin-bottom: 0; 47 | padding-bottom: 1px; 48 | padding-right: 3px; 49 | padding-left: 3px; 50 | line-height: 1.4em; 51 | font-size: 1.5em; 52 | } 53 | 54 | h2.projectTitle { 55 | display: block; 56 | padding: 15px; 57 | border-bottom: none; 58 | } 59 | 60 | .leftAligned h2.projectTitle { 61 | text-align: left; 62 | } 63 | 64 | 65 | h3 { 66 | font-size: .9em; 67 | font-style: normal; 68 | } 69 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/CssPropertyTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class CssPropertyTests 4 | { 5 | [Fact] 6 | public void Common() 7 | { 8 | Assert.Equal(CssProperty.Color, CssProperty.Get("color")); 9 | Assert.Equal(CssProperty.Display, CssProperty.Get("display")); 10 | Assert.Equal(CssProperty.Font, CssProperty.Get("font")); 11 | Assert.Equal(CssProperty.Width, CssProperty.Get("width")); 12 | } 13 | 14 | // clip-path, clip-rule and mask properties [css-masking-1] 15 | 16 | [Fact] 17 | public void Clipping() 18 | { 19 | Assert.Equal(CssProperty.ClipPath, CssProperty.Get("clip-path")); 20 | Assert.Equal(CssProperty.ClipRule, CssProperty.Get("clip-rule")); 21 | Assert.Equal(CssProperty.StrokeLinecap, CssProperty.Get("stroke-linecap")); 22 | Assert.Equal(CssProperty.StrokeWidth, CssProperty.Get("stroke-width")); 23 | } 24 | 25 | [Fact] 26 | public void Svg() 27 | { 28 | Assert.Equal(CssProperty.Fill, CssProperty.Get("fill")); 29 | Assert.Equal(CssProperty.Stroke, CssProperty.Get("stroke")); 30 | Assert.Equal(CssProperty.StrokeLinecap, CssProperty.Get("stroke-linecap")); 31 | Assert.Equal(CssProperty.StrokeWidth, CssProperty.Get("stroke-width")); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/parts/paginator.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | carbon-paginator { 4 | display: block; 5 | padding: 50px 65px; 6 | max-width: 1095px; 7 | margin: 0 auto; 8 | overflow: auto; 9 | border-top: 1px solid rgba(0, 0, 0, .08); 10 | box-sizing: border-box; 11 | 12 | a { 13 | display: inline-block; 14 | width: 50%; 15 | border-bottom: none !important; 16 | box-shadow: none !important; 17 | box-sizing: border-box; 18 | 19 | b, .small { transition: all 0.2s ease-in-out; } 20 | 21 | svg { 22 | stroke: $arrowColor; 23 | height: 52px; 24 | width: 15px; 25 | position: relative; 26 | display: inline-block; 27 | stroke-width: 4px; 28 | transition: stroke 0.2s ease-in-out; 29 | } 30 | 31 | &.prev { 32 | float: left; 33 | margin-left: -35px; 34 | 35 | > svg { 36 | transform: scaleX(-1); 37 | float: left; 38 | padding-left: 20px; 39 | } 40 | } 41 | 42 | &.next { 43 | text-align: right; 44 | float: right; 45 | margin-right: -35px; 46 | 47 | > svg { 48 | float: right; 49 | padding-left: 20px; 50 | } 51 | } 52 | 53 | &:hover { 54 | .small { opacity: .5; } 55 | svg { stroke: $accentColor; } 56 | } 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Values/CssColorSpace.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | internal enum CssColorSpace : byte 4 | { 5 | // | Chrome | Safari | Firefox 6 | Rgb = 1, // rgb | 7 | Hsl = 2, // hsl 8 | Hwb = 3, // hwb 9 | Lab = 4, // lab | 111+ | 15+ | 10 | Lch = 5, // lch | 111+ | 15+ | 11 | OkLab = 6, // oklab | 12 | OkLch = 7, // oklch | 13 | sRgb = 8, // 14 | sRgbLinear = 9, // srgb-linear | 15 | DisplayP3 = 10, // display-p3 16 | A98Rgb = 11, // a98-rgb 17 | ProPhotoRgb = 12, // prophoto-rgb 18 | Rec2020 = 13, // rec2020 19 | Xyz = 15, // xyz 20 | XyzD50 = 16, // xyz-d65 21 | XyzD65 = 17, // xyz-d65 22 | } 23 | 24 | 25 | // = 26 | // srgb | 27 | // srgb-linear | 28 | // display-p3 | 29 | // a98-rgb | 30 | // prophoto-rgb | 31 | // rec2020 32 | 33 | 34 | // Safari... 35 | // A98RGB 36 | // DisplayP3 37 | // LCH 38 | // Lab 39 | // LinearSRGB 40 | // ProPhotoRGB 41 | // Rec2020 42 | // SRGB 43 | // XYZ_D50 44 | // XYZ_D65 45 | 46 | // https://github.com/WebKit/WebKit/blob/54fe2cbb5eddd356eea81e08228e470d49a83f94/Source/WebCore/platform/graphics/ColorTypes.h -------------------------------------------------------------------------------- /src/Carbon.Css/Comptability/BrowserPrefix.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class BrowserPrefix : IEquatable 4 | { 5 | public static readonly BrowserPrefix Moz = new(BrowserPrefixKind.Moz, "-moz-"); 6 | public static readonly BrowserPrefix MS = new(BrowserPrefixKind.Ms, "-ms-"); 7 | public static readonly BrowserPrefix Webkit = new(BrowserPrefixKind.Webkit, "-webkit-"); 8 | 9 | private BrowserPrefix(BrowserPrefixKind kind, string text) 10 | { 11 | Kind = kind; 12 | Text = text; 13 | } 14 | 15 | public BrowserPrefixKind Kind { get; } 16 | 17 | public string Text { get; } 18 | 19 | public static implicit operator string(BrowserPrefix prefix) => prefix.Text; 20 | 21 | public bool Equals(BrowserPrefix? other) => other is not null && Kind == other.Kind; 22 | 23 | public override bool Equals(object? obj) 24 | { 25 | return obj is BrowserPrefix other && Equals(other); 26 | } 27 | 28 | public override int GetHashCode() => (int)Kind; 29 | 30 | public static bool operator ==(BrowserPrefix left, BrowserPrefix right) 31 | { 32 | return left.Equals(right); 33 | } 34 | 35 | public static bool operator !=(BrowserPrefix left, BrowserPrefix right) 36 | { 37 | return !left.Equals(right); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Helpers/SourceHelper.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Carbon.Css.Helpers; 4 | 5 | public static class SourceHelper 6 | { 7 | public static SourceLocation GetLocation(this string text, int position) 8 | { 9 | int i = 1; 10 | int line = 1; 11 | int column = 1; 12 | int charCode; 13 | 14 | using var reader = new StringReader(text); 15 | 16 | while ((charCode = reader.Read()) != -1) 17 | { 18 | var c = (char)charCode; 19 | 20 | if (c is '\n') 21 | { 22 | line++; 23 | column++; 24 | } 25 | 26 | i++; 27 | 28 | if (i == position) break; 29 | } 30 | 31 | return new SourceLocation(i, line, column); 32 | } 33 | 34 | public static List GetLinesAround(ReadOnlySpan text, int number, int window = 0) 35 | { 36 | var lines = new List(); 37 | var i = 1; 38 | 39 | foreach (var line in text.EnumerateLines()) 40 | { 41 | if ((i >= (number - window)) && (i <= (number + window))) 42 | { 43 | lines.Add(new LineInfo(i, line.ToString())); 44 | } 45 | 46 | i++; 47 | 48 | if (i > (number + window)) break; 49 | } 50 | 51 | return lines; 52 | } 53 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Rules/ImportRule.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Carbon.Css; 4 | 5 | public sealed class ImportRule(CssUrlValue url) : CssRule 6 | { 7 | public override RuleType Type => RuleType.Import; 8 | 9 | public CssUrlValue Url { get; } = url; 10 | 11 | public void WriteTo(TextWriter writer) 12 | { 13 | writer.Write("@import "); 14 | 15 | Url.WriteTo(writer); 16 | 17 | writer.Write(';'); 18 | } 19 | 20 | public override string ToString() 21 | { 22 | using var writer = new StringWriter(); 23 | 24 | WriteTo(writer); 25 | 26 | return writer.ToString(); 27 | } 28 | } 29 | 30 | /* 31 | 6.3 The @import rule 32 | 33 | The '@import' rule allows users to import style rules from other style sheets. 34 | In CSS 2.1, any @import rules must precede all other rules (except the @charset rule, if present). 35 | See the section on parsing for when user agents must ignore @import rules. 36 | The '@import' keyword must be followed by the URI of the style sheet to include. 37 | A string is also allowed; it will be interpreted as if it had url(...) around it. 38 | 39 | The following lines are equivalent in meaning and illustrate both '@import' syntaxes (one with "url()" and one with a bare string): 40 | 41 | @import "mystyle.css"; 42 | @import url("mystyle.css"); 43 | */ 44 | -------------------------------------------------------------------------------- /src/Carbon.Css/Patching/PatchFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | internal static class PatchFactory 4 | { 5 | public static readonly PrefixNamePatcher PrefixName = new(); 6 | public static readonly PrefixNameAndValuePatcher PrefixNameAndValue = new(); 7 | 8 | // transform 0.04s linear, opacity 0.04s linear, visibility 0.04s linear; 9 | // -webkit-transform 0.04s linear, opacity 0.04s linear, visibility 0.04s linear; 10 | 11 | public static CssValue PatchValue(CssValue value, in BrowserInfo browser) 12 | { 13 | if (value.Kind != NodeKind.ValueList) return value; 14 | 15 | var valueList = (CssValueList)value; 16 | 17 | var list = new List(); 18 | 19 | foreach (var node in valueList) 20 | { 21 | if (node.Kind is NodeKind.ValueList) // For comma separated componented lists 22 | { 23 | list.Add(PatchValue(node, browser)); 24 | } 25 | else if (node.Kind is NodeKind.String && ((CssString)node).Text is "transform") 26 | { 27 | list.Add(new CssString(browser.Prefix.Text + "transform")); 28 | } 29 | else 30 | { 31 | list.Add(node); 32 | } 33 | } 34 | 35 | return new CssValueList(list, valueList.Separator); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/ResolverTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class ResolverTests 4 | { 5 | [Fact] 6 | public void A() 7 | { 8 | var text = File.ReadAllText(TestHelper.GetTestFile("webcat/all.scss").FullName); 9 | var expected = File.ReadAllText(TestHelper.GetTestFile("webcat/expected.txt").FullName).ReplaceLineEndings(Environment.NewLine); 10 | 11 | var sheet = StyleSheet.Parse(text, new CssContext()); 12 | 13 | var data = new Dictionary 14 | { 15 | ["accentColor"] = CssValue.Parse("#20b9eb"), 16 | ["colorScheme"] = CssValue.Parse("light"), 17 | ["fontScheme"] = CssValue.Parse("serif"), 18 | ["textAlignment"] = CssValue.Parse("left") 19 | }; 20 | 21 | sheet.SetResolver(new CssResolver("webcat")); 22 | 23 | var writer = new StringWriter(); 24 | 25 | sheet.WriteTo(writer, data); 26 | 27 | writer.Flush(); 28 | 29 | var output = writer.ToString(); 30 | 31 | Assert.Equal(expected, output); 32 | } 33 | } 34 | 35 | public sealed class CssResolver(string basePath) : ICssResolver 36 | { 37 | public string ScopedPath { get; } = basePath; 38 | 39 | public Stream Open(string absolutePath) 40 | { 41 | return TestHelper.GetTestFile("webcat/" + absolutePath).Open(FileMode.Open); 42 | } 43 | } -------------------------------------------------------------------------------- /src/Carbon.Css/_/BoxLayoutMode.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | [Flags] 4 | public enum BoxLayoutMode 5 | { 6 | Unknown = 0, 7 | 8 | // 9 | // Defines how the box participates in the flow layout 10 | Block = 1 << 0, 11 | Inline = 1 << 1, 12 | RunIn = 1 << 2, // run-in 13 | 14 | // 15 | // Defines how the children of the box are laid out. 16 | Flow = 1 << 5, 17 | FlowRoot = 1 << 6, // flow-root 18 | Table = 1 << 7, 19 | Flex = 1 << 8, 20 | Grid = 1 << 9, 21 | Ruby = 1 << 10, 22 | 23 | // 24 | ListItem = 1 << 13, 25 | 26 | // 27 | TableRowGroup = 1 << 15, 28 | TableHeaderGroup = 1 << 16, 29 | TableFooterGroup = 1 << 17, 30 | TableRow = 1 << 18, 31 | TableCell = 1 << 19, 32 | TableColumnGroup = 1 << 20, 33 | TableColumn = 1 << 21, 34 | TableCaption = 1 << 22, 35 | RubyBase = 1 << 23, 36 | RubyText = 1 << 24, 37 | RubyBaseContainer = 1 << 25, 38 | RubyTextContainer = 1 << 26, 39 | 40 | // 41 | Contents = 1 << 30, 42 | None = 1 << 31, 43 | 44 | InlineBlock = Inline | FlowRoot // inline flow-root 45 | } 46 | 47 | // flex -> block flex 48 | 49 | // https://www.w3.org/TR/css-display-3/ -------------------------------------------------------------------------------- /src/Carbon.Css/Parser/Tokenizer/CssTokenKind.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Parser; 2 | 3 | public enum CssTokenKind 4 | { 5 | // Identifier, // selector or identifer (IDENT) 6 | 7 | Name, // name (followed by a :) 8 | 9 | // Values 10 | String, 11 | Number, 12 | Unit, // {em,ex,in,cm,mm,pt,pc,px 13 | 14 | Uri, // uri({string}) 15 | 16 | Dollar, // ${variableName} 17 | 18 | AtSymbol, // @{ident} 19 | Comma, // , 20 | Semicolon, // ; 21 | Colon, // : 22 | Ampersand, // & 23 | Tilde, // ~ 24 | 25 | BlockStart, // { 26 | BlockEnd, // } 27 | 28 | LeftParenthesis, // ( 29 | RightParenthesis, // ) 30 | 31 | InterpolatedStringStart, // #{ 32 | InterpolatedStringEnd, // } 33 | // Trivia 34 | 35 | Directive, // //= * 36 | Whitespace, 37 | Comment, 38 | 39 | // Binary Operators ------------------------ 40 | 41 | // Logical 42 | And = 30, // && 43 | Or = 31, // || 44 | 45 | // Equality 46 | Equals = 40, // == 47 | NotEquals = 41, // != 48 | 49 | // Relational 50 | Gt = 50, // > 51 | Gte = 51, // >= 52 | Lt = 52, // < 53 | Lte = 53, // <= 54 | 55 | // Math 56 | Divide = 60, // / 57 | Multiply = 61, // * 58 | Add = 62, // + 59 | Subtract = 63, // - 60 | Mod = 64 // % 61 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Helpers/ReadOnlySpanExtensions.cs: -------------------------------------------------------------------------------- 1 | using Carbon.Color; 2 | 3 | namespace Carbon.Css.Helpers; 4 | 5 | internal static class ReadOnlySpanExtensions 6 | { 7 | public static Rgba32 ReadColor(this ReadOnlySpan text, out int read) 8 | { 9 | read = 0; 10 | 11 | char current; 12 | 13 | bool isFunction = false; 14 | 15 | while (read < text.Length) 16 | { 17 | current = text[read]; 18 | 19 | // if we detect a function, read until ) 20 | // otherwise, read until end or space 21 | 22 | if (isFunction) 23 | { 24 | if (current is ')') 25 | { 26 | read++; 27 | break; 28 | } 29 | } 30 | else if (current is '(') 31 | { 32 | isFunction = true; 33 | } 34 | else if (current is ' ' or ',') 35 | { 36 | break; 37 | } 38 | 39 | read++; 40 | } 41 | 42 | return Rgba32.Parse(text.Slice(0, read)); 43 | } 44 | 45 | public static bool TryReadWhitespace(this ReadOnlySpan text, out int read) 46 | { 47 | read = 0; 48 | 49 | while (text.Length > read && text[read] is ' ') 50 | { 51 | read++; 52 | } 53 | 54 | return read > 0; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/CssUnitNamesTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class CssUnitNamesTests 4 | { 5 | [Fact] 6 | public void AreSame() 7 | { 8 | Assert.Same(CssUnitNames.Hz, CssUnitNames.Get("Hz")); 9 | Assert.Same(CssUnitNames.Em, CssUnitNames.Get("em")); 10 | Assert.Same(CssUnitNames.Px, CssUnitNames.Get("px")); 11 | Assert.Same(CssUnitNames.Vmin, CssUnitNames.Get("vmin")); 12 | Assert.Same(CssUnitNames.Vmax, CssUnitNames.Get("vmax")); 13 | 14 | // Safari 15+ 15 | Assert.Same(CssUnitNames.Dvh, CssUnitNames.Get("dvh")); 16 | Assert.Same(CssUnitNames.Dvw, CssUnitNames.Get("dvw")); 17 | 18 | Assert.Same(CssUnitNames.Svw, CssUnitNames.Get("svw")); 19 | Assert.Same(CssUnitNames.Svh, CssUnitNames.Get("svh")); 20 | Assert.Same(CssUnitNames.Svi, CssUnitNames.Get("svi")); 21 | Assert.Same(CssUnitNames.Svb, CssUnitNames.Get("svb")); 22 | 23 | Assert.Same(CssUnitNames.Lvw, CssUnitNames.Get("lvw")); 24 | Assert.Same(CssUnitNames.Lvh, CssUnitNames.Get("lvh")); 25 | Assert.Same(CssUnitNames.Lvi, CssUnitNames.Get("lvi")); 26 | Assert.Same(CssUnitNames.Lvb, CssUnitNames.Get("lvb")); 27 | 28 | Assert.Same(CssUnitNames.Vi, CssUnitNames.Get("vi")); 29 | Assert.Same(CssUnitNames.Vb, CssUnitNames.Get("vb")); 30 | 31 | Assert.Same(CssUnitNames.Deg, CssUnitNames.Get("deg")); 32 | 33 | // svmin, svmax 34 | } 35 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/Values/CssPlacementTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class CssPlacementTests 4 | { 5 | [Fact] 6 | public void ParseValues() 7 | { 8 | var placement = CssPlacement.Parse("center start"); 9 | 10 | // vertically centered, left justified 11 | Assert.Equal((CssBoxAlignment.Center, CssBoxAlignment.Start), (placement.Align, placement.Justify)); 12 | 13 | Assert.Equal("center start", placement.ToString()); 14 | } 15 | 16 | [Fact] 17 | public void ParseValue() 18 | { 19 | var placement = CssPlacement.Parse("center"); 20 | 21 | Assert.Equal((CssBoxAlignment.Center, CssBoxAlignment.Center), (placement.Align, placement.Justify)); 22 | 23 | Assert.Equal("center", placement.ToString()); 24 | } 25 | 26 | [Fact] 27 | public void ParseValue2() 28 | { 29 | var placement = CssPlacement.Parse("end space-evenly"); 30 | 31 | Assert.Equal((CssBoxAlignment.End, CssBoxAlignment.SpaceEvenly), (placement.Align, placement.Justify)); 32 | 33 | Assert.Equal("end space-evenly", placement.ToString()); 34 | } 35 | 36 | [Fact] 37 | public void ParseValue3() 38 | { 39 | var placement = CssPlacement.Parse("space-around start"); 40 | 41 | Assert.Equal((CssBoxAlignment.SpaceAround, CssBoxAlignment.Start), (placement.Align, placement.Justify)); 42 | 43 | Assert.Equal("space-around start", placement.ToString()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/NativeFunctionTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class NativeFunctionTests 4 | { 5 | // 10.4. Trigonometric Functions: sin(), cos(), tan(), asin(), acos(), atan(), and atan2() 6 | 7 | [Theory] 8 | [InlineData("width: calc(sin(0.25turn) * 1s)")] 9 | [InlineData("width: calc(1rem * pow(1.5, -1))")] 10 | [InlineData("width: calc(100px * sqrt(9))")] 11 | [InlineData("width: calc(100px * tan(0.785398163rad))")] 12 | [InlineData("width: mod(1000px, 29rem)")] 13 | [InlineData("width: hypot(3px, 4px, 5px)")] 14 | [InlineData("transform: rotate(asin(2 * 0.125))", "transform: rotate(asin(0.25))")] 15 | [InlineData("transform: rotate(acos(2 * 0.125))", "transform: rotate(acos(0.25))")] 16 | [InlineData("width: abs(20% - 100px)")] 17 | [InlineData("line-height: rem(5.5, 2)")] 18 | [InlineData("margin-top: env(safe-area-inset-top, 20px)")] 19 | [InlineData("width: round(down, var(--height), var(--interval))")] 20 | [InlineData("grid-template-columns: fit-content(300px) fit-content(300px) 1fr")] 21 | [InlineData("offset-path: path(\"M 10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80\")")] 22 | public void A(string declaration, string? expected = null) 23 | { 24 | var css = StyleSheet.Parse( 25 | $$""" 26 | div { 27 | {{declaration}}; 28 | } 29 | """); 30 | 31 | Assert.Equal( 32 | $$""" 33 | div { 34 | {{expected ?? declaration}}; 35 | } 36 | """, css.ToString(), ignoreLineEndingDifferences: true); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Carbon.Css/_/CssBoxAlignment.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Carbon.Css; 4 | 5 | [JsonConverter(typeof(JsonStringEnumConverter))] 6 | public enum CssBoxAlignment 7 | { 8 | Unknown = 0, 9 | Start = 1, // flush left or top 10 | End = 2, // flush right or bottom 11 | Center = 3, // centered 12 | 13 | SelfStart = 4, // self-start 14 | SelfEnd = 5, // self-end 15 | 16 | // 17 | // 18 | Baseline = 7, // baseline 19 | FirstBaseline = 8, // first baseline 20 | LastBaseline = 9, // last baseline 21 | 22 | // 23 | SpaceAround = 10, // space-around 24 | SpaceBetween = 11, // space-between 25 | SpaceEvenly = 12, // space-evenly 26 | Stretch = 13, // stretch 27 | 28 | // ? 29 | SafeCenter = 15, 30 | UnsafeCenter = 16, 31 | } 32 | 33 | // | | ? 34 | 35 | // = space-between | space-around | space-evenly | stretch 36 | // = [ first | last ]? baseline 37 | // = unsafe | safe 38 | // = center | start | end | flex-start | flex-end 39 | 40 | // CSS Box Alignment v3 41 | // https://drafts.csswg.org/css-align-3/#propdef-align-content 42 | 43 | // Notes about flex-start 44 | 45 | // https://stackoverflow.com/questions/50919447/flexbox-flex-start-self-start-and-start-whats-the-difference -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/nested.css: -------------------------------------------------------------------------------- 1 | #networkLinks .block { 2 | .edit { 3 | opacity: 0; 4 | position: absolute; 5 | top: 0; bottom: 0; right: 20px; 6 | margin: auto 0; 7 | width: 26px; height: 26px; 8 | text-align: center; 9 | background: #3ea9f5; 10 | cursor: pointer; 11 | border-radius: 100%; 12 | z-index: 105; 13 | transition: margin 0.1s ease-out, opacity 0.1s ease-out; 14 | 15 | &:before { 16 | font-family: 'carbonmade'; 17 | font-size: 12px; 18 | line-height: 26px; 19 | color: #fff; 20 | text-align: center; 21 | } 22 | } 23 | 24 | .destroy { 25 | display: none; 26 | position: absolute; 27 | top: 0; bottom: 0; right: 60px; 28 | margin: auto 0; 29 | width: 26px; height: 26px; 30 | cursor: pointer; 31 | border-radius: 100%; 32 | text-align: center; 33 | z-index: 105; 34 | 35 | &:before { 36 | font-family: 'carbonmade'; 37 | font-size: 17px; 38 | line-height: 26px; 39 | color: rgba(0,0,0,0.1); 40 | text-align: center; 41 | } 42 | 43 | &:hover:before { color: rgba(0,0,0,0.25); } 44 | } 45 | 46 | .input { 47 | background-color: #fff; 48 | box-shadow: inset 0 0 0 1px #e6e6e6; 49 | color: #222; 50 | height: 40px; 51 | line-height: 24px; 52 | padding: 5px 6px 5px 165px; 53 | margin-left: 0; 54 | box-sizing: border-box; 55 | } 56 | 57 | .emptyGuts, 58 | .populatedGuts, 59 | .editGuts { 60 | cursor: default; 61 | z-index: 100; 62 | } 63 | 64 | .controls { padding: 5px 0 10px 210px; } 65 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/base/base.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | html, 4 | body { 5 | margin: 0; 6 | padding: 0; 7 | height: 100%; 8 | background: $backgroundColor; 9 | color: $textColor; 10 | font: 18px/1.45 $fontFamily; 11 | -webkit-font-smoothing: $fontSmoothing; 12 | } 13 | 14 | html { overflow-y: scroll; } 15 | 16 | main { 17 | display: block; 18 | min-height: 100%; 19 | height: auto !important; 20 | height: 100%; 21 | position: relative; 22 | padding-bottom: 81px; 23 | overflow: hidden; 24 | box-sizing: border-box; 25 | } 26 | 27 | .content { 28 | position: relative; 29 | margin: 0; 30 | background: $backgroundColorAlt; 31 | } 32 | 33 | a { 34 | text-decoration: none; 35 | color: $linkColor; 36 | border-bottom: 1px solid $linkUnderlineColor; 37 | 38 | &:hover { border-bottom-color: rgba($linkUnderlineColor, 0.1); } 39 | } 40 | 41 | a:hover, a:hover b, a:hover i { color: $accentColor !important; } 42 | 43 | ol, ul, li { 44 | margin: 0; 45 | padding: 0; 46 | } 47 | 48 | ol, ul { 49 | list-style: none; 50 | } 51 | 52 | table { 53 | border-collapse: collapse; 54 | border-spacing: 0; 55 | } 56 | 57 | hr { 58 | display: block; 59 | height: 1px; 60 | width: 100%; 61 | margin: 0; 62 | padding: 0; 63 | background: $backgroundColorAlt; 64 | border: none; 65 | box-shadow: none; 66 | } 67 | 68 | section { 69 | display: block; 70 | margin: 0; 71 | padding: 0; 72 | } 73 | 74 | img { border: 0; } 75 | 76 | // resets 77 | div, span, 78 | table, tr, th, td { 79 | margin: 0; 80 | padding: 0; 81 | border: 0; 82 | vertical-align: baseline; 83 | } 84 | 85 | // carbon 86 | carbon-piece { display: block; } 87 | -------------------------------------------------------------------------------- /src/Carbon.Css/Helpers/StringSplitter.cs: -------------------------------------------------------------------------------- 1 | using Carbon.Css.Helpers; 2 | 3 | namespace Carbon.Css; 4 | 5 | internal ref struct StringSplitter(ReadOnlySpan text, char separator) 6 | { 7 | private readonly ReadOnlySpan _text = text; 8 | private readonly char _separator = separator; 9 | private int _position = 0; 10 | 11 | public bool TryGetNext(out ReadOnlySpan result) 12 | { 13 | if (IsEof) 14 | { 15 | result = default; 16 | 17 | return false; 18 | } 19 | 20 | int start = _position; 21 | 22 | int separatorIndex = _text[_position..].IndexOf(_separator); 23 | 24 | if (separatorIndex > -1) 25 | { 26 | _position += separatorIndex + 1; 27 | 28 | result = _text.Slice(start, separatorIndex); 29 | } 30 | else 31 | { 32 | _position = _text.Length; 33 | 34 | result = _text[start..]; 35 | } 36 | 37 | return true; 38 | } 39 | 40 | public bool TryGetNextF32(out float result) 41 | { 42 | if (TryGetNext(out var segment)) 43 | { 44 | result = NumberHelper.ParseCssNumberAsF32(segment); 45 | 46 | return true; 47 | } 48 | else 49 | { 50 | result = default; 51 | 52 | return false; 53 | } 54 | } 55 | 56 | public readonly char Current => _text[_position]; 57 | 58 | public void ReadWhitespace() 59 | { 60 | while (_position < _text.Length && Current is ' ') 61 | { 62 | _position++; 63 | } 64 | } 65 | 66 | public readonly bool IsEof => _position >= _text.Length; 67 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/Values/CssBoxAlignmentTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class CssBoxAlignmentTests 4 | { 5 | [Fact] 6 | public void ValuesDontChange() 7 | { 8 | Assert.Equal(1, (byte)CssBoxAlignment.Start); 9 | Assert.Equal(2, (byte)CssBoxAlignment.End); 10 | Assert.Equal(3, (byte)CssBoxAlignment.Center); 11 | } 12 | 13 | [Fact] 14 | public void Canonicalize() 15 | { 16 | Assert.Equal("start", CssBoxAlignment.Start.Canonicalize()); 17 | Assert.Equal("end", CssBoxAlignment.End.Canonicalize()); 18 | Assert.Equal("center", CssBoxAlignment.Center.Canonicalize()); 19 | 20 | // Baselines 21 | Assert.Equal("first baseline", CssBoxAlignment.FirstBaseline.Canonicalize()); 22 | Assert.Equal("last baseline", CssBoxAlignment.LastBaseline.Canonicalize()); 23 | 24 | Assert.Equal("space-around", CssBoxAlignment.SpaceAround.Canonicalize()); 25 | Assert.Equal("space-between", CssBoxAlignment.SpaceBetween.Canonicalize()); 26 | Assert.Equal("space-evenly", CssBoxAlignment.SpaceEvenly.Canonicalize()); 27 | 28 | // Overflow 29 | Assert.Equal("safe center", CssBoxAlignment.SafeCenter.Canonicalize()); 30 | Assert.Equal("unsafe center", CssBoxAlignment.UnsafeCenter.Canonicalize()); 31 | } 32 | 33 | [Fact] 34 | public void CanonicalizeFlex() 35 | { 36 | Assert.Equal("flex-start", CssBoxAlignment.Start.Canonicalize(BoxLayoutMode.Flex)); 37 | Assert.Equal("flex-end", CssBoxAlignment.End.Canonicalize(BoxLayoutMode.Flex)); 38 | Assert.Equal("center", CssBoxAlignment.Center.Canonicalize(BoxLayoutMode.Flex)); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Parser/Tokenizer/CssToken.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Parser; 2 | 3 | public readonly struct CssToken: ISpanFormattable 4 | { 5 | public CssToken(CssTokenKind kind, char value, int position) 6 | { 7 | Kind = kind; 8 | Text = value.ToString(); 9 | Position = position; 10 | } 11 | 12 | public CssToken(CssTokenKind kind, string value, int position) 13 | { 14 | Kind = kind; 15 | Text = value; 16 | Position = position; 17 | } 18 | 19 | public CssTokenKind Kind { get; } 20 | 21 | public int Position { get; } 22 | 23 | public string Text { get; } 24 | 25 | public readonly override string ToString() => $"{Kind}: '{Text}'"; 26 | 27 | public readonly void Deconstruct(out CssTokenKind kind, out string text) 28 | { 29 | kind = Kind; 30 | text = Text; 31 | } 32 | 33 | bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) 34 | { 35 | return destination.TryWrite($"{Kind}: '{Text}'", out charsWritten); 36 | } 37 | 38 | string IFormattable.ToString(string? format, IFormatProvider? formatProvider) 39 | { 40 | return $"{Kind}: '{Text}'"; 41 | } 42 | 43 | #region Helpers 44 | 45 | public readonly bool IsTrivia => Kind is CssTokenKind.Whitespace or CssTokenKind.Comment; 46 | 47 | public readonly bool IsBinaryOperator => (int)Kind > 30 && (int)Kind < 65; 48 | 49 | public readonly bool IsEqualityOperator => Kind is CssTokenKind.Equals or CssTokenKind.NotEquals; 50 | 51 | public readonly bool IsLogicalOperator => Kind is CssTokenKind.And or CssTokenKind.Or; 52 | 53 | #endregion 54 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/PseudoElementTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class PseudoElementTests 4 | { 5 | [Fact] 6 | public void A() 7 | { 8 | var css = StyleSheet.Parse( 9 | """ 10 | //= support Safari >= 5 11 | .block ::-webkit-input-placeholder { color: #cfcece ; font-weight: 400; } 12 | .block :-ms-input-placeholder { color: #cfcece ; font-weight: 400; } 13 | .block ::-moz-placeholder { color: #cfcece ; font-weight: 400; } 14 | .block :-moz-placeholder { color: #cfcece ; font-weight: 400; } 15 | """); 16 | 17 | Assert.Equal( 18 | """ 19 | .block ::-webkit-input-placeholder { 20 | color: #cfcece; 21 | font-weight: 400; 22 | } 23 | .block :-ms-input-placeholder { 24 | color: #cfcece; 25 | font-weight: 400; 26 | } 27 | .block ::-moz-placeholder { 28 | color: #cfcece; 29 | font-weight: 400; 30 | } 31 | .block :-moz-placeholder { 32 | color: #cfcece; 33 | font-weight: 400; 34 | } 35 | """, css.ToString(), ignoreLineEndingDifferences: true); 36 | } 37 | 38 | [Fact] 39 | public void B() 40 | { 41 | var css = StyleSheet.Parse( 42 | """ 43 | .block::after { content: '' } 44 | """); 45 | 46 | Assert.Equal( 47 | """ 48 | .block::after { 49 | content: ''; 50 | } 51 | """, css.ToString(), ignoreLineEndingDifferences: true); 52 | } 53 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/Scss/EachTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class EachTests 4 | { 5 | [Fact] 6 | public void A_to() 7 | { 8 | var sheet = StyleSheet.Parse( 9 | """ 10 | $sizes: 40px, 50px, 80px; 11 | 12 | @each $size in $sizes { 13 | .icon-#{$size} { font-size: $size; } 14 | } 15 | """); 16 | 17 | Assert.Equal( 18 | """ 19 | .icon-40px { 20 | font-size: 40px; 21 | } 22 | .icon-50px { 23 | font-size: 50px; 24 | } 25 | .icon-80px { 26 | font-size: 80px; 27 | } 28 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 29 | } 30 | 31 | [Fact] 32 | public void EachMap() 33 | { 34 | var sheet = StyleSheet.Parse( 35 | """ 36 | $font-weights: ("regular": 400, "medium": 500, "bold": 700); 37 | 38 | @each $name, $value in $font-weights { 39 | .font-#{$name} { font-weight: $value; } 40 | } 41 | """); 42 | 43 | var assignment = (CssAssignment)sheet.Children[0]; 44 | var map = (CssMap)assignment.Value; 45 | 46 | Assert.Equal(3, map.Count); 47 | Assert.Equal("400", map["regular"].ToString()); 48 | 49 | Assert.Equal( 50 | """ 51 | .font-regular { 52 | font-weight: 400; 53 | } 54 | .font-medium { 55 | font-weight: 500; 56 | } 57 | .font-bold { 58 | font-weight: 700; 59 | } 60 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/NodeKind.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Carbon.Css; 4 | 5 | [JsonConverter(typeof(JsonStringEnumConverter))] 6 | public enum NodeKind 7 | { 8 | Unknown = 0, 9 | 10 | Document, 11 | Comment, 12 | Rule, 13 | Expression, 14 | Declaration, 15 | Block, 16 | Function, 17 | Selector, 18 | Assignment, 19 | Variable, 20 | InterpolatedString, 21 | Reference, // & 22 | Sequence, 23 | 24 | // Values 25 | ValueList, 26 | Url, 27 | Number, 28 | String, 29 | Boolean, 30 | Color, 31 | Undefined, 32 | 33 | // Measurements 34 | Angle, 35 | Frequency, 36 | Length, 37 | Time, 38 | Percentage, 39 | Resolution, 40 | UnknownMeasurement, 41 | 42 | // Extensions 43 | Directive, 44 | 45 | // SCSS 46 | Mixin, 47 | Include, 48 | If, 49 | For, 50 | Each, 51 | While, 52 | Map 53 | } 54 | 55 | /* 56 | stylesheet : [ CDO | CDC | S | statement ]*; 57 | statement : ruleset | at-rule; 58 | at-rule : ATKEYWORD S* any* [ block | ';' S* ]; 59 | block : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*; 60 | ruleset : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*; 61 | selector : any+; 62 | declaration : property S* ':' S* value; 63 | property : IDENT; 64 | value : [ any | block | ATKEYWORD S* ]+; 65 | any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING 66 | | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES 67 | | DASHMATCH | ':' | FUNCTION S* [any|unused]* ')' 68 | | '(' S* [any|unused]* ')' | '[' S* [any|unused]* ']' 69 | ] S*; 70 | unused : block | ATKEYWORD S* | ';' S* | CDO S* | CDC S*; 71 | */ 72 | -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Rules/StyleRule.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | 4 | namespace Carbon.Css; 5 | 6 | public sealed class StyleRule(CssSelector selector) : CssRule 7 | { 8 | public StyleRule(string selectorText) 9 | : this(CssSelector.Parse(selectorText)) { } 10 | 11 | public StyleRule(string selectorText, IReadOnlyList children) 12 | : this(CssSelector.Parse(selectorText)) 13 | { 14 | foreach (var child in children) 15 | { 16 | child.Parent = this; 17 | 18 | base.Children.Add(child); 19 | } 20 | } 21 | 22 | public override RuleType Type => RuleType.Style; 23 | 24 | public CssSelector Selector { get; } = selector; 25 | 26 | public int Depth { get; set; } 27 | 28 | public override StyleRule CloneNode() 29 | { 30 | var clone = new StyleRule(Selector) { 31 | Depth = Depth, 32 | Flags = Flags 33 | }; 34 | 35 | foreach (var child in Children) 36 | { 37 | clone.Add(child.CloneNode()); 38 | } 39 | 40 | return clone; 41 | } 42 | 43 | public override string ToString() 44 | { 45 | var sb = StringBuilderCache.Acquire(); 46 | 47 | using var sw = new StringWriter(sb); 48 | 49 | var writer = new CssWriter(sw); 50 | 51 | writer.WriteStyleRule(this, 0); 52 | 53 | return StringBuilderCache.ExtractAndRelease(sb); 54 | } 55 | 56 | public void WriteTo(TextWriter writer) 57 | { 58 | new CssWriter(writer).WriteStyleRule(this, 0); 59 | } 60 | 61 | #region Add Helper 62 | 63 | public void Add(string name, string value) 64 | { 65 | _children.Add(new CssDeclaration(name, value)); 66 | } 67 | 68 | #endregion 69 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/parts/header.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | header { 4 | display: block; 5 | height: 44px; 6 | padding: 23px 0; 7 | margin: 0; 8 | background: $backgroundColorAlt; 9 | 10 | p { 11 | font-size: 16px; 12 | line-height: 44px; 13 | text-align: center; 14 | color: $smallColor; 15 | } 16 | 17 | .inner { 18 | position: relative; 19 | max-width: 1020px; 20 | margin: 0 auto; 21 | padding: 0 30px; 22 | box-sizing: border-box; 23 | 24 | .col-1 { 25 | float: left; 26 | position: absolute; 27 | z-index: 1; 28 | } 29 | } 30 | } 31 | 32 | a.back { 33 | position: relative; 34 | display: inline-block; 35 | box-shadow: 0 0 0 1px $borderColor !important; 36 | height: 44px; 37 | line-height: if($fontScheme == sans, 44px, 48px); 38 | padding: 0 20px 0 35px; 39 | font-size: 14px; 40 | color: $arrowColor; 41 | font-weight: 400; 42 | text-align: right; 43 | border-bottom: none !important; 44 | transition: color 0.2s ease-in-out, box-shadow 0.2s ease-in-out; 45 | 46 | &:before { 47 | position: absolute; 48 | top: 18px; left: 15px; 49 | content: ' '; 50 | display: block; 51 | width: 10px; 52 | height: 1px; 53 | background: $arrowColor; 54 | transform: rotate(-45deg); 55 | transition: background 0.2s ease-in-out; 56 | } 57 | 58 | &:after { 59 | position: absolute; 60 | bottom: 18px; left: 15px; 61 | content: ' '; 62 | display: block; 63 | width: 10px; 64 | height: 1px; 65 | background: $arrowColor; 66 | transform: rotate(45deg); 67 | transition: background 0.2s ease-in-out; 68 | } 69 | } 70 | 71 | a.back:hover:before, 72 | a.back:hover:after { 73 | background: rgba($accentColor, 1) !important; 74 | } 75 | 76 | a.back:hover { color: $accentColor !important; box-shadow: 0 0 0 1px rgba($accentColor, 0.5) !important; } 77 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/components/animations.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | .hide { 4 | position: absolute; 5 | top: 20px; 6 | right: 20px; 7 | display: block; 8 | width: 40px; 9 | height: 40px; 10 | line-height: 40px; 11 | font-size: 40px; 12 | cursor: pointer; 13 | z-index: 12; 14 | border-bottom: none !important; 15 | box-shadow: none !important; 16 | text-align: center; 17 | visibility: hidden; 18 | opacity: 0; 19 | transition: visibility 0.1s ease, opacity 0.1s ease; 20 | transform: rotate(-90deg); 21 | 22 | > svg { 23 | height: 30px; 24 | width: 30px; 25 | stroke-width: 5px; 26 | stroke: $accentColor; 27 | 28 | &:hover { 29 | stroke: $textColor; 30 | } 31 | } 32 | } 33 | 34 | .aboutOpened { 35 | .hide { 36 | visibility: visible; 37 | opacity: 1; 38 | } 39 | 40 | .mainInfo { 41 | background: $backgroundColor; 42 | transition: background 0.3s ease; 43 | } 44 | 45 | #aboutLink { 46 | color: #fff !important; 47 | box-shadow: 0 0 0 1px $accentColor !important; 48 | background: $accentColor; 49 | } 50 | 51 | .about { 52 | background: $backgroundColorAlt; 53 | transition: visibility 0.15s ease, opacity 0.15s ease; 54 | visibility: visible; 55 | opacity: 1; 56 | z-index: 9; 57 | } 58 | } 59 | 60 | .contactOpened { 61 | .hide { 62 | visibility: visible; 63 | opacity: 1; 64 | } 65 | 66 | .mainInfo { 67 | transition: background 0.3s ease; 68 | background: $backgroundColor; 69 | } 70 | 71 | #contactLink { 72 | color: #fff !important; 73 | box-shadow: 0 0 0 1px $accentColor !important; 74 | background: $accentColor; 75 | } 76 | 77 | .contact { 78 | background: $backgroundColorAlt; 79 | transition: visibility 0.1s ease, opacity 0.1s ease; 80 | visibility: visible; 81 | opacity: 1; 82 | z-index: 9; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/mixins.css: -------------------------------------------------------------------------------- 1 | @mixin column($count, $gap) { 2 | -webkit-column-count: $count; 3 | -moz-column-count: $count; 4 | -ms-column-count: $count; 5 | column-count: $count; 6 | -webkit-column-gap: $gap; 7 | -moz-column-gap: $gap; 8 | -ms-column-gap: $gap; 9 | column-gap: $gap; 10 | } 11 | 12 | 13 | @mixin transform($property) { 14 | -webkit-transform: $property; 15 | -moz-transform: $property; 16 | -ms-transform: $property; 17 | transform: $property; 18 | } 19 | 20 | @mixin transition($property, $duration, $function, $delay: 0) { 21 | -webkit-transition: $property $duration $function $delay; 22 | -moz-transition: $property $duration $function $delay; 23 | -ms-transition: $property $duration $function $delay; 24 | transition: $property $duration $function $delay; 25 | } 26 | 27 | @mixin prefix-transition($property, $duration, $function, $delay: 0) { 28 | -webkit-transition: -webkit-$property $duration $function $delay; 29 | -moz-transition: -moz-$property $duration $function $delay; 30 | -ms-transition: -ms-$property $duration $function $delay; 31 | transition: $property $duration $function $delay; 32 | } 33 | 34 | @mixin gradient-linear($start, $end) { 35 | background-image: -webkit-gradient(linear, left top, left bottom, from($start), to($end)); 36 | background-image: -webkit-linear-gradient(top, $start, $end); 37 | background-image: -moz-linear-gradient(top, $start, $end); 38 | background-image: -o-linear-gradient(top, $start, $end); 39 | background-image: -ms-linear-gradient(top, $start, $end); 40 | background-image: linear-gradient(top, $start, $end); 41 | } 42 | 43 | @mixin box-sizing($property) { 44 | -webkit-box-sizing: $property; 45 | -moz-box-sizing: $property; 46 | -ms-box-sizing: $property; 47 | box-sizing: $property; 48 | } 49 | -------------------------------------------------------------------------------- /src/Carbon.Css/PseudoClassNames.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Frozen; 2 | 3 | namespace Carbon.Css; 4 | 5 | public static class PseudoClassNames 6 | { 7 | private static readonly FrozenSet s_items = FrozenSet.ToFrozenSet([ 8 | "active", 9 | "any", 10 | "any-link", 11 | "autofill", 12 | "blank", // experimental 13 | "buffering", 14 | "checked", 15 | "current", 16 | "default", 17 | "defined", 18 | "dir", 19 | "disabled", 20 | "empty", 21 | "enabled", 22 | "first", 23 | "first-child", 24 | "first-of-type", 25 | "first-line", 26 | "fullscreen", 27 | "focus", 28 | "focus-visible", 29 | "focus-within", 30 | "has", 31 | "host", 32 | "hover", 33 | "indeterminate", 34 | "in-range", 35 | "invalid", 36 | "is", 37 | "lang", 38 | "last-child", 39 | "last-of-type", 40 | "left", 41 | "link", 42 | "local-link", 43 | "modal", 44 | "not", 45 | "nth-child", 46 | "nth-last-child", 47 | "nth-last-of-type", 48 | "nth-of-type", 49 | "only-child", 50 | "only-of-type", 51 | "optional", 52 | "out-of-range", 53 | "past", 54 | "paused", 55 | "picture-in-picture", 56 | "placeholder-shown", 57 | "playing", 58 | "popover-open", 59 | "read-only", 60 | "read-write", 61 | "required", 62 | "right", 63 | "root", 64 | "scope", 65 | "seeking", 66 | "stalled", 67 | "target", 68 | "target-within", 69 | "user-invalid", 70 | "user-valid", 71 | "valid", 72 | "visited", 73 | "volume-locked", 74 | "where" 75 | ]); 76 | 77 | public static bool Contains(string name) => s_items.Contains(name); 78 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/CssUnitValueTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | 4 | namespace Carbon.Css.Tests; 5 | 6 | public class CssUnitValueTests 7 | { 8 | [Fact] 9 | public void CanRoundtrip() 10 | { 11 | var value = new CssUnitValue(100, CssUnitInfo.Px); 12 | 13 | Assert.Equal("\"100px\"", JsonSerializer.Serialize(value)); 14 | Assert.Equal(value, JsonSerializer.Deserialize("\"100px\"")); 15 | } 16 | 17 | [Theory] 18 | [InlineData("1px", 1d, "px")] 19 | [InlineData("100em", 100d, "em")] 20 | [InlineData("0.5rem", 0.5d, "rem")] 21 | [InlineData("1000000000deg", 1000000000d, "deg")] 22 | public void ParseUtf8(string text, double value, string unit) 23 | { 24 | var result = CssUnitValue.Parse(Encoding.UTF8.GetBytes(text)); 25 | 26 | Assert.Equal(value, result.Value); 27 | Assert.Equal(unit, result.Unit.Name); 28 | 29 | var json = $"\"{text}\""; 30 | 31 | Assert.Equal(json, JsonSerializer.Serialize(result)); 32 | Assert.Equal(result, JsonSerializer.Deserialize(json)); 33 | Assert.Equal(result, CssUnitValue.Parse(text)); 34 | } 35 | 36 | [Fact] 37 | public void ParseUtf8_Space() 38 | { 39 | var result = CssUnitValue.Parse("1 px"u8); 40 | 41 | Assert.Equal(1, result.Value); 42 | Assert.Same(CssUnitInfo.Px, result.Unit); 43 | } 44 | 45 | [Fact] 46 | public void ParseUtf_Unitless() 47 | { 48 | var result = CssUnitValue.Parse("-1234567890"u8); 49 | 50 | Assert.Equal(-1234567890, result.Value); 51 | Assert.Same(CssUnitInfo.Number, result.Unit); 52 | } 53 | 54 | [Fact] 55 | public void ParseUtf_NoDecimal() 56 | { 57 | var result = CssUnitValue.Parse("1234567890.0123456789"u8); 58 | 59 | Assert.Equal(1234567890.0123456789d, result.Value); 60 | Assert.Same(CssUnitInfo.Number, result.Unit); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/ContainerRuleTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class ContainerRuleTests 4 | { 5 | [Fact] 6 | public void A() 7 | { 8 | var sheet = StyleSheet.Parse( 9 | """ 10 | @container (width > 150px) { 11 | div { background: red; } 12 | } 13 | """); 14 | 15 | Assert.True(sheet.Children[0] is ContainerRule); 16 | 17 | Assert.Equal( 18 | """ 19 | @container (width > 150px) { 20 | div { 21 | background: red; 22 | } 23 | } 24 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 25 | } 26 | 27 | [Fact] 28 | public void CanUseNamedContainerRules() 29 | { 30 | var sheet = StyleSheet.Parse( 31 | """ 32 | @container sidebar (min-width: 700px) { 33 | .card { 34 | font-size: max(1.5em, 1.23em + 2cqi); 35 | } 36 | } 37 | """); 38 | 39 | Assert.True(sheet.Children[0] is ContainerRule); 40 | 41 | 42 | Assert.Equal( 43 | """ 44 | @container sidebar (min-width: 700px) { 45 | .card { 46 | font-size: max(1.5em, 1.23em + 2cqi); 47 | } 48 | } 49 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 50 | } 51 | 52 | [Fact] 53 | public void Q() 54 | { 55 | var sheet = StyleSheet.Parse( 56 | """ 57 | @container sidebar (min-width: 700px) { 58 | .card { 59 | font-size: 5px + 10px; 60 | } 61 | } 62 | """); 63 | 64 | Assert.Equal( 65 | """ 66 | @container sidebar (min-width: 700px) { 67 | .card { 68 | font-size: 15px; 69 | } 70 | } 71 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 72 | } 73 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/pages/about.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | .about { 4 | position: absolute; 5 | top: 0; 6 | padding: 0; 7 | width: 100%; 8 | 9 | transition: visibility 0.3s ease, opacity 0.3s ease; 10 | visibility: hidden; opacity: 0; 11 | z-index: 9; 12 | border-top: $border; 13 | 14 | .innerWrapper { 15 | max-width: 1060px; 16 | margin: 0 auto; 17 | min-height: 150px; 18 | } 19 | 20 | .inner { 21 | width: 100%; 22 | margin: 0 auto; 23 | padding: 0px; 24 | } 25 | 26 | .bio { 27 | margin: 0 auto; 28 | 29 | p { 30 | font-size: 21px; 31 | margin: 0 0 20px; 32 | 33 | &:last-child { margin: 0; } 34 | } 35 | } 36 | } 37 | 38 | .row { 39 | padding: 50px; 40 | width: 100%; 41 | margin: 0 auto; 42 | box-sizing: border-box; 43 | 44 | h4 { text-transform: uppercase; } 45 | 46 | table { 47 | padding: 0 0 .5em; 48 | border-spacing: 0; 49 | 50 | td { 51 | padding: 0; 52 | 53 | &:first-child { 54 | padding: 0 1em 0 0; 55 | width: 60px; 56 | } 57 | } 58 | } 59 | 60 | .item { margin: 0 0 40px; } 61 | 62 | .col-1 { 63 | width: 33%; 64 | float: left; 65 | 66 | p { 67 | margin:0 0 10px; 68 | text-align: left; 69 | } 70 | } 71 | 72 | .col-2 { 73 | width: 66%; 74 | float: right; 75 | 76 | .item { 77 | br { 78 | display: block; 79 | height: 40px; 80 | line-height: 40px; 81 | margin: 0; padding: 0; 82 | } 83 | 84 | .organization { margin: 0; color: $accentColor; } 85 | 86 | .title { margin: 0; } 87 | 88 | small { 89 | display: block; 90 | margin: 0px 0 20px; 91 | 92 | p { 93 | margin: 0; 94 | color: $smallColor; 95 | } 96 | } 97 | 98 | -webkit-column-break-inside: avoid; 99 | break-inside: avoid-column; 100 | } 101 | 102 | p { margin: 0.5em 0; } 103 | } 104 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/base/forms.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | input, 4 | textarea { 5 | position: relative; 6 | width: 100%; 7 | font-size: 18px; 8 | border: none; 9 | border-bottom: $border; 10 | background: $inputBackgroundColor; 11 | padding: 14px; 12 | box-sizing: border-box; 13 | border-radius: 0; 14 | -webkit-font-smoothing: antialiased; 15 | color: $textColor; 16 | } 17 | 18 | input:focus, 19 | textarea:focus { 20 | outline: none; 21 | } 22 | 23 | input:focus, 24 | textarea:focus, 25 | .field.focused input { 26 | border-bottom: 1px solid rgba($accentColor, 0.5) !important; 27 | } 28 | 29 | textarea { resize: none; } 30 | 31 | button { 32 | display: inline-block; 33 | color: $accentColor; 34 | position: relative; 35 | margin: 0; 36 | padding: 0 20px; 37 | height: 46px; 38 | line-height: 46px; 39 | font-weight: 300; 40 | font-size: 16px; 41 | border: none; 42 | background: transparent; 43 | transition: background 0.2s ease-in-out, color 0.2s ease-in-out; 44 | cursor: pointer; 45 | box-shadow: inset 0 0 0 1px rgba($accentColor, .3); 46 | 47 | -webkit-font-smoothing: antialiased; 48 | 49 | &:focus { outline: none; } 50 | } 51 | 52 | button:hover { 53 | color: #fff; 54 | background: $accentColor; 55 | } 56 | 57 | .field { 58 | position: relative; 59 | margin: 0 0 20px; 60 | 61 | > label { display: none; } 62 | 63 | > .message { 64 | display: block; 65 | font-size: 12px; 66 | color: #ef6469; 67 | position: absolute; 68 | top: 0; 69 | right: 0; 70 | font-style: normal; 71 | visibility: hidden; 72 | opacity: 0; 73 | transition: margin 0.1s ease-out; 74 | margin-right: 30px; 75 | line-height: if($fontScheme == sans, 4.3em, 4.5em); 76 | } 77 | 78 | &.invalid { 79 | > .message { 80 | visibility: visible; 81 | opacity: 1; 82 | transition: margin 0.08s ease-out; 83 | margin-right: 15px; 84 | } 85 | 86 | > label { display: none; } 87 | } 88 | } 89 | 90 | // audio button 91 | carbon-player .control, 92 | carbon-player > .posterPlay { 93 | border-radius: 0; 94 | } 95 | -------------------------------------------------------------------------------- /src/Carbon.Css/Comptability/CssCompatibility.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public class CssCompatibility( 4 | CompatibilityTable prefixed = default, 5 | CompatibilityTable standard = default, 6 | bool patchValues = false) 7 | { 8 | public static readonly CssCompatibility All = new(); 9 | public static readonly CssCompatibility Default = new(); 10 | 11 | public CompatibilityTable Prefixed { get; } = prefixed; 12 | 13 | public CompatibilityTable Standard { get; } = standard; 14 | 15 | public bool PatchValues { get; } = patchValues; 16 | 17 | public virtual CssPatch GetPatch(CssDeclaration declaration, in BrowserInfo browser) 18 | { 19 | if (PatchValues) 20 | { 21 | return PatchFactory.PrefixNameAndValue.Patch(browser, declaration); 22 | } 23 | 24 | return PatchFactory.PrefixName.Patch(browser, declaration); 25 | } 26 | 27 | public virtual bool HasPatch(CssDeclaration declaration, in BrowserInfo browser) => IsPrefixed(browser); 28 | 29 | public bool IsPrefixed(in BrowserInfo browser) => browser.Type switch 30 | { 31 | BrowserType.Chrome => Prefixed.Chrome > 0f && !IsStandard(browser), 32 | BrowserType.Firefox => Prefixed.Firefox > 0f && !IsStandard(browser), 33 | BrowserType.Edge => Prefixed.Edge > 0f && !IsStandard(browser), 34 | BrowserType.Safari => Prefixed.Safari > 0f && !IsStandard(browser), 35 | _ => false 36 | }; 37 | 38 | public bool IsStandard(in BrowserInfo browser) => browser.Type switch 39 | { 40 | BrowserType.Chrome => Standard.Safari != 0 && Standard.Chrome <= browser.Version, 41 | BrowserType.Edge => Standard.Edge != 0 && Standard.Edge <= browser.Version, 42 | BrowserType.Firefox => Standard.Firefox != 0 && Standard.Firefox <= browser.Version, 43 | BrowserType.Safari => Standard.Safari != 0 && Standard.Safari <= browser.Version, 44 | _ => false 45 | }; 46 | 47 | public virtual bool HasPatches => Prefixed.IsDefined; 48 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/StartingStyleRules.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class StartingStyleRules 4 | { 5 | [Fact] 6 | public void CanParseRoot() 7 | { 8 | var sheet = StyleSheet.Parse( 9 | """ 10 | @starting-style { 11 | h1 { 12 | background-color: transparent; 13 | } 14 | } 15 | """); 16 | 17 | Assert.True(sheet.Children[0] is StartingStyleRule); 18 | 19 | Assert.Equal( 20 | """ 21 | @starting-style { 22 | h1 { 23 | background-color: transparent; 24 | } 25 | } 26 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 27 | } 28 | 29 | [Fact] 30 | public void B() 31 | { 32 | var sheet = StyleSheet.Parse( 33 | """ 34 | @starting-style { 35 | color: red; 36 | background-color: transparent; 37 | } 38 | """); 39 | 40 | Assert.True(sheet.Children[0] is StartingStyleRule); 41 | 42 | Assert.Equal( 43 | """ 44 | @starting-style { 45 | color: red; 46 | background-color: transparent; 47 | } 48 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 49 | } 50 | 51 | [Fact] 52 | public void CanNest() 53 | { 54 | var sheet = StyleSheet.Parse( 55 | """ 56 | div { 57 | @starting-style { 58 | color: red; 59 | background-color: transparent; 60 | } 61 | } 62 | """, new CssContext { SupportsNesting = true }); 63 | 64 | var style = (StyleRule)sheet.Children[0]; 65 | 66 | Assert.True(style[0] is StartingStyleRule); 67 | 68 | Assert.Equal( 69 | """ 70 | div { 71 | @starting-style { 72 | color: red; 73 | background-color: transparent; 74 | } 75 | } 76 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 77 | } 78 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/CssDeclaration.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Text; 3 | 4 | namespace Carbon.Css; 5 | 6 | public sealed class CssDeclaration : CssNode 7 | { 8 | public CssDeclaration(string name, string value, string? priority = null) 9 | : this(name, CssValue.Parse(value), priority) 10 | { } 11 | 12 | public CssDeclaration(string name, CssValue value, string? priority = null) 13 | : base(NodeKind.Declaration) 14 | { 15 | 16 | Value = value; 17 | Priority = priority; 18 | Info = CssProperty.Get(name); 19 | } 20 | 21 | public CssDeclaration(string name, CssValue value, NodeKind kind) 22 | : base(kind) 23 | { 24 | Info = CssProperty.Get(name); 25 | Value = value; 26 | } 27 | 28 | public CssDeclaration(CssProperty property, CssValue value, string? priority) 29 | : base(NodeKind.Declaration) 30 | { 31 | Info = property; 32 | Value = value; 33 | Priority = priority; 34 | } 35 | 36 | public string Name => Info.Name; 37 | 38 | public CssValue Value { get; } 39 | 40 | public CssProperty Info { get; } 41 | 42 | public string? Priority { get; } 43 | 44 | public override CssDeclaration CloneNode() => new(Info, (CssValue)Value.CloneNode(), Priority); 45 | 46 | public void WriteTo(StringBuilder sb) 47 | { 48 | sb.Append(Info.Name); 49 | sb.Append(": "); 50 | sb.Append(Value.ToString()); 51 | 52 | if (Priority is not null) 53 | { 54 | sb.Append(" !"); 55 | sb.Append(Priority); 56 | } 57 | } 58 | 59 | [SkipLocalsInit] 60 | public override string ToString() 61 | { 62 | // color: red !important 63 | 64 | var sb = new ValueStringBuilder(stackalloc char[64]); 65 | 66 | sb.Append(Info.Name); 67 | sb.Append(": "); 68 | Value.WriteTo(ref sb); 69 | 70 | if (Priority is not null) 71 | { 72 | sb.Append(" !"); 73 | sb.Append(Priority); 74 | } 75 | 76 | return sb.ToString(); 77 | } 78 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Model/CssScope.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | using Carbon.Css.Helpers; 4 | 5 | namespace Carbon.Css; 6 | 7 | public sealed class CssScope 8 | { 9 | private readonly IDictionary _items; 10 | 11 | public CssScope(IDictionary items) 12 | { 13 | _items = items; 14 | } 15 | 16 | public CssScope(CssScope? parent = null) 17 | { 18 | Parent = parent; 19 | _items = new Dictionary(); 20 | } 21 | 22 | public object? This { get; set; } 23 | 24 | public CssScope? Parent { get; } 25 | 26 | public CssValue this[string name] 27 | { 28 | get => GetValue(name); 29 | set => _items[name] = value; 30 | } 31 | 32 | public void Add(string name, CssValue value) 33 | { 34 | _items.Add(name, value); 35 | } 36 | 37 | public bool TryGetValue(string key, [NotNullWhen(true)] out CssValue? value) 38 | { 39 | return _items.TryGetValue(key, out value); 40 | } 41 | 42 | public CssValue GetValue(string name, int counter = 0) 43 | { 44 | if (counter > 50) 45 | { 46 | ThrowHelper.RecursionDetected(); 47 | } 48 | 49 | if (_items.TryGetValue(name, out CssValue? value)) 50 | { 51 | if (value.Kind == NodeKind.Variable) 52 | { 53 | var variable = (CssVariable)value; 54 | 55 | if (variable.Symbol == name) 56 | { 57 | ThrowHelper.SelfReferencing(); 58 | } 59 | 60 | return GetValue(variable.Symbol, counter + 1); 61 | } 62 | 63 | return value; 64 | } 65 | 66 | if (Parent is not null) 67 | { 68 | return Parent.GetValue(name, Count + 1); 69 | } 70 | else 71 | { 72 | return new CssUndefined(name); 73 | } 74 | } 75 | 76 | public int Count => _items.Count; 77 | 78 | public void Clear() 79 | { 80 | _items.Clear(); 81 | } 82 | 83 | public CssScope GetChildScope() => new(this); 84 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Helpers/NumberHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers.Text; 2 | using System.Globalization; 3 | 4 | namespace Carbon.Css.Helpers; 5 | 6 | internal static class NumberHelper 7 | { 8 | public static float ParseCssNumberAsF32(ReadOnlySpan text) 9 | { 10 | bool isPercentage = text[^1] is '%'; 11 | bool isDeg = false; 12 | 13 | if (isPercentage) 14 | { 15 | text = text[0..^1]; 16 | } 17 | else if (text is [.., 'd', 'e', 'g']) 18 | { 19 | isDeg = true; 20 | text = text[0..^3]; 21 | } 22 | 23 | if (!float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out float result)) 24 | { 25 | throw new Exception(text.ToString()); 26 | } 27 | 28 | if (isPercentage) 29 | { 30 | result /= 100f; 31 | } 32 | 33 | if (isDeg) 34 | { 35 | return FromDegrees(result); 36 | } 37 | 38 | return result; 39 | } 40 | 41 | private static float FromDegrees(float degrees) 42 | { 43 | degrees = degrees %= 360f; 44 | 45 | // - 360f 46 | 47 | // 0 - 6 48 | 49 | return degrees / 60f; 50 | } 51 | 52 | public static double ReadNumber(ReadOnlySpan text, out int read) 53 | { 54 | read = 0; 55 | 56 | // leading - 57 | if (text[0] is '-') 58 | { 59 | read++; 60 | } 61 | 62 | while (text.Length > read && (char.IsAsciiDigit(text[read]) || text[read] is '.')) 63 | { 64 | read++; 65 | } 66 | 67 | return double.Parse(text[..read], CultureInfo.InvariantCulture); 68 | } 69 | 70 | public static double ReadNumber(ReadOnlySpan text, out int read) 71 | { 72 | read = 0; 73 | 74 | // leading - 75 | if (text[0] is (byte)'-') 76 | { 77 | read++; 78 | } 79 | 80 | // 0-9 or . 81 | while (text.Length > read && text[read] is (>= 48 and <= 57) or 46) 82 | { 83 | read++; 84 | } 85 | 86 | _ = Utf8Parser.TryParse(text[..read], out double value, out _); 87 | 88 | return value; 89 | } 90 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Values/CssValueList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace Carbon.Css; 6 | 7 | // A list of component values 8 | 9 | public sealed class CssValueList( 10 | IList values, 11 | CssValueSeparator separator = CssValueSeparator.Comma) : CssValue(NodeKind.ValueList), IReadOnlyList 12 | { 13 | private readonly IList _items = values; 14 | 15 | public CssValueSeparator Separator { get; } = separator; 16 | 17 | public CssValue this[int index] => _items[index]; 18 | 19 | public int Count => _items.Count; 20 | 21 | public override CssNode CloneNode() 22 | { 23 | var clonedValues = new CssValue[_items.Count]; 24 | 25 | for (int i = 0; i < _items.Count; i++) 26 | { 27 | clonedValues[i] = (CssValue)_items[i].CloneNode(); 28 | } 29 | 30 | return new CssValueList(clonedValues, Separator); 31 | } 32 | 33 | internal override void WriteTo(TextWriter writer) 34 | { 35 | string separator = Separator is CssValueSeparator.Space ? " " : ", "; 36 | 37 | for (int i = 0; i < _items.Count; i++) 38 | { 39 | if (i > 0) 40 | { 41 | writer.Write(separator); 42 | } 43 | 44 | _items[i].WriteTo(writer); 45 | } 46 | } 47 | 48 | internal override void WriteTo(scoped ref ValueStringBuilder sb) 49 | { 50 | string separator = Separator is CssValueSeparator.Space ? " " : ", "; 51 | 52 | for (int i = 0; i < _items.Count; i++) 53 | { 54 | if (i > 0) 55 | { 56 | sb.Append(separator); 57 | } 58 | 59 | _items[i].WriteTo(ref sb); 60 | } 61 | } 62 | 63 | public override string ToString() 64 | { 65 | return string.Join(Separator is CssValueSeparator.Space ? " " : ", ", _items); 66 | } 67 | 68 | #region IEnumerator 69 | 70 | IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator(); 71 | 72 | IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator(); 73 | 74 | #endregion 75 | } 76 | -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Selectors/Selector.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | using Carbon.Css.Parser; 4 | 5 | namespace Carbon.Css.Selectors; 6 | 7 | public class Selector : CssNode 8 | { 9 | public Selector(CssSelectorType type) 10 | : base(NodeKind.Selector) 11 | { 12 | Type = type; 13 | } 14 | 15 | public CssSelectorType Type { get; } 16 | 17 | public required string Text { get; set; } 18 | 19 | public CssValue? Arguments { get; set; } 20 | 21 | public CombinatorType Combinator { get; set; } 22 | 23 | public Selector? Next { get; set; } 24 | 25 | public bool HasNestingParent 26 | { 27 | get 28 | { 29 | if (Type is CssSelectorType.NestingParent) return true; 30 | 31 | Selector? test = this; 32 | 33 | while ((test = test.Next) != null) 34 | { 35 | if (test.Type is CssSelectorType.NestingParent) 36 | { 37 | return true; 38 | } 39 | } 40 | 41 | return false; 42 | } 43 | } 44 | 45 | // IsSimple 46 | // IsCompound 47 | 48 | public override string ToString() 49 | { 50 | var sb = new ValueStringBuilder(128); 51 | 52 | WriteTo(ref sb); 53 | 54 | return sb.ToString(); 55 | } 56 | 57 | internal void WriteTo(ref ValueStringBuilder sb) 58 | { 59 | sb.Append(Text); 60 | 61 | if (Arguments != null) 62 | { 63 | sb.Append('('); 64 | Arguments.WriteTo(ref sb); 65 | sb.Append(')'); 66 | } 67 | 68 | if (Next != null) 69 | { 70 | sb.Append(Combinator switch { 71 | CombinatorType.SubsequentSibling => " ~ ", 72 | CombinatorType.AdjacentSibling => " + ", 73 | CombinatorType.Descendant => " ", 74 | CombinatorType.Child => " > ", 75 | _ => "" 76 | }); 77 | 78 | Next.WriteTo(ref sb); 79 | } 80 | } 81 | 82 | public static CssSelector Parse(string text) 83 | { 84 | using var parser = new CssParser(text); 85 | 86 | return parser.ReadSelector(); 87 | } 88 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Model/CssContext.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public sealed class CssContext 4 | { 5 | private CompatibilityTable compatibility = default; 6 | 7 | private BrowserInfo[]? browserSupport = null; 8 | private Dictionary? _mixins; 9 | 10 | public Dictionary Mixins => _mixins ??= []; 11 | 12 | public BrowserInfo[]? BrowserSupport => browserSupport; 13 | 14 | public int MaxInlineSize { get; set; } 15 | 16 | // Firefox 117 17 | // Safar 17.? 18 | 19 | public bool SupportsNesting { get; set; } 20 | 21 | internal bool TryGetBrowser(BrowserType type, out BrowserInfo browser) 22 | { 23 | if (browserSupport != null) 24 | { 25 | for (int i = 0; i < browserSupport.Length; i++) 26 | { 27 | ref BrowserInfo b = ref browserSupport[i]; 28 | 29 | if (b.Type == type) 30 | { 31 | browser = b; 32 | 33 | return true; 34 | } 35 | } 36 | } 37 | 38 | browser = default; 39 | 40 | return false; 41 | } 42 | 43 | public CompatibilityTable Compatibility => compatibility; 44 | 45 | public void SetCompatibility(params BrowserInfo[] targets) 46 | { 47 | if (browserSupport is not null) return; 48 | 49 | browserSupport = targets; 50 | 51 | Array.Sort(browserSupport, static (a, b) => a.Prefix.Text.CompareTo(b.Prefix.Text)); 52 | 53 | float chrome = 0; 54 | float firefox = 0; 55 | float edge = 0; 56 | float safari = 0; 57 | 58 | foreach (var browser in browserSupport) 59 | { 60 | switch (browser.Type) 61 | { 62 | case BrowserType.Chrome : chrome = browser.Version; break; 63 | case BrowserType.Firefox : firefox = browser.Version; break; 64 | case BrowserType.Edge : edge = browser.Version; break; 65 | case BrowserType.Safari : safari = browser.Version; break; 66 | } 67 | } 68 | 69 | compatibility = new CompatibilityTable(chrome, edge, firefox, safari); 70 | } 71 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Comptability/CssCursor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Frozen; 2 | 3 | namespace Carbon.Css; 4 | 5 | // https://developer.mozilla.org/en-US/docs/Web/CSS/cursor 6 | 7 | public static class CssCursor 8 | { 9 | private static readonly CssCompatibility s_grabCursor = new( 10 | prefixed: new(firefox: 1.5f, safari: 4f), 11 | standard: new(chrome: 68, firefox: 27, edge: 15, safari: 11) 12 | ); 13 | 14 | private static readonly CssCompatibility s_zoomCursor = new( 15 | prefixed: new(safari: 3), 16 | standard: new(chrome: 37, edge: 12, firefox: 24, safari: 9) 17 | ); 18 | 19 | private static readonly FrozenDictionary s_table = new Dictionary() 20 | { 21 | ["auto"] = CssCompatibility.All, 22 | ["alias"] = CssCompatibility.All, 23 | ["copy"] = CssCompatibility.All, 24 | ["crosshair"] = CssCompatibility.All, 25 | 26 | ["default"] = CssCompatibility.All, 27 | ["e-resize"] = CssCompatibility.All, 28 | ["grab"] = s_grabCursor, 29 | ["grabbing"] = s_grabCursor, 30 | 31 | ["help"] = CssCompatibility.All, 32 | 33 | ["move"] = CssCompatibility.All, 34 | ["n-resize"] = CssCompatibility.All, 35 | ["ne-resize"] = CssCompatibility.All, 36 | ["nw-resize"] = CssCompatibility.All, 37 | 38 | ["pointer"] = CssCompatibility.All, 39 | ["progress"] = CssCompatibility.All, 40 | ["s-resize"] = CssCompatibility.All, 41 | ["se-resize"] = CssCompatibility.All, 42 | ["sw-resize"] = CssCompatibility.All, 43 | 44 | ["text"] = CssCompatibility.All, 45 | ["wait"] = CssCompatibility.All, 46 | ["w-resize"] = CssCompatibility.All, 47 | 48 | ["zoom-in"] = s_zoomCursor, 49 | ["zoom-out"] = s_zoomCursor 50 | }.ToFrozenDictionary(); 51 | 52 | public static bool NeedsPatch(string value, BrowserInfo browser) 53 | { 54 | if (s_table.TryGetValue(value, out CssCompatibility? c)) 55 | { 56 | if (c.IsPrefixed(browser)) 57 | { 58 | return true; 59 | } 60 | } 61 | 62 | return false; 63 | } 64 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/CssUnitTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class CssUnitTests 4 | { 5 | // switch = 224ms 6 | // dictionary = 149ms 7 | 8 | [Fact] 9 | public void Lookup() 10 | { 11 | 12 | Assert.Equal(CssUnitInfo.Px, CssUnitInfo.Get("px")); 13 | Assert.Equal(CssUnitInfo.Pt, CssUnitInfo.Get("pt")); 14 | Assert.Equal(CssUnitInfo.Deg, CssUnitInfo.Get("deg")); 15 | Assert.Equal(CssUnitInfo.Rlh, CssUnitInfo.Get("rlh")); 16 | Assert.Equal(CssUnitInfo.Rem, CssUnitInfo.Get("rem")); 17 | Assert.Equal(CssUnitInfo.Dvw, CssUnitInfo.Get("dvw")); 18 | Assert.Equal(CssUnitInfo.Dvh, CssUnitInfo.Get("dvh")); 19 | 20 | 21 | // 511ms -> 466ms 22 | for (int i = 0; i < 1_000_000; i++) 23 | { 24 | CssUnitInfo.Get("px"); 25 | CssUnitInfo.Get("pt"); 26 | CssUnitInfo.Get("deg"); 27 | CssUnitInfo.Get("rlh"); 28 | CssUnitInfo.Get("rem"); 29 | CssUnitInfo.Get("dvw"); 30 | CssUnitInfo.Get("dvh"); 31 | } 32 | } 33 | 34 | [Fact] 35 | public void RootElementTests() 36 | { 37 | Assert.Equal(CssUnitInfo.Rcap, CssUnitInfo.Get("rcap")); 38 | Assert.Equal(CssUnitInfo.Rex, CssUnitInfo.Get("rex")); 39 | Assert.Equal(CssUnitInfo.Rcap, CssUnitInfo.Get("rcap")); 40 | Assert.Equal(CssUnitInfo.Rch, CssUnitInfo.Get("rch")); 41 | } 42 | 43 | [Fact] 44 | public void Equality() 45 | { 46 | Assert.True(CssUnitInfo.Px.Equals(CssUnitInfo.Px)); 47 | Assert.False(CssUnitInfo.Pt.Equals(CssUnitInfo.Px)); 48 | } 49 | 50 | [Theory] 51 | [InlineData("px", NodeKind.Length)] 52 | [InlineData("pt", NodeKind.Length)] 53 | [InlineData("kHz", NodeKind.Frequency)] 54 | [InlineData("s", NodeKind.Time)] 55 | [InlineData("dvh", NodeKind.Length)] 56 | [InlineData("dvw", NodeKind.Length)] 57 | public void KindIsCorrect(string text, NodeKind kind) 58 | { 59 | Assert.Equal(kind, CssUnitInfo.Get(text).Kind); 60 | } 61 | 62 | [Fact] 63 | public void UnknownUnitEquality() 64 | { 65 | Assert.True(CssUnitInfo.Get("ns").Equals(CssUnitInfo.Get("ns"))); 66 | Assert.False(CssUnitInfo.Get("ns").Equals(CssUnitInfo.Get("ms"))); 67 | } 68 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Gradients/ColorStop.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text.Json.Serialization; 3 | 4 | using Carbon.Color; 5 | using Carbon.Css.Helpers; 6 | 7 | namespace Carbon.Css.Gradients; 8 | 9 | [method: JsonConstructor] 10 | public readonly struct ColorStop(Rgba32 color, double? position) 11 | { 12 | [JsonPropertyName("color")] 13 | public readonly Rgba32 Color { get; } = color; 14 | 15 | [JsonPropertyName("position")] 16 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 17 | public readonly double? Position { get; } = position; 18 | 19 | public static ColorStop Parse(ReadOnlySpan text) 20 | { 21 | return Read(text, out _); 22 | } 23 | 24 | public static ColorStop Read(ReadOnlySpan text, out int read) 25 | { 26 | if (text.IsEmpty) 27 | { 28 | throw new ArgumentException("May not be empty", nameof(text)); 29 | } 30 | 31 | if (text.TryReadWhitespace(out read)) 32 | { 33 | text = text[read..]; 34 | } 35 | 36 | Rgba32 color = text.ReadColor(out int colorRead); 37 | text = text[colorRead..]; 38 | 39 | read += colorRead; 40 | 41 | // #000 42 | // rgba(255, 255, 255, 50%) 50% 43 | 44 | double? angle = null; 45 | 46 | if (text.Length > 0) 47 | { 48 | int commaIndex = text.IndexOf(','); 49 | 50 | if (commaIndex > -1) 51 | { 52 | text = text.Slice(0, commaIndex); 53 | 54 | read += commaIndex; 55 | } 56 | else 57 | { 58 | read += text.Length; 59 | } 60 | 61 | text = text.Trim(); 62 | 63 | if (text.Length > 0) 64 | { 65 | angle = double.Parse(text[0..^1], provider: CultureInfo.InvariantCulture) / 100d; 66 | } 67 | } 68 | 69 | return new ColorStop(color, angle); 70 | } 71 | 72 | public readonly override string ToString() 73 | { 74 | if (Position != null) 75 | { 76 | return string.Create(CultureInfo.InvariantCulture, $"{Color.ToHexString()} {Position.Value:0%}"); 77 | } 78 | else 79 | { 80 | return Color.ToHexString(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/Values/CssGapTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | 4 | namespace Carbon.Css.Tests; 5 | 6 | public class CssGapTests 7 | { 8 | [Fact] 9 | public void CanConstruct() 10 | { 11 | var gap = new CssGap(CssUnitValue.Parse("10%")); 12 | 13 | Assert.Equal("10%", gap.X.ToString()); 14 | Assert.Equal("10%", gap.Y.ToString()); 15 | } 16 | 17 | [Fact] 18 | public void CanParseSpan() 19 | { 20 | var gap = new CssGap(CssUnitValue.Parse(['1', '0', '%'])); 21 | 22 | Assert.Equal("10%", gap.X.ToString()); 23 | Assert.Equal("10%", gap.Y.ToString()); 24 | } 25 | 26 | [Fact] 27 | public void ParseSingleValue() 28 | { 29 | var gap = CssGap.Parse("10px"); 30 | 31 | Assert.Equal("10px", gap.X.ToString()); 32 | Assert.Equal("10px", gap.Y.ToString()); 33 | } 34 | 35 | [Theory] 36 | [InlineData("10px")] 37 | [InlineData("1em 2em")] 38 | [InlineData("20% 30%")] 39 | public void CanRoundtrip(string text) 40 | { 41 | var json = $"\"{text}\""; 42 | 43 | Assert.Equal(json, JsonSerializer.Serialize(CssGap.Parse(text))); 44 | Assert.Equal(json, JsonSerializer.Serialize(CssGap.Parse(Encoding.UTF8.GetBytes(text)))); 45 | 46 | var deserialized = JsonSerializer.Deserialize(json); 47 | 48 | Assert.Equal(text, deserialized.ToString()); 49 | Assert.Equal(CssGap.Parse(text), deserialized); 50 | 51 | } 52 | 53 | [Fact] 54 | public void ParseSingleValueUtf8() 55 | { 56 | var gap = CssGap.Parse("10px"u8); 57 | 58 | Assert.Equal("10px", gap.X.ToString()); 59 | Assert.Equal("10px", gap.Y.ToString()); 60 | } 61 | 62 | [Fact] 63 | public void ParseDecimalValue() 64 | { 65 | var gap = CssGap.Parse("10.5rem"); 66 | 67 | Assert.Equal("10.5rem", gap.X.ToString()); 68 | Assert.Equal("10.5rem", gap.Y.ToString()); 69 | } 70 | 71 | [Fact] 72 | public void ParseDoubleValue() 73 | { 74 | var gap = CssGap.Parse("10px 20px"); 75 | 76 | Assert.Equal("10px", gap.X.ToString()); 77 | Assert.Equal("20px", gap.Y.ToString()); 78 | } 79 | 80 | [Fact] 81 | public void ParseDoubleValueUtf8() 82 | { 83 | var gap = CssGap.Parse("10px 20px"u8); 84 | 85 | Assert.Equal("10px", gap.X.ToString()); 86 | Assert.Equal("20px", gap.Y.ToString()); 87 | } 88 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/test53.css: -------------------------------------------------------------------------------- 1 | /*============================================================ 2 | From elements 3 | ============================================================*/ 4 | .block ::-webkit-input-placeholder { color: #cfcece; font-weight: 400; } 5 | .block :-ms-input-placeholder { color: #cfcece; font-weight: 400; } 6 | .block ::-moz-placeholder { color: #cfcece; font-weight: 400; } 7 | .block :-moz-placeholder { color: #cfcece; font-weight: 400; } 8 | 9 | .block.empty ::-webkit-input-placeholder { color: #b5ada9; font-weight: 400; } 10 | .block.empty :-ms-input-placeholder { color: #b5ada9; font-weight: 400; } 11 | .block.empty ::-moz-placeholder { color: #b5ada9; font-weight: 400; } 12 | .block.empty :-moz-placeholder { color: #b5ada9; font-weight: 400; } 13 | 14 | @keyframes domainProcessing { 15 | 0% { box-shadow: inset 0 0 0 3px rgba(248, 202, 92, 1), 0 0 0 3px rgba(248, 202, 92, 0.6); } 16 | 50% { box-shadow: inset 0 0 0 3px rgba(248, 202, 92, 0.4), 0 0 0 3px rgba(248, 202, 92, 0.2); } 17 | 100% { box-shadow: inset 0 0 0 3px rgba(248, 202, 92, 1), 0 0 0 3px rgba(248, 202, 92, 0.2); } 18 | } 19 | 20 | @keyframes domainProcessing2 { 21 | 0% { border-color: rgba(248, 202, 92, 0.4); } 22 | 20% { border-color: rgba(248, 202, 92, 0.2); } 23 | 100% { border-color: rgba(248, 202, 92, 0.2); } 24 | } 25 | 26 | /*============================================================ 27 | block styles 28 | ============================================================*/ 29 | .block { 30 | position: relative; 31 | min-height: 60px; 32 | border-bottom: solid 1px #f4f4f4; 33 | background: #fff; 34 | z-index: 1; 35 | 36 | label { 37 | position: absolute; 38 | top: 10px; left: 0; 39 | width: 200px; 40 | line-height: 60px; 41 | font-size: 16px; 42 | font-weight: 500; 43 | text-align: right; 44 | color: #4c4c4c; 45 | user-select: none; 46 | z-index: 3; 47 | } 48 | 49 | .text, 50 | .placeholderText { 51 | position: relative; 52 | display: block; 53 | overflow: hidden; 54 | padding: 0 50px 0 225px; 55 | line-height: 60px; 56 | font-size: 18px; 57 | font-weight: 400; 58 | color: #333; 59 | text-overflow: ellipsis; 60 | white-space: nowrap; 61 | -webkit-font-smoothing: antialiased; 62 | z-index: 2; 63 | } 64 | 65 | .description { 66 | padding: 5px 250px 20px 225px; 67 | 68 | p { 69 | font-size: 15px; 70 | line-height: 20px; 71 | color: #a3a3a3; 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/CssSelectorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Parser.Tests; 2 | 3 | public class CssSelectorTests 4 | { 5 | [Theory] 6 | [InlineData(":nth-child(1)")] 7 | [InlineData(":nth-child(-1)")] 8 | [InlineData(":nth-child(+1)")] 9 | public void NthChildTests(string text) 10 | { 11 | var selector = CssSelector.Parse(text); 12 | 13 | Assert.Equal(text, selector.ToString()); 14 | } 15 | 16 | [Fact] 17 | public void ParseSelector() 18 | { 19 | var sheet = StyleSheet.Parse("div > h1 { width: 100px; }"); 20 | 21 | var style = sheet.Children[0] as StyleRule; 22 | 23 | var selector = style.Selector; 24 | 25 | var a = (CssString)selector[0][0]; 26 | var b = (CssString)selector[0][1]; 27 | 28 | Assert.Equal("div", a.Text); 29 | Assert.Equal(" ", a.Trailing[0].Text); 30 | 31 | Assert.Equal(">", b.Text); 32 | Assert.Equal(" ", b.Trailing[0].Text); 33 | 34 | Assert.Single(sheet.Children); 35 | Assert.Equal(RuleType.Style, style.Type); 36 | Assert.Equal("div > h1", style.Selector.ToString()); 37 | 38 | Assert.Single(style.Children); 39 | 40 | var x = (CssDeclaration)style[0]; 41 | 42 | Assert.Equal("width", x.Name.ToString()); 43 | Assert.Equal("100px", x.Value.ToString()); 44 | Assert.Equal( 45 | """ 46 | div > h1 { 47 | width: 100px; 48 | } 49 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 50 | } 51 | 52 | [Fact] 53 | public void A() 54 | { 55 | Assert.Equal("#a", CssSelector.Parse("#a").ToString()); 56 | Assert.Equal(".a", CssSelector.Parse(".a").ToString()); 57 | } 58 | 59 | [Fact] 60 | public void C() 61 | { 62 | Assert.Equal("#networkLinks .block .edit:before", CssSelector.Parse("#networkLinks .block .edit:before").ToString()); 63 | } 64 | 65 | [Fact] 66 | public void MultiSelector() 67 | { 68 | var selector = CssSelector.Parse("h1, h2, h3"); 69 | 70 | var h1 = selector[0]; 71 | var h2 = selector[1]; 72 | var h3 = selector[2]; 73 | 74 | Assert.Equal("h1", h1[0].ToString()); 75 | Assert.Equal("h2", h2[0].ToString()); 76 | Assert.Equal("h3", h3[0].ToString()); 77 | 78 | Assert.Null(h1[0].Trailing); 79 | Assert.Null(h2[0].Trailing); 80 | Assert.Null(h3[0].Trailing); 81 | 82 | Assert.Equal("h1, h2, h3", selector.ToString()); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Carbon.Css/Model/CssUnitType.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Carbon.Css; 4 | 5 | [JsonConverter(typeof(JsonStringEnumConverter))] 6 | public enum CssUnitType 7 | { 8 | None = 0, // Number | Percentage 9 | 10 | // Length 11 | Cm = 1, // centimeters 1cm = 96px/2.54 12 | Mm = 2, // millimeters 1mm = 1/10th of 1cm 13 | Q = 3, // quarter-millimeters 1Q = 1/40th of 1cm 14 | In = 4, // inches 1in = 2.54cm = 96px 15 | Pc = 5, // picas 1pc = 1/6th of 1in 16 | Pt = 6, // points 1pt = 1/72th of 1in 17 | Px = 7, // pixels 1px = 1/96th of 1in 18 | 19 | // Length // relative to 20 | Em = 10, // font size of the element 21 | Ex = 11, // x-height of the element’s font 22 | Cap = 12, // cap height (the nominal height of capital letters) of the element’s font 23 | Ch = 13, // average character advance of a narrow glyph in the element’s font, as represented by the “0” (ZERO, U+0030) glyph 24 | Ic = 14, // average character advance of a fullwidth glyph in the element’s font, as represented by the “水” (CJK water ideograph, U+6C34) glyph 25 | Rem = 15, // font size of the root element 26 | Lh = 16, // line height of the element 27 | Rlh = 17, // line height of the root element 28 | Vw = 18, // 1% of viewport’s width 29 | Vh = 19, // 1% of viewport’s height 30 | Vi = 20, // 1% of viewport’s size in the root element’s inline axis 31 | Vb = 21, // 1% of viewport’s size in the root element’s block axis 32 | Vmin = 22, // 1% of viewport’s smaller dimension 33 | Vmax = 23, // 1% of viewport’s larger dimension 34 | 35 | Cqw = 24, // 1% of a query container's width 36 | Cqh = 25, // 1% of a query container's height 37 | Cqi = 26, // 1% of a query container's inline size 38 | Cqb = 27, // 1% of a query container's block size 39 | Cqmin = 28, // The smaller value of either cqi or cqb 40 | Cqmax = 29, // The larger value of either cqi or cqb 41 | 42 | // Angle 43 | Deg = 30, 44 | Grad = 31, 45 | Rad = 32, 46 | Turn = 33, 47 | 48 | // Time 49 | S = 34, 50 | Ms = 35, 51 | 52 | // Frequency 53 | H = 40, 54 | Khz = 41, 55 | 56 | // Resolution 57 | Dpi = 50, 58 | Dpcm = 51, 59 | Dppx = 52, 60 | X = 53, // aka Dppx 61 | 62 | Custom = 100 63 | } 64 | 65 | // https://www.w3.org/TR/css-values/ -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/Values/CssUrlValue.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | 4 | namespace Carbon.Css; 5 | 6 | public readonly struct CssUrlValue 7 | { 8 | // url('') 9 | 10 | public CssUrlValue(string value) 11 | { 12 | ArgumentNullException.ThrowIfNull(value); 13 | 14 | Value = value; 15 | } 16 | 17 | public CssUrlValue(byte[] data, string contentType) 18 | { 19 | ArgumentNullException.ThrowIfNull(data); 20 | 21 | var sb = new ValueStringBuilder(12 + contentType.Length + (data.Length * 2)); 22 | 23 | sb.Append("data:"); 24 | sb.Append(contentType); 25 | sb.Append(";base64,"); 26 | sb.Append(Convert.ToBase64String(data)); 27 | 28 | Value = sb.ToString(); 29 | } 30 | 31 | public string Value { get; } 32 | 33 | public void WriteTo(TextWriter writer) 34 | { 35 | writer.Write("url('"); 36 | writer.Write(Value); 37 | writer.Write("')"); 38 | } 39 | 40 | public readonly override string ToString() 41 | { 42 | var sb = new StringWriter(); 43 | 44 | WriteTo(sb); 45 | 46 | return sb.ToString(); 47 | } 48 | 49 | public readonly bool IsPath => !Value.Contains(':'); // ! https:// 50 | 51 | public readonly bool IsExternal => !IsPath; 52 | 53 | public readonly string GetAbsolutePath(string basePath) /* /styles/ */ 54 | { 55 | if (Value.Length is 0) return null!; 56 | 57 | if (!IsPath) 58 | { 59 | throw new ArgumentException(string.Concat("Has scheme: ", Value.AsSpan(0, Value.IndexOf(':')))); 60 | } 61 | 62 | // Already absolute 63 | if (Value[0] is '/') 64 | { 65 | return Value.ToString(); 66 | } 67 | 68 | if (basePath[0] is '/') 69 | { 70 | basePath = basePath[1..]; 71 | } 72 | 73 | // TODO: Eliminate this allocation 74 | 75 | // https://dev/styles/ 76 | var baseUri = new Uri("https://dev/" + basePath); 77 | 78 | // Absolute path 79 | return new Uri(baseUri, relativeUri: Value).AbsolutePath; 80 | } 81 | 82 | private static ReadOnlySpan TrimChars => ['\'', '\"', '(', ')']; 83 | 84 | public static CssUrlValue Parse(ReadOnlySpan text) 85 | { 86 | if (text.StartsWith("url", StringComparison.Ordinal)) 87 | { 88 | text = text.Slice(3); 89 | } 90 | 91 | if (text[0] is '(' or '"' or '\'') 92 | { 93 | text = text.Trim(TrimChars); 94 | } 95 | 96 | return new CssUrlValue(text.ToString()); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/CssValueTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class CssValueTests 4 | { 5 | [Fact] 6 | public void Q() 7 | { 8 | var value = CssUnitValue.Parse("97.916666666666666666666666666667%"); 9 | 10 | Assert.Equal(97.916666666666666666666666666667d, value.Value); 11 | } 12 | 13 | [Fact] 14 | public void NumbersAreStoredAtDoublePrecision() 15 | { 16 | string text = "97.916666666666666666666666666667%"; 17 | 18 | var value = CssValue.Parse(text) as CssUnitValue; 19 | 20 | Assert.Equal(97.916666666666666666666666666667d, value.Value); 21 | Assert.Equal(97.916666666666666666666666666667d, CssUnitValue.Parse(text).Value); 22 | } 23 | 24 | [Fact] 25 | public void ParseValues() 26 | { 27 | var (value, unit) = (CssUnitValue)CssValue.Parse("14px"); 28 | 29 | Assert.Equal(14f, value); 30 | Assert.Equal("px", unit.Name); 31 | } 32 | 33 | [Fact] 34 | public void ParseValues2() 35 | { 36 | var (value, unit) = CssUnitValue.Parse("14px"); 37 | 38 | Assert.Equal(14f, value); 39 | Assert.Equal("px", unit.Name); 40 | } 41 | 42 | [Fact] 43 | public void ParsePx() 44 | { 45 | Assert.Equal("14px", CssValue.Parse("14px").ToString()); 46 | Assert.Equal("14px", CssUnitValue.Parse("14px").ToString()); 47 | } 48 | 49 | [Fact] 50 | public void A() 51 | { 52 | Assert.Equal("left", CssValue.Parse("left").ToString()); 53 | } 54 | 55 | [Fact] 56 | public void Px() 57 | { 58 | var (value, unit) = CssUnitValue.Parse("12px"); 59 | 60 | Assert.Equal(12d, value); 61 | Assert.Same(CssUnitInfo.Px, unit); 62 | } 63 | 64 | [Fact] 65 | public void Em() 66 | { 67 | var (value, unit) = CssUnitValue.Parse("50em"); 68 | 69 | Assert.Equal(50d, value); 70 | Assert.Same(CssUnitInfo.Em, unit); 71 | } 72 | 73 | [Fact] 74 | public void Percent() 75 | { 76 | Assert.Equal("50%", CssValue.Parse("50%").ToString()); 77 | Assert.Equal("50%", CssUnitValue.Parse("50%").ToString()); 78 | } 79 | 80 | [Fact] 81 | public void ValueList() 82 | { 83 | var value = CssValue.Parse("100px 100px 100px 100px") as CssValueList; 84 | 85 | Assert.Equal(4, value.Count); 86 | Assert.Equal("100px 100px 100px 100px", value.ToString()); 87 | } 88 | 89 | [Fact] 90 | public void Url() 91 | { 92 | Assert.Equal("hi.jpeg", CssUrlValue.Parse("url(hi.jpeg)").Value); 93 | Assert.Equal("hi.jpeg", CssUrlValue.Parse("url('hi.jpeg')").Value); 94 | } 95 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/CssColorTests.cs: -------------------------------------------------------------------------------- 1 | using Carbon.Color; 2 | 3 | namespace Carbon.Css.Tests; 4 | 5 | public class CssColorTests 6 | { 7 | [Theory] 8 | [InlineData("hsl(120deg 100% 50%)", "#0f0")] // green 9 | [InlineData("hsl(0deg 100% 50%)", "#f00")] // red 10 | [InlineData("hsl(240deg 100% 50%)", "#00f")] // blue 11 | [InlineData("hsla(240deg 100% 50% / 100%)", "#00f")] // blue 12 | public void CanParseHsl(string text, string expected) 13 | { 14 | var color = CssColor.Parse(text); 15 | 16 | Assert.Equal(expected, color.Value.ToRgba32().ToHexString()); 17 | } 18 | 19 | [Fact] 20 | public void CanParseRgb() 21 | { 22 | for (int i = 0; i < 100; i++) 23 | { 24 | var r = (byte)Random.Shared.Next(0, 255); 25 | var g = (byte)Random.Shared.Next(0, 255); 26 | var b = (byte)Random.Shared.Next(0, 255); 27 | 28 | var rgb = new Rgba32(r, g, b); 29 | 30 | var c1 = CssColor.Parse($"rgb({r},{g},{b})"); 31 | var c2 = CssColor.Parse($"rgb({r} {g} {b} / 100%)"); 32 | var c3 = CssColor.Parse($"rgb({r} {g} {b})"); 33 | var c4 = CssColor.Parse($"rgb({r} {g} {b} / 1)"); 34 | var c5 = CssColor.Parse($"rgba({r} {g} {b} / 1)"); 35 | 36 | Assert.Equal(rgb, c1.Value.ToRgba32()); 37 | Assert.Equal(rgb, c2.Value.ToRgba32()); 38 | Assert.Equal(rgb, c3.Value.ToRgba32()); 39 | Assert.Equal(rgb, c4.Value.ToRgba32()); 40 | Assert.Equal(rgb, c5.Value.ToRgba32()); 41 | } 42 | } 43 | } 44 | 45 | // color(rec2020 0.42053 0.979780 0.00579) 46 | 47 | // color(display-p3 34% 58% 73%) 48 | // color(display-p3 .34 .58 .73) 49 | // color(display-p3 34% 58% 73% / 50%) 50 | // color(display-p3 .34 .58 .73 / .5) 51 | // color(display-p3 100% 100% 100%) 52 | // color(display-p3 1 1 1) 53 | // color(display-p3 0% 0% 0%) 54 | // color(display-p3 0 0 0) 55 | // color(display-p3 none none none) 56 | // color(display-p3 0) 57 | // color(display-p3) 58 | 59 | // color(xyz-d65 22% 26% 53%) 60 | // color(xyz-d65 .22 .26 .53) 61 | // color(xyz-d65 .22 .26 .53 / 50%) 62 | // color(xyz-d65 .22 .26 .53 / .5) 63 | // color(xyz-d65 100% 100% 100%) 64 | // color(xyz-d65 1 1 1) 65 | // color(xyz-d65 0% 0% 0%) 66 | // color(xyz-d65 0 0 0) 67 | // color(xyz-d65 none none none) 68 | // color(xyz-d65) 69 | 70 | 71 | // | rgb(146.064 107.457 131.223) 72 | // | rgba 73 | // | hsl 74 | // | hsla 75 | // | hwb 76 | // | lab(29.69% 44.888% -29.04%) 77 | // | lch(60.2345 59.2 95.2) 78 | // | oklab(40.101% 0.1147 0.0453) 79 | // | oklch(78.3% 0.108 326.5) 80 | 81 | 82 | // https://codepen.io/argyleink/pen/RwyOyeq 83 | // https://www.bram.us/2020/04/27/colors-in-css-hello-space-separated-functional-color-notations/ -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/PlaceholderTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class PlaceholderTests 4 | { 5 | [Fact] 6 | public void InsideMediaBlock() 7 | { 8 | var css = StyleSheet.Parse( 9 | """ 10 | @media only screen and (max-width: 770px) { 11 | ::-webkit-input-placeholder { color: rgba(0,0,0,.2); -webkit-font-smoothing: antialiased; } 12 | :-ms-input-placeholder { color: red; } 13 | } 14 | """); 15 | 16 | Assert.Equal( 17 | """ 18 | @media only screen and (max-width: 770px) { 19 | ::-webkit-input-placeholder { 20 | color: rgba(0, 0, 0, 0.2); 21 | -webkit-font-smoothing: antialiased; 22 | } 23 | :-ms-input-placeholder { 24 | color: red; 25 | } 26 | } 27 | """, css.ToString(), ignoreLineEndingDifferences: true); 28 | } 29 | 30 | [Fact] 31 | public void PlaceholderTests1() 32 | { 33 | var css = StyleSheet.Parse( 34 | """ 35 | //= support Safari >= 5 36 | .block ::-webkit-input-placeholder { color: #cfcece ; font-weight: 400; } 37 | .block :-ms-input-placeholder { color: #cfcece ; font-weight: 400; } 38 | .block ::-moz-placeholder { color: #cfcece ; font-weight: 400; } 39 | .block :-moz-placeholder { color: #cfcece ; font-weight: 400; } 40 | """); 41 | 42 | Assert.Equal( 43 | """ 44 | .block ::-webkit-input-placeholder { 45 | color: #cfcece; 46 | font-weight: 400; 47 | } 48 | .block :-ms-input-placeholder { 49 | color: #cfcece; 50 | font-weight: 400; 51 | } 52 | .block ::-moz-placeholder { 53 | color: #cfcece; 54 | font-weight: 400; 55 | } 56 | .block :-moz-placeholder { 57 | color: #cfcece; 58 | font-weight: 400; 59 | } 60 | """, css.ToString(), ignoreLineEndingDifferences: true); 61 | } 62 | 63 | [Fact] 64 | public void PlaceholderTest2() 65 | { 66 | var css = StyleSheet.Parse( 67 | """ 68 | //= support Safari >= 5 69 | .block ::placeholder { color: #cfcece ; font-weight: 400; } 70 | """); 71 | 72 | Assert.Equal( 73 | """ 74 | .block ::placeholder { 75 | color: #cfcece; 76 | font-weight: 400; 77 | } 78 | """, css.ToString(), ignoreLineEndingDifferences: true); 79 | } 80 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Ast/CssBlock.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace Carbon.Css; 4 | 5 | public class CssBlock : CssNode, IEnumerable 6 | { 7 | protected readonly List _children; 8 | 9 | public CssBlock(NodeKind kind) 10 | : base(kind) 11 | { 12 | _children = []; 13 | } 14 | 15 | public CssBlock(NodeKind kind, List children) 16 | : base(kind) 17 | { 18 | _children = children; 19 | } 20 | 21 | public List Children => _children; 22 | 23 | public CssBlockFlags Flags { get; set; } 24 | 25 | internal bool IsSimple => Flags == default; 26 | 27 | internal bool IsComplex => Flags != default; 28 | 29 | public bool HasChildren => _children.Count > 0; 30 | 31 | public CssDeclaration? GetDeclaration(string name) 32 | { 33 | foreach (var child in _children) 34 | { 35 | if (child is CssDeclaration declaration && declaration.Name.Equals(name, StringComparison.Ordinal)) 36 | { 37 | return declaration; 38 | } 39 | } 40 | 41 | return null; 42 | } 43 | 44 | #region List Members 45 | 46 | public int IndexOf(CssNode node) => _children.IndexOf(node); 47 | 48 | public void Insert(int index, CssNode item) 49 | { 50 | item.Parent = this; 51 | 52 | _children.Insert(index, item); 53 | } 54 | 55 | public void RemoveAt(int index) => _children.RemoveAt(index); 56 | 57 | public CssNode this[int index] 58 | { 59 | get => _children[index]; 60 | set => _children[index] = value; 61 | } 62 | 63 | public void Add(CssNode node) 64 | { 65 | node.Parent = this; 66 | 67 | _children.Add(node); 68 | } 69 | 70 | public void AddRange(List nodes) 71 | { 72 | foreach (var node in nodes) 73 | { 74 | node.Parent = this; 75 | } 76 | 77 | _children.AddRange(nodes); 78 | } 79 | 80 | public bool Remove(CssNode item) => _children.Remove(item); 81 | 82 | IEnumerator IEnumerable.GetEnumerator() => _children.GetEnumerator(); 83 | 84 | IEnumerator IEnumerable.GetEnumerator() => _children.GetEnumerator(); 85 | 86 | #endregion 87 | } 88 | 89 | // A block starts with a left curly brace ({) and ends with the matching right curly brace (}). 90 | // In between there may be any tokens, except that parentheses (( )), brackets ([ ]), and braces ({ }) must always occur in matching pairs and may be nested. 91 | // Single (') and double quotes (") must also occur in matching pairs, and characters between them are parsed as a string. 92 | // See Tokenization above for the definition of a string. -------------------------------------------------------------------------------- /src/Carbon.Css/Model/ValueList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.IO; 3 | using System.Runtime.CompilerServices; 4 | using System.Text; 5 | 6 | namespace Carbon.Css; 7 | 8 | public sealed class CssSequence : CssValue, IEnumerable 9 | { 10 | private readonly List _children; 11 | 12 | public CssSequence() 13 | : base(NodeKind.Sequence) 14 | { 15 | _children = []; 16 | } 17 | 18 | public CssSequence(int capacity) 19 | : base(NodeKind.Sequence) 20 | { 21 | _children = new List(capacity); 22 | } 23 | 24 | public CssSequence(params CssValue[] items) 25 | : base(NodeKind.Sequence) 26 | { 27 | _children = new List(items); 28 | } 29 | 30 | public void Add(CssValue item) 31 | { 32 | _children.Add(item); 33 | } 34 | 35 | public void AddRange(IEnumerable items) 36 | { 37 | _children.AddRange(items); 38 | } 39 | 40 | public int Count => _children.Count; 41 | 42 | public CssValue this[int index] => _children[index]; 43 | 44 | [SkipLocalsInit] 45 | public override string ToString() 46 | { 47 | var sb = new ValueStringBuilder(stackalloc char[64]); 48 | 49 | WriteTo(ref sb); 50 | 51 | return sb.ToString(); 52 | } 53 | 54 | internal override void WriteTo(scoped ref ValueStringBuilder sb) 55 | { 56 | for (int i = 0; i < _children.Count; i++) 57 | { 58 | CssValue item = _children[i]; 59 | 60 | item.WriteTo(ref sb); 61 | 62 | // Skip trailing trivia 63 | if ((item.Trailing != null || item.Kind is NodeKind.Sequence) && (i + 1) != _children.Count) 64 | { 65 | sb.Append(' '); 66 | } 67 | } 68 | } 69 | 70 | internal override void WriteTo(TextWriter writer) 71 | { 72 | for (int i = 0; i < _children.Count; i++) 73 | { 74 | CssValue item = _children[i]; 75 | 76 | item.WriteTo(writer); 77 | 78 | // Skip trailing trivia 79 | if ((item.Trailing != null || item.Kind is NodeKind.Sequence) && (i + 1) != _children.Count) 80 | { 81 | writer.Write(' '); 82 | } 83 | } 84 | } 85 | 86 | public bool Contains(NodeKind kind) 87 | { 88 | foreach (var token in _children) 89 | { 90 | if (token.Kind == kind) return true; 91 | } 92 | 93 | return false; 94 | } 95 | 96 | public IEnumerator GetEnumerator() => _children.GetEnumerator(); 97 | 98 | IEnumerator IEnumerable.GetEnumerator() => _children.GetEnumerator(); 99 | } 100 | -------------------------------------------------------------------------------- /src/Carbon.Css/_/CssBoxAlignmentExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public static class CssBoxAlignmentExtensions 4 | { 5 | public static string Canonicalize(this CssBoxAlignment alignment, BoxLayoutMode mode) 6 | { 7 | if (mode.HasFlag(BoxLayoutMode.Flex)) 8 | { 9 | switch (alignment) 10 | { 11 | case CssBoxAlignment.Start : return "flex-start"; 12 | case CssBoxAlignment.End : return "flex-end"; 13 | } 14 | } 15 | 16 | return Canonicalize(alignment); 17 | } 18 | 19 | public static string Canonicalize(this CssBoxAlignment alignment) => alignment switch 20 | { 21 | CssBoxAlignment.Start => "start", 22 | CssBoxAlignment.End => "end", 23 | CssBoxAlignment.Center => "center", 24 | 25 | CssBoxAlignment.SelfStart => "self-start", 26 | CssBoxAlignment.SelfEnd => "self-end", 27 | 28 | // Baseline 29 | CssBoxAlignment.Baseline => "baseline", 30 | CssBoxAlignment.FirstBaseline => "first baseline", 31 | CssBoxAlignment.LastBaseline => "last baseline", 32 | 33 | // Distributed 34 | CssBoxAlignment.SpaceAround => "space-around", 35 | CssBoxAlignment.SpaceBetween => "space-between", 36 | CssBoxAlignment.SpaceEvenly => "space-evenly", 37 | CssBoxAlignment.Stretch => "stretch", 38 | 39 | // Overflow 40 | CssBoxAlignment.SafeCenter => "safe center", 41 | CssBoxAlignment.UnsafeCenter => "unsafe center", 42 | 43 | _ => throw new Exception($"Unexpected alignment. Was {alignment}") 44 | }; 45 | 46 | public static CssBoxAlignment Parse(ReadOnlySpan text) => text switch 47 | { 48 | "flex-start" => CssBoxAlignment.Start, 49 | "flex-end" => CssBoxAlignment.End, 50 | "self-start" => CssBoxAlignment.SelfStart, 51 | "self-end" => CssBoxAlignment.SelfEnd, 52 | "start" => CssBoxAlignment.Start, 53 | "end" => CssBoxAlignment.End, 54 | "center" => CssBoxAlignment.Center, 55 | "baseline" => CssBoxAlignment.Baseline, 56 | "first baseline" => CssBoxAlignment.FirstBaseline, 57 | "last baseline" => CssBoxAlignment.LastBaseline, 58 | "space-around" => CssBoxAlignment.SpaceAround, 59 | "space-between" => CssBoxAlignment.SpaceBetween, 60 | "space-evenly" => CssBoxAlignment.SpaceEvenly, 61 | "stretch" => CssBoxAlignment.Stretch, 62 | "safe center" => CssBoxAlignment.SafeCenter, 63 | "unsafe center" => CssBoxAlignment.UnsafeCenter, 64 | _ => CssBoxAlignment.Unknown 65 | }; 66 | 67 | // TODO: left, right, top, bottom 68 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/data/webcat/base/variables.scss: -------------------------------------------------------------------------------- 1 | //= partial 2 | 3 | // Colors -------------------------------------------------------- 4 | $backgroundColor: #f2f2f2; 5 | $backgroundColorAlt: #f2f2f2; 6 | 7 | $maskColor: rgba(255, 255, 255, 0.9); 8 | 9 | $textColor: #333; 10 | $linkUnderlineColor: rgba(0, 0, 0, 0.15); 11 | $captionColor: rgba($textColor, 0.6); 12 | $smallColor: rgba($textColor, 0.6); 13 | $quoteColor: rgba($textColor, 0.3); 14 | $fontSmoothing: inherit; 15 | 16 | $borderColor: rgba(0, 0, 0, 0.1); 17 | $border: 1px solid rgba(0, 0, 0, 0.08); 18 | $arrowColor: rgba(0, 0, 0, 0.3); 19 | 20 | $inputBackgroundColor: rgba(255, 255, 255, 0.8); 21 | 22 | // TODO: rename back to superLight once we've migrated 23 | @if $colorScheme == light { 24 | $backgroundColor: #ffffff; 25 | $backgroundColorAlt: #f9f9f9; 26 | 27 | $maskColor: rgba(255, 255, 255, 0.9); 28 | 29 | $textColor: #333; 30 | $linkUnderlineColor: rgba(0, 0, 0, 0.15); 31 | $captionColor: rgba(0, 0, 0, 0.5); 32 | $smallColor: rgba(0, 0, 0, 0.5); 33 | $quoteColor: rgba(0, 0, 0, 0.3); 34 | $fontSmoothing: inherit; 35 | 36 | $borderColor: rgba(0, 0, 0, 0.1); 37 | $border: 1px solid rgba(0, 0, 0, 0.08); 38 | $arrowColor: rgba(0, 0, 0, 0.3); 39 | 40 | $inputBackgroundColor: rgba(255, 255, 255, 0.8); 41 | } 42 | 43 | @if $colorScheme == dark { 44 | $backgroundColor: #2b2b2b; 45 | $backgroundColorAlt: #212121; 46 | 47 | $maskColor: rgba(43, 43, 43, 0.9); 48 | 49 | $textColor: #eee; 50 | $linkUnderlineColor: rgba(255, 255, 255, 0.25); 51 | $captionColor: rgba(255, 255, 255, 0.5); 52 | $smallColor: rgba(255, 255, 255, 0.5); 53 | $quoteColor: rgba(255, 255, 255, 0.3); 54 | $fontSmoothing: antialiased; 55 | 56 | $borderColor: rgba(255, 255, 255,0.1); 57 | $border: 1px solid rgba(255, 255, 255, 0.05); 58 | $arrowColor: rgba(255, 255, 255, 0.3); 59 | 60 | $inputBackgroundColor: rgba(255, 255, 255, 0.02); 61 | } 62 | 63 | @if $colorScheme == superDark { 64 | $backgroundColor: #1a1a1a; 65 | $backgroundColorAlt: #111; 66 | 67 | $maskColor: rgba(0, 0, 0, 0.9); 68 | 69 | $textColor: #efefef; 70 | $linkUnderlineColor: rgba(255, 255, 255, 0.25); 71 | $captionColor: rgba(255, 255, 255, 0.5); 72 | $smallColor: rgba(255, 255, 255, 0.5); 73 | $quoteColor: rgba(255, 255, 255, 0.3); 74 | $fontSmoothing: antialiased; 75 | 76 | $borderColor: rgba(255, 255, 255, 0.1); 77 | $border: 1px solid rgba(255, 255, 255, 0.05); 78 | $arrowColor: rgba(255, 255, 255, 0.3); 79 | 80 | $inputBackgroundColor: rgba(255, 255, 255, 0.02); 81 | } 82 | 83 | $linkColor: $textColor; 84 | 85 | // Fonts -------------------------------------------------------- 86 | $fontFamily: 'Karla', Helvetica, Arial, sans-serif; // sans 87 | 88 | @if $fontScheme == serif { 89 | $fontFamily: 'Vollkorn', serif; // serif 90 | } 91 | 92 | @if $accentColor == undefined { 93 | $accentColor: #7eb54e; 94 | } -------------------------------------------------------------------------------- /src/Carbon.Css/_/CssPlacement.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Carbon.Css; 4 | 5 | [DataContract] 6 | public readonly struct CssPlacement : IEquatable 7 | { 8 | public CssPlacement(CssBoxAlignment value) 9 | { 10 | Justify = value; 11 | Align = value; 12 | } 13 | 14 | public CssPlacement(CssBoxAlignment align, CssBoxAlignment justify) 15 | { 16 | Align = align; 17 | Justify = justify; 18 | } 19 | 20 | // Cross Axis : Y (align) | when columns 21 | [DataMember(Name = "align", Order = 1)] 22 | public readonly CssBoxAlignment Align { get; } 23 | 24 | // Main Axis : X (justify) 25 | [DataMember(Name = "justify", Order = 2)] 26 | public readonly CssBoxAlignment Justify { get; } 27 | 28 | public static CssPlacement Parse(string text) 29 | { 30 | return Parse(text.AsSpan()); 31 | } 32 | 33 | public static CssPlacement Parse(ReadOnlySpan text) 34 | { 35 | // center center 36 | // top left 37 | // start end 38 | 39 | int spaceIndex = text.IndexOf(' '); 40 | 41 | if (spaceIndex == -1) 42 | { 43 | CssBoxAlignment value = CssBoxAlignmentExtensions.Parse(text); 44 | 45 | return new CssPlacement(value, value); 46 | } 47 | 48 | var lhs = text.Slice(0, spaceIndex); 49 | var rhs = text.Slice(spaceIndex + 1); 50 | 51 | CssBoxAlignment align = CssBoxAlignmentExtensions.Parse(lhs); 52 | CssBoxAlignment justify = CssBoxAlignmentExtensions.Parse(rhs); 53 | 54 | return new CssPlacement(align, justify); 55 | } 56 | 57 | // left, center, and right 58 | // top, center, and bottom 59 | 60 | public readonly bool Equals(CssPlacement other) 61 | { 62 | return Justify == other.Justify 63 | && Align == other.Align; 64 | } 65 | 66 | public override bool Equals(object? obj) 67 | { 68 | return obj is CssPlacement other && Equals(other); 69 | } 70 | 71 | public override int GetHashCode() 72 | { 73 | return HashCode.Combine(Justify, Align); 74 | } 75 | 76 | public readonly override string ToString() 77 | { 78 | if (Align == Justify) 79 | { 80 | return Align.Canonicalize(); 81 | } 82 | 83 | return Align.Canonicalize() + " " + Justify.Canonicalize(); 84 | } 85 | 86 | public static bool operator ==(CssPlacement left, CssPlacement right) 87 | { 88 | return left.Equals(right); 89 | } 90 | 91 | public static bool operator !=(CssPlacement left, CssPlacement right) 92 | { 93 | return !left.Equals(right); 94 | } 95 | } 96 | 97 | 98 | // y x 99 | // place-items : / / 101 | -------------------------------------------------------------------------------- /src/Carbon.Css/Gradients/LinearGradientDirectionHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Gradients; 2 | 3 | using static LinearGradientDirection; 4 | 5 | public static class LinearGradientDirectionHelper 6 | { 7 | public static bool TryParse(ReadOnlySpan text, out LinearGradientDirection result, out int read) 8 | { 9 | read = 0; 10 | 11 | if (text.Length < 3) 12 | { 13 | result = default; 14 | 15 | return false; 16 | } 17 | 18 | if (text.StartsWith("to ", StringComparison.Ordinal)) // 'to ' 19 | { 20 | read += 3; 21 | 22 | text = text[3..]; 23 | } 24 | 25 | if (text.StartsWith("top", StringComparison.Ordinal)) 26 | { 27 | read += 3; 28 | 29 | text = text[3..]; 30 | 31 | if (text.StartsWith(" left", StringComparison.Ordinal)) 32 | { 33 | read += 5; 34 | 35 | result = TopLeft; 36 | } 37 | else if (text.StartsWith(" right", StringComparison.Ordinal)) 38 | { 39 | read += 6; 40 | 41 | result = TopRight; 42 | } 43 | else 44 | { 45 | result = Top; 46 | } 47 | } 48 | else if (text.StartsWith("bottom", StringComparison.Ordinal)) 49 | { 50 | read += 6; 51 | 52 | text = text[6..]; 53 | 54 | if (text.StartsWith(" left", StringComparison.Ordinal)) 55 | { 56 | read += 5; 57 | 58 | result = Left; 59 | } 60 | else if (text.StartsWith(" right", StringComparison.Ordinal)) 61 | { 62 | read += 6; 63 | 64 | result = BottomRight; 65 | } 66 | else 67 | { 68 | result = Bottom; 69 | } 70 | } 71 | else if (text.StartsWith("left", StringComparison.Ordinal)) 72 | { 73 | read += 4; 74 | 75 | result = Left; 76 | } 77 | else if (text.StartsWith("right", StringComparison.Ordinal)) 78 | { 79 | read += 5; 80 | 81 | result = Right; 82 | } 83 | else 84 | { 85 | result = default; 86 | 87 | return false; 88 | } 89 | 90 | return true; 91 | } 92 | 93 | public static string Canonicalize(LinearGradientDirection value) => value switch 94 | { 95 | Bottom => "bottom", 96 | BottomLeft => "bottom left", 97 | BottomRight => "bottom right", 98 | Left => "left", 99 | Right => "right", 100 | Top => "top", 101 | TopLeft => "top left", 102 | TopRight => "top right", 103 | _ => throw new Exception($"Unexpected direction: {value}") 104 | }; 105 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Comptability/BrowserInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css; 2 | 3 | public readonly struct BrowserInfo(BrowserType type, float version) 4 | { 5 | public BrowserType Type { get; } = type; 6 | 7 | public float Version { get; } = version; 8 | 9 | public BrowserPrefix Prefix => GetPrefix(Type); 10 | 11 | public static BrowserInfo Chrome(float version) => new(BrowserType.Chrome, version); 12 | public static BrowserInfo Edge(float version) => new(BrowserType.Edge, version); 13 | 14 | public static BrowserInfo Firefox(float version) => new(BrowserType.Firefox, version); 15 | public static BrowserInfo Safari(float version) => new(BrowserType.Safari, version); 16 | 17 | public static readonly BrowserInfo Chrome1 = Chrome(1); 18 | public static readonly BrowserInfo Chrome4 = Chrome(4); 19 | public static readonly BrowserInfo Chrome7 = Chrome(7); 20 | public static readonly BrowserInfo Chrome10 = Chrome(10); 21 | public static readonly BrowserInfo Chrome13 = Chrome(13); 22 | public static readonly BrowserInfo Chrome26 = Chrome(26); 23 | public static readonly BrowserInfo Chrome36 = Chrome(36); 24 | public static readonly BrowserInfo Chrome50 = Chrome(50); 25 | 26 | public static readonly BrowserInfo Firefox1 = Firefox(1); 27 | public static readonly BrowserInfo Firefox4 = Firefox(4); 28 | public static readonly BrowserInfo Firefox5 = Firefox(5); 29 | public static readonly BrowserInfo Firefox6 = Firefox(6); 30 | public static readonly BrowserInfo Firefox9 = Firefox(9); 31 | public static readonly BrowserInfo Firefox10 = Firefox(10); 32 | public static readonly BrowserInfo Firefox16 = Firefox(16); 33 | public static readonly BrowserInfo Firefox20 = Firefox(20); 34 | public static readonly BrowserInfo Firefox21 = Firefox(21); 35 | public static readonly BrowserInfo Firefox29 = Firefox(29); 36 | 37 | public static readonly BrowserInfo Safari1 = Safari(1); 38 | public static readonly BrowserInfo Safari3 = Safari(3); 39 | public static readonly BrowserInfo Safari4 = Safari(4); 40 | public static readonly BrowserInfo Safari5 = Safari(5); 41 | public static readonly BrowserInfo Safari6 = Safari(6); 42 | public static readonly BrowserInfo Safari7 = Safari(7); 43 | public static readonly BrowserInfo Safari10 = Safari(10); 44 | public static readonly BrowserInfo Safari13 = Safari(13); 45 | public static readonly BrowserInfo Safari15 = Safari(15); 46 | public static readonly BrowserInfo Safari17 = Safari(17); 47 | 48 | public static BrowserPrefix GetPrefix(BrowserType type) => type switch 49 | { 50 | BrowserType.Chrome => BrowserPrefix.Webkit, 51 | BrowserType.Firefox => BrowserPrefix.Moz, 52 | BrowserType.Edge => BrowserPrefix.Webkit, // Edge is based on Chromium as of v88 53 | BrowserType.Safari => BrowserPrefix.Webkit, 54 | _ => throw new Exception($"Unexpected browser. Was {type}") 55 | }; 56 | 57 | public override string ToString() => $"{Type}/{Version}"; 58 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Model/TokenList.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | 6 | namespace Carbon.Css; 7 | 8 | using Parser; 9 | 10 | public sealed class TokenList : List 11 | { 12 | [SkipLocalsInit] 13 | public override string ToString() 14 | { 15 | if (Count is 1) 16 | { 17 | return this[0].IsTrivia ? string.Empty : this[0].Text; 18 | } 19 | else 20 | { 21 | var sb = new ValueStringBuilder(stackalloc char[64]); 22 | 23 | WriteTo(ref sb); 24 | 25 | return sb.ToString(); 26 | } 27 | } 28 | 29 | private void WriteTo(scoped ref ValueStringBuilder writer) 30 | { 31 | var span = CollectionsMarshal.AsSpan(this); 32 | 33 | for (int i = 0; i < Count; i++) 34 | { 35 | ref readonly CssToken token = ref span[i]; 36 | 37 | if (token.IsTrivia) 38 | { 39 | // Skip leading and trailing trivia 40 | if (i is 0 || i + 1 == span.Length) continue; 41 | 42 | writer.Append(' '); 43 | 44 | continue; 45 | } 46 | 47 | writer.Append(token.Text); 48 | } 49 | } 50 | 51 | public void WriteTo(TextWriter writer, CssScope scope) 52 | { 53 | var span = CollectionsMarshal.AsSpan(this); 54 | 55 | for (int i = 0; i < span.Length; i++) 56 | { 57 | ref readonly CssToken token = ref span[i]; 58 | bool isEnd = i + 1 == span.Length; 59 | 60 | if (token.Kind is CssTokenKind.Dollar && !isEnd) 61 | { 62 | string variableName = this[++i].Text; 63 | 64 | if (scope.TryGetValue(variableName, out var value)) 65 | { 66 | value.WriteTo(writer); 67 | } 68 | else 69 | { 70 | throw new Exception($"{variableName} not found"); 71 | } 72 | 73 | continue; 74 | } 75 | 76 | if (token.IsTrivia) 77 | { 78 | // Skip leading and trailing trivia 79 | if (i is 0 || isEnd) continue; 80 | 81 | writer.Write(' '); 82 | 83 | continue; 84 | } 85 | 86 | writer.Write(token.Text); 87 | } 88 | } 89 | 90 | public void WriteTo(TextWriter writer) 91 | { 92 | if (Count is 1) 93 | { 94 | if (this[0].IsTrivia) return; 95 | 96 | writer.Write(this[0].Text); 97 | 98 | return; 99 | } 100 | 101 | for (int i = 0; i < Count; i++) 102 | { 103 | CssToken token = this[i]; 104 | 105 | if (token.IsTrivia) 106 | { 107 | // Skip leading and trailing trivia 108 | if (i is 0 || i + 1 == Count) continue; 109 | 110 | writer.Write(' '); 111 | 112 | continue; 113 | } 114 | 115 | writer.Write(token.Text); 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /src/Carbon.Css/Parser/SourceReader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Runtime.CompilerServices; 3 | using System.Text; 4 | 5 | namespace Carbon.Css.Parser; 6 | 7 | internal sealed class SourceReader(TextReader textReader) : IDisposable 8 | { 9 | public const char EofChar = '\0'; 10 | 11 | private readonly TextReader _textReader = textReader; 12 | private char _current = '.'; 13 | private int _position = 0; 14 | 15 | private readonly StringBuilder sb = new(); 16 | 17 | private int _markStart = -1; 18 | private int _marked = -1; 19 | 20 | public char Current => _current; 21 | 22 | public bool IsEof => _current is EofChar; 23 | 24 | public int Position => _position; 25 | 26 | public char Peek() 27 | { 28 | int charCode = _textReader.Peek(); 29 | 30 | return (charCode > 0) ? (char)charCode : EofChar; 31 | } 32 | 33 | /// 34 | /// Returns the current character and advances to the next 35 | /// 36 | /// 37 | public char Read() 38 | { 39 | char c = _current; 40 | 41 | Advance(); 42 | 43 | return c; 44 | } 45 | 46 | /// 47 | /// Returns the current character and advances to the next 48 | /// 49 | /// 50 | internal string ReadSymbol(string result) 51 | { 52 | Advance(); 53 | 54 | return result; 55 | } 56 | 57 | [SkipLocalsInit] 58 | public string Read(int count) 59 | { 60 | Span buffer = count <= 8 61 | ? stackalloc char[8] 62 | : new char[count]; 63 | 64 | for (int i = 0; i < count; i++) 65 | { 66 | buffer[i] = _current; 67 | 68 | Advance(); 69 | } 70 | 71 | return new string(buffer[..2]); 72 | } 73 | 74 | /// 75 | /// Advances to the next character and returns it 76 | /// 77 | public void Advance() 78 | { 79 | if (IsEof) throw new Exception("Cannot read past EOF."); 80 | 81 | if (_marked != -1 && (_marked <= _position)) 82 | { 83 | sb.Append(_current); 84 | } 85 | 86 | int charCode = _textReader.Read(); // -1 if there are no more chars to read (e.g. stream has ended) 87 | 88 | _current = (charCode > 0) ? (char)charCode : EofChar; 89 | 90 | _position++; 91 | } 92 | 93 | #region Mark 94 | 95 | public int MarkStart => _markStart; 96 | 97 | public int Mark(bool appendCurrent = true) 98 | { 99 | _markStart = _position; 100 | _marked = _position; 101 | 102 | if (!appendCurrent) 103 | { 104 | _marked++; 105 | } 106 | 107 | return _position; 108 | 109 | } 110 | 111 | public string Unmark() 112 | { 113 | _marked = -1; 114 | 115 | var value = sb.ToString(); 116 | 117 | sb.Clear(); 118 | 119 | return value; 120 | } 121 | 122 | #endregion 123 | 124 | #region IDisposable 125 | 126 | public void Dispose() 127 | { 128 | _textReader.Dispose(); 129 | } 130 | 131 | #endregion 132 | } 133 | -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/CalcTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class CalcTests 4 | { 5 | [Fact] 6 | public void A() 7 | { 8 | var css = StyleSheet.Parse( 9 | """ 10 | div { 11 | width: calc(100vw - 380px - var(--cover-block-padding) * 2) 12 | } 13 | """); 14 | 15 | Assert.Equal( 16 | """ 17 | div { 18 | width: calc(100vw - 380px - var(--cover-block-padding) * 2); 19 | } 20 | """, css.ToString(), ignoreLineEndingDifferences: true); 21 | } 22 | 23 | [Fact] 24 | public void B() 25 | { 26 | var css = StyleSheet.Parse( 27 | """ 28 | $containerWidth: 50px; 29 | div { 30 | width: calc(100vw - 380px - $containerWidth * 2) 31 | } 32 | """); 33 | 34 | Assert.Equal( 35 | """ 36 | div { 37 | width: calc(100vw - 380px - 50px * 2); 38 | } 39 | """, css.ToString(), ignoreLineEndingDifferences: true); 40 | } 41 | 42 | [Fact] 43 | public void C() 44 | { 45 | var css = StyleSheet.Parse( 46 | """ 47 | $varName: --containerWidth; 48 | div { width: calc(380px - var($varName) * 2); } 49 | """); 50 | 51 | Assert.Equal( 52 | """ 53 | div { 54 | width: calc(380px - var(--containerWidth) * 2); 55 | } 56 | """, css.ToString(), ignoreLineEndingDifferences: true); 57 | } 58 | 59 | [Fact] 60 | public void D() 61 | { 62 | var css = StyleSheet.Parse( 63 | """ 64 | $varName: --containerWidth; 65 | div { width: calc(380px - 50px); } 66 | """); 67 | 68 | Assert.Equal( 69 | """ 70 | div { 71 | width: calc(380px - 50px); 72 | } 73 | """, css.ToString(), ignoreLineEndingDifferences: true); 74 | } 75 | 76 | [Fact] 77 | public void E() 78 | { 79 | var css = StyleSheet.Parse( 80 | """ 81 | div { width: 50px + 100px; } 82 | """); 83 | 84 | Assert.Equal( 85 | """ 86 | div { 87 | width: 150px; 88 | } 89 | """, css.ToString(), ignoreLineEndingDifferences: true); 90 | } 91 | 92 | [Fact] 93 | public void F() 94 | { 95 | var css = StyleSheet.Parse( 96 | """ 97 | div { width: 50px * 2; } 98 | """); 99 | 100 | Assert.Equal( 101 | """ 102 | div { 103 | width: 100px; 104 | } 105 | """, css.ToString(), ignoreLineEndingDifferences: true); 106 | } 107 | 108 | [Fact] 109 | public void G() 110 | { 111 | var css = StyleSheet.Parse( 112 | """ 113 | $width: 50px + 50px; 114 | div { width: calc($width); } 115 | """); 116 | 117 | Assert.Equal( 118 | """ 119 | div { 120 | width: calc(100px); 121 | } 122 | """, css.ToString(), ignoreLineEndingDifferences: true); 123 | } 124 | } -------------------------------------------------------------------------------- /src/Carbon.Css.Tests/Scss/ForRuleTests.cs: -------------------------------------------------------------------------------- 1 | namespace Carbon.Css.Tests; 2 | 3 | public class ForRuleTests 4 | { 5 | [Fact] 6 | public void A_to() 7 | { 8 | var sheet = StyleSheet.Parse( 9 | """ 10 | @for $i from 1 to 5 { 11 | div { width: #{$i}px } 12 | } 13 | """); 14 | 15 | Assert.Equal( 16 | """ 17 | div { 18 | width: 1px; 19 | } 20 | div { 21 | width: 2px; 22 | } 23 | div { 24 | width: 3px; 25 | } 26 | div { 27 | width: 4px; 28 | } 29 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 30 | } 31 | 32 | [Fact] 33 | public void A() 34 | { 35 | var sheet = StyleSheet.Parse( 36 | """ 37 | @for $i from 1 through 5 { 38 | div { width: #{$i}px } 39 | } 40 | """); 41 | 42 | Assert.Equal( 43 | """ 44 | div { 45 | width: 1px; 46 | } 47 | div { 48 | width: 2px; 49 | } 50 | div { 51 | width: 3px; 52 | } 53 | div { 54 | width: 4px; 55 | } 56 | div { 57 | width: 5px; 58 | } 59 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 60 | } 61 | 62 | [Fact] 63 | public void B() 64 | { 65 | var sheet = StyleSheet.Parse(""" 66 | @for $i from 1 through 5 { 67 | .col-#{$i} { width: #{$i}px } 68 | } 69 | """); 70 | 71 | Assert.Equal(""" 72 | .col-1 { 73 | width: 1px; 74 | } 75 | .col-2 { 76 | width: 2px; 77 | } 78 | .col-3 { 79 | width: 3px; 80 | } 81 | .col-4 { 82 | width: 4px; 83 | } 84 | .col-5 { 85 | width: 5px; 86 | } 87 | """, sheet.ToString(), ignoreLineEndingDifferences: true); 88 | } 89 | 90 | [Fact] 91 | public void C() 92 | { 93 | var dic = new Dictionary { 94 | ["columnCount"] = CssValue.Parse("5"), 95 | ["columnWidth"] = CssValue.Number(100 / 5d), 96 | ["gap"] = CssValue.Parse("10px") 97 | }; 98 | 99 | var sheet = StyleSheet.Parse(""" 100 | @for $i from 1 through $columnCount { 101 | .col-#{$i} { 102 | left: #{$columnWidth * $i}%; 103 | margin: $gap * 0.5; 104 | } 105 | } 106 | """); 107 | 108 | Assert.Equal(""" 109 | .col-1 { 110 | left: 20%; 111 | margin: 5px; 112 | } 113 | .col-2 { 114 | left: 40%; 115 | margin: 5px; 116 | } 117 | .col-3 { 118 | left: 60%; 119 | margin: 5px; 120 | } 121 | .col-4 { 122 | left: 80%; 123 | margin: 5px; 124 | } 125 | .col-5 { 126 | left: 100%; 127 | margin: 5px; 128 | } 129 | """, sheet.ToString(dic), ignoreLineEndingDifferences: true); 130 | } 131 | } 132 | --------------------------------------------------------------------------------