From 8f2dfa2674dfc78c9e4dc5781e87a4635eb303a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Fri, 13 Nov 2026 09:04:10 +0100 Subject: [PATCH] Update the CSS engine to ExCSS master - Vendor ExCSS 589afaf instead of PeachPDF 0.9.6's copy, keep only the renderer's hooks and the fixes ExCSS doesn't have yet: escaped surrogates, alignment prefixes without a keyword, transition: none and list-style-type: hebrew - Fix the parsing ExCSS got wrong compared to the old copy: inset and spread in text-shadow, "top 2px" in perspective-origin, math functions in grid tracks and basic shapes, and palette-mix() in font-palette - Map the flow-relative longhands, white-space-collapse, text-wrap-mode, text-align-all and font-variant-caps in the renderer, ExCSS keeps them as properties of their own - Let a later declaration of the same longhand win over a deferred var() declaration, also through a shorthand or a flow-relative name - Match :first-child and friends, which ExCSS creates as plain pseudo-classes, and honor the s flag of attribute selectors - Resolve the colors the renderer's own parsers don't know through the engine, and light-dark() as its light color - Lay out display: flow-root as block and keep ignoring display: contents, ExCSS accepts both now - Don't treat overflow: clip as a scroll container or a formatting context - Close the gaps ExCSS fixed and record the ones it introduced --- Source/HtmlRenderer/Core/CssData.cs | 66 +- Source/HtmlRenderer/Core/CssDefaults.cs | 12 +- .../Core/CssEngine/AspectRatioGrammar.cs | 97 - .../CssEngine/BackgroundPositionGrammar.cs | 186 -- .../Core/CssEngine/BackgroundSizeGrammar.cs | 86 - .../Core/CssEngine/BasicShapeGrammar.cs | 386 ---- .../Core/CssEngine/BoxShadowGrammar.cs | 198 -- .../Core/CssEngine/Calc/CalcCategory.cs | 5 +- .../Core/CssEngine/Calc/CalcNode.cs | 2 +- .../Core/CssEngine/Calc/CalcParser.cs | 207 ++- .../Core/CssEngine/Calc/CalcSerializer.cs | 310 ++-- .../Core/CssEngine/Calc/CalcTypeChecker.cs | 63 +- .../Core/CssEngine/Calc/CalcValue.cs | 9 +- .../ColorInterpolationMethodGrammar.cs | 165 -- .../Conditions/ContainerCondition.cs | 82 + .../Conditions/ContainerConditionType.cs | 21 + .../Conditions/DeclarationCondition.cs | 4 - .../Conditions/IContainerCondition.cs | 25 + .../Core/CssEngine/ContainGrammar.cs | 84 + .../Core/CssEngine/CssProperty.cs | 90 - .../Enumerations/AttrCaseSensitivity.cs | 18 + .../Core/CssEngine/Enumerations/BreakMode.cs | 4 +- .../Core/CssEngine/Enumerations/ClearMode.cs | 4 +- .../Core/CssEngine/Enumerations/Colors.cs | 2 +- .../CssEngine/Enumerations/Combinators.cs | 31 +- .../CssEngine/Enumerations/DisplayMode.cs | 4 +- .../CssEngine/Enumerations/FeatureNames.cs | 7 + .../Core/CssEngine/Enumerations/Floating.cs | 4 +- .../CssEngine/Enumerations/FunctionNames.cs | 23 +- .../Enumerations/HorizontalAlignment.cs | 5 +- .../Core/CssEngine/Enumerations/Hyphens.cs | 9 + .../Core/CssEngine/Enumerations/Keywords.cs | 28 +- .../Core/CssEngine/Enumerations/LineBreak.cs | 11 + .../Enumerations/MediaFeatureComparison.cs | 11 + .../Core/CssEngine/Enumerations/Overflow.cs | 3 +- .../CssEngine/Enumerations/OverflowWrap.cs | 3 +- .../CssEngine/Enumerations/PropertyNames.cs | 330 +++- .../Enumerations/PseudoClassNames.cs | 25 + .../Enumerations/PseudoElementNames.cs | 17 + .../CssEngine/Enumerations/RotationAxis.cs | 10 + .../Core/CssEngine/Enumerations/RuleNames.cs | 9 + .../Core/CssEngine/Enumerations/RuleType.cs | 4 +- .../CssEngine/Enumerations/TextAlignAll.cs | 13 + .../CssEngine/Enumerations/TextAlignLast.cs | 3 +- .../CssEngine/Enumerations/TextOrientation.cs | 9 + .../CssEngine/Enumerations/TextOverflow.cs | 8 + .../CssEngine/Enumerations/TextWrapMode.cs | 8 + .../CssEngine/Enumerations/TextWrapStyle.cs | 11 + .../Core/CssEngine/Enumerations/UnitNames.cs | 35 + .../Enumerations/WhiteSpaceCollapse.cs | 11 + .../CssEngine/Enumerations/WritingMode.cs | 11 + .../CssEngine/Extensions/CharExtensions.cs | 24 +- .../Extensions/CollectionExtensions.cs | 3 +- .../ColorFunctionExtensions.Relative.cs | 324 ++++ .../Extensions/ColorFunctionExtensions.cs | 611 +++++-- .../Extensions/PortableExtensions.cs | 16 +- .../Extensions/ValueConverterExtensions.cs | 9 +- .../CssEngine/Extensions/ValueExtensions.cs | 127 +- .../Factories/AttributeSelectorFactory.cs | 37 +- .../Factories/LegacyGradientSyntax.cs | 380 ++++ .../Factories/MediaFeatureFactory.cs | 11 +- .../CssEngine/Factories/PropertyFactory.cs | 735 +++++--- .../Factories/PseudoClassSelectorFactory.cs | 75 +- .../Factories/PseudoElementSelectorFactory.cs | 17 +- .../Factories/VendorPropertyAliases.cs | 159 ++ .../CssEngine/Factories/VendorValueAliases.cs | 84 + .../Core/CssEngine/FilterGrammar.cs | 170 ++ .../CssEngine/FontFeatureTagListGrammar.cs | 105 ++ .../CssEngine/FontLanguageOverrideGrammar.cs | 36 + .../Core/CssEngine/FontSizeAdjustGrammar.cs | 55 + .../Core/CssEngine/FontStretchGrammar.cs | 27 + .../CssEngine/FontVariantAlternatesGrammar.cs | 107 ++ .../Core/CssEngine/FontVariantGrammar.cs | 230 +++ .../Formatting/CompressedStyleFormatter.cs | 23 +- .../Core/CssEngine/GridLineGrammar.cs | 41 +- .../CssEngine/GridTemplateAreasGrammar.cs | 40 +- .../Core/CssEngine/GridTrackListGrammar.cs | 165 +- .../CssEngine/HangingPunctuationGrammar.cs | 55 + .../CssEngine/HyphenateLimitCharsGrammar.cs | 62 + .../Core/CssEngine/ImageOrientationGrammar.cs | 57 + .../Core/CssEngine/ImageResolutionGrammar.cs | 50 + .../Core/CssEngine/InitialLetterGrammar.cs | 59 + .../MediaFeatures/AnyHoverMediaFeature.cs | 3 - .../MediaFeatures/AnyPointerMediaFeature.cs | 3 - .../MediaFeatures/DynamicRangeMediaFeature.cs | 13 + .../MediaFeatures/ForcedColorsMediaFeature.cs | 13 + .../InvertedColorsMediaFeature.cs | 13 + .../CssEngine/MediaFeatures/MediaFeature.cs | 58 +- .../OverflowBlockMediaFeature.cs | 13 + .../OverflowInlineMediaFeature.cs | 13 + .../PrefersColorSchemeMediaFeature.cs | 3 - .../PrefersContrastMediaFeature.cs | 3 - .../PrefersReducedDataMediaFeature.cs | 13 + .../PrefersReducedMotionMediaFeature.cs | 3 - .../PrefersReducedTransparencyMediaFeature.cs | 3 - .../MediaFeatures/UpdateMediaFeature.cs | 3 - .../VideoDynamicRangeMediaFeature.cs | 13 + .../Core/CssEngine/Model/Converters.cs | 246 ++- .../HtmlRenderer/Core/CssEngine/Model/Map.cs | 181 +- .../Core/CssEngine/Model/Medium.cs | 2 +- .../Core/CssEngine/Model/ParserExtensions.cs | 44 +- .../Core/CssEngine/Model/ParserOptions.cs | 5 + .../HtmlRenderer/Core/CssEngine/Model/Pool.cs | 72 +- .../Core/CssEngine/Model/Priority.cs | 14 +- .../Core/CssEngine/Model/StyleDeclaration.cs | 1594 +++++++++++++++-- .../Core/CssEngine/Model/Stylesheet.cs | 9 +- .../Core/CssEngine/Model/StylesheetNode.cs | 5 +- .../Core/CssEngine/Model/Symbols.cs | 84 +- .../CssEngine/Model/TextFlowConverters.cs | 397 ++++ .../Core/CssEngine/Model/TextPosition.cs | 4 +- .../Core/CssEngine/Model/TokenValue.cs | 4 +- .../Core/CssEngine/Model/TransformMatrix.cs | 4 +- .../HtmlRenderer/Core/CssEngine/Model/Url.cs | 20 +- .../Core/CssEngine/Model/ValueBuilder.cs | 15 +- .../CssEngine/OverflowClipMarginGrammar.cs | 46 + .../Core/CssEngine/PaletteMixGrammar.cs | 184 -- .../Core/CssEngine/Parser/Lexer.cs | 372 ++-- .../Core/CssEngine/Parser/LexerBase.cs | 16 +- .../CssEngine/Parser/SelectorConstructor.cs | 323 +++- .../CssEngine/Parser/StylesheetComposer.cs | 778 ++++++-- .../Core/CssEngine/Parser/StylesheetParser.cs | 34 +- .../Core/CssEngine/RhythmGrammar.cs | 121 ++ .../Core/CssEngine/Rules/ContainerRule.cs | 10 +- .../Core/CssEngine/Rules/CounterStyleRule.cs | 103 ++ .../Rules/FontFeatureValueSetRule.cs | 45 + .../CssEngine/Rules/FontFeatureValuesRule.cs | 37 + .../CssEngine/Rules/FontPaletteValuesRule.cs | 6 - .../Core/CssEngine/Rules/IContainer.cs | 8 +- .../Core/CssEngine/Rules/ICounterStyleRule.cs | 42 + .../Rules/IFontFeatureValueSetRule.cs | 19 + .../CssEngine/Rules/IFontFeatureValuesRule.cs | 17 + .../Core/CssEngine/Rules/IPropertyRule.cs | 6 +- .../CssEngine/Rules/IStartingStyleRule.cs | 11 + .../Core/CssEngine/Rules/IStyleRule.cs | 10 +- .../Core/CssEngine/Rules/KeyframeRule.cs | 1 + .../Core/CssEngine/Rules/KeyframesRule.cs | 4 +- .../Core/CssEngine/Rules/LayerRule.cs | 5 +- .../CssEngine/Rules/LayerStatementRule.cs | 6 +- .../Core/CssEngine/Rules/MarginStyleRule.cs | 8 +- .../Core/CssEngine/Rules/PageRule.cs | 2 +- .../Core/CssEngine/Rules/PropertyRule.cs | 25 +- .../Core/CssEngine/Rules/StartingStyleRule.cs | 22 + .../Core/CssEngine/Rules/StyleRule.cs | 29 +- .../Core/CssEngine/Rules/SupportsRule.cs | 1 + .../CssEngine/Selectors/AttrBeginsSelector.cs | 7 +- .../Selectors/AttrContainsSelector.cs | 7 +- .../CssEngine/Selectors/AttrEndsSelector.cs | 7 +- .../CssEngine/Selectors/AttrHyphenSelector.cs | 7 +- .../CssEngine/Selectors/AttrListSelector.cs | 7 +- .../CssEngine/Selectors/AttrMatchSelector.cs | 7 +- .../Selectors/AttrNotMatchSelector.cs | 7 +- .../CssEngine/Selectors/AttrSelectorBase.cs | 21 +- .../Core/CssEngine/Selectors/ChildSelector.cs | 17 +- .../CssEngine/Selectors/ComplexSelector.cs | 8 + .../Core/CssEngine/Selectors/HasSelector.cs | 7 +- .../Core/CssEngine/Selectors/IAttrSelector.cs | 3 +- .../Core/CssEngine/Selectors/IdSelector.cs | 2 +- .../CssEngine/Selectors/KeyframeSelector.cs | 6 +- .../Core/CssEngine/Selectors/LangSelector.cs | 30 + .../Core/CssEngine/Selectors/ListSelector.cs | 8 +- .../CssEngine/Selectors/MatchesSelector.cs | 12 +- .../Core/CssEngine/Selectors/NotSelector.cs | 6 +- .../CssEngine/Selectors/OnlyChildSelector.cs | 1 + .../CssEngine/Selectors/OnlyOfTypeSelector.cs | 1 + .../CssEngine/Selectors/RelativeSelector.cs | 34 + .../Core/CssEngine/Selectors/Selectors.cs | 3 + .../CssEngine/StyleProperties/AllProperty.cs | 39 + .../Animation/AnimationCompositionProperty.cs | 14 + .../Animation/AnimationNameProperty.cs | 2 +- .../Animation/AnimationProperty.cs | 22 +- .../Animation/AnimationRangeEndProperty.cs | 14 + .../Animation/AnimationRangeProperty.cs | 16 + .../Animation/AnimationRangeStartProperty.cs | 14 + .../Animation/AnimationTimelineProperty.cs | 14 + .../Animation/ScrollTimelineAxisProperty.cs | 14 + .../Animation/ScrollTimelineNameProperty.cs | 14 + .../Animation/ScrollTimelineProperty.cs | 18 + .../Animation/TimelineScopeProperty.cs | 12 + .../Animation/ViewTimelineAxisProperty.cs | 14 + .../Animation/ViewTimelineInsetProperty.cs | 14 + .../Animation/ViewTimelineNameProperty.cs | 14 + .../Animation/ViewTimelineProperty.cs | 20 + .../Background/BackgroundBlendModeProperty.cs | 15 + .../Background/BackgroundClipProperty.cs | 2 +- .../Background/BackgroundProperty.cs | 33 +- .../StyleProperties/BleedProperty.cs | 16 + .../StyleProperties/BookmarkProperties.cs | 69 + .../BorderEndEndRadiusProperty.cs | 15 + .../BorderEndStartRadiusProperty.cs | 15 + .../BorderStartEndRadiusProperty.cs | 15 + .../BorderStartStartRadiusProperty.cs | 15 + .../Box/AspectRatioProperty.cs | 3 - .../StyleProperties/Box/BoxShadowProperty.cs | 4 +- .../StyleProperties/Box/BoxSizingProperty.cs | 2 +- .../StyleProperties/CaptionSideProperty.cs | 2 +- .../StyleProperties/ColorSchemeProperty.cs | 14 + .../Columns/ColumnGapProperty.cs | 4 +- .../Columns/ColumnsProperty.cs | 5 +- .../Container/ContainProperties.cs | 104 ++ .../Container/ContainerNameProperty.cs | 2 +- .../StyleProperties/ContentProperty.cs | 11 +- .../Corner/CornerBottomLeftShapeProperty.cs | 15 + .../Corner/CornerBottomRightShapeProperty.cs | 15 + .../Corner/CornerEndEndShapeProperty.cs | 15 + .../Corner/CornerEndStartShapeProperty.cs | 15 + .../Corner/CornerShorthands.cs | 114 ++ .../Corner/CornerStartEndShapeProperty.cs | 15 + .../Corner/CornerStartStartShapeProperty.cs | 15 + .../Corner/CornerTopLeftShapeProperty.cs | 15 + .../Corner/CornerTopRightShapeProperty.cs | 15 + .../CounterSystemDescriptorProperty.cs | 15 + .../StyleProperties/CustomProperty.cs | 4 + .../Fill/FillOpacityProperty.cs | 4 +- .../StyleProperties/Fill/FillProperty.cs | 4 +- .../StyleProperties/Fill/FillRuleProperty.cs | 2 +- .../Filter/BackdropFilterProperty.cs | 14 + .../StyleProperties/Filter/FilterProperty.cs | 14 + .../Flexbox/LegacyBoxProperty.cs | 66 + .../Flow/FloatReferenceProperty.cs | 15 + .../Flow/FootnoteDisplayProperty.cs | 15 + .../Flow/FootnotePolicyProperty.cs | 15 + .../StyleProperties/Flow/OverflowXProperty.cs | 15 + .../StyleProperties/Flow/OverflowYProperty.cs | 15 + .../StyleProperties/Flow/PositionProperty.cs | 2 +- .../Font/FontFaceVariantProperty.cs | 20 + .../Font/FontFeatureSettingsProperty.cs | 15 + .../Font/FontKerningProperty.cs | 15 + .../Font/FontLanguageOverrideProperty.cs | 15 + .../Font/FontOpticalSizingProperty.cs | 15 + .../Font/FontPaletteProperty.cs | 5 +- .../StyleProperties/Font/FontProperty.cs | 30 +- .../Font/FontSizeAdjustProperty.cs | 2 +- .../Font/FontStretchProperty.cs | 2 +- .../Font/FontSynthesisPositionProperty.cs | 15 + .../Font/FontSynthesisProperty.cs | 18 + .../Font/FontSynthesisSmallCapsProperty.cs | 15 + .../Font/FontSynthesisStyleProperty.cs | 15 + .../Font/FontSynthesisWeightProperty.cs | 15 + .../Font/FontVariantAlternatesProperty.cs | 15 + .../Font/FontVariantCapsProperty.cs | 15 + .../Font/FontVariantEastAsianProperty.cs | 15 + .../Font/FontVariantEmojiProperty.cs | 15 + .../Font/FontVariantLigaturesProperty.cs | 15 + .../Font/FontVariantNumericProperty.cs | 15 + .../Font/FontVariantPositionProperty.cs | 15 + .../Font/FontVariantProperty.cs | 9 +- .../Font/FontVariationSettingsProperty.cs | 15 + .../Font/FontWeightProperty.cs | 2 +- .../FontFeatureValueProperty.cs | 18 + .../CssEngine/StyleProperties/GapProperty.cs | 2 +- .../StyleProperties/Grid/GridAreaProperty.cs | 6 +- .../Grid/GridAutoColumnsProperty.cs | 2 +- .../Grid/GridAutoFlowProperty.cs | 2 +- .../Grid/GridAutoRowsProperty.cs | 2 +- .../Grid/GridColumnEndProperty.cs | 5 +- .../Grid/GridColumnProperty.cs | 5 +- .../Grid/GridColumnStartProperty.cs | 5 +- .../StyleProperties/Grid/GridProperty.cs | 5 +- .../Grid/GridRowEndProperty.cs | 5 +- .../StyleProperties/Grid/GridRowProperty.cs | 5 +- .../Grid/GridRowStartProperty.cs | 5 +- .../Grid/GridTemplateAreasProperty.cs | 5 +- .../Grid/GridTemplateColumnsProperty.cs | 5 +- .../Grid/GridTemplateProperty.cs | 5 +- .../Grid/GridTemplateRowsProperty.cs | 5 +- .../Grid/JustifyItemsProperty.cs | 2 +- .../Grid/JustifySelfProperty.cs | 2 +- .../Grid/PlaceContentProperty.cs | 2 +- .../Grid/PlaceItemsProperty.cs | 2 +- .../StyleProperties/Grid/PlaceSelfProperty.cs | 2 +- .../StyleProperties/IPropertyValue.cs | 32 - .../Images/ImageOrientationProperty.cs | 14 + .../Images/ImageResolutionProperty.cs | 14 + .../Interaction/AccentColorProperty.cs | 15 + .../Interaction/CaretColorProperty.cs | 15 + .../Interaction/InteractivityProperty.cs | 15 + .../Interaction/PointerEventsProperty.cs | 15 + .../Interaction/TouchActionProperty.cs | 15 + .../Interaction/UserSelectProperty.cs | 15 + .../List/CounterSetProperty.cs | 20 + .../StyleProperties/List/ListStyleProperty.cs | 5 +- .../Logical/BorderBlockColorProperty.cs | 3 +- .../Logical/BorderBlockEndColorProperty.cs | 15 + .../Logical/BorderBlockEndProperty.cs | 20 + .../Logical/BorderBlockEndStyleProperty.cs | 15 + .../Logical/BorderBlockEndWidthProperty.cs | 15 + .../Logical/BorderBlockProperty.cs | 9 +- .../Logical/BorderBlockStartColorProperty.cs | 15 + .../Logical/BorderBlockStartProperty.cs | 20 + .../Logical/BorderBlockStartStyleProperty.cs | 15 + .../Logical/BorderBlockStartWidthProperty.cs | 15 + .../Logical/BorderBlockStyleProperty.cs | 3 +- .../Logical/BorderBlockWidthProperty.cs | 3 +- .../Logical/BorderInlineColorProperty.cs | 3 +- .../Logical/BorderInlineEndColorProperty.cs | 15 + .../Logical/BorderInlineEndProperty.cs | 20 + .../Logical/BorderInlineEndStyleProperty.cs | 15 + .../Logical/BorderInlineEndWidthProperty.cs | 15 + .../Logical/BorderInlineProperty.cs | 9 +- .../Logical/BorderInlineStartColorProperty.cs | 15 + .../Logical/BorderInlineStartProperty.cs | 20 + .../Logical/BorderInlineStartStyleProperty.cs | 15 + .../Logical/BorderInlineStartWidthProperty.cs | 15 + .../Logical/BorderInlineStyleProperty.cs | 3 +- .../Logical/BorderInlineWidthProperty.cs | 3 +- .../Logical/InsetBlockEndProperty.cs | 15 + .../Logical/InsetBlockProperty.cs | 2 +- .../Logical/InsetBlockStartProperty.cs | 15 + .../Logical/InsetInlineEndProperty.cs | 15 + .../Logical/InsetInlineProperty.cs | 2 +- .../Logical/InsetInlineStartProperty.cs | 15 + .../Logical/MarginBlockEndProperty.cs | 15 + .../Logical/MarginBlockProperty.cs | 2 +- .../Logical/MarginBlockStartProperty.cs | 15 + .../Logical/MarginInlineEndProperty.cs | 15 + .../Logical/MarginInlineProperty.cs | 2 +- .../Logical/MarginInlineStartProperty.cs | 15 + .../Logical/PaddingBlockEndProperty.cs | 15 + .../Logical/PaddingBlockProperty.cs | 2 +- .../Logical/PaddingBlockStartProperty.cs | 15 + .../Logical/PaddingInlineEndProperty.cs | 15 + .../Logical/PaddingInlineProperty.cs | 2 +- .../Logical/PaddingInlineStartProperty.cs | 15 + .../StyleProperties/MarksProperty.cs | 18 + .../StyleProperties/Mask/MaskClipProperty.cs | 14 + .../Mask/MaskCompositeProperty.cs | 14 + .../StyleProperties/Mask/MaskImageProperty.cs | 14 + .../StyleProperties/Mask/MaskModeProperty.cs | 14 + .../Mask/MaskOriginProperty.cs | 14 + .../Mask/MaskPositionProperty.cs | 14 + .../Mask/MaskRepeatProperty.cs | 14 + .../StyleProperties/Mask/MaskSizeProperty.cs | 14 + .../StyleProperties/Math/MathDepthProperty.cs | 15 + .../StyleProperties/Math/MathShiftProperty.cs | 15 + .../StyleProperties/Math/MathStyleProperty.cs | 15 + .../Outline/OutlineOffsetProperty.cs | 14 + .../Overflow/OverflowClipMarginProperties.cs | 115 ++ .../StyleProperties/PageNameProperty.cs | 7 +- .../StyleProperties/PageSizeProperty.cs | 11 +- .../CssEngine/StyleProperties/Property.cs | 4 + .../StyleProperties/QuotesProperty.cs | 2 +- .../StyleProperties/RowGapProperty.cs | 4 +- .../Scroll/OverscrollBehaviorProperty.cs | 16 + .../Scroll/OverscrollBehaviorXProperty.cs | 14 + .../Scroll/OverscrollBehaviorYProperty.cs | 14 + .../Scroll/ScrollBehaviorProperty.cs | 14 + .../Scroll/ScrollMarginBottomProperty.cs | 14 + .../Scroll/ScrollMarginLeftProperty.cs | 14 + .../Scroll/ScrollMarginProperty.cs | 17 + .../Scroll/ScrollMarginRightProperty.cs | 14 + .../Scroll/ScrollMarginTopProperty.cs | 14 + .../Scroll/ScrollPaddingBottomProperty.cs | 14 + .../Scroll/ScrollPaddingLeftProperty.cs | 14 + .../Scroll/ScrollPaddingProperty.cs | 17 + .../Scroll/ScrollPaddingRightProperty.cs | 14 + .../Scroll/ScrollPaddingTopProperty.cs | 14 + .../Scroll/ScrollSnapAlignProperty.cs | 14 + .../Scroll/ScrollSnapStopProperty.cs | 14 + .../Scroll/ScrollSnapTypeProperty.cs | 14 + .../Scroll/ScrollbarColorProperty.cs | 14 + .../Scroll/ScrollbarGutterProperty.cs | 14 + .../Scroll/ScrollbarWidthProperty.cs | 14 + .../Shapes/ShapeImageThresholdProperty.cs | 14 + .../Shapes/ShapeMarginProperty.cs | 14 + .../Shapes/ShapeOutsideProperty.cs | 14 + .../StyleProperties/ShorthandProperty.cs | 15 +- .../StyleProperties/StringSetProperty.cs | 10 +- .../Stroke/StrokeDasharrayProperty.cs | 4 +- .../Stroke/StrokeDashoffsetProperty.cs | 4 +- .../Stroke/StrokeLinecapProperty.cs | 2 +- .../Stroke/StrokeLinejoinProperty.cs | 4 +- .../Stroke/StrokeMiterlimitProperty.cs | 4 +- .../Stroke/StrokeOpacityProperty.cs | 4 +- .../StyleProperties/Stroke/StrokeProperty.cs | 4 +- .../Stroke/StrokeWidthProperty.cs | 4 +- .../Svg/AlignmentBaselineProperty.cs | 14 + .../Svg/BaselineShiftProperty.cs | 14 + .../StyleProperties/Svg/ClipRuleProperty.cs | 14 + .../Svg/ColorInterpolationFiltersProperty.cs | 14 + .../Svg/ColorInterpolationProperty.cs | 14 + .../StyleProperties/Svg/CxProperty.cs | 14 + .../StyleProperties/Svg/CyProperty.cs | 14 + .../StyleProperties/Svg/DProperty.cs | 14 + .../Svg/DominantBaselineProperty.cs | 14 + .../StyleProperties/Svg/FloodColorProperty.cs | 14 + .../Svg/FloodOpacityProperty.cs | 14 + .../StyleProperties/Svg/InlineSizeProperty.cs | 14 + .../Svg/LightingColorProperty.cs | 14 + .../StyleProperties/Svg/MarkerEndProperty.cs | 14 + .../StyleProperties/Svg/MarkerMidProperty.cs | 14 + .../StyleProperties/Svg/MarkerProperty.cs | 14 + .../Svg/MarkerStartProperty.cs | 14 + .../StyleProperties/Svg/PaintOrderProperty.cs | 14 + .../StyleProperties/Svg/RProperty.cs | 14 + .../StyleProperties/Svg/RxProperty.cs | 14 + .../StyleProperties/Svg/RyProperty.cs | 14 + .../StyleProperties/Svg/StopColorProperty.cs | 14 + .../Svg/StopOpacityProperty.cs | 14 + .../StyleProperties/Svg/SvgConverters.cs | 171 ++ .../Svg/TransformBoxProperty.cs | 14 + .../StyleProperties/Svg/XProperty.cs | 14 + .../StyleProperties/Svg/YProperty.cs | 14 + .../Text/HyphenationProperties.cs | 89 + .../{ => Text}/HyphensProperty.cs | 5 +- .../StyleProperties/Text/LineBreakProperty.cs | 14 + .../Text/LineLayoutProperties.cs | 75 + .../Text/OverflowWrapProperty.cs | 2 +- .../StyleProperties/Text/RhythmProperties.cs | 181 ++ .../Text/TextAlignAllProperty.cs | 14 + .../Text/TextAlignLastProperty.cs | 4 +- .../StyleProperties/Text/TextAlignProperty.cs | 5 +- .../Text/TextAnchorProperty.cs | 2 +- .../Text/TextCombineUprightProperty.cs | 14 + .../Text/TextDecorationInsetProperty.cs | 15 + .../Text/TextDecorationProperty.cs | 3 +- .../Text/TextDecorationSkipInkProperty.cs | 15 + .../Text/TextDecorationSkipProperty.cs | 16 + .../Text/TextDecorationThicknessProperty.cs | 18 + .../Text/TextEmphasisColorProperty.cs | 14 + .../Text/TextEmphasisPositionProperty.cs | 15 + .../Text/TextEmphasisProperty.cs | 153 ++ .../Text/TextEmphasisStyleProperty.cs | 17 + .../Text/TextIndentProperty.cs | 2 +- .../Text/TextJustifyProperty.cs | 2 +- .../Text/TextOrientationProperty.cs | 14 + .../Text/TextOverflowProperty.cs | 14 + .../Text/TextRenderingProperty.cs | 15 + .../Text/TextShadowProperty.cs | 2 +- .../Text/TextUnderlineOffsetProperty.cs | 15 + .../Text/TextUnderlinePositionProperty.cs | 15 + .../Text/TextWrapModeProperty.cs | 14 + .../StyleProperties/Text/TextWrapProperty.cs | 14 + .../Text/TextWrapStyleProperty.cs | 14 + .../Text/WhiteSpaceCollapseProperty.cs | 14 + .../Text/WhiteSpaceProperty.cs | 5 +- .../StyleProperties/Text/WordBreakProperty.cs | 2 +- .../Text/WritingModeProperty.cs | 14 + .../Transform/PerspectiveProperty.cs | 2 +- .../Transform/RotateProperty.cs | 15 + .../Transform/ScaleProperty.cs | 15 + .../Transform/TransformOriginProperty.cs | 22 +- .../Transform/TranslateProperty.cs | 15 + .../Transition/TransitionBehaviorProperty.cs | 14 + .../Transition/TransitionProperty.cs | 26 +- .../Visibility/BufferedRenderingProperty.cs | 15 + .../Visibility/ClipPathProperty.cs | 3 - .../Visibility/ForcedColorAdjustProperty.cs | 15 + .../Visibility/ImageRenderingProperty.cs | 15 + .../Visibility/IsolationProperty.cs | 15 + .../Visibility/MixBlendModeProperty.cs | 15 + .../Visibility/PrintColorAdjustProperty.cs | 15 + .../Visibility/ShapeRenderingProperty.cs | 15 + .../Visibility/VectorEffectProperty.cs | 15 + .../Core/CssEngine/TextEncoding.cs | 6 +- .../Core/CssEngine/Tokens/CommentToken.cs | 2 +- .../Core/CssEngine/Tokens/NumberToken.cs | 2 +- .../Core/CssEngine/Tokens/RangeToken.cs | 50 +- .../Core/CssEngine/Tokens/Token.cs | 4 +- .../AspectRatioValueConverter.cs | 51 +- .../BackgroundPositionValueConverter.cs | 72 - .../BackgroundSizeValueConverter.cs | 54 - .../ValueConverters/BorderRadiusConverter.cs | 34 +- .../BoxShadowValueConverter.cs | 36 - .../CalcValueConverter.cs | 11 +- .../ValueConverters/ClipPathValueConverter.cs | 365 +++- .../ColorInterpolationMethodGrammar.cs | 148 ++ .../ColorPairValueConverter.cs | 46 + .../ValueConverters/ColorSchemeGrammar.cs | 50 + .../ColorSchemeValueConverter.cs | 38 + .../ConditionalStartsWithValueConverter.cs | 1 - .../ContainmentValueConverters.cs | 167 ++ .../ContentFunctionConverter.cs | 43 +- .../CornerShapeValueConverter.cs | 85 + .../CounterSystemValueConverter.cs | 66 + .../DictionaryValueConverter.cs | 4 +- .../EasingArgumentRangeValueConverter.cs | 77 + .../ValueConverters/EndListValueConverter.cs | 99 +- .../ExtendedImageConverters.cs | 62 +- .../ValueConverters/FilterValueConverter.cs | 18 + .../FontGrammarValueConverter.cs | 96 + .../FontPaletteValueConverter.cs | 71 +- .../FontSynthesisShorthandValueConverter.cs | 97 + .../FontVariantShorthandValueConverter.cs | 111 ++ .../GcpmElementFunctionConverter.cs | 49 + .../ValueConverters/GcpmFunctionValue.cs | 24 + .../ValueConverters/GradientConverter.cs | 200 +-- .../CssEngine/ValueConverters/GrammarValue.cs | 19 + .../ValueConverters/GrammarValueConverter.cs | 58 + .../GridAreaShorthandValueConverter.cs | 12 +- .../GridAutoFlowValueConverter.cs | 10 +- .../GridAutoTracksValueConverter.cs | 5 +- .../GridColumnRowShorthandValueConverter.cs | 8 +- .../ValueConverters/GridLineValueConverter.cs | 6 +- .../ValueConverters/GridPlacementShorthand.cs | 30 +- .../GridShorthandValueConverter.cs | 27 +- .../GridTemplateAreasValueConverter.cs | 8 +- .../ValueConverters/GridTemplateShorthand.cs | 68 +- .../GridTemplateShorthandValueConverter.cs | 11 +- .../GridTemplateValueConverter.cs | 60 +- .../ImageOrientationValueConverter.cs | 18 + .../ImageResolutionValueConverter.cs | 18 + .../ImplicitResetValueConverter.cs | 44 + .../KeywordGrammarValueConverter.cs | 45 + .../KeywordSetValueConverter.cs | 143 ++ .../LeaderFunctionConverter.cs | 38 + .../LinearEasingValueConverter.cs | 87 + .../ValueConverters/MarksValueConverter.cs | 42 + .../ValueConverters/MaskConverters.cs | 38 + .../NonNegativeValueConverter.cs | 37 + .../OneOrMoreValueConverter.cs | 2 +- .../ValueConverters/OptionValueConverter.cs | 14 +- .../OrderIndependentOptionsConverter.cs | 10 +- .../OrderedOptionsConverter.cs | 22 +- ...erflowClipMarginShorthandValueConverter.cs | 47 + .../ValueConverters/PeriodicValueConverter.cs | 17 +- .../RunningFunctionConverter.cs | 26 + .../ValueConverters/ScrollConverters.cs | 75 + .../ValueConverters/ShadowValueConverter.cs | 69 + .../ShapeOutsideValueConverter.cs | 57 + .../StartsWithValueConverter.cs | 33 +- .../StringFunctionConverter.cs | 57 +- .../StringSetValueConverter.cs | 70 +- .../TargetCounterFunctionConverter.cs | 41 + .../ValueConverters/TargetGrammar.cs | 43 + .../TargetTextFunctionConverter.cs | 36 + .../ValueConverters/TextEmphasisGrammar.cs | 140 ++ .../ValueConverters/TextIndentGrammar.cs | 43 + .../TextUnderlinePositionGrammar.cs | 46 + .../TimeBasedShorthandConverter.cs | 191 -- .../ValueConverters/TimelineGrammar.cs | 177 ++ .../TimelineValueConverters.cs | 194 ++ .../TokenGrammarValueConverter.cs | 59 + .../UnorderedOptionsConverter.cs | 4 +- .../Core/CssEngine/Values/Angle.cs | 69 +- .../Core/CssEngine/Values/Color.cs | 284 ++- .../Core/CssEngine/Values/ColorSpaceMath.cs | 643 +++++-- .../Core/CssEngine/Values/Frequency.cs | 2 +- .../Core/CssEngine/Values/Length.cs | 271 ++- .../Core/CssEngine/Values/MatrixTransform.cs | 2 +- .../Core/CssEngine/Values/Number.cs | 8 +- .../Core/CssEngine/Values/Percent.cs | 6 +- .../CssEngine/Values/PerspectiveTransform.cs | 8 +- .../Core/CssEngine/Values/Point.cs | 10 +- .../Core/CssEngine/Values/PortNumbers.cs | 2 +- .../Core/CssEngine/Values/RadialGradient.cs | 2 +- .../Core/CssEngine/Values/Resolution.cs | 37 +- .../Core/CssEngine/Values/RotateTransform.cs | 6 +- .../Core/CssEngine/Values/ScaleTransform.cs | 2 +- .../Core/CssEngine/Values/SkewTransform.cs | 4 +- .../Core/CssEngine/Values/Time.cs | 19 +- Source/HtmlRenderer/Core/Dom/CssBox.cs | 6 +- .../Core/Fragmentation/MonolithicContent.cs | 7 +- Source/HtmlRenderer/Core/MediaQueryMatcher.cs | 4 +- .../HtmlRenderer/Core/Parse/CssValueParser.cs | 30 +- Source/HtmlRenderer/Core/Parse/DomParser.cs | 99 +- .../HtmlRenderer/Core/Utils/CssConstants.cs | 2 + Source/HtmlRenderer/Core/Utils/CssUtils.cs | 109 +- .../CssModules/CssDisplayTests.cs | 11 + .../Text/CssFonts4IntegrationTests.cs | 13 +- .../Text/TextAlignStartEndIntegrationTests.cs | 2 - .../Css/AspectRatioGrammarTests.cs | 142 -- .../HtmlRenderer.Test/Css/AspectRatioTests.cs | 42 + .../Css/BasicShapeGrammarTests.cs | 343 ---- .../HtmlRenderer.Test/Css/BasicShapeTests.cs | 146 ++ .../Css/BoxShadowGrammarTests.cs | 174 -- .../HtmlRenderer.Test/Css/BoxShadowTests.cs | 67 + .../ColorInterpolationMethodGrammarTests.cs | 10 +- .../Css/ColumnGapPropertyTests.cs | 1 - .../HtmlRenderer.Test/Css/ContainerTests.cs | 1 - .../Css/CssBackgrounds3ParseTests.cs | 1 - .../Css/CssBackgrounds4ParseTests.cs | 15 +- .../Css/CssBorders4ParseTests.cs | 37 +- .../Css/CssFonts4PropertyTests.cs | 13 +- .../Css/CssImages4ParseTests.cs | 2 - .../Css/CssText3PropertyTests.cs | 5 - .../Test/HtmlRenderer.Test/Css/CssUi3Tests.cs | 5 - .../Css/FillPropertyTests.cs | 6 +- .../Css/FontKeywordPropertyTests.cs | 14 +- .../Css/FontPaletteParsingTests.cs | 11 +- .../HtmlRenderer.Test/Css/GapPropertyTests.cs | 2 - .../HtmlRenderer.Test/Css/GrammarTests.cs | 11 +- .../Css/LogicalPropertySerializationTests.cs | 15 +- .../Css/PaletteMixGrammarTests.cs | 59 - .../HtmlRenderer.Test/Css/PaletteMixTests.cs | 41 + .../Css/ParserExtensionsCreateRuleTests.cs | 6 +- .../HtmlRenderer.Test/Css/PropertyBoxTests.cs | 20 +- .../Css/PropertyBreakTests.cs | 18 +- .../Css/RowGapPropertyTests.cs | 1 - .../RelationalPseudoClassSelectorTests.cs | 2 - .../Css/StrokePropertyTests.cs | 18 +- .../HtmlRenderer.Test/Css/SupportsTests.cs | 4 +- .../Css/SyntaxAndDataTypesTests.cs | 1 - .../Css/TextPropertyTests.cs | 24 +- .../Css/TokenizationTests.cs | 2 +- .../Css/TypedCssPropertyTests.cs | 126 -- .../WptCssPositionInsetLogicalParsingTests.cs | 8 - .../CssModules/CompositingTests.cs | 1 - .../CssModules/CssAnimationsTests.cs | 4 - .../CssModules/CssColor5Tests.cs | 25 +- .../CssModules/CssColorAdjustTests.cs | 18 +- .../CssModules/CssColorTests.cs | 70 +- .../CssModules/CssConditional5Tests.cs | 2 - .../CssModules/CssContainTests.cs | 1 - .../CssModules/CssEasingTests.cs | 3 - .../CssModules/CssFonts5Tests.cs | 1 - .../CssModules/CssGcpmTests.cs | 2 - .../CssModules/CssInlineTests.cs | 5 - .../CssModules/CssLogicalTests.cs | 32 +- .../CssModules/CssMaskingTests.cs | 27 +- .../CssModules/CssOverflowTests.cs | 2 - .../CssModules/CssPosition4Tests.cs | 1 - .../CssModules/CssScrollSnapTests.cs | 14 - .../CssModules/CssScrollbarsTests.cs | 11 - .../CssModules/CssShapesTests.cs | 1 - .../CssModules/CssSizing4Tests.cs | 1 - .../CssModules/CssSyntaxTests.cs | 2 - .../CssModules/CssText4Tests.cs | 50 +- .../CssModules/CssTextDecor4Tests.cs | 7 - .../CssModules/CssTransformsTests.cs | 16 +- .../CssModules/CssUi4Tests.cs | 21 +- .../CssModules/CssValuesTests.cs | 25 +- .../CssModules/CssVariablesTests.cs | 28 + .../CssModules/CssViewTransitionsTests.cs | 1 - .../CssModules/CssWritingModesTests.cs | 3 - .../CssModules/FilterEffectsTests.cs | 21 +- .../CssModules/MediaQueriesTests.cs | 1 - .../CssModules/PseudoElements4Tests.cs | 12 +- .../CssModules/Selectors4Tests.cs | 34 +- .../Fragmentation/MonolithicContentTests.cs | 1 - Spec/compositing-1.yml | 12 +- Spec/css-animations-1.yml | 5 - Spec/css-backgrounds-3.yml | 5 +- Spec/css-backgrounds-4.yml | 8 +- Spec/css-borders-4.yml | 15 +- Spec/css-break-3.yml | 4 +- Spec/css-break-4.yml | 2 +- Spec/css-color-4.yml | 63 +- Spec/css-color-5.yml | 14 +- Spec/css-color-adjust-1.yml | 14 +- Spec/css-conditional-5.yml | 8 +- Spec/css-contain-2.yml | 8 +- Spec/css-display-3.yml | 4 +- Spec/css-display-4.yml | 4 +- Spec/css-easing-1.yml | 5 - Spec/css-fonts-4.yml | 6 +- Spec/css-fonts-5.yml | 1 - Spec/css-gcpm-3.yml | 8 +- Spec/css-grid-2.yml | 4 +- Spec/css-images-3.yml | 8 +- Spec/css-images-4.yml | 5 +- Spec/css-inline-3.yml | 24 +- Spec/css-logical-1.yml | 17 +- Spec/css-masking-1.yml | 20 +- Spec/css-multicol-1.yml | 2 - Spec/css-nesting-1.yml | 1 - Spec/css-overflow-3.yml | 10 +- Spec/css-position-3.yml | 1 - Spec/css-position-4.yml | 4 +- Spec/css-pseudo-4.yml | 14 +- Spec/css-scroll-snap-1.yml | 20 +- Spec/css-scrollbars-1.yml | 8 +- Spec/css-shapes-1.yml | 18 +- Spec/css-sizing-4.yml | 4 +- Spec/css-syntax-3.yml | 5 +- Spec/css-text-3.yml | 10 +- Spec/css-text-4.yml | 42 +- Spec/css-text-decor-4.yml | 13 +- Spec/css-transforms-1.yml | 7 +- Spec/css-transforms-2.yml | 9 +- Spec/css-ui-3.yml | 8 +- Spec/css-ui-4.yml | 18 +- Spec/css-values-4.yml | 15 +- Spec/css-view-transitions-1.yml | 4 +- Spec/css-writing-modes-4.yml | 12 +- Spec/css21.yml | 8 +- Spec/filter-effects-1.yml | 18 +- Spec/mediaqueries-4.yml | 4 +- Spec/mediaqueries-5.yml | 4 +- Spec/selectors-4.yml | 66 +- 679 files changed, 19025 insertions(+), 6068 deletions(-) delete mode 100644 Source/HtmlRenderer/Core/CssEngine/AspectRatioGrammar.cs delete mode 100644 Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs delete mode 100644 Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs delete mode 100644 Source/HtmlRenderer/Core/CssEngine/BasicShapeGrammar.cs delete mode 100644 Source/HtmlRenderer/Core/CssEngine/BoxShadowGrammar.cs delete mode 100644 Source/HtmlRenderer/Core/CssEngine/ColorInterpolationMethodGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Conditions/ContainerCondition.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Conditions/ContainerConditionType.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Conditions/IContainerCondition.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ContainGrammar.cs delete mode 100644 Source/HtmlRenderer/Core/CssEngine/CssProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/AttrCaseSensitivity.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/Hyphens.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/LineBreak.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/MediaFeatureComparison.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/RotationAxis.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignAll.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/TextOrientation.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/TextOverflow.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/TextWrapMode.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/TextWrapStyle.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/WhiteSpaceCollapse.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Enumerations/WritingMode.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.Relative.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Factories/LegacyGradientSyntax.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Factories/VendorPropertyAliases.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Factories/VendorValueAliases.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/FilterGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/FontFeatureTagListGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/FontLanguageOverrideGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/FontSizeAdjustGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/FontStretchGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/FontVariantAlternatesGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/FontVariantGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/HangingPunctuationGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/HyphenateLimitCharsGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ImageOrientationGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ImageResolutionGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/InitialLetterGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/DynamicRangeMediaFeature.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/ForcedColorsMediaFeature.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/InvertedColorsMediaFeature.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/OverflowBlockMediaFeature.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/OverflowInlineMediaFeature.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedDataMediaFeature.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/MediaFeatures/VideoDynamicRangeMediaFeature.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Model/TextFlowConverters.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/OverflowClipMarginGrammar.cs delete mode 100644 Source/HtmlRenderer/Core/CssEngine/PaletteMixGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/RhythmGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/CounterStyleRule.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/FontFeatureValueSetRule.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/FontFeatureValuesRule.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/ICounterStyleRule.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IFontFeatureValueSetRule.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IFontFeatureValuesRule.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/IStartingStyleRule.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Rules/StartingStyleRule.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/LangSelector.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/Selectors/RelativeSelector.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/AllProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationCompositionProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationRangeEndProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationRangeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationRangeStartProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationTimelineProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ScrollTimelineAxisProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ScrollTimelineNameProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ScrollTimelineProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/TimelineScopeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineAxisProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineInsetProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineNameProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundBlendModeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BleedProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BookmarkProperties.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderEndEndRadiusProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderEndStartRadiusProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderStartEndRadiusProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderStartStartRadiusProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/ColorSchemeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Container/ContainProperties.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerBottomLeftShapeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerBottomRightShapeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerEndEndShapeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerEndStartShapeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerShorthands.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerStartEndShapeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerStartStartShapeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerTopLeftShapeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerTopRightShapeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/CounterSystemDescriptorProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Filter/BackdropFilterProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Filter/FilterProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/LegacyBoxProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/FloatReferenceProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/FootnoteDisplayProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/FootnotePolicyProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/OverflowXProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/OverflowYProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontFaceVariantProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontFeatureSettingsProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontKerningProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontLanguageOverrideProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontOpticalSizingProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisPositionProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisSmallCapsProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisStyleProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisWeightProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantAlternatesProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantCapsProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantEastAsianProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantEmojiProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantLigaturesProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantNumericProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantPositionProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariationSettingsProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/FontFeatureValueProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Images/ImageOrientationProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Images/ImageResolutionProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/AccentColorProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/CaretColorProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/InteractivityProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/PointerEventsProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/TouchActionProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/UserSelectProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/CounterSetProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndColorProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndStyleProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndWidthProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartColorProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartStyleProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartWidthProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndColorProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndStyleProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndWidthProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartColorProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartStyleProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartWidthProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetBlockEndProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetBlockStartProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetInlineEndProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetInlineStartProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginBlockEndProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginBlockStartProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginInlineEndProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginInlineStartProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingBlockEndProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingBlockStartProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingInlineEndProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingInlineStartProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/MarksProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskClipProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskCompositeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskImageProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskModeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskOriginProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskPositionProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskRepeatProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskSizeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Math/MathDepthProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Math/MathShiftProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Math/MathStyleProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Outline/OutlineOffsetProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Overflow/OverflowClipMarginProperties.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/OverscrollBehaviorProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/OverscrollBehaviorXProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/OverscrollBehaviorYProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollBehaviorProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginBottomProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginLeftProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginRightProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginTopProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingBottomProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingLeftProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingRightProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingTopProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollSnapAlignProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollSnapStopProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollSnapTypeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollbarColorProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollbarGutterProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollbarWidthProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Shapes/ShapeImageThresholdProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Shapes/ShapeMarginProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Shapes/ShapeOutsideProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/AlignmentBaselineProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/BaselineShiftProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/ClipRuleProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/ColorInterpolationFiltersProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/ColorInterpolationProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/CxProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/CyProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/DProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/DominantBaselineProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/FloodColorProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/FloodOpacityProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/InlineSizeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/LightingColorProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerEndProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerMidProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerStartProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/PaintOrderProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/RProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/RxProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/RyProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/StopColorProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/StopOpacityProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/SvgConverters.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/TransformBoxProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/XProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/YProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/HyphenationProperties.cs rename Source/HtmlRenderer/Core/CssEngine/StyleProperties/{ => Text}/HyphensProperty.cs (64%) create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/LineBreakProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/LineLayoutProperties.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/RhythmProperties.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAlignAllProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextCombineUprightProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationInsetProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationSkipInkProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationSkipProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationThicknessProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisColorProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisPositionProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisStyleProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextOrientationProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextOverflowProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextRenderingProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextUnderlineOffsetProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextUnderlinePositionProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextWrapModeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextWrapProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextWrapStyleProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WhiteSpaceCollapseProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WritingModeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/RotateProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/ScaleProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/TranslateProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transition/TransitionBehaviorProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/BufferedRenderingProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ForcedColorAdjustProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ImageRenderingProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/IsolationProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/MixBlendModeProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/PrintColorAdjustProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ShapeRenderingProperty.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/VectorEffectProperty.cs delete mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/BackgroundPositionValueConverter.cs delete mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/BackgroundSizeValueConverter.cs delete mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/BoxShadowValueConverter.cs rename Source/HtmlRenderer/Core/CssEngine/{Calc => ValueConverters}/CalcValueConverter.cs (61%) create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorInterpolationMethodGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorPairValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorSchemeGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorSchemeValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ContainmentValueConverters.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/CornerShapeValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/CounterSystemValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/EasingArgumentRangeValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/FilterValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontGrammarValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontSynthesisShorthandValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontVariantShorthandValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/GcpmElementFunctionConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/GcpmFunctionValue.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/GrammarValue.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/GrammarValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ImageOrientationValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ImageResolutionValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ImplicitResetValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/KeywordGrammarValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/KeywordSetValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/LeaderFunctionConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/LinearEasingValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/MarksValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/MaskConverters.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/NonNegativeValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/OverflowClipMarginShorthandValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/RunningFunctionConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ScrollConverters.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ShadowValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/ShapeOutsideValueConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/TargetCounterFunctionConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/TargetGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/TargetTextFunctionConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/TextEmphasisGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/TextIndentGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/TextUnderlinePositionGrammar.cs delete mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/TimeBasedShorthandConverter.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/TimelineGrammar.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/TimelineValueConverters.cs create mode 100644 Source/HtmlRenderer/Core/CssEngine/ValueConverters/TokenGrammarValueConverter.cs delete mode 100644 Source/Test/HtmlRenderer.Test/Css/AspectRatioGrammarTests.cs create mode 100644 Source/Test/HtmlRenderer.Test/Css/AspectRatioTests.cs delete mode 100644 Source/Test/HtmlRenderer.Test/Css/BasicShapeGrammarTests.cs create mode 100644 Source/Test/HtmlRenderer.Test/Css/BasicShapeTests.cs delete mode 100644 Source/Test/HtmlRenderer.Test/Css/BoxShadowGrammarTests.cs create mode 100644 Source/Test/HtmlRenderer.Test/Css/BoxShadowTests.cs delete mode 100644 Source/Test/HtmlRenderer.Test/Css/PaletteMixGrammarTests.cs create mode 100644 Source/Test/HtmlRenderer.Test/Css/PaletteMixTests.cs delete mode 100644 Source/Test/HtmlRenderer.Test/Css/TypedCssPropertyTests.cs diff --git a/Source/HtmlRenderer/Core/CssData.cs b/Source/HtmlRenderer/Core/CssData.cs index 7996cb334..343bdf8cb 100644 --- a/Source/HtmlRenderer/Core/CssData.cs +++ b/Source/HtmlRenderer/Core/CssData.cs @@ -348,7 +348,7 @@ private static void CollectIndexKeys(ISelector selector, List<(SelectorBucketKin // re-derived at match time (see DoesSelectorMatch), including for synthesized // pseudo-element boxes whose HtmlTag is null - the tag/class/id index can't // safely represent that, so fall back to a full scan for these. - if (compoundSelector.Any(s => s is PseudoElementSelector || s is FirstChildSelector)) + if (compoundSelector.Any(s => s is PseudoElementSelector || IsChildIndexedPseudoClass(s))) { keys.Add((SelectorBucketKind.Universal, string.Empty)); return; @@ -629,6 +629,14 @@ private static bool DoesSelectorMatch(AttrAvailableSelector attrAvailableSelecto return false; } + // Selectors 4 §6.3, the s flag compares the value case-sensitively + private static StringComparison ValueComparison(AttrSelectorBase selector) + { + return selector.CaseSensitivity == AttrCaseSensitivity.Sensitive + ? StringComparison.Ordinal + : StringComparison.InvariantCultureIgnoreCase; + } + private static bool DoesSelectorMatch(AttrMatchSelector attrMatchSelector, CssBox box) { if (box == null || box.HtmlTag == null || box.HtmlTag.Attributes == null) @@ -640,7 +648,7 @@ private static bool DoesSelectorMatch(AttrMatchSelector attrMatchSelector, CssBo { if (attribute.Key.Equals(attrMatchSelector.Attribute, StringComparison.InvariantCultureIgnoreCase)) { - return attribute.Value.Equals(attrMatchSelector.Value, StringComparison.InvariantCultureIgnoreCase); + return attribute.Value.Equals(attrMatchSelector.Value, ValueComparison(attrMatchSelector)); } } @@ -660,7 +668,7 @@ private static bool DoesSelectorMatch(AttrListSelector attrListSelector, CssBox { var attributeValues = attribute.Value.Split(' ').Where(x => x.Length > 0).ToArray(); - return attributeValues.Any(value => value.Equals(attrListSelector.Value, StringComparison.InvariantCultureIgnoreCase)); + return attributeValues.Any(value => value.Equals(attrListSelector.Value, ValueComparison(attrListSelector))); } } @@ -678,7 +686,7 @@ private static bool DoesSelectorMatch(AttrContainsSelector attrContainsSelector, { if (attribute.Key.Equals(attrContainsSelector.Attribute, StringComparison.InvariantCultureIgnoreCase)) { - return attribute.Value.IndexOf(attrContainsSelector.Value, StringComparison.InvariantCultureIgnoreCase) >= 0; + return attribute.Value.IndexOf(attrContainsSelector.Value, ValueComparison(attrContainsSelector)) >= 0; } } @@ -690,7 +698,7 @@ private static bool DoesSelectorMatch(AttrBeginsSelector s, CssBox box) if (box == null || box.HtmlTag == null || box.HtmlTag.Attributes == null) return false; foreach (var attr in box.HtmlTag.Attributes) if (attr.Key.Equals(s.Attribute, StringComparison.InvariantCultureIgnoreCase)) - return attr.Value.StartsWith(s.Value, StringComparison.InvariantCultureIgnoreCase); + return attr.Value.StartsWith(s.Value, ValueComparison(s)); return false; } @@ -699,7 +707,7 @@ private static bool DoesSelectorMatch(AttrEndsSelector s, CssBox box) if (box == null || box.HtmlTag == null || box.HtmlTag.Attributes == null) return false; foreach (var attr in box.HtmlTag.Attributes) if (attr.Key.Equals(s.Attribute, StringComparison.InvariantCultureIgnoreCase)) - return attr.Value.EndsWith(s.Value, StringComparison.InvariantCultureIgnoreCase); + return attr.Value.EndsWith(s.Value, ValueComparison(s)); return false; } @@ -708,8 +716,8 @@ private static bool DoesSelectorMatch(AttrHyphenSelector s, CssBox box) if (box == null || box.HtmlTag == null || box.HtmlTag.Attributes == null) return false; foreach (var attr in box.HtmlTag.Attributes) if (attr.Key.Equals(s.Attribute, StringComparison.InvariantCultureIgnoreCase)) - return attr.Value.Equals(s.Value, StringComparison.InvariantCultureIgnoreCase) - || attr.Value.StartsWith(s.Value + "-", StringComparison.InvariantCultureIgnoreCase); + return attr.Value.Equals(s.Value, ValueComparison(s)) + || attr.Value.StartsWith(s.Value + "-", ValueComparison(s)); return false; } @@ -724,6 +732,15 @@ private static bool DoesSelectorMatch(PseudoClassSelector pseudoClassSelector, C if (pseudoClassSelector.Class == "root") return box.HtmlTag != null && box.HtmlTag.Name.Equals("html", StringComparison.InvariantCultureIgnoreCase); + // Selectors 4 §13.3 and §13.4, the bare forms are their nth-*() function with An+B = 1 + switch (pseudoClassSelector.Class) + { + case "first-child": return MatchesChildPosition(box, 0, 1, AllSelector.Create(), false, false); + case "last-child": return MatchesChildPosition(box, 0, 1, AllSelector.Create(), false, true); + case "first-of-type": return MatchesChildPosition(box, 0, 1, AllSelector.Create(), true, false); + case "last-of-type": return MatchesChildPosition(box, 0, 1, AllSelector.Create(), true, true); + } + // CSS 2.1 §5.11.2. There's no history, every link is unvisited and :visited never matches. return pseudoClassSelector.Class == "link" && box.IsClickable; } @@ -740,10 +757,15 @@ private static bool DoesSelectorMatch(PseudoElementSelector pseudoElementSelecto return false; } - /// - /// The child-indexed pseudo-classes, Selectors 4 §13.3 and §13.4, including the bare forms like - /// :first-child, which PseudoClassSelectorFactory creates with Step 0 and Offset 1. - /// + private static bool IsChildIndexedPseudoClass(ISelector selector) + { + if (selector is ChildSelector) return true; + if (!(selector is PseudoClassSelector pseudoClass)) return false; + return pseudoClass.Class == "first-child" || pseudoClass.Class == "last-child" + || pseudoClass.Class == "first-of-type" || pseudoClass.Class == "last-of-type"; + } + + /// The child-indexed pseudo-classes, Selectors 4 §13.3 and §13.4. private static bool DoesSelectorMatch(ChildSelector childSelector, CssBox box) { if (box == null || box.HtmlTag == null) @@ -756,6 +778,18 @@ private static bool DoesSelectorMatch(ChildSelector childSelector, CssBox box) return DoesColumnSelectorMatch(childSelector, box, childSelector is LastColumnSelector); } + var sameTypeOnly = childSelector is FirstTypeSelector || childSelector is LastTypeSelector; + var fromEnd = childSelector is LastChildSelector || childSelector is LastTypeSelector; + return MatchesChildPosition(box, childSelector.Step, childSelector.Offset, childSelector.Kind, sameTypeOnly, fromEnd); + } + + private static bool MatchesChildPosition(CssBox box, int step, int offset, ISelector of, bool sameTypeOnly, bool fromEnd) + { + if (box == null || box.HtmlTag == null) + { + return false; + } + var parentBox = DomUtils.GetNearestParentElementBox(box); if (parentBox == null) { @@ -764,7 +798,6 @@ private static bool DoesSelectorMatch(ChildSelector childSelector, CssBox box) IEnumerable siblingBoxes = parentBox.Boxes.Where(b => b.HtmlTag != null); - var sameTypeOnly = childSelector is FirstTypeSelector || childSelector is LastTypeSelector; if (sameTypeOnly) { siblingBoxes = siblingBoxes.Where(b => @@ -772,7 +805,7 @@ private static bool DoesSelectorMatch(ChildSelector childSelector, CssBox box) } // The "of S" filter of :nth-child()/:nth-last-child(). Without it Kind is AllSelector. - siblingBoxes = siblingBoxes.Where(b => DoesSelectorMatch(childSelector.Kind, b)); + siblingBoxes = siblingBoxes.Where(b => DoesSelectorMatch(of, b)); var siblings = siblingBoxes.ToList(); var index = siblings.IndexOf(box); @@ -781,10 +814,9 @@ private static bool DoesSelectorMatch(ChildSelector childSelector, CssBox box) return false; } - var fromEndPosition = childSelector is LastChildSelector || childSelector is LastTypeSelector; - var position = fromEndPosition ? siblings.Count - index : index + 1; + var position = fromEnd ? siblings.Count - index : index + 1; - return MatchesAnPlusB(position, childSelector.Step, childSelector.Offset); + return MatchesAnPlusB(position, step, offset); } /// diff --git a/Source/HtmlRenderer/Core/CssDefaults.cs b/Source/HtmlRenderer/Core/CssDefaults.cs index 109022872..aa5efd949 100644 --- a/Source/HtmlRenderer/Core/CssDefaults.cs +++ b/Source/HtmlRenderer/Core/CssDefaults.cs @@ -163,6 +163,7 @@ @media print { { "font-size", "medium" }, { "font-style", "normal" }, { "font-variant", "normal" }, + { "font-variant-caps", "normal" }, { "font-weight", "normal" }, { "font-stretch", "normal" }, { "float", "none" }, @@ -196,6 +197,7 @@ @media print { { "orphans", "2" }, { "page", "auto" }, { "text-align", "" }, + { "text-align-all", "" }, { "text-decoration-line", "" }, { "text-indent", "0" }, { "top", "auto" }, @@ -207,6 +209,8 @@ @media print { { "word-break", "normal" }, { "visibility", "visible" }, { "white-space", "normal" }, + { "white-space-collapse", "collapse" }, + { "text-wrap-mode", "wrap" }, }; /// @@ -219,10 +223,10 @@ @media print { "border-spacing", "border-collapse", "color", "empty-cells", - "white-space", + "white-space", "white-space-collapse", "text-wrap-mode", "visibility", - "text-indent", "text-align", - "font-family", "font-size", "font-style", "font-variant", "font-weight", "font-stretch", + "text-indent", "text-align", "text-align-all", + "font-family", "font-size", "font-style", "font-variant", "font-variant-caps", "font-weight", "font-stretch", "list-style-image", "list-style-position", "list-style-type", "list-style", "line-height", "word-break", @@ -237,7 +241,7 @@ @media print { internal static string GetInitialValue(string propertyName) { string value; - return InitialValues.TryGetValue(propertyName, out value) ? value : null; + return InitialValues.TryGetValue(Utils.CssUtils.ToPhysicalPropertyName(propertyName), out value) ? value : null; } } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/AspectRatioGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/AspectRatioGrammar.cs deleted file mode 100644 index 54e2e7907..000000000 --- a/Source/HtmlRenderer/Core/CssEngine/AspectRatioGrammar.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace TheArtOfDev.HtmlRenderer.Core.CssEngine -{ - /// - /// The aspect-ratio grammar (CSS Sizing 4 §4.1) and the <ratio> type (CSS Values 4 §5.7). - /// - internal static class AspectRatioGrammar - { - /// - /// is width / height, or null for a bare auto or a degenerate ratio. - /// - internal static bool TryParse(IReadOnlyList tokens, out double? ratio) => - TryParse(tokens, out ratio, out _); - - /// - /// Like , tells - /// auto <ratio>, where a replaced element's natural ratio wins, from a bare <ratio>. - /// - internal static bool TryParse(IReadOnlyList tokens, out double? ratio, out bool hasAuto) - { - ratio = null; - hasAuto = false; - - Token[] toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); - if (toks.Length == 0) return false; - - var i = 0; - - if (IsAuto(toks[i])) { hasAuto = true; i++; } - - if (i == toks.Length) return hasAuto; // bare `auto` - - // = [ / ]? - if (!TryRatioCore(toks, ref i, out ratio)) return false; - - // An optional trailing `auto` (the `||` allows either order: ` auto`). - if (i < toks.Length && !hasAuto && IsAuto(toks[i])) { hasAuto = true; i++; } - - if (i != toks.Length) return false; // trailing junk - - return true; - } - - /// - /// A bare <ratio> without auto, for the @property syntax matcher. - /// - internal static bool TryParseRatio(IReadOnlyList tokens, out double? ratio) - { - ratio = null; - - Token[] toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); - - var i = 0; - if (!TryRatioCore(toks, ref i, out ratio)) return false; - return i == toks.Length; // no trailing junk (and, since we never consume `auto`, no `auto`) - } - - private static bool TryRatioCore(Token[] toks, ref int i, out double? ratio) - { - ratio = null; - - // [ / ]? - if (i >= toks.Length || !TryNonNegativeNumber(toks[i], out var width)) return false; - double height = 1; - i++; - - if (i < toks.Length && IsSlash(toks[i])) - { - i++; - if (i >= toks.Length || !TryNonNegativeNumber(toks[i], out height)) return false; - i++; - } - - // A zero term degenerates to "no preferred aspect ratio". - ratio = width <= 0 || height <= 0 ? (double?)null : width / height; - return true; - } - - private static bool IsAuto(Token token) => token.Type == TokenType.Ident && token.Data.Isi(Keywords.Auto); - - private static bool IsSlash(Token token) => token.Type == TokenType.Delim && token.Data == "/"; - - private static bool TryNonNegativeNumber(Token token, out double value) - { - if (token is NumberToken number && number.Value >= 0f) - { - value = number.Value; - return true; - } - - value = 0; - return false; - } - } -} diff --git a/Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs deleted file mode 100644 index b3e52ec8f..000000000 --- a/Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs +++ /dev/null @@ -1,186 +0,0 @@ -using System.Collections.Generic; - -namespace TheArtOfDev.HtmlRenderer.Core.CssEngine -{ - /// - /// Grammar of a background-position value, CSS Backgrounds 3 §2.6: the 1 to 4 token forms, including edge offsets - /// like "right 20px bottom 10px". Used by and - /// to validate and serialize the value. - /// - internal static class BackgroundPositionGrammar - { - internal enum AxisKeyword { None, Left, Right, Top, Bottom, Center } - - internal readonly struct Component - { - public AxisKeyword Keyword { get; } - - /// The length/percentage/calc() token for this component, or null when there is - /// none (a keyword-only component, or the implicit "center" filled in for a 1-token value). - public Token Offset { get; } - - public Component(AxisKeyword keyword, Token offset) - { - Keyword = keyword; - Offset = offset; - } - } - - internal sealed class ParsedPosition - { - public Component X { get; } - public Component Y { get; } - - /// The 1-2 authored components in original left-to-right order, needed to - /// re-serialize in the exact order/form the author wrote (CSS round-trips verbatim). - public IReadOnlyList AuthoredOrder { get; } - - public ParsedPosition(Component x, Component y, IReadOnlyList authoredOrder) - { - X = x; - Y = y; - AuthoredOrder = authoredOrder; - } - } - - internal static ParsedPosition TryParse(IReadOnlyList tokens) - { - switch (tokens.Count) - { - case 1: - return ParseSingle(tokens[0]); - case 2: - return ParseDouble(tokens[0], tokens[1]); - case 3: - return ParseTriple(tokens[0], tokens[1], tokens[2]); - case 4: - return ParseQuadruple(tokens[0], tokens[1], tokens[2], tokens[3]); - default: - return null; - } - } - - private static ParsedPosition ParseSingle(Token token) - { - var keyword = KeywordOf(token); - if (keyword == AxisKeyword.None) - { - if (!IsLengthOrPercent(token)) return null; - - var bare = new Component(AxisKeyword.None, token); - var center = new Component(AxisKeyword.Center, null); - return new ParsedPosition(bare, center, new[] { bare }); - } - - if (IsVerticalOnly(keyword)) - { - var y = new Component(keyword, null); - var x = new Component(AxisKeyword.Center, null); - return new ParsedPosition(x, y, new[] { y }); - } - - var xComp = new Component(keyword, null); - var yComp = new Component(AxisKeyword.Center, null); - return new ParsedPosition(xComp, yComp, new[] { xComp }); - } - - private static ParsedPosition ParseDouble(Token t0, Token t1) - { - // "h then v" (horizontal-ish first): left/right/center/bare-length, then top/bottom/center/bare-length. - if (IsHSlotValid(t0) && IsVSlotValid(t1)) - { - var x = ToComponent(t0); - var y = ToComponent(t1); - return new ParsedPosition(x, y, new[] { x, y }); - } - - // "v then h" (vertical-ish first, e.g. "bottom center", "top left"). - if (IsVSlotValid(t0) && IsHSlotValid(t1)) - { - var y = ToComponent(t0); - var x = ToComponent(t1); - return new ParsedPosition(x, y, new[] { y, x }); - } - - return null; - } - - private static ParsedPosition ParseTriple(Token t0, Token t1, Token t2) - { - // hi, vi, offset (offset applies to the vertical group): e.g. "right bottom 20px". - if (IsEdgeKeywordSlot(t0, horizontal: true) && IsEdgeKeywordSlot(t1, horizontal: false) && IsLengthOrPercent(t2)) - { - var x = new Component(KeywordOf(t0), null); - var y = new Component(KeywordOf(t1), t2); - return new ParsedPosition(x, y, new[] { x, y }); - } - - // hi, offset, vi (offset applies to the horizontal group): e.g. "right 20px bottom". - if (IsEdgeKeywordSlot(t0, horizontal: true) && IsLengthOrPercent(t1) && IsEdgeKeywordSlot(t2, horizontal: false)) - { - var x = new Component(KeywordOf(t0), t1); - var y = new Component(KeywordOf(t2), null); - return new ParsedPosition(x, y, new[] { x, y }); - } - - return null; - } - - private static ParsedPosition ParseQuadruple(Token t0, Token t1, Token t2, Token t3) - { - // hi, offset, vi, offset: e.g. "right 20px bottom 10px". - if (IsEdgeKeywordSlot(t0, horizontal: true) && IsLengthOrPercent(t1) && - IsEdgeKeywordSlot(t2, horizontal: false) && IsLengthOrPercent(t3)) - { - var x = new Component(KeywordOf(t0), t1); - var y = new Component(KeywordOf(t2), t3); - return new ParsedPosition(x, y, new[] { x, y }); - } - - return null; - } - - private static Component ToComponent(Token token) => - new Component(KeywordOf(token), KeywordOf(token) == AxisKeyword.None ? token : null); - - private static AxisKeyword KeywordOf(Token token) - { - if (token.Type != TokenType.Ident) return AxisKeyword.None; - if (token.Data.Isi(Keywords.Left)) return AxisKeyword.Left; - if (token.Data.Isi(Keywords.Right)) return AxisKeyword.Right; - if (token.Data.Isi(Keywords.Top)) return AxisKeyword.Top; - if (token.Data.Isi(Keywords.Bottom)) return AxisKeyword.Bottom; - if (token.Data.Isi(Keywords.Center)) return AxisKeyword.Center; - return AxisKeyword.None; - } - - private static bool IsHorizontalOnly(AxisKeyword k) => k == AxisKeyword.Left || k == AxisKeyword.Right; - private static bool IsVerticalOnly(AxisKeyword k) => k == AxisKeyword.Top || k == AxisKeyword.Bottom; - - private static bool IsLengthOrPercent(Token token) => - Converters.LengthOrPercentConverter.Convert(new[] { token }) != null; - - /// Valid for the 2-value "h" slot: left/right/center keywords, or a bare length/percent - not top/bottom. - private static bool IsHSlotValid(Token token) - { - var k = KeywordOf(token); - return k != AxisKeyword.None ? !IsVerticalOnly(k) : IsLengthOrPercent(token); - } - - /// Valid for the 2-value "v" slot: top/bottom/center keywords, or a bare length/percent - not left/right. - private static bool IsVSlotValid(Token token) - { - var k = KeywordOf(token); - return k != AxisKeyword.None ? !IsHorizontalOnly(k) : IsLengthOrPercent(token); - } - - /// Valid for the 3/4-value "hi"/"vi" edge-keyword slots: keyword only, no bare-length fallback. - private static bool IsEdgeKeywordSlot(Token token, bool horizontal) - { - var k = KeywordOf(token); - if (k == AxisKeyword.Center) return true; - if (k == AxisKeyword.None) return false; - return horizontal ? IsHorizontalOnly(k) : IsVerticalOnly(k); - } - } -} diff --git a/Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs deleted file mode 100644 index 9d56175fd..000000000 --- a/Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Collections.Generic; - -namespace TheArtOfDev.HtmlRenderer.Core.CssEngine -{ - /// - /// Grammar of a background-size value, CSS Backgrounds 3 §2.9: auto, cover, contain or - /// 1 to 2 length, percentage or auto tokens. Used by - /// to validate and serialize the value. - /// - internal static class BackgroundSizeGrammar - { - internal readonly struct Component - { - public bool IsAuto { get; } - - /// The length/percentage/calc() token for this component, null when . - public Token Value { get; } - - public Component(bool isAuto, Token value) - { - IsAuto = isAuto; - Value = value; - } - } - - internal sealed class ParsedSize - { - public bool IsCover { get; } - public bool IsContain { get; } - public Component Width { get; } - public Component Height { get; } - - /// False for the 1-token auto/length/percent form (height defaults to auto for - /// resolution purposes, but must NOT be re-serialized - "2em" stays "2em", not "2em auto"). - public bool HasExplicitHeight { get; } - - private ParsedSize(bool isCover, bool isContain, Component width, Component height, bool hasExplicitHeight) - { - IsCover = isCover; - IsContain = isContain; - Width = width; - Height = height; - HasExplicitHeight = hasExplicitHeight; - } - - public static ParsedSize Cover() => new ParsedSize(true, false, default, default, false); - public static ParsedSize Contain() => new ParsedSize(false, true, default, default, false); - public static ParsedSize SingleValue(Component width) => new ParsedSize(false, false, width, AutoComponent, false); - public static ParsedSize TwoValue(Component width, Component height) => new ParsedSize(false, false, width, height, true); - } - - private static readonly Component AutoComponent = new Component(true, null); - - internal static ParsedSize TryParse(IReadOnlyList tokens) - { - if (tokens.Count == 1) - { - var t = tokens[0]; - if (IsKeyword(t, Keywords.Cover)) return ParsedSize.Cover(); - if (IsKeyword(t, Keywords.Contain)) return ParsedSize.Contain(); - - var w = ToComponent(t); - return w is null ? null : ParsedSize.SingleValue(w.Value); - } - - if (tokens.Count == 2) - { - var w = ToComponent(tokens[0]); - var h = ToComponent(tokens[1]); - return w is null || h is null ? null : ParsedSize.TwoValue(w.Value, h.Value); - } - - return null; - } - - private static Component? ToComponent(Token token) - { - if (IsKeyword(token, Keywords.Auto)) return AutoComponent; - if (Converters.LengthOrPercentConverter.Convert(new[] { token }) != null) return new Component(false, token); - return null; - } - - private static bool IsKeyword(Token token, string keyword) => - token.Type == TokenType.Ident && token.Data.Isi(keyword); - } -} diff --git a/Source/HtmlRenderer/Core/CssEngine/BasicShapeGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/BasicShapeGrammar.cs deleted file mode 100644 index 451ee9959..000000000 --- a/Source/HtmlRenderer/Core/CssEngine/BasicShapeGrammar.cs +++ /dev/null @@ -1,386 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace TheArtOfDev.HtmlRenderer.Core.CssEngine -{ - /// - /// The <basic-shape> functions polygon(), inset(), circle() and ellipse() - /// (CSS Shapes 1 §3), parsed into unresolved component strings and enums. - /// - internal static class BasicShapeGrammar - { - internal enum BasicShapeKind { Polygon, Inset, Circle, Ellipse } - - internal enum FillRule { Nonzero, Evenodd } - - internal enum ShapeRadiusKind { LengthPercentage, ClosestSide, FarthestSide } - - /// A <shape-radius>: either an explicit length-percentage (component string - /// in ) or one of the closest-side/farthest-side keywords. - internal readonly struct ShapeRadius - { - public ShapeRadiusKind Kind { get; } - - /// The length-percentage component string when is - /// ; otherwise null. - public string Length { get; } - - private ShapeRadius(ShapeRadiusKind kind, string length) - { - Kind = kind; - Length = length; - } - - public static readonly ShapeRadius ClosestSide = new ShapeRadius(ShapeRadiusKind.ClosestSide, null); - public static readonly ShapeRadius FarthestSide = new ShapeRadius(ShapeRadiusKind.FarthestSide, null); - public static ShapeRadius FromLength(string length) => new ShapeRadius(ShapeRadiusKind.LengthPercentage, length); - } - - /// A single <length-percentage> <length-percentage> polygon vertex, - /// stored as the two authored component strings. - internal readonly struct Point - { - public string X { get; } - public string Y { get; } - - public Point(string x, string y) - { - X = x; - Y = y; - } - } - - internal sealed class ParsedBasicShape - { - public BasicShapeKind Kind { get; private set; } - - // --- polygon() --- - public FillRule PolygonFillRule { get; private set; } - public IReadOnlyList PolygonPoints { get; private set; } - - // --- inset() --- - /// The four inset offsets in [top, right, bottom, left] order (CSS shorthand-filled), - /// each an authored length-percentage component string. - public IReadOnlyList InsetEdges { get; private set; } - - /// Whether an inset(... round <border-radius>) corner radius was present. - public bool InsetHasRound { get; private set; } - public IReadOnlyList InsetRoundRadius { get; private set; } - - // --- circle() / ellipse() --- - /// circle: the single radius. ellipse: the x-radius. - public ShapeRadius RadiusX { get; private set; } - /// ellipse: the y-radius. Unused for circle. - public ShapeRadius RadiusY { get; private set; } - /// Center x as an authored length-percentage component string (position keywords - /// already resolved to 0%/50%/100%). Default 50%. - public string CenterX { get; private set; } - /// Center y, same convention as . - public string CenterY { get; private set; } - - internal static ParsedBasicShape Polygon(FillRule fillRule, IReadOnlyList points) => new ParsedBasicShape - { - Kind = BasicShapeKind.Polygon, - PolygonFillRule = fillRule, - PolygonPoints = points, - }; - - internal static ParsedBasicShape Inset(IReadOnlyList edges, bool hasRound, IReadOnlyList roundRadius) => new ParsedBasicShape - { - Kind = BasicShapeKind.Inset, - InsetEdges = edges, - InsetHasRound = hasRound, - InsetRoundRadius = roundRadius, - }; - - internal static ParsedBasicShape Circle(ShapeRadius radius, string centerX, string centerY) => new ParsedBasicShape - { - Kind = BasicShapeKind.Circle, - RadiusX = radius, - CenterX = centerX, - CenterY = centerY, - }; - - internal static ParsedBasicShape Ellipse(ShapeRadius radiusX, ShapeRadius radiusY, string centerX, string centerY) => new ParsedBasicShape - { - Kind = BasicShapeKind.Ellipse, - RadiusX = radiusX, - RadiusY = radiusY, - CenterX = centerX, - CenterY = centerY, - }; - } - - /// - /// Null for anything that isn't a basic shape, none included. - /// - internal static ParsedBasicShape TryParse(IReadOnlyList tokens) - { - var significant = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); - - if (significant.Length != 1) return null; - var function = significant[0] as FunctionToken; - if (function == null) return null; - - var args = function.ArgumentTokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); - - if (function.Data.Isi(FunctionNames.Polygon)) return ParsePolygon(args); - if (function.Data.Isi(FunctionNames.Inset)) return ParseInset(args); - if (function.Data.Isi(FunctionNames.Circle)) return ParseCircle(args); - if (function.Data.Isi(FunctionNames.Ellipse)) return ParseEllipse(args); - - return null; - } - - private static ParsedBasicShape ParsePolygon(IReadOnlyList args) - { - var groups = SplitByComma(args); - if (groups.Count == 0) return null; - - var fillRule = FillRule.Nonzero; - var firstGroup = 0; - - // An optional leading fill-rule ident is its own comma-separated group: "polygon(evenodd, x y, ...)". - if (groups[0].Count == 1 && TryFillRule(groups[0][0], out var parsedRule)) - { - fillRule = parsedRule; - firstGroup = 1; - } - - var points = new List(); - - for (var i = firstGroup; i < groups.Count; i++) - { - var group = groups[i]; - if (group.Count != 2) return null; - if (!IsLengthPercentage(group[0]) || !IsLengthPercentage(group[1])) return null; - points.Add(new Point(group[0].ToValue(), group[1].ToValue())); - } - - return points.Count == 0 ? null : ParsedBasicShape.Polygon(fillRule, points); - } - - private static ParsedBasicShape ParseInset(IReadOnlyList args) - { - if (args.Count == 0) return null; - - var roundIndex = -1; - for (var i = 0; i < args.Count; i++) - { - if (args[i].Type == TokenType.Ident && args[i].Data.Isi(Keywords.Round)) - { - roundIndex = i; - break; - } - } - - var lengthTokens = roundIndex >= 0 ? args.Take(roundIndex).ToArray() : args.ToArray(); - Token[] roundTokens = roundIndex >= 0 ? args.Skip(roundIndex + 1).ToArray() : new Token[0]; - - if (lengthTokens.Length < 1 || lengthTokens.Length > 4) return null; - if (lengthTokens.Any(t => !IsLengthPercentage(t))) return null; - - // "round" with no radius following it is malformed. - if (roundIndex >= 0 && roundTokens.Length == 0) return null; - - var values = lengthTokens.Select(t => t.ToValue()).ToArray(); - var (top, right, bottom, left) = ExpandBox(values); - - return ParsedBasicShape.Inset(new[] { top, right, bottom, left }, roundIndex >= 0, roundTokens); - } - - private static ParsedBasicShape ParseCircle(IReadOnlyList args) - { - if (!SplitAtKeyword(args, Keywords.At, out var radiusTokens, out var hasAt, out var positionTokens)) - return null; - - ShapeRadius radius; - switch (radiusTokens.Count) - { - case 0: - radius = ShapeRadius.ClosestSide; - break; - case 1: - if (!TryShapeRadius(radiusTokens[0], out radius)) return null; - break; - default: - return null; - } - - if (!ResolveCenter(hasAt, positionTokens, out var centerX, out var centerY)) return null; - - return ParsedBasicShape.Circle(radius, centerX, centerY); - } - - private static ParsedBasicShape ParseEllipse(IReadOnlyList args) - { - if (!SplitAtKeyword(args, Keywords.At, out var radiusTokens, out var hasAt, out var positionTokens)) - return null; - - ShapeRadius rx, ry; - switch (radiusTokens.Count) - { - case 0: - rx = ShapeRadius.ClosestSide; - ry = ShapeRadius.ClosestSide; - break; - case 2: - if (!TryShapeRadius(radiusTokens[0], out rx)) return null; - if (!TryShapeRadius(radiusTokens[1], out ry)) return null; - break; - default: - return null; - } - - if (!ResolveCenter(hasAt, positionTokens, out var centerX, out var centerY)) return null; - - return ParsedBasicShape.Ellipse(rx, ry, centerX, centerY); - } - - /// Resolves the optional at <position> tail (via the shared - /// ) to center-x/center-y component strings; defaults to - /// 50% 50% when absent. - private static bool ResolveCenter(bool hasAt, IReadOnlyList positionTokens, out string centerX, out string centerY) - { - centerX = "50%"; - centerY = "50%"; - - if (!hasAt) return true; - - var parsed = BackgroundPositionGrammar.TryParse(positionTokens); - if (parsed is null) return false; - - centerX = ComponentToLength(parsed.X, horizontal: true); - centerY = ComponentToLength(parsed.Y, horizontal: false); - return true; - } - - private static string ComponentToLength(BackgroundPositionGrammar.Component c, bool horizontal) - { - switch (c.Keyword) - { - case BackgroundPositionGrammar.AxisKeyword.Center: - return "50%"; - case BackgroundPositionGrammar.AxisKeyword.Left: - case BackgroundPositionGrammar.AxisKeyword.Top: - // "left"/"top" == 0% edge; "left 20px" == 20px in from that edge. - return c.Offset != null ? c.Offset.ToValue() : "0%"; - case BackgroundPositionGrammar.AxisKeyword.Right: - case BackgroundPositionGrammar.AxisKeyword.Bottom: - // "right"/"bottom" == 100% edge; "right 20px" == 20px in from the far edge. - return c.Offset != null ? $"calc(100% - {c.Offset.ToValue()})" : "100%"; - default: // a bare length-percentage - return c.Offset.ToValue(); - } - } - - /// Splits tokens at the first standalone ident equal to - /// (e.g. at). Fails if the keyword appears with nothing after it. - private static bool SplitAtKeyword(IReadOnlyList tokens, string keyword, - out IReadOnlyList before, out bool found, out IReadOnlyList after) - { - for (var i = 0; i < tokens.Count; i++) - { - if (tokens[i].Type == TokenType.Ident && tokens[i].Data.Isi(keyword)) - { - before = tokens.Take(i).ToArray(); - after = tokens.Skip(i + 1).ToArray(); - found = true; - return after.Count > 0; - } - } - - before = tokens.ToArray(); - after = new Token[0]; - found = false; - return true; - } - - private static List> SplitByComma(IReadOnlyList tokens) - { - var groups = new List>(); - var current = new List(); - - foreach (var token in tokens) - { - if (token.Type == TokenType.Comma) - { - groups.Add(current); - current = new List(); - } - else - { - current.Add(token); - } - } - - groups.Add(current); - - // A trailing/leading/doubled comma yields an empty group, which is always malformed here. - return groups.Any(g => g.Count == 0) && tokens.Count > 0 ? new List>() : groups; - } - - private static (string top, string right, string bottom, string left) ExpandBox(IReadOnlyList values) - { - switch (values.Count) - { - case 1: return (values[0], values[0], values[0], values[0]); - case 2: return (values[0], values[1], values[0], values[1]); - case 3: return (values[0], values[1], values[2], values[1]); - default: return (values[0], values[1], values[2], values[3]); - } - } - - private static bool TryFillRule(Token token, out FillRule fillRule) - { - if (token.Type == TokenType.Ident) - { - if (token.Data.Isi(Keywords.Nonzero)) { fillRule = FillRule.Nonzero; return true; } - if (token.Data.Isi(Keywords.Evenodd)) { fillRule = FillRule.Evenodd; return true; } - } - - fillRule = FillRule.Nonzero; - return false; - } - - private static bool TryShapeRadius(Token token, out ShapeRadius radius) - { - if (token.Type == TokenType.Ident) - { - if (token.Data.Isi(Keywords.ClosestSide)) { radius = ShapeRadius.ClosestSide; return true; } - if (token.Data.Isi(Keywords.FarthestSide)) { radius = ShapeRadius.FarthestSide; return true; } - radius = default; - return false; - } - - if (IsLengthPercentage(token)) - { - // , CSS Shapes 1 §3.1 - var unitToken = token as UnitToken; - var numberToken = token as NumberToken; - if ((unitToken != null && unitToken.Value < 0f) || (numberToken != null && numberToken.Value < 0f)) - { - radius = default(ShapeRadius); - return false; - } - - radius = ShapeRadius.FromLength(token.ToValue()); - return true; - } - - radius = default; - return false; - } - - private static bool IsLengthPercentage(Token token) - { - if (token.Type == TokenType.Dimension || token.Type == TokenType.Percentage) return true; - // calc() stays unevaluated, ToValue() keeps its text - var function = token as FunctionToken; - if (function != null && CalcParser.IsCalcFamily(function.Data)) return true; - // Unitless zero is a valid length. - var number = token as NumberToken; - return number != null && number.Value == 0f; - } - } -} diff --git a/Source/HtmlRenderer/Core/CssEngine/BoxShadowGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/BoxShadowGrammar.cs deleted file mode 100644 index 02661e877..000000000 --- a/Source/HtmlRenderer/Core/CssEngine/BoxShadowGrammar.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace TheArtOfDev.HtmlRenderer.Core.CssEngine -{ - /// - /// Grammar of a box-shadow value, CSS Backgrounds 3 §6.1. Keeps the components as authored - /// strings, em/rem lengths can only be resolved against the box. - /// - internal static class BoxShadowGrammar - { - /// - /// One shadow layer. and default to "0", a null - /// means currentcolor. - /// - internal sealed class ShadowLayer - { - public bool Inset { get; set; } - public string OffsetX { get; set; } - public string OffsetY { get; set; } - public string Blur { get; set; } - public string Spread { get; set; } - public string Color { get; set; } - } - - private static readonly IValueConverter ColorValidator = Converters.ColorConverter.WithCurrentColor(); - - /// - /// The layers in declaration order, an empty list for none, or null if the value is invalid. - /// - internal static List TryParse(IReadOnlyList tokens) - { - var significant = NormalizeHexColorTokens(tokens.Where(t => t.Type != TokenType.Whitespace).ToArray()); - - if (significant.Count == 0) return null; - - if (significant.Count == 1 && significant[0].Type == TokenType.Ident && significant[0].Data.Isi(Keywords.None)) - return new List(); - - var layers = new List(); - - foreach (var group in SplitByComma(significant)) - { - if (group.Count == 0) return null; // leading/trailing/doubled comma - - var layer = ParseLayer(group); - if (layer is null) return null; - - layers.Add(layer); - } - - return layers; - } - - private static ShadowLayer ParseLayer(IReadOnlyList group) - { - var insetSeen = false; - var lengths = new List(); - var colorTokens = new List(); - var lengthsClosed = false; // set once a non-length token appears after lengths have begun - - foreach (var token in group) - { - if (IsInset(token)) - { - if (insetSeen) return null; // "inset" at most once - insetSeen = true; - if (lengths.Count > 0) lengthsClosed = true; - } - else if (IsLength(token)) - { - if (lengthsClosed) return null; // {2,4} must be one contiguous run - lengths.Add(token); - } - else - { - if (lengths.Count > 0) lengthsClosed = true; - colorTokens.Add(token); - } - } - - // Two to four lengths: offset-x, offset-y, [blur], [spread]. - if (lengths.Count < 2 || lengths.Count > 4) return null; - - // Blur radius (the third length) must be non-negative. - if (lengths.Count >= 3 && LengthValue(lengths[2]) < 0) return null; - - // Any color present must be a single valid . - string color = null; - if (colorTokens.Count > 0) - { - if (!IsValidColor(colorTokens)) return null; - color = string.Concat(colorTokens.Select(t => t.ToValue())); - } - - return new ShadowLayer - { - Inset = insetSeen, - OffsetX = lengths[0].ToValue(), - OffsetY = lengths[1].ToValue(), - Blur = lengths.Count >= 3 ? lengths[2].ToValue() : "0", - Spread = lengths.Count >= 4 ? lengths[3].ToValue() : "0", - Color = color, - }; - } - - /// - /// Outside of a declaration value the tokenizer gives a hex color as a bare - /// token instead of a , which doesn't accept. - /// - private static bool IsValidColor(IReadOnlyList colorTokens) - { - if (colorTokens.Count == 1 && colorTokens[0].Type == TokenType.Hash) - return IsHexColor(colorTokens[0].Data); - - return ColorValidator.Convert(colorTokens) != null; - } - - private static bool IsHexColor(string data) - { - var length = data.Length; - return (length == 3 || length == 4 || length == 6 || length == 8) && data.All(Uri.IsHexDigit); - } - - /// - /// Outside of a declaration value the tokenizer splits a hex color starting with a digit (#08f) - /// into a # delimiter and a number or dimension, this merges them back into one hash token. - /// - private static List NormalizeHexColorTokens(IReadOnlyList tokens) - { - var result = new List(tokens.Count); - - for (var i = 0; i < tokens.Count; i++) - { - var token = tokens[i]; - - if (token.Type == TokenType.Delim && token.Data == "#" && i + 1 < tokens.Count - && (tokens[i + 1].Type == TokenType.Number || tokens[i + 1].Type == TokenType.Dimension) - && IsHexColor(tokens[i + 1].ToValue())) - { - result.Add(new KeywordToken(TokenType.Hash, tokens[i + 1].ToValue(), token.Position)); - i++; - continue; - } - - result.Add(token); - } - - return result; - } - - private static bool IsInset(Token token) => - token.Type == TokenType.Ident && token.Data.Isi(Keywords.Inset); - - /// A box-shadow offset/blur/spread is a <length> (not a length-percentage): - /// a dimension, or the unitless zero. Percentages are not valid here. - private static bool IsLength(Token token) - { - if (token.Type == TokenType.Dimension) return true; - var number = token as NumberToken; - return number != null && number.Value == 0f; - } - - private static float LengthValue(Token token) - { - var unit = token as UnitToken; - if (unit != null) return unit.Value; - - var number = token as NumberToken; - if (number != null) return number.Value; - - return 0f; - } - - private static List> SplitByComma(IReadOnlyList tokens) - { - var groups = new List>(); - var current = new List(); - - foreach (var token in tokens) - { - if (token.Type == TokenType.Comma) - { - groups.Add(current); - current = new List(); - } - else - { - current.Add(token); - } - } - - groups.Add(current); - return groups; - } - } -} diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs index 7c327ee19..eb8c080cf 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs @@ -3,8 +3,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// The type a calc() expression resolves to (CSS Values 4 §10.9). Flags, so a sum of a length and - /// a percentage is Length | Percentage. + /// The CSS calc() type-checking category a (sub-)expression resolves to. A flags enum so that + /// Length | Percentage can represent the combined <length-percentage> type once a + /// length and a percentage have been added or subtracted together. /// [Flags] internal enum CalcCategory diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcNode.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcNode.cs index 0198713fd..60e72cc66 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcNode.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcNode.cs @@ -3,7 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// A node of a parsed calc(), min(), max() or clamp() expression (CSS Values 4 §10.10). + /// AST node for a parsed calc()/min()/max()/clamp() expression (CSS Values 4 §10). /// internal abstract class CalcNode { diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcParser.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcParser.cs index 3b3b6d591..825fa62cd 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcParser.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcParser.cs @@ -3,12 +3,17 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// Parses calc(), min(), max() and clamp() (CSS Values 4 §10.8), both when a declaration is - /// converted and again at layout time from the folded text. Returns null on a syntax error. + /// Recursive-descent parser for calc()/min()/max()/clamp() expressions (CSS Values 4 §10). Grammar: + /// <calc-sum> := <calc-product> (('+'|'-') <calc-product>)*, + /// <calc-product> := <calc-value> (('*'|'/') <calc-value>)*, + /// <calc-value> := <number> | <dimension> | <percentage> | '(' <calc-sum> ')' | <nested-call> | <unary-sign> <calc-value>. + /// Binary +/- require whitespace on both sides; *// do not. Returns null on + /// any grammar violation. An optional set of variables binds identifiers to numbers (the channel keywords + /// of a relative color, CSS Color 5 section 4): a bound identifier is a <calc-value> that stands for its number. /// internal static class CalcParser { - public static CalcNode Parse(FunctionToken function) + public static CalcNode Parse(FunctionToken function, IReadOnlyDictionary variables = null) { var name = function.Data; @@ -16,8 +21,8 @@ public static CalcNode Parse(FunctionToken function) { var tokens = new List(function.ArgumentTokens); var pos = 0; - var node = ParseSum(tokens, ref pos); - if (node is null) return null; + var node = ParseSum(tokens, ref pos, variables); + if (node == null) return null; SkipWhitespace(tokens, ref pos); return pos == tokens.Count ? node : null; @@ -25,7 +30,7 @@ public static CalcNode Parse(FunctionToken function) if (name.Isi(FunctionNames.Min) || name.Isi(FunctionNames.Max)) { - var args = ParseCommaSeparatedSums(function, out var groupCount); + var args = ParseCommaSeparatedSums(function, variables, out var groupCount); return groupCount >= 1 && args != null ? new CallCalcNode(name.Isi(FunctionNames.Min) ? FunctionNames.Min : FunctionNames.Max, args) : null; @@ -33,26 +38,33 @@ public static CalcNode Parse(FunctionToken function) if (name.Isi(FunctionNames.Clamp)) { - var args = ParseCommaSeparatedSums(function, out var groupCount); + var args = ParseCommaSeparatedSums(function, variables, out var groupCount); return groupCount == 3 && args != null ? new CallCalcNode(FunctionNames.Clamp, args) : null; } return null; } - private static List ParseCommaSeparatedSums(FunctionToken function, out int groupCount) + public static bool IsCalcFamily(string name) + { + return name.Isi(FunctionNames.Calc) || name.Isi(FunctionNames.Min) || + name.Isi(FunctionNames.Max) || name.Isi(FunctionNames.Clamp); + } + + private static List ParseCommaSeparatedSums(FunctionToken function, IReadOnlyDictionary variables, out int groupCount) { var groups = function.ArgumentTokens.ToList(); groupCount = groups.Count; var args = new List(groups.Count); + foreach (var group in groups) { if (group.Count == 0) return null; var pos = 0; - var node = ParseSum(group, ref pos); - if (node is null) return null; + var node = ParseSum(group, ref pos, variables); + if (node == null) return null; SkipWhitespace(group, ref pos); if (pos != group.Count) return null; @@ -63,10 +75,10 @@ private static List ParseCommaSeparatedSums(FunctionToken function, ou return args; } - private static CalcNode ParseSum(List tokens, ref int pos) + private static CalcNode ParseSum(List tokens, ref int pos, IReadOnlyDictionary variables) { - var left = ParseProduct(tokens, ref pos); - if (left is null) return null; + var left = ParseProduct(tokens, ref pos, variables); + if (left == null) return null; while (true) { @@ -81,7 +93,8 @@ private static CalcNode ParseSum(List tokens, ref int pos) break; } - // A sign without whitespace before it was already merged into the number token + // Binary +/- must have whitespace on both sides; without it this can't be a valid binary + // operator here (an attached sign is already merged into the adjacent numeric token). if (!sawWhitespaceBefore) return null; var op = tokens[pos].Data[0]; @@ -91,8 +104,8 @@ private static CalcNode ParseSum(List tokens, ref int pos) SkipWhitespace(tokens, ref pos); if (pos == afterOp) return null; - var right = ParseProduct(tokens, ref pos); - if (right is null) return null; + var right = ParseProduct(tokens, ref pos, variables); + if (right == null) return null; left = new BinaryCalcNode(op, left, right); } @@ -100,10 +113,10 @@ private static CalcNode ParseSum(List tokens, ref int pos) return left; } - private static CalcNode ParseProduct(List tokens, ref int pos) + private static CalcNode ParseProduct(List tokens, ref int pos, IReadOnlyDictionary variables) { - var left = ParseValue(tokens, ref pos); - if (left is null) return null; + var left = ParseValue(tokens, ref pos, variables); + if (left == null) return null; while (true) { @@ -121,8 +134,8 @@ private static CalcNode ParseProduct(List tokens, ref int pos) pos++; SkipWhitespace(tokens, ref pos); - var right = ParseValue(tokens, ref pos); - if (right is null) return null; + var right = ParseValue(tokens, ref pos, variables); + if (right == null) return null; left = new BinaryCalcNode(op, left, right); } @@ -130,7 +143,7 @@ private static CalcNode ParseProduct(List tokens, ref int pos) return left; } - private static CalcNode ParseValue(List tokens, ref int pos) + private static CalcNode ParseValue(List tokens, ref int pos, IReadOnlyDictionary variables) { SkipWhitespace(tokens, ref pos); if (pos >= tokens.Count) return null; @@ -141,51 +154,25 @@ private static CalcNode ParseValue(List tokens, ref int pos) { case TokenType.Number: pos++; - return new NumberCalcNode(((NumberToken)token).Value); + return new NumberCalcNode(((NumberToken) token).Value); case TokenType.Percentage: pos++; - return new PercentageCalcNode(((UnitToken)token).Value); + return new PercentageCalcNode(((UnitToken) token).Value); - case TokenType.Dimension: - { - var unitToken = (UnitToken)token; - var lengthUnit = Length.GetUnit(unitToken.Unit); - if (IsSupportedLengthUnit(lengthUnit)) - { - pos++; - return new DimensionCalcNode(unitToken.Value, lengthUnit); - } - - var angleUnit = Angle.GetUnit(unitToken.Unit); - if (angleUnit != Angle.Unit.None) - { - pos++; - return new AngleCalcNode(unitToken.Value, angleUnit); - } - - var timeUnit = Time.GetUnit(unitToken.Unit); - if (timeUnit != Time.Unit.None) - { - pos++; - return new TimeCalcNode(unitToken.Value, timeUnit); - } - - var resolutionUnit = Resolution.GetUnit(unitToken.Unit); - if (resolutionUnit != Resolution.Unit.None) - { - pos++; - return new ResolutionCalcNode(unitToken.Value, resolutionUnit); - } + // An identifier bound by the caller stands for its number. + case TokenType.Ident when variables != null && variables.TryGetValue(token.Data.ToLowerInvariant(), out var bound): + pos++; + return new NumberCalcNode(bound); - return null; - } + case TokenType.Dimension: + return ParseDimension((UnitToken) token, tokens, ref pos); case TokenType.RoundBracketOpen: { pos++; - var inner = ParseSum(tokens, ref pos); - if (inner is null) return null; + var inner = ParseSum(tokens, ref pos, variables); + if (inner == null) return null; SkipWhitespace(tokens, ref pos); if (pos >= tokens.Count || tokens[pos].Type != TokenType.RoundBracketClose) return null; @@ -198,23 +185,24 @@ private static CalcNode ParseValue(List tokens, ref int pos) var negative = token.Data == "-"; pos++; - // Only reachable before ( or a function, a signed number is a single token + // A unary sign must be immediately followed by a parenthesized group or a nested + // function call (a signed number/dimension is already a single token from the lexer). if (pos >= tokens.Count || (tokens[pos].Type != TokenType.RoundBracketOpen && tokens[pos].Type != TokenType.Function)) { return null; } - var operand = ParseValue(tokens, ref pos); - return operand is null ? null : new UnaryCalcNode(negative, operand); + var operand = ParseValue(tokens, ref pos, variables); + return operand == null ? null : new UnaryCalcNode(negative, operand); } case TokenType.Function: { - var nested = (FunctionToken)token; + var nested = (FunctionToken) token; if (!IsCalcFamily(nested.Data)) return null; pos++; - return Parse(nested); + return Parse(nested, variables); } default: @@ -222,17 +210,96 @@ private static CalcNode ParseValue(List tokens, ref int pos) } } - private static bool IsSupportedLengthUnit(Length.Unit unit) + private static CalcNode ParseDimension(UnitToken token, List tokens, ref int pos) { - return unit == Length.Unit.Em || unit == Length.Unit.Rem || unit == Length.Unit.Ex || unit == Length.Unit.Px || - unit == Length.Unit.Mm || unit == Length.Unit.Cm || unit == Length.Unit.In || unit == Length.Unit.Pt || unit == Length.Unit.Pc; + var lengthUnit = Length.GetUnit(token.Unit); + if (IsSupportedLengthUnit(lengthUnit)) + { + pos++; + return new DimensionCalcNode(token.Value, lengthUnit); + } + + var angleUnit = Angle.GetUnit(token.Unit); + if (angleUnit != Angle.Unit.None) + { + pos++; + return new AngleCalcNode(token.Value, angleUnit); + } + + var timeUnit = Time.GetUnit(token.Unit); + if (timeUnit != Time.Unit.None) + { + pos++; + return new TimeCalcNode(token.Value, timeUnit); + } + + var resolutionUnit = Resolution.GetUnit(token.Unit); + if (resolutionUnit != Resolution.Unit.None) + { + pos++; + return new ResolutionCalcNode(token.Value, resolutionUnit); + } + + return null; } - /// Whether is calc, min, max or clamp, ignoring case. - public static bool IsCalcFamily(string name) + private static bool IsSupportedLengthUnit(Length.Unit unit) { - return name.Isi(FunctionNames.Calc) || name.Isi(FunctionNames.Min) || - name.Isi(FunctionNames.Max) || name.Isi(FunctionNames.Clamp); + switch (unit) + { + case Length.Unit.Em: + case Length.Unit.Rem: + case Length.Unit.Ex: + case Length.Unit.Ch: + case Length.Unit.Vw: + case Length.Unit.Vh: + case Length.Unit.Vmin: + case Length.Unit.Vmax: + case Length.Unit.Px: + case Length.Unit.Mm: + case Length.Unit.Cm: + case Length.Unit.In: + case Length.Unit.Pt: + case Length.Unit.Pc: + case Length.Unit.Q: + case Length.Unit.Cap: + case Length.Unit.Ic: + case Length.Unit.Lh: + case Length.Unit.Rlh: + case Length.Unit.Rex: + case Length.Unit.Rch: + case Length.Unit.Rcap: + case Length.Unit.Ric: + case Length.Unit.Vi: + case Length.Unit.Vb: + case Length.Unit.Svw: + case Length.Unit.Svh: + case Length.Unit.Svi: + case Length.Unit.Svb: + case Length.Unit.Svmin: + case Length.Unit.Svmax: + case Length.Unit.Lvw: + case Length.Unit.Lvh: + case Length.Unit.Lvi: + case Length.Unit.Lvb: + case Length.Unit.Lvmin: + case Length.Unit.Lvmax: + case Length.Unit.Dvw: + case Length.Unit.Dvh: + case Length.Unit.Dvi: + case Length.Unit.Dvb: + case Length.Unit.Dvmin: + case Length.Unit.Dvmax: + case Length.Unit.Cqw: + case Length.Unit.Cqh: + case Length.Unit.Cqi: + case Length.Unit.Cqb: + case Length.Unit.Cqmin: + case Length.Unit.Cqmax: + return true; + default: + return false; + } } private static void SkipWhitespace(List tokens, ref int pos) diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcSerializer.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcSerializer.cs index 70110181c..c680398ac 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcSerializer.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcSerializer.cs @@ -1,11 +1,16 @@ +using System; using System.Collections.Generic; using System.Globalization; namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// The text of a type-checked calc() (CSS Values 4 §10.10.1): numbers, angles and absolute lengths - /// fold to a single value, anything with em, rem, ex or % stays an expression for layout. + /// Produces the canonical CSS text for a validated calc()/min()/max()/clamp() tree. An expression made + /// only of numbers folds to a plain number; one made only of angles folds to a plain angle in + /// deg; one made only of absolute lengths (px, cm, mm, Q, in, pt, pc) folds to a plain + /// px length. Anything that needs layout context (em, rem, %, viewport units, ...) is + /// re-serialized to normalized calc()/min()/max()/clamp() text, with spaces around binary operators + /// and only the parentheses that precedence requires. /// internal static class CalcSerializer { @@ -13,44 +18,58 @@ public static string Serialize(CalcNode node, CalcCategory category) { if (category == CalcCategory.Number) { - var value = CalcTypeChecker.FoldNumber(node) ?? 0d; - return ((float)value).ToString(null, CultureInfo.InvariantCulture); + var number = CalcTypeChecker.FoldNumber(node); + if (number != null) return FormatNumber(number.Value); } - - if (category == CalcCategory.Angle) + else if (category == CalcCategory.Angle) { - var radians = FoldAngle(node) ?? 0d; - var degrees = radians * (180.0 / System.Math.PI); - return new Angle((float)degrees, Angle.Unit.Deg).ToString(null, CultureInfo.InvariantCulture); + // Every angle unit converts by a fixed constant, so a valid angle expression always folds. + var degrees = FoldAngle(node); + if (degrees != null) return new Angle((float) degrees.Value, Angle.Unit.Deg).ToString(null, CultureInfo.InvariantCulture); } - - if (TryFoldPixels(node, out var pixels)) + else if (category == CalcCategory.Length && TryFoldPixels(node, out var pixels)) { - return new Length((float)pixels, Length.Unit.Px).ToString(null, CultureInfo.InvariantCulture); + return new Length((float) pixels, Length.Unit.Px).ToString(null, CultureInfo.InvariantCulture); } return SerializeExpression(node); } - /// Folds an Angle-category subtree to radians; always succeeds for a validated tree. + /// + /// Evaluates a pure-number expression and returns the folded value, or null when the tree is not + /// purely numeric. This is the constant-folding used for <integer>/<number> + /// contexts; it never needs layout context. + /// + public static double? EvaluateNumber(CalcNode node) + { + return CalcTypeChecker.FoldNumber(node); + } + + private static string FormatNumber(double value) + { + return ((float) value).ToString(null, CultureInfo.InvariantCulture); + } + + // ---- Angle folding (degrees) ---- + private static double? FoldAngle(CalcNode node) { switch (node) { case AngleCalcNode angle: - return new Angle((float)angle.Value, angle.Unit).ToRadian(); + return ToDegrees(angle.Value, angle.Unit); case UnaryCalcNode unary: { var value = FoldAngle(unary.Operand); - return value is null ? null : (double?)(unary.Negative ? -value.Value : value.Value); + return value == null ? (double?) null : unary.Negative ? -value.Value : value.Value; } case BinaryCalcNode binary when binary.Operator == '+' || binary.Operator == '-': { var left = FoldAngle(binary.Left); var right = FoldAngle(binary.Right); - if (left is null || right is null) return null; + if (left == null || right == null) return null; return binary.Operator == '+' ? left.Value + right.Value : left.Value - right.Value; } @@ -60,89 +79,179 @@ public static string Serialize(CalcNode node, CalcCategory category) if (leftAngle != null) { var scalar = CalcTypeChecker.FoldNumber(binary.Right); - return scalar is null ? (double?)null : leftAngle.Value * scalar.Value; + return scalar == null ? (double?) null : leftAngle.Value * scalar.Value; } var rightAngle = FoldAngle(binary.Right); - if (rightAngle != null) - { - var scalar = CalcTypeChecker.FoldNumber(binary.Left); - return scalar is null ? (double?)null : rightAngle.Value * scalar.Value; - } - - return null; + if (rightAngle == null) return null; + var factor = CalcTypeChecker.FoldNumber(binary.Left); + return factor == null ? (double?) null : rightAngle.Value * factor.Value; } case BinaryCalcNode binary when binary.Operator == '/': { var left = FoldAngle(binary.Left); - if (left is null) return null; var divisor = CalcTypeChecker.FoldNumber(binary.Right); - return divisor is null || divisor == 0d ? (double?)null : left.Value / divisor.Value; + if (left == null || divisor == null || divisor.Value == 0d) return null; + return left.Value / divisor.Value; } case CallCalcNode call: + return FoldCall(call, FoldAngle); + + default: + return null; + } + } + + private static double ToDegrees(double value, Angle.Unit unit) + { + switch (unit) + { + case Angle.Unit.Grad: return value * 0.9d; + case Angle.Unit.Rad: return value * (180d / Math.PI); + case Angle.Unit.Turn: return value * 360d; + default: return value; + } + } + + // ---- Absolute length folding (CSS pixels) ---- + + private static bool TryFoldPixels(CalcNode node, out double pixels) + { + var value = FoldPixels(node); + pixels = value ?? 0d; + return value != null; + } + + private static double? FoldPixels(CalcNode node) + { + switch (node) + { + // A unitless number is only ever a scalar operand of '*' or '/' in a Length expression. + case NumberCalcNode number: + return number.Value; + + case DimensionCalcNode dimension: { - var values = new List(call.Arguments.Count); - foreach (var argument in call.Arguments) - { - var value = FoldAngle(argument); - if (value is null) return null; - values.Add(value.Value); - } + var length = new Length((float) dimension.Value, dimension.Unit); + return length.IsAbsolute ? length.ToPixel() : (double?) null; + } + + case UnaryCalcNode unary: + { + var value = FoldPixels(unary.Operand); + return value == null ? (double?) null : unary.Negative ? -value.Value : value.Value; + } - if (call.Name.Isi(FunctionNames.Min)) return Min(values); - if (call.Name.Isi(FunctionNames.Max)) return Max(values); - if (call.Name.Isi(FunctionNames.Clamp) && values.Count == 3) - return values[0] > values[2] ? values[2] : System.Math.Min(System.Math.Max(values[1], values[0]), values[2]); + case BinaryCalcNode binary: + { + var left = FoldPixels(binary.Left); + var right = FoldPixels(binary.Right); + if (left == null || right == null) return null; - return null; + switch (binary.Operator) + { + case '+': return left.Value + right.Value; + case '-': return left.Value - right.Value; + case '*': return left.Value * right.Value; + case '/': return right.Value != 0d ? left.Value / right.Value : (double?) null; + default: return null; + } } + case CallCalcNode call: + return FoldCall(call, FoldPixels); + default: return null; } } + private static double? FoldCall(CallCalcNode call, Func fold) + { + var values = new List(call.Arguments.Count); + foreach (var argument in call.Arguments) + { + var value = fold(argument); + if (value == null) return null; + values.Add(value.Value); + } + + if (call.Name.Isi(FunctionNames.Min)) + { + var result = values[0]; + for (var i = 1; i < values.Count; i++) if (values[i] < result) result = values[i]; + return result; + } + + if (call.Name.Isi(FunctionNames.Max)) + { + var result = values[0]; + for (var i = 1; i < values.Count; i++) if (values[i] > result) result = values[i]; + return result; + } + + if (call.Name.Isi(FunctionNames.Clamp) && values.Count == 3) + { + // clamp(min, val, max) == max(min, min(val, max)); if min > max, min wins. + var upper = values[1] < values[2] ? values[1] : values[2]; + return upper < values[0] ? values[0] : upper; + } + + return null; + } + + // ---- Normalized expression text ---- + private static string SerializeExpression(CalcNode node) { if (node is CallCalcNode call) { var args = new List(call.Arguments.Count); foreach (var argument in call.Arguments) args.Add(SerializeOperand(argument, false, 0)); - return $"{call.Name}({string.Join(", ", args)})"; + return call.Name + "(" + string.Join(", ", args) + ")"; } - return $"calc({SerializeOperand(node, false, 0)})"; + return "calc(" + SerializeOperand(node, false, 0) + ")"; } - /// - /// Serializes an operand of a parent with the given precedence (0 none, 1 + and -, 2 * and /), - /// in parentheses where re-parsing would group it differently. - /// + // parentPrecedence: 0 = none, 1 = sum-level + and -, 2 = product-level * and /, 3 = unary operand. + // Parentheses are added whenever omitting them would change the meaning on re-parse: a + // lower-precedence child under a higher-precedence parent, or a same-precedence child on the right + // (the operators are left-associative). private static string SerializeOperand(CalcNode node, bool isRightOperand, int parentPrecedence) { switch (node) { case NumberCalcNode number: - return ((float)number.Value).ToString(null, CultureInfo.InvariantCulture); + return FormatNumber(number.Value); + // Units are always written here, unlike Length.ToString which drops the unit of a zero + // length - a unitless 0 inside calc() would be a number and break type checking. case DimensionCalcNode dimension: - return new Length((float)dimension.Value, dimension.Unit).ToString(null, CultureInfo.InvariantCulture); + return FormatNumber(dimension.Value) + new Length(0f, dimension.Unit).UnitString; case PercentageCalcNode percentage: - return new Length((float)percentage.Value, Length.Unit.Percent).ToString(null, CultureInfo.InvariantCulture); + return FormatNumber(percentage.Value) + "%"; + + case AngleCalcNode angle: + return FormatNumber(angle.Value) + new Angle(0f, angle.Unit).UnitString; case TimeCalcNode time: - return new Time((float)time.Value, time.Unit).ToString(null, CultureInfo.InvariantCulture); + return new Time((float) time.Value, time.Unit).ToString(null, CultureInfo.InvariantCulture); case ResolutionCalcNode resolution: - return new Resolution((float)resolution.Value, resolution.Unit).ToString(null, CultureInfo.InvariantCulture); + return new Resolution((float) resolution.Value, resolution.Unit) + .ToString(null, CultureInfo.InvariantCulture); case UnaryCalcNode unary: { var operand = SerializeOperand(unary.Operand, false, 3); - return unary.Negative ? $"-{operand}" : operand; + if (!unary.Negative) return operand; + + // "--x" would not re-parse as a double negation. + return operand.StartsWith("-", StringComparison.Ordinal) ? "-(" + operand + ")" : "-" + operand; } case CallCalcNode call: @@ -151,107 +260,16 @@ private static string SerializeOperand(CalcNode node, bool isRightOperand, int p case BinaryCalcNode binary: { var precedence = binary.Operator == '+' || binary.Operator == '-' ? 1 : 2; - var text = - $"{SerializeOperand(binary.Left, false, precedence)} {binary.Operator} {SerializeOperand(binary.Right, true, precedence)}"; - var needsParens = precedence < parentPrecedence || (precedence == parentPrecedence && isRightOperand); - return needsParens ? $"({text})" : text; + var text = SerializeOperand(binary.Left, false, precedence) + " " + binary.Operator + " " + + SerializeOperand(binary.Right, true, precedence); + var needsParentheses = precedence < parentPrecedence || + (precedence == parentPrecedence && isRightOperand); + return needsParentheses ? "(" + text + ")" : text; } default: return string.Empty; } } - - private static bool TryFoldPixels(CalcNode node, out double pixels) - { - switch (node) - { - case NumberCalcNode number: - pixels = number.Value; - return true; - - case DimensionCalcNode dimension when dimension.Unit != Length.Unit.Em && dimension.Unit != Length.Unit.Rem && dimension.Unit != Length.Unit.Ex: - pixels = new Length((float)dimension.Value, dimension.Unit).ToPixels(0, 0, 0); - return true; - - case UnaryCalcNode unary when TryFoldPixels(unary.Operand, out var operand): - pixels = unary.Negative ? -operand : operand; - return true; - - case BinaryCalcNode binary when TryFoldPixels(binary.Left, out var left) && TryFoldPixels(binary.Right, out var right): - switch (binary.Operator) - { - case '+': - pixels = left + right; - return true; - case '-': - pixels = left - right; - return true; - case '*': - pixels = left * right; - return true; - case '/' when right != 0d: - pixels = left / right; - return true; - default: - pixels = 0; - return false; - } - - case CallCalcNode call: - { - var values = new List(call.Arguments.Count); - foreach (var argument in call.Arguments) - { - if (!TryFoldPixels(argument, out var value)) - { - pixels = 0; - return false; - } - - values.Add(value); - } - - if (call.Name.Isi(FunctionNames.Min)) - { - pixels = Min(values); - return true; - } - - if (call.Name.Isi(FunctionNames.Max)) - { - pixels = Max(values); - return true; - } - - if (call.Name.Isi(FunctionNames.Clamp) && values.Count == 3) - { - pixels = values[0] > values[2] ? values[2] : System.Math.Min(System.Math.Max(values[1], values[0]), values[2]); - return true; - } - - pixels = 0; - return false; - } - - default: - pixels = 0; - return false; - } - } - - private static double Min(List values) - { - var result = values[0]; - for (var i = 1; i < values.Count; i++) if (values[i] < result) result = values[i]; - return result; - } - - private static double Max(List values) - { - var result = values[0]; - for (var i = 1; i < values.Count; i++) if (values[i] > result) result = values[i]; - return result; - } } } diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcTypeChecker.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcTypeChecker.cs index 3bee69a49..5c503ed04 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcTypeChecker.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcTypeChecker.cs @@ -1,12 +1,14 @@ -using System; using System.Collections.Generic; using System.Linq; namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// Type checking of a calc() tree (CSS Values 4 §10.9), null when the types don't combine. Only a - /// number may be multiplied with or divide another type, so a division by zero is found here. + /// Implements CSS calc()'s type-checking rules (CSS Values 4 §10.10) over a parsed + /// tree, returning the resolved category or null when the expression is + /// dimensionally inconsistent (e.g. adding a length to a time, or dividing by something that isn't a + /// number). Also folds pure-number subtrees, which is what makes a constant divide-by-zero divisor + /// detectable here. /// internal static class CalcTypeChecker { @@ -36,17 +38,13 @@ internal static class CalcTypeChecker return Check(unary.Operand); case BinaryCalcNode binary when binary.Operator == '+' || binary.Operator == '-': - { - var left = Check(binary.Left); - var right = Check(binary.Right); - return Combine(left, right); - } + return Combine(Check(binary.Left), Check(binary.Right)); case BinaryCalcNode binary when binary.Operator == '*': { var left = Check(binary.Left); var right = Check(binary.Right); - if (left is null || right is null) return null; + if (left == null || right == null) return null; if (left == CalcCategory.Number) return right; if (right == CalcCategory.Number) return left; return null; @@ -56,10 +54,12 @@ internal static class CalcTypeChecker { var left = Check(binary.Left); var right = Check(binary.Right); - if (left is null || right != CalcCategory.Number) return null; + if (left == null || right != CalcCategory.Number) return null; + // A Number-category subtree is built entirely from number leaves, so it always folds - + // this is what lets a constant divide-by-zero be rejected here rather than deferred. var divisor = FoldNumber(binary.Right); - return divisor == null || divisor == 0d ? null : left; + return divisor == null || divisor.Value == 0d ? (CalcCategory?) null : left; } case CallCalcNode call: @@ -68,9 +68,9 @@ internal static class CalcTypeChecker foreach (var argument in call.Arguments) { var category = Check(argument); - if (category is null) return null; - combined = combined is null ? category : Combine(combined, category); - if (combined is null) return null; + if (category == null) return null; + combined = combined == null ? category : Combine(combined, category); + if (combined == null) return null; } return combined; @@ -81,7 +81,7 @@ internal static class CalcTypeChecker } } - /// Folds a pure-Number subtree to a concrete value; null if it isn't purely numeric. + // Folds a pure-number subtree to a concrete value; null if it isn't purely numeric. public static double? FoldNumber(CalcNode node) { switch (node) @@ -92,27 +92,22 @@ internal static class CalcTypeChecker case UnaryCalcNode unary: { var value = FoldNumber(unary.Operand); - return value is null ? null : (double?)(unary.Negative ? -value.Value : value.Value); + return value == null ? (double?) null : unary.Negative ? -value.Value : value.Value; } case BinaryCalcNode binary: { var left = FoldNumber(binary.Left); var right = FoldNumber(binary.Right); - if (left is null || right is null) return null; + if (left == null || right == null) return null; switch (binary.Operator) { - case '+': - return left.Value + right.Value; - case '-': - return left.Value - right.Value; - case '*': - return left.Value * right.Value; - case '/': - return right.Value != 0d ? (double?)(left.Value / right.Value) : null; - default: - return null; + case '+': return left.Value + right.Value; + case '-': return left.Value - right.Value; + case '*': return left.Value * right.Value; + case '/': return right.Value != 0d ? left.Value / right.Value : (double?) null; + default: return null; } } @@ -122,26 +117,34 @@ internal static class CalcTypeChecker foreach (var argument in call.Arguments) { var value = FoldNumber(argument); - if (value is null) return null; + if (value == null) return null; values.Add(value.Value); } if (call.Name.Isi(FunctionNames.Min)) return values.Min(); if (call.Name.Isi(FunctionNames.Max)) return values.Max(); if (call.Name.Isi(FunctionNames.Clamp) && values.Count == 3) - return values[0] > values[2] ? values[2] : Math.Max(values[0], Math.Min(values[2], values[1])); + { + var min = values[0]; + var val = values[1]; + var max = values[2]; + // clamp(min, val, max) == max(min, min(val, max)); if min > max, min wins. + var upper = val < max ? val : max; + return upper < min ? min : upper; + } return null; } default: + // Dimension/percentage leaves are never purely numeric. return null; } } private static CalcCategory? Combine(CalcCategory? left, CalcCategory? right) { - if (left is null || right is null) return null; + if (left == null || right == null) return null; if (left == CalcCategory.Number && right == CalcCategory.Number) return CalcCategory.Number; if (left.Value.HasFlag(CalcCategory.Number) || right.Value.HasFlag(CalcCategory.Number)) return null; return left.Value | right.Value; diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcValue.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcValue.cs index 928e2d5ce..5012a212d 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcValue.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcValue.cs @@ -3,7 +3,10 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// An accepted calc(), min(), max() or clamp() value, is the folded text. + /// The value produced by for an accepted calc()/min()/max()/clamp() + /// expression. Keeps the parsed and its type-checked ; + /// is the canonical text from (folded to a plain + /// value where the expression needs no layout context), and the raw tokens. /// internal sealed class CalcValue : IPropertyValue { @@ -15,9 +18,9 @@ public CalcValue(CalcNode node, CalcCategory category, IEnumerable tokens Original = new TokenValue(tokens); } - internal CalcNode Node { get; } + public CalcNode Node { get; } - internal CalcCategory Category { get; } + public CalcCategory Category { get; } public string CssText { get; } diff --git a/Source/HtmlRenderer/Core/CssEngine/ColorInterpolationMethodGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/ColorInterpolationMethodGrammar.cs deleted file mode 100644 index 4cbfdfe5a..000000000 --- a/Source/HtmlRenderer/Core/CssEngine/ColorInterpolationMethodGrammar.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace TheArtOfDev.HtmlRenderer.Core.CssEngine -{ - /// - /// The <color-interpolation-method> of a gradient (CSS Color 4 §13.3). Only the color - /// spaces implements are accepted, so a98-rgb, prophoto-rgb, rec2020 - /// and display-p3-linear are rejected. - /// - internal static class ColorInterpolationMethodGrammar - { - private const string In = "in"; - private const string Hue = "hue"; - - private static readonly HashSet ColorSpaces = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "srgb", "srgb-linear", "display-p3", "lab", "oklab", "xyz", "xyz-d65", "xyz-d50", - "hsl", "hwb", "lch", "oklch", - }; - - private static readonly HashSet PolarColorSpaces = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "hsl", "hwb", "lch", "oklch", - }; - - private static readonly HashSet HueDirections = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "shorter", "longer", "increasing", "decreasing", - }; - - public static bool IsColorSpace(string name) - { - return name != null && ColorSpaces.Contains(name); - } - - public static bool IsPolarColorSpace(string name) - { - return name != null && PolarColorSpaces.Contains(name); - } - - public static bool IsHueDirection(string name) - { - return name != null && HueDirections.Contains(name); - } - - /// - /// If (one gradient comma-group's tokens) contains an in - /// interpolation-method prelude, validates it and returns the group with the contiguous - /// in … slice removed as (for the caller to validate as the - /// gradient's own direction/shape/position, since the two combine with || in either order). - /// Returns when an in is present but the method is malformed - /// (unknown/unsupported space, a hue-interpolation method on a rectangular space or without the - /// trailing hue, or in with nothing after it) — the caller drops the declaration. - /// When no in is present, is - /// , is the group unchanged, and it returns - /// . - /// - public static bool TryExtractInterpolationMethod( - IEnumerable group, out List remainder, out bool hasInterpolationMethod) - { - var tokens = new List(group); - - int inIndex = IndexOfIdent(tokens, 0, In); - if (inIndex < 0) - { - remainder = tokens; - hasInterpolationMethod = false; - return true; - } - - hasInterpolationMethod = true; - - // in - int spaceIndex = NextNonWhitespace(tokens, inIndex + 1); - string spaceName; - if (spaceIndex < 0 || !IsIdent(tokens[spaceIndex], out spaceName) || !IsColorSpace(spaceName)) - { - remainder = null; - return false; - } - - int lastMethodIndex = spaceIndex; - - // A hue-interpolation method ( hue) is only valid after a polar space, and only complete - // with the trailing "hue" keyword. - if (IsPolarColorSpace(spaceName)) - { - int dirIndex = NextNonWhitespace(tokens, spaceIndex + 1); - string dir; - if (dirIndex >= 0 && IsIdent(tokens[dirIndex], out dir) && IsHueDirection(dir)) - { - int hueIndex = NextNonWhitespace(tokens, dirIndex + 1); - string hueWord; - if (hueIndex < 0 || !IsIdent(tokens[hueIndex], out hueWord) || - !string.Equals(hueWord, Hue, StringComparison.OrdinalIgnoreCase)) - { - remainder = null; - return false; - } - - lastMethodIndex = hueIndex; - } - } - - remainder = BuildRemainder(tokens, inIndex, lastMethodIndex); - return true; - } - - // The group with tokens [firstMethodIndex..lastMethodIndex] removed, then whitespace-trimmed at the - // edges so the leftover direction/shape validates the same as an un-prefixed group would. - private static List BuildRemainder(List tokens, int firstMethodIndex, int lastMethodIndex) - { - var remainder = new List(tokens.Count); - - for (int i = 0; i < tokens.Count; i++) - { - if (i >= firstMethodIndex && i <= lastMethodIndex) continue; - remainder.Add(tokens[i]); - } - - while (remainder.Count > 0 && remainder[0].Type == TokenType.Whitespace) - remainder.RemoveAt(0); - while (remainder.Count > 0 && remainder[remainder.Count - 1].Type == TokenType.Whitespace) - remainder.RemoveAt(remainder.Count - 1); - - return remainder; - } - - private static int IndexOfIdent(List tokens, int start, string data) - { - for (int i = start; i < tokens.Count; i++) - { - string value; - if (IsIdent(tokens[i], out value) && - string.Equals(value, data, StringComparison.OrdinalIgnoreCase)) - return i; - } - - return -1; - } - - private static int NextNonWhitespace(List tokens, int start) - { - for (int i = start; i < tokens.Count; i++) - { - if (tokens[i].Type != TokenType.Whitespace) return i; - } - - return -1; - } - - private static bool IsIdent(Token token, out string data) - { - if (token.Type == TokenType.Ident) - { - data = token.Data; - return true; - } - - data = null; - return false; - } - } -} diff --git a/Source/HtmlRenderer/Core/CssEngine/Conditions/ContainerCondition.cs b/Source/HtmlRenderer/Core/CssEngine/Conditions/ContainerCondition.cs new file mode 100644 index 000000000..e5ce27e52 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Conditions/ContainerCondition.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.IO; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ContainerCondition : IContainerCondition + { + private static readonly IReadOnlyList NoOperands = new IContainerCondition[0]; + + private ContainerCondition(ContainerConditionType type, IReadOnlyList operands, + string property, string value) + { + Type = type; + Operands = operands; + Property = property; + Value = value; + } + + public ContainerConditionType Type { get; } + public IReadOnlyList Operands { get; } + public string Property { get; } + public string Value { get; } + + public static ContainerCondition Size(string text) => + new ContainerCondition(ContainerConditionType.Size, NoOperands, null, text); + + public static ContainerCondition Declaration(string property, string value) => + new ContainerCondition(ContainerConditionType.Declaration, NoOperands, property, value); + + public static ContainerCondition Style(IContainerCondition query) => + new ContainerCondition(ContainerConditionType.Style, new[] { query }, null, null); + + public static ContainerCondition Not(IContainerCondition operand) => + new ContainerCondition(ContainerConditionType.Not, new[] { operand }, null, null); + + public static ContainerCondition And(List operands) => + new ContainerCondition(ContainerConditionType.And, operands, null, null); + + public static ContainerCondition Or(List operands) => + new ContainerCondition(ContainerConditionType.Or, operands, null, null); + + public void ToCss(TextWriter writer, IStyleFormatter formatter) + { + switch (Type) + { + case ContainerConditionType.Size: + writer.Write(Value); + break; + case ContainerConditionType.Declaration: + writer.Write(Value == null ? Property : $"{Property}: {Value}"); + break; + case ContainerConditionType.Style: + // Directly inside style( ), a lone feature needs no parentheses of its own. + writer.Write("style("); + Operands[0].ToCss(writer, formatter); + writer.Write(")"); + break; + case ContainerConditionType.Not: + writer.Write("not "); + WriteOperand(Operands[0], writer, formatter); + break; + default: + var connector = Type == ContainerConditionType.And ? " and " : " or "; + for (var i = 0; i < Operands.Count; i++) + { + if (i > 0) writer.Write(connector); + WriteOperand(Operands[i], writer, formatter); + } + break; + } + } + + // Features and nested combinators are parenthesised inside a combinator; style() and size features carry their own. + private static void WriteOperand(IContainerCondition operand, TextWriter writer, IStyleFormatter formatter) + { + var wrap = operand.Type != ContainerConditionType.Size && operand.Type != ContainerConditionType.Style; + if (wrap) writer.Write("("); + operand.ToCss(writer, formatter); + if (wrap) writer.Write(")"); + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Conditions/ContainerConditionType.cs b/Source/HtmlRenderer/Core/CssEngine/Conditions/ContainerConditionType.cs new file mode 100644 index 000000000..dc849e5f1 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Conditions/ContainerConditionType.cs @@ -0,0 +1,21 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The kind of a node in a parsed @container condition containing a style() query. + /// + internal enum ContainerConditionType : byte + { + /// A size feature such as (min-width: 400px); holds its text. + Size, + /// A style( ... ) query; the single operand is the query inside the function. + Style, + /// A style feature --name: value (or the boolean form --name, whose value is null). + Declaration, + /// Operands joined by and. + And, + /// Operands joined by or. + Or, + /// A not negation of the single operand. + Not + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Conditions/DeclarationCondition.cs b/Source/HtmlRenderer/Core/CssEngine/Conditions/DeclarationCondition.cs index 85e4c74e3..35dba9e9b 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Conditions/DeclarationCondition.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Conditions/DeclarationCondition.cs @@ -13,10 +13,6 @@ public DeclarationCondition(Property property, TokenValue tokenValue) _tokenValue = tokenValue; } - /// - /// CSS Conditional 3 §6.1. A value the parser accepts counts as supported, even where layout - /// ignores it. - /// public bool Check() { return _property is UnknownProperty == false && diff --git a/Source/HtmlRenderer/Core/CssEngine/Conditions/IContainerCondition.cs b/Source/HtmlRenderer/Core/CssEngine/Conditions/IContainerCondition.cs new file mode 100644 index 000000000..78275df9d --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Conditions/IContainerCondition.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// A node of the parsed condition of an @container rule that uses style() queries + /// (CSS Containment 3), possibly mixed with size features. The tree is data only; ExCSS does not evaluate it. + /// + internal interface IContainerCondition : IStyleFormattable + { + ContainerConditionType Type { get; } + + /// The operands of an And/Or (two or more), Not or Style (exactly one) node; empty for leaves. + IReadOnlyList Operands { get; } + + /// The property or custom property name of a Declaration node; otherwise null. + string Property { get; } + + /// + /// For a Declaration node the raw value text (null for the boolean form style(--flag)); + /// for a Size node the text of the feature including its parentheses; otherwise null. + /// + string Value { get; } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ContainGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/ContainGrammar.cs new file mode 100644 index 000000000..305a1323a --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ContainGrammar.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The grammars of CSS Containment 2 §2 (contain) and CSS Sizing 4 §5.1 + /// (contain-intrinsic-*). The authored text is preserved by the converters that call these. + /// + internal static class ContainGrammar + { + private const int Size = 1; + private const int Layout = 2; + private const int Style = 4; + private const int Paint = 8; + private const int InlineSize = 16; + + /// + /// none | strict | content | [ [ size | inline-size ] || layout || style || paint ]. + /// none, strict and content stand alone; the other keywords come in any order, + /// each at most once, and size and inline-size not together. + /// + internal static bool TryParseContain(IEnumerable value) + { + var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (tokens.Length == 0 || tokens.Any(t => t.Type != TokenType.Ident)) return false; + + if (tokens.Length == 1 && + (tokens[0].Data.Isi(Keywords.None) || tokens[0].Data.Isi("strict") || tokens[0].Data.Isi("content"))) + { + return true; + } + + var seen = 0; + + foreach (var token in tokens) + { + var kind = KindOf(token.Data); + + if (kind == 0 || (seen & kind) != 0) return false; + + seen |= kind; + } + + return (seen & (Size | InlineSize)) != (Size | InlineSize); + } + + private static int KindOf(string keyword) => + keyword.Isi("size") ? Size + : keyword.Isi("inline-size") ? InlineSize + : keyword.Isi("layout") ? Layout + : keyword.Isi("style") ? Style + : keyword.Isi("paint") ? Paint + : 0; + + /// auto? [ none | <length [0,∞]> ] over the tokens of one value. + internal static bool TryParseIntrinsic(IEnumerable value) + { + var tokens = new List(value.Where(t => t.Type != TokenType.Whitespace)); + + if (tokens.Count > 0 && tokens[0].Type == TokenType.Ident && tokens[0].Data.Isi(Keywords.Auto)) + { + tokens.RemoveAt(0); + } + + if (tokens.Count != 1) return false; + + var token = tokens[0]; + + if (token.Type == TokenType.Ident) return token.Data.Isi(Keywords.None); + + return IsNonNegativeLength(token); + } + + /// A <length> (a bare 0 or a calc() included) that is not negative. + internal static bool IsNonNegativeLength(Token token) + { + if (token is UnitToken { Value: < 0f }) return false; + + return Converters.LengthConverter.Convert(new[] { token }) != null; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/CssProperty.cs b/Source/HtmlRenderer/Core/CssEngine/CssProperty.cs deleted file mode 100644 index c47b38093..000000000 --- a/Source/HtmlRenderer/Core/CssEngine/CssProperty.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace TheArtOfDev.HtmlRenderer.Core.CssEngine -{ - /// The five CSS-wide keywords (CSS Cascade & Inheritance §7.3) a property value can be. - internal enum CssGlobalKeyword - { - Inherit, - Initial, - Unset, - Revert, - RevertLayer - } - - /// - /// A typed box-side property value that carries the parsed value through the cascade to the box, - /// so a layout consumer reads the object rather than re-parsing the authored string. A value is in exactly - /// one of three states: - /// - /// a CSS-wide keyword ( non-null), - /// unresolved — the authored text still contains var() and has not been resolved yet - /// (; is default), or - /// resolved — holds the parsed . - /// - /// The authored/keyword text is retained so the string-based getter/snapshot/revert cascade path keeps - /// working (). Grid templates (T = GridTemplate) are the first adopter; the - /// mechanism is intended to eventually back every box property. - /// - internal sealed class CssProperty - { - private readonly string _cssText; - - private CssProperty(CssGlobalKeyword? global, bool unresolved, T value, string cssText) - { - GlobalValue = global; - IsUnresolved = unresolved; - Value = value; - _cssText = cssText; - } - - /// The CSS-wide keyword, when this value is one; otherwise null. - public CssGlobalKeyword? GlobalValue { get; } - - public bool IsGlobalValue => GlobalValue.HasValue; - - /// The authored text still contains an unresolved var() — is not - /// meaningful until resolution replaces this with a resolved value. - public bool IsUnresolved { get; } - - /// The parsed value — meaningful only when the value is neither global nor unresolved. - public T Value { get; } - - public static CssProperty Global(CssGlobalKeyword keyword) => - new CssProperty(keyword, unresolved: false, value: default(T), CssGlobalKeywords.ToText(keyword)); - - public static CssProperty Unresolved(string rawText) => - new CssProperty(global: null, unresolved: true, value: default(T), rawText); - - public static CssProperty FromValue(string cssText, T value) => - new CssProperty(global: null, unresolved: false, value, cssText); - - public override string ToString() => _cssText; - } - - /// Maps between the CSS-wide keyword string literals and . - internal static class CssGlobalKeywords - { - public static bool TryParse(string value, out CssGlobalKeyword keyword) - { - if (value.Isi(Keywords.Inherit)) { keyword = CssGlobalKeyword.Inherit; return true; } - if (value.Isi(Keywords.Initial)) { keyword = CssGlobalKeyword.Initial; return true; } - if (value.Isi(Keywords.Unset)) { keyword = CssGlobalKeyword.Unset; return true; } - if (value.Isi(Keywords.Revert)) { keyword = CssGlobalKeyword.Revert; return true; } - if (value.Isi(Keywords.RevertLayer)) { keyword = CssGlobalKeyword.RevertLayer; return true; } - keyword = default; - return false; - } - - public static string ToText(CssGlobalKeyword keyword) - { - switch (keyword) - { - case CssGlobalKeyword.Inherit: return Keywords.Inherit; - case CssGlobalKeyword.Initial: return Keywords.Initial; - case CssGlobalKeyword.Unset: return Keywords.Unset; - case CssGlobalKeyword.Revert: return Keywords.Revert; - case CssGlobalKeyword.RevertLayer: return Keywords.RevertLayer; - default: return Keywords.Initial; - } - } - } -} diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/AttrCaseSensitivity.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/AttrCaseSensitivity.cs new file mode 100644 index 000000000..79c3f48a2 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/AttrCaseSensitivity.cs @@ -0,0 +1,18 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The case-sensitivity flag of an attribute selector value (Selectors 4): + /// [attr=value i] and [attr=value s]. + /// + internal enum AttrCaseSensitivity + { + /// No flag; the document language decides how the value is compared. + Default, + + /// The i flag: the value is compared ASCII case-insensitively. + Insensitive, + + /// The s flag: the value is compared case-sensitively. + Sensitive + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/BreakMode.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/BreakMode.cs index dbd7ca9f4..6508e4565 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/BreakMode.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/BreakMode.cs @@ -11,6 +11,8 @@ internal enum BreakMode : byte Column, AvoidPage, AvoidColumn, - AvoidRegion + AvoidRegion, + Recto, + Verso } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/ClearMode.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/ClearMode.cs index b19a8307a..28bcc58bf 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/ClearMode.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/ClearMode.cs @@ -5,6 +5,8 @@ internal enum ClearMode : byte None, Left, Right, - Both + Both, + InlineStart, + InlineEnd } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Colors.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Colors.cs index 5c1ccdeb7..870a28824 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Colors.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Colors.cs @@ -7,7 +7,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal static class Colors { private static readonly Dictionary NamedColors = - new Dictionary(StringComparer.OrdinalIgnoreCase) + new (StringComparer.OrdinalIgnoreCase) { {"aliceblue", new Color(240, 248, 255)}, {"antiquewhite", new Color(250, 235, 215)}, diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Combinators.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Combinators.cs index e023138ad..45e491d6e 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Combinators.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Combinators.cs @@ -2,22 +2,19 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal static class Combinators { - // const (not static readonly) so these can be used as constant patterns in switch statements - // (e.g. AttributeSelectorFactory's reflection-free dispatch). String literals are interned, so - // reference/value equality behaviour is unchanged. - public const string Exactly = "="; - public const string Unlike = "!="; - public const string InList = "~="; - public const string InToken = "|="; - public const string Begins = "^="; - public const string Ends = "$="; - public const string InText = "*="; - public const string Column = "||"; - public const string Pipe = "|"; - public const string Adjacent = "+"; - public const string Descendent = " "; - public const string Deep = ">>>"; - public const string Child = ">"; - public const string Sibling = "~"; + public static readonly string Exactly = "="; + public static readonly string Unlike = "!="; + public static readonly string InList = "~="; + public static readonly string InToken = "|="; + public static readonly string Begins = "^="; + public static readonly string Ends = "$="; + public static readonly string InText = "*="; + public static readonly string Column = "||"; + public static readonly string Pipe = "|"; + public static readonly string Adjacent = "+"; + public static readonly string Descendent = " "; + public static readonly string Deep = ">>>"; + public static readonly string Child = ">"; + public static readonly string Sibling = "~"; } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/DisplayMode.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/DisplayMode.cs index 5f2739ce8..a1ec8f55c 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/DisplayMode.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/DisplayMode.cs @@ -20,6 +20,8 @@ internal enum DisplayMode : byte Flex, InlineFlex, Grid, - InlineGrid + InlineGrid, + Contents, + FlowRoot } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/FeatureNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/FeatureNames.cs index 69e71400f..905e3ff8c 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/FeatureNames.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/FeatureNames.cs @@ -51,5 +51,12 @@ internal static class FeatureNames public static readonly string Update = "update"; public static readonly string AnyHover = "any-hover"; public static readonly string AnyPointer = "any-pointer"; + public static readonly string ForcedColors = "forced-colors"; + public static readonly string PrefersReducedData = "prefers-reduced-data"; + public static readonly string InvertedColors = "inverted-colors"; + public static readonly string DynamicRange = "dynamic-range"; + public static readonly string VideoDynamicRange = "video-dynamic-range"; + public static readonly string OverflowBlock = "overflow-block"; + public static readonly string OverflowInline = "overflow-inline"; } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Floating.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Floating.cs index 6ae2fe94b..05fa91470 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Floating.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Floating.cs @@ -4,6 +4,8 @@ internal enum Floating : byte { None, Left, - Right + Right, + InlineStart, + InlineEnd } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/FunctionNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/FunctionNames.cs index 94c802298..a7468d429 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/FunctionNames.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/FunctionNames.cs @@ -23,14 +23,27 @@ internal static class FunctionNames public static readonly string RadialGradient = "radial-gradient"; public static readonly string RepeatingLinearGradient = "repeating-linear-gradient"; public static readonly string RepeatingRadialGradient = "repeating-radial-gradient"; + public static readonly string ConicGradient = "conic-gradient"; + public static readonly string RepeatingConicGradient = "repeating-conic-gradient"; public static readonly string Image = "image"; + public static readonly string ImageSet = "image-set"; + public static readonly string CrossFade = "cross-fade"; + public static readonly string Element = "element"; public static readonly string Counter = "counter"; public static readonly string Counters = "counters"; public static readonly string Content = "content"; + public static readonly string StringFn = "string"; + public static readonly string TargetCounter = "target-counter"; + public static readonly string TargetCounters = "target-counters"; + public static readonly string TargetText = "target-text"; + public static readonly string Leader = "leader"; + public static readonly string Running = "running"; public static readonly string Calc = "calc"; + public static readonly string Style = "style"; public static readonly string Min = "min"; public static readonly string Max = "max"; public static readonly string Clamp = "clamp"; + public static readonly string Var = "var"; public static readonly string Toggle = "toggle"; public static readonly string Translate = "translate"; public static readonly string TranslateX = "translateX"; @@ -54,6 +67,7 @@ internal static class FunctionNames public static readonly string ScaleZ = "scaleZ"; public static readonly string Steps = "steps"; public static readonly string CubicBezier = "cubic-bezier"; + public static readonly string Linear = "linear"; public static readonly string Perspective = "perspective"; public static readonly string Gray = "gray"; public static readonly string Hwb = "hwb"; @@ -62,11 +76,8 @@ internal static class FunctionNames public static readonly string Lch = "lch"; public static readonly string Oklch = "oklch"; public static readonly string ColorMix = "color-mix"; - public static readonly string ConicGradient = "conic-gradient"; - public static readonly string RepeatingConicGradient = "repeating-conic-gradient"; - public static readonly string Var = "var"; - public static readonly string ImageSet = "image-set"; - public static readonly string CrossFade = "cross-fade"; - public static readonly string Element = "element"; + public static readonly string ColorFunction = "color"; + public static readonly string LightDark = "light-dark"; + public static readonly string DeviceCmyk = "device-cmyk"; } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/HorizontalAlignment.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/HorizontalAlignment.cs index ad5286781..47ca9871d 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/HorizontalAlignment.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/HorizontalAlignment.cs @@ -5,6 +5,9 @@ internal enum HorizontalAlignment : byte Left, Center, Right, - Justify + Justify, + Start, + End, + MatchParent } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Hyphens.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Hyphens.cs new file mode 100644 index 000000000..6c1f67174 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Hyphens.cs @@ -0,0 +1,9 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal enum Hyphens : byte + { + None, + Manual, + Auto + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs index 232ba506e..8ce50190f 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs @@ -5,9 +5,9 @@ internal static class Keywords public static readonly string Important = "important"; public static readonly string Inherit = "inherit"; public static readonly string Initial = "initial"; + public static readonly string Manual = "manual"; public static readonly string None = "none"; public static readonly string Auto = "auto"; - public static readonly string Manual = "manual"; public static readonly string From = "from"; public static readonly string To = "to"; public static readonly string Always = "always"; @@ -22,6 +22,8 @@ internal static class Keywords public static readonly string AvoidPage = "avoid-page"; public static readonly string AvoidColumn = "avoid-column"; public static readonly string AvoidRegion = "avoid-region"; + public static readonly string Recto = "recto"; + public static readonly string Verso = "verso"; public static readonly string Inset = "inset"; public static readonly string Outset = "outset"; public static readonly string Ridge = "ridge"; @@ -29,6 +31,8 @@ internal static class Keywords public static readonly string Fixed = "fixed"; public static readonly string Local = "local"; public static readonly string Scroll = "scroll"; + public static readonly string Clip = "clip"; + public static readonly string Ellipsis = "ellipsis"; public static readonly string NoRepeat = "no-repeat"; public static readonly string Repeat = "repeat"; public static readonly string RepeatX = "repeat-x"; @@ -52,6 +56,8 @@ internal static class Keywords public static readonly string TableRowGroup = "table-row-group"; public static readonly string Flex = "flex"; public static readonly string InlineFlex = "inline-flex"; + public static readonly string Contents = "contents"; + public static readonly string FlowRoot = "flow-root"; public static readonly string Grid = "grid"; public static readonly string InlineGrid = "inline-grid"; public static readonly string Visible = "visible"; @@ -134,6 +140,7 @@ internal static class Keywords public static readonly string Cell = "cell"; public static readonly string Crosshair = "crosshair"; public static readonly string Text = "text"; + public static readonly string BorderArea = "border-area"; public static readonly string VerticalText = "vertical-text"; public static readonly string Alias = "alias"; public static readonly string Copy = "copy"; @@ -177,6 +184,10 @@ internal static class Keywords public static readonly string Linear = "linear"; public static readonly string StepStart = "step-start"; public static readonly string StepEnd = "step-end"; + public static readonly string JumpStart = "jump-start"; + public static readonly string JumpEnd = "jump-end"; + public static readonly string JumpNone = "jump-none"; + public static readonly string JumpBoth = "jump-both"; public static readonly string Disc = "disc"; public static readonly string Circle = "circle"; public static readonly string Square = "square"; @@ -290,6 +301,10 @@ internal static class Keywords public static readonly string BreakAll = "break-all"; public static readonly string KeepAll = "keep-all"; public static readonly string BreakWord = "break-word"; + public static readonly string Anywhere = "anywhere"; + public static readonly string MatchParent = "match-parent"; + public static readonly string InlineStart = "inline-start"; + public static readonly string InlineEnd = "inline-end"; public static readonly string Nonzero = "nonzero"; public static readonly string Evenodd = "evenodd"; public static readonly string Row = "row"; @@ -305,6 +320,7 @@ internal static class Keywords public static readonly string AutoFlow = "auto-flow"; public static readonly string Subgrid = "subgrid"; public static readonly string Dense = "dense"; + public static readonly string Span = "span"; public static readonly string Content = "content"; public static readonly string Revert = "revert"; public static readonly string RevertLayer = "revert-layer"; @@ -319,8 +335,12 @@ internal static class Keywords public static readonly string SelfEnd = "self-end"; public static readonly string Size = "size"; public static readonly string InlineSize = "inline-size"; - - // The `span` keyword of a grid-placement value (CSS Grid 1 §8.3). - public static readonly string Span = "span"; + public static readonly string NoPreference = "no-preference"; + public static readonly string Reduce = "reduce"; + public static readonly string Active = "active"; + public static readonly string Inverted = "inverted"; + public static readonly string Standard = "standard"; + public static readonly string High = "high"; + public static readonly string Paged = "paged"; } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/LineBreak.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/LineBreak.cs new file mode 100644 index 000000000..f6b3241d2 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/LineBreak.cs @@ -0,0 +1,11 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal enum LineBreak : byte + { + Auto, + Loose, + Normal, + Strict, + Anywhere + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/MediaFeatureComparison.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/MediaFeatureComparison.cs new file mode 100644 index 000000000..a24afef12 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/MediaFeatureComparison.cs @@ -0,0 +1,11 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal enum MediaFeatureComparison : byte + { + Equal, + LessThan, + LessThanOrEqual, + GreaterThan, + GreaterThanOrEqual + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Overflow.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Overflow.cs index d04300cd4..62e2db540 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Overflow.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Overflow.cs @@ -5,6 +5,7 @@ internal enum Overflow : byte Auto, Visible, Hidden, - Scroll + Scroll, + Clip } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/OverflowWrap.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/OverflowWrap.cs index 921c52c04..5e1580e7e 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/OverflowWrap.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/OverflowWrap.cs @@ -3,6 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal enum OverflowWrap : byte { Normal, - BreakWord + BreakWord, + Anywhere } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs index c8ffd137f..9e030f397 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs @@ -2,12 +2,15 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal static class PropertyNames { + public static readonly string AccentColor = "accent-color"; + public static readonly string All = "all"; public static readonly string AlignContent = "align-content"; public static readonly string AlignItems = "align-items"; public static readonly string AlignBaseline = "alignment-baseline"; public static readonly string AlignSelf = "align-self"; public static readonly string Accelerator = "accelerator"; public static readonly string Animation = "animation"; + public static readonly string AnimationComposition = "animation-composition"; public static readonly string AnimationDelay = "animation-delay"; public static readonly string AnimationDirection = "animation-direction"; public static readonly string AnimationDuration = "animation-duration"; @@ -15,8 +18,14 @@ internal static class PropertyNames public static readonly string AnimationIterationCount = "animation-iteration-count"; public static readonly string AnimationName = "animation-name"; public static readonly string AnimationPlayState = "animation-play-state"; + public static readonly string AnimationRange = "animation-range"; + public static readonly string AnimationRangeEnd = "animation-range-end"; + public static readonly string AnimationRangeStart = "animation-range-start"; + public static readonly string AnimationTimeline = "animation-timeline"; public static readonly string AnimationTimingFunction = "animation-timing-function"; + public static readonly string BackdropFilter = "backdrop-filter"; public static readonly string BackgroundAttachment = "background-attachment"; + public static readonly string BackgroundBlendMode = "background-blend-mode"; public static readonly string BackgroundColor = "background-color"; public static readonly string BackgroundClip = "background-clip"; public static readonly string BackgroundOrigin = "background-origin"; @@ -29,6 +38,12 @@ internal static class PropertyNames public static readonly string Background = "background"; public static readonly string Behavior = "behavior"; public static readonly string BaselineShift = "baseline-shift"; + public static readonly string BlockEllipsis = "block-ellipsis"; + public static readonly string BlockStep = "block-step"; + public static readonly string BlockStepAlign = "block-step-align"; + public static readonly string BlockStepInsert = "block-step-insert"; + public static readonly string BlockStepRound = "block-step-round"; + public static readonly string BlockStepSize = "block-step-size"; public static readonly string BorderColor = "border-color"; public static readonly string BorderSpacing = "border-spacing"; public static readonly string BorderCollapse = "border-collapse"; @@ -38,6 +53,18 @@ internal static class PropertyNames public static readonly string BorderTopRightRadius = "border-top-right-radius"; public static readonly string BorderBottomLeftRadius = "border-bottom-left-radius"; public static readonly string BorderBottomRightRadius = "border-bottom-right-radius"; + public static readonly string BorderTopRadius = "border-top-radius"; + public static readonly string BorderRightRadius = "border-right-radius"; + public static readonly string BorderBottomRadius = "border-bottom-radius"; + public static readonly string BorderLeftRadius = "border-left-radius"; + public static readonly string BorderBlockStartRadius = "border-block-start-radius"; + public static readonly string BorderBlockEndRadius = "border-block-end-radius"; + public static readonly string BorderInlineStartRadius = "border-inline-start-radius"; + public static readonly string BorderInlineEndRadius = "border-inline-end-radius"; + public static readonly string BorderStartStartRadius = "border-start-start-radius"; + public static readonly string BorderStartEndRadius = "border-start-end-radius"; + public static readonly string BorderEndStartRadius = "border-end-start-radius"; + public static readonly string BorderEndEndRadius = "border-end-end-radius"; public static readonly string BoxShadow = "box-shadow"; public static readonly string BoxSizing = "box-sizing"; public static readonly string AspectRatio = "aspect-ratio"; @@ -68,9 +95,47 @@ internal static class PropertyNames public static readonly string BorderLeftWidth = "border-left-width"; public static readonly string BorderRightWidth = "border-right-width"; public static readonly string BorderBottomWidth = "border-bottom-width"; + public static readonly string BorderBlockStartWidth = "border-block-start-width"; + public static readonly string BorderBlockStartStyle = "border-block-start-style"; + public static readonly string BorderBlockStartColor = "border-block-start-color"; + public static readonly string BorderBlockEndWidth = "border-block-end-width"; + public static readonly string BorderBlockEndStyle = "border-block-end-style"; + public static readonly string BorderBlockEndColor = "border-block-end-color"; + public static readonly string BorderInlineStartWidth = "border-inline-start-width"; + public static readonly string BorderInlineStartStyle = "border-inline-start-style"; + public static readonly string BorderInlineStartColor = "border-inline-start-color"; + public static readonly string BorderInlineEndWidth = "border-inline-end-width"; + public static readonly string BorderInlineEndStyle = "border-inline-end-style"; + public static readonly string BorderInlineEndColor = "border-inline-end-color"; + public static readonly string BorderBlockWidth = "border-block-width"; + public static readonly string BorderBlockStyle = "border-block-style"; + public static readonly string BorderBlockColor = "border-block-color"; + public static readonly string BorderInlineWidth = "border-inline-width"; + public static readonly string BorderInlineStyle = "border-inline-style"; + public static readonly string BorderInlineColor = "border-inline-color"; + public static readonly string BorderBlock = "border-block"; + public static readonly string BorderBlockStart = "border-block-start"; + public static readonly string BorderBlockEnd = "border-block-end"; + public static readonly string BorderInline = "border-inline"; + public static readonly string BorderInlineStart = "border-inline-start"; + public static readonly string BorderInlineEnd = "border-inline-end"; public static readonly string BorderWidth = "border-width"; + public static readonly string Bleed = "bleed"; + public static readonly string BookmarkLabel = "bookmark-label"; + public static readonly string BookmarkLevel = "bookmark-level"; + public static readonly string BookmarkState = "bookmark-state"; + public static readonly string BookmarkTarget = "bookmark-target"; public static readonly string Border = "border"; public static readonly string Bottom = "bottom"; + public static readonly string BufferedRendering = "buffered-rendering"; + public static readonly string CaretColor = "caret-color"; + public static readonly string InsetBlockStart = "inset-block-start"; + public static readonly string InsetBlockEnd = "inset-block-end"; + public static readonly string InsetInlineStart = "inset-inline-start"; + public static readonly string InsetInlineEnd = "inset-inline-end"; + public static readonly string InsetBlock = "inset-block"; + public static readonly string InsetInline = "inset-inline"; + public static readonly string Inset = "inset"; public static readonly string Columns = "columns"; public static readonly string ColumnCount = "column-count"; public static readonly string ColumnFill = "column-fill"; @@ -92,12 +157,74 @@ internal static class PropertyNames public static readonly string ClipRule = "clip-rule"; public static readonly string Color = "color"; public static readonly string ColorInterpolationFilters = "color-interpolation-filters"; - public static readonly string ContainerName = "container-name"; + public static readonly string ColorScheme = "color-scheme"; + public static readonly string ColorInterpolation = "color-interpolation"; + public static readonly string ContainerName= "container-name"; public static readonly string ContainerType = "container-type"; public static readonly string Content = "content"; + public static readonly string FloatReference = "float-reference"; + public static readonly string FootnoteDisplay = "footnote-display"; + public static readonly string FootnotePolicy = "footnote-policy"; + public static readonly string ForcedColorAdjust = "forced-color-adjust"; + public static readonly string ImageRendering = "image-rendering"; + public static readonly string Interactivity = "interactivity"; + public static readonly string Isolation = "isolation"; + public static readonly string MathDepth = "math-depth"; + public static readonly string MathShift = "math-shift"; + public static readonly string MathStyle = "math-style"; + public static readonly string MixBlendMode = "mix-blend-mode"; + public static readonly string PrintColorAdjust = "print-color-adjust"; + public static readonly string ShapeRendering = "shape-rendering"; + public static readonly string Contain = "contain"; + public static readonly string ContainIntrinsicWidth = "contain-intrinsic-width"; + public static readonly string ContainIntrinsicHeight = "contain-intrinsic-height"; + public static readonly string ContainIntrinsicInlineSize = "contain-intrinsic-inline-size"; + public static readonly string ContainIntrinsicBlockSize = "contain-intrinsic-block-size"; + public static readonly string ContainIntrinsicSize = "contain-intrinsic-size"; + public static readonly string Container = "container"; + public static readonly string ContentVisibility = "content-visibility"; + public static readonly string CornerTopLeftShape = "corner-top-left-shape"; + public static readonly string CornerTopRightShape = "corner-top-right-shape"; + public static readonly string CornerBottomRightShape = "corner-bottom-right-shape"; + public static readonly string CornerBottomLeftShape = "corner-bottom-left-shape"; + public static readonly string CornerStartStartShape = "corner-start-start-shape"; + public static readonly string CornerStartEndShape = "corner-start-end-shape"; + public static readonly string CornerEndStartShape = "corner-end-start-shape"; + public static readonly string CornerEndEndShape = "corner-end-end-shape"; + public static readonly string CornerShape = "corner-shape"; + public static readonly string Corner = "corner"; + public static readonly string CornerTopShape = "corner-top-shape"; + public static readonly string CornerTop = "corner-top"; + public static readonly string CornerRightShape = "corner-right-shape"; + public static readonly string CornerRight = "corner-right"; + public static readonly string CornerBottomShape = "corner-bottom-shape"; + public static readonly string CornerBottom = "corner-bottom"; + public static readonly string CornerLeftShape = "corner-left-shape"; + public static readonly string CornerLeft = "corner-left"; + public static readonly string CornerBlockStartShape = "corner-block-start-shape"; + public static readonly string CornerBlockStart = "corner-block-start"; + public static readonly string CornerBlockEndShape = "corner-block-end-shape"; + public static readonly string CornerBlockEnd = "corner-block-end"; + public static readonly string CornerInlineStartShape = "corner-inline-start-shape"; + public static readonly string CornerInlineStart = "corner-inline-start"; + public static readonly string CornerInlineEndShape = "corner-inline-end-shape"; + public static readonly string CornerInlineEnd = "corner-inline-end"; + public static readonly string CornerTopLeft = "corner-top-left"; + public static readonly string CornerTopRight = "corner-top-right"; + public static readonly string CornerBottomRight = "corner-bottom-right"; + public static readonly string CornerBottomLeft = "corner-bottom-left"; + public static readonly string CornerStartStart = "corner-start-start"; + public static readonly string CornerStartEnd = "corner-start-end"; + public static readonly string CornerEndStart = "corner-end-start"; + public static readonly string CornerEndEnd = "corner-end-end"; + public static readonly string StringSet = "string-set"; public static readonly string CounterIncrement = "counter-increment"; public static readonly string CounterReset = "counter-reset"; + public static readonly string CounterSet = "counter-set"; public static readonly string Cursor = "cursor"; + public static readonly string Cx = "cx"; + public static readonly string Cy = "cy"; + public static readonly string D = "d"; public static readonly string Direction = "direction"; public static readonly string Display = "display"; public static readonly string DominantBaseline = "dominant-baseline"; @@ -114,18 +241,37 @@ internal static class PropertyNames public static readonly string FlexGrow = "flex-grow"; public static readonly string FlexShrink = "flex-shrink"; public static readonly string FlexWrap = "flex-wrap"; + public static readonly string FloodColor = "flood-color"; + public static readonly string FloodOpacity = "flood-opacity"; public static readonly string Float = "float"; public static readonly string FontFamily = "font-family"; public static readonly string FontFeatureSettings = "font-feature-settings"; + public static readonly string FontKerning = "font-kerning"; + public static readonly string FontLanguageOverride = "font-language-override"; + public static readonly string FontOpticalSizing = "font-optical-sizing"; public static readonly string FontPalette = "font-palette"; public static readonly string FontSize = "font-size"; public static readonly string FontSizeAdjust = "font-size-adjust"; public static readonly string FontStyle = "font-style"; public static readonly string FontStretch = "font-stretch"; + public static readonly string FontSynthesis = "font-synthesis"; + public static readonly string FontSynthesisPosition = "font-synthesis-position"; + public static readonly string FontSynthesisSmallCaps = "font-synthesis-small-caps"; + public static readonly string FontSynthesisStyle = "font-synthesis-style"; + public static readonly string FontSynthesisWeight = "font-synthesis-weight"; public static readonly string FontVariant = "font-variant"; + public static readonly string FontVariantAlternates = "font-variant-alternates"; + public static readonly string FontVariantCaps = "font-variant-caps"; + public static readonly string FontVariantEastAsian = "font-variant-east-asian"; + public static readonly string FontVariantEmoji = "font-variant-emoji"; + public static readonly string FontVariantLigatures = "font-variant-ligatures"; + public static readonly string FontVariantNumeric = "font-variant-numeric"; + public static readonly string FontVariantPosition = "font-variant-position"; + public static readonly string FontVariationSettings = "font-variation-settings"; public static readonly string FontWeight = "font-weight"; public static readonly string Font = "font"; public static readonly string Gap = "gap"; + public static readonly string InlineSize = "inline-size"; public static readonly string Grid = "grid"; public static readonly string GridTemplate = "grid-template"; public static readonly string GridTemplateColumns = "grid-template-columns"; @@ -141,17 +287,26 @@ internal static class PropertyNames public static readonly string GridRowStart = "grid-row-start"; public static readonly string GridRowEnd = "grid-row-end"; public static readonly string GridArea = "grid-area"; - public static readonly string JustifyItems = "justify-items"; - public static readonly string JustifySelf = "justify-self"; - public static readonly string PlaceItems = "place-items"; - public static readonly string PlaceContent = "place-content"; - public static readonly string PlaceSelf = "place-self"; public static readonly string GlyphOrientationHorizontal = "glyph-orientation-horizontal"; public static readonly string GlyphOrientationVertical = "glyph-orientation-vertical"; + public static readonly string HangingPunctuation = "hanging-punctuation"; public static readonly string Height = "height"; + public static readonly string HyphenateCharacter = "hyphenate-character"; + public static readonly string HyphenateLimitChars = "hyphenate-limit-chars"; + public static readonly string HyphenateLimitLast = "hyphenate-limit-last"; + public static readonly string HyphenateLimitLines = "hyphenate-limit-lines"; + public static readonly string HyphenateLimitZone = "hyphenate-limit-zone"; public static readonly string Hyphens = "hyphens"; + public static readonly string ImageOrientation = "image-orientation"; + public static readonly string ImageResolution = "image-resolution"; public static readonly string ImeMode = "ime-mode"; + public static readonly string InitialLetter = "initial-letter"; public static readonly string JustifyContent = "justify-content"; + public static readonly string JustifyItems = "justify-items"; + public static readonly string JustifySelf = "justify-self"; + public static readonly string PlaceItems = "place-items"; + public static readonly string PlaceContent = "place-content"; + public static readonly string PlaceSelf = "place-self"; public static readonly string LayoutGrid = "layout-grid"; public static readonly string LayoutGridChar = "layout-grid-char"; public static readonly string LayoutGridType = "layout-grid-type"; @@ -159,7 +314,11 @@ internal static class PropertyNames public static readonly string LayoutGridLine = "layout-grid-line"; public static readonly string Left = "left"; public static readonly string LetterSpacing = "letter-spacing"; + public static readonly string LineBreak = "line-break"; + public static readonly string LightingColor = "lighting-color"; public static readonly string LineHeight = "line-height"; + public static readonly string LineClamp = "line-clamp"; + public static readonly string LineHeightStep = "line-height-step"; public static readonly string ListStyleImage = "list-style-image"; public static readonly string ListStylePosition = "list-style-position"; public static readonly string ListStyleType = "list-style-type"; @@ -168,12 +327,27 @@ internal static class PropertyNames public static readonly string MarginLeft = "margin-left"; public static readonly string MarginTop = "margin-top"; public static readonly string MarginBottom = "margin-bottom"; + public static readonly string MarginBlockStart = "margin-block-start"; + public static readonly string MarginBlockEnd = "margin-block-end"; + public static readonly string MarginInlineStart = "margin-inline-start"; + public static readonly string MarginInlineEnd = "margin-inline-end"; + public static readonly string MarginBlock = "margin-block"; + public static readonly string MarginInline = "margin-inline"; public static readonly string Margin = "margin"; + public static readonly string Marks = "marks"; public static readonly string Marker = "marker"; public static readonly string MarkerEnd = "marker-end"; public static readonly string MarkerMid = "marker-mid"; public static readonly string MarkerStart = "marker-start"; public static readonly string Mask = "mask"; + public static readonly string MaskClip = "mask-clip"; + public static readonly string MaskComposite = "mask-composite"; + public static readonly string MaskImage = "mask-image"; + public static readonly string MaskMode = "mask-mode"; + public static readonly string MaskOrigin = "mask-origin"; + public static readonly string MaskPosition = "mask-position"; + public static readonly string MaskRepeat = "mask-repeat"; + public static readonly string MaskSize = "mask-size"; public static readonly string MaxHeight = "max-height"; public static readonly string MaxWidth = "max-width"; public static readonly string MinHeight = "min-height"; @@ -182,32 +356,76 @@ internal static class PropertyNames public static readonly string Order = "order"; public static readonly string Orphans = "orphans"; public static readonly string OutlineColor = "outline-color"; + public static readonly string OutlineOffset = "outline-offset"; public static readonly string OutlineStyle = "outline-style"; public static readonly string OutlineWidth = "outline-width"; public static readonly string Outline = "outline"; public static readonly string Overflow = "overflow"; public static readonly string OverflowX = "overflow-x"; public static readonly string OverflowY = "overflow-y"; + public static readonly string OverflowClipMargin = "overflow-clip-margin"; + public static readonly string OverflowClipMarginTop = "overflow-clip-margin-top"; + public static readonly string OverflowClipMarginRight = "overflow-clip-margin-right"; + public static readonly string OverflowClipMarginBottom = "overflow-clip-margin-bottom"; + public static readonly string OverflowClipMarginLeft = "overflow-clip-margin-left"; + public static readonly string OverflowClipMarginBlock = "overflow-clip-margin-block"; + public static readonly string OverflowClipMarginBlockStart = "overflow-clip-margin-block-start"; + public static readonly string OverflowClipMarginBlockEnd = "overflow-clip-margin-block-end"; + public static readonly string OverflowClipMarginInline = "overflow-clip-margin-inline"; + public static readonly string OverflowClipMarginInlineStart = "overflow-clip-margin-inline-start"; + public static readonly string OverflowClipMarginInlineEnd = "overflow-clip-margin-inline-end"; public static readonly string OverflowWrap = "overflow-wrap"; public static readonly string PaddingTop = "padding-top"; public static readonly string PaddingRight = "padding-right"; public static readonly string PaddingLeft = "padding-left"; public static readonly string PaddingBottom = "padding-bottom"; + public static readonly string PaddingBlockStart = "padding-block-start"; + public static readonly string PaddingBlockEnd = "padding-block-end"; + public static readonly string PaddingInlineStart = "padding-inline-start"; + public static readonly string PaddingInlineEnd = "padding-inline-end"; + public static readonly string PaddingBlock = "padding-block"; + public static readonly string PaddingInline = "padding-inline"; public static readonly string Padding = "padding"; public static readonly string PageBreakAfter = "page-break-after"; public static readonly string PageBreakBefore = "page-break-before"; public static readonly string PageBreakInside = "page-break-inside"; - public static readonly string PageName = "page"; + public static readonly string PaintOrder = "paint-order"; public static readonly string Perspective = "perspective"; public static readonly string PerspectiveOrigin = "perspective-origin"; public static readonly string PointerEvents = "pointer-events"; public static readonly string Position = "position"; + public static readonly string OverscrollBehavior = "overscroll-behavior"; + public static readonly string OverscrollBehaviorX = "overscroll-behavior-x"; + public static readonly string OverscrollBehaviorY = "overscroll-behavior-y"; public static readonly string Quotes = "quotes"; + public static readonly string R = "r"; + public static readonly string Rx = "rx"; + public static readonly string Ry = "ry"; public static readonly string Right = "right"; public static readonly string RowGap = "row-gap"; public static readonly string RubyAlign = "ruby-align"; public static readonly string RubyOverhang = "ruby-overhang"; public static readonly string RubyPosition = "ruby-position"; + public static readonly string ScrollBehavior = "scroll-behavior"; + public static readonly string ScrollMargin = "scroll-margin"; + public static readonly string ScrollMarginTop = "scroll-margin-top"; + public static readonly string ScrollMarginRight = "scroll-margin-right"; + public static readonly string ScrollMarginBottom = "scroll-margin-bottom"; + public static readonly string ScrollMarginLeft = "scroll-margin-left"; + public static readonly string ScrollPadding = "scroll-padding"; + public static readonly string ScrollPaddingTop = "scroll-padding-top"; + public static readonly string ScrollPaddingRight = "scroll-padding-right"; + public static readonly string ScrollPaddingBottom = "scroll-padding-bottom"; + public static readonly string ScrollPaddingLeft = "scroll-padding-left"; + public static readonly string ScrollSnapAlign = "scroll-snap-align"; + public static readonly string ScrollSnapStop = "scroll-snap-stop"; + public static readonly string ScrollSnapType = "scroll-snap-type"; + public static readonly string ScrollTimeline = "scroll-timeline"; + public static readonly string ScrollTimelineAxis = "scroll-timeline-axis"; + public static readonly string ScrollTimelineName = "scroll-timeline-name"; + public static readonly string ShapeImageThreshold = "shape-image-threshold"; + public static readonly string ShapeMargin = "shape-margin"; + public static readonly string ShapeOutside = "shape-outside"; public static readonly string Scrollbar3dLightColor = "scrollbar3d-light-color"; public static readonly string ScrollbarArrowColor = "scrollbar-arrow-color"; public static readonly string ScrollbarDarkShadowColor = "scrollbar-dark-shadow-color"; @@ -215,6 +433,11 @@ internal static class PropertyNames public static readonly string ScrollbarHighlightColor = "scrollbar-highlight-color"; public static readonly string ScrollbarShadowColor = "scrollbar-shadow-color"; public static readonly string ScrollbarTrackColor = "scrollbar-track-color"; + public static readonly string ScrollbarColor = "scrollbar-color"; + public static readonly string ScrollbarGutter = "scrollbar-gutter"; + public static readonly string ScrollbarWidth = "scrollbar-width"; + public static readonly string StopColor = "stop-color"; + public static readonly string StopOpacity = "stop-opacity"; public static readonly string Stroke = "stroke"; public static readonly string StrokeDasharray = "stroke-dasharray"; public static readonly string StrokeDashoffset = "stroke-dashoffset"; @@ -223,103 +446,90 @@ internal static class PropertyNames public static readonly string StrokeMiterlimit = "stroke-miterlimit"; public static readonly string StrokeOpacity = "stroke-opacity"; public static readonly string StrokeWidth = "stroke-width"; - public static readonly string StringSet = "string-set"; + public static readonly string TabSize = "tab-size"; public static readonly string TableLayout = "table-layout"; public static readonly string TextAlign = "text-align"; + public static readonly string TextAlignAll = "text-align-all"; public static readonly string TextAlignLast = "text-align-last"; public static readonly string TextAnchor = "text-anchor"; public static readonly string TextAutospace = "text-autospace"; + public static readonly string TextCombineUpright = "text-combine-upright"; + public static readonly string TextBox = "text-box"; + public static readonly string TextBoxEdge = "text-box-edge"; + public static readonly string TextBoxTrim = "text-box-trim"; public static readonly string TextDecoration = "text-decoration"; public static readonly string TextDecorationColor = "text-decoration-color"; public static readonly string TextDecorationLine = "text-decoration-line"; + public static readonly string TextDecorationInset = "text-decoration-inset"; + public static readonly string TextDecorationSkip = "text-decoration-skip"; + public static readonly string TextDecorationSkipInk = "text-decoration-skip-ink"; public static readonly string TextDecorationStyle = "text-decoration-style"; + public static readonly string TextDecorationThickness = "text-decoration-thickness"; + public static readonly string TextEmphasis = "text-emphasis"; + public static readonly string TextEmphasisColor = "text-emphasis-color"; + public static readonly string TextEmphasisPosition = "text-emphasis-position"; + public static readonly string TextEmphasisStyle = "text-emphasis-style"; public static readonly string TextIndent = "text-indent"; public static readonly string TextJustify = "text-justify"; + public static readonly string TextOrientation = "text-orientation"; public static readonly string TextOverflow = "text-overflow"; + public static readonly string TextRendering = "text-rendering"; public static readonly string TextShadow = "text-shadow"; public static readonly string TextTransform = "text-transform"; + public static readonly string TextWrap = "text-wrap"; + public static readonly string TextWrapMode = "text-wrap-mode"; + public static readonly string TextWrapStyle = "text-wrap-style"; + public static readonly string TextUnderlineOffset = "text-underline-offset"; public static readonly string TextUnderlinePosition = "text-underline-position"; + public static readonly string TouchAction = "touch-action"; + public static readonly string TimelineScope = "timeline-scope"; public static readonly string Transform = "transform"; + public static readonly string TransformBox = "transform-box"; public static readonly string TransformOrigin = "transform-origin"; public static readonly string TransformStyle = "transform-style"; + public static readonly string Translate = "translate"; + public static readonly string Rotate = "rotate"; + public static readonly string Scale = "scale"; public static readonly string Transition = "transition"; + public static readonly string TransitionBehavior = "transition-behavior"; public static readonly string TransitionDelay = "transition-delay"; public static readonly string TransitionDuration = "transition-duration"; public static readonly string TransitionTimingFunction = "transition-timing-function"; public static readonly string TransitionProperty = "transition-property"; public static readonly string Top = "top"; + public static readonly string ViewTimeline = "view-timeline"; + public static readonly string ViewTimelineAxis = "view-timeline-axis"; + public static readonly string ViewTimelineInset = "view-timeline-inset"; + public static readonly string ViewTimelineName = "view-timeline-name"; public static readonly string UnicodeBidirectional = "unicode-bidi"; + public static readonly string UserSelect = "user-select"; + public static readonly string VectorEffect = "vector-effect"; public static readonly string VerticalAlign = "vertical-align"; public static readonly string Visibility = "visibility"; public static readonly string WhiteSpace = "white-space"; + public static readonly string WhiteSpaceCollapse = "white-space-collapse"; public static readonly string Widows = "widows"; public static readonly string Width = "width"; public static readonly string WordBreak = "word-break"; public static readonly string WordSpacing = "word-spacing"; public static readonly string WordWrap = "word-wrap"; public static readonly string WritingMode = "writing-mode"; + public static readonly string X = "x"; + public static readonly string Y = "y"; public static readonly string ZIndex = "z-index"; public static readonly string Zoom = "zoom"; public static readonly string UnicodeRange = "unicode-range"; public static readonly string Src = "src"; - // @property descriptors (CSS Properties & Values API Level 1 §3) + // @property descriptors (CSS Properties and Values API 1 3) public static readonly string Syntax = "syntax"; public static readonly string InitialValue = "initial-value"; public static readonly string Inherits = "inherits"; public static readonly string BasePalette = "base-palette"; public static readonly string OverrideColors = "override-colors"; public static readonly string ObjectFit = "object-fit"; - public static readonly string ObjectPosition = "object-position"; + // CSS Paged Media 3: the "page" property assigns a box to a named page; "size" is an @page descriptor. + public static readonly string PageName = "page"; public static readonly string Size = "size"; - - // CSS Logical 1 §4 - public static readonly string MarginBlock = "margin-block"; - public static readonly string MarginBlockStart = "margin-block-start"; - public static readonly string MarginBlockEnd = "margin-block-end"; - public static readonly string MarginInline = "margin-inline"; - public static readonly string MarginInlineStart = "margin-inline-start"; - public static readonly string MarginInlineEnd = "margin-inline-end"; - - public static readonly string PaddingBlock = "padding-block"; - public static readonly string PaddingBlockStart = "padding-block-start"; - public static readonly string PaddingBlockEnd = "padding-block-end"; - public static readonly string PaddingInline = "padding-inline"; - public static readonly string PaddingInlineStart = "padding-inline-start"; - public static readonly string PaddingInlineEnd = "padding-inline-end"; - - public static readonly string Inset = "inset"; - public static readonly string InsetBlock = "inset-block"; - public static readonly string InsetBlockStart = "inset-block-start"; - public static readonly string InsetBlockEnd = "inset-block-end"; - public static readonly string InsetInline = "inset-inline"; - public static readonly string InsetInlineStart = "inset-inline-start"; - public static readonly string InsetInlineEnd = "inset-inline-end"; - - public static readonly string BorderBlock = "border-block"; - public static readonly string BorderBlockStart = "border-block-start"; - public static readonly string BorderBlockEnd = "border-block-end"; - public static readonly string BorderInline = "border-inline"; - public static readonly string BorderInlineStart = "border-inline-start"; - public static readonly string BorderInlineEnd = "border-inline-end"; - - public static readonly string BorderBlockWidth = "border-block-width"; - public static readonly string BorderBlockStyle = "border-block-style"; - public static readonly string BorderBlockColor = "border-block-color"; - public static readonly string BorderInlineWidth = "border-inline-width"; - public static readonly string BorderInlineStyle = "border-inline-style"; - public static readonly string BorderInlineColor = "border-inline-color"; - - public static readonly string BorderBlockStartWidth = "border-block-start-width"; - public static readonly string BorderBlockStartStyle = "border-block-start-style"; - public static readonly string BorderBlockStartColor = "border-block-start-color"; - public static readonly string BorderBlockEndWidth = "border-block-end-width"; - public static readonly string BorderBlockEndStyle = "border-block-end-style"; - public static readonly string BorderBlockEndColor = "border-block-end-color"; - public static readonly string BorderInlineStartWidth = "border-inline-start-width"; - public static readonly string BorderInlineStartStyle = "border-inline-start-style"; - public static readonly string BorderInlineStartColor = "border-inline-start-color"; - public static readonly string BorderInlineEndWidth = "border-inline-end-width"; - public static readonly string BorderInlineEndStyle = "border-inline-end-style"; - public static readonly string BorderInlineEndColor = "border-inline-end-color"; + public static readonly string ObjectPosition = "object-position"; } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoClassNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoClassNames.cs index e99dec18c..b90878a44 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoClassNames.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoClassNames.cs @@ -41,6 +41,8 @@ internal static class PseudoClassNames public static readonly string Matches = "matches"; public static readonly string Is = "is"; public static readonly string Where = "where"; + public static readonly string WebkitAny = "-webkit-any"; + public static readonly string MozAny = "-moz-any"; public static readonly string NthChild = "nth-child"; public static readonly string NthLastChild = "nth-last-child"; public static readonly string NthOfType = "nth-of-type"; @@ -51,6 +53,29 @@ internal static class PseudoClassNames public static readonly string Lang = "lang"; public static readonly string Contains = "contains"; public static readonly string HostContext = "host-context"; + public static readonly string Host = "host"; + public static readonly string State = "state"; + public static readonly string Defined = "defined"; + public static readonly string Modal = "modal"; + public static readonly string Open = "open"; + public static readonly string PopoverOpen = "popover-open"; + public static readonly string Fullscreen = "fullscreen"; + public static readonly string PictureInPicture = "picture-in-picture"; + public static readonly string UserValid = "user-valid"; + public static readonly string UserInvalid = "user-invalid"; + public static readonly string Autofill = "autofill"; + public static readonly string TargetWithin = "target-within"; + public static readonly string LocalLink = "local-link"; + public static readonly string Current = "current"; + public static readonly string Past = "past"; + public static readonly string Future = "future"; + public static readonly string Playing = "playing"; + public static readonly string Paused = "paused"; + public static readonly string Seeking = "seeking"; + public static readonly string Buffering = "buffering"; + public static readonly string Stalled = "stalled"; + public static readonly string Muted = "muted"; + public static readonly string VolumeLocked = "volume-locked"; public static readonly string Separator = ":"; } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoElementNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoElementNames.cs index c78fd111f..42f35c7b6 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoElementNames.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoElementNames.cs @@ -9,6 +9,23 @@ internal static class PseudoElementNames public static readonly string FirstLine = "first-line"; public static readonly string FirstLetter = "first-letter"; public static readonly string Content = "content"; + public static readonly string Placeholder = "placeholder"; + public static readonly string Backdrop = "backdrop"; + public static readonly string FileSelectorButton = "file-selector-button"; + public static readonly string DetailsContent = "details-content"; + public static readonly string TargetText = "target-text"; + public static readonly string SpellingError = "spelling-error"; + public static readonly string GrammarError = "grammar-error"; + public static readonly string Cue = "cue"; + public static readonly string CueRegion = "cue-region"; + public static readonly string ViewTransition = "view-transition"; + public static readonly string Part = "part"; + public static readonly string Slotted = "slotted"; + public static readonly string Highlight = "highlight"; + public static readonly string ViewTransitionGroup = "view-transition-group"; + public static readonly string ViewTransitionImagePair = "view-transition-image-pair"; + public static readonly string ViewTransitionOld = "view-transition-old"; + public static readonly string ViewTransitionNew = "view-transition-new"; public static readonly string Separator = "::"; } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/RotationAxis.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RotationAxis.cs new file mode 100644 index 000000000..a7818596c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RotationAxis.cs @@ -0,0 +1,10 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// The axis keywords of the rotate property (CSS Transforms 2). + internal enum RotationAxis : byte + { + X, + Y, + Z + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleNames.cs index 6ada85658..54ea94326 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleNames.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleNames.cs @@ -16,5 +16,14 @@ internal static class RuleNames public static readonly string Property = "property"; public static readonly string Layer = "layer"; public static readonly string FontPaletteValues = "font-palette-values"; + public static readonly string StartingStyle = "starting-style"; + public static readonly string CounterStyle = "counter-style"; + public static readonly string FontFeatureValues = "font-feature-values"; + public static readonly string Styleset = "styleset"; + public static readonly string CharacterVariant = "character-variant"; + public static readonly string Swash = "swash"; + public static readonly string Ornaments = "ornaments"; + public static readonly string Annotation = "annotation"; + public static readonly string Stylistic = "stylistic"; } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleType.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleType.cs index f13b491b6..b98212b0c 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleType.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleType.cs @@ -23,6 +23,8 @@ internal enum RuleType : byte Property, Layer, LayerStatement, - FontPaletteValues + FontPaletteValues, + StartingStyle, + FontFeatureValueSet } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignAll.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignAll.cs new file mode 100644 index 000000000..e2e18294d --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignAll.cs @@ -0,0 +1,13 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal enum TextAlignAll : byte + { + Start, + End, + Left, + Right, + Center, + Justify, + MatchParent + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignLast.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignLast.cs index 6c7a72e2e..c7f3fcccd 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignLast.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignLast.cs @@ -8,6 +8,7 @@ internal enum TextAlignLast : byte Left, Right, Center, - Justify + Justify, + MatchParent } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextOrientation.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextOrientation.cs new file mode 100644 index 000000000..caeea58e8 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextOrientation.cs @@ -0,0 +1,9 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal enum TextOrientation : byte + { + Mixed, + Upright, + Sideways + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextOverflow.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextOverflow.cs new file mode 100644 index 000000000..88fb3066d --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextOverflow.cs @@ -0,0 +1,8 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal enum TextOverflow : byte + { + Clip, + Ellipsis + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextWrapMode.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextWrapMode.cs new file mode 100644 index 000000000..52c0a6dd8 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextWrapMode.cs @@ -0,0 +1,8 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal enum TextWrapMode : byte + { + Wrap, + NoWrap + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextWrapStyle.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextWrapStyle.cs new file mode 100644 index 000000000..3c292caff --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextWrapStyle.cs @@ -0,0 +1,11 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal enum TextWrapStyle : byte + { + Auto, + Balance, + Stable, + Pretty, + AvoidOrphans + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/UnitNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/UnitNames.cs index a1ad6394d..7b5ab0858 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/UnitNames.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/UnitNames.cs @@ -27,6 +27,41 @@ internal static class UnitNames public static readonly string Pt = "pt"; public static readonly string Rem = "rem"; public static readonly string Pc = "pc"; + public static readonly string Q = "q"; + public static readonly string Cap = "cap"; + public static readonly string Ic = "ic"; + public static readonly string Lh = "lh"; + public static readonly string Rlh = "rlh"; + public static readonly string Rex = "rex"; + public static readonly string Rch = "rch"; + public static readonly string Rcap = "rcap"; + public static readonly string Ric = "ric"; + public static readonly string Vi = "vi"; + public static readonly string Vb = "vb"; + public static readonly string Svw = "svw"; + public static readonly string Svh = "svh"; + public static readonly string Svi = "svi"; + public static readonly string Svb = "svb"; + public static readonly string Svmin = "svmin"; + public static readonly string Svmax = "svmax"; + public static readonly string Lvw = "lvw"; + public static readonly string Lvh = "lvh"; + public static readonly string Lvi = "lvi"; + public static readonly string Lvb = "lvb"; + public static readonly string Lvmin = "lvmin"; + public static readonly string Lvmax = "lvmax"; + public static readonly string Dvw = "dvw"; + public static readonly string Dvh = "dvh"; + public static readonly string Dvi = "dvi"; + public static readonly string Dvb = "dvb"; + public static readonly string Dvmin = "dvmin"; + public static readonly string Dvmax = "dvmax"; + public static readonly string Cqw = "cqw"; + public static readonly string Cqh = "cqh"; + public static readonly string Cqi = "cqi"; + public static readonly string Cqb = "cqb"; + public static readonly string Cqmin = "cqmin"; + public static readonly string Cqmax = "cqmax"; public static readonly string Percent = "%"; } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/WhiteSpaceCollapse.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/WhiteSpaceCollapse.cs new file mode 100644 index 000000000..e9d49b720 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/WhiteSpaceCollapse.cs @@ -0,0 +1,11 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal enum WhiteSpaceCollapse : byte + { + Collapse, + Preserve, + PreserveBreaks, + PreserveSpaces, + BreakSpaces + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/WritingMode.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/WritingMode.cs new file mode 100644 index 000000000..48292f3d5 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/WritingMode.cs @@ -0,0 +1,11 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal enum WritingMode : byte + { + HorizontalTb, + VerticalRl, + VerticalLr, + SidewaysRl, + SidewaysLr + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Extensions/CharExtensions.cs b/Source/HtmlRenderer/Core/CssEngine/Extensions/CharExtensions.cs index b2054ec40..5754dc5c0 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Extensions/CharExtensions.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Extensions/CharExtensions.cs @@ -12,7 +12,7 @@ public static int FromHex(this char c) public static string ToHex(this char character) { - return ((int)character).ToString("x"); + return ((int) character).ToString("x"); } public static bool IsInRange(this char c, int lower, int upper) @@ -39,12 +39,12 @@ public static bool IsNormalPathCharacter(this char c) public static bool IsUppercaseAscii(this char c) { - return c >= Symbols.CapitalA && c <= Symbols.CapitalZ; + return c is >= Symbols.CapitalA and <= Symbols.CapitalZ; } public static bool IsLowercaseAscii(this char c) { - return c >= Symbols.LowerA && c <= Symbols.LowerZ; + return c is >= Symbols.LowerA and <= Symbols.LowerZ; } public static bool IsAlphanumericAscii(this char c) @@ -54,8 +54,8 @@ public static bool IsAlphanumericAscii(this char c) public static bool IsHex(this char c) { - return c.IsDigit() || (c >= Symbols.CapitalA && c <= Symbols.CapitalF) || - (c >= Symbols.LowerA && c <= Symbols.LowerF); + return c.IsDigit() || c is >= Symbols.CapitalA and <= Symbols.CapitalF || + c is >= Symbols.LowerA and <= Symbols.LowerF; } public static bool IsNonAscii(this char c) @@ -65,9 +65,9 @@ public static bool IsNonAscii(this char c) public static bool IsNonPrintable(this char c) { - return /*c >= Symbols.Null &&*/ c <= Symbols.Backspace - || (c >= Symbols.ShiftOut && c <= Symbols.UnitSeparator) - || (c >= Symbols.Delete && c < Symbols.NonBreakingSpace); + return c is /*>= Symbols.Null and*/ <= Symbols.Backspace + or >= Symbols.ShiftOut and <= Symbols.UnitSeparator + or >= Symbols.Delete and < Symbols.NonBreakingSpace; } public static bool IsLetter(this char c) @@ -87,17 +87,17 @@ public static bool IsNameStart(this char c) public static bool IsLineBreak(this char c) { - return c == Symbols.LineFeed || c == Symbols.CarriageReturn; + return c is Symbols.LineFeed or Symbols.CarriageReturn; } public static bool IsSpaceCharacter(this char c) { - return c == Symbols.Space || c == Symbols.Tab || c == Symbols.LineFeed || c == Symbols.CarriageReturn || c == Symbols.FormFeed; + return c is Symbols.Space or Symbols.Tab or Symbols.LineFeed or Symbols.CarriageReturn or Symbols.FormFeed; } public static bool IsDigit(this char c) { - return c >= Symbols.Zero && c <= Symbols.Nine; + return c is >= Symbols.Zero and <= Symbols.Nine; } // HTML forbids the use of Universal Character Set / Unicode code points @@ -109,7 +109,7 @@ public static bool IsDigit(this char c) public static bool IsInvalid(this int c) { return c == 0 || c > Symbols.MaximumCodepoint || - (c >= Symbols.UTF16SurrogateMin && c <= Symbols.UTF16SurrogateMax); + c is >= Symbols.UTF16SurrogateMin and <= Symbols.UTF16SurrogateMax; } public static bool IsOneOf(this char c, char a, char b) diff --git a/Source/HtmlRenderer/Core/CssEngine/Extensions/CollectionExtensions.cs b/Source/HtmlRenderer/Core/CssEngine/Extensions/CollectionExtensions.cs index ca617f4c6..3683f84e7 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Extensions/CollectionExtensions.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Extensions/CollectionExtensions.cs @@ -20,8 +20,7 @@ public static T GetItemByIndex(this IEnumerable items, int index) var i = 0; foreach (var item in items) - { - if (i++ == index) + { if (i++ == index) { return item; } diff --git a/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.Relative.cs b/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.Relative.cs new file mode 100644 index 000000000..c3f021dcf --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.Relative.cs @@ -0,0 +1,324 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + // Relative color syntax (CSS Color 5 section 4): rgb(from red r g b), hsl(from ...), hwb(from ...), lab(from ...), lch(from ...), oklab(from ...), + // oklch(from ...) and color(from ... ...). The origin color is converted to the space of the function it is written in, its components + // become channel keywords (r g b, h s l, l a b, ... and alpha) that stand for numbers, and each component of the result is then a number, a percentage, + // none, a keyword, or a calc() over keywords. + internal static partial class ColorFunctionExtensions + { + private static bool IsRelative(IEnumerable args) + { + foreach (var token in args) + { + if (token.Type == TokenType.Whitespace) continue; + return token.Type == TokenType.Ident && token.Data.Equals("from", StringComparison.OrdinalIgnoreCase); + } + + return false; + } + + private static Color? ParseRelative(string name, IEnumerable args, bool wantOklab, out double[] oklab) + { + oklab = null; + + var tokens = new List(args.Where(t => t.Type != TokenType.Whitespace)); + // from [/ alpha]: no commas, and the origin is a single token (a nested function arrives as one). + if (tokens.Count < 5 || tokens.Any(t => t.Type == TokenType.Comma)) return null; + + if (!Relative.TryGetSpec(name.ToLowerInvariant(), out var spec)) return null; + + // Exactly the components, then optionally a slash and one alpha. + var rest = new List(tokens.Skip(2)); + var slash = rest.FindIndex(t => t.Type == TokenType.Delim && t.Data == "/"); + if (slash < 0 ? rest.Count != spec.ComponentCount : slash != spec.ComponentCount || rest.Count != spec.ComponentCount + 2) return null; + + if (!Extract(rest, spec.ComponentCount, out var comps, out var alphaTok, allowImplicitAlpha: false)) return null; + if (spec.SpaceToken && !Relative.TryReadSpace(comps[0], out spec)) return null; + + // color-mix() operands and relative origins nest without bound in the grammar; they share one depth limit. + if (_colorMixDepth >= MaxColorMixDepth) return null; + Color? origin; + double[] originOklab; + _colorMixDepth++; + try + { + origin = ResolveColor(new[] { tokens[1] }, wantOklab: true, out originOklab); + } + finally + { + _colorMixDepth--; + } + + // A device-cmyk() origin has no sRGB value to take channels from. + if (!origin.HasValue || origin.Value.IsDeviceCmyk) return null; + var originColor = origin.Value; + + var coords = Relative.OriginCoordinates(originColor, originOklab, spec.Space); + var variables = new Dictionary(4); + for (var i = 0; i < 3; i++) variables[spec.Keywords[i]] = coords[i] * spec.Scale; + variables["alpha"] = originColor.A / 255.0; + + var offset = spec.SpaceToken ? 1 : 0; + var values = new double[3]; + for (var i = 0; i < 3; i++) + { + var token = comps[offset + i]; + var value = spec.IsHue(i) ? Relative.EvaluateHue(token, variables) : Relative.Evaluate(token, spec.References[i], variables); + if (value is null) return null; + values[i] = value.Value; + } + + float alpha; + if (alphaTok != null) + { + var a = Relative.Evaluate(alphaTok, 1.0, variables); + if (a is null) return null; + alpha = (float)Clamp(a.Value, 0, 1); + } + else + { + alpha = originColor.A / 255f; + } + + return Relative.Build(spec, values, alpha, wantOklab, out oklab); + } + + // What each function says about its components: the space its result is in, the channel keywords, what 100% of each component is, which one is a hue. + private sealed class RelativeSpec + { + public RelativeSpec(ColorSpaceMath.Space space, string[] keywords, double[] references, int hueSlot, double scale, bool spaceToken, int componentCount, string function) + { + Space = space; + Keywords = keywords; + References = references; + HueSlot = hueSlot; + Scale = scale; + SpaceToken = spaceToken; + ComponentCount = componentCount; + Function = function; + } + + public ColorSpaceMath.Space Space { get; } + public string[] Keywords { get; } + public double[] References { get; } + public int HueSlot { get; } + public double Scale { get; } + public bool SpaceToken { get; } + public int ComponentCount { get; } + public string Function { get; } + + public bool IsHue(int slot) => slot == HueSlot; + } + + private static class Relative + { + public static bool TryGetSpec(string name, out RelativeSpec spec) + { + spec = null; + switch (name) + { + case "rgb": + case "rgba": + spec = new RelativeSpec(ColorSpaceMath.Space.Srgb, new[] { "r", "g", "b" }, new double[] { 255, 255, 255 }, -1, 255, false, 3, "rgb"); + return true; + case "hsl": + case "hsla": + spec = new RelativeSpec(ColorSpaceMath.Space.Hsl, new[] { "h", "s", "l" }, new double[] { 360, 100, 100 }, 0, 1, false, 3, "hsl"); + return true; + case "hwb": + spec = new RelativeSpec(ColorSpaceMath.Space.Hwb, new[] { "h", "w", "b" }, new double[] { 360, 100, 100 }, 0, 1, false, 3, "hwb"); + return true; + case "lab": + spec = new RelativeSpec(ColorSpaceMath.Space.Lab, new[] { "l", "a", "b" }, new double[] { 100, 125, 125 }, -1, 1, false, 3, "lab"); + return true; + case "oklab": + spec = new RelativeSpec(ColorSpaceMath.Space.Oklab, new[] { "l", "a", "b" }, new[] { 1, 0.4, 0.4 }, -1, 1, false, 3, "oklab"); + return true; + case "lch": + spec = new RelativeSpec(ColorSpaceMath.Space.Lch, new[] { "l", "c", "h" }, new double[] { 100, 150, 360 }, 2, 1, false, 3, "lch"); + return true; + case "oklch": + spec = new RelativeSpec(ColorSpaceMath.Space.Oklch, new[] { "l", "c", "h" }, new[] { 1, 0.4, 360 }, 2, 1, false, 3, "oklch"); + return true; + case "color": + // The space is the first component; its keywords follow from it (TryReadSpace). + spec = new RelativeSpec(ColorSpaceMath.Space.Srgb, new[] { "r", "g", "b" }, new double[] { 1, 1, 1 }, -1, 1, true, 4, "color"); + return true; + default: + return false; + } + } + + public static bool TryReadSpace(Token token, out RelativeSpec spec) + { + spec = null; + if (token.Type != TokenType.Ident || !PredefinedSpaces.TryGetValue(token.Data, out var space)) return false; + + var isXyz = space == ColorSpaceMath.Space.XyzD65 || space == ColorSpaceMath.Space.XyzD50; + spec = new RelativeSpec(space, isXyz ? new[] { "x", "y", "z" } : new[] { "r", "g", "b" }, new double[] { 1, 1, 1 }, -1, 1, true, 4, "color"); + return true; + } + + // The origin's components in the space of the function it is relative to. A plain sRGB color converts from its own 8-bit value (so rgb(from red r g b) is red + // exactly); one that can lie outside the gamut from the coordinates it was written with, unclipped. + public static double[] OriginCoordinates(Color origin, double[] oklab, ColorSpaceMath.Space space) + { + if (oklab != null && space != ColorSpaceMath.Space.Hsl && space != ColorSpaceMath.Space.Hwb) + return ColorSpaceMath.ToSpaceFromOklab(oklab, space); + + return ColorSpaceMath.ToSpace(origin.R / 255.0, origin.G / 255.0, origin.B / 255.0, space); + } + + // A component: a number (or a percentage of the reference), none (0), a channel keyword, or a calc() over the keywords. + public static double? Evaluate(Token token, double reference, Dictionary variables) + { + if (IsNone(token)) return 0.0; + + if (token.Type == TokenType.Ident) + return variables.TryGetValue(token.Data.ToLowerInvariant(), out var keyword) ? keyword : (double?)null; + + if (token is FunctionToken function && CalcParser.IsCalcFamily(function.Data)) + { + var result = Calc(CalcParser.Parse(function, variables)); + if (result is null) return null; + switch (result.Value.Category) + { + case CalcCategory.Number: return result.Value.Value; + case CalcCategory.Percentage: return result.Value.Value / 100.0 * reference; + default: return null; + } + } + + return Component(token, reference); + } + + public static double? EvaluateHue(Token token, Dictionary variables) + { + if (IsNone(token)) return 0.0; + + if (token.Type == TokenType.Ident) + return variables.TryGetValue(token.Data.ToLowerInvariant(), out var keyword) ? keyword : (double?)null; + + if (token is FunctionToken function && CalcParser.IsCalcFamily(function.Data)) + { + var result = Calc(CalcParser.Parse(function, variables)); + if (result is null) return null; + return result.Value.Category == CalcCategory.Number || result.Value.Category == CalcCategory.Angle ? result.Value.Value : (double?)null; + } + + return HueDegrees(token); + } + + // A calc() tree to a number, with the category it type-checks as: numbers, percentages and angles (in degrees), the only kinds a color component takes. + // Anything else (lengths, times, ...) is not a color component, so it is null. + private static (double Value, CalcCategory Category)? Calc(CalcNode node) + { + switch (node) + { + case NumberCalcNode number: + return (number.Value, CalcCategory.Number); + case PercentageCalcNode percentage: + return (percentage.Value, CalcCategory.Percentage); + case AngleCalcNode angle: + return (ToDegrees(angle), CalcCategory.Angle); + case UnaryCalcNode unary: + { + var operand = Calc(unary.Operand); + if (operand is null) return null; + return (unary.Negative ? -operand.Value.Value : operand.Value.Value, operand.Value.Category); + } + case BinaryCalcNode binary: + { + var left = Calc(binary.Left); + var right = Calc(binary.Right); + if (left is null || right is null) return null; + var l = left.Value; + var r = right.Value; + switch (binary.Operator) + { + case '+' when l.Category == r.Category: + return (l.Value + r.Value, l.Category); + case '-' when l.Category == r.Category: + return (l.Value - r.Value, l.Category); + case '*' when l.Category == CalcCategory.Number: + return (l.Value * r.Value, r.Category); + case '*' when r.Category == CalcCategory.Number: + return (l.Value * r.Value, l.Category); + case '/' when r.Category == CalcCategory.Number && r.Value != 0: + return (l.Value / r.Value, l.Category); + default: + return null; + } + } + case CallCalcNode call: + { + var values = new List<(double Value, CalcCategory Category)>(call.Arguments.Count); + foreach (var argument in call.Arguments) + { + var value = Calc(argument); + if (value is null) return null; + if (values.Count > 0 && values[0].Category != value.Value.Category) return null; + values.Add(value.Value); + } + + if (values.Count == 0) return null; + var category = values[0].Category; + if (call.Name.Equals(FunctionNames.Min, StringComparison.OrdinalIgnoreCase)) return (values.Min(v => v.Value), category); + if (call.Name.Equals(FunctionNames.Max, StringComparison.OrdinalIgnoreCase)) return (values.Max(v => v.Value), category); + if (call.Name.Equals(FunctionNames.Clamp, StringComparison.OrdinalIgnoreCase) && values.Count == 3) + return (values[0].Value > values[2].Value ? values[2].Value : Clamp(values[1].Value, values[0].Value, values[2].Value), category); + return null; + } + default: + return null; + } + } + + private static double ToDegrees(AngleCalcNode angle) + { + switch (angle.Unit) + { + case Angle.Unit.Grad: return angle.Value * 0.9; + case Angle.Unit.Turn: return angle.Value * 360.0; + case Angle.Unit.Rad: return angle.Value * 180.0 / Math.PI; + default: return angle.Value; + } + } + + public static Color? Build(RelativeSpec spec, double[] v, float alpha, bool wantOklab, out double[] oklab) + { + oklab = null; + switch (spec.Function) + { + case "rgb": + return new Color((byte)Math.Round(Clamp(v[0], 0, 255)), (byte)Math.Round(Clamp(v[1], 0, 255)), (byte)Math.Round(Clamp(v[2], 0, 255)), Clamp01Byte(alpha)); + case "hsl": + return Color.FromHsla((float)(v[0] / 360.0), (float)(v[1] / 100.0), (float)(v[2] / 100.0), alpha); + case "hwb": + return Color.FromHwba((float)(v[0] / 360.0), (float)(v[1] / 100.0), (float)(v[2] / 100.0), alpha); + case "lab": + case "oklab": + { + var coords = new[] { Clamp(v[0], 0, spec.References[0]), v[1], v[2] }; + if (wantOklab) oklab = ColorSpaceMath.ToOklab(coords, spec.Space); + return Color.FromSpaceComponents(coords, spec.Space, alpha); + } + case "lch": + case "oklch": + { + var coords = new[] { Clamp(v[0], 0, spec.References[0]), Math.Max(v[1], 0), v[2] }; + if (wantOklab) oklab = ColorSpaceMath.ToOklab(coords, spec.Space); + return Color.FromSpaceComponents(coords, spec.Space, alpha); + } + default: + if (wantOklab) oklab = ColorSpaceMath.ToOklab(v, spec.Space); + return Color.FromSpaceComponents(v, spec.Space, alpha); + } + } + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.cs b/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.cs index ad5c46ac3..4bff4ebf1 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.cs @@ -5,34 +5,101 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// Resolves the color functions to an sRGB : rgb(), rgba(), hsl(), hsla(), hwb(), - /// lab(), lch(), oklab() and oklch() (CSS Color 4), color-mix() (CSS Color 5) and gray(). + /// Resolves the CSS color function forms from a tokenized value into a concrete + /// : the legacy/CSS Color 4 rgb()/rgba()/hsl()/hsla()/hwb()/gray() and the + /// CSS Color 4/5 lab()/oklab()/lch()/oklch()/color()/color-mix(). Tokenization is the object model's + /// job, so this consumes the already-parsed stream (nested functions + /// arrive as a single token) rather than re-scanning the source text. /// - internal static class ColorFunctionExtensions + internal static partial class ColorFunctionExtensions { /// - /// Resolves a named, hex or function color to sRGB. Unlike , - /// which keeps a function as specified, this always computes the color. + /// Fully resolves a tokenized color value to a concrete - named colors and + /// hex via , plus every color function + /// (rgb/hsl/hwb/gray/lab/oklab/lch/oklch/color()/color-mix/device-cmyk), including the relative + /// color syntax of the first ones (rgb(from red r g b)). Unlike ToColor (which the object-model + /// cascade uses and which deliberately preserves a function's specified form for serialization), + /// this resolver always computes the sRGB value. A component written as none is 0 here (a + /// missing alpha is transparent); it only stays missing inside a color-mix(). /// - public static Color? ToResolvedColor(this IEnumerable value) + public static Color? ToResolvedColor(this IEnumerable value) => ResolveColor(value, wantOklab: false, out _); + + // The one resolver behind ToResolvedColor and TryResolveOklab, so a nested color-mix() is read once per level, not once per question asked of it. + private static Color? ResolveColor(IEnumerable value, bool wantOklab, out double[] oklab) { - var basic = value.ToColor(); + oklab = null; + var enumerable = value as Token[] ?? value.ToArray(); + + var basic = enumerable.ToColor(); if (basic.HasValue) return basic; + var element = enumerable.OnlyOrDefault(); + + // A hex color nested inside a function (e.g. a color-mix() operand) tokenizes as a Hash + // keyword token rather than a ColorToken, so ToColor misses it - resolve it here. + if (element is { Type: TokenType.Hash } hash) return Color.FromHex(hash.Data); + + return element is FunctionToken function ? ParseColorFunction(function, wantOklab, out oklab) : null; + } + + /// + /// Reads light-dark(<color>, <color>) (CSS Color 5 section 4): the tokens of its light and of its dark arm, in order. Which + /// arm applies depends on the color scheme of the element the color is used on, which a parser does not know, so the value is not + /// resolved to one ( is null for it); each arm is any color, light-dark() included. + /// False if the value is not a light-dark() of exactly two arms. + /// + public static bool TryGetLightDark(this IEnumerable value, out List light, out List dark) + { + light = dark = null; var element = value.OnlyOrDefault(); + return element is FunctionToken function && + function.Data.Equals(FunctionNames.LightDark, StringComparison.OrdinalIgnoreCase) && + TrySplitColorPair(function.ArgumentTokens, out light, out dark); + } - // Inside a function, e.g. as a color-mix() operand, a hex color is a hash token - if (element != null && element.Type == TokenType.Hash) return Color.FromHex(element.Data); + /// + /// Reads a light-dark() value as the two sRGB colors its arms resolve to; an arm that is not a color + /// resolves (currentcolor, or a nested light-dark()) is null. + /// + public static bool TryResolveLightDark(this IEnumerable value, out Color? light, out Color? dark) + { + light = dark = null; + if (!value.TryGetLightDark(out var lightTokens, out var darkTokens)) return false; + light = lightTokens.ToResolvedColor(); + dark = darkTokens.ToResolvedColor(); + return true; + } - var function = element as FunctionToken; - return function != null ? ParseColorFunction(function) : null; + // Exactly two comma-separated, non-empty arms (whitespace dropped). + internal static bool TrySplitColorPair(IEnumerable args, out List first, out List second) + { + first = second = null; + var arms = new List> { new List() }; + foreach (var token in args) + { + if (token.Type == TokenType.Comma) arms.Add(new List()); + else if (token.Type != TokenType.Whitespace) arms[arms.Count - 1].Add(token); + } + + if (arms.Count != 2 || arms[0].Count == 0 || arms[1].Count == 0) return false; + first = arms[0]; + second = arms[1]; + return true; } - public static Color? ParseColorFunction(FunctionToken function) + public static Color? ParseColorFunction(FunctionToken function) => ParseColorFunction(function, wantOklab: false, out _); + + // oklab: for lab(), lch(), oklab(), oklch(), color() and color-mix() (when wantOklab), the Oklab coordinates the color was written with, which are + // outside the sRGB gamut where the returned Color is clipped to it; null for every other color. + private static Color? ParseColorFunction(FunctionToken function, bool wantOklab, out double[] oklab) { + oklab = null; var name = function.Data; var args = function.ArgumentTokens; + // Relative color syntax (rgb(from red r g b) and the like) is read by whichever function it is written in. + if (IsRelative(args)) return ParseRelative(name, args, wantOklab, out oklab); + if (name.Equals(FunctionNames.Rgb, StringComparison.OrdinalIgnoreCase) || name.Equals(FunctionNames.Rgba, StringComparison.OrdinalIgnoreCase)) return ParseRgb(args); @@ -48,135 +115,301 @@ internal static class ColorFunctionExtensions return ParseGray(args); if (name.Equals(FunctionNames.Lab, StringComparison.OrdinalIgnoreCase)) - return ParseRect(args, Color.FromLab, abReference: 125.0, lReference: 100.0); + return ParseRect(args, Color.FromLab, abReference: 125.0, lReference: 100.0, ColorSpaceMath.Space.Lab, wantOklab, out oklab); if (name.Equals(FunctionNames.Oklab, StringComparison.OrdinalIgnoreCase)) - return ParseRect(args, Color.FromOklab, abReference: 0.4, lReference: 1.0); + return ParseRect(args, Color.FromOklab, abReference: 0.4, lReference: 1.0, ColorSpaceMath.Space.Oklab, wantOklab, out oklab); if (name.Equals(FunctionNames.Lch, StringComparison.OrdinalIgnoreCase)) - return ParsePolar(args, Color.FromLch, chromaReference: 150.0, lReference: 100.0); + return ParsePolar(args, Color.FromLch, chromaReference: 150.0, lReference: 100.0, ColorSpaceMath.Space.Lch, wantOklab, out oklab); if (name.Equals(FunctionNames.Oklch, StringComparison.OrdinalIgnoreCase)) - return ParsePolar(args, Color.FromOklch, chromaReference: 0.4, lReference: 1.0); + return ParsePolar(args, Color.FromOklch, chromaReference: 0.4, lReference: 1.0, ColorSpaceMath.Space.Oklch, wantOklab, out oklab); if (name.Equals(FunctionNames.ColorMix, StringComparison.OrdinalIgnoreCase)) - return ParseColorMix(args); + return ParseColorMix(args, wantOklab, out oklab); + + if (name.Equals(FunctionNames.ColorFunction, StringComparison.OrdinalIgnoreCase)) + return ParseColorSpaceFunction(args, wantOklab, out oklab); + + if (name.Equals(FunctionNames.DeviceCmyk, StringComparison.OrdinalIgnoreCase)) + return ParseDeviceCmyk(args); return null; } - // rgb()/rgba(): 3 components (int or % of 255) + optional alpha + // ── device-cmyk(): 4 components (number or %) + optional alpha ─────────── + // https://www.w3.org/TR/css-color-5/#device-cmyk - alpha is only ever introduced by '/', and there is no relative ("from") form. + private static Color? ParseDeviceCmyk(IEnumerable args) + { + if (!Extract(args, 4, out var comps, out var alphaTok, allowImplicitAlpha: false)) return null; + var c = CmykComponent(comps[0]); + var m = CmykComponent(comps[1]); + var y = CmykComponent(comps[2]); + var k = CmykComponent(comps[3]); + if (c is null || m is null || y is null || k is null) return null; + return Color.FromDeviceCmyk(c.Value, m.Value, y.Value, k.Value, Alpha(alphaTok)); + } + + // A is a or , each normalized to 0..1; none is 0. + private static float? CmykComponent(Token token) + { + if (IsNone(token)) return 0f; + var number = AsTokens(token).ToSingle(); + if (number.HasValue) return number.Value; + return AsTokens(token).ToPercent()?.NormalizedValue; + } + + // ── rgb()/rgba(): 3 components (int or % of 255) + optional alpha ───────── private static Color? ParseRgb(IEnumerable args) { - List comps; - Token alphaTok; - if (!Extract(args, 3, out comps, out alphaTok)) return null; - var r = AsTokens(comps[0]).ToRgbComponent(); - var g = AsTokens(comps[1]).ToRgbComponent(); - var b = AsTokens(comps[2]).ToRgbComponent(); - if (r == null || g == null || b == null) return null; - var a = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f; - return new Color(r.Value, g.Value, b.Value, Clamp01Byte(a)); + if (!Extract(args, 3, out var comps, out var alphaTok)) return null; + // none (a missing component) is only part of the space-separated syntax, not the legacy comma one. + if (HasComma(args) && (comps.Any(IsNone) || (alphaTok != null && IsNone(alphaTok)))) return null; + var r = RgbComponent(comps[0]); + var g = RgbComponent(comps[1]); + var b = RgbComponent(comps[2]); + if (r is null || g is null || b is null) return null; + return new Color(r.Value, g.Value, b.Value, Clamp01Byte(Alpha(alphaTok))); } - // hsl()/hsla(): hue angle + saturation% + lightness% + optional alpha + // ── hsl()/hsla(): hue angle + saturation% + lightness% + optional alpha ── private static Color? ParseHsl(IEnumerable args) { - List comps; - Token alphaTok; - if (!Extract(args, 3, out comps, out alphaTok)) return null; + if (!Extract(args, 3, out var comps, out var alphaTok)) return null; + if (HasComma(args) && (comps.Any(IsNone) || (alphaTok != null && IsNone(alphaTok)))) return null; var hue = HueDegrees(comps[0]); - var sat = AsTokens(comps[1]).ToPercent(); - var light = AsTokens(comps[2]).ToPercent(); - if (hue == null || sat == null || light == null) return null; - var a = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f; - return Color.FromHsla((float)(hue.Value / 360.0), sat.Value.NormalizedValue, light.Value.NormalizedValue, a); + var sat = PercentValue(comps[1]); + var light = PercentValue(comps[2]); + if (hue is null || sat is null || light is null) return null; + return Color.FromHsla((float)(hue.Value / 360.0), sat.Value, light.Value, Alpha(alphaTok)); } - // hwb(): hue angle + whiteness% + blackness% + optional alpha + // ── hwb(): hue angle + whiteness% + blackness% + optional alpha ────────── private static Color? ParseHwb(IEnumerable args) { - List comps; - Token alphaTok; - if (!Extract(args, 3, out comps, out alphaTok)) return null; + if (!Extract(args, 3, out var comps, out var alphaTok)) return null; var hue = HueDegrees(comps[0]); - var white = AsTokens(comps[1]).ToPercent(); - var black = AsTokens(comps[2]).ToPercent(); - if (hue == null || white == null || black == null) return null; - var a = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f; - return Color.FromHwba((float)(hue.Value / 360.0), white.Value.NormalizedValue, black.Value.NormalizedValue, a); + var white = PercentValue(comps[1]); + var black = PercentValue(comps[2]); + if (hue is null || white is null || black is null) return null; + return Color.FromHwba((float)(hue.Value / 360.0), white.Value, black.Value, Alpha(alphaTok)); } - // gray(): a single lightness component + optional alpha + // ── gray(): a single lightness component + optional alpha ──────────────── private static Color? ParseGray(IEnumerable args) { - List comps; - Token alphaTok; - if (!Extract(args, 1, out comps, out alphaTok)) return null; + if (!Extract(args, 1, out var comps, out var alphaTok)) return null; var value = AsTokens(comps[0]).ToRgbComponent(); - if (value == null) return null; - var a = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f; - return Color.FromGray(value.Value, a); + if (value is null) return null; + return Color.FromGray(value.Value, Alpha(alphaTok)); } - // lab()/oklab(): L + a + b + optional alpha - private static Color? ParseRect(IEnumerable args, Func build, double abReference, double lReference) + // ── lab()/oklab(): L + a + b + optional alpha ──────────────────────────── + private static Color? ParseRect(IEnumerable args, Func build, double abReference, double lReference, + ColorSpaceMath.Space space, bool wantOklab, out double[] oklab) { - List comps; - Token alphaTok; - if (!Extract(args, 3, out comps, out alphaTok)) return null; + oklab = null; + if (!TryRectComponents(args, abReference, lReference, out var v, out var alpha, out _)) return null; + if (wantOklab) oklab = ColorSpaceMath.ToOklab(v, space); + return build(v[0], v[1], v[2], alpha); + } + + private static bool TryRectComponents(IEnumerable args, double abReference, double lReference, out double[] v, out float alpha, out int missing) + { + v = new double[0]; + alpha = 1f; + missing = 0; + if (!Extract(args, 3, out var comps, out var alphaTok)) return false; var l = Component(comps[0], lReference); var a = Component(comps[1], abReference); var b = Component(comps[2], abReference); - if (l == null || a == null || b == null) return null; - var alpha = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f; - return build(l.Value, a.Value, b.Value, alpha); + if (l is null || a is null || b is null) return false; + alpha = Alpha(alphaTok); + missing = MissingMask(comps, alphaTok); + // CSS Color 4 section 9: the lightness is clamped to its range when the color is parsed (the reference is 100% of it), before any mix. + v = new[] { Clamp(l.Value, 0, lReference), a.Value, b.Value }; + return true; } - // lch()/oklch(): L + C + hue angle + optional alpha - private static Color? ParsePolar(IEnumerable args, Func build, double chromaReference, double lReference) + // ── lch()/oklch(): L + C + hue angle + optional alpha ──────────────────── + private static Color? ParsePolar(IEnumerable args, Func build, double chromaReference, double lReference, + ColorSpaceMath.Space space, bool wantOklab, out double[] oklab) { - List comps; - Token alphaTok; - if (!Extract(args, 3, out comps, out alphaTok)) return null; + oklab = null; + if (!TryPolarComponents(args, chromaReference, lReference, out var v, out var alpha, out _)) return null; + if (wantOklab) oklab = ColorSpaceMath.ToOklab(v, space); + return build(v[0], v[1], v[2], alpha); + } + + private static bool TryPolarComponents(IEnumerable args, double chromaReference, double lReference, out double[] v, out float alpha, out int missing) + { + v = new double[0]; + alpha = 1f; + missing = 0; + if (!Extract(args, 3, out var comps, out var alphaTok)) return false; var l = Component(comps[0], lReference); var c = Component(comps[1], chromaReference); var h = HueDegrees(comps[2]); - if (l == null || c == null || h == null) return null; - var alpha = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f; - return build(l.Value, c.Value, h.Value, alpha); + if (l is null || c is null || h is null) return false; + alpha = Alpha(alphaTok); + missing = MissingMask(comps, alphaTok); + // CSS Color 4 section 9: the lightness is clamped to its range and a negative chroma to 0 when the color is parsed. + v = new[] { Clamp(l.Value, 0, lReference), Math.Max(c.Value, 0), h.Value }; + return true; + } + + // ── color( c1 c2 c3 [/ alpha]) ──────────────────────────────────── + // https://www.w3.org/TR/css-color-4/#color-function - the predefined spaces. A colour in a + // wide-gamut space is clipped to sRGB, like the other wide-gamut functions. + private static readonly Dictionary PredefinedSpaces = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["srgb"] = ColorSpaceMath.Space.Srgb, + ["srgb-linear"] = ColorSpaceMath.Space.SrgbLinear, + ["display-p3"] = ColorSpaceMath.Space.DisplayP3, + ["a98-rgb"] = ColorSpaceMath.Space.A98Rgb, + ["prophoto-rgb"] = ColorSpaceMath.Space.ProphotoRgb, + ["rec2020"] = ColorSpaceMath.Space.Rec2020, + ["xyz"] = ColorSpaceMath.Space.XyzD65, + ["xyz-d65"] = ColorSpaceMath.Space.XyzD65, + ["xyz-d50"] = ColorSpaceMath.Space.XyzD50 + }; + + private static Color? ParseColorSpaceFunction(IEnumerable args, bool wantOklab, out double[] oklab) + { + oklab = null; + if (!TryColorFunctionComponents(args, out var space, out var v, out var alpha, out _)) return null; + if (wantOklab) oklab = ColorSpaceMath.ToOklab(v, space); + return Color.FromSpaceComponents(v, space, alpha); + } + + private static bool TryColorFunctionComponents(IEnumerable args, out ColorSpaceMath.Space space, out double[] v, out float alpha, out int missing) + { + space = default; + v = new double[0]; + alpha = 1f; + missing = 0; + // The alpha is only ever introduced by "/": a fifth bare value is malformed. + if (!Extract(args, 4, out var comps, out var alphaTok, allowImplicitAlpha: false)) return false; + if (comps[0].Type != TokenType.Ident || !PredefinedSpaces.TryGetValue(comps[0].Data, out space)) return false; + + // The components of every predefined space are a number or a percentage of 1 (the xyz spaces too). + var c1 = Component(comps[1], 1.0); + var c2 = Component(comps[2], 1.0); + var c3 = Component(comps[3], 1.0); + if (c1 is null || c2 is null || c3 is null) return false; + v = new[] { c1.Value, c2.Value, c3.Value }; + alpha = Alpha(alphaTok); + missing = MissingMask(comps.GetRange(1, 3), alphaTok); + return true; + } + + // ── color-mix(in [], c1 [p1], c2 [p2]) ─────────────── + private static Color? ParseColorMix(IEnumerable args, bool wantOklab, out double[] oklab) + { + oklab = null; + if (!TryColorMixOperands(args, out var c1, out var c2, out var t, out var space, out var hue, out var alphaMultiplier, + out var oklab1, out var oklab2, out var written1, out var written2)) + return null; + + // Both operands device-cmyk(): a mix in the C/M/Y/K components themselves (see Color.MixCmyk). One of them with an sRGB color has + // no profile-free conversion, so it stays invalid, as does asking for the Oklab coordinates of a CMYK mix. + if (c1.IsDeviceCmyk != c2.IsDeviceCmyk) return null; + if (c1.IsDeviceCmyk) return Color.MixCmyk(c1, c2, t, alphaMultiplier); + + if (wantOklab) + { + var mixed = Color.MixComponents(c1, c2, t, space, hue, out _, oklab1, oklab2, written1, written2); + oklab = ColorSpaceMath.ToOklab(mixed, space); + } + + return Color.Mix(c1, c2, t, space, hue, alphaMultiplier, oklab1, oklab2, written1, written2); + } + + /// + /// Reads a lab(), lch(), oklab(), oklch(), color() or color-mix() color as the Oklab coordinates it names, + /// without the clip to the sRGB gamut that applies: an out-of-gamut color keeps its out-of-range + /// coordinates, which is what a browser mixes two colors from (CSS Color 4 section 13). False for any other color (the clipped sRGB value + /// is the color then). + /// + public static bool TryResolveOklab(this IEnumerable value, out double[] oklab) + { + var color = ResolveColor(value, wantOklab: true, out oklab); + if (color.HasValue && oklab != null) return true; + + oklab = new double[0]; + return false; + } + + /// + /// Reads any color as an operand of a mix in Oklab (an animation between two colors): its Oklab coordinates as written, without the clip to the sRGB gamut, and + /// which of them are missing (bits 0 to 2 for L, a and b; for the alpha) - a component written as none, carried to + /// the analogous Oklab component where there is one (CSS Color 4 section 12.4). False for a color that does not resolve. + /// + public static bool TryResolveMixOperand(this IEnumerable value, out double[] oklab, out int missing) + { + missing = 0; + var tokens = new List(value.Where(t => t.Type != TokenType.Whitespace)); + var color = ResolveColor(tokens, wantOklab: true, out oklab); + if (!(color is { } resolved) || resolved.IsDeviceCmyk) + { + oklab = new double[0]; + return false; + } + + oklab ??= ColorSpaceMath.ToSpace(resolved.R / 255.0, resolved.G / 255.0, resolved.B / 255.0, ColorSpaceMath.Space.Oklab); + + var written = WrittenComponents(tokens); + if (written.HasValue) + missing = ColorSpaceMath.MapMissing(written.Value.Space, written.Value.Missing, ColorSpaceMath.Space.Oklab); + return true; } - // color-mix(in [], c1 [p1], c2 [p2]) - private static Color? ParseColorMix(IEnumerable args) + // The operands, weight and space of a color-mix(), after the CSS Color 5 section 3 percentage normalization. + private static bool TryColorMixOperands(IEnumerable args, out Color c1, out Color c2, out double t, out ColorSpaceMath.Space space, + out ColorSpaceMath.HueMethod hue, out double alphaMultiplier, out double[] oklab1, out double[] oklab2, + out ColorSpaceMath.WrittenCoords? written1, out ColorSpaceMath.WrittenCoords? written2) { + c1 = c2 = default; + oklab1 = oklab2 = null; + written1 = written2 = null; + t = alphaMultiplier = 0; + space = default; + hue = default; + var segments = SplitByComma(args); - if (segments.Count != 3) return null; + if (segments.Count != 3) return false; - var config = segments[0].Where(token => token.Type != TokenType.Whitespace) - .Select(token => token.Data.ToLowerInvariant()).ToList(); - if (config.Count < 2 || config[0] != "in") return null; - var space = MapSpace(config[1]); - var hue = MapHue(config, 2); + var config = segments[0].Where(x => x.Type != TokenType.Whitespace) + .Select(x => x.Data.ToLowerInvariant()).ToList(); + if (config.Count < 2 || config[0] != "in") return false; + space = MapSpace(config[1]); + hue = MapHue(config, 2); - Color c1, c2; - double? p1, p2; - if (!ParseMixOperand(segments[1], out c1, out p1)) return null; - if (!ParseMixOperand(segments[2], out c2, out p2)) return null; + if (!ParseMixOperand(segments[1], out c1, out var p1, out oklab1, out written1)) return false; + if (!ParseMixOperand(segments[2], out c2, out var p2, out oklab2, out written2)) return false; - // CSS Color 5 §3.2 - if (p1 == null && p2 == null) { p1 = 0.5; p2 = 0.5; } - else if (p1 == null) p1 = Math.Max(0, 1.0 - p2.Value); - else if (p2 == null) p2 = Math.Max(0, 1.0 - p1.Value); + // CSS Color 5 section 3 normalization: a missing percentage is 100% minus the other; percentages + // scale to sum 1, and a pre-normalization sum below 100% scales the result alpha down. + if (p1 is null && p2 is null) { p1 = 0.5; p2 = 0.5; } + else if (p1 is null) p1 = Math.Max(0, 1.0 - p2.Value); + else if (p2 is null) p2 = Math.Max(0, 1.0 - p1.Value); var sum = p1.Value + p2.Value; - if (sum <= 0) return null; - var alphaMultiplier = sum < 1.0 ? sum : 1.0; - var t = p2.Value / sum; - - return Color.Mix(c1, c2, t, space, hue, alphaMultiplier); + if (sum <= 0) return false; + alphaMultiplier = sum < 1.0 ? sum : 1.0; + t = p2.Value / sum; + return true; } - private static bool ParseMixOperand(List segment, out Color color, out double? percent) + private const int MaxColorMixDepth = 32; + + [ThreadStatic] + private static int _colorMixDepth; + + private static bool ParseMixOperand(List segment, out Color color, out double? percent, out double[] oklab, out ColorSpaceMath.WrittenCoords? written) { - color = default(Color); + color = default; + oklab = null; + written = null; percent = null; var tokens = new List(); @@ -186,31 +419,116 @@ private static bool ParseMixOperand(List segment, out Color color, out do } if (tokens.Count == 0) return false; - // The percentage may come before or after the color (CSS Color 5 §3) + // The optional percentage may appear before or after the color (CSS Color 5 section 3.1), e.g. + // both `red 30%` and `30% red` are valid. if (tokens[tokens.Count - 1].Type == TokenType.Percentage) { - var value = AsTokens(tokens[tokens.Count - 1]).ToPercent(); - percent = value.HasValue ? value.Value.NormalizedValue : (double?)null; + percent = AsTokens(tokens[tokens.Count - 1]).ToPercent()?.NormalizedValue; tokens.RemoveAt(tokens.Count - 1); } else if (tokens[0].Type == TokenType.Percentage) { - var value = AsTokens(tokens[0]).ToPercent(); - percent = value.HasValue ? value.Value.NormalizedValue : (double?)null; + percent = AsTokens(tokens[0]).ToPercent()?.NormalizedValue; tokens.RemoveAt(0); } - var resolved = tokens.ToResolvedColor(); - if (resolved == null) return false; + // A color-mix operand may itself be any color form (named/hex/rgb/oklch/...), so resolve it + // through the full resolver rather than the named/hex-only ToColor. The operand comes back with + // the coordinates it was written with too, when it is one that can lie outside the gamut the Color is clipped to. + // color-mix() operands nest without bound in the grammar, and each level costs several frames of the machine + // stack, so past a depth no stylesheet needs the color is invalid, as any other value that cannot be read. + if (_colorMixDepth >= MaxColorMixDepth) return false; + Color? resolved; + _colorMixDepth++; + try + { + resolved = ResolveColor(tokens, wantOklab: true, out oklab); + } + finally + { + _colorMixDepth--; + } + if (resolved is null) return false; color = resolved.Value; + written = WrittenComponents(tokens); return true; } - // Token helpers + // The components a color operand was written with (null for any other color). Those of lab(), lch(), oklab(), oklch() and color() are always + // read, for a gray's hue and an out-of-gamut color; the rest only when one of their components or the alpha is missing (none), as that is all a mix needs. + private static ColorSpaceMath.WrittenCoords? WrittenComponents(List tokens) + { + if (tokens.Count != 1 || !(tokens[0] is FunctionToken function)) return null; + + var name = function.Data; + var args = function.ArgumentTokens; + double[] v; + int missing; + + // A relative color's written components are the ones its channel keywords resolved to: a mix takes it from its Oklab coordinates. + if (IsRelative(args)) return null; + + if (name.Equals(FunctionNames.Lab, StringComparison.OrdinalIgnoreCase) && TryRectComponents(args, 125.0, 100.0, out v, out _, out missing)) + return new ColorSpaceMath.WrittenCoords(ColorSpaceMath.Space.Lab, v, missing); + if (name.Equals(FunctionNames.Oklab, StringComparison.OrdinalIgnoreCase) && TryRectComponents(args, 0.4, 1.0, out v, out _, out missing)) + return new ColorSpaceMath.WrittenCoords(ColorSpaceMath.Space.Oklab, v, missing); + if (name.Equals(FunctionNames.Lch, StringComparison.OrdinalIgnoreCase) && TryPolarComponents(args, 150.0, 100.0, out v, out _, out missing)) + return new ColorSpaceMath.WrittenCoords(ColorSpaceMath.Space.Lch, v, missing); + if (name.Equals(FunctionNames.Oklch, StringComparison.OrdinalIgnoreCase) && TryPolarComponents(args, 0.4, 1.0, out v, out _, out missing)) + return new ColorSpaceMath.WrittenCoords(ColorSpaceMath.Space.Oklch, v, missing); + if (name.Equals(FunctionNames.ColorFunction, StringComparison.OrdinalIgnoreCase) && TryColorFunctionComponents(args, out var space, out v, out _, out missing)) + return new ColorSpaceMath.WrittenCoords(space, v, missing); + + if (!Extract(args, 3, out var comps, out var alphaTok) || HasComma(args)) return null; + missing = MissingMask(comps, alphaTok); + if (missing == 0) return null; + + if (name.Equals(FunctionNames.Rgb, StringComparison.OrdinalIgnoreCase) || name.Equals(FunctionNames.Rgba, StringComparison.OrdinalIgnoreCase)) + { + var r = RgbComponent(comps[0]); + var g = RgbComponent(comps[1]); + var b = RgbComponent(comps[2]); + return r is null || g is null || b is null + ? (ColorSpaceMath.WrittenCoords?)null + : new ColorSpaceMath.WrittenCoords(ColorSpaceMath.Space.Srgb, new[] { r.Value / 255.0, g.Value / 255.0, b.Value / 255.0 }, missing); + } + + var isHsl = name.Equals(FunctionNames.Hsl, StringComparison.OrdinalIgnoreCase) || name.Equals(FunctionNames.Hsla, StringComparison.OrdinalIgnoreCase); + if (isHsl || name.Equals(FunctionNames.Hwb, StringComparison.OrdinalIgnoreCase)) + { + var hue = HueDegrees(comps[0]); + var second = PercentValue(comps[1]); + var third = PercentValue(comps[2]); + return hue is null || second is null || third is null + ? (ColorSpaceMath.WrittenCoords?)null + : new ColorSpaceMath.WrittenCoords(isHsl ? ColorSpaceMath.Space.Hsl : ColorSpaceMath.Space.Hwb, + new[] { hue.Value, second.Value * 100.0, third.Value * 100.0 }, missing); + } + + return null; + } - // Splits the arguments into `count` components and an optional alpha (CSS Color 4 §4.1). Commas - // and whitespace both separate, the alpha follows a "/" or is the extra last value. - private static bool Extract(IEnumerable args, int count, out List components, out Token alpha) + // Which of the three components (bits 0 to 2) and the alpha (ColorSpaceMath.AlphaMissing) were written as none. + private static int MissingMask(List components, Token alpha) + { + var mask = 0; + for (var i = 0; i < 3 && i < components.Count; i++) + { + if (IsNone(components[i])) mask |= 1 << i; + } + + if (alpha != null && IsNone(alpha)) mask |= ColorSpaceMath.AlphaMissing; + return mask; + } + + // ── Token helpers ──────────────────────────────────────────────────────── + + // Partitions a function's argument tokens into `count` component tokens and an optional alpha + // token. Whitespace and commas separate; a "/" delimiter (CSS Color 4) marks the alpha that + // follows. Without a slash, a trailing extra value (legacy rgba/hsla comma alpha) is the alpha - + // unless allowImplicitAlpha is false (color() has no such legacy form: alpha is only ever + // introduced by '/'), in which case an unslashed trailing value is malformed. + private static bool Extract(IEnumerable args, int count, out List components, out Token alpha, bool allowImplicitAlpha = true) { components = new List(); alpha = null; @@ -236,6 +554,13 @@ private static bool Extract(IEnumerable args, int count, out List if (values.Count < count) return false; for (var i = 0; i < count; i++) components.Add(values[i]); + if (!allowImplicitAlpha) + { + if (slashIndex < 0) return values.Count == count; + if (slashIndex < values.Count) alpha = values[slashIndex]; + return true; + } + var alphaAt = slashIndex >= 0 ? slashIndex : count; if (alphaAt < values.Count) alpha = values[alphaAt]; return true; @@ -261,16 +586,35 @@ private static List> SplitByComma(IEnumerable tokens) return result; } - private static Token[] AsTokens(Token token) + private static Token[] AsTokens(Token token) => new[] { token }; + + private static bool IsNone(Token token) => + token.Type == TokenType.Ident && token.Data.Equals("none", StringComparison.OrdinalIgnoreCase); + + private static bool HasComma(IEnumerable args) => args.Any(t => t.Type == TokenType.Comma); + + private static double Clamp(double value, double min, double max) => value < min ? min : (value > max ? max : value); + + // A missing component (none) is 0 once the color is used for anything but a mix. + private static byte? RgbComponent(Token token) => IsNone(token) ? (byte)0 : AsTokens(token).ToRgbComponent(); + + private static float? PercentValue(Token token) + { + if (IsNone(token)) return 0f; + return AsTokens(token).ToPercent()?.NormalizedValue; + } + + // The alpha of a color: a number or percentage; a missing alpha (none) is transparent once the color is used for anything but a mix. + private static float Alpha(Token token) { - return new[] { token }; + if (token is null) return 1f; + return IsNone(token) ? 0f : AsTokens(token).ToAlphaValue() ?? 1f; } // A number (returned as-is) or a percentage resolved against `reference`; "none" resolves to 0. private static double? Component(Token token, double reference) { - if (token.Type == TokenType.Ident && token.Data.Equals("none", StringComparison.OrdinalIgnoreCase)) - return 0.0; + if (IsNone(token)) return 0.0; var number = AsTokens(token).ToSingle(); if (number.HasValue) return number.Value; var percent = AsTokens(token).ToPercent(); @@ -279,11 +623,9 @@ private static Token[] AsTokens(Token token) private static double? HueDegrees(Token token) { - if (token.Type == TokenType.Ident && token.Data.Equals("none", StringComparison.OrdinalIgnoreCase)) - return 0.0; + if (IsNone(token)) return 0.0; var angle = AsTokens(token).ToAngleNumber(); if (!angle.HasValue) return null; - switch (angle.Value.Type) { case Angle.Unit.Grad: return angle.Value.Value * 0.9; @@ -295,7 +637,8 @@ private static Token[] AsTokens(Token token) private static byte Clamp01Byte(float alpha) { - return (byte)Math.Round(Math.Min(Math.Max(alpha, 0f), 1f) * 255); + var clamped = alpha < 0f ? 0f : (alpha > 1f ? 1f : alpha); + return (byte)Math.Round(clamped * 255); } private static ColorSpaceMath.Space MapSpace(string name) @@ -304,6 +647,9 @@ private static ColorSpaceMath.Space MapSpace(string name) { case "srgb-linear": return ColorSpaceMath.Space.SrgbLinear; case "display-p3": return ColorSpaceMath.Space.DisplayP3; + case "a98-rgb": return ColorSpaceMath.Space.A98Rgb; + case "prophoto-rgb": return ColorSpaceMath.Space.ProphotoRgb; + case "rec2020": return ColorSpaceMath.Space.Rec2020; case "lab": return ColorSpaceMath.Space.Lab; case "oklab": return ColorSpaceMath.Space.Oklab; case "xyz": @@ -322,46 +668,15 @@ private static ColorSpaceMath.HueMethod MapHue(List idents, int startInd for (var i = startIndex; i + 1 < idents.Count; i++) { if (idents[i + 1] != "hue") continue; - return MapHueName(idents[i]); + switch (idents[i]) + { + case "longer": return ColorSpaceMath.HueMethod.Longer; + case "increasing": return ColorSpaceMath.HueMethod.Increasing; + case "decreasing": return ColorSpaceMath.HueMethod.Decreasing; + default: return ColorSpaceMath.HueMethod.Shorter; + } } return ColorSpaceMath.HueMethod.Shorter; } - - private static ColorSpaceMath.HueMethod MapHueName(string name) - { - switch (name) - { - case "longer": return ColorSpaceMath.HueMethod.Longer; - case "increasing": return ColorSpaceMath.HueMethod.Increasing; - case "decreasing": return ColorSpaceMath.HueMethod.Decreasing; - default: return ColorSpaceMath.HueMethod.Shorter; - } - } - - /// - /// Blends two colors per the CSS palette-mix() rules (CSS Fonts 4), reusing color-mix()'s - /// color-space mapping () and CSS Color 5 §3 percentage normalization so the two - /// functions interpolate identically. / are 0..100 percentages - /// (or null); is a hue-interpolation direction (or null for the default). - /// - internal static Color MixPaletteColors(Color c1, Color c2, string spaceName, string hueName, double? p1, double? p2) - { - var space = MapSpace(spaceName.ToLowerInvariant()); - var hue = MapHueName(hueName.ToLowerInvariant()); - - double? f1 = p1.HasValue ? Math.Min(Math.Max(p1.Value, 0), 100) / 100.0 : (double?)null; - double? f2 = p2.HasValue ? Math.Min(Math.Max(p2.Value, 0), 100) / 100.0 : (double?)null; - - if (f1 == null && f2 == null) { f1 = 0.5; f2 = 0.5; } - else if (f1 == null) f1 = 1.0 - f2; - else if (f2 == null) f2 = 1.0 - f1; - - var sum = f1.Value + f2.Value; - if (sum <= 0) return c1; - - var alphaMultiplier = sum < 1.0 ? sum : 1.0; - var t = f2.Value / sum; - return Color.Mix(c1, c2, t, space, hue, alphaMultiplier); - } } } diff --git a/Source/HtmlRenderer/Core/CssEngine/Extensions/PortableExtensions.cs b/Source/HtmlRenderer/Core/CssEngine/Extensions/PortableExtensions.cs index c37739b2a..17ab81f8a 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Extensions/PortableExtensions.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Extensions/PortableExtensions.cs @@ -1,3 +1,8 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; + #if !NET40 && !SL50 namespace TheArtOfDev.HtmlRenderer.Core.CssEngine @@ -8,7 +13,16 @@ public static string ConvertFromUtf32(this int utf32) { return char.ConvertFromUtf32(utf32); } + + public static PropertyInfo[] GetProperties( +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] +#endif + this Type type) + { + return type.GetRuntimeProperties().ToArray(); + } } } -#endif +#endif \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueConverterExtensions.cs b/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueConverterExtensions.cs index 441ef42e4..12ceaa8d1 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueConverterExtensions.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueConverterExtensions.cs @@ -74,7 +74,7 @@ public static IValueConverter FromList(this IValueConverter converter) return new ListValueConverter(converter); } - public static IValueConverter ToConverter(this IReadOnlyDictionary values) + public static IValueConverter ToConverter(this Dictionary values) { return new DictionaryValueConverter(values); } @@ -84,9 +84,10 @@ public static IValueConverter Periodic(this IValueConverter converter, params st return new PeriodicValueConverter(converter, labels); } - public static IValueConverter RequiresEnd(this IValueConverter listConverter, IValueConverter endConverter) + public static IValueConverter RequiresEnd(this IValueConverter listConverter, IValueConverter endConverter, + IReadOnlyDictionary layerDefaults = null, IReadOnlyDictionary layerFallbacks = null) { - return new EndListValueConverter(listConverter, endConverter); + return new EndListValueConverter(listConverter, endConverter, layerDefaults, layerFallbacks); } public static IValueConverter Required(this IValueConverter converter) @@ -132,6 +133,8 @@ public static IValueConverter OrNone(this IValueConverter primary) public static IValueConverter OrDefault(this IValueConverter primary) { + // inherit/initial/revert/revert-layer/unset are CSS-wide keywords, valid on every property + // (CSS Cascade 4 7.3, Cascade 5 for revert-layer). OrGlobalValue is the full set. return primary.OrGlobalValue(); } diff --git a/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueExtensions.cs b/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueExtensions.cs index 65126a5e2..00775786f 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueExtensions.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueExtensions.cs @@ -6,13 +6,6 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal static class ValueExtensions { - private static bool IsWeight(int value) - { - return value == 100 || value == 200 || value == 300 || value == 400 || - value == 500 || value == 600 || value == 700 || value == 800 || - value == 900; - } - public static Token OnlyOrDefault(this IEnumerable value) { var result = default(Token); @@ -42,7 +35,7 @@ public static string ToUri(this IEnumerable value) { var element = value.OnlyOrDefault(); - if (element != null && element.Type == TokenType.Url) return element.Data; + if (element is { Type: TokenType.Url }) return element.Data; return null; } @@ -86,7 +79,7 @@ public static Length ToLength(this FontSize fontSize) { var element = value.OnlyOrDefault(); - if (element != null && element.Type == TokenType.Percentage) + if (element is { Type: TokenType.Percentage }) return new Percent(((UnitToken)element).Value); return null; @@ -97,13 +90,13 @@ public static Length ToLength(this FontSize fontSize) var enumerable = value as Token[] ?? value.ToArray(); var percent = ToPercent(enumerable); - if (percent != null) + if (percent is not null) { return percent; } var element = value.OnlyOrDefault(); - if (!(element is NumberToken token)) + if (element is not NumberToken token) { return null; } @@ -125,13 +118,13 @@ public static Length ToLength(this FontSize fontSize) var enumerable = value as Token[] ?? value.ToArray(); var percent = ToPercent(enumerable); - if (percent != null) + if (percent is not null) { return new Number(percent.Value.Value, Number.Unit.Percent); } var element = value.OnlyOrDefault(); - if (!(element is NumberToken token)) + if (element is not NumberToken token) { return null; } @@ -150,7 +143,7 @@ public static string ToCssString(this IEnumerable value) { var element = value.OnlyOrDefault(); - if (element != null && element.Type == TokenType.String) return element.Data; + if (element is { Type: TokenType.String }) return element.Data; return null; } @@ -188,6 +181,34 @@ public static string ToIdentifier(this IEnumerable value) return null; } + /// + /// An animation-name: a custom identifier (never a CSS-wide keyword) or a quoted string, which + /// names the same animation as the identifier spelled the same way. Case is preserved. + /// + public static string ToAnimationName(this IEnumerable value) + { + var element = value.OnlyOrDefault(); + + if (element == null) return null; + + if (element.Type == TokenType.Ident) + { + var data = element.Data; + + return data.Isi(Keywords.Inherit) || data.Isi(Keywords.Initial) || data.Isi(Keywords.Unset) || + data.Isi(Keywords.Revert) || data.Isi(Keywords.RevertLayer) + ? null + : data; + } + + if (element.Type == TokenType.String) + { + return "\"" + element.Data.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; + } + + return null; + } + public static string ToIdentifierCaseInsensitive(this IEnumerable value) { var element = value.OnlyOrDefault(); @@ -214,7 +235,7 @@ public static string ToAnimatableIdentifier(this IEnumerable value) { var element = value.OnlyOrDefault(); - if (element != null && element.Type == TokenType.Number) return ((NumberToken)element).Value; + if (element != null && element.Type == TokenType.Number) return ((NumberToken) element).Value; return null; } @@ -235,9 +256,25 @@ public static string ToAnimatableIdentifier(this IEnumerable value) { var element = value.OnlyOrDefault(); - if (element != null && element.Type == TokenType.Number && ((NumberToken)element).IsInteger) + if (element != null && element.Type == TokenType.Number && ((NumberToken) element).IsInteger) { - return ((NumberToken)element).IntegerValue; + return ((NumberToken) element).IntegerValue; + } + + // CSS Values 4 10: a calc() whose type is is also valid where an is + // expected, and its result is rounded to the nearest integer (halves away from zero, 10.9). + if (element is FunctionToken function && CalcParser.IsCalcFamily(function.Data)) + { + var node = CalcParser.Parse(function); + if (node == null || CalcTypeChecker.Check(node) != CalcCategory.Number) return null; + + var number = CalcSerializer.EvaluateNumber(node); + if (number == null) return null; + + var rounded = Math.Round(number.Value, MidpointRounding.AwayFromZero); + if (double.IsNaN(rounded) || rounded < int.MinValue || rounded > int.MaxValue) return null; + + return (int) rounded; } return null; @@ -255,10 +292,11 @@ public static string ToAnimatableIdentifier(this IEnumerable value) return element > 0 ? element : null; } - public static int? ToWeightInteger(this IEnumerable value) + // CSS Fonts 4: font-weight accepts any , fractions included. + public static double? ToWeightNumber(this IEnumerable value) { - var element = value.ToPositiveInteger(); - return element.HasValue && IsWeight(element.Value) ? element : null; + var element = value.ToSingle(); + return element >= 1f && element <= 1000f ? element : null; } public static int? ToBinary(this IEnumerable value) @@ -284,13 +322,13 @@ public static string ToAnimatableIdentifier(this IEnumerable value) var enumerable = value as Token[] ?? value.ToArray(); var element = enumerable.ToNaturalInteger(); - if (element.HasValue) return (byte)Math.Min(element.Value, 255); + if (element.HasValue) return (byte) Math.Min(element.Value, 255); var percent = enumerable.ToPercent(); if (!percent.HasValue) return null; - return (byte)(255f * percent.Value.NormalizedValue); + return (byte) (255f * percent.Value.NormalizedValue); } public static Angle? ToAngle(this IEnumerable value) @@ -299,7 +337,7 @@ public static string ToAnimatableIdentifier(this IEnumerable value) if (element == null || element.Type != TokenType.Dimension) return null; - var token = (UnitToken)element; + var token = (UnitToken) element; var unit = Angle.GetUnit(token.Unit); if (unit != Angle.Unit.None) return new Angle(token.Value, unit); @@ -307,6 +345,25 @@ public static string ToAnimatableIdentifier(this IEnumerable value) return null; } + /// + /// [ <angle> | <zero> ] - the argument grammar of the CSS Transforms rotate and skew + /// functions, which keep the legacy unitless-zero exception that the general <angle> + /// grammar () does not allow. + /// + public static Angle? ToAngleOrZero(this IEnumerable value) + { + var tokens = value as Token[] ?? value.ToArray(); + var angle = tokens.ToAngle(); + + if (angle.HasValue) return angle; + + var element = tokens.OnlyOrDefault(); + + return element is { Type: TokenType.Number } && ((NumberToken) element).Value == 0f + ? new Angle(0f, Angle.Unit.Deg) + : null; + } + public static Angle? ToAngleNumber(this IEnumerable value) { var enumerable = value as Token[] ?? value.ToArray(); @@ -330,14 +387,14 @@ public static string ToAnimatableIdentifier(this IEnumerable value) switch (element.Type) { case TokenType.Dimension: - { - var token = (UnitToken)element; - var unit = Length.GetUnit(token.Unit); - - if (unit != Length.Unit.None) return new Length(token.Value, unit); - break; - } - case TokenType.Number when ((NumberToken)element).Value == 0f: + { + var token = (UnitToken) element; + var unit = Length.GetUnit(token.Unit); + + if (unit != Length.Unit.None) return new Length(token.Value, unit); + break; + } + case TokenType.Number when ((NumberToken) element).Value == 0f: return Length.Zero; } } @@ -351,7 +408,7 @@ public static string ToAnimatableIdentifier(this IEnumerable value) if (element == null || element.Type != TokenType.Dimension) return null; - var token = (UnitToken)element; + var token = (UnitToken) element; var unit = Resolution.GetUnit(token.Unit); if (unit != Resolution.Unit.None) return new Resolution(token.Value, unit); @@ -365,7 +422,7 @@ public static string ToAnimatableIdentifier(this IEnumerable value) if (element == null || element.Type != TokenType.Dimension) return null; - var token = (UnitToken)element; + var token = (UnitToken) element; var unit = Time.GetUnit(token.Unit); if (unit != Time.Unit.None) return new Time(token.Value, unit); @@ -483,6 +540,8 @@ public static string ToText(this IEnumerable value) return string.Join(string.Empty, value.Select(m => m.ToValue())); } + // Whether the value contains a call to the named function anywhere, including nested inside another + // function's arguments - e.g. var() inside calc(var(--x) + 1px). public static bool ContainsFunction(this IEnumerable tokens, string functionName) { foreach (var token in tokens) @@ -503,7 +562,7 @@ public static bool ContainsFunction(this IEnumerable tokens, string funct if (element != null && element.Type == TokenType.Ident) return Color.FromName(element.Data); - if (element != null && element.Type == TokenType.Color && !((ColorToken)element).IsValid) + if (element != null && element.Type == TokenType.Color && !((ColorToken) element).IsValid) { return Color.FromHex(element.Data); } diff --git a/Source/HtmlRenderer/Core/CssEngine/Factories/AttributeSelectorFactory.cs b/Source/HtmlRenderer/Core/CssEngine/Factories/AttributeSelectorFactory.cs index dc067c397..8d4d0217b 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Factories/AttributeSelectorFactory.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Factories/AttributeSelectorFactory.cs @@ -4,7 +4,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class AttributeSelectorFactory { - private static readonly Lazy Lazy = new Lazy(() => new AttributeSelectorFactory()); + private static readonly Lazy Lazy = new(() => new AttributeSelectorFactory()); private AttributeSelectorFactory() { @@ -13,6 +13,12 @@ private AttributeSelectorFactory() internal static AttributeSelectorFactory Instance => Lazy.Value; public IAttrSelector Create(string combinator, string match, string value, string prefix) + { + return Create(combinator, match, value, prefix, AttrCaseSensitivity.Default); + } + + public IAttrSelector Create(string combinator, string match, string value, string prefix, + AttrCaseSensitivity caseSensitivity) { var name = match; @@ -22,20 +28,21 @@ public IAttrSelector Create(string combinator, string match, string value, strin _ = AttributeSelectorFactory.FormMatch(prefix, match); } - // A reflection-free dispatch (no Activator.CreateInstance) so the two-arg Attr*Selector - // constructors are statically reachable and survive trimming/AOT (IsTrimmable=true) - see - // upstream ExCSS commit c497ca7. Unknown combinators fall back to a presence selector. - switch (combinator) - { - case Combinators.Exactly: return new AttrMatchSelector(name, value); - case Combinators.InList: return new AttrListSelector(name, value); - case Combinators.InToken: return new AttrHyphenSelector(name, value); - case Combinators.Begins: return new AttrBeginsSelector(name, value); - case Combinators.Ends: return new AttrEndsSelector(name, value); - case Combinators.InText: return new AttrContainsSelector(name, value); - case Combinators.Unlike: return new AttrNotMatchSelector(name, value); - default: return new AttrAvailableSelector(name, value); - } + if (combinator == Combinators.Exactly) + return new AttrMatchSelector(name, value, caseSensitivity); + if (combinator == Combinators.InList) + return new AttrListSelector(name, value, caseSensitivity); + if (combinator == Combinators.InToken) + return new AttrHyphenSelector(name, value, caseSensitivity); + if (combinator == Combinators.Begins) + return new AttrBeginsSelector(name, value, caseSensitivity); + if (combinator == Combinators.Ends) + return new AttrEndsSelector(name, value, caseSensitivity); + if (combinator == Combinators.InText) + return new AttrContainsSelector(name, value, caseSensitivity); + if (combinator == Combinators.Unlike) + return new AttrNotMatchSelector(name, value, caseSensitivity); + return new AttrAvailableSelector(name, value); } private static string FormFront(string prefix, string match) diff --git a/Source/HtmlRenderer/Core/CssEngine/Factories/LegacyGradientSyntax.cs b/Source/HtmlRenderer/Core/CssEngine/Factories/LegacyGradientSyntax.cs new file mode 100644 index 000000000..fe0a441f0 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Factories/LegacyGradientSyntax.cs @@ -0,0 +1,380 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Rewrites the pre-standard vendor gradient functions (-webkit-linear-gradient(), + /// -moz-radial-gradient(), and their repeating- forms; -o- and -ms- spellings too) + /// into the standard linear-gradient()/radial-gradient() token stream, so the value converters + /// only ever see standard syntax. Only applied when is on. + /// + /// + /// Not a pure rename, the prefixed grammar differs from the standard one: + /// + /// + /// Linear: an angle is measured counter-clockwise from the east axis (standard: clockwise from north), + /// so standard = 90deg - legacy; a side/corner keyword names where the line starts + /// (top = the standard to bottom), so each keyword is flipped to its opposite; with neither, + /// both default to top-to-bottom. + /// + /// + /// Radial: the center position comes first (standard: after at), the size keywords are + /// cover/contain (standard farthest-corner/closest-side). + /// + /// + /// A function whose first group already uses the standard to <side> form passes through unchanged. + /// -webkit-gradient() (a different function with a different grammar) is not handled. + /// + internal static class LegacyGradientSyntax + { + private static readonly string[] Prefixes = {"-webkit-", "-moz-", "-o-", "-ms-"}; + + private static readonly string[] StandardNames = + { + FunctionNames.LinearGradient, FunctionNames.RepeatingLinearGradient, + FunctionNames.RadialGradient, FunctionNames.RepeatingRadialGradient + }; + + /// + /// Returns with every legacy gradient function (at any nesting depth) rewritten + /// to standard syntax, or itself when it contains none. + /// + internal static TokenValue Rewrite(TokenValue value) + { + if (value == null) return null; + + List tokens = null; + for (var i = 0; i < value.Count; i++) + { + var token = value[i]; + var rewritten = Rewrite(token); + + if (ReferenceEquals(rewritten, token)) + { + tokens?.Add(token); + continue; + } + + if (tokens == null) + { + tokens = new List(value.Count); + for (var j = 0; j < i; j++) tokens.Add(value[j]); + } + + tokens.Add(rewritten); + } + + return tokens == null ? value : new TokenValue(tokens) {StylesheetText = value.StylesheetText}; + } + + /// + /// Returns rewritten to standard syntax if it is (or contains) a legacy gradient + /// function; otherwise the same instance. + /// + internal static Token Rewrite(Token token) + { + if (!(token is FunctionToken function)) return token; + + // Rewrite nested functions first (a gradient inside image-set(), cross-fade(), ...). + var arguments = new List(); + var changed = false; + foreach (var argument in function) + { + var rewrittenArgument = Rewrite(argument); + changed |= !ReferenceEquals(rewrittenArgument, argument); + arguments.Add(rewrittenArgument); + } + + if (TryGetStandardName(function.Data, out var standardName, out var isLinear)) + { + var groups = SplitGroups(Without(arguments)); + if (groups.Count > 0) groups = isLinear ? RewriteLinear(groups) : RewriteRadial(groups); + + var rewritten = new FunctionToken(standardName, function.Position); + for (var i = 0; i < groups.Count; i++) + { + if (i > 0) + { + rewritten.AddArgumentToken(Token.Comma); + rewritten.AddArgumentToken(Token.Whitespace); + } + + foreach (var t in groups[i]) rewritten.AddArgumentToken(t); + } + + rewritten.AddArgumentToken(new Token(TokenType.RoundBracketClose, ")", function.Position)); + return rewritten; + } + + if (!changed) return token; + + var copy = new FunctionToken(function.Data, function.Position); + foreach (var argument in arguments) copy.AddArgumentToken(argument); + return copy; + } + + // The arguments without the function's own closing parenthesis. + private static List Without(List arguments) + { + var result = new List(arguments); + if (result.Count > 0 && result[result.Count - 1].Type == TokenType.RoundBracketClose) + result.RemoveAt(result.Count - 1); + return result; + } + + private static bool TryGetStandardName(string name, out string standardName, out bool isLinear) + { + standardName = string.Empty; + isLinear = false; + + if (name.Length < 2 || name[0] != '-') return false; + + foreach (var prefix in Prefixes) + { + if (!name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) continue; + + var rest = name.Substring(prefix.Length); + foreach (var candidate in StandardNames) + { + if (!rest.Equals(candidate, StringComparison.OrdinalIgnoreCase)) continue; + + standardName = candidate; + isLinear = candidate.IndexOf("linear", StringComparison.Ordinal) >= 0; + return true; + } + + return false; + } + + return false; + } + + // Splits on top-level commas (nested functions are self-contained tokens, so a flat split is top-level), + // trimming surrounding whitespace from each group. + private static List> SplitGroups(IEnumerable tokens) + { + var groups = new List> {new List()}; + + foreach (var token in tokens) + { + if (token.Type == TokenType.Comma) groups.Add(new List()); + else groups[groups.Count - 1].Add(token); + } + + foreach (var group in groups) + { + while (group.Count > 0 && group[0].Type == TokenType.Whitespace) group.RemoveAt(0); + while (group.Count > 0 && group[group.Count - 1].Type == TokenType.Whitespace) + group.RemoveAt(group.Count - 1); + } + + return groups; + } + + private static List> RewriteLinear(List> groups) + { + var first = groups[0]; + + if (TryGetAngleDegrees(first, out var legacyDegrees)) + { + var standard = (90 - legacyDegrees) % 360; + if (standard < 0) standard += 360; + + groups[0] = new List + { + new UnitToken(TokenType.Dimension, + standard.ToString("0.####", CultureInfo.InvariantCulture), "deg", first[0].Position) + }; + } + else if (TryFlipSideKeywords(first, out var flipped)) + { + groups[0] = flipped; + } + + // Otherwise no direction group was written (or it is already standard `to ...`): the legacy default, + // `top`, is the standard default, `to bottom`, so the groups pass through untouched. + return groups; + } + + private static bool TryGetAngleDegrees(List group, out double degrees) + { + degrees = 0; + if (group.Count != 1) return false; + + var token = group[0]; + if (token is NumberToken number) return number.Value == 0; + if (!(token is UnitToken unit) || unit.Type != TokenType.Dimension) return false; + + double value = unit.Value; + if (unit.Unit.Isi("deg")) degrees = value; + else if (unit.Unit.Isi("grad")) degrees = value * 0.9; + else if (unit.Unit.Isi("rad")) degrees = value * 180 / Math.PI; + else if (unit.Unit.Isi("turn")) degrees = value * 360; + else return false; + + return true; + } + + // `top`/`left`/`bottom`/`right` (one, or a vertical+horizontal pair) -> the standard `to `. + private static bool TryGetSideKeyword(Token token, out string opposite, out bool vertical) + { + opposite = string.Empty; + vertical = false; + if (token.Type != TokenType.Ident) return false; + + if (token.Data.Isi("top")) + { + opposite = "bottom"; + vertical = true; + } + else if (token.Data.Isi("bottom")) + { + opposite = "top"; + vertical = true; + } + else if (token.Data.Isi("left")) opposite = "right"; + else if (token.Data.Isi("right")) opposite = "left"; + else return false; + + return true; + } + + private static bool TryFlipSideKeywords(List group, out List flipped) + { + flipped = new List(); + string vert = null, horiz = null; + + foreach (var token in group) + { + if (token.Type == TokenType.Whitespace) continue; + if (!TryGetSideKeyword(token, out var opposite, out var vertical)) return false; + + if (vertical) + { + if (vert != null) return false; + vert = opposite; + } + else + { + if (horiz != null) return false; + horiz = opposite; + } + } + + if (vert == null && horiz == null) return false; + + var position = group[0].Position; + flipped.Add(Ident("to", position)); + if (vert != null) + { + flipped.Add(Token.Whitespace); + flipped.Add(Ident(vert, position)); + } + + if (horiz != null) + { + flipped.Add(Token.Whitespace); + flipped.Add(Ident(horiz, position)); + } + + return true; + } + + private static Token Ident(string text, TextPosition position) + { + return new KeywordToken(TokenType.Ident, text, position); + } + + private static bool IsShapeOrSizeKeyword(string s) + { + return s.Isi("circle") || s.Isi("ellipse") || s.Isi("cover") || s.Isi("contain") || + s.Isi("closest-side") || s.Isi("closest-corner") || s.Isi("farthest-side") || + s.Isi("farthest-corner"); + } + + private static bool IsPositionKeyword(string s) + { + return s.Isi("left") || s.Isi("right") || s.Isi("top") || s.Isi("bottom") || s.Isi("center"); + } + + // A header group has no color, so it can only hold position/shape/size keywords and numeric lengths - + // which is what tells it apart from a color stop (every stop carries a color). + private static bool IsHeaderGroup(List group) + { + if (group.Count == 0) return false; + + foreach (var token in group) + { + switch (token.Type) + { + case TokenType.Whitespace: + case TokenType.Dimension: + case TokenType.Percentage: + case TokenType.Number: + break; + case TokenType.Ident when IsShapeOrSizeKeyword(token.Data) || IsPositionKeyword(token.Data): + break; + default: + return false; + } + } + + return true; + } + + private static bool HasShapeOrSize(List group) + { + foreach (var token in group) + { + if (token.Type == TokenType.Ident && IsShapeOrSizeKeyword(token.Data)) return true; + } + + return false; + } + + private static List> RewriteRadial(List> groups) + { + // Legacy header: [,]? [ || ,]? then the color stops. + var headerCount = 0; + while (headerCount < 2 && headerCount < groups.Count && IsHeaderGroup(groups[headerCount])) headerCount++; + + if (headerCount == 0) return groups; + + List position = null, shapeSize = null; + if (headerCount == 2) + { + position = groups[0]; + shapeSize = groups[1]; + } + else if (HasShapeOrSize(groups[0])) shapeSize = groups[0]; + else position = groups[0]; + + var header = new List(); + if (shapeSize != null) + { + foreach (var token in shapeSize) + { + if (token.Type == TokenType.Ident && token.Data.Isi("cover")) + header.Add(Ident("farthest-corner", token.Position)); + else if (token.Type == TokenType.Ident && token.Data.Isi("contain")) + header.Add(Ident("closest-side", token.Position)); + else header.Add(token); + } + } + + if (position != null) + { + if (header.Count > 0) header.Add(Token.Whitespace); + header.Add(Ident("at", position[0].Position)); + header.Add(Token.Whitespace); + header.AddRange(position); + } + + var result = new List> {header}; + for (var i = headerCount; i < groups.Count; i++) result.Add(groups[i]); + return result; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Factories/MediaFeatureFactory.cs b/Source/HtmlRenderer/Core/CssEngine/Factories/MediaFeatureFactory.cs index 7464800de..8a9e88c96 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Factories/MediaFeatureFactory.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Factories/MediaFeatureFactory.cs @@ -5,12 +5,12 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class MediaFeatureFactory { - private static readonly Lazy Lazy = new Lazy(() => new MediaFeatureFactory()); + private static readonly Lazy Lazy = new (() => new MediaFeatureFactory()); #region Creators private readonly Dictionary _creators = - new Dictionary(StringComparer.OrdinalIgnoreCase) + new (StringComparer.OrdinalIgnoreCase) { {FeatureNames.MinWidth, () => new WidthMediaFeature(FeatureNames.MinWidth)}, {FeatureNames.MaxWidth, () => new WidthMediaFeature(FeatureNames.MaxWidth)}, @@ -70,6 +70,13 @@ internal sealed class MediaFeatureFactory {FeatureNames.Update, () => new UpdateMediaFeature()}, {FeatureNames.AnyHover, () => new AnyHoverMediaFeature()}, {FeatureNames.AnyPointer, () => new AnyPointerMediaFeature()}, + {FeatureNames.ForcedColors, () => new ForcedColorsMediaFeature()}, + {FeatureNames.PrefersReducedData, () => new PrefersReducedDataMediaFeature()}, + {FeatureNames.InvertedColors, () => new InvertedColorsMediaFeature()}, + {FeatureNames.DynamicRange, () => new DynamicRangeMediaFeature()}, + {FeatureNames.VideoDynamicRange, () => new VideoDynamicRangeMediaFeature()}, + {FeatureNames.OverflowBlock, () => new OverflowBlockMediaFeature()}, + {FeatureNames.OverflowInline, () => new OverflowInlineMediaFeature()}, }; #endregion diff --git a/Source/HtmlRenderer/Core/CssEngine/Factories/PropertyFactory.cs b/Source/HtmlRenderer/Core/CssEngine/Factories/PropertyFactory.cs index fd5b48a17..dcd59f9c7 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Factories/PropertyFactory.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Factories/PropertyFactory.cs @@ -6,39 +6,38 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class PropertyFactory { - private static readonly Lazy Lazy = new Lazy(() => new PropertyFactory()); + private static readonly Lazy Lazy = new(() => new PropertyFactory()); - private readonly List _animatables = new List(); + private readonly List _animatables = new(); - private readonly Dictionary _fontsBuilder = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _fonts = new(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary _longhandsBuilder = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _longhands = new(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary _mappingsBuilder = new Dictionary(); + private readonly Dictionary _mappings = new(); - private readonly Dictionary _shorthandsBuilder = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _shorthands = new(StringComparer.OrdinalIgnoreCase); - // Shorthands that parse and expand but are never rebuilt from their longhands when serializing: - // the flow-relative ones, whose longhands are the physical ones, and grid/grid-template. - private readonly HashSet _logicalShorthands = new HashSet(StringComparer.OrdinalIgnoreCase); + // Shorthands that parse and expand like any other, but are NOT used to reconstruct a shorthand when + // serializing a declaration block: the grid mega-shorthands (grid, grid-template), whose multi-slash / + // areas grammar isn't worth reconstructing. Excluded from GetShorthands (the serialization query) only; + // CreateShorthand/GetLonghands/IsShorthand still work. + private readonly HashSet _logicalShorthands = new(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary _fonts; - - // @property descriptors, stored raw, nothing reads them yet - private readonly Dictionary _propertyDescriptors; - - // @font-palette-values descriptors (font-family / base-palette / override-colors). Stored raw via - // UnknownProperty and validated later when the palette is resolved. - private readonly Dictionary _fontPaletteDescriptors; - - private readonly Dictionary _longhands; - - private readonly Dictionary _mappings; - - private readonly Dictionary _shorthands; + // The `all` shorthand's longhands (CSS Cascade 4 3.2): every registered longhand except direction and + // unicode-bidi. Aliases (e.g. word-wrap, whose creator yields overflow-wrap) are skipped so no property + // is expanded twice. Lazy so it runs after the constructor has registered everything. + private readonly Lazy _allLonghands; private PropertyFactory() { + AddLonghand(PropertyNames.AccentColor, () => new AccentColorProperty(), true); + _allLonghands = new Lazy(GetAllLonghands); + + // `all` is registered without a static longhand mapping: its list is every registered longhand and is + // built lazily (see GetAllLonghands), so properties added anywhere else are covered automatically. + _shorthands.Add(PropertyNames.All, () => new AllProperty()); + AddLonghand(PropertyNames.AlignContent, () => new AlignContentProperty()); AddLonghand(PropertyNames.AlignItems, () => new AlignItemsProperty()); AddLonghand(PropertyNames.AlignSelf, () => new AlignSelfProperty()); @@ -50,7 +49,12 @@ private PropertyFactory() PropertyNames.AnimationDirection, PropertyNames.AnimationFillMode, PropertyNames.AnimationIterationCount, - PropertyNames.AnimationPlayState); + PropertyNames.AnimationPlayState, + PropertyNames.AnimationTimeline, + PropertyNames.AnimationComposition, + PropertyNames.AnimationRangeStart, + PropertyNames.AnimationRangeEnd); + AddLonghand(PropertyNames.AnimationComposition, () => new AnimationCompositionProperty()); AddLonghand(PropertyNames.AnimationDelay, () => new AnimationDelayProperty()); AddLonghand(PropertyNames.AnimationDirection, () => new AnimationDirectionProperty()); AddLonghand(PropertyNames.AnimationDuration, () => new AnimationDurationProperty()); @@ -58,6 +62,12 @@ private PropertyFactory() AddLonghand(PropertyNames.AnimationIterationCount, () => new AnimationIterationCountProperty()); AddLonghand(PropertyNames.AnimationName, () => new AnimationNameProperty()); AddLonghand(PropertyNames.AnimationPlayState, () => new AnimationPlayStateProperty()); + AddShorthand(PropertyNames.AnimationRange, () => new AnimationRangeProperty(), + PropertyNames.AnimationRangeStart, + PropertyNames.AnimationRangeEnd); + AddLonghand(PropertyNames.AnimationRangeEnd, () => new AnimationRangeEndProperty()); + AddLonghand(PropertyNames.AnimationRangeStart, () => new AnimationRangeStartProperty()); + AddLonghand(PropertyNames.AnimationTimeline, () => new AnimationTimelineProperty()); AddLonghand(PropertyNames.AnimationTimingFunction, () => new AnimationTimingFunctionProperty()); AddShorthand(PropertyNames.Background, () => new BackgroundProperty(), @@ -70,6 +80,7 @@ private PropertyFactory() PropertyNames.BackgroundRepeat, PropertyNames.BackgroundSize); AddLonghand(PropertyNames.BackgroundAttachment, () => new BackgroundAttachmentProperty()); + AddLonghand(PropertyNames.BackgroundBlendMode, () => new BackgroundBlendModeProperty()); AddLonghand(PropertyNames.BackgroundColor, () => new BackgroundColorProperty(), true); AddLonghand(PropertyNames.BackgroundClip, () => new BackgroundClipProperty()); AddLonghand(PropertyNames.BackgroundOrigin, () => new BackgroundOriginProperty()); @@ -77,19 +88,39 @@ private PropertyFactory() AddLonghand(PropertyNames.BackgroundImage, () => new BackgroundImageProperty()); AddLonghand(PropertyNames.BackgroundPosition, () => new BackgroundPositionProperty(), true); AddLonghand(PropertyNames.BackgroundRepeat, () => new BackgroundRepeatProperty()); + AddLonghand(PropertyNames.MaskImage, () => new MaskImageProperty()); + AddLonghand(PropertyNames.MaskMode, () => new MaskModeProperty()); + AddLonghand(PropertyNames.MaskRepeat, () => new MaskRepeatProperty()); + AddLonghand(PropertyNames.MaskPosition, () => new MaskPositionProperty(), true); + AddLonghand(PropertyNames.MaskSize, () => new MaskSizeProperty(), true); + AddLonghand(PropertyNames.MaskOrigin, () => new MaskOriginProperty()); + AddLonghand(PropertyNames.MaskClip, () => new MaskClipProperty()); + AddLonghand(PropertyNames.MaskComposite, () => new MaskCompositeProperty()); AddLonghand(PropertyNames.BorderSpacing, () => new BorderSpacingProperty()); AddLonghand(PropertyNames.BorderCollapse, () => new BorderCollapseProperty()); AddLonghand(PropertyNames.BoxSizing, () => new BoxSizingProperty()); - AddLonghand(PropertyNames.BoxShadow, () => new BoxShadowProperty(), true); AddLonghand(PropertyNames.AspectRatio, () => new AspectRatioProperty()); - AddLonghand(PropertyNames.ClipPath, () => new ClipPathProperty(), true); + AddLonghand(PropertyNames.ImageOrientation, () => new ImageOrientationProperty()); + AddLonghand(PropertyNames.ImageResolution, () => new ImageResolutionProperty()); + AddLonghand(PropertyNames.BoxShadow, () => new BoxShadowProperty(), true); AddLonghand(PropertyNames.BoxDecorationBreak, () => new BoxDecorationBreak()); AddLonghand(PropertyNames.BreakAfter, () => new BreakAfterProperty()); AddLonghand(PropertyNames.BreakBefore, () => new BreakBeforeProperty()); AddLonghand(PropertyNames.BreakInside, () => new BreakInsideProperty()); AddLonghand(PropertyNames.BackfaceVisibility, () => new BackfaceVisibilityProperty()); + AddShorthand(PropertyNames.BlockStep, () => new BlockStepProperty(), + PropertyNames.BlockStepSize, + PropertyNames.BlockStepInsert, + PropertyNames.BlockStepAlign, + PropertyNames.BlockStepRound); + AddLonghand(PropertyNames.BlockStepSize, () => new BlockStepSizeProperty()); + AddLonghand(PropertyNames.BlockStepInsert, () => new BlockStepInsertProperty()); + AddLonghand(PropertyNames.BlockStepAlign, () => new BlockStepAlignProperty()); + AddLonghand(PropertyNames.BlockStepRound, () => new BlockStepRoundProperty()); + AddLonghand(PropertyNames.BlockEllipsis, () => new BlockEllipsisProperty()); + AddShorthand(PropertyNames.BorderRadius, () => new BorderRadiusProperty(), PropertyNames.BorderTopLeftRadius, PropertyNames.BorderTopRightRadius, @@ -100,6 +131,21 @@ private PropertyFactory() AddLonghand(PropertyNames.BorderBottomLeftRadius, () => new BorderBottomLeftRadiusProperty(), true); AddLonghand(PropertyNames.BorderBottomRightRadius, () => new BorderBottomRightRadiusProperty(), true); + // CSS Borders 4: the corner-*-shape longhands, and the corner shorthands (corner-shape, the + // two-corner border-*-radius / corner-*-shape shorthands, and the corner-* shorthands that set a + // corner's shape and radius together). Like the logical shorthands they are registered with + // AddLogicalShorthand, so existing border-*-radius longhands never serialize into them. + AddLonghand(PropertyNames.CornerTopLeftShape, () => new CornerTopLeftShapeProperty()); + AddLonghand(PropertyNames.CornerTopRightShape, () => new CornerTopRightShapeProperty()); + AddLonghand(PropertyNames.CornerBottomRightShape, () => new CornerBottomRightShapeProperty()); + AddLonghand(PropertyNames.CornerBottomLeftShape, () => new CornerBottomLeftShapeProperty()); + AddLonghand(PropertyNames.CornerStartStartShape, () => new CornerStartStartShapeProperty()); + AddLonghand(PropertyNames.CornerStartEndShape, () => new CornerStartEndShapeProperty()); + AddLonghand(PropertyNames.CornerEndStartShape, () => new CornerEndStartShapeProperty()); + AddLonghand(PropertyNames.CornerEndEndShape, () => new CornerEndEndShapeProperty()); + CornerShorthands.Register((name, converter, longhands) => + AddLogicalShorthand(name, () => new CornerShorthandProperty(name, converter), longhands)); + AddShorthand(PropertyNames.BorderImage, () => new BorderImageProperty(), PropertyNames.BorderImageOutset, PropertyNames.BorderImageRepeat, @@ -172,9 +218,116 @@ private PropertyFactory() AddLonghand(PropertyNames.Bottom, () => new BottomProperty(), true); + // CSS Logical Properties and Values Level 1. Flow-relative properties are kept as their own + // longhands (ExCSS does no layout, so they are never resolved to a physical edge). The logical + // shorthands expand to them, but must not make the physical longhands serialize back into a + // logical shorthand, hence AddLogicalShorthand. `inset` is a purely physical shorthand + // (top/right/bottom/left) that is likewise kept out of serialization. + AddLogicalShorthand(PropertyNames.MarginBlock, () => new MarginBlockProperty(), + PropertyNames.MarginBlockStart, + PropertyNames.MarginBlockEnd); + AddLonghand(PropertyNames.MarginBlockStart, () => new MarginBlockStartProperty(), true); + AddLonghand(PropertyNames.MarginBlockEnd, () => new MarginBlockEndProperty(), true); + AddLogicalShorthand(PropertyNames.PaddingBlock, () => new PaddingBlockProperty(), + PropertyNames.PaddingBlockStart, + PropertyNames.PaddingBlockEnd); + AddLonghand(PropertyNames.PaddingBlockStart, () => new PaddingBlockStartProperty(), true); + AddLonghand(PropertyNames.PaddingBlockEnd, () => new PaddingBlockEndProperty(), true); + AddLogicalShorthand(PropertyNames.InsetBlock, () => new InsetBlockProperty(), + PropertyNames.InsetBlockStart, + PropertyNames.InsetBlockEnd); + AddLonghand(PropertyNames.InsetBlockStart, () => new InsetBlockStartProperty(), true); + AddLonghand(PropertyNames.InsetBlockEnd, () => new InsetBlockEndProperty(), true); + AddLogicalShorthand(PropertyNames.MarginInline, () => new MarginInlineProperty(), + PropertyNames.MarginInlineStart, + PropertyNames.MarginInlineEnd); + AddLonghand(PropertyNames.MarginInlineStart, () => new MarginInlineStartProperty(), true); + AddLonghand(PropertyNames.MarginInlineEnd, () => new MarginInlineEndProperty(), true); + AddLogicalShorthand(PropertyNames.PaddingInline, () => new PaddingInlineProperty(), + PropertyNames.PaddingInlineStart, + PropertyNames.PaddingInlineEnd); + AddLonghand(PropertyNames.PaddingInlineStart, () => new PaddingInlineStartProperty(), true); + AddLonghand(PropertyNames.PaddingInlineEnd, () => new PaddingInlineEndProperty(), true); + AddLogicalShorthand(PropertyNames.InsetInline, () => new InsetInlineProperty(), + PropertyNames.InsetInlineStart, + PropertyNames.InsetInlineEnd); + AddLonghand(PropertyNames.InsetInlineStart, () => new InsetInlineStartProperty(), true); + AddLonghand(PropertyNames.InsetInlineEnd, () => new InsetInlineEndProperty(), true); + AddLogicalShorthand(PropertyNames.Inset, () => new InsetProperty(), + PropertyNames.Top, + PropertyNames.Right, + PropertyNames.Bottom, + PropertyNames.Left); + AddLogicalShorthand(PropertyNames.BorderBlockStart, () => new BorderBlockStartProperty(), + PropertyNames.BorderBlockStartWidth, + PropertyNames.BorderBlockStartStyle, + PropertyNames.BorderBlockStartColor); + AddLogicalShorthand(PropertyNames.BorderBlockEnd, () => new BorderBlockEndProperty(), + PropertyNames.BorderBlockEndWidth, + PropertyNames.BorderBlockEndStyle, + PropertyNames.BorderBlockEndColor); + AddLogicalShorthand(PropertyNames.BorderBlock, () => new BorderBlockProperty(), + PropertyNames.BorderBlockStartWidth, + PropertyNames.BorderBlockStartStyle, + PropertyNames.BorderBlockStartColor, + PropertyNames.BorderBlockEndWidth, + PropertyNames.BorderBlockEndStyle, + PropertyNames.BorderBlockEndColor); + AddLogicalShorthand(PropertyNames.BorderBlockWidth, () => new BorderBlockWidthProperty(), + PropertyNames.BorderBlockStartWidth, + PropertyNames.BorderBlockEndWidth); + AddLonghand(PropertyNames.BorderBlockStartWidth, () => new BorderBlockStartWidthProperty(), true); + AddLonghand(PropertyNames.BorderBlockEndWidth, () => new BorderBlockEndWidthProperty(), true); + AddLogicalShorthand(PropertyNames.BorderBlockStyle, () => new BorderBlockStyleProperty(), + PropertyNames.BorderBlockStartStyle, + PropertyNames.BorderBlockEndStyle); + AddLonghand(PropertyNames.BorderBlockStartStyle, () => new BorderBlockStartStyleProperty()); + AddLonghand(PropertyNames.BorderBlockEndStyle, () => new BorderBlockEndStyleProperty()); + AddLogicalShorthand(PropertyNames.BorderBlockColor, () => new BorderBlockColorProperty(), + PropertyNames.BorderBlockStartColor, + PropertyNames.BorderBlockEndColor); + AddLonghand(PropertyNames.BorderBlockStartColor, () => new BorderBlockStartColorProperty(), true); + AddLonghand(PropertyNames.BorderBlockEndColor, () => new BorderBlockEndColorProperty(), true); + AddLogicalShorthand(PropertyNames.BorderInlineStart, () => new BorderInlineStartProperty(), + PropertyNames.BorderInlineStartWidth, + PropertyNames.BorderInlineStartStyle, + PropertyNames.BorderInlineStartColor); + AddLogicalShorthand(PropertyNames.BorderInlineEnd, () => new BorderInlineEndProperty(), + PropertyNames.BorderInlineEndWidth, + PropertyNames.BorderInlineEndStyle, + PropertyNames.BorderInlineEndColor); + AddLogicalShorthand(PropertyNames.BorderInline, () => new BorderInlineProperty(), + PropertyNames.BorderInlineStartWidth, + PropertyNames.BorderInlineStartStyle, + PropertyNames.BorderInlineStartColor, + PropertyNames.BorderInlineEndWidth, + PropertyNames.BorderInlineEndStyle, + PropertyNames.BorderInlineEndColor); + AddLogicalShorthand(PropertyNames.BorderInlineWidth, () => new BorderInlineWidthProperty(), + PropertyNames.BorderInlineStartWidth, + PropertyNames.BorderInlineEndWidth); + AddLonghand(PropertyNames.BorderInlineStartWidth, () => new BorderInlineStartWidthProperty(), true); + AddLonghand(PropertyNames.BorderInlineEndWidth, () => new BorderInlineEndWidthProperty(), true); + AddLogicalShorthand(PropertyNames.BorderInlineStyle, () => new BorderInlineStyleProperty(), + PropertyNames.BorderInlineStartStyle, + PropertyNames.BorderInlineEndStyle); + AddLonghand(PropertyNames.BorderInlineStartStyle, () => new BorderInlineStartStyleProperty()); + AddLonghand(PropertyNames.BorderInlineEndStyle, () => new BorderInlineEndStyleProperty()); + AddLogicalShorthand(PropertyNames.BorderInlineColor, () => new BorderInlineColorProperty(), + PropertyNames.BorderInlineStartColor, + PropertyNames.BorderInlineEndColor); + AddLonghand(PropertyNames.BorderInlineStartColor, () => new BorderInlineStartColorProperty(), true); + AddLonghand(PropertyNames.BorderInlineEndColor, () => new BorderInlineEndColorProperty(), true); + AddLonghand(PropertyNames.BorderStartStartRadius, () => new BorderStartStartRadiusProperty(), true); + AddLonghand(PropertyNames.BorderStartEndRadius, () => new BorderStartEndRadiusProperty(), true); + AddLonghand(PropertyNames.BorderEndStartRadius, () => new BorderEndStartRadiusProperty(), true); + AddLonghand(PropertyNames.BorderEndEndRadius, () => new BorderEndEndRadiusProperty(), true); + AddShorthand(PropertyNames.Columns, () => new ColumnsProperty(), PropertyNames.ColumnWidth, PropertyNames.ColumnCount); + AddLonghand(PropertyNames.BufferedRendering, () => new BufferedRenderingProperty()); + AddLonghand(PropertyNames.CaretColor, () => new CaretColorProperty(), true); AddLonghand(PropertyNames.ColumnCount, () => new ColumnCountProperty(), true); AddLonghand(PropertyNames.ColumnWidth, () => new ColumnWidthProperty(), true); @@ -182,6 +335,51 @@ private PropertyFactory() AddLonghand(PropertyNames.ColumnGap, () => new ColumnGapProperty(), true); AddLonghand(PropertyNames.ColumnSpan, () => new ColumnSpanProperty()); + AddLonghand(PropertyNames.FloatReference, () => new FloatReferenceProperty()); + AddLonghand(PropertyNames.FootnoteDisplay, () => new FootnoteDisplayProperty()); + AddLonghand(PropertyNames.FootnotePolicy, () => new FootnotePolicyProperty()); + AddLonghand(PropertyNames.ForcedColorAdjust, () => new ForcedColorAdjustProperty()); + AddLonghand(PropertyNames.GridTemplateColumns, () => new GridTemplateColumnsProperty()); + AddLonghand(PropertyNames.GridTemplateRows, () => new GridTemplateRowsProperty()); + AddLonghand(PropertyNames.GridTemplateAreas, () => new GridTemplateAreasProperty()); + AddLonghand(PropertyNames.GridAutoColumns, () => new GridAutoColumnsProperty()); + AddLonghand(PropertyNames.GridAutoRows, () => new GridAutoRowsProperty()); + AddLonghand(PropertyNames.GridAutoFlow, () => new GridAutoFlowProperty()); + + // The grid mega-shorthands parse/expand like any other, but are excluded from serialization + // reconstruction (via _logicalShorthands): reconstructing a `grid`/`grid-template` from its + // longhands is not worth the complexity and could change existing output. + AddLogicalShorthand(PropertyNames.GridTemplate, () => new GridTemplateProperty(), + PropertyNames.GridTemplateRows, PropertyNames.GridTemplateColumns, PropertyNames.GridTemplateAreas); + AddLogicalShorthand(PropertyNames.Grid, () => new GridProperty(), + PropertyNames.GridTemplateRows, PropertyNames.GridTemplateColumns, PropertyNames.GridTemplateAreas, + PropertyNames.GridAutoFlow, PropertyNames.GridAutoRows, PropertyNames.GridAutoColumns); + + AddLonghand(PropertyNames.GridColumnStart, () => new GridColumnStartProperty()); + AddLonghand(PropertyNames.GridColumnEnd, () => new GridColumnEndProperty()); + AddLonghand(PropertyNames.GridRowStart, () => new GridRowStartProperty()); + AddLonghand(PropertyNames.GridRowEnd, () => new GridRowEndProperty()); + + AddShorthand(PropertyNames.GridColumn, () => new GridColumnProperty(), + PropertyNames.GridColumnStart, PropertyNames.GridColumnEnd); + AddShorthand(PropertyNames.GridRow, () => new GridRowProperty(), + PropertyNames.GridRowStart, PropertyNames.GridRowEnd); + AddShorthand(PropertyNames.GridArea, () => new GridAreaProperty(), + PropertyNames.GridRowStart, PropertyNames.GridColumnStart, + PropertyNames.GridRowEnd, PropertyNames.GridColumnEnd); + + AddLonghand(PropertyNames.ImageRendering, () => new ImageRenderingProperty()); + AddLonghand(PropertyNames.Interactivity, () => new InteractivityProperty()); + AddLonghand(PropertyNames.Isolation, () => new IsolationProperty()); + AddLonghand(PropertyNames.JustifyItems, () => new JustifyItemsProperty()); + AddLonghand(PropertyNames.JustifySelf, () => new JustifySelfProperty()); + AddShorthand(PropertyNames.PlaceItems, () => new PlaceItemsProperty(), + PropertyNames.AlignItems, PropertyNames.JustifyItems); + AddShorthand(PropertyNames.PlaceContent, () => new PlaceContentProperty(), + PropertyNames.AlignContent, PropertyNames.JustifyContent); + AddShorthand(PropertyNames.PlaceSelf, () => new PlaceSelfProperty(), + PropertyNames.AlignSelf, PropertyNames.JustifySelf); + AddShorthand(PropertyNames.ColumnRule, () => new ColumnRuleProperty(), PropertyNames.ColumnRuleWidth, PropertyNames.ColumnRuleStyle, @@ -194,11 +392,35 @@ private PropertyFactory() AddLonghand(PropertyNames.Clear, () => new ClearProperty()); AddLonghand(PropertyNames.Clip, () => new ClipProperty(), true); AddLonghand(PropertyNames.Color, () => new ColorProperty(), true); + AddLonghand(PropertyNames.ColorScheme, () => new ColorSchemeProperty()); AddLonghand(PropertyNames.ContainerName, () => new ContainerNameProperty()); AddLonghand(PropertyNames.ContainerType, () => new ContainerTypeProperty()); AddLonghand(PropertyNames.Content, () => new ContentProperty()); + AddLonghand(PropertyNames.MathDepth, () => new MathDepthProperty()); + AddLonghand(PropertyNames.MathShift, () => new MathShiftProperty()); + AddLonghand(PropertyNames.MathStyle, () => new MathStyleProperty()); + AddLonghand(PropertyNames.MixBlendMode, () => new MixBlendModeProperty(), true); + AddLonghand(PropertyNames.PointerEvents, () => new PointerEventsProperty()); + AddLonghand(PropertyNames.PrintColorAdjust, () => new PrintColorAdjustProperty()); + AddLonghand(PropertyNames.ShapeRendering, () => new ShapeRenderingProperty()); + AddLonghand(PropertyNames.Contain, () => new ContainProperty()); + AddShorthand(PropertyNames.ContainIntrinsicSize, () => new ContainIntrinsicSizeProperty(), + PropertyNames.ContainIntrinsicWidth, PropertyNames.ContainIntrinsicHeight); + AddLonghand(PropertyNames.ContainIntrinsicWidth, () => new ContainIntrinsicWidthProperty(), true); + AddLonghand(PropertyNames.ContainIntrinsicHeight, () => new ContainIntrinsicHeightProperty(), true); + AddLonghand(PropertyNames.ContainIntrinsicInlineSize, () => new ContainIntrinsicInlineSizeProperty(), true); + AddLonghand(PropertyNames.ContainIntrinsicBlockSize, () => new ContainIntrinsicBlockSizeProperty(), true); + AddShorthand(PropertyNames.Container, () => new ContainerProperty(), + PropertyNames.ContainerName, PropertyNames.ContainerType); + AddLonghand(PropertyNames.ContentVisibility, () => new ContentVisibilityProperty()); + AddLonghand(PropertyNames.StringSet, () => new StringSetProperty()); AddLonghand(PropertyNames.CounterIncrement, () => new CounterIncrementProperty()); AddLonghand(PropertyNames.CounterReset, () => new CounterResetProperty()); + AddLonghand(PropertyNames.CounterSet, () => new CounterSetProperty()); + AddLonghand(PropertyNames.BookmarkLabel, () => new BookmarkLabelProperty()); + AddLonghand(PropertyNames.BookmarkLevel, () => new BookmarkLevelProperty()); + AddLonghand(PropertyNames.BookmarkState, () => new BookmarkStateProperty()); + AddLonghand(PropertyNames.BookmarkTarget, () => new BookmarkTargetProperty()); AddLonghand(PropertyNames.Cursor, () => new CursorProperty()); AddLonghand(PropertyNames.Direction, () => new DirectionProperty()); AddLonghand(PropertyNames.Display, () => new DisplayProperty()); @@ -213,7 +435,7 @@ private PropertyFactory() AddLonghand(PropertyNames.FlexBasis, () => new FlexBasisProperty(), true); AddLonghand(PropertyNames.FlexDirection, () => new FlexDirectionProperty()); AddShorthand(PropertyNames.FlexFlow, () => new FlexFlowProperty(), - PropertyNames.FlexDirection, + PropertyNames.FlexDirection, PropertyNames.FlexWrap); AddLonghand(PropertyNames.FlexGrow, () => new FlexGrowProperty()); AddLonghand(PropertyNames.FlexShrink, () => new FlexShrinkProperty()); @@ -225,30 +447,86 @@ private PropertyFactory() PropertyNames.FontSize, PropertyNames.FontStretch, PropertyNames.FontStyle, - PropertyNames.FontVariant, + PropertyNames.FontVariantCaps, PropertyNames.FontWeight, - PropertyNames.LineHeight); + PropertyNames.LineHeight, + // The longhands below cannot be written in "font" but are reset by it (CSS Fonts 4 7.7). + PropertyNames.FontVariantLigatures, + PropertyNames.FontVariantAlternates, + PropertyNames.FontVariantNumeric, + PropertyNames.FontVariantEastAsian, + PropertyNames.FontVariantPosition, + PropertyNames.FontVariantEmoji, + PropertyNames.FontKerning, + PropertyNames.FontOpticalSizing, + PropertyNames.FontSizeAdjust, + PropertyNames.FontVariationSettings, + PropertyNames.FontFeatureSettings, + PropertyNames.FontLanguageOverride); AddLonghand(PropertyNames.FontFamily, () => new FontFamilyProperty(), false, true); - AddLonghand(PropertyNames.FontPalette, () => new FontPaletteProperty()); AddLonghand(PropertyNames.FontSize, () => new FontSizeProperty(), true); AddLonghand(PropertyNames.FontSizeAdjust, () => new FontSizeAdjustProperty(), true); AddLonghand(PropertyNames.FontStyle, () => new FontStyleProperty(), false, true); - AddLonghand(PropertyNames.FontVariant, () => new FontVariantProperty(), false, true); AddLonghand(PropertyNames.FontWeight, () => new FontWeightProperty(), true, true); AddLonghand(PropertyNames.FontStretch, () => new FontStretchProperty(), true, true); + AddLonghand(PropertyNames.FontPalette, () => new FontPaletteProperty()); + AddLonghand(PropertyNames.FontKerning, () => new FontKerningProperty()); + AddLonghand(PropertyNames.FontOpticalSizing, () => new FontOpticalSizingProperty()); + AddLonghand(PropertyNames.FontFeatureSettings, () => new FontFeatureSettingsProperty()); + AddLonghand(PropertyNames.FontVariationSettings, () => new FontVariationSettingsProperty()); + AddLonghand(PropertyNames.FontLanguageOverride, () => new FontLanguageOverrideProperty()); + + AddShorthand(PropertyNames.FontSynthesis, () => new FontSynthesisProperty(), + PropertyNames.FontSynthesisWeight, + PropertyNames.FontSynthesisStyle, + PropertyNames.FontSynthesisSmallCaps, + PropertyNames.FontSynthesisPosition); + AddLonghand(PropertyNames.FontSynthesisWeight, () => new FontSynthesisWeightProperty()); + AddLonghand(PropertyNames.FontSynthesisStyle, () => new FontSynthesisStyleProperty()); + AddLonghand(PropertyNames.FontSynthesisSmallCaps, () => new FontSynthesisSmallCapsProperty()); + AddLonghand(PropertyNames.FontSynthesisPosition, () => new FontSynthesisPositionProperty()); + + // font-variant is a shorthand of the font-variant-* longhands (CSS Fonts 4 6). The @font-face + // font-variant descriptor is a different, flat property (FontFaceVariantProperty). + AddShorthand(PropertyNames.FontVariant, () => new FontVariantProperty(), + PropertyNames.FontVariantLigatures, + PropertyNames.FontVariantAlternates, + PropertyNames.FontVariantCaps, + PropertyNames.FontVariantNumeric, + PropertyNames.FontVariantEastAsian, + PropertyNames.FontVariantPosition, + PropertyNames.FontVariantEmoji); + AddLonghand(PropertyNames.FontVariantLigatures, () => new FontVariantLigaturesProperty()); + AddLonghand(PropertyNames.FontVariantAlternates, () => new FontVariantAlternatesProperty()); + AddLonghand(PropertyNames.FontVariantCaps, () => new FontVariantCapsProperty()); + AddLonghand(PropertyNames.FontVariantNumeric, () => new FontVariantNumericProperty()); + AddLonghand(PropertyNames.FontVariantEastAsian, () => new FontVariantEastAsianProperty()); + AddLonghand(PropertyNames.FontVariantPosition, () => new FontVariantPositionProperty()); + AddLonghand(PropertyNames.FontVariantEmoji, () => new FontVariantEmojiProperty()); + _fonts.Add(PropertyNames.FontVariant, () => new FontFaceVariantProperty()); AddShorthand(PropertyNames.Gap, () => new GapProperty(), - PropertyNames.RowGap, + PropertyNames.RowGap, PropertyNames.ColumnGap); AddLonghand(PropertyNames.Height, () => new HeightProperty(), true); AddLonghand(PropertyNames.Hyphens, () => new HyphensProperty()); + AddLonghand(PropertyNames.HyphenateCharacter, () => new HyphenateCharacterProperty()); + AddLonghand(PropertyNames.HyphenateLimitChars, () => new HyphenateLimitCharsProperty()); + AddLonghand(PropertyNames.HyphenateLimitLast, () => new HyphenateLimitLastProperty()); + AddLonghand(PropertyNames.HyphenateLimitLines, () => new HyphenateLimitLinesProperty()); + AddLonghand(PropertyNames.HyphenateLimitZone, () => new HyphenateLimitZoneProperty()); + AddLonghand(PropertyNames.HangingPunctuation, () => new HangingPunctuationProperty()); + AddLonghand(PropertyNames.InitialLetter, () => new InitialLetterProperty()); AddLonghand(PropertyNames.JustifyContent, () => new JustifyContentProperty()); - + AddLonghand(PropertyNames.Left, () => new LeftProperty(), true); AddLonghand(PropertyNames.LetterSpacing, () => new LetterSpacingProperty()); + AddLonghand(PropertyNames.LineBreak, () => new LineBreakProperty()); AddLonghand(PropertyNames.LineHeight, () => new LineHeightProperty(), true); + AddLonghand(PropertyNames.LineClamp, () => new LineClampProperty()); + AddLonghand(PropertyNames.LineHeightStep, () => new LineHeightStepProperty()); AddShorthand(PropertyNames.ListStyle, () => new ListStyleProperty(), PropertyNames.ListStyleType, @@ -281,10 +559,28 @@ private PropertyFactory() PropertyNames.OutlineStyle, PropertyNames.OutlineColor); AddLonghand(PropertyNames.OutlineColor, () => new OutlineColorProperty(), true); + AddLonghand(PropertyNames.OutlineOffset, () => new OutlineOffsetProperty(), true); AddLonghand(PropertyNames.OutlineStyle, () => new OutlineStyleProperty()); AddLonghand(PropertyNames.OutlineWidth, () => new OutlineWidthProperty(), true); AddLonghand(PropertyNames.Overflow, () => new OverflowProperty()); + AddLonghand(PropertyNames.OverflowX, () => new OverflowXProperty()); + AddLonghand(PropertyNames.OverflowY, () => new OverflowYProperty()); + AddLonghand(PropertyNames.OverflowClipMarginTop, () => new OverflowClipMarginTopProperty(), true); + AddLonghand(PropertyNames.OverflowClipMarginRight, () => new OverflowClipMarginRightProperty(), true); + AddLonghand(PropertyNames.OverflowClipMarginBottom, () => new OverflowClipMarginBottomProperty(), true); + AddLonghand(PropertyNames.OverflowClipMarginLeft, () => new OverflowClipMarginLeftProperty(), true); + AddLonghand(PropertyNames.OverflowClipMarginBlockStart, () => new OverflowClipMarginBlockStartProperty(), true); + AddLonghand(PropertyNames.OverflowClipMarginBlockEnd, () => new OverflowClipMarginBlockEndProperty(), true); + AddLonghand(PropertyNames.OverflowClipMarginInlineStart, () => new OverflowClipMarginInlineStartProperty(), true); + AddLonghand(PropertyNames.OverflowClipMarginInlineEnd, () => new OverflowClipMarginInlineEndProperty(), true); + AddShorthand(PropertyNames.OverflowClipMargin, () => new OverflowClipMarginProperty(), + PropertyNames.OverflowClipMarginTop, PropertyNames.OverflowClipMarginRight, + PropertyNames.OverflowClipMarginBottom, PropertyNames.OverflowClipMarginLeft); + AddShorthand(PropertyNames.OverflowClipMarginBlock, () => new OverflowClipMarginBlockProperty(), + PropertyNames.OverflowClipMarginBlockStart, PropertyNames.OverflowClipMarginBlockEnd); + AddShorthand(PropertyNames.OverflowClipMarginInline, () => new OverflowClipMarginInlineProperty(), + PropertyNames.OverflowClipMarginInlineStart, PropertyNames.OverflowClipMarginInlineEnd); AddLonghand(PropertyNames.OverflowWrap, () => new OverflowWrapProperty()); AddShorthand(PropertyNames.Padding, () => new PaddingProperty(), @@ -303,9 +599,45 @@ private PropertyFactory() AddLonghand(PropertyNames.Perspective, () => new PerspectiveProperty(), true); AddLonghand(PropertyNames.PerspectiveOrigin, () => new PerspectiveOriginProperty(), true); AddLonghand(PropertyNames.Position, () => new PositionProperty()); + AddShorthand(PropertyNames.OverscrollBehavior, () => new OverscrollBehaviorProperty(), + PropertyNames.OverscrollBehaviorX, + PropertyNames.OverscrollBehaviorY); + AddLonghand(PropertyNames.OverscrollBehaviorX, () => new OverscrollBehaviorXProperty()); + AddLonghand(PropertyNames.OverscrollBehaviorY, () => new OverscrollBehaviorYProperty()); + AddLonghand(PropertyNames.Quotes, () => new QuotesProperty()); AddLonghand(PropertyNames.Right, () => new RightProperty(), true); AddLonghand(PropertyNames.RowGap, () => new RowGapProperty(), true); + AddLonghand(PropertyNames.ScrollBehavior, () => new ScrollBehaviorProperty()); + AddShorthand(PropertyNames.ScrollMargin, () => new ScrollMarginProperty(), + PropertyNames.ScrollMarginTop, + PropertyNames.ScrollMarginRight, + PropertyNames.ScrollMarginBottom, + PropertyNames.ScrollMarginLeft); + AddLonghand(PropertyNames.ScrollMarginTop, () => new ScrollMarginTopProperty(), true); + AddLonghand(PropertyNames.ScrollMarginRight, () => new ScrollMarginRightProperty(), true); + AddLonghand(PropertyNames.ScrollMarginBottom, () => new ScrollMarginBottomProperty(), true); + AddLonghand(PropertyNames.ScrollMarginLeft, () => new ScrollMarginLeftProperty(), true); + AddShorthand(PropertyNames.ScrollPadding, () => new ScrollPaddingProperty(), + PropertyNames.ScrollPaddingTop, + PropertyNames.ScrollPaddingRight, + PropertyNames.ScrollPaddingBottom, + PropertyNames.ScrollPaddingLeft); + AddLonghand(PropertyNames.ScrollPaddingTop, () => new ScrollPaddingTopProperty(), true); + AddLonghand(PropertyNames.ScrollPaddingRight, () => new ScrollPaddingRightProperty(), true); + AddLonghand(PropertyNames.ScrollPaddingBottom, () => new ScrollPaddingBottomProperty(), true); + AddLonghand(PropertyNames.ScrollPaddingLeft, () => new ScrollPaddingLeftProperty(), true); + AddLonghand(PropertyNames.ScrollSnapAlign, () => new ScrollSnapAlignProperty()); + AddLonghand(PropertyNames.ScrollSnapStop, () => new ScrollSnapStopProperty()); + AddLonghand(PropertyNames.ScrollSnapType, () => new ScrollSnapTypeProperty()); + AddLonghand(PropertyNames.ScrollbarColor, () => new ScrollbarColorProperty(), true); + AddLonghand(PropertyNames.ScrollbarGutter, () => new ScrollbarGutterProperty()); + AddLonghand(PropertyNames.ScrollbarWidth, () => new ScrollbarWidthProperty()); + AddShorthand(PropertyNames.ScrollTimeline, () => new ScrollTimelineProperty(), + PropertyNames.ScrollTimelineName, + PropertyNames.ScrollTimelineAxis); + AddLonghand(PropertyNames.ScrollTimelineAxis, () => new ScrollTimelineAxisProperty()); + AddLonghand(PropertyNames.ScrollTimelineName, () => new ScrollTimelineNameProperty()); AddLonghand(PropertyNames.Stroke, () => new StrokeProperty(), true); AddLonghand(PropertyNames.StrokeDasharray, () => new StrokeDasharrayProperty(), true); AddLonghand(PropertyNames.StrokeDashoffset, () => new StrokeDashoffsetProperty(), true); @@ -314,34 +646,107 @@ private PropertyFactory() AddLonghand(PropertyNames.StrokeMiterlimit, () => new StrokeMiterlimitProperty(), true); AddLonghand(PropertyNames.StrokeOpacity, () => new StrokeOpacityProperty(), true); AddLonghand(PropertyNames.StrokeWidth, () => new StrokeWidthProperty(), true); - AddLonghand(PropertyNames.StringSet, () => new StringSetProperty()); - AddLonghand(PropertyNames.PageName, () => new PageNameProperty()); + AddLonghand(PropertyNames.StopColor, () => new StopColorProperty(), true); + AddLonghand(PropertyNames.StopOpacity, () => new StopOpacityProperty(), true); + AddLonghand(PropertyNames.FloodColor, () => new FloodColorProperty(), true); + AddLonghand(PropertyNames.FloodOpacity, () => new FloodOpacityProperty(), true); + AddLonghand(PropertyNames.LightingColor, () => new LightingColorProperty(), true); + AddLonghand(PropertyNames.MarkerStart, () => new MarkerStartProperty()); + AddLonghand(PropertyNames.MarkerMid, () => new MarkerMidProperty()); + AddLonghand(PropertyNames.MarkerEnd, () => new MarkerEndProperty()); + AddLonghand(PropertyNames.PaintOrder, () => new PaintOrderProperty()); + AddLonghand(PropertyNames.ClipRule, () => new ClipRuleProperty()); + AddLonghand(PropertyNames.ColorInterpolation, () => new ColorInterpolationProperty()); + AddLonghand(PropertyNames.ColorInterpolationFilters, () => new ColorInterpolationFiltersProperty()); + AddLonghand(PropertyNames.DominantBaseline, () => new DominantBaselineProperty()); + AddLonghand(PropertyNames.AlignBaseline, () => new AlignmentBaselineProperty()); + AddLonghand(PropertyNames.BaselineShift, () => new BaselineShiftProperty(), true); + AddLonghand(PropertyNames.TransformBox, () => new TransformBoxProperty()); + AddLonghand(PropertyNames.InlineSize, () => new InlineSizeProperty(), true); + AddLonghand(PropertyNames.X, () => new XProperty(), true); + AddLonghand(PropertyNames.Y, () => new YProperty(), true); + AddLonghand(PropertyNames.Cx, () => new CxProperty(), true); + AddLonghand(PropertyNames.Cy, () => new CyProperty(), true); + AddLonghand(PropertyNames.R, () => new RProperty(), true); + AddLonghand(PropertyNames.Rx, () => new RxProperty(), true); + AddLonghand(PropertyNames.Ry, () => new RyProperty(), true); + AddLonghand(PropertyNames.D, () => new DProperty(), true); + AddShorthand(PropertyNames.Marker, () => new MarkerProperty(), + PropertyNames.MarkerStart, + PropertyNames.MarkerMid, + PropertyNames.MarkerEnd); AddLonghand(PropertyNames.TableLayout, () => new TableLayoutProperty()); - AddLonghand(PropertyNames.TextAlign, () => new TextAlignProperty()); + AddShorthand(PropertyNames.TextAlign, () => new TextAlignProperty(), + PropertyNames.TextAlignAll, + PropertyNames.TextAlignLast); + AddLonghand(PropertyNames.TextAlignAll, () => new TextAlignAllProperty()); AddLonghand(PropertyNames.TextAlignLast, () => new TextAlignLastProperty()); + AddLonghand(PropertyNames.TextOverflow, () => new TextOverflowProperty()); AddLonghand(PropertyNames.TextAnchor, () => new TextAnchorProperty()); AddShorthand(PropertyNames.TextDecoration, () => new TextDecorationProperty(), PropertyNames.TextDecorationLine, PropertyNames.TextDecorationStyle, - PropertyNames.TextDecorationColor); + PropertyNames.TextDecorationColor, + PropertyNames.TextDecorationThickness); AddLonghand(PropertyNames.TextDecorationStyle, () => new TextDecorationStyleProperty()); AddLonghand(PropertyNames.TextDecorationLine, () => new TextDecorationLineProperty()); AddLonghand(PropertyNames.TextDecorationColor, () => new TextDecorationColorProperty(), true); - + AddLonghand(PropertyNames.TextDecorationThickness, () => new TextDecorationThicknessProperty()); + AddLonghand(PropertyNames.TextDecorationSkipInk, () => new TextDecorationSkipInkProperty()); + AddLonghand(PropertyNames.TextDecorationSkip, () => new TextDecorationSkipProperty()); + AddLonghand(PropertyNames.TextDecorationInset, () => new TextDecorationInsetProperty()); + + AddShorthand(PropertyNames.TextEmphasis, () => new TextEmphasisProperty(), + PropertyNames.TextEmphasisStyle, + PropertyNames.TextEmphasisColor); + AddLonghand(PropertyNames.TextEmphasisStyle, () => new TextEmphasisStyleProperty()); + AddLonghand(PropertyNames.TextEmphasisColor, () => new TextEmphasisColorProperty(), true); + AddLonghand(PropertyNames.TextEmphasisPosition, () => new TextEmphasisPositionProperty()); + + AddLonghand(PropertyNames.TextCombineUpright, () => new TextCombineUprightProperty()); AddLonghand(PropertyNames.TextIndent, () => new TextIndentProperty(), true); AddLonghand(PropertyNames.TextJustify, () => new TextJustifyProperty()); + AddLonghand(PropertyNames.TextOrientation, () => new TextOrientationProperty()); + AddLonghand(PropertyNames.TextRendering, () => new TextRenderingProperty()); + AddLonghand(PropertyNames.TabSize, () => new TabSizeProperty()); + + AddShorthand(PropertyNames.TextBox, () => new TextBoxProperty(), + PropertyNames.TextBoxTrim, + PropertyNames.TextBoxEdge); + AddLonghand(PropertyNames.TextBoxTrim, () => new TextBoxTrimProperty()); + AddLonghand(PropertyNames.TextBoxEdge, () => new TextBoxEdgeProperty()); AddLonghand(PropertyNames.TextTransform, () => new TextTransformProperty()); + AddLonghand(PropertyNames.TextUnderlineOffset, () => new TextUnderlineOffsetProperty()); + AddLonghand(PropertyNames.TextUnderlinePosition, () => new TextUnderlinePositionProperty()); AddLonghand(PropertyNames.TextShadow, () => new TextShadowProperty(), true); + AddShorthand(PropertyNames.TextWrap, () => new TextWrapProperty(), + PropertyNames.TextWrapMode, + PropertyNames.TextWrapStyle); + AddLonghand(PropertyNames.TextWrapMode, () => new TextWrapModeProperty()); + AddLonghand(PropertyNames.TextWrapStyle, () => new TextWrapStyleProperty()); + AddLonghand(PropertyNames.TouchAction, () => new TouchActionProperty()); + AddLonghand(PropertyNames.TimelineScope, () => new TimelineScopeProperty()); AddLonghand(PropertyNames.Transform, () => new TransformProperty(), true); + AddLonghand(PropertyNames.ClipPath, () => new ClipPathProperty(), true); + AddLonghand(PropertyNames.Filter, () => new FilterProperty(), true); + AddLonghand(PropertyNames.BackdropFilter, () => new BackdropFilterProperty(), true); + AddLonghand(PropertyNames.ShapeOutside, () => new ShapeOutsideProperty()); + AddLonghand(PropertyNames.ShapeMargin, () => new ShapeMarginProperty(), true); + AddLonghand(PropertyNames.ShapeImageThreshold, () => new ShapeImageThresholdProperty(), true); AddLonghand(PropertyNames.TransformOrigin, () => new TransformOriginProperty(), true); AddLonghand(PropertyNames.TransformStyle, () => new TransformStyleProperty()); + AddLonghand(PropertyNames.Translate, () => new TranslateProperty(), true); + AddLonghand(PropertyNames.Rotate, () => new RotateProperty(), true); + AddLonghand(PropertyNames.Scale, () => new ScaleProperty(), true); AddShorthand(PropertyNames.Transition, () => new TransitionProperty(), PropertyNames.TransitionProperty, PropertyNames.TransitionDuration, PropertyNames.TransitionTimingFunction, - PropertyNames.TransitionDelay); + PropertyNames.TransitionDelay, + PropertyNames.TransitionBehavior); + AddLonghand(PropertyNames.TransitionBehavior, () => new TransitionBehaviorProperty()); AddLonghand(PropertyNames.TransitionDelay, () => new TransitionDelayProperty()); AddLonghand(PropertyNames.TransitionDuration, () => new TransitionDurationProperty()); AddLonghand(PropertyNames.TransitionTimingFunction, () => new TransitionTimingFunctionProperty()); @@ -349,190 +754,45 @@ private PropertyFactory() AddLonghand(PropertyNames.Top, () => new TopProperty(), true); AddLonghand(PropertyNames.UnicodeBidirectional, () => new UnicodeBidirectionalProperty()); + AddLonghand(PropertyNames.UserSelect, () => new UserSelectProperty()); + AddLonghand(PropertyNames.VectorEffect, () => new VectorEffectProperty()); AddLonghand(PropertyNames.VerticalAlign, () => new VerticalAlignProperty(), true); + AddShorthand(PropertyNames.ViewTimeline, () => new ViewTimelineProperty(), + PropertyNames.ViewTimelineName, + PropertyNames.ViewTimelineAxis, + PropertyNames.ViewTimelineInset); + AddLonghand(PropertyNames.ViewTimelineAxis, () => new ViewTimelineAxisProperty()); + AddLonghand(PropertyNames.ViewTimelineInset, () => new ViewTimelineInsetProperty()); + AddLonghand(PropertyNames.ViewTimelineName, () => new ViewTimelineNameProperty()); AddLonghand(PropertyNames.Visibility, () => new VisibilityProperty(), true); - AddLonghand(PropertyNames.WhiteSpace, () => new WhiteSpaceProperty()); + AddShorthand(PropertyNames.WhiteSpace, () => new WhiteSpaceProperty(), + PropertyNames.WhiteSpaceCollapse, + PropertyNames.TextWrapMode); + AddLonghand(PropertyNames.WhiteSpaceCollapse, () => new WhiteSpaceCollapseProperty()); AddLonghand(PropertyNames.Widows, () => new WidowsProperty()); AddLonghand(PropertyNames.Width, () => new WidthProperty(), true); AddLonghand(PropertyNames.WordBreak, () => new WordBreakProperty(), true); AddLonghand(PropertyNames.WordSpacing, () => new WordSpacingProperty(), true); AddLonghand(PropertyNames.WordWrap, () => new OverflowWrapProperty()); + AddLonghand(PropertyNames.WritingMode, () => new WritingModeProperty()); AddLonghand(PropertyNames.ZIndex, () => new ZIndexProperty(), true); AddLonghand(PropertyNames.ObjectFit, () => new ObjectFitProperty()); - AddLonghand(PropertyNames.ObjectPosition, () => new ObjectPositionProperty(), true); + AddLonghand(PropertyNames.PageName, () => new PageNameProperty()); AddLonghand(PropertyNames.Size, () => new PageSizeProperty()); + AddLonghand(PropertyNames.Bleed, () => new BleedProperty()); + AddLonghand(PropertyNames.Marks, () => new MarksProperty()); + AddLonghand(PropertyNames.ObjectPosition, () => new ObjectPositionProperty(), true); - _fontsBuilder.Add(PropertyNames.Src, () => new SrcProperty()); - _fontsBuilder.Add(PropertyNames.UnicodeRange, () => new UnicodeRangeProperty()); - - // CSS Grid 2, parsed and cascaded, there's no grid layout - AddLonghand(PropertyNames.GridTemplateColumns, () => new GridTemplateColumnsProperty()); - AddLonghand(PropertyNames.GridTemplateRows, () => new GridTemplateRowsProperty()); - AddLonghand(PropertyNames.GridTemplateAreas, () => new GridTemplateAreasProperty()); - AddLonghand(PropertyNames.GridColumnStart, () => new GridColumnStartProperty()); - AddLonghand(PropertyNames.GridColumnEnd, () => new GridColumnEndProperty()); - AddLonghand(PropertyNames.GridRowStart, () => new GridRowStartProperty()); - AddLonghand(PropertyNames.GridRowEnd, () => new GridRowEndProperty()); - - AddShorthand(PropertyNames.GridColumn, () => new GridColumnProperty(), - PropertyNames.GridColumnStart, PropertyNames.GridColumnEnd); - AddShorthand(PropertyNames.GridRow, () => new GridRowProperty(), - PropertyNames.GridRowStart, PropertyNames.GridRowEnd); - AddShorthand(PropertyNames.GridArea, () => new GridAreaProperty(), - PropertyNames.GridRowStart, PropertyNames.GridColumnStart, - PropertyNames.GridRowEnd, PropertyNames.GridColumnEnd); - - AddLonghand(PropertyNames.GridAutoFlow, () => new GridAutoFlowProperty()); - AddLonghand(PropertyNames.GridAutoColumns, () => new GridAutoColumnsProperty()); - AddLonghand(PropertyNames.GridAutoRows, () => new GridAutoRowsProperty()); - - // Never reconstructed from their longhands when serializing. - AddLogicalShorthand(PropertyNames.GridTemplate, () => new GridTemplateProperty(), - PropertyNames.GridTemplateRows, PropertyNames.GridTemplateColumns, PropertyNames.GridTemplateAreas); - AddLogicalShorthand(PropertyNames.Grid, () => new GridProperty(), - PropertyNames.GridTemplateRows, PropertyNames.GridTemplateColumns, PropertyNames.GridTemplateAreas, - PropertyNames.GridAutoFlow, PropertyNames.GridAutoRows, PropertyNames.GridAutoColumns); - - AddLonghand(PropertyNames.JustifyItems, () => new JustifyItemsProperty()); - AddLonghand(PropertyNames.JustifySelf, () => new JustifySelfProperty()); - AddShorthand(PropertyNames.PlaceItems, () => new PlaceItemsProperty(), - PropertyNames.AlignItems, PropertyNames.JustifyItems); - AddShorthand(PropertyNames.PlaceContent, () => new PlaceContentProperty(), - PropertyNames.AlignContent, PropertyNames.JustifyContent); - AddShorthand(PropertyNames.PlaceSelf, () => new PlaceSelfProperty(), - PropertyNames.AlignSelf, PropertyNames.JustifySelf); - - // CSS Logical 1 §4. The flow-relative properties are aliases of the physical ones, mapped as - // horizontal-tb ltr whatever the element's direction. - AddLonghand(PropertyNames.MarginBlockStart, () => new MarginTopProperty(), true); - AddLonghand(PropertyNames.MarginBlockEnd, () => new MarginBottomProperty(), true); - AddLonghand(PropertyNames.MarginInlineStart, () => new MarginLeftProperty(), true); - AddLonghand(PropertyNames.MarginInlineEnd, () => new MarginRightProperty(), true); - AddLogicalShorthand(PropertyNames.MarginBlock, () => new MarginBlockProperty(), - PropertyNames.MarginTop, - PropertyNames.MarginBottom); - AddLogicalShorthand(PropertyNames.MarginInline, () => new MarginInlineProperty(), - PropertyNames.MarginLeft, - PropertyNames.MarginRight); - - AddLonghand(PropertyNames.PaddingBlockStart, () => new PaddingTopProperty(), true); - AddLonghand(PropertyNames.PaddingBlockEnd, () => new PaddingBottomProperty(), true); - AddLonghand(PropertyNames.PaddingInlineStart, () => new PaddingLeftProperty(), true); - AddLonghand(PropertyNames.PaddingInlineEnd, () => new PaddingRightProperty(), true); - AddLogicalShorthand(PropertyNames.PaddingBlock, () => new PaddingBlockProperty(), - PropertyNames.PaddingTop, - PropertyNames.PaddingBottom); - AddLogicalShorthand(PropertyNames.PaddingInline, () => new PaddingInlineProperty(), - PropertyNames.PaddingLeft, - PropertyNames.PaddingRight); - - AddLonghand(PropertyNames.InsetBlockStart, () => new TopProperty(), true); - AddLonghand(PropertyNames.InsetBlockEnd, () => new BottomProperty(), true); - AddLonghand(PropertyNames.InsetInlineStart, () => new LeftProperty(), true); - AddLonghand(PropertyNames.InsetInlineEnd, () => new RightProperty(), true); - AddLogicalShorthand(PropertyNames.Inset, () => new InsetProperty(), - PropertyNames.Top, - PropertyNames.Right, - PropertyNames.Bottom, - PropertyNames.Left); - AddLogicalShorthand(PropertyNames.InsetBlock, () => new InsetBlockProperty(), - PropertyNames.Top, - PropertyNames.Bottom); - AddLogicalShorthand(PropertyNames.InsetInline, () => new InsetInlineProperty(), - PropertyNames.Left, - PropertyNames.Right); - - AddLonghand(PropertyNames.BorderBlockStartWidth, () => new BorderTopWidthProperty(), true); - AddLonghand(PropertyNames.BorderBlockStartStyle, () => new BorderTopStyleProperty()); - AddLonghand(PropertyNames.BorderBlockStartColor, () => new BorderTopColorProperty(), true); - AddLonghand(PropertyNames.BorderBlockEndWidth, () => new BorderBottomWidthProperty(), true); - AddLonghand(PropertyNames.BorderBlockEndStyle, () => new BorderBottomStyleProperty()); - AddLonghand(PropertyNames.BorderBlockEndColor, () => new BorderBottomColorProperty(), true); - AddLonghand(PropertyNames.BorderInlineStartWidth, () => new BorderLeftWidthProperty(), true); - AddLonghand(PropertyNames.BorderInlineStartStyle, () => new BorderLeftStyleProperty()); - AddLonghand(PropertyNames.BorderInlineStartColor, () => new BorderLeftColorProperty(), true); - AddLonghand(PropertyNames.BorderInlineEndWidth, () => new BorderRightWidthProperty(), true); - AddLonghand(PropertyNames.BorderInlineEndStyle, () => new BorderRightStyleProperty()); - AddLonghand(PropertyNames.BorderInlineEndColor, () => new BorderRightColorProperty(), true); - - AddLogicalShorthand(PropertyNames.BorderBlockStart, () => new BorderTopProperty(), - PropertyNames.BorderTopWidth, - PropertyNames.BorderTopStyle, - PropertyNames.BorderTopColor); - AddLogicalShorthand(PropertyNames.BorderBlockEnd, () => new BorderBottomProperty(), - PropertyNames.BorderBottomWidth, - PropertyNames.BorderBottomStyle, - PropertyNames.BorderBottomColor); - AddLogicalShorthand(PropertyNames.BorderInlineStart, () => new BorderLeftProperty(), - PropertyNames.BorderLeftWidth, - PropertyNames.BorderLeftStyle, - PropertyNames.BorderLeftColor); - AddLogicalShorthand(PropertyNames.BorderInlineEnd, () => new BorderRightProperty(), - PropertyNames.BorderRightWidth, - PropertyNames.BorderRightStyle, - PropertyNames.BorderRightColor); - - AddLogicalShorthand(PropertyNames.BorderBlock, () => new BorderBlockProperty(), - PropertyNames.BorderTopWidth, - PropertyNames.BorderTopStyle, - PropertyNames.BorderTopColor, - PropertyNames.BorderBottomWidth, - PropertyNames.BorderBottomStyle, - PropertyNames.BorderBottomColor); - AddLogicalShorthand(PropertyNames.BorderInline, () => new BorderInlineProperty(), - PropertyNames.BorderLeftWidth, - PropertyNames.BorderLeftStyle, - PropertyNames.BorderLeftColor, - PropertyNames.BorderRightWidth, - PropertyNames.BorderRightStyle, - PropertyNames.BorderRightColor); - - AddLogicalShorthand(PropertyNames.BorderBlockWidth, () => new BorderBlockWidthProperty(), - PropertyNames.BorderTopWidth, - PropertyNames.BorderBottomWidth); - AddLogicalShorthand(PropertyNames.BorderBlockStyle, () => new BorderBlockStyleProperty(), - PropertyNames.BorderTopStyle, - PropertyNames.BorderBottomStyle); - AddLogicalShorthand(PropertyNames.BorderBlockColor, () => new BorderBlockColorProperty(), - PropertyNames.BorderTopColor, - PropertyNames.BorderBottomColor); - AddLogicalShorthand(PropertyNames.BorderInlineWidth, () => new BorderInlineWidthProperty(), - PropertyNames.BorderLeftWidth, - PropertyNames.BorderRightWidth); - AddLogicalShorthand(PropertyNames.BorderInlineStyle, () => new BorderInlineStyleProperty(), - PropertyNames.BorderLeftStyle, - PropertyNames.BorderRightStyle); - AddLogicalShorthand(PropertyNames.BorderInlineColor, () => new BorderInlineColorProperty(), - PropertyNames.BorderLeftColor, - PropertyNames.BorderRightColor); - - _fonts = _fontsBuilder; - - _propertyDescriptors = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { PropertyNames.Syntax, () => new UnknownProperty(PropertyNames.Syntax) }, - { PropertyNames.InitialValue, () => new UnknownProperty(PropertyNames.InitialValue) }, - { PropertyNames.Inherits, () => new UnknownProperty(PropertyNames.Inherits) }, - }; - - _fontPaletteDescriptors = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { PropertyNames.FontFamily, () => new UnknownProperty(PropertyNames.FontFamily) }, - { PropertyNames.BasePalette, () => new UnknownProperty(PropertyNames.BasePalette) }, - { PropertyNames.OverrideColors, () => new UnknownProperty(PropertyNames.OverrideColors) }, - }; - - _longhands = _longhandsBuilder; - _mappings = _mappingsBuilder; - _shorthands = _shorthandsBuilder; + _fonts.Add(PropertyNames.Src, () => new SrcProperty()); + _fonts.Add(PropertyNames.UnicodeRange, () => new UnicodeRangeProperty()); } internal static PropertyFactory Instance => Lazy.Value; private void AddShorthand(string name, ShorthandCreator creator, params string[] longhands) { - _shorthandsBuilder.Add(name, creator); - _mappingsBuilder.Add(name, longhands); + _shorthands.Add(name, creator); + _mappings.Add(name, longhands); } private void AddLogicalShorthand(string name, ShorthandCreator creator, params string[] longhands) @@ -543,11 +803,11 @@ private void AddLogicalShorthand(string name, ShorthandCreator creator, params s private void AddLonghand(string name, LonghandCreator creator, bool animatable = false, bool font = false) { - _longhandsBuilder.Add(name, creator); + _longhands.Add(name, creator); if (animatable) _animatables.Add(name); - if (font) _fontsBuilder.Add(name, creator); + if (font) _fonts.Add(name, creator); } public Property Create(string name) @@ -560,6 +820,8 @@ private static Property CreateCustomProperty(string name) return IsCustomPropertyName(name) ? new CustomProperty(name) : null; } + // A custom property name is at least two dashes followed by at least one code point (CSS Custom + // Properties 1 2). "--" alone is reserved and not a valid custom property. internal static bool IsCustomPropertyName(string name) { return name != null && name.Length > 2 && name[0] == '-' && name[1] == '-'; @@ -570,18 +832,52 @@ public Property CreateFont(string name) return _fonts.TryGetValue(name, out var propertyCreator) ? propertyCreator() : null; } + // The @property descriptors (syntax / initial-value / inherits). Their values have no fixed grammar + // - initial-value depends on the syntax, syntax is an arbitrary string - so each is stored raw via + // an UnknownProperty (Converters.Any). public Property CreatePropertyDescriptor(string name) { - return _propertyDescriptors.TryGetValue(name, out var propertyCreator) ? propertyCreator() : null; + return PropertyFactory.IsPropertyDescriptor(name) ? new UnknownProperty(name) : null; } + private static bool IsPropertyDescriptor(string name) + { + return name.Is(PropertyNames.Syntax) || name.Is(PropertyNames.InitialValue) || + name.Is(PropertyNames.Inherits); + } + + // The @font-palette-values descriptors (font-family / base-palette / override-colors). Their values + // are stored raw via an UnknownProperty (Converters.Any); the resolver re-tokenizes them later. public Property CreateFontPaletteDescriptor(string name) { - return _fontPaletteDescriptors.TryGetValue(name, out var propertyCreator) ? propertyCreator() : null; + return IsFontPaletteDescriptor(name) ? new UnknownProperty(name) : null; + } + + private static bool IsFontPaletteDescriptor(string name) + { + return name.Is(PropertyNames.FontFamily) || name.Is(PropertyNames.BasePalette) || + name.Is(PropertyNames.OverrideColors); } - public Property CreateViewport(string name) + // @font-feature-values blocks hold author-chosen names (e.g. "nice-style: 1 2;"), so any ident is + // accepted; the value must be one or more non-negative integers. + public Property CreateFontFeatureValueDescriptor(string name) { + return string.IsNullOrEmpty(name) ? null : new FontFeatureValueProperty(name); + } + + // The @counter-style descriptors. Values are stored raw via an UnknownProperty (Converters.Any), + // except `system`, whose small grammar is validated at parse time. Unknown descriptors are dropped. + public Property CreateCounterStyleDescriptor(string name) + { + if (name.Isi(CounterSystemName)) return new CounterSystemDescriptorProperty(); + + return Array.Exists(CounterStyleRule.DescriptorNames, d => d.Isi(name)) ? new UnknownProperty(name) : null; + } + + private const string CounterSystemName = "system"; + + public Property CreateViewport(string name) { var feature = MediaFeatureFactory.Instance.Create(name); return feature != null ? new FeatureProperty(feature) : null; @@ -618,6 +914,8 @@ public bool IsAnimatable(string name) public string[] GetLonghands(string name) { + if (name.Is(PropertyNames.All)) return _allLonghands.Value; + return _mappings.TryGetValue(name, out var mapping) ? mapping : Array.Empty(); @@ -626,9 +924,18 @@ public string[] GetLonghands(string name) public IEnumerable GetShorthands(string name) { return from mapping in _mappings - where !_logicalShorthands.Contains(mapping.Key) - && mapping.Value.Contains(name, StringComparison.OrdinalIgnoreCase) - select mapping.Key; + where !_logicalShorthands.Contains(mapping.Key) + && mapping.Value.Contains(name, StringComparison.OrdinalIgnoreCase) + select mapping.Key; + } + + private string[] GetAllLonghands() + { + return _longhands + .Where(m => !m.Key.Is(PropertyNames.Direction) && !m.Key.Is(PropertyNames.UnicodeBidirectional) + && m.Value().Name.Is(m.Key)) + .Select(m => m.Key) + .ToArray(); } private delegate Property LonghandCreator(); diff --git a/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoClassSelectorFactory.cs b/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoClassSelectorFactory.cs index 19a77715f..c34bf4cfd 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoClassSelectorFactory.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoClassSelectorFactory.cs @@ -7,16 +7,32 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class PseudoClassSelectorFactory { private static readonly Lazy Lazy = - new Lazy(() => new PseudoClassSelectorFactory()); + new(() => + { + var factory = new PseudoClassSelectorFactory(); + Selectors.Add(PseudoElementNames.Before, + PseudoElementSelectorFactory.Instance.Create(PseudoElementNames.Before)); + Selectors.Add(PseudoElementNames.After, + PseudoElementSelectorFactory.Instance.Create(PseudoElementNames.After)); + Selectors.Add(PseudoElementNames.FirstLine, + PseudoElementSelectorFactory.Instance.Create(PseudoElementNames.FirstLine)); + Selectors.Add(PseudoElementNames.FirstLetter, + PseudoElementSelectorFactory.Instance.Create(PseudoElementNames.FirstLetter)); + return factory; + } + ); #region Selectors - private static Dictionary BuildSelectors() - { - var selectors = new HashSet(StringComparer.OrdinalIgnoreCase) + private static readonly Dictionary Selectors = + new HashSet(StringComparer.OrdinalIgnoreCase) { PseudoClassNames.Root, PseudoClassNames.Scope, + PseudoClassNames.FirstOfType, + PseudoClassNames.LastOfType, + PseudoClassNames.FirstChild, + PseudoClassNames.LastChild, PseudoClassNames.Empty, PseudoClassNames.AnyLink, PseudoClassNames.Link, @@ -43,38 +59,43 @@ private static Dictionary BuildSelectors() PseudoClassNames.OutOfRange, PseudoClassNames.Optional, PseudoClassNames.Shadow, + PseudoClassNames.Host, + PseudoClassNames.Defined, + PseudoClassNames.Modal, + PseudoClassNames.Open, + PseudoClassNames.PopoverOpen, + PseudoClassNames.Fullscreen, + PseudoClassNames.PictureInPicture, + PseudoClassNames.UserValid, + PseudoClassNames.UserInvalid, + PseudoClassNames.Autofill, + PseudoClassNames.TargetWithin, + PseudoClassNames.LocalLink, + PseudoClassNames.Current, + PseudoClassNames.Past, + PseudoClassNames.Future, + PseudoClassNames.Playing, + PseudoClassNames.Paused, + PseudoClassNames.Seeking, + PseudoClassNames.Buffering, + PseudoClassNames.Stalled, + PseudoClassNames.Muted, + PseudoClassNames.VolumeLocked, } .ToDictionary(x => x, PseudoClassSelector.Create); - selectors.Add(PseudoElementNames.Before, - PseudoElementSelectorFactory.Instance.Create(PseudoElementNames.Before)); - selectors.Add(PseudoElementNames.After, - PseudoElementSelectorFactory.Instance.Create(PseudoElementNames.After)); - selectors.Add(PseudoElementNames.FirstLine, - PseudoElementSelectorFactory.Instance.Create(PseudoElementNames.FirstLine)); - selectors.Add(PseudoElementNames.FirstLetter, - PseudoElementSelectorFactory.Instance.Create(PseudoElementNames.FirstLetter)); - - // :first-child (CSS 2.1 §5.11.1) and friends are their nth-*() function with An+B = 1. - // Kind is never null, same as what ChildFunctionState.Produce() creates. - selectors.Add(PseudoClassNames.FirstChild, new FirstChildSelector().With(0, 1, AllSelector.Create())); - selectors.Add(PseudoClassNames.LastChild, new LastChildSelector().With(0, 1, AllSelector.Create())); - selectors.Add(PseudoClassNames.FirstOfType, new FirstTypeSelector().With(0, 1, AllSelector.Create())); - selectors.Add(PseudoClassNames.LastOfType, new LastTypeSelector().With(0, 1, AllSelector.Create())); - selectors.Add(PseudoClassNames.OnlyChild, new OnlyChildSelector()); - selectors.Add(PseudoClassNames.OnlyType, new OnlyOfTypeSelector()); - - return selectors; - } - - private static readonly Dictionary Selectors = BuildSelectors(); - #endregion + private static readonly ISelector OnlyChild = new OnlyChildSelector(); + private static readonly ISelector OnlyOfType = new OnlyOfTypeSelector(); + internal static PseudoClassSelectorFactory Instance => Lazy.Value; public ISelector Create(string name) { + if (name.Equals(PseudoClassNames.OnlyChild, StringComparison.OrdinalIgnoreCase)) return OnlyChild; + if (name.Equals(PseudoClassNames.OnlyType, StringComparison.OrdinalIgnoreCase)) return OnlyOfType; + return Selectors.TryGetValue(name, out var selector) ? selector : null; } } diff --git a/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoElementSelectorFactory.cs b/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoElementSelectorFactory.cs index 4d581eccb..deb6b842d 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoElementSelectorFactory.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoElementSelectorFactory.cs @@ -7,7 +7,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class PseudoElementSelectorFactory { private static readonly Lazy Lazy = - new Lazy(() => new PseudoElementSelectorFactory()); + new(() => new PseudoElementSelectorFactory()); private readonly StylesheetParser _parser; @@ -16,6 +16,8 @@ internal sealed class PseudoElementSelectorFactory private readonly Dictionary _selectors = new HashSet(StringComparer.OrdinalIgnoreCase) { + //TODO some lack implementation (selection, content, ...) + // some implementations are dubious (first-line, first-letter, ...) PseudoElementNames.Before, PseudoElementNames.After, PseudoElementNames.Marker, @@ -23,9 +25,18 @@ internal sealed class PseudoElementSelectorFactory PseudoElementNames.FirstLine, PseudoElementNames.FirstLetter, PseudoElementNames.Content, + PseudoElementNames.Placeholder, + PseudoElementNames.Backdrop, + PseudoElementNames.FileSelectorButton, + PseudoElementNames.DetailsContent, + PseudoElementNames.TargetText, + PseudoElementNames.SpellingError, + PseudoElementNames.GrammarError, + PseudoElementNames.Cue, + PseudoElementNames.CueRegion, + PseudoElementNames.ViewTransition, } - .ToDictionary(x => x, PseudoElementSelector.Create) - ; + .ToDictionary(x => x, PseudoElementSelector.Create); #endregion diff --git a/Source/HtmlRenderer/Core/CssEngine/Factories/VendorPropertyAliases.cs b/Source/HtmlRenderer/Core/CssEngine/Factories/VendorPropertyAliases.cs new file mode 100644 index 000000000..f62359e90 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Factories/VendorPropertyAliases.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The legacy vendor-prefixed (-webkit-, -moz-) and pre-standard spellings of properties that + /// took the standard grammar. Only consulted when is on. + /// An alias is resolved to its standard name before a property is created, so the property object (and + /// everything downstream of it) only ever sees the standard name. Aliases of properties that are not registered + /// resolve to nothing and are dropped like any other unknown declaration. + /// + internal static class VendorPropertyAliases + { + private static readonly Dictionary Table = Build(); + + // Vendor properties that are accepted and stored verbatim: they have no standard counterpart ExCSS models + // (or a different legacy grammar), but real-world stylesheets are full of them and dropping them would lose data. + private static readonly HashSet AcceptedOnly = new(StringComparer.OrdinalIgnoreCase) + { + "-webkit-font-smoothing", "-moz-osx-font-smoothing", + "-webkit-text-size-adjust", "-moz-text-size-adjust", "-ms-text-size-adjust", "text-size-adjust", + "-webkit-tap-highlight-color", "-webkit-touch-callout", "-webkit-overflow-scrolling", + "-webkit-user-drag", "-webkit-user-select", "-moz-user-select", "-ms-user-select", + "-webkit-text-stroke", "-webkit-text-stroke-width", "-webkit-text-stroke-color", + "-webkit-text-fill-color", "-webkit-text-security", "-webkit-box-reflect" + }; + + /// Resolves to its standard name, or returns it unchanged if it is not an alias. + internal static string Canonicalize(string name) + { + return name != null && name.Length > 1 && Table.TryGetValue(name, out var canonical) ? canonical : name; + } + + /// + /// Like , but only when the standard property is actually registered; an alias of + /// an unregistered property keeps its own spelling (it may be an accepted-only property). + /// + internal static string Resolve(string name) + { + var canonical = Canonicalize(name); + + return ReferenceEquals(canonical, name) || PropertyFactory.Instance.Create(canonical) == null + ? name + : canonical; + } + + /// + /// Creates the property for allowing vendor spellings: the name as written, then its + /// standard name, then the legacy -webkit-box-* container properties, then the accepted-only vendor + /// properties. Returns when none applies. + /// + internal static Property Create(string name) + { + var property = PropertyFactory.Instance.Create(name); + if (property != null) return property; + + var canonical = Canonicalize(name); + if (!ReferenceEquals(canonical, name)) + { + property = PropertyFactory.Instance.Create(canonical); + if (property != null) return property; + } + + var legacyBox = LegacyBoxProperty.BaseNameOf(name); + if (legacyBox != null) return new LegacyBoxProperty(name, legacyBox); + + return name != null && AcceptedOnly.Contains(name) ? new UnknownProperty(name) : null; + } + + private static Dictionary Build() + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["-webkit-backdrop-filter"] = "backdrop-filter", + ["-webkit-line-clamp"] = "line-clamp", + ["color-adjust"] = "print-color-adjust", + ["-webkit-user-select"] = "user-select", + ["-moz-user-select"] = "user-select", + ["-ms-user-select"] = "user-select", + ["-webkit-text-size-adjust"] = "text-size-adjust", + ["-moz-text-size-adjust"] = "text-size-adjust", + ["-ms-text-size-adjust"] = "text-size-adjust", + // Prince's own spellings of properties that became standard. + ["-prince-hyphenate-character"] = "hyphenate-character", + ["-prince-hyphenate-limit-lines"] = "hyphenate-limit-lines", + ["-prince-bookmark-level"] = "bookmark-level", + ["-prince-bookmark-label"] = "bookmark-label", + ["-prince-bookmark-state"] = "bookmark-state", + ["-prince-bookmark-target"] = "bookmark-target", + // The legacy column-break-* names are aliases of break-*; their values are translated by + // VendorValueAliases (always -> column, avoid -> avoid-column). + ["-webkit-column-break-before"] = "break-before", + ["-webkit-column-break-after"] = "break-after", + ["-webkit-column-break-inside"] = "break-inside", + // WebKit's logical box properties predate margin-inline-* / padding-inline-*. + ["-webkit-margin-start"] = "margin-inline-start", + ["-moz-margin-start"] = "margin-inline-start", + ["-webkit-margin-end"] = "margin-inline-end", + ["-moz-margin-end"] = "margin-inline-end", + ["-webkit-padding-start"] = "padding-inline-start", + ["-moz-padding-start"] = "padding-inline-start", + ["-webkit-padding-end"] = "padding-inline-end", + ["-moz-padding-end"] = "padding-inline-end" + }; + + // Properties that shipped behind -webkit- and/or -moz- before unprefixing and kept the standard grammar. + // Standard names that are not registered (yet) are harmless: Create() finds nothing and the declaration + // is dropped as an unknown one. + var prefixed = new[] + { + "transform", "transform-origin", "transform-style", + "perspective", "perspective-origin", "backface-visibility", + "box-shadow", "box-sizing", "box-decoration-break", "mask", "initial-letter", + "border-radius", "border-top-left-radius", "border-top-right-radius", + "border-bottom-left-radius", "border-bottom-right-radius", + "border-image", + "background-clip", "background-origin", "background-size", + "print-color-adjust", + "clip-path", "filter", "opacity", + "columns", "column-count", "column-width", "column-gap", + "column-fill", "column-span", "column-rule", + "column-rule-width", "column-rule-style", "column-rule-color", + "hyphens", "tab-size", + "flex", "flex-flow", "flex-direction", "flex-wrap", + "flex-grow", "flex-shrink", "flex-basis", "order", + "align-items", "align-self", "align-content", "justify-content", + "text-decoration", "text-decoration-line", "text-decoration-style", + "text-decoration-color", "text-decoration-skip-ink", + "text-emphasis", "text-emphasis-style", "text-emphasis-color", "text-emphasis-position", + "font-feature-settings", "font-kerning", + "writing-mode", "text-orientation", + "mask-image", "mask-mode", "mask-repeat", "mask-position", + "mask-size", "mask-origin", "mask-clip", "mask-composite", + "animation", "animation-name", "animation-duration", "animation-timing-function", + "animation-delay", "animation-iteration-count", "animation-direction", + "animation-fill-mode", "animation-play-state", + "transition", "transition-property", "transition-duration", + "transition-timing-function", "transition-delay" + }; + + foreach (var name in prefixed) + { + map["-webkit-" + name] = name; + map["-moz-" + name] = name; + } + + // The item side of the 2009 flexbox model: box-flex's and box-ordinal-group's mean the + // same thing as flex-grow and order. The container side needs LegacyBoxProperty. + foreach (var prefix in new[] {"-webkit-", "-moz-"}) + { + map[prefix + "box-flex"] = "flex-grow"; + map[prefix + "box-ordinal-group"] = "order"; + } + + return map; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Factories/VendorValueAliases.cs b/Source/HtmlRenderer/Core/CssEngine/Factories/VendorValueAliases.cs new file mode 100644 index 000000000..c6fe800d8 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Factories/VendorValueAliases.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Legacy vendor-prefixed keyword values (display: -webkit-flex, position: -webkit-sticky) + /// and legacy keyword vocabularies (-webkit-column-break-before: always) that mean exactly a standard + /// keyword. Applied to a declaration's value before it is validated, only when vendor-prefix aliasing is enabled. + /// + internal static class VendorValueAliases + { + private static readonly Dictionary DisplayValues = new(StringComparer.OrdinalIgnoreCase) + { + ["-webkit-flex"] = Keywords.Flex, + ["-webkit-inline-flex"] = Keywords.InlineFlex, + ["-ms-flexbox"] = Keywords.Flex, + ["-ms-inline-flexbox"] = Keywords.InlineFlex, + // The 2009 flexbox model. The legacy -webkit-box-orient: vertical (block-stacking) case needs layout + // knowledge; the box itself maps to a flex container. + ["-webkit-box"] = Keywords.Flex, + ["-moz-box"] = Keywords.Flex, + ["-webkit-inline-box"] = Keywords.InlineFlex, + ["-moz-inline-box"] = Keywords.InlineFlex + }; + + /// + /// Returns with vendor keyword spellings replaced, or + /// itself when nothing needs replacing. + /// + /// The property name as the author wrote it. + /// The standard name of the property created for it. + internal static TokenValue Normalize(string writtenName, string propertyName, TokenValue value) + { + if (value == null || value.Count == 0) return value; + + Func map = null; + + if (propertyName.Isi(PropertyNames.Display)) + map = t => DisplayValues.TryGetValue(t.Data, out var standard) ? standard : null; + else if (propertyName.Isi(PropertyNames.Position)) + map = t => t.Data.Isi("-webkit-sticky") ? Keywords.Sticky : null; + else if (writtenName.Isi("-webkit-column-break-before") || writtenName.Isi("-webkit-column-break-after")) + map = t => t.Data.Isi(Keywords.Always) ? Keywords.Column + : t.Data.Isi(Keywords.Avoid) ? Keywords.AvoidColumn + : null; + else if (writtenName.Isi("-webkit-column-break-inside")) + map = t => t.Data.Isi(Keywords.Avoid) ? Keywords.AvoidColumn : null; + else if (propertyName.Isi(PropertyNames.TransitionProperty) || propertyName.Isi(PropertyNames.Transition)) + map = t => + { + var canonical = VendorPropertyAliases.Resolve(t.Data); + return ReferenceEquals(canonical, t.Data) ? null : canonical; + }; + + if (map == null) return value; + + List tokens = null; + for (var i = 0; i < value.Count; i++) + { + var token = value[i]; + var replacement = token.Type == TokenType.Ident ? map(token) : null; + + if (replacement == null) + { + tokens?.Add(token); + continue; + } + + if (tokens == null) + { + tokens = new List(value.Count); + for (var j = 0; j < i; j++) tokens.Add(value[j]); + } + + tokens.Add(new Token(TokenType.Ident, replacement, token.Position)); + } + + if (tokens == null) return value; + + return new TokenValue(tokens) {StylesheetText = value.StylesheetText}; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/FilterGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/FilterGrammar.cs new file mode 100644 index 000000000..cd4db7437 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/FilterGrammar.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Grammar of the filter / backdrop-filter value (Filter Effects 1 §3): + /// none | <filter-value-list>, where each entry is a url() reference or one of + /// blur() brightness() contrast() drop-shadow() grayscale() hue-rotate() invert() opacity() saturate() sepia(). + /// The entries are separated by whitespace only. Arguments are kept as authored text. + /// + internal static class FilterGrammar + { + private const string Blur = "blur"; + private const string Brightness = "brightness"; + private const string Contrast = "contrast"; + private const string DropShadow = "drop-shadow"; + private const string Grayscale = "grayscale"; + private const string HueRotate = "hue-rotate"; + private const string Invert = "invert"; + private const string Opacity = "opacity"; + private const string Saturate = "saturate"; + private const string Sepia = "sepia"; + + private static readonly HashSet KnownFunctions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + Blur, Brightness, Contrast, DropShadow, Grayscale, HueRotate, Invert, Opacity, Saturate, Sepia + }; + + private static readonly IValueConverter AngleCalc = new CalcValueConverter(CalcCategory.Angle); + private static readonly IValueConverter LengthCalc = new CalcValueConverter(CalcCategory.Length); + + private static readonly IValueConverter NumberOrPercentCalc = + new CalcValueConverter(CalcCategory.Number | CalcCategory.Percentage); + + /// + /// One parsed filter entry. is the lower-cased function name, or url for a + /// url() reference. holds the raw authored text of each argument. + /// + internal sealed class FilterFunction + { + public FilterFunction(string name, IReadOnlyList arguments) + { + Name = name; + Arguments = arguments; + } + + public string Name { get; } + public IReadOnlyList Arguments { get; } + } + + /// + /// Parses the tokens of a filter value. Returns null when invalid, an empty list for the keyword + /// none, otherwise the entries in source order. + /// + internal static List TryParse(IEnumerable value) + { + var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (tokens.Length == 0) return null; + + if (tokens.Length == 1 && tokens[0].Type == TokenType.Ident && tokens[0].Data.Isi(Keywords.None)) + return new List(); + + var result = new List(); + + foreach (var token in tokens) + { + if (token.Type == TokenType.Url) + { + result.Add(new FilterFunction(FunctionNames.Url, new[] { token.Data })); + continue; + } + + if (!(token is FunctionToken function) || !KnownFunctions.Contains(function.Data)) return null; + + var name = function.Data.ToLowerInvariant(); + var args = function.ArgumentTokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (!TryParseArguments(name, args, out var arguments)) return null; + + result.Add(new FilterFunction(name, arguments)); + } + + return result; + } + + private static bool TryParseArguments(string name, Token[] args, out List arguments) + { + if (name == DropShadow) return TryParseDropShadow(args, out arguments); + + arguments = new List(); + + // Every other function takes one optional argument. + if (args.Length == 0) return true; + if (args.Length != 1) return false; + + var token = args[0]; + var valid = name == HueRotate ? IsAngle(token) + : name == Blur ? IsNonNegativeLength(token) + : IsNonNegativeNumberOrPercentage(token); + + if (valid) arguments.Add(token.ToValue()); + + return valid; + } + + // drop-shadow( [ ? && {2,3} ] ): the colour leads or trails the lengths, the optional + // third length (blur radius) is non-negative. + private static bool TryParseDropShadow(Token[] args, out List arguments) + { + arguments = null; + + var lengths = new List(); + var colors = new List(); + + foreach (var token in args) + { + if (IsLength(token)) lengths.Add(token); + else colors.Add(token); + } + + if (lengths.Count < 2 || lengths.Count > 3 || colors.Count > 1) return false; + if (lengths.Count == 3 && IsNegative(lengths[2])) return false; + + if (colors.Count == 1) + { + // The lengths must be contiguous, so the colour is first or last. + var index = Array.IndexOf(args, colors[0]); + if (index != 0 && index != args.Length - 1) return false; + if (Converters.CurrentColorConverter.Convert(colors) == null) return false; + } + + arguments = new List(args.Select(t => t.ToValue())); + return true; + } + + private static bool IsLength(Token token) + { + if (token is NumberToken number) return number.Value == 0f; + if (token.Type == TokenType.Dimension) + return Length.GetUnit(((UnitToken)token).Unit.ToLowerInvariant()) != Length.Unit.None; + + return token is FunctionToken && LengthCalc.Convert(new[] { token }) != null; + } + + private static bool IsNegative(Token token) => + (token is NumberToken number && number.Value < 0f) || (token is UnitToken unit && unit.Value < 0f); + + private static bool IsAngle(Token token) + { + if (token is NumberToken number) return number.Value == 0f; // unitless zero, CSS Values 4 §6.1 + if (token.Type == TokenType.Dimension) + return Angle.GetUnit(((UnitToken)token).Unit.ToLowerInvariant()) != Angle.Unit.None; + + return token is FunctionToken && AngleCalc.Convert(new[] { token }) != null; + } + + private static bool IsNonNegativeLength(Token token) => IsLength(token) && !IsNegative(token); + + private static bool IsNonNegativeNumberOrPercentage(Token token) + { + if (token is NumberToken number) return number.Value >= 0f; + if (token.Type == TokenType.Percentage) return ((UnitToken)token).Value >= 0f; + + return token is FunctionToken && NumberOrPercentCalc.Convert(new[] { token }) != null; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/FontFeatureTagListGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/FontFeatureTagListGrammar.cs new file mode 100644 index 000000000..fb2a4c8a3 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/FontFeatureTagListGrammar.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The tag-list grammars of font-feature-settings and font-variation-settings (CSS Fonts 4 + /// 6.9 and 6.10). + /// + internal static class FontFeatureTagListGrammar + { + public static readonly IValueConverter FeatureSettingsConverter = + new FontGrammarValueConverter(TryParseFeatureSettings); + + public static readonly IValueConverter VariationSettingsConverter = + new FontGrammarValueConverter(TryParseVariationSettings); + + /// normal | <feature-tag-value>#, where <feature-tag-value> = + /// <opentype-tag> [ <integer [0,∞]> | on | off ]?. + public static bool TryParseFeatureSettings(IReadOnlyList tokens, out string text) + { + return TryParseList(tokens, TryParseFeatureValue, out text); + } + + /// normal | [ <opentype-tag> <number> ]# + public static bool TryParseVariationSettings(IReadOnlyList tokens, out string text) + { + return TryParseList(tokens, TryParseAxisValue, out text); + } + + private delegate bool ItemParser(IReadOnlyList item, out string text); + + private static bool TryParseList(IReadOnlyList tokens, ItemParser itemParser, out string text) + { + text = null; + + if (tokens.Count == 1 && FontGrammar.IsIdent(tokens[0], Keywords.Normal)) + { + text = Keywords.Normal; + return true; + } + + var items = new List(); + + foreach (var item in ValueExtensions.ToList(tokens)) + { + var significant = item.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (!itemParser(significant, out var itemText)) return false; + + items.Add(itemText); + } + + if (items.Count == 0) return false; + + text = string.Join(", ", items); + return true; + } + + private static bool TryParseFeatureValue(IReadOnlyList item, out string text) + { + text = null; + + if (item.Count == 0 || item.Count > 2 || !FontGrammar.IsOpenTypeTag(item[0])) return false; + + var tag = FontGrammar.Quote(item[0].Data); + + if (item.Count == 1) + { + text = tag; + return true; + } + + var value = item[1]; + + if (value.Type == TokenType.Ident && (value.Data.Isi("on") || value.Data.Isi("off"))) + { + text = tag + " " + value.Data.ToLowerInvariant(); + return true; + } + + if (value is NumberToken number && number.IsInteger && + int.TryParse(number.Data, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var integer) && + integer >= 0) + { + text = tag + " " + integer.ToString(CultureInfo.InvariantCulture); + return true; + } + + return false; + } + + private static bool TryParseAxisValue(IReadOnlyList item, out string text) + { + text = null; + + if (item.Count != 2 || !FontGrammar.IsOpenTypeTag(item[0]) || !(item[1] is NumberToken number)) + return false; + + text = FontGrammar.Quote(item[0].Data) + " " + number.Data; + return true; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/FontLanguageOverrideGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/FontLanguageOverrideGrammar.cs new file mode 100644 index 000000000..d2ff01d4f --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/FontLanguageOverrideGrammar.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The value grammar of font-language-override (CSS Fonts 4 6.6): normal | <string>, + /// the string being an OpenType language system tag of one to four printable ASCII characters. + /// + internal static class FontLanguageOverrideGrammar + { + public static readonly IValueConverter Converter = new FontGrammarValueConverter(TryParse); + + public static bool TryParse(IReadOnlyList tokens, out string text) + { + text = null; + + if (tokens.Count != 1) return false; + + if (FontGrammar.IsIdent(tokens[0], Keywords.Normal)) + { + text = Keywords.Normal; + return true; + } + + if (tokens[0].Type != TokenType.String) return false; + + var data = tokens[0].Data; + + if (data.Length < 1 || data.Length > 4 || !data.All(FontGrammar.IsPrintableAscii)) return false; + + text = FontGrammar.Quote(data); + return true; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/FontSizeAdjustGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/FontSizeAdjustGrammar.cs new file mode 100644 index 000000000..1a2e8006a --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/FontSizeAdjustGrammar.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The value grammar of font-size-adjust (CSS Fonts 5 3.2): + /// none | [ ex-height | cap-height | ch-width | ic-width | ic-height ]? [ from-font | <number [0,∞]> ]. + /// + internal static class FontSizeAdjustGrammar + { + private static readonly string[] Metrics = {"ex-height", "cap-height", "ch-width", "ic-width", "ic-height"}; + + public static readonly IValueConverter Converter = new FontGrammarValueConverter(TryParse); + + public static bool TryParse(IReadOnlyList tokens, out string text) + { + text = null; + + if (tokens.Count == 0 || tokens.Count > 2) return false; + + if (tokens.Count == 1 && FontGrammar.IsIdent(tokens[0], Keywords.None)) + { + text = Keywords.None; + return true; + } + + var index = 0; + var metric = string.Empty; + + if (tokens[0].Type == TokenType.Ident && FontGrammar.Contains(Metrics, tokens[0].Data)) + { + metric = tokens[0].Data.ToLowerInvariant() + " "; + index = 1; + } + + if (index != tokens.Count - 1) return false; + + var factor = tokens[index]; + + if (FontGrammar.IsIdent(factor, "from-font")) + { + text = metric + "from-font"; + return true; + } + + if (factor is NumberToken number && number.Value >= 0f) + { + text = metric + number.Data; + return true; + } + + return false; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/FontStretchGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/FontStretchGrammar.cs new file mode 100644 index 000000000..0a1295209 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/FontStretchGrammar.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The <percentage [0,∞]> form of font-stretch (CSS Fonts 4 3.3), accepted next to + /// the nine width keywords. The font shorthand takes the keywords only. + /// + internal static class FontStretchGrammar + { + public static readonly IValueConverter PercentageConverter = new FontGrammarValueConverter(TryParsePercentage); + + public static bool TryParsePercentage(IReadOnlyList tokens, out string text) + { + text = null; + + if (tokens.Count != 1 || !(tokens[0] is UnitToken percentage) || + percentage.Type != TokenType.Percentage || percentage.Value < 0f) + { + return false; + } + + text = percentage.ToValue(); + return true; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/FontVariantAlternatesGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/FontVariantAlternatesGrammar.cs new file mode 100644 index 000000000..7190c9987 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/FontVariantAlternatesGrammar.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The value grammar of font-variant-alternates (CSS Fonts 4 6.8): + /// normal | [ stylistic(<feature-value-name>) || historical-forms || + /// styleset(<feature-value-name>#) || character-variant(<feature-value-name>#) || + /// swash(<feature-value-name>) || ornaments(<feature-value-name>) || + /// annotation(<feature-value-name>) ]. Each of the seven components may appear at most once, in + /// any order. Only the grammar is validated: whether a name is registered by an + /// @font-feature-values rule is a per-element, used-value question and not a parse error. + /// + internal static class FontVariantAlternatesGrammar + { + public const string HistoricalForms = "historical-forms"; + + private static readonly string[] SingleIdentFunctions = {"stylistic", "swash", "ornaments", "annotation"}; + + private static readonly string[] IdentListFunctions = {"styleset", "character-variant"}; + + public static readonly IValueConverter Converter = new FontGrammarValueConverter(TryParse); + + internal static bool IsAlternatesToken(Token token) + { + return token.Type == TokenType.Function && + (FontGrammar.Contains(SingleIdentFunctions, token.Data) || + FontGrammar.Contains(IdentListFunctions, token.Data)) || + FontGrammar.IsIdent(token, HistoricalForms); + } + + public static bool TryParse(IReadOnlyList tokens, out string text) + { + text = null; + + if (tokens.Count == 0) return false; + + if (tokens.Count == 1 && FontGrammar.IsIdent(tokens[0], Keywords.Normal)) + { + text = Keywords.Normal; + return true; + } + + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var clauses = new List(); + + foreach (var token in tokens) + { + var clause = ParseClause(token, seen); + + if (clause == null) return false; + + clauses.Add(clause); + } + + text = string.Join(" ", clauses); + return true; + } + + private static string ParseClause(Token token, HashSet seen) + { + if (token.Type == TokenType.Ident) + { + return FontGrammar.IsIdent(token, HistoricalForms) && seen.Add(HistoricalForms) + ? HistoricalForms + : null; + } + + if (!(token is FunctionToken function) || !seen.Add(function.Data)) return null; + + var name = function.Data.ToLowerInvariant(); + var arguments = function.ArgumentTokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (FontGrammar.Contains(SingleIdentFunctions, name)) + { + return arguments.Length == 1 && arguments[0].Type == TokenType.Ident + ? name + "(" + arguments[0].Data + ")" + : null; + } + + if (!FontGrammar.Contains(IdentListFunctions, name)) return null; + + // # - one or more comma-separated identifiers. + var names = new List(); + var expectName = true; + + foreach (var argument in arguments) + { + if (expectName) + { + if (argument.Type != TokenType.Ident) return null; + names.Add(argument.Data); + } + else if (argument.Type != TokenType.Comma) + { + return null; + } + + expectName = !expectName; + } + + return names.Count > 0 && !expectName ? name + "(" + string.Join(", ", names) + ")" : null; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/FontVariantGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/FontVariantGrammar.cs new file mode 100644 index 000000000..936862a73 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/FontVariantGrammar.cs @@ -0,0 +1,230 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The value grammars of the font-variant-* longhands (CSS Fonts 4 6) and of the + /// font-variant shorthand that combines them. + /// + internal static class FontVariantGrammar + { + public static readonly string[] CapsKeywords = + { + Keywords.Normal, "small-caps", "all-small-caps", "petite-caps", "all-petite-caps", "unicase", + "titling-caps" + }; + + public static readonly string[] PositionKeywords = {Keywords.Normal, "sub", "super"}; + + public static readonly string[] EmojiKeywords = {Keywords.Normal, "text", "emoji", "unicode"}; + + // Each inner array is one mutually exclusive axis. The "||" combinator allows every component at + // most once, so a second keyword from an axis already seen (the same one repeated or its opposite) + // makes the value invalid. + private static readonly string[][] LigatureAxes = + { + new[] {"common-ligatures", "no-common-ligatures"}, + new[] {"discretionary-ligatures", "no-discretionary-ligatures"}, + new[] {"historical-ligatures", "no-historical-ligatures"}, + new[] {"contextual", "no-contextual"} + }; + + private static readonly string[][] NumericAxes = + { + new[] {"lining-nums", "oldstyle-nums"}, + new[] {"proportional-nums", "tabular-nums"}, + new[] {"diagonal-fractions", "stacked-fractions"}, + new[] {"ordinal"}, + new[] {"slashed-zero"} + }; + + private static readonly string[][] EastAsianAxes = + { + new[] {"jis78-forms", "jis83-forms", "jis90-forms", "jis04-forms", "simplified", "traditional"}, + new[] {"full-width", "proportional-width"}, + new[] {"ruby"} + }; + + public static bool TryParseCaps(IReadOnlyList tokens, out string text) => + FontGrammar.TryParseKeyword(tokens, CapsKeywords, out text); + + public static bool TryParsePosition(IReadOnlyList tokens, out string text) => + FontGrammar.TryParseKeyword(tokens, PositionKeywords, out text); + + public static bool TryParseEmoji(IReadOnlyList tokens, out string text) => + FontGrammar.TryParseKeyword(tokens, EmojiKeywords, out text); + + /// normal | none | [ <common-lig-values> || ... ] + public static bool TryParseLigatures(IReadOnlyList tokens, out string text) => + TryParseKeywordsOrAxes(tokens, LigatureAxes, true, out text); + + /// normal | [ <numeric-figure-values> || ... ] + public static bool TryParseNumeric(IReadOnlyList tokens, out string text) => + TryParseKeywordsOrAxes(tokens, NumericAxes, false, out text); + + /// normal | [ <east-asian-variant-values> || <east-asian-width-values> || ruby ] + public static bool TryParseEastAsian(IReadOnlyList tokens, out string text) => + TryParseKeywordsOrAxes(tokens, EastAsianAxes, false, out text); + + private static bool TryParseKeywordsOrAxes(IReadOnlyList tokens, string[][] axes, bool allowNone, + out string text) + { + text = null; + + if (tokens.Count == 1 && FontGrammar.IsIdent(tokens[0], Keywords.Normal)) + { + text = Keywords.Normal; + return true; + } + + if (allowNone && tokens.Count == 1 && FontGrammar.IsIdent(tokens[0], Keywords.None)) + { + text = Keywords.None; + return true; + } + + return TryParseAxes(tokens, axes, out text); + } + + private static bool TryParseAxes(IReadOnlyList tokens, string[][] axes, out string text) + { + text = null; + + if (tokens.Count == 0) return false; + + var seen = new HashSet(); + var keywords = new List(); + + foreach (var token in tokens) + { + if (token.Type != TokenType.Ident) return false; + + var axis = System.Array.FindIndex(axes, a => FontGrammar.Contains(a, token.Data)); + + if (axis < 0 || !seen.Add(axis)) return false; + + keywords.Add(token.Data.ToLowerInvariant()); + } + + text = string.Join(" ", keywords); + return true; + } + + private enum Slot + { + Ligatures, + Alternates, + Caps, + Numeric, + EastAsian, + Position, + Emoji + } + + /// + /// The font-variant shorthand: normal | none | [ <caps> || <ligatures> || + /// <alternates> || <numeric> || <east-asian> || <position> || <emoji> ], the + /// components in any order. normal resets every longhand to normal; none sets + /// font-variant-ligatures to none and every other longhand to normal. Longhands a + /// component list does not mention are null in and + /// (they reset to their initial value). Both arrays follow the slot order + /// of . + /// + public static bool TryParseShorthand(IReadOnlyList input, out string[] texts, out List[] tokens) + { + texts = null; + tokens = null; + + if (input.Count == 0) return false; + + var resultTexts = new string[7]; + var resultTokens = new List[7]; + + if (input.Count == 1 && input[0].Type == TokenType.Ident && + (input[0].Data.Isi(Keywords.Normal) || input[0].Data.Isi(Keywords.None))) + { + var none = input[0].Data.Isi(Keywords.None); + + for (var i = 0; i < resultTexts.Length; i++) + { + var keyword = none && i == (int) Slot.Ligatures ? Keywords.None : Keywords.Normal; + resultTexts[i] = keyword; + resultTokens[i] = new List {new Token(TokenType.Ident, keyword, TextPosition.Empty)}; + } + + texts = resultTexts; + tokens = resultTokens; + return true; + } + + foreach (var token in input) + { + var slot = Classify(token); + + if (slot < 0) return false; + + // Caps, position and emoji each take a single keyword; the other slots accumulate tokens. + var list = resultTokens[slot] ??= new List(); + + if (slot == (int) Slot.Caps || slot == (int) Slot.Position || slot == (int) Slot.Emoji) + { + if (list.Count > 0) return false; + } + else if (list.Count > 0) + { + list.Add(Token.Whitespace); + } + + list.Add(token); + } + + for (var slot = 0; slot < resultTokens.Length; slot++) + { + var list = resultTokens[slot]; + + if (list == null) continue; + + var significant = list.Where(t => t.Type != TokenType.Whitespace).ToArray(); + FontGrammarParser parser = slot switch + { + (int) Slot.Ligatures => TryParseLigatures, + (int) Slot.Alternates => FontVariantAlternatesGrammar.TryParse, + (int) Slot.Numeric => TryParseNumeric, + (int) Slot.EastAsian => TryParseEastAsian, + (int) Slot.Caps => TryParseCaps, + (int) Slot.Position => TryParsePosition, + _ => TryParseEmoji + }; + + if (!parser(significant, out resultTexts[slot])) return false; + } + + texts = resultTexts; + tokens = resultTokens; + return true; + } + + // normal/none are only valid as the sole component, so any occurrence here classifies as invalid. + private static int Classify(Token token) + { + if (token.Type == TokenType.Function) return FontVariantAlternatesGrammar.IsAlternatesToken(token) ? (int) Slot.Alternates : -1; + + if (token.Type != TokenType.Ident) return -1; + + var name = token.Data; + + if (name.Isi(Keywords.Normal) || name.Isi(Keywords.None)) return -1; + + if (FontGrammar.Contains(CapsKeywords, name)) return (int) Slot.Caps; + if (FontGrammar.Contains(PositionKeywords, name)) return (int) Slot.Position; + if (FontGrammar.Contains(EmojiKeywords, name)) return (int) Slot.Emoji; + if (FontVariantAlternatesGrammar.IsAlternatesToken(token)) return (int) Slot.Alternates; + if (LigatureAxes.Any(axis => FontGrammar.Contains(axis, name))) return (int) Slot.Ligatures; + if (NumericAxes.Any(axis => FontGrammar.Contains(axis, name))) return (int) Slot.Numeric; + if (EastAsianAxes.Any(axis => FontGrammar.Contains(axis, name))) return (int) Slot.EastAsian; + + return -1; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Formatting/CompressedStyleFormatter.cs b/Source/HtmlRenderer/Core/CssEngine/Formatting/CompressedStyleFormatter.cs index 0bc6be3e7..5f54a0538 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Formatting/CompressedStyleFormatter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Formatting/CompressedStyleFormatter.cs @@ -103,20 +103,19 @@ private void WriteJoined(StringBuilder sb, string separator, bool first = true) { - using(var writer = new StringWriter(sb)){ - foreach (var element in elements) + using var writer = new StringWriter(sb); + foreach (var element in elements) + { + if (first) + { + first = false; + } + else { - if (first) - { - first = false; - } - else - { - writer.Write(separator); - } - - element.ToCss(writer, this); + writer.Write(separator); } + + element.ToCss(writer, this); } } } diff --git a/Source/HtmlRenderer/Core/CssEngine/GridLineGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/GridLineGrammar.cs index aed72c3e4..5c74f4988 100644 --- a/Source/HtmlRenderer/Core/CssEngine/GridLineGrammar.cs +++ b/Source/HtmlRenderer/Core/CssEngine/GridLineGrammar.cs @@ -3,25 +3,35 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// A <grid-line>, CSS Grid 2 §8.3. + /// + /// A parsed grid <grid-line> (one edge of grid-column/grid-row/grid-area): + /// auto, an integer line number (may be negative — counting from the end edge), or span N. + /// Named lines (<custom-ident>) are populated by the named-line grammar extension. + /// internal sealed class GridLine { public bool IsAuto { get; private set; } public bool IsSpan { get; private set; } - /// The line number, the span count, or which line of the name it is. + /// The line number (for a line reference), the span count (when ), or + /// the 1-based Nth index for a named line (defaults to 1). public int Value { get; private set; } = 1; + /// The custom-ident of a named line reference, or null when this is not a named line. public string Name { get; private set; } - public static readonly GridLine Auto = new GridLine { IsAuto = true }; - public static GridLine Span(int n) => new GridLine { IsSpan = true, Value = n }; - public static GridLine Line(int n) => new GridLine { Value = n }; - public static GridLine Named(string name) => new GridLine { Name = name, Value = 1 }; - public static GridLine NamedNth(string name, int n) => new GridLine { Name = name, Value = n }; + public static readonly GridLine Auto = new() { IsAuto = true }; + public static GridLine Span(int n) => new() { IsSpan = true, Value = n }; + public static GridLine Line(int n) => new() { Value = n }; + public static GridLine Named(string name) => new() { Name = name, Value = 1 }; + public static GridLine NamedNth(string name, int n) => new() { Name = name, Value = n }; } - /// The <grid-line> of the grid-row-*/grid-column-* longhands, CSS Grid 2 §8.3. + /// + /// Shared grammar for a single grid <grid-line> value. Used to validate the + /// grid-column-start/-end/grid-row-start/-end longhands and the placement + /// shorthands. + /// internal static class GridLineGrammar { internal static GridLine TryParse(IReadOnlyList tokens) @@ -29,30 +39,37 @@ internal static GridLine TryParse(IReadOnlyList tokens) var toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); if (toks.Length == 0) return null; + // auto if (toks.Length == 1 && toks[0].Type == TokenType.Ident && toks[0].Data.Isi(Keywords.Auto)) return GridLine.Auto; - if (toks.Length == 1 && toks[0] is NumberToken number && number.IsInteger && number.IntegerValue != 0) + // + if (toks.Length == 1 && toks[0] is NumberToken { IsInteger: true } number && number.IntegerValue != 0) return GridLine.Line(number.IntegerValue); + // span if (toks.Length == 2 && toks[0].Type == TokenType.Ident && toks[0].Data.Isi(Keywords.Span) - && toks[1] is NumberToken spanCount && spanCount.IsInteger && spanCount.IntegerValue >= 1) + && toks[1] is NumberToken { IsInteger: true } spanCount && spanCount.IntegerValue >= 1) return GridLine.Span(spanCount.IntegerValue); + // — a named line reference. if (toks.Length == 1 && IsCustomIdent(toks[0])) return GridLine.Named(toks[0].Data); + // / — the Nth line with that name (order-independent). if (toks.Length == 2) { - if (IsCustomIdent(toks[0]) && toks[1] is NumberToken n1 && n1.IsInteger && n1.IntegerValue != 0) + if (IsCustomIdent(toks[0]) && toks[1] is NumberToken { IsInteger: true } n1 && n1.IntegerValue != 0) return GridLine.NamedNth(toks[0].Data, n1.IntegerValue); - if (IsCustomIdent(toks[1]) && toks[0] is NumberToken n2 && n2.IsInteger && n2.IntegerValue != 0) + if (IsCustomIdent(toks[1]) && toks[0] is NumberToken { IsInteger: true } n2 && n2.IntegerValue != 0) return GridLine.NamedNth(toks[1].Data, n2.IntegerValue); } return null; } + /// Whether the token is a <custom-ident> usable as a grid line name — an ident + /// that is not one of the reserved keywords a grid-line value or the CSS-wide keywords may take. private static bool IsCustomIdent(Token token) { if (token.Type != TokenType.Ident) return false; diff --git a/Source/HtmlRenderer/Core/CssEngine/GridTemplateAreasGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/GridTemplateAreasGrammar.cs index 55bdf6033..773604952 100644 --- a/Source/HtmlRenderer/Core/CssEngine/GridTemplateAreasGrammar.cs +++ b/Source/HtmlRenderer/Core/CssEngine/GridTemplateAreasGrammar.cs @@ -3,22 +3,26 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// A grid-template-areas value, CSS Grid 2 §7.3. + /// + /// A parsed grid-template-areas value (CSS Grid §7.3): a rectangular grid of named cells. Each + /// named area's 0-based inclusive bounding rectangle is recorded; a . (or a run of only dots) is + /// an unnamed empty cell. + /// internal sealed class GridAreas { - public int RowCount { get; set; } - public int ColCount { get; set; } + public int RowCount { get; internal set; } + public int ColCount { get; internal set; } - /// The area name per [row, col], null for a null cell token. - public string[,] Cells { get; set; } + /// The area name at each cell ([row, col]), or null for an empty . cell. + public string[,] Cells { get; internal set; } - /// 0-based inclusive cell rectangle per area name. - public IReadOnlyDictionary Areas { get; set; } + /// Each named area's 0-based inclusive rectangle (rowStart, colStart, rowEnd, colEnd). + public IReadOnlyDictionary Areas { get; internal set; } } /// - /// The <string>+ of grid-template-areas, CSS Grid 2 §7.3. Returns null for none - /// too, the property handles that keyword. + /// Shared grammar for a grid-template-areas value (a series of quoted strings). Returns null for + /// anything invalid — including the none keyword, which the property accepts separately. /// internal static class GridTemplateAreasGrammar { @@ -27,6 +31,7 @@ internal static GridAreas TryParse(IReadOnlyList tokens) var toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); if (toks.Length == 0) return null; + // Every token must be a string; each string is one row. if (toks.Any(t => t.Type != TokenType.String)) return null; var rows = new List(toks.Length); @@ -35,15 +40,16 @@ internal static GridAreas TryParse(IReadOnlyList tokens) { var cells = ((StringToken)t).Data .Split((char[])null, System.StringSplitOptions.RemoveEmptyEntries); - if (cells.Length == 0) return null; + if (cells.Length == 0) return null; // an empty string is invalid if (colCount < 0) colCount = cells.Length; - else if (cells.Length != colCount) return null; + else if (cells.Length != colCount) return null; // ragged rows are invalid var normalized = new string[colCount]; for (var c = 0; c < colCount; c++) { if (IsEmptyCell(cells[c])) { normalized[c] = null; continue; } - if (cells[c].Contains('.')) return null; + // A named cell is a — a mixed "a.b" is invalid, not a name. + if (cells[c].IndexOf('.') >= 0) return null; normalized[c] = cells[c]; } rows.Add(normalized); @@ -66,11 +72,12 @@ internal static GridAreas TryParse(IReadOnlyList tokens) bounds[name] = (r, c, r, c); } - // Every cell in an area's bounding box has to carry its name. - foreach (var entry in bounds) + // Each named area must be a single filled rectangle: every cell in its bounding box carries the + // name (which also guarantees no cell with the name lies outside the box). + foreach (var pair in bounds) { - var name = entry.Key; - var b = entry.Value; + var name = pair.Key; + var b = pair.Value; for (var r = b.R1; r <= b.R2; r++) for (var c = b.C1; c <= b.C2; c++) if (grid[r, c] != name) return null; @@ -79,6 +86,7 @@ internal static GridAreas TryParse(IReadOnlyList tokens) return new GridAreas { RowCount = rowCount, ColCount = colCount, Cells = grid, Areas = bounds }; } + /// A cell of only U+002E FULL STOP characters (., ...) is an empty cell. private static bool IsEmptyCell(string cell) => cell.All(ch => ch == '.'); } } diff --git a/Source/HtmlRenderer/Core/CssEngine/GridTrackListGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/GridTrackListGrammar.cs index f60120aca..27740bd3d 100644 --- a/Source/HtmlRenderer/Core/CssEngine/GridTrackListGrammar.cs +++ b/Source/HtmlRenderer/Core/CssEngine/GridTrackListGrammar.cs @@ -3,23 +3,28 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// CSS Grid 2 §7.2.1. + /// The kind of a single grid <track-size>/<track-breadth> + /// (CSS Grid Layout Module Level 1/2 §7.2). internal enum GridTrackKind { - Length, // authored text in Value, a length or a length-percentage math function - Percent, // authored text in Value - Flex, // factor in Flex + Length, // a (raw component string in Value) + Percent, // a (raw component string in Value) + Flex, // a / fr value (Flex holds the fr number) Auto, MinContent, MaxContent, - FitContent, // argument in Value - Minmax // Min, Max + FitContent, // fit-content() — the argument is in Value + Minmax // minmax(Min, Max) } - /// CSS Grid 2 §7.2.3.2. + /// Which flavour of layout-time repeat() a template carries (CSS Grid §7.2.3.2). internal enum GridAutoRepeatKind { None, AutoFill, AutoFit } - /// A <track-size>, lengths and percentages keep their authored text. + /// + /// A single grid <track-size>. Fixed/percentage/fit-content values keep their authored + /// component string (); fr keeps its numeric factor (); + /// minmax() keeps its two sub-breadths. + /// internal sealed class GridTrackSize { public GridTrackKind Kind { get; private set; } @@ -28,53 +33,72 @@ internal sealed class GridTrackSize public GridTrackSize Min { get; private set; } public GridTrackSize Max { get; private set; } - public static GridTrackSize Length(string v) => new GridTrackSize { Kind = GridTrackKind.Length, Value = v }; - public static GridTrackSize Percent(string v) => new GridTrackSize { Kind = GridTrackKind.Percent, Value = v }; - public static GridTrackSize FlexFactor(double fr) => new GridTrackSize { Kind = GridTrackKind.Flex, Flex = fr }; - public static readonly GridTrackSize Auto = new GridTrackSize { Kind = GridTrackKind.Auto }; - public static readonly GridTrackSize MinContent = new GridTrackSize { Kind = GridTrackKind.MinContent }; - public static readonly GridTrackSize MaxContent = new GridTrackSize { Kind = GridTrackKind.MaxContent }; - public static GridTrackSize FitContentTo(string v) => new GridTrackSize { Kind = GridTrackKind.FitContent, Value = v }; - public static GridTrackSize Minmax(GridTrackSize min, GridTrackSize max) => new GridTrackSize { Kind = GridTrackKind.Minmax, Min = min, Max = max }; + public static GridTrackSize Length(string v) => new() { Kind = GridTrackKind.Length, Value = v }; + public static GridTrackSize Percent(string v) => new() { Kind = GridTrackKind.Percent, Value = v }; + public static GridTrackSize FlexFactor(double fr) => new() { Kind = GridTrackKind.Flex, Flex = fr }; + public static readonly GridTrackSize Auto = new() { Kind = GridTrackKind.Auto }; + public static readonly GridTrackSize MinContent = new() { Kind = GridTrackKind.MinContent }; + public static readonly GridTrackSize MaxContent = new() { Kind = GridTrackKind.MaxContent }; + public static GridTrackSize FitContentTo(string v) => new() { Kind = GridTrackKind.FitContent, Value = v }; + public static GridTrackSize Minmax(GridTrackSize min, GridTrackSize max) => + new() { Kind = GridTrackKind.Minmax, Min = min, Max = max }; } /// - /// A grid-template-columns/-rows value. repeat(<integer>, …) is expanded into - /// , an auto-fill/auto-fit repeat only depends on the container size and is kept apart. + /// A parsed grid-template-columns/grid-template-rows value: the fixed (fully + /// repeat(N,…)-expanded) tracks, plus at most one layout-time repeat(auto-fill|auto-fit,…) + /// section recorded as an insertion point since its count is resolved during layout. /// internal sealed class GridTemplate { - /// The tracks without the auto-repeat ones, those go in at . - public IReadOnlyList Tracks { get; set; } = new List(); + /// The fixed tracks (with the auto-repeat section, if any, NOT included here — it is + /// spliced in at at layout time). + public IReadOnlyList Tracks { get; internal set; } = new List(); - public GridAutoRepeatKind AutoRepeat { get; set; } = GridAutoRepeatKind.None; + public GridAutoRepeatKind AutoRepeat { get; internal set; } = GridAutoRepeatKind.None; - public IReadOnlyList AutoRepeatTracks { get; set; } + /// The repeated track template for an auto-fill/auto-fit section. + public IReadOnlyList AutoRepeatTracks { get; internal set; } - public int AutoRepeatInsertIndex { get; set; } + /// The index into at which the auto-repeat section is inserted. + public int AutoRepeatInsertIndex { get; internal set; } - /// Line name → sorted 1-based line numbers, line 1 is before the first track. - public IReadOnlyDictionary> LineNames { get; set; } + /// Named lines declared with [name] in the top-level track list: name → the sorted, + /// deduped 1-based line numbers it labels (line 1 is before the first track). Empty when none. + public IReadOnlyDictionary> LineNames { get; internal set; } = new Dictionary>(); - /// CSS Grid 2 §9, no , only the line names after the keyword. - public bool IsSubgrid { get; set; } + /// The subgrid keyword (CSS Grid Layout Module Level 2 §9): this axis adopts the parent + /// grid's tracks instead of defining its own. A subgrid template carries no — only + /// any explicit [name] line names declared after the keyword (in ). + public bool IsSubgrid { get; internal set; } public bool IsNone => !IsSubgrid && Tracks.Count == 0 && AutoRepeat == GridAutoRepeatKind.None; } /// - /// CSS Grid 2 §7.2 track lists of grid-template-columns/-rows and the - /// <track-size>+ of grid-auto-columns/-rows. + /// Shared grammar for the grid <track-list> (grid-template-columns/ + /// grid-template-rows) and <track-size>+ (grid-auto-columns/ + /// grid-auto-rows). Mirrors the ExCSS grammar-helper precedent so the grammar is defined once. + /// Named lines ([name]) and grid-template-areas are out of the fixed track-list scope + /// here and rejected. /// internal static class GridTrackListGrammar { - /// Returns null for none too, the property handles that keyword. + /// + /// Parses a <track-list> (the grid-template-columns/-rows value). Returns + /// null when the value is not a valid track list — including the literal none, which + /// the property accepts separately. + /// internal static GridTemplate TryParse(IReadOnlyList tokens) { var toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); if (toks.Length == 0) return null; + // subgrid (CSS Grid Level 2 §9): the axis adopts the parent grid's tracks. The keyword may be + // followed by an optional ([name] groups only; repeat() of line names is a v1 + // deferral, mirroring the "named lines inside repeat()" restriction below). No is + // allowed after subgrid. if (toks[0].Type == TokenType.Ident && toks[0].Data.Isi(Keywords.Subgrid)) return TryParseSubgrid(toks); @@ -87,6 +111,8 @@ internal static GridTemplate TryParse(IReadOnlyList tokens) var i = 0; while (i < toks.Length) { + // [name …] — one or more named lines at the current line position (1-based; line 1 is before + // the first track). Whitespace is already stripped, so the bracket group is contiguous. if (toks[i].Type == TokenType.SquareBracketOpen) { var lineIndex = fixedTracks.Count + 1; @@ -111,10 +137,12 @@ internal static GridTemplate TryParse(IReadOnlyList tokens) if (repeatKind == GridAutoRepeatKind.None) { + // repeat(N, …) — expand inline. fixedTracks.AddRange(repeatTracks); } else { + // repeat(auto-fill|auto-fit, …) — at most one per track list. if (autoRepeat != GridAutoRepeatKind.None) return null; autoRepeat = repeatKind; autoRepeatTracks = repeatTracks; @@ -144,7 +172,13 @@ internal static GridTemplate TryParse(IReadOnlyList tokens) }; } - /// subgrid <line-name-list>?, the caller matched the keyword. + /// + /// Parses a subgrid [ <line-name-list> ]? value (the leading subgrid ident already + /// matched by the caller). The optional line-name list is a run of [name …] bracket groups, one + /// group per grid line (line 1 is before the first adopted track); each group's names are recorded at + /// that 1-based line index. Anything else after subgrid (a track size, an unclosed/ill-formed + /// bracket, a non-ident inside [ ]) is invalid. + /// private static GridTemplate TryParseSubgrid(Token[] toks) { var lineNames = new Dictionary>(); @@ -153,7 +187,7 @@ private static GridTemplate TryParseSubgrid(Token[] toks) while (i < toks.Length) { - if (toks[i].Type != TokenType.SquareBracketOpen) return null; + if (toks[i].Type != TokenType.SquareBracketOpen) return null; // only [name] groups may follow i++; while (i < toks.Length && toks[i].Type != TokenType.SquareBracketClose) { @@ -178,7 +212,10 @@ private static GridTemplate TryParseSubgrid(Token[] toks) }; } - /// <track-size>+, no repeat(). + /// + /// Parses a <track-size>+ list (grid-auto-columns/grid-auto-rows) — one or + /// more track sizes, no repeat(). Returns null on any invalid token. + /// internal static IReadOnlyList TryParseTrackSizeList(IReadOnlyList tokens) { var toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); @@ -203,16 +240,18 @@ private static bool TryParseRepeat(FunctionToken fn, out GridAutoRepeatKind kind var groups = SplitByComma(args); if (groups.Count < 2) return false; + // First group is the repetition count: an integer, or auto-fill / auto-fit. var first = groups[0]; if (first.Count != 1) return false; var countTokens = 0; if (IsIdent(first[0], Keywords.AutoFill)) kind = GridAutoRepeatKind.AutoFill; else if (IsIdent(first[0], Keywords.AutoFit)) kind = GridAutoRepeatKind.AutoFit; - else if (first[0] is NumberToken n && n.IsInteger && n.IntegerValue >= 1) countTokens = n.IntegerValue; + else if (first[0] is NumberToken { IsInteger: true } n && n.IntegerValue >= 1) countTokens = n.IntegerValue; else return false; - // A nested repeat() is a single function token, which TryParseTrackSize rejects. + // Remaining groups are the repeated s. (A repeat body may not itself contain a + // repeat(), which SplitByComma naturally enforces since a nested function is one token.) var body = new List(); for (var gi = 1; gi < groups.Count; gi++) { @@ -231,6 +270,7 @@ private static bool TryParseRepeat(FunctionToken fn, out GridAutoRepeatKind kind return true; } + // repeat(N, body) — expand N copies now. tracks = new List(body.Count * countTokens); for (var r = 0; r < countTokens; r++) tracks.AddRange(body); @@ -241,12 +281,12 @@ private static bool TryParseTrackSize(Token token, out GridTrackSize track) { track = null; + // minmax(, ) and fit-content(). if (token is FunctionToken fn) { if (fn.Data.Isi(FunctionNames.Minmax)) return TryParseMinmax(fn, out track); if (fn.Data.Isi(FunctionNames.FitContent)) return TryParseFitContent(fn, out track); - if (IsLengthPercentageCalc(fn)) { track = GridTrackSize.Length(fn.ToValue()); return true; } - return false; + return TryParseBreadth(token, out track); } if (TryParseBreadth(token, out track)) return true; @@ -257,12 +297,6 @@ private static bool TryParseBreadth(Token token, out GridTrackSize track) { track = null; - if (token is FunctionToken calc && IsLengthPercentageCalc(calc)) - { - track = GridTrackSize.Length(calc.ToValue()); - return true; - } - if (token.Type == TokenType.Ident) { if (token.Data.Isi(Keywords.Auto)) { track = GridTrackSize.Auto; return true; } @@ -282,16 +316,23 @@ private static bool TryParseBreadth(Token token, out GridTrackSize track) return true; } + // Any other dimension is a — the raw string is preserved. track = GridTrackSize.Length(token.ToValue()); return true; } - var zero = token as NumberToken; - if (zero != null && zero.Value == 0f) { track = GridTrackSize.Length("0"); return true; } + // Unitless zero is a valid length. + if (token is NumberToken { Value: 0f }) { track = GridTrackSize.Length("0"); return true; } + + if (IsMathLengthPercentage(token)) { track = GridTrackSize.Length(token.ToValue()); return true; } return false; } + // CSS Values 4 §10, a math function resolving to a is one wherever that type is + private static bool IsMathLengthPercentage(Token token) => + token is FunctionToken && Converters.LengthOrPercentConverter.Convert(new[] { token }) != null; + private static bool TryParseMinmax(FunctionToken fn, out GridTrackSize track) { track = null; @@ -301,6 +342,7 @@ private static bool TryParseMinmax(FunctionToken fn, out GridTrackSize track) if (!TryParseBreadth(groups[0][0], out var min)) return false; if (!TryParseBreadth(groups[1][0], out var max)) return false; + // The min of a minmax() is an — a flex value is invalid there. if (min.Kind == GridTrackKind.Flex) return false; track = GridTrackSize.Minmax(min, max); @@ -313,39 +355,20 @@ private static bool TryParseFitContent(FunctionToken fn, out GridTrackSize track var args = fn.ArgumentTokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); if (args.Length != 1) return false; - var arg = args[0]; - if (arg.Type == TokenType.Percentage) { track = GridTrackSize.FitContentTo(arg.ToValue()); return true; } - if (arg is UnitToken && arg.Type == TokenType.Dimension && !((UnitToken)arg).Unit.Isi("fr")) + // fit-content(). + var t = args[0]; + if (t.Type is TokenType.Percentage) { track = GridTrackSize.FitContentTo(t.ToValue()); return true; } + if (t is UnitToken && t.Type == TokenType.Dimension && !((UnitToken)t).Unit.Isi("fr")) { - track = GridTrackSize.FitContentTo(arg.ToValue()); - return true; - } - var zeroArg = arg as NumberToken; - if (zeroArg != null && zeroArg.Value == 0f) { track = GridTrackSize.FitContentTo("0"); return true; } - var calc = arg as FunctionToken; - if (calc != null && IsLengthPercentageCalc(calc)) - { - track = GridTrackSize.FitContentTo(calc.ToValue()); + track = GridTrackSize.FitContentTo(t.ToValue()); return true; } + if (t is NumberToken { Value: 0f }) { track = GridTrackSize.FitContentTo("0"); return true; } + if (IsMathLengthPercentage(t)) { track = GridTrackSize.FitContentTo(t.ToValue()); return true; } return false; } - /// Whether is a math function of type <length-percentage>. - private static bool IsLengthPercentageCalc(FunctionToken fn) - { - if (!CalcParser.IsCalcFamily(fn.Data)) return false; - - var node = CalcParser.Parse(fn); - if (node == null) return false; - - var category = CalcTypeChecker.Check(node); - return category == CalcCategory.Length - || category == CalcCategory.Percentage - || category == CalcCategory.LengthPercentage; - } - private static bool IsIdent(Token token, string keyword) => token.Type == TokenType.Ident && token.Data.Isi(keyword); diff --git a/Source/HtmlRenderer/Core/CssEngine/HangingPunctuationGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/HangingPunctuationGrammar.cs new file mode 100644 index 000000000..691fde92b --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/HangingPunctuationGrammar.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// The punctuation a hanging-punctuation value lets hang outside the line box. + [Flags] + internal enum HangingPunctuationFlags + { + None = 0, + First = 1, + Last = 2, + AllowEnd = 4, + ForceEnd = 8 + } + + /// + /// The grammar for hanging-punctuation (CSS Text 3 8): + /// none | [ first || [ force-end | allow-end ] || last ]. + /// + internal static class HangingPunctuationGrammar + { + internal static bool TryParse(IEnumerable tokens, out HangingPunctuationFlags flags) + { + flags = HangingPunctuationFlags.None; + + var toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); + if (toks.Length == 0) return false; + + if (toks.Length == 1 && toks[0].Type == TokenType.Ident && toks[0].Data.Isi(Keywords.None)) return true; + + foreach (var token in toks) + { + if (token.Type != TokenType.Ident) return false; + + var flag = token.Data.Isi("first") ? HangingPunctuationFlags.First + : token.Data.Isi("last") ? HangingPunctuationFlags.Last + : token.Data.Isi("allow-end") ? HangingPunctuationFlags.AllowEnd + : token.Data.Isi("force-end") ? HangingPunctuationFlags.ForceEnd + : HangingPunctuationFlags.None; + + // "||" permits each component once; allow-end and force-end are exclusive alternatives. + if (flag == HangingPunctuationFlags.None || (flags & flag) != 0) return false; + + flags |= flag; + } + + const HangingPunctuationFlags both = HangingPunctuationFlags.AllowEnd | HangingPunctuationFlags.ForceEnd; + return (flags & both) != both; + } + + internal static bool IsValid(IReadOnlyList tokens) => TryParse(tokens, out _); + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/HyphenateLimitCharsGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/HyphenateLimitCharsGrammar.cs new file mode 100644 index 000000000..2ca393c9d --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/HyphenateLimitCharsGrammar.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The grammar for hyphenate-limit-chars (CSS Text 4 6.3.4): [ auto | <integer [0,inf]> ]{1,3}. + /// The first value is the minimum word length, the second the minimum characters before the break, the third the + /// minimum after it. A missing third value takes the second's value; a missing second value is auto. + /// + internal static class HyphenateLimitCharsGrammar + { + /// + /// Validates the tokens. Each out parameter is null for auto (or an omitted trailing value), otherwise + /// the parsed non-negative minimum. + /// + internal static bool TryParse(IEnumerable tokens, out int? wordMin, out int? beforeMin, out int? afterMin) + { + wordMin = null; + beforeMin = null; + afterMin = null; + + var toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); + if (toks.Length == 0 || toks.Length > 3) return false; + + if (!TryValue(toks[0], out wordMin)) return false; + + if (toks.Length >= 2 && !TryValue(toks[1], out beforeMin)) return false; + + if (toks.Length == 3) + { + if (!TryValue(toks[2], out afterMin)) return false; + } + else + { + afterMin = beforeMin; + } + + return true; + } + + internal static bool IsValid(IReadOnlyList tokens) => TryParse(tokens, out _, out _, out _); + + private static bool TryValue(Token token, out int? value) + { + if (token.Type == TokenType.Ident && token.Data.Isi(Keywords.Auto)) + { + value = null; + return true; + } + + if (token is NumberToken number && number.IsInteger && number.IntegerValue >= 0) + { + value = number.IntegerValue; + return true; + } + + value = null; + return false; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ImageOrientationGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/ImageOrientationGrammar.cs new file mode 100644 index 000000000..f0c02d1d1 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ImageOrientationGrammar.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Grammar of image-orientation (CSS Images 4 §5.2): from-image | none | [ <angle> || flip ]. + /// + internal static class ImageOrientationGrammar + { + private const string FromImage = "from-image"; + private const string Flip = "flip"; + + private static readonly IValueConverter AngleCalc = new CalcValueConverter(CalcCategory.Angle); + + internal static bool IsValid(IEnumerable value) + { + var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (tokens.Length < 1 || tokens.Length > 2) return false; + + if (tokens.Length == 1 && tokens[0].Type == TokenType.Ident && + (tokens[0].Data.Isi(FromImage) || tokens[0].Data.Isi(Keywords.None))) + return true; + + var hasAngle = false; + var hasFlip = false; + + foreach (var token in tokens) + { + if (token.Type == TokenType.Ident && token.Data.Isi(Flip) && !hasFlip) + { + hasFlip = true; + } + else if (!hasAngle && IsAngle(token)) + { + hasAngle = true; + } + else + { + return false; + } + } + + return true; + } + + // A bare 0 is not accepted: image-orientation does not take the unitless-zero legacy form. + private static bool IsAngle(Token token) + { + if (token is UnitToken unit && token.Type == TokenType.Dimension) + return Angle.GetUnit(unit.Unit.ToLowerInvariant()) != Angle.Unit.None; + + return token is FunctionToken && AngleCalc.Convert(new[] { token }) != null; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ImageResolutionGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/ImageResolutionGrammar.cs new file mode 100644 index 000000000..129f2606e --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ImageResolutionGrammar.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Grammar of image-resolution (CSS Images 4 §6.1): [ from-image || <resolution> ] && snap?. + /// The resolution must be positive. + /// + internal static class ImageResolutionGrammar + { + private const string FromImage = "from-image"; + private const string Snap = "snap"; + + internal static bool IsValid(IEnumerable value) + { + var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (tokens.Length < 1 || tokens.Length > 3) return false; + + var fromImage = false; + var snap = false; + var resolution = false; + + foreach (var token in tokens) + { + if (token.Type == TokenType.Ident && token.Data.Isi(FromImage) && !fromImage) + { + fromImage = true; + } + else if (token.Type == TokenType.Ident && token.Data.Isi(Snap) && !snap) + { + snap = true; + } + else if (!resolution && token.Type == TokenType.Dimension && token is UnitToken unit && + unit.Value > 0f && Resolution.GetUnit(unit.Unit.ToLowerInvariant()) != Resolution.Unit.None) + { + resolution = true; + } + else + { + return false; + } + } + + // "snap" only modifies a resolution or from-image; it is not a value on its own. + return fromImage || resolution; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/InitialLetterGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/InitialLetterGrammar.cs new file mode 100644 index 000000000..58a7bb6a1 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/InitialLetterGrammar.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The grammar for initial-letter (CSS Inline 3 7.4): + /// normal | <number [1,inf]> <integer [1,inf]> | <number [1,inf]> [ drop | raise ]?. + /// + internal static class InitialLetterGrammar + { + /// + /// Parses the tokens. is the number of lines the letter's height spans and + /// the line its baseline sits on; both are 0 for normal. + /// + internal static bool TryParse(IEnumerable tokens, out double size, out int sink) + { + size = 0; + sink = 0; + + var toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (toks.Length == 1 && toks[0].Type == TokenType.Ident && toks[0].Data.Isi(Keywords.Normal)) return true; + if (toks.Length == 0 || toks.Length > 2) return false; + + if (!(toks[0] is NumberToken sizeToken) || sizeToken.Value < 1f) return false; + + var parsedSize = (double)sizeToken.Value; + var parsedSink = (int)Math.Floor(parsedSize); + + if (toks.Length == 2) + { + if (toks[1].Type == TokenType.Ident && toks[1].Data.Isi("drop")) + { + } + else if (toks[1].Type == TokenType.Ident && toks[1].Data.Isi("raise")) + { + parsedSink = 1; + } + else if (toks[1] is NumberToken sinkToken && sinkToken.IsInteger && sinkToken.IntegerValue >= 1 + && !sinkToken.Data.StartsWith("+", StringComparison.Ordinal)) + { + parsedSink = sinkToken.IntegerValue; + } + else + { + return false; + } + } + + size = parsedSize; + sink = parsedSink; + return true; + } + + internal static bool IsValid(IReadOnlyList tokens) => TryParse(tokens, out _, out _); + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyHoverMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyHoverMediaFeature.cs index 4aa0f727a..58baefa27 100644 --- a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyHoverMediaFeature.cs +++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyHoverMediaFeature.cs @@ -1,8 +1,5 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// - /// Media Queries 5 §7.3, matched in MediaQueryMatcher. - /// internal sealed class AnyHoverMediaFeature : MediaFeature { public AnyHoverMediaFeature() : base(FeatureNames.AnyHover) diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyPointerMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyPointerMediaFeature.cs index 252ed534b..9087e0d5d 100644 --- a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyPointerMediaFeature.cs +++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyPointerMediaFeature.cs @@ -1,8 +1,5 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// - /// Media Queries 5 §7.3, matched in MediaQueryMatcher. - /// internal sealed class AnyPointerMediaFeature : MediaFeature { public AnyPointerMediaFeature() : base(FeatureNames.AnyPointer) diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/DynamicRangeMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/DynamicRangeMediaFeature.cs new file mode 100644 index 000000000..6c34d68e1 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/DynamicRangeMediaFeature.cs @@ -0,0 +1,13 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class DynamicRangeMediaFeature : MediaFeature + { + private static readonly IValueConverter TheConverter = Converters.Toggle(Keywords.Standard, Keywords.High); + + public DynamicRangeMediaFeature() : base(FeatureNames.DynamicRange) + { + } + + internal override IValueConverter Converter => TheConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/ForcedColorsMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/ForcedColorsMediaFeature.cs new file mode 100644 index 000000000..ba76e182d --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/ForcedColorsMediaFeature.cs @@ -0,0 +1,13 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ForcedColorsMediaFeature : MediaFeature + { + private static readonly IValueConverter TheConverter = Converters.Toggle(Keywords.None, Keywords.Active); + + public ForcedColorsMediaFeature() : base(FeatureNames.ForcedColors) + { + } + + internal override IValueConverter Converter => TheConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/InvertedColorsMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/InvertedColorsMediaFeature.cs new file mode 100644 index 000000000..bfabda15a --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/InvertedColorsMediaFeature.cs @@ -0,0 +1,13 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class InvertedColorsMediaFeature : MediaFeature + { + private static readonly IValueConverter TheConverter = Converters.Toggle(Keywords.None, Keywords.Inverted); + + public InvertedColorsMediaFeature() : base(FeatureNames.InvertedColors) + { + } + + internal override IValueConverter Converter => TheConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/MediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/MediaFeature.cs index 4a6125b06..382c89a3e 100644 --- a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/MediaFeature.cs +++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/MediaFeature.cs @@ -1,26 +1,9 @@ -using System.IO; using System.Linq; +using System; +using System.IO; namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// - /// How a media feature's stored value constrains the device value, unifying the min-/max- - /// name-prefix forms with the Media Queries 4 range operators (>=/<=/>/<). - /// - internal enum MediaFeatureComparison - { - /// Exact match (the feature: value form, or a boolean feature). - Exact, - /// Device value must be ≥ the feature value (min-* or >=). - Minimum, - /// Device value must be ≤ the feature value (max-* or <=). - Maximum, - /// Device value must be strictly > the feature value. - GreaterThan, - /// Device value must be strictly < the feature value. - LessThan - } - internal abstract class MediaFeature : StylesheetNode, IMediaFeature { private TokenValue _tokenValue; @@ -41,32 +24,29 @@ internal MediaFeature(string name) public string Name { get; } - public string Value => HasValue ? _tokenValue.Text : string.Empty; - - public bool HasValue => _tokenValue != null && _tokenValue.Count > 0; - - /// - /// The comparison the stored value imposes on the device value, derived from the min-/ - /// max- name prefix or, for the range syntax (width >= 48rem), the parsed operator. - /// - internal MediaFeatureComparison Comparison + // How the feature value is compared to the environment: min-* maps to + // GreaterThanOrEqual, max-* to LessThanOrEqual, and range syntax + // ("width >= 600px") maps from its operator. ":" and "=" are Equal. + public MediaFeatureComparison Comparison { get { - if (IsMinimum) return MediaFeatureComparison.Minimum; - if (IsMaximum) return MediaFeatureComparison.Maximum; - - switch (_constraintDelimiter) - { - case TokenType.GreaterThanOrEqual: return MediaFeatureComparison.Minimum; - case TokenType.GreaterThan: return MediaFeatureComparison.GreaterThan; - case TokenType.LessThanOrEqual: return MediaFeatureComparison.Maximum; - case TokenType.LessThan: return MediaFeatureComparison.LessThan; - default: return MediaFeatureComparison.Exact; - } + if (IsMinimum) return MediaFeatureComparison.GreaterThanOrEqual; + if (IsMaximum) return MediaFeatureComparison.LessThanOrEqual; + + if (_constraintDelimiter == TokenType.GreaterThan) return MediaFeatureComparison.GreaterThan; + if (_constraintDelimiter == TokenType.GreaterThanOrEqual) return MediaFeatureComparison.GreaterThanOrEqual; + if (_constraintDelimiter == TokenType.LessThan) return MediaFeatureComparison.LessThan; + if (_constraintDelimiter == TokenType.LessThanOrEqual) return MediaFeatureComparison.LessThanOrEqual; + + return MediaFeatureComparison.Equal; } } + public string Value => HasValue ? _tokenValue.Text : string.Empty; + + public bool HasValue => _tokenValue is {Count: > 0}; + /// The stored value parsed as a , or null if absent/not a length. internal Length? AsLength() => HasValue ? _tokenValue.ToLength() : null; diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/OverflowBlockMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/OverflowBlockMediaFeature.cs new file mode 100644 index 000000000..5af9fdc8c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/OverflowBlockMediaFeature.cs @@ -0,0 +1,13 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class OverflowBlockMediaFeature : MediaFeature + { + private static readonly IValueConverter TheConverter = Converters.Toggle(Keywords.None, Keywords.Scroll).Or(Keywords.Paged); + + public OverflowBlockMediaFeature() : base(FeatureNames.OverflowBlock) + { + } + + internal override IValueConverter Converter => TheConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/OverflowInlineMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/OverflowInlineMediaFeature.cs new file mode 100644 index 000000000..41830049b --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/OverflowInlineMediaFeature.cs @@ -0,0 +1,13 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class OverflowInlineMediaFeature : MediaFeature + { + private static readonly IValueConverter TheConverter = Converters.Toggle(Keywords.None, Keywords.Scroll); + + public OverflowInlineMediaFeature() : base(FeatureNames.OverflowInline) + { + } + + internal override IValueConverter Converter => TheConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersColorSchemeMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersColorSchemeMediaFeature.cs index 64197a593..76a40300f 100644 --- a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersColorSchemeMediaFeature.cs +++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersColorSchemeMediaFeature.cs @@ -1,8 +1,5 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// - /// Media Queries 5 §12.5, matched against the adapter's color scheme in MediaQueryMatcher. - /// internal sealed class PrefersColorSchemeMediaFeature : MediaFeature { public PrefersColorSchemeMediaFeature() : base(FeatureNames.PrefersColorScheme) diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersContrastMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersContrastMediaFeature.cs index 52e2faff1..d3cccc4a6 100644 --- a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersContrastMediaFeature.cs +++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersContrastMediaFeature.cs @@ -1,8 +1,5 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// - /// Media Queries 5 §12.3, matched in MediaQueryMatcher. - /// internal sealed class PrefersContrastMediaFeature : MediaFeature { public PrefersContrastMediaFeature() : base(FeatureNames.PrefersContrast) diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedDataMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedDataMediaFeature.cs new file mode 100644 index 000000000..40d6c7864 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedDataMediaFeature.cs @@ -0,0 +1,13 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class PrefersReducedDataMediaFeature : MediaFeature + { + private static readonly IValueConverter TheConverter = Converters.Toggle(Keywords.NoPreference, Keywords.Reduce); + + public PrefersReducedDataMediaFeature() : base(FeatureNames.PrefersReducedData) + { + } + + internal override IValueConverter Converter => TheConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedMotionMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedMotionMediaFeature.cs index a7e69c3fb..c3b82d0e4 100644 --- a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedMotionMediaFeature.cs +++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedMotionMediaFeature.cs @@ -1,8 +1,5 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// - /// Media Queries 5 §12.1, matched in MediaQueryMatcher. - /// internal sealed class PrefersReducedMotionMediaFeature : MediaFeature { public PrefersReducedMotionMediaFeature() : base(FeatureNames.PrefersReducedMotion) diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedTransparencyMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedTransparencyMediaFeature.cs index 124a93849..85f785ef3 100644 --- a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedTransparencyMediaFeature.cs +++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedTransparencyMediaFeature.cs @@ -1,8 +1,5 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// - /// Media Queries 5 §12.2, matched in MediaQueryMatcher. - /// internal sealed class PrefersReducedTransparencyMediaFeature : MediaFeature { public PrefersReducedTransparencyMediaFeature() : base(FeatureNames.PrefersReducedTransparency) diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/UpdateMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/UpdateMediaFeature.cs index 7e8f564b5..8fb6d4b47 100644 --- a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/UpdateMediaFeature.cs +++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/UpdateMediaFeature.cs @@ -1,8 +1,5 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// - /// Media Queries 5 §5.4, matched in MediaQueryMatcher. - /// internal sealed class UpdateMediaFeature : MediaFeature { public UpdateMediaFeature() : base(FeatureNames.Update) diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/VideoDynamicRangeMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/VideoDynamicRangeMediaFeature.cs new file mode 100644 index 000000000..9e517650b --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/VideoDynamicRangeMediaFeature.cs @@ -0,0 +1,13 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class VideoDynamicRangeMediaFeature : MediaFeature + { + private static readonly IValueConverter TheConverter = Converters.Toggle(Keywords.Standard, Keywords.High); + + public VideoDynamicRangeMediaFeature() : base(FeatureNames.VideoDynamicRange) + { + } + + internal override IValueConverter Converter => TheConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs b/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs index 893bec5ca..dbe2e14e7 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs @@ -15,7 +15,7 @@ internal static class Converters public static readonly IValueConverter ResolutionConverter = new StructValueConverter(ValueExtensions.ToResolution); - + public static readonly IValueConverter TimeConverter = new StructValueConverter IReadOnlyList NestedRules { get; } + + /// + /// CSS Nesting: every rule nested inside this rule's block, in source order - nested style rules + /// (as in ) interleaved with nested conditional group rules + /// (@media, @supports, @container, @layer). A nested group's own rules + /// are style rules whose selector is this rule's, holding the group's declarations. + /// + IReadOnlyList NestedRuleList { get; } } -} \ No newline at end of file +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Rules/KeyframeRule.cs b/Source/HtmlRenderer/Core/CssEngine/Rules/KeyframeRule.cs index da080927c..b1d8cdcce 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Rules/KeyframeRule.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Rules/KeyframeRule.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Linq; diff --git a/Source/HtmlRenderer/Core/CssEngine/Rules/KeyframesRule.cs b/Source/HtmlRenderer/Core/CssEngine/Rules/KeyframesRule.cs index e01623418..f347ec2dd 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Rules/KeyframesRule.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Rules/KeyframesRule.cs @@ -15,10 +15,12 @@ protected override void ReplaceWith(IRule rule) { var newRule = rule as KeyframesRule; Name = newRule?.Name; + NameIsString = newRule?.NameIsString ?? false; base.ReplaceWith(rule); } public string Name { get; set; } + internal bool NameIsString { get; set; } public RuleList Rules { get; } IRuleList IKeyframesRule.Rules => Rules; @@ -42,7 +44,7 @@ public KeyframeRule Find(string key) public override void ToCss(TextWriter writer, IStyleFormatter formatter) { var rules = formatter.Block(Rules); - writer.Write(formatter.Rule("@keyframes", Name, rules)); + writer.Write(formatter.Rule("@keyframes", NameIsString ? Name.StylesheetString() : Name, rules)); } IKeyframeRule IKeyframesRule.Find(string key) diff --git a/Source/HtmlRenderer/Core/CssEngine/Rules/LayerRule.cs b/Source/HtmlRenderer/Core/CssEngine/Rules/LayerRule.cs index 0ec9e01bb..aed1a736f 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Rules/LayerRule.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Rules/LayerRule.cs @@ -3,8 +3,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// The block form of @layer, CSS Cascade 5 §6.4.4.1. CssData indexes its rules as - /// if they were unlayered. + /// The block form of @layer (@layer name { rules } or anonymous @layer { rules }). + /// A grouping rule holding the style rules assigned to a cascade layer; cascade precedence between + /// layers is resolved by the consumer when the rules are indexed, not here. /// internal sealed class LayerRule : GroupingRule, ILayerRule { diff --git a/Source/HtmlRenderer/Core/CssEngine/Rules/LayerStatementRule.cs b/Source/HtmlRenderer/Core/CssEngine/Rules/LayerStatementRule.cs index d065656c2..e0bc077f9 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Rules/LayerStatementRule.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Rules/LayerStatementRule.cs @@ -4,8 +4,10 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// The statement form of @layer, CSS Cascade 5 §6.4.4.2. Parsed, but the layer order it - /// declares isn't used. + /// The statement form of @layer (@layer a, b, c;). It declares the relative order of + /// one or more named cascade layers without providing any rules; a later block-form + /// attaches rules to a layer already ordered by this statement, per + /// CSS Cascade 5. /// internal sealed class LayerStatementRule : Rule { diff --git a/Source/HtmlRenderer/Core/CssEngine/Rules/MarginStyleRule.cs b/Source/HtmlRenderer/Core/CssEngine/Rules/MarginStyleRule.cs index 92a91b981..112e6a789 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Rules/MarginStyleRule.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Rules/MarginStyleRule.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -7,10 +8,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class MarginStyleRule : Rule, IStyleRule { // @page margin boxes don't participate in CSS Nesting. - public IReadOnlyList NestedRules - { - get { return new IStyleRule[0]; } - } + public IReadOnlyList NestedRules => Array.Empty(); + + public IReadOnlyList NestedRuleList => Array.Empty(); public MarginStyleRule(StylesheetParser parser) : base(RuleType.Style, parser) { diff --git a/Source/HtmlRenderer/Core/CssEngine/Rules/PageRule.cs b/Source/HtmlRenderer/Core/CssEngine/Rules/PageRule.cs index 061f4b70e..83dcdcde4 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Rules/PageRule.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Rules/PageRule.cs @@ -17,7 +17,7 @@ internal PageRule(StylesheetParser parser) public override void ToCss(TextWriter writer, IStyleFormatter formatter) { writer.Write(formatter.Rule("@page", Selector == null ? "" : SelectorText, "{")); - + Style.ToCss(writer, formatter); if (Style.Any()) diff --git a/Source/HtmlRenderer/Core/CssEngine/Rules/PropertyRule.cs b/Source/HtmlRenderer/Core/CssEngine/Rules/PropertyRule.cs index 893497a79..1ef9c8f8f 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Rules/PropertyRule.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Rules/PropertyRule.cs @@ -4,8 +4,10 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// An @property rule (CSS Properties and Values API 1 §3). Parsed only, the registration has no - /// effect on the cascade. + /// An @property at-rule (CSS Properties and Values API 1 §3): registers a typed custom property + /// with syntax, initial-value and inherits descriptors. The block is stored like a + /// ; the registered name (a dashed-ident such as --my-color) is + /// captured from the prelude like . /// internal sealed class PropertyRule : DeclarationRule, IPropertyRule { @@ -19,31 +21,24 @@ protected override Property CreateNewProperty(string name) return PropertyFactory.Instance.CreatePropertyDescriptor(name); } - protected override void ReplaceWith(IRule rule) - { - var other = rule as PropertyRule; - if (other != null) Name = other.Name; - base.ReplaceWith(rule); - } - public string Name { get; set; } public string Syntax { - get { return GetValue(PropertyNames.Syntax); } - set { SetValue(PropertyNames.Syntax, value); } + get => GetValue(PropertyNames.Syntax); + set => SetValue(PropertyNames.Syntax, value); } public string InitialValue { - get { return GetValue(PropertyNames.InitialValue); } - set { SetValue(PropertyNames.InitialValue, value); } + get => GetValue(PropertyNames.InitialValue); + set => SetValue(PropertyNames.InitialValue, value); } public string Inherits { - get { return GetValue(PropertyNames.Inherits); } - set { SetValue(PropertyNames.Inherits, value); } + get => GetValue(PropertyNames.Inherits); + set => SetValue(PropertyNames.Inherits, value); } public override void ToCss(TextWriter writer, IStyleFormatter formatter) diff --git a/Source/HtmlRenderer/Core/CssEngine/Rules/StartingStyleRule.cs b/Source/HtmlRenderer/Core/CssEngine/Rules/StartingStyleRule.cs new file mode 100644 index 000000000..42f5d0522 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Rules/StartingStyleRule.cs @@ -0,0 +1,22 @@ +using System.IO; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The @starting-style { rules } grouping rule (CSS Transitions 2). Holds the style rules that + /// define the starting values for transitions when an element is first rendered. + /// + internal sealed class StartingStyleRule : GroupingRule, IStartingStyleRule + { + internal StartingStyleRule(StylesheetParser parser) + : base(RuleType.StartingStyle, parser) + { + } + + public override void ToCss(TextWriter writer, IStyleFormatter formatter) + { + var rules = formatter.Block(Rules); + writer.Write(formatter.Rule("@starting-style", null, rules)); + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Rules/StyleRule.cs b/Source/HtmlRenderer/Core/CssEngine/Rules/StyleRule.cs index 9bfaadd37..bc27bd862 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Rules/StyleRule.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Rules/StyleRule.cs @@ -6,10 +6,15 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class StyleRule : Rule, IStyleRule { - // CSS Nesting 1 §3, with selectors already resolved against this rule. Not in Children, so Selector, - // Style and ToCss don't see them. + // CSS Nesting: rules written inside this rule's block, resolved to absolute selectors against + // this parent. Kept in a dedicated list (not Children) so Selector/Style lookups and ToCss are + // unaffected. Populated by StylesheetComposer.FillDeclarations. private readonly List _nestedRules = new List(); + // Every rule written inside this rule's block - nested style rules and nested conditional group + // rules (@media, @supports, @container, @layer) - in source order. + private readonly List _nestedRuleList = new List(); + public StyleRule(StylesheetParser parser) : base(RuleType.Style, parser) { AppendChild(AllSelector.Create()); @@ -18,7 +23,23 @@ public StyleRule(StylesheetParser parser) : base(RuleType.Style, parser) public IReadOnlyList NestedRules => _nestedRules; - public void AddNestedRule(IStyleRule rule) => _nestedRules.Add(rule); + public IReadOnlyList NestedRuleList => _nestedRuleList; + + public void AddNestedRule(IStyleRule rule) + { + _nestedRules.Add(rule); + _nestedRuleList.Add(rule); + } + + /// + /// Adds a nested rule in source order: a style rule (also reported by ) or + /// a conditional group rule (reported by only). + /// + public void AddNestedRule(IRule rule) + { + if (rule is IStyleRule styleRule) AddNestedRule(styleRule); + else _nestedRuleList.Add(rule); + } public override void ToCss(TextWriter writer, IStyleFormatter formatter) { @@ -33,7 +54,7 @@ public ISelector Selector public string SelectorText { - get => Selector.Text; + get => Selector?.Text ?? string.Empty; set => Selector = Parser.ParseSelector(value); } diff --git a/Source/HtmlRenderer/Core/CssEngine/Rules/SupportsRule.cs b/Source/HtmlRenderer/Core/CssEngine/Rules/SupportsRule.cs index b872a2cc4..eb7befc6b 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Rules/SupportsRule.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Rules/SupportsRule.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Linq; diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrBeginsSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrBeginsSelector.cs index 92d44e022..55a048f5b 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrBeginsSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrBeginsSelector.cs @@ -3,7 +3,12 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class AttrBeginsSelector : AttrSelectorBase { public AttrBeginsSelector(string attribute, string value) - : base(attribute, value, $"[{attribute}^={value.StylesheetString()}]") + : this(attribute, value, AttrCaseSensitivity.Default) + { + } + + public AttrBeginsSelector(string attribute, string value, AttrCaseSensitivity caseSensitivity) + : base(attribute, value, $"[{attribute}^={value.StylesheetString()}{Flag(caseSensitivity)}]", caseSensitivity) { } } diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrContainsSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrContainsSelector.cs index f5131014e..1def1ecb3 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrContainsSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrContainsSelector.cs @@ -3,7 +3,12 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class AttrContainsSelector : AttrSelectorBase { public AttrContainsSelector(string attribute, string value) - : base(attribute, value, $"[{attribute}*={value.StylesheetString()}]") + : this(attribute, value, AttrCaseSensitivity.Default) + { + } + + public AttrContainsSelector(string attribute, string value, AttrCaseSensitivity caseSensitivity) + : base(attribute, value, $"[{attribute}*={value.StylesheetString()}{Flag(caseSensitivity)}]", caseSensitivity) { } } diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrEndsSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrEndsSelector.cs index 2464bfdc9..a78d96aca 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrEndsSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrEndsSelector.cs @@ -3,7 +3,12 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class AttrEndsSelector : AttrSelectorBase { public AttrEndsSelector(string attribute, string value) - : base(attribute, value, $"[{attribute}$={value.StylesheetString()}]") + : this(attribute, value, AttrCaseSensitivity.Default) + { + } + + public AttrEndsSelector(string attribute, string value, AttrCaseSensitivity caseSensitivity) + : base(attribute, value, $"[{attribute}$={value.StylesheetString()}{Flag(caseSensitivity)}]", caseSensitivity) { } } diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrHyphenSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrHyphenSelector.cs index 35f8ccd06..c117c3f76 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrHyphenSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrHyphenSelector.cs @@ -3,7 +3,12 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class AttrHyphenSelector : AttrSelectorBase { public AttrHyphenSelector(string attribute, string value) - : base(attribute, value, $"[{attribute}|={value.StylesheetString()}]") + : this(attribute, value, AttrCaseSensitivity.Default) + { + } + + public AttrHyphenSelector(string attribute, string value, AttrCaseSensitivity caseSensitivity) + : base(attribute, value, $"[{attribute}|={value.StylesheetString()}{Flag(caseSensitivity)}]", caseSensitivity) { } } diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrListSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrListSelector.cs index 2b1a89131..32e1c2348 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrListSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrListSelector.cs @@ -3,7 +3,12 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class AttrListSelector : AttrSelectorBase { public AttrListSelector(string attribute, string value) - : base(attribute, value, $"[{attribute}~={value.StylesheetString()}]") + : this(attribute, value, AttrCaseSensitivity.Default) + { + } + + public AttrListSelector(string attribute, string value, AttrCaseSensitivity caseSensitivity) + : base(attribute, value, $"[{attribute}~={value.StylesheetString()}{Flag(caseSensitivity)}]", caseSensitivity) { } } diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrMatchSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrMatchSelector.cs index 9ea5d39b7..1ca017ed9 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrMatchSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrMatchSelector.cs @@ -3,7 +3,12 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class AttrMatchSelector : AttrSelectorBase { public AttrMatchSelector(string attribute, string value) - : base(attribute, value, $"[{attribute}={value.StylesheetString()}]") + : this(attribute, value, AttrCaseSensitivity.Default) + { + } + + public AttrMatchSelector(string attribute, string value, AttrCaseSensitivity caseSensitivity) + : base(attribute, value, $"[{attribute}={value.StylesheetString()}{Flag(caseSensitivity)}]", caseSensitivity) { } } diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrNotMatchSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrNotMatchSelector.cs index 85a8cfbf1..d968a2955 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrNotMatchSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrNotMatchSelector.cs @@ -3,7 +3,12 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class AttrNotMatchSelector : AttrSelectorBase { public AttrNotMatchSelector(string attribute, string value) - : base(attribute, value, $"[{attribute}!={value.StylesheetString()}]") + : this(attribute, value, AttrCaseSensitivity.Default) + { + } + + public AttrNotMatchSelector(string attribute, string value, AttrCaseSensitivity caseSensitivity) + : base(attribute, value, $"[{attribute}!={value.StylesheetString()}{Flag(caseSensitivity)}]", caseSensitivity) { } } diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrSelectorBase.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrSelectorBase.cs index 29557eb37..fdaf19cc3 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrSelectorBase.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/AttrSelectorBase.cs @@ -2,12 +2,31 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal abstract class AttrSelectorBase : SelectorBase, IAttrSelector { - protected AttrSelectorBase(string attribute, string value, string text) : base(Priority.OneClass, text) + protected AttrSelectorBase(string attribute, string value, string text) + : this(attribute, value, text, AttrCaseSensitivity.Default) + { + } + + protected AttrSelectorBase(string attribute, string value, string text, AttrCaseSensitivity caseSensitivity) + : base(Priority.OneClass, text) { Attribute = attribute; Value = value; + CaseSensitivity = caseSensitivity; } + public string Attribute { get; } public string Value { get; } + public AttrCaseSensitivity CaseSensitivity { get; } + + internal static string Flag(AttrCaseSensitivity caseSensitivity) + { + return caseSensitivity switch + { + AttrCaseSensitivity.Insensitive => " i", + AttrCaseSensitivity.Sensitive => " s", + _ => string.Empty + }; + } } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/ChildSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/ChildSelector.cs index 4e7d74007..7e74fa7f7 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/ChildSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/ChildSelector.cs @@ -18,19 +18,12 @@ public override void ToCss(TextWriter writer, IStyleFormatter formatter) { var a = Step.ToString(); - string b; - if (Offset > 0) + var b = Offset switch { - b = "+" + Offset; - } - else if (Offset < 0) - { - b = Offset.ToString(); - } - else - { - b = string.Empty; - } + > 0 => "+" + Offset, + < 0 => Offset.ToString(), + _ => string.Empty + }; writer.Write(":{0}({1}n{2})", _name, a, b); } diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/ComplexSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/ComplexSelector.cs index 1aba835a7..ceb052bd1 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/ComplexSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/ComplexSelector.cs @@ -17,6 +17,14 @@ public ComplexSelector() public int Length => _selectors.Count; public bool IsReady { get; private set; } + public CombinatorSelector this[int index] => _selectors[index]; + + /// The compounds and their combinators in source order, without copying them. + public IReadOnlyList Entries => _selectors; + + /// The last compound (the subject), or null for an empty selector. + public ISelector Subject => _selectors.Count > 0 ? _selectors[_selectors.Count - 1].Selector : null; + public Priority Specificity { get diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/HasSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/HasSelector.cs index 43cf86b82..376b86946 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/HasSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/HasSelector.cs @@ -3,8 +3,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// :has(), Selectors 4 §4.5. Only the descendant form, SelectorConstructor drops a leading - /// combinator. + /// The relational pseudo-class :has(S) - matches an element with a descendant matching S. + /// Retains the parsed argument selector rather than collapsing it to a formatted string. /// internal sealed class HasSelector : StylesheetNode, ISelector { @@ -15,7 +15,8 @@ public HasSelector(ISelector inner) public ISelector Inner { get; } - // Selectors 4 §15 + // The specificity of :has() is the specificity of its most specific argument (CSS Selectors 4 + // 16.1), which ListSelector.Specificity supplies as the static max when the argument is a list. public Priority Specificity => Inner.Specificity; public string Text => this.ToCss(); diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/IAttrSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/IAttrSelector.cs index 86d2fef09..26a4918c3 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/IAttrSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/IAttrSelector.cs @@ -2,7 +2,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal interface IAttrSelector : ISelector { - string Attribute { get; } + string Attribute { get; } string Value { get; } + AttrCaseSensitivity CaseSensitivity { get; } } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/IdSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/IdSelector.cs index bcad28ea2..be67d4e5b 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/IdSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/IdSelector.cs @@ -8,7 +8,7 @@ private IdSelector(string name) : base(Priority.OneId, $"#{name}") } public string Id { get; } - + public static IdSelector Create(string name) { return new IdSelector(name); diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/KeyframeSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/KeyframeSelector.cs index b947efff4..8145e3e71 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/KeyframeSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/KeyframeSelector.cs @@ -30,9 +30,9 @@ public override void ToCss(TextWriter writer, IStyleFormatter formatter) /// /// One comma-separated entry of an @page selector list, e.g. the two entries of - /// @page chapter1:left, chapter2:left are (Name: "chapter1", Pseudo: "left") and - /// (Name: "chapter2", Pseudo: "left"). Either part may be absent: @page chapter has - /// Pseudo: null; @page :first has Name: null. + /// @page chapter1:left, chapter2:left are ("chapter1", "left") and + /// ("chapter2", "left"). Either part may be absent: @page chapter has a null pseudo; + /// @page :first has a null name. /// internal readonly struct PageSelectorEntry { diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/LangSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/LangSelector.cs new file mode 100644 index 000000000..f4870b2b9 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/LangSelector.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The :lang() pseudo-class. Each argument is an identifier or a string, and several + /// can be given as a comma-separated list. + /// + internal sealed class LangSelector : SelectorBase + { + internal LangSelector(IReadOnlyList> ranges) + : base(Priority.OneClass, CreateText(ranges)) + { + LanguageRanges = ranges.Select(r => r.Key).ToList(); + } + + /// The language ranges in source order, without quotes. + public IReadOnlyList LanguageRanges { get; } + + /// The first language range. + public string LanguageRange => LanguageRanges[0]; + + private static string CreateText(IReadOnlyList> ranges) + { + var arguments = ranges.Select(r => r.Value ? r.Key.StylesheetString() : r.Key); + return $"{PseudoClassNames.Separator}{PseudoClassNames.Lang.StylesheetFunction(string.Join(",", arguments))}"; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/ListSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/ListSelector.cs index a0c4581b7..a384449fc 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/ListSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/ListSelector.cs @@ -7,8 +7,12 @@ internal sealed class ListSelector : Selectors, ISelector { public bool IsInvalid { get; internal set; } - // Selectors 4 §15, the most specific selector of an :is()/:not()/:has() argument. A rule's - // own selector list counts with the selector that matched, see CssData.GetMatchedSpecificity. + // A comma-separated selector list's specificity is not the sum of its alternatives (that sum is + // only meaningful for a CompoundSelector, whose members all constrain one element at once). As the + // argument of :is()/:not()/:has() it is the static max across the alternatives - the most specific + // one, regardless of which (if any) matches a given element (CSS Selectors 4 16.1). A top-level + // list (".a, .b { }") is shorthand for separate rules, each with its own per-alternative + // specificity, so no single value is meaningful there; max is the closest sensible answer. public override Priority Specificity => _selectors.Count == 0 ? Priority.Zero : _selectors.Max(s => s.Specificity); diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/MatchesSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/MatchesSelector.cs index f3edb4a17..467874085 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/MatchesSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/MatchesSelector.cs @@ -3,7 +3,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// :is(), its legacy alias :matches() and :where(), Selectors 4 §4.2 and §4.4. + /// The matches-any pseudo-class :is(S) / :matches(S) - matches an element that matches + /// any selector in S. :is() is the current name and :matches() the legacy alias; both + /// share this class, and controls which form it round-trips to. /// internal sealed class MatchesSelector : StylesheetNode, ISelector { @@ -17,11 +19,11 @@ public MatchesSelector(ISelector inner, string keyword) public string Keyword { get; } - // Selectors 4 §15 + // The specificity of :is()/:matches() is the specificity of its most specific argument (CSS + // Selectors 4 16.1), which ListSelector.Specificity supplies as the static max for a list argument. + // :where() always has zero specificity. public Priority Specificity => - string.Equals(Keyword, PseudoClassNames.Where, System.StringComparison.OrdinalIgnoreCase) - ? Priority.Zero - : Inner.Specificity; + Keyword == PseudoClassNames.Where ? Priority.Zero : Inner.Specificity; public string Text => this.ToCss(); diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/NotSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/NotSelector.cs index 48986d77f..a053ba0e3 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/NotSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/NotSelector.cs @@ -3,7 +3,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// :not(), Selectors 4 §4.3. + /// The negation pseudo-class :not(S) - matches an element that does not match S. Retains the + /// parsed argument selector rather than collapsing it to a formatted string. /// internal sealed class NotSelector : StylesheetNode, ISelector { @@ -14,7 +15,8 @@ public NotSelector(ISelector inner) public ISelector Inner { get; } - // Selectors 4 §15 + // The specificity of :not() is the specificity of its most specific argument (CSS Selectors 4 + // 16.1), which ListSelector.Specificity supplies as the static max when the argument is a list. public Priority Specificity => Inner.Specificity; public string Text => this.ToCss(); diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/OnlyChildSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/OnlyChildSelector.cs index acf99e8a1..b19fa2ff2 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/OnlyChildSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/OnlyChildSelector.cs @@ -1,5 +1,6 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { + /// The :only-child structural pseudo-class. internal sealed class OnlyChildSelector : SelectorBase { public OnlyChildSelector() diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/OnlyOfTypeSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/OnlyOfTypeSelector.cs index d1ed238b8..305d853b5 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/OnlyOfTypeSelector.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/OnlyOfTypeSelector.cs @@ -1,5 +1,6 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { + /// The :only-of-type structural pseudo-class. internal sealed class OnlyOfTypeSelector : SelectorBase { public OnlyOfTypeSelector() diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/RelativeSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/RelativeSelector.cs new file mode 100644 index 000000000..076e633cf --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/RelativeSelector.cs @@ -0,0 +1,34 @@ +using System.IO; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// One alternative of the relative selector list taken by :has() (CSS Selectors 4): a complex + /// selector preceded by an explicit leading combinator (>, + or ~), as in + /// :has(> a). An alternative with no leading combinator (implicit descendant) is kept as a + /// plain selector rather than wrapped. + /// + internal sealed class RelativeSelector : StylesheetNode, ISelector + { + public RelativeSelector(Combinator combinator, ISelector selector) + { + Combinator = combinator; + Selector = selector; + } + + public Combinator Combinator { get; } + public ISelector Selector { get; } + + // The leading combinator never contributes to specificity. + public Priority Specificity => Selector.Specificity; + + public string Text => this.ToCss(); + + public override void ToCss(TextWriter writer, IStyleFormatter formatter) + { + writer.Write(Combinator.Delimiter); + writer.Write(' '); + writer.Write(Selector.Text); + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/Selectors.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/Selectors.cs index 6010b032c..65e8c293a 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Selectors/Selectors.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/Selectors.cs @@ -13,6 +13,9 @@ protected Selectors() _selectors = new List(); } + // A CompoundSelector's specificity is the sum of its members - they all constrain the same + // element. A ListSelector overrides this, because a comma-separated list's specificity is not a + // sum (see ListSelector). public virtual Priority Specificity { get diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/AllProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/AllProperty.cs new file mode 100644 index 000000000..32b380c36 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/AllProperty.cs @@ -0,0 +1,39 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The all shorthand (CSS Cascade 4 3.2): resets every property except direction, + /// unicode-bidi and custom properties to a single CSS-wide keyword. Its grammar is exactly the + /// five keywords, so hands each longhand the keyword unchanged. + /// The longhand list is built lazily by from every registered longhand. + /// + internal sealed class AllProperty : ShorthandProperty + { + private static readonly IValueConverter KeywordConverter = + new IdentifierValueConverter(Keywords.Initial, null) + .Or(Keywords.Inherit) + .Or(Keywords.Unset) + .Or(Keywords.Revert) + .Or(Keywords.RevertLayer); + + internal AllProperty() + : base(PropertyNames.All) + { + } + + internal override IValueConverter Converter => KeywordConverter; + + // Every longhand receives the keyword. A few longhands' own grammars don't list the CSS-wide keywords + // (fill, for instance); `all` must still reset them, so such a longhand keeps the keyword verbatim. + public override void Export(Property[] properties) + { + foreach (var property in properties) + { + var value = DeclaredValue.ExtractFor(property.Name); + + if (!property.TrySetValue(value)) property.DeclaredValue = Converters.Any.Convert(value); + + property.IsImportant = IsImportant; + } + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationCompositionProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationCompositionProperty.cs new file mode 100644 index 000000000..14d244826 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationCompositionProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class AnimationCompositionProperty : Property + { + private static readonly IValueConverter ListConverter = TimelineConverters.AnimationComposition.FromList().OrDefault(); + + internal AnimationCompositionProperty() + : base(PropertyNames.AnimationComposition) + { + } + + internal override IValueConverter Converter => ListConverter; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationNameProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationNameProperty.cs index 5fad16516..6047ee465 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationNameProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationNameProperty.cs @@ -3,7 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class AnimationNameProperty : Property { private static readonly IValueConverter ListConverter = - Converters.IdentifierConverter.FromList().OrNone().OrDefault(); + Converters.AnimationNameConverter.FromList().OrNone().OrDefault(); internal AnimationNameProperty() : base(PropertyNames.AnimationName) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationProperty.cs index 3420ad191..be2348c67 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationProperty.cs @@ -4,18 +4,16 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class AnimationProperty : ShorthandProperty { - private static readonly IValueConverter ListConverter = new TimeBasedShorthandConverter( - 0, // duration index - 2, // delay index - TimeConverter.Option().For(PropertyNames.AnimationDuration), // 0 - TransitionConverter.Option().For(PropertyNames.AnimationTimingFunction), // 1 - TimeConverter.Option().For(PropertyNames.AnimationDelay), // 2 - PositiveOrInfiniteNumberConverter.Option().For(PropertyNames.AnimationIterationCount), // 3 - AnimationDirectionConverter.Option().For(PropertyNames.AnimationDirection), // 4 - AnimationFillStyleConverter.Option().For(PropertyNames.AnimationFillMode), // 5 - PlayStateConverter.Option().For(PropertyNames.AnimationPlayState), // 6 - IdentifierConverter.Option().For(PropertyNames.AnimationName) // 7 - ).FromList().OrDefault(); + private static readonly IValueConverter ListConverter = WithAny( + TimeConverter.Option().For(PropertyNames.AnimationDuration), + TransitionConverter.Option().For(PropertyNames.AnimationTimingFunction), + TimeConverter.Option().For(PropertyNames.AnimationDelay), + PositiveOrInfiniteNumberConverter.Option().For(PropertyNames.AnimationIterationCount), + AnimationDirectionConverter.Option().For(PropertyNames.AnimationDirection), + AnimationFillStyleConverter.Option().For(PropertyNames.AnimationFillMode), + PlayStateConverter.Option().For(PropertyNames.AnimationPlayState), + TimelineConverters.ShorthandAnimationName.Option().For(PropertyNames.AnimationName), + TimelineConverters.ShorthandTimeline.Option().For(PropertyNames.AnimationTimeline)).FromList().OrDefault(); internal AnimationProperty() : base(PropertyNames.Animation) { diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationRangeEndProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationRangeEndProperty.cs new file mode 100644 index 000000000..14881f2df --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationRangeEndProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class AnimationRangeEndProperty : Property + { + private static readonly IValueConverter ListConverter = TimelineConverters.RangeEdge.FromList().OrDefault(); + + internal AnimationRangeEndProperty() + : base(PropertyNames.AnimationRangeEnd) + { + } + + internal override IValueConverter Converter => ListConverter; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationRangeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationRangeProperty.cs new file mode 100644 index 000000000..bedab400a --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationRangeProperty.cs @@ -0,0 +1,16 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + using static Converters; + + internal sealed class AnimationRangeProperty : ShorthandProperty + { + private static readonly IValueConverter ListConverter = new AnimationRangeValueConverter().FromList().OrDefault(); + + internal AnimationRangeProperty() + : base(PropertyNames.AnimationRange) + { + } + + internal override IValueConverter Converter => ListConverter; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationRangeStartProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationRangeStartProperty.cs new file mode 100644 index 000000000..1a0f5b4a7 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationRangeStartProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class AnimationRangeStartProperty : Property + { + private static readonly IValueConverter ListConverter = TimelineConverters.RangeEdge.FromList().OrDefault(); + + internal AnimationRangeStartProperty() + : base(PropertyNames.AnimationRangeStart) + { + } + + internal override IValueConverter Converter => ListConverter; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationTimelineProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationTimelineProperty.cs new file mode 100644 index 000000000..f094ffa9d --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/AnimationTimelineProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class AnimationTimelineProperty : Property + { + private static readonly IValueConverter ListConverter = TimelineConverters.AnimationTimeline.FromList().OrDefault(); + + internal AnimationTimelineProperty() + : base(PropertyNames.AnimationTimeline) + { + } + + internal override IValueConverter Converter => ListConverter; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ScrollTimelineAxisProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ScrollTimelineAxisProperty.cs new file mode 100644 index 000000000..0ab810f59 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ScrollTimelineAxisProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollTimelineAxisProperty : Property + { + private static readonly IValueConverter ListConverter = TimelineConverters.TimelineAxis.FromList().OrDefault(); + + internal ScrollTimelineAxisProperty() + : base(PropertyNames.ScrollTimelineAxis) + { + } + + internal override IValueConverter Converter => ListConverter; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ScrollTimelineNameProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ScrollTimelineNameProperty.cs new file mode 100644 index 000000000..5f5f46550 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ScrollTimelineNameProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollTimelineNameProperty : Property + { + private static readonly IValueConverter ListConverter = TimelineConverters.TimelineName.FromList().OrDefault(); + + internal ScrollTimelineNameProperty() + : base(PropertyNames.ScrollTimelineName) + { + } + + internal override IValueConverter Converter => ListConverter; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ScrollTimelineProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ScrollTimelineProperty.cs new file mode 100644 index 000000000..b9f39fc35 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ScrollTimelineProperty.cs @@ -0,0 +1,18 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + using static Converters; + + internal sealed class ScrollTimelineProperty : ShorthandProperty + { + private static readonly IValueConverter ListConverter = WithOrder( + TimelineConverters.TimelineName.Required().For(PropertyNames.ScrollTimelineName), + TimelineConverters.TimelineAxis.Option().For(PropertyNames.ScrollTimelineAxis)).FromList().OrDefault(); + + internal ScrollTimelineProperty() + : base(PropertyNames.ScrollTimeline) + { + } + + internal override IValueConverter Converter => ListConverter; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/TimelineScopeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/TimelineScopeProperty.cs new file mode 100644 index 000000000..bd92690cd --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/TimelineScopeProperty.cs @@ -0,0 +1,12 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TimelineScopeProperty : Property + { + internal TimelineScopeProperty() + : base(PropertyNames.TimelineScope) + { + } + + internal override IValueConverter Converter => TimelineConverters.TimelineScope; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineAxisProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineAxisProperty.cs new file mode 100644 index 000000000..ed3c11d39 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineAxisProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ViewTimelineAxisProperty : Property + { + private static readonly IValueConverter ListConverter = TimelineConverters.TimelineAxis.FromList().OrDefault(); + + internal ViewTimelineAxisProperty() + : base(PropertyNames.ViewTimelineAxis) + { + } + + internal override IValueConverter Converter => ListConverter; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineInsetProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineInsetProperty.cs new file mode 100644 index 000000000..e07e7c98f --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineInsetProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ViewTimelineInsetProperty : Property + { + private static readonly IValueConverter ListConverter = TimelineConverters.TimelineInset.FromList().OrDefault(); + + internal ViewTimelineInsetProperty() + : base(PropertyNames.ViewTimelineInset) + { + } + + internal override IValueConverter Converter => ListConverter; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineNameProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineNameProperty.cs new file mode 100644 index 000000000..40d492331 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineNameProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ViewTimelineNameProperty : Property + { + private static readonly IValueConverter ListConverter = TimelineConverters.TimelineName.FromList().OrDefault(); + + internal ViewTimelineNameProperty() + : base(PropertyNames.ViewTimelineName) + { + } + + internal override IValueConverter Converter => ListConverter; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineProperty.cs new file mode 100644 index 000000000..c58e1b9db --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Animation/ViewTimelineProperty.cs @@ -0,0 +1,20 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + using static Converters; + + internal sealed class ViewTimelineProperty : ShorthandProperty + { + private static readonly IValueConverter ListConverter = WithOrder( + TimelineConverters.TimelineName.Required().For(PropertyNames.ViewTimelineName), + WithAny( + TimelineConverters.TimelineAxis.Option().For(PropertyNames.ViewTimelineAxis), + TimelineConverters.TimelineInset.Option().For(PropertyNames.ViewTimelineInset))).FromList().OrDefault(); + + internal ViewTimelineProperty() + : base(PropertyNames.ViewTimeline) + { + } + + internal override IValueConverter Converter => ListConverter; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundBlendModeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundBlendModeProperty.cs new file mode 100644 index 000000000..605ffb56f --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundBlendModeProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BackgroundBlendModeProperty : Property + { + private static readonly IValueConverter StyleConverter = + Map.BlendModes.ToConverter().FromList().OrDefault(BlendMode.Normal); + + internal BackgroundBlendModeProperty() + : base(PropertyNames.BackgroundBlendMode) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundClipProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundClipProperty.cs index 5f2374fb0..77cd077aa 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundClipProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundClipProperty.cs @@ -3,7 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class BackgroundClipProperty : Property { private static readonly IValueConverter ListConverter = - Converters.BoxModelConverter.FromList().OrDefault(BoxModel.BorderBox); + Converters.BackgroundClipConverter.FromList().OrDefault(BoxModel.BorderBox); internal BackgroundClipProperty() : base(PropertyNames.BackgroundClip) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundProperty.cs index 8eefb71b7..b93526d83 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Background/BackgroundProperty.cs @@ -1,5 +1,6 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { + using System.Collections.Generic; using static Converters; internal sealed class BackgroundProperty : ShorthandProperty @@ -13,26 +14,48 @@ internal BackgroundProperty() private static readonly IValueConverter NormalLayerConverter = WithAny( OptionalImageSourceConverter.Option().For(PropertyNames.BackgroundImage), - WithOrder( + // [ / ]?: the size cannot stand without the position before its slash. + WithOrderDependingOnFirst( PointConverter.Option().For(PropertyNames.BackgroundPosition), BackgroundSizeConverter.StartsWithDelimiter().Option().For(PropertyNames.BackgroundSize)), BackgroundRepeatsConverter.Option().For(PropertyNames.BackgroundRepeat), BackgroundAttachmentConverter.Option().For(PropertyNames.BackgroundAttachment), BoxModelConverter.Option().For(PropertyNames.BackgroundOrigin), - BoxModelConverter.Option().For(PropertyNames.BackgroundClip)); + BackgroundClipConverter.Option().For(PropertyNames.BackgroundClip)); private static readonly IValueConverter FinalLayerConverter = WithAny( OptionalImageSourceConverter.Option().For(PropertyNames.BackgroundImage), - WithOrder( + // [ / ]?: the size cannot stand without the position before its slash. + WithOrderDependingOnFirst( PointConverter.Option().For(PropertyNames.BackgroundPosition), BackgroundSizeConverter.StartsWithDelimiter().Option().For(PropertyNames.BackgroundSize)), BackgroundRepeatsConverter.Option().For(PropertyNames.BackgroundRepeat), BackgroundAttachmentConverter.Option().For(PropertyNames.BackgroundAttachment), BoxModelConverter.Option().For(PropertyNames.BackgroundOrigin), - BoxModelConverter.Option().For(PropertyNames.BackgroundClip), + BackgroundClipConverter.Option().For(PropertyNames.BackgroundClip), CurrentColorConverter.Option().For(PropertyNames.BackgroundColor)); + // What a layer takes for a longhand its own text leaves out (the initial values, CSS Backgrounds 3 section 2.10). + // The colour is not here: it is set by the last layer only. + private static readonly Dictionary LayerDefaults = new() + { + [PropertyNames.BackgroundImage] = "none", + [PropertyNames.BackgroundPosition] = "0% 0%", + [PropertyNames.BackgroundSize] = "auto", + [PropertyNames.BackgroundRepeat] = "repeat", + [PropertyNames.BackgroundAttachment] = "scroll", + [PropertyNames.BackgroundOrigin] = "padding-box", + [PropertyNames.BackgroundClip] = "border-box", + }; + + // One value sets both background-origin and background-clip; with two, the first is the origin and the second + // the clip. The converter hands the one box to the origin, so a layer with no clip of its own takes its origin's. + private static readonly Dictionary LayerFallbacks = new() + { + [PropertyNames.BackgroundClip] = PropertyNames.BackgroundOrigin, + }; + private static readonly IValueConverter StyleConverter = - NormalLayerConverter.RequiresEnd(FinalLayerConverter).OrDefault(); + NormalLayerConverter.RequiresEnd(FinalLayerConverter, LayerDefaults, LayerFallbacks).OrDefault(); } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BleedProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BleedProperty.cs new file mode 100644 index 000000000..b8302a00c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BleedProperty.cs @@ -0,0 +1,16 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The bleed page descriptor (CSS Paged Media 3 section 13): auto | <length>, the + /// extent of the page bleed area outside the page box. + /// + internal sealed class BleedProperty : Property + { + internal BleedProperty() + : base(PropertyNames.Bleed) + { + } + + internal override IValueConverter Converter => Converters.AutoLengthConverter.OrDefault(); + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BookmarkProperties.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BookmarkProperties.cs new file mode 100644 index 000000000..954d2d0af --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BookmarkProperties.cs @@ -0,0 +1,69 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + using static Converters; + + /// + /// The bookmark-level property (CSS Generated Content 3): none | <integer> (a positive + /// integer), the level of the PDF outline entry an element generates. + /// + internal sealed class BookmarkLevelProperty : Property + { + private static readonly IValueConverter StyleConverter = PositiveIntegerConverter.OrNone().OrDefault(); + + internal BookmarkLevelProperty() + : base(PropertyNames.BookmarkLevel) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// + /// The bookmark-label property (CSS Generated Content 3): a <content-list>, the text of + /// the outline entry. The initial value is content(text). + /// + internal sealed class BookmarkLabelProperty : Property + { + private static readonly IValueConverter StyleConverter = ContentListItemConverter.Many().OrDefault(); + + internal BookmarkLabelProperty() + : base(PropertyNames.BookmarkLabel) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// + /// The bookmark-state property (CSS Generated Content 3): open | closed. + /// + internal sealed class BookmarkStateProperty : Property + { + private static readonly IValueConverter StyleConverter = + Assign("open", "open").Or("closed", "closed").OrDefault(); + + internal BookmarkStateProperty() + : base(PropertyNames.BookmarkState) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// + /// The bookmark-target property: self | none | <url> | attr(), the document location the + /// outline entry points at. The initial value is self. + /// + internal sealed class BookmarkTargetProperty : Property + { + private static readonly IValueConverter StyleConverter = + Assign("self", "self").Or(UrlConverter).Or(AttrConverter).OrNone().OrDefault(); + + internal BookmarkTargetProperty() + : base(PropertyNames.BookmarkTarget) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderEndEndRadiusProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderEndEndRadiusProperty.cs new file mode 100644 index 000000000..1e50cb6b6 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderEndEndRadiusProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderEndEndRadiusProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.BorderRadiusConverter.OrDefault(Length.Zero); + + internal BorderEndEndRadiusProperty() + : base(PropertyNames.BorderEndEndRadius, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderEndStartRadiusProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderEndStartRadiusProperty.cs new file mode 100644 index 000000000..7f7d8dd1d --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderEndStartRadiusProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderEndStartRadiusProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.BorderRadiusConverter.OrDefault(Length.Zero); + + internal BorderEndStartRadiusProperty() + : base(PropertyNames.BorderEndStartRadius, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderStartEndRadiusProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderStartEndRadiusProperty.cs new file mode 100644 index 000000000..7039dcc63 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderStartEndRadiusProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderStartEndRadiusProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.BorderRadiusConverter.OrDefault(Length.Zero); + + internal BorderStartEndRadiusProperty() + : base(PropertyNames.BorderStartEndRadius, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderStartStartRadiusProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderStartStartRadiusProperty.cs new file mode 100644 index 000000000..28dfd9360 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/BorderRadius/BorderStartStartRadiusProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderStartStartRadiusProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.BorderRadiusConverter.OrDefault(Length.Zero); + + internal BorderStartStartRadiusProperty() + : base(PropertyNames.BorderStartStartRadius, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/AspectRatioProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/AspectRatioProperty.cs index 2917e270e..98a66b9a2 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/AspectRatioProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/AspectRatioProperty.cs @@ -1,8 +1,5 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// - /// aspect-ratio, CSS Sizing 4 §4.1. - /// internal sealed class AspectRatioProperty : Property { private static readonly IValueConverter StyleConverter = new AspectRatioValueConverter().OrDefault(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/BoxShadowProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/BoxShadowProperty.cs index 2798696c0..4cf798b18 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/BoxShadowProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/BoxShadowProperty.cs @@ -2,7 +2,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class BoxShadowProperty : Property { - private static readonly IValueConverter StyleConverter = new BoxShadowValueConverter().OrDefault(); + private static readonly IValueConverter StyleConverter = Converters.MultipleShadowConverter.OrDefault(); internal BoxShadowProperty() : base(PropertyNames.BoxShadow, PropertyFlags.Animatable) @@ -11,4 +11,4 @@ internal BoxShadowProperty() internal override IValueConverter Converter => StyleConverter; } -} +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/BoxSizingProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/BoxSizingProperty.cs index d6e4e8683..b91c83cbf 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/BoxSizingProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/BoxSizingProperty.cs @@ -4,7 +4,7 @@ internal class BoxSizingProperty : Property { private static readonly IValueConverter StyleConverter = Converters.BoxSizingConverter.OrDefault(Keywords.ContentBox); - public BoxSizingProperty() + public BoxSizingProperty() : base(PropertyNames.BoxSizing, PropertyFlags.None) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/CaptionSideProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/CaptionSideProperty.cs index 6d28eff76..0a9e3295e 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/CaptionSideProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/CaptionSideProperty.cs @@ -4,7 +4,7 @@ internal sealed class CaptionSideProperty : Property { private static readonly IValueConverter StyleConverter = Converters.CaptionSideConverter.OrDefault(true); - internal CaptionSideProperty() : base(PropertyNames.CaptionSide) + internal CaptionSideProperty() : base(PropertyNames.CaptionSide, PropertyFlags.Inherited) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/ColorSchemeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/ColorSchemeProperty.cs new file mode 100644 index 000000000..6c0993e00 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/ColorSchemeProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ColorSchemeProperty : Property + { + private static readonly IValueConverter StyleConverter = new ColorSchemeValueConverter().OrDefault(); + + internal ColorSchemeProperty() + : base(PropertyNames.ColorScheme, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnGapProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnGapProperty.cs index b4fc95832..2f562e1e2 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnGapProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnGapProperty.cs @@ -3,9 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class ColumnGapProperty : Property { private static readonly IValueConverter StyleConverter = - Converters.LengthOrPercentConverter - .OrGlobalValue() - .OrDefault(new Length(1f, Length.Unit.Em)); + Converters.GapConverter.OrGlobalValue(); internal ColumnGapProperty() : base(PropertyNames.ColumnGap, PropertyFlags.Animatable) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnsProperty.cs index 0e71c5b2b..c4f715dbd 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnsProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnsProperty.cs @@ -4,8 +4,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class ColumnsProperty : ShorthandProperty { - // CSS Multi-column 1 §3.3. Both longhands take "auto", a positional match would let column-width - // claim the "auto" of "columns: auto 12em" and strand the length. + // columns is order-independent (CSS Multi-column 1 - "<'column-width'> || <'column-count'>"), and + // both longhands accept "auto", so a positional match lets column-width claim "auto" and strand the + // length in "columns: auto 12em". Match in any order, like list-style (TylerBrinks/ExCSS#185). private static readonly IValueConverter StyleConverter = WithAnyOrderIndependent( AutoLengthConverter.Option().For(PropertyNames.ColumnWidth), OptionalIntegerConverter.Option().For(PropertyNames.ColumnCount)).OrDefault(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Container/ContainProperties.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Container/ContainProperties.cs new file mode 100644 index 000000000..9f819c6f4 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Container/ContainProperties.cs @@ -0,0 +1,104 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// contain (CSS Containment 2 §2): + /// none | strict | content | [ [ size | inline-size ] || layout || style || paint ]. + /// + internal sealed class ContainProperty : Property + { + private static readonly IValueConverter StyleConverter = + new GrammarValueConverter(ContainGrammar.TryParseContain).OrDefault(); + + internal ContainProperty() + : base(PropertyNames.Contain) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// content-visibility (CSS Containment 2 §4): visible | auto | hidden. + internal sealed class ContentVisibilityProperty : Property + { + private static readonly IValueConverter StyleConverter = + new IdentifierValueConverter(Keywords.Visible, true) + .Or(Keywords.Auto) + .Or(Keywords.Hidden) + .OrDefault(); + + internal ContentVisibilityProperty() + : base(PropertyNames.ContentVisibility) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// The contain-intrinsic-* longhands (CSS Sizing 4 §5.1): auto? [ none | <length [0,∞]> ]. + internal abstract class ContainIntrinsicLonghandProperty : Property + { + protected static readonly IValueConverter IntrinsicConverter = + new GrammarValueConverter(ContainGrammar.TryParseIntrinsic).OrDefault(); + + protected ContainIntrinsicLonghandProperty(string name) + : base(name, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => IntrinsicConverter; + } + + internal sealed class ContainIntrinsicWidthProperty : ContainIntrinsicLonghandProperty + { + internal ContainIntrinsicWidthProperty() : base(PropertyNames.ContainIntrinsicWidth) + { + } + } + + internal sealed class ContainIntrinsicHeightProperty : ContainIntrinsicLonghandProperty + { + internal ContainIntrinsicHeightProperty() : base(PropertyNames.ContainIntrinsicHeight) + { + } + } + + internal sealed class ContainIntrinsicInlineSizeProperty : ContainIntrinsicLonghandProperty + { + internal ContainIntrinsicInlineSizeProperty() : base(PropertyNames.ContainIntrinsicInlineSize) + { + } + } + + internal sealed class ContainIntrinsicBlockSizeProperty : ContainIntrinsicLonghandProperty + { + internal ContainIntrinsicBlockSizeProperty() : base(PropertyNames.ContainIntrinsicBlockSize) + { + } + } + + /// contain-intrinsic-size (CSS Sizing 4 §5.1): [ auto? [ none | <length> ] ]{1,2}. + internal sealed class ContainIntrinsicSizeProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = new ContainIntrinsicSizeValueConverter().OrDefault(); + + internal ContainIntrinsicSizeProperty() + : base(PropertyNames.ContainIntrinsicSize, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// The container shorthand (CSS Containment 3 §2): <'container-name'> [ / <'container-type'> ]?. + internal sealed class ContainerProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = new ContainerValueConverter().OrDefault(); + + internal ContainerProperty() + : base(PropertyNames.Container) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Container/ContainerNameProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Container/ContainerNameProperty.cs index 59b01d3b5..1ea8e95d3 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Container/ContainerNameProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Container/ContainerNameProperty.cs @@ -3,7 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class ContainerNameProperty : Property { private static readonly IValueConverter StyleConverter = - Converters.StringConverter.OrDefault(); + Converters.LiteralsConverter.Or(Converters.StringConverter).OrDefault(); internal ContainerNameProperty() : base(PropertyNames.ContainerName) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/ContentProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/ContentProperty.cs index eb829da0d..056c81c7f 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/ContentProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/ContentProperty.cs @@ -14,7 +14,7 @@ internal ContentProperty() : base(PropertyNames.Content) internal override IValueConverter Converter => StyleConverter; private static readonly Dictionary ContentModes = - new Dictionary(StringComparer.OrdinalIgnoreCase) + new(StringComparer.OrdinalIgnoreCase) { {Keywords.OpenQuote, new OpenQuoteContentMode()}, {Keywords.NoOpenQuote, new NoOpenQuoteContentMode()}, @@ -22,17 +22,12 @@ internal ContentProperty() : base(PropertyNames.Content) {Keywords.NoCloseQuote, new NoCloseQuoteContentMode()} }; - private static readonly ContentMode[] Default = new ContentMode[] { new NormalContentMode() }; + private static readonly ContentMode[] Default = {new NormalContentMode()}; private static readonly IValueConverter StyleConverter = Assign(Keywords.Normal, Default).OrNone().Or( ContentModes.ToConverter().Or( UrlConverter).Or( - Converters.GradientConverter).Or( - StringConverter).Or( - AttrConverter).Or( - CounterConverter).Or( - new ContentFunctionConverter()).Or( - new StringFunctionConverter()).Many()).OrDefault(); + ContentListItemConverter).Many()).OrDefault(); private abstract class ContentMode { diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerBottomLeftShapeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerBottomLeftShapeProperty.cs new file mode 100644 index 000000000..35f863c7c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerBottomLeftShapeProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class CornerBottomLeftShapeProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.CornerShapeConverter.OrDefault(Keywords.Round); + + internal CornerBottomLeftShapeProperty() + : base(PropertyNames.CornerBottomLeftShape) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerBottomRightShapeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerBottomRightShapeProperty.cs new file mode 100644 index 000000000..033ff5547 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerBottomRightShapeProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class CornerBottomRightShapeProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.CornerShapeConverter.OrDefault(Keywords.Round); + + internal CornerBottomRightShapeProperty() + : base(PropertyNames.CornerBottomRightShape) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerEndEndShapeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerEndEndShapeProperty.cs new file mode 100644 index 000000000..92152f51c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerEndEndShapeProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class CornerEndEndShapeProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.CornerShapeConverter.OrDefault(Keywords.Round); + + internal CornerEndEndShapeProperty() + : base(PropertyNames.CornerEndEndShape) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerEndStartShapeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerEndStartShapeProperty.cs new file mode 100644 index 000000000..4de6bf43f --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerEndStartShapeProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class CornerEndStartShapeProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.CornerShapeConverter.OrDefault(Keywords.Round); + + internal CornerEndStartShapeProperty() + : base(PropertyNames.CornerEndStartShape) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerShorthands.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerShorthands.cs new file mode 100644 index 000000000..248c04d55 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerShorthands.cs @@ -0,0 +1,114 @@ +using System; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + using static Converters; + + /// One of the CSS Borders 4 corner shorthands: a name, the grammar it parses and the longhands it expands to. + internal sealed class CornerShorthandProperty : ShorthandProperty + { + private readonly IValueConverter _converter; + + internal CornerShorthandProperty(string name, IValueConverter converter) + : base(name, PropertyFlags.Animatable) + { + _converter = converter; + } + + internal override IValueConverter Converter => _converter; + } + + /// + /// The shorthands of CSS Borders 4: border-top-radius... (3.6), corner-shape, + /// corner-top-shape... (3.8) and corner, corner-top, corner-top-left... (3.9). + /// + internal static class CornerShorthands + { + private static readonly string[] PhysicalCorners = { "top-left", "top-right", "bottom-right", "bottom-left" }; + + private static readonly string[] LogicalCorners = { "start-start", "start-end", "end-start", "end-end" }; + + // Side name -> the two corners it covers, in the order the shorthand's two values apply to them. + private static readonly string[][] Sides = + { + new[] { "top", "top-left", "top-right" }, + new[] { "right", "top-right", "bottom-right" }, + new[] { "bottom", "bottom-left", "bottom-right" }, + new[] { "left", "top-left", "bottom-left" }, + new[] { "block-start", "start-start", "start-end" }, + new[] { "block-end", "end-start", "end-end" }, + new[] { "inline-start", "start-start", "end-start" }, + new[] { "inline-end", "start-end", "end-end" } + }; + + private static string Shape(string corner) => "corner-" + corner + "-shape"; + + private static string Radius(string corner) => "border-" + corner + "-radius"; + + /// Hands every shorthand to as (name, converter, longhands). + public static void Register(Action add) + { + var radii = new[] + { + PropertyNames.BorderTopLeftRadius, PropertyNames.BorderTopRightRadius, + PropertyNames.BorderBottomRightRadius, PropertyNames.BorderBottomLeftRadius + }; + var shapes = new string[4]; + + for (var i = 0; i < 4; i++) shapes[i] = Shape(PhysicalCorners[i]); + + // corner-shape: {1,4} + add(PropertyNames.CornerShape, CornerShapeConverter.Periodic(shapes).OrDefault(), shapes); + + // corner: || + add(PropertyNames.Corner, WithAny( + CornerShapeConverter.Periodic(shapes).Option().For(shapes), + BorderRadiusShorthandConverter.Option().For(radii)).OrDefault(), + Concat(shapes, radii)); + + foreach (var side in Sides) + { + var sideShapes = new[] { Shape(side[1]), Shape(side[2]) }; + var sideRadii = new[] { Radius(side[1]), Radius(side[2]) }; + var radiusConverter = new BorderRadiusConverter(sideRadii); + + // corner-top-shape: {1,2} + add("corner-" + side[0] + "-shape", CornerShapeConverter.Periodic(sideShapes).OrDefault(), sideShapes); + + // border-top-radius: {1,2} [ / {1,2} ]? + add("border-" + side[0] + "-radius", radiusConverter.OrDefault(), sideRadii); + + // corner-top: || + add("corner-" + side[0], WithAny( + CornerShapeConverter.Periodic(sideShapes).Option().For(sideShapes), + radiusConverter.Option().For(sideRadii)).OrDefault(), + Concat(sideShapes, sideRadii)); + } + + foreach (var corners in new[] { PhysicalCorners, LogicalCorners }) + { + foreach (var corner in corners) + { + var shape = Shape(corner); + var radius = Radius(corner); + + // corner-top-left: || + add("corner-" + corner, WithAny( + CornerShapeConverter.Option().For(shape), + Converters.BorderRadiusConverter.Option().For(radius)).OrDefault(), + new[] { shape, radius }); + } + } + } + + private static string[] Concat(string[] a, string[] b) + { + var result = new string[a.Length + b.Length]; + + a.CopyTo(result, 0); + b.CopyTo(result, a.Length); + + return result; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerStartEndShapeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerStartEndShapeProperty.cs new file mode 100644 index 000000000..48348a054 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerStartEndShapeProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class CornerStartEndShapeProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.CornerShapeConverter.OrDefault(Keywords.Round); + + internal CornerStartEndShapeProperty() + : base(PropertyNames.CornerStartEndShape) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerStartStartShapeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerStartStartShapeProperty.cs new file mode 100644 index 000000000..24c121f4c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerStartStartShapeProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class CornerStartStartShapeProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.CornerShapeConverter.OrDefault(Keywords.Round); + + internal CornerStartStartShapeProperty() + : base(PropertyNames.CornerStartStartShape) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerTopLeftShapeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerTopLeftShapeProperty.cs new file mode 100644 index 000000000..5fd9f14f9 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerTopLeftShapeProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class CornerTopLeftShapeProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.CornerShapeConverter.OrDefault(Keywords.Round); + + internal CornerTopLeftShapeProperty() + : base(PropertyNames.CornerTopLeftShape) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerTopRightShapeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerTopRightShapeProperty.cs new file mode 100644 index 000000000..5d7759bbf --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Corner/CornerTopRightShapeProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class CornerTopRightShapeProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.CornerShapeConverter.OrDefault(Keywords.Round); + + internal CornerTopRightShapeProperty() + : base(PropertyNames.CornerTopRightShape) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/CounterSystemDescriptorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/CounterSystemDescriptorProperty.cs new file mode 100644 index 000000000..fdc54e535 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/CounterSystemDescriptorProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// The system descriptor of an @counter-style rule. + internal sealed class CounterSystemDescriptorProperty : Property + { + private static readonly IValueConverter StyleConverter = new CounterSystemValueConverter(); + + internal CounterSystemDescriptorProperty() + : base("system") + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/CustomProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/CustomProperty.cs index 45cace094..37d4d3512 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/CustomProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/CustomProperty.cs @@ -1,5 +1,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { + /// + /// A custom property - a declaration whose name begins with two dashes, e.g. --main-color + /// (CSS Custom Properties 1 §2). Its value has no fixed grammar, so it is stored as-is. + /// internal sealed class CustomProperty : Property { internal CustomProperty(string name) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Fill/FillOpacityProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Fill/FillOpacityProperty.cs index f81b55ea0..840a100d7 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Fill/FillOpacityProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Fill/FillOpacityProperty.cs @@ -2,10 +2,10 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class FillOpacityProperty : Property { - private static readonly IValueConverter StyleConverter = Converters.NumberConverter.OrDefault(1f); + private static readonly IValueConverter StyleConverter = Converters.OptionalPercentOrNumberConverter; internal FillOpacityProperty() - : base(PropertyNames.FillOpacity, PropertyFlags.Animatable) + : base(PropertyNames.FillOpacity, PropertyFlags.Inherited | PropertyFlags.Animatable) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Fill/FillProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Fill/FillProperty.cs index 7b2f4f766..2f5e721c4 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Fill/FillProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Fill/FillProperty.cs @@ -2,10 +2,10 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class FillProperty : Property { - private static readonly IValueConverter StyleConverter = Converters.PaintConverter.OrGlobalValue(); + private static readonly IValueConverter StyleConverter = Converters.PaintConverter; internal FillProperty() - : base(PropertyNames.Fill, PropertyFlags.Animatable) + : base(PropertyNames.Fill, PropertyFlags.Inherited | PropertyFlags.Animatable) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Fill/FillRuleProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Fill/FillRuleProperty.cs index 786adfe83..6c6889960 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Fill/FillRuleProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Fill/FillRuleProperty.cs @@ -6,7 +6,7 @@ internal sealed class FillRuleProperty : Property Converters.FillRuleConverter.OrDefault(FillRule.Nonzero); public FillRuleProperty() - : base(PropertyNames.FillRule, PropertyFlags.Animatable) + : base(PropertyNames.FillRule, PropertyFlags.Inherited | PropertyFlags.Animatable) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Filter/BackdropFilterProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Filter/BackdropFilterProperty.cs new file mode 100644 index 000000000..140910cf7 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Filter/BackdropFilterProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BackdropFilterProperty : Property + { + private static readonly IValueConverter StyleConverter = new FilterValueConverter().OrDefault(); + + internal BackdropFilterProperty() + : base(PropertyNames.BackdropFilter, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Filter/FilterProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Filter/FilterProperty.cs new file mode 100644 index 000000000..46a5ee0e8 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Filter/FilterProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FilterProperty : Property + { + private static readonly IValueConverter StyleConverter = new FilterValueConverter().OrDefault(); + + internal FilterProperty() + : base(PropertyNames.Filter, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/LegacyBoxProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/LegacyBoxProperty.cs new file mode 100644 index 000000000..b439fca28 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flexbox/LegacyBoxProperty.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The container-side properties of the pre-standard display: -webkit-box ("flexbox 2009") model: + /// box-orient, box-direction, box-pack and box-align, under the -webkit- and + /// -moz- prefixes. Only created when vendor-prefix aliasing is enabled; they keep their own name (no + /// standard counterpart has the same grammar). The item-side box-flex and box-ordinal-group are + /// plain aliases of flex-grow and order. + /// + internal sealed class LegacyBoxProperty : Property + { + private static readonly string[] Prefixes = {"-webkit-", "-moz-"}; + + private static readonly Dictionary Converters = + new(StringComparer.OrdinalIgnoreCase) + { + ["box-orient"] = Keywords("horizontal", "vertical", "inline-axis", "block-axis"), + ["box-direction"] = Keywords("normal", "reverse"), + ["box-pack"] = Keywords("start", "end", "center", "justify"), + ["box-align"] = Keywords("start", "end", "center", "baseline", "stretch") + }; + + private readonly IValueConverter _converter; + + internal LegacyBoxProperty(string name, string baseName) + : base(name) + { + _converter = Converters[baseName]; + } + + internal override IValueConverter Converter => _converter; + + private static IValueConverter Keywords(params string[] keywords) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var keyword in keywords) map[keyword] = keyword; + + return map.ToConverter().OrGlobalValue(); + } + + /// + /// The base name (box-orient...) of if it is a legacy box property, + /// else . + /// + internal static string BaseNameOf(string propertyName) + { + if (propertyName == null) return null; + + foreach (var prefix in Prefixes) + { + if (!propertyName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) continue; + + var rest = propertyName.Substring(prefix.Length); + foreach (var baseName in Converters.Keys) + { + if (rest.Equals(baseName, StringComparison.OrdinalIgnoreCase)) return baseName; + } + } + + return null; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/FloatReferenceProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/FloatReferenceProperty.cs new file mode 100644 index 000000000..94b6b1ab8 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/FloatReferenceProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FloatReferenceProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("inline", "column", "region", "page").OrDefault(); + + internal FloatReferenceProperty() + : base(PropertyNames.FloatReference) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/FootnoteDisplayProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/FootnoteDisplayProperty.cs new file mode 100644 index 000000000..ed4dbc264 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/FootnoteDisplayProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FootnoteDisplayProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("block", "inline", "compact").OrDefault(); + + internal FootnoteDisplayProperty() + : base(PropertyNames.FootnoteDisplay) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/FootnotePolicyProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/FootnotePolicyProperty.cs new file mode 100644 index 000000000..1da3e0b65 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/FootnotePolicyProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FootnotePolicyProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("auto", "line", "block").OrDefault(); + + internal FootnotePolicyProperty() + : base(PropertyNames.FootnotePolicy) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/OverflowXProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/OverflowXProperty.cs new file mode 100644 index 000000000..1232330d9 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/OverflowXProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class OverflowXProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.OverflowModeConverter.OrDefault(Overflow.Visible); + + internal OverflowXProperty() + : base(PropertyNames.OverflowX) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/OverflowYProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/OverflowYProperty.cs new file mode 100644 index 000000000..2f71bae95 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/OverflowYProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class OverflowYProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.OverflowModeConverter.OrDefault(Overflow.Visible); + + internal OverflowYProperty() + : base(PropertyNames.OverflowY) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/PositionProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/PositionProperty.cs index 04137baf6..ff69c18ef 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/PositionProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Flow/PositionProperty.cs @@ -3,7 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class PositionProperty : Property { private static readonly IValueConverter StyleConverter = - Converters.PositionModeConverter.OrDefault(PositionMode.Static); + Converters.PositionModeConverter.Or(new RunningFunctionConverter()).OrDefault(PositionMode.Static); internal PositionProperty() : base(PropertyNames.Position) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontFaceVariantProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontFaceVariantProperty.cs new file mode 100644 index 000000000..952c49c09 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontFaceVariantProperty.cs @@ -0,0 +1,20 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The font-variant descriptor of @font-face: a flat declaration restricted to + /// normal | small-caps. It cannot reuse , which is the + /// font-variant shorthand of the style properties. + /// + internal sealed class FontFaceVariantProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.FontVariantConverter.OrDefault(FontVariant.Normal); + + internal FontFaceVariantProperty() + : base(PropertyNames.FontVariant, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontFeatureSettingsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontFeatureSettingsProperty.cs new file mode 100644 index 000000000..f5c2977f4 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontFeatureSettingsProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontFeatureSettingsProperty : Property + { + private static readonly IValueConverter StyleConverter = + FontFeatureTagListGrammar.FeatureSettingsConverter.OrGlobalValue(); + + internal FontFeatureSettingsProperty() + : base(PropertyNames.FontFeatureSettings, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontKerningProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontKerningProperty.cs new file mode 100644 index 000000000..6db70cdaa --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontKerningProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontKerningProperty : Property + { + private static readonly IValueConverter StyleConverter = + FontGrammarValueConverter.ForKeywords(Keywords.Auto, Keywords.Normal, Keywords.None).OrGlobalValue(); + + internal FontKerningProperty() + : base(PropertyNames.FontKerning, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontLanguageOverrideProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontLanguageOverrideProperty.cs new file mode 100644 index 000000000..15ec870be --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontLanguageOverrideProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontLanguageOverrideProperty : Property + { + private static readonly IValueConverter StyleConverter = + FontLanguageOverrideGrammar.Converter.OrGlobalValue(); + + internal FontLanguageOverrideProperty() + : base(PropertyNames.FontLanguageOverride, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontOpticalSizingProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontOpticalSizingProperty.cs new file mode 100644 index 000000000..14e7bf9b1 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontOpticalSizingProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontOpticalSizingProperty : Property + { + private static readonly IValueConverter StyleConverter = + FontGrammarValueConverter.ForKeywords(Keywords.Auto, Keywords.None).OrGlobalValue(); + + internal FontOpticalSizingProperty() + : base(PropertyNames.FontOpticalSizing, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontPaletteProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontPaletteProperty.cs index d358f75ab..8809f7acd 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontPaletteProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontPaletteProperty.cs @@ -2,9 +2,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// /// The font-palette property (CSS Fonts Module Level 4): selects a color palette for a COLR/CPAL - /// color font. Inherited. The value grammar (normal | light | dark | <dashed-ident> | palette-mix()) - /// is validated by ; the authored text is preserved for the paint-time - /// resolver (CssBoxProperties.ActualFontPalette). + /// color font. Inherited. The value grammar (normal | light | dark | <dashed-ident>) is + /// validated by ; the authored text is preserved verbatim. /// internal sealed class FontPaletteProperty : Property { diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontProperty.cs index ef5412bfd..3fc04cb04 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontProperty.cs @@ -1,14 +1,18 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { + using System.Collections.Generic; using static Converters; internal sealed class FontProperty : ShorthandProperty { - private static readonly IValueConverter StyleConverter = WithOrder( + // The only font-variant values the shorthand embeds are those of : + // normal | small-caps. They set font-variant-caps; the other font-variant-* longhands - like + // font-kerning, font-feature-settings, ... - are reset implicitly (CSS Fonts 4 7.7). + private static readonly IValueConverter ShorthandConverter = WithOrder( WithAny( FontStyleConverter.Option().For(PropertyNames.FontStyle), - FontVariantConverter.Option().For(PropertyNames.FontVariant), - FontWeightConverter.Or(WeightIntegerConverter).Option().For(PropertyNames.FontWeight), + FontVariantConverter.Option().For(PropertyNames.FontVariantCaps), + FontWeightConverter.Or(WeightNumberConverter).Option().For(PropertyNames.FontWeight), FontStretchConverter.Option().For(PropertyNames.FontStretch)), WithOrder( FontSizeConverter.Required().For(PropertyNames.FontSize), @@ -17,6 +21,26 @@ internal sealed class FontProperty : ShorthandProperty .Or(SystemFontConverter) .OrGlobalValue(); + private static readonly IValueConverter StyleConverter = new ImplicitResetValueConverter( + ShorthandConverter, + new Dictionary + { + // font-variant-caps is written by the shorthand, but only normal | small-caps can be. + {PropertyNames.FontVariantCaps, new[] {Keywords.Normal, "small-caps"}}, + {PropertyNames.FontVariantLigatures, new[] {Keywords.Normal}}, + {PropertyNames.FontVariantAlternates, new[] {Keywords.Normal}}, + {PropertyNames.FontVariantNumeric, new[] {Keywords.Normal}}, + {PropertyNames.FontVariantEastAsian, new[] {Keywords.Normal}}, + {PropertyNames.FontVariantPosition, new[] {Keywords.Normal}}, + {PropertyNames.FontVariantEmoji, new[] {Keywords.Normal}}, + {PropertyNames.FontKerning, new[] {Keywords.Auto}}, + {PropertyNames.FontOpticalSizing, new[] {Keywords.Auto}}, + {PropertyNames.FontSizeAdjust, new[] {Keywords.None}}, + {PropertyNames.FontVariationSettings, new[] {Keywords.Normal}}, + {PropertyNames.FontFeatureSettings, new[] {Keywords.Normal}}, + {PropertyNames.FontLanguageOverride, new[] {Keywords.Normal}} + }); + internal FontProperty() : base(PropertyNames.Font, PropertyFlags.Inherited | PropertyFlags.Animatable) { diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSizeAdjustProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSizeAdjustProperty.cs index bd7417b2a..f1c6734be 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSizeAdjustProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSizeAdjustProperty.cs @@ -2,7 +2,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class FontSizeAdjustProperty : Property { - private static readonly IValueConverter StyleConverter = Converters.OptionalNumberConverter.OrDefault(); + private static readonly IValueConverter StyleConverter = FontSizeAdjustGrammar.Converter.OrDefault(); internal FontSizeAdjustProperty() : base(PropertyNames.FontSizeAdjust, PropertyFlags.Inherited | PropertyFlags.Animatable) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontStretchProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontStretchProperty.cs index c5bf8f318..e3955a51d 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontStretchProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontStretchProperty.cs @@ -3,7 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class FontStretchProperty : Property { private static readonly IValueConverter StyleConverter = - Converters.FontStretchConverter.OrDefault(FontStretch.Normal); + Converters.FontStretchConverter.Or(FontStretchGrammar.PercentageConverter).OrDefault(FontStretch.Normal); internal FontStretchProperty() : base(PropertyNames.FontStretch, PropertyFlags.Inherited | PropertyFlags.Animatable) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisPositionProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisPositionProperty.cs new file mode 100644 index 000000000..d5a2f1ac5 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisPositionProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontSynthesisPositionProperty : Property + { + private static readonly IValueConverter StyleConverter = + FontGrammarValueConverter.ForKeywords(Keywords.Auto, Keywords.None).OrGlobalValue(); + + internal FontSynthesisPositionProperty() + : base(PropertyNames.FontSynthesisPosition, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisProperty.cs new file mode 100644 index 000000000..834f8681c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisProperty.cs @@ -0,0 +1,18 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The font-synthesis shorthand (CSS Fonts 4 3.5) of the four font-synthesis-* longhands. + /// + internal sealed class FontSynthesisProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = + new FontSynthesisShorthandValueConverter().OrGlobalValue(); + + internal FontSynthesisProperty() + : base(PropertyNames.FontSynthesis, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisSmallCapsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisSmallCapsProperty.cs new file mode 100644 index 000000000..a1cca7ced --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisSmallCapsProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontSynthesisSmallCapsProperty : Property + { + private static readonly IValueConverter StyleConverter = + FontGrammarValueConverter.ForKeywords(Keywords.Auto, Keywords.None).OrGlobalValue(); + + internal FontSynthesisSmallCapsProperty() + : base(PropertyNames.FontSynthesisSmallCaps, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisStyleProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisStyleProperty.cs new file mode 100644 index 000000000..f37217236 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisStyleProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontSynthesisStyleProperty : Property + { + private static readonly IValueConverter StyleConverter = + FontGrammarValueConverter.ForKeywords(Keywords.Auto, Keywords.None).OrGlobalValue(); + + internal FontSynthesisStyleProperty() + : base(PropertyNames.FontSynthesisStyle, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisWeightProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisWeightProperty.cs new file mode 100644 index 000000000..c0e9bf8b0 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontSynthesisWeightProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontSynthesisWeightProperty : Property + { + private static readonly IValueConverter StyleConverter = + FontGrammarValueConverter.ForKeywords(Keywords.Auto, Keywords.None).OrGlobalValue(); + + internal FontSynthesisWeightProperty() + : base(PropertyNames.FontSynthesisWeight, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantAlternatesProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantAlternatesProperty.cs new file mode 100644 index 000000000..89fe8723a --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantAlternatesProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontVariantAlternatesProperty : Property + { + private static readonly IValueConverter StyleConverter = + FontVariantAlternatesGrammar.Converter.OrGlobalValue(); + + internal FontVariantAlternatesProperty() + : base(PropertyNames.FontVariantAlternates, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantCapsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantCapsProperty.cs new file mode 100644 index 000000000..3cf691a45 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantCapsProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontVariantCapsProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.FontVariantConverter.Or(new FontGrammarValueConverter(FontVariantGrammar.TryParseCaps)).OrGlobalValue(); + + internal FontVariantCapsProperty() + : base(PropertyNames.FontVariantCaps, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantEastAsianProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantEastAsianProperty.cs new file mode 100644 index 000000000..f5b4cc103 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantEastAsianProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontVariantEastAsianProperty : Property + { + private static readonly IValueConverter StyleConverter = + new FontGrammarValueConverter(FontVariantGrammar.TryParseEastAsian).OrGlobalValue(); + + internal FontVariantEastAsianProperty() + : base(PropertyNames.FontVariantEastAsian, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantEmojiProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantEmojiProperty.cs new file mode 100644 index 000000000..22a0db9fc --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantEmojiProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontVariantEmojiProperty : Property + { + private static readonly IValueConverter StyleConverter = + new FontGrammarValueConverter(FontVariantGrammar.TryParseEmoji).OrGlobalValue(); + + internal FontVariantEmojiProperty() + : base(PropertyNames.FontVariantEmoji, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantLigaturesProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantLigaturesProperty.cs new file mode 100644 index 000000000..5c6213c2c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantLigaturesProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontVariantLigaturesProperty : Property + { + private static readonly IValueConverter StyleConverter = + new FontGrammarValueConverter(FontVariantGrammar.TryParseLigatures).OrGlobalValue(); + + internal FontVariantLigaturesProperty() + : base(PropertyNames.FontVariantLigatures, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantNumericProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantNumericProperty.cs new file mode 100644 index 000000000..b90fd1209 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantNumericProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontVariantNumericProperty : Property + { + private static readonly IValueConverter StyleConverter = + new FontGrammarValueConverter(FontVariantGrammar.TryParseNumeric).OrGlobalValue(); + + internal FontVariantNumericProperty() + : base(PropertyNames.FontVariantNumeric, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantPositionProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantPositionProperty.cs new file mode 100644 index 000000000..f5cfba6c6 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantPositionProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontVariantPositionProperty : Property + { + private static readonly IValueConverter StyleConverter = + new FontGrammarValueConverter(FontVariantGrammar.TryParsePosition).OrGlobalValue(); + + internal FontVariantPositionProperty() + : base(PropertyNames.FontVariantPosition, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantProperty.cs index e3d01b64b..cbc6d5206 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariantProperty.cs @@ -1,9 +1,14 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - internal sealed class FontVariantProperty : Property + /// + /// The font-variant shorthand (CSS Fonts 4 6): normal | none | [ <caps> || <ligatures> + /// || <alternates> || <numeric> || <east-asian> || <position> || <emoji> ], expanding to + /// the seven font-variant-* longhands. + /// + internal sealed class FontVariantProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = - Converters.FontVariantConverter.OrDefault(FontVariant.Normal); + new FontVariantShorthandValueConverter().OrGlobalValue(); internal FontVariantProperty() : base(PropertyNames.FontVariant, PropertyFlags.Inherited) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariationSettingsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariationSettingsProperty.cs new file mode 100644 index 000000000..54853d90d --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontVariationSettingsProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FontVariationSettingsProperty : Property + { + private static readonly IValueConverter StyleConverter = + FontFeatureTagListGrammar.VariationSettingsConverter.OrGlobalValue(); + + internal FontVariationSettingsProperty() + : base(PropertyNames.FontVariationSettings, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontWeightProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontWeightProperty.cs index 7ad4e6d2a..f6745e3a9 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontWeightProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Font/FontWeightProperty.cs @@ -5,7 +5,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class FontWeightProperty : Property { private static readonly IValueConverter StyleConverter = FontWeightConverter.Or( - WeightIntegerConverter).OrDefault(FontWeight.Normal); + WeightNumberConverter).OrDefault(FontWeight.Normal); internal FontWeightProperty() : base(PropertyNames.FontWeight, PropertyFlags.Inherited | PropertyFlags.Animatable) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/FontFeatureValueProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/FontFeatureValueProperty.cs new file mode 100644 index 000000000..8ff0f7f8a --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/FontFeatureValueProperty.cs @@ -0,0 +1,18 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// A feature-value entry inside a @styleset/@swash/etc. block: an author-chosen name + /// whose value is one or more non-negative integers (<integer [0,inf]>+). + /// + internal sealed class FontFeatureValueProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.NaturalIntegerConverter.Many(); + + internal FontFeatureValueProperty(string name) + : base(name) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/GapProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/GapProperty.cs index 100e0ee9f..322198b59 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/GapProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/GapProperty.cs @@ -2,7 +2,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal class GapProperty : ShorthandProperty { - private static readonly IValueConverter StyleConverter = Converters.LengthOrPercentConverter + private static readonly IValueConverter StyleConverter = Converters.GapConverter .OrGlobalValue() .Periodic(PropertyNames.RowGap, PropertyNames.ColumnGap); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAreaProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAreaProperty.cs index 2d3368061..27c905a88 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAreaProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAreaProperty.cs @@ -1,6 +1,10 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-area, CSS Grid 2 §8.4. + /// + /// The grid-area shorthand (CSS Grid §8.3.1): <grid-line> [ / <grid-line> ]{0,3}, + /// expanding to grid-row-start / grid-column-start / grid-row-end / + /// grid-column-end. + /// internal sealed class GridAreaProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.GridAreaConverter.OrGlobalValue(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoColumnsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoColumnsProperty.cs index a87f81d62..9a6f6105a 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoColumnsProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoColumnsProperty.cs @@ -1,6 +1,6 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-auto-columns, CSS Grid 2 §7.6. + /// The grid-auto-columns property (CSS Grid §7.6): the size of implicitly-created columns. internal sealed class GridAutoColumnsProperty : Property { private static readonly IValueConverter StyleConverter = new GridAutoTracksValueConverter().OrDefault(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoFlowProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoFlowProperty.cs index 23b2ee980..da7d1f48f 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoFlowProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoFlowProperty.cs @@ -1,6 +1,6 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-auto-flow, CSS Grid 2 §7.7. + /// The grid-auto-flow property (CSS Grid §7.7): the auto-placement direction and packing. internal sealed class GridAutoFlowProperty : Property { private static readonly IValueConverter StyleConverter = new GridAutoFlowValueConverter().OrDefault(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoRowsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoRowsProperty.cs index 3824e2080..d558f3a59 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoRowsProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoRowsProperty.cs @@ -1,6 +1,6 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-auto-rows, CSS Grid 2 §7.6. + /// The grid-auto-rows property (CSS Grid §7.6): the size of implicitly-created rows. internal sealed class GridAutoRowsProperty : Property { private static readonly IValueConverter StyleConverter = new GridAutoTracksValueConverter().OrDefault(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnEndProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnEndProperty.cs index baff211cd..57e4e3fba 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnEndProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnEndProperty.cs @@ -1,6 +1,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-column-end, CSS Grid 2 §8.3. + /// + /// The grid-column-end longhand (CSS Grid §8.3), one edge of a grid item's placement. + /// Validated by the shared . + /// internal sealed class GridColumnEndProperty : Property { private static readonly IValueConverter StyleConverter = Converters.GridLineConverter.OrDefault(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnProperty.cs index 97b789e54..9a0672ff0 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnProperty.cs @@ -1,6 +1,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-column, CSS Grid 2 §8.4. + /// + /// The grid-column shorthand (CSS Grid §8.3.1): <grid-line> [ / <grid-line> ]?, + /// expanding to grid-column-start / grid-column-end. + /// internal sealed class GridColumnProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.GridColumnConverter.OrGlobalValue(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnStartProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnStartProperty.cs index 72bf6b915..c338567ea 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnStartProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnStartProperty.cs @@ -1,6 +1,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-column-start, CSS Grid 2 §8.3. + /// + /// The grid-column-start longhand (CSS Grid §8.3), one edge of a grid item's placement. + /// Validated by the shared . + /// internal sealed class GridColumnStartProperty : Property { private static readonly IValueConverter StyleConverter = Converters.GridLineConverter.OrDefault(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridProperty.cs index 9d124d866..c8b932657 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridProperty.cs @@ -1,6 +1,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid, CSS Grid 2 §7.8. + /// + /// The grid shorthand (CSS Grid §7.8): expands to the three grid-template-* longhands plus + /// grid-auto-flow / grid-auto-rows / grid-auto-columns. + /// internal sealed class GridProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.GridConverter.OrGlobalValue(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowEndProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowEndProperty.cs index e771a471d..3989c33ec 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowEndProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowEndProperty.cs @@ -1,6 +1,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-row-end, CSS Grid 2 §8.3. + /// + /// The grid-row-end longhand (CSS Grid §8.3), one edge of a grid item's placement. + /// Validated by the shared . + /// internal sealed class GridRowEndProperty : Property { private static readonly IValueConverter StyleConverter = Converters.GridLineConverter.OrDefault(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowProperty.cs index 4d92a4734..55839a80f 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowProperty.cs @@ -1,6 +1,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-row, CSS Grid 2 §8.4. + /// + /// The grid-row shorthand (CSS Grid §8.3.1): <grid-line> [ / <grid-line> ]?, + /// expanding to grid-row-start / grid-row-end. + /// internal sealed class GridRowProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.GridRowConverter.OrGlobalValue(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowStartProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowStartProperty.cs index a4a7d2506..ee2d49be0 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowStartProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowStartProperty.cs @@ -1,6 +1,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-row-start, CSS Grid 2 §8.3. + /// + /// The grid-row-start longhand (CSS Grid §8.3), one edge of a grid item's placement. + /// Validated by the shared . + /// internal sealed class GridRowStartProperty : Property { private static readonly IValueConverter StyleConverter = Converters.GridLineConverter.OrDefault(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateAreasProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateAreasProperty.cs index bdf390075..b319f5161 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateAreasProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateAreasProperty.cs @@ -1,6 +1,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-template-areas, CSS Grid 2 §7.3. + /// + /// The grid-template-areas property (CSS Grid §7.3). Validated by the shared + /// ; the authored text is preserved. + /// internal sealed class GridTemplateAreasProperty : Property { private static readonly IValueConverter StyleConverter = new GridTemplateAreasValueConverter().OrDefault(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateColumnsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateColumnsProperty.cs index 3b08f0021..d90e344bd 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateColumnsProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateColumnsProperty.cs @@ -1,6 +1,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-template-columns, CSS Grid 2 §7.2. + /// + /// The grid-template-columns property (CSS Grid Layout Module Level 1/2 §7.2). Validated by the + /// shared ; the authored text is preserved. + /// internal sealed class GridTemplateColumnsProperty : Property { private static readonly IValueConverter StyleConverter = new GridTemplateValueConverter().OrDefault(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateProperty.cs index b920ada57..cea068bdf 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateProperty.cs @@ -1,6 +1,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-template, CSS Grid 2 §7.4. + /// + /// The grid-template shorthand (CSS Grid §7.4): expands to grid-template-rows / + /// grid-template-columns / grid-template-areas. + /// internal sealed class GridTemplateProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.GridTemplateConverter.OrGlobalValue(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateRowsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateRowsProperty.cs index bc6958572..42bbc4488 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateRowsProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateRowsProperty.cs @@ -1,6 +1,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-template-rows, CSS Grid 2 §7.2. + /// + /// The grid-template-rows property (CSS Grid Layout Module Level 1/2 §7.2). Validated by the + /// shared ; the authored text is preserved. + /// internal sealed class GridTemplateRowsProperty : Property { private static readonly IValueConverter StyleConverter = new GridTemplateValueConverter().OrDefault(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifyItemsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifyItemsProperty.cs index 5f0f1411a..9014a682d 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifyItemsProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifyItemsProperty.cs @@ -1,6 +1,6 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// CSS Box Alignment 3 §7.1 + /// The justify-items property (CSS Box Alignment): default inline-axis alignment of grid items. internal sealed class JustifyItemsProperty : Property { private static readonly IValueConverter StyleConverter = Converters.JustifyItemsConverter; diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifySelfProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifySelfProperty.cs index e22f19827..281659998 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifySelfProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifySelfProperty.cs @@ -1,6 +1,6 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// CSS Box Alignment 3 §6.1 + /// The justify-self property (CSS Box Alignment): a grid item's own inline-axis alignment. internal sealed class JustifySelfProperty : Property { private static readonly IValueConverter StyleConverter = Converters.JustifySelfConverter; diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceContentProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceContentProperty.cs index 50559aa1a..954b116fe 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceContentProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceContentProperty.cs @@ -1,6 +1,6 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// CSS Box Alignment 3 §5.2 + /// The place-content shorthand (CSS Box Alignment): <align-content> <justify-content>?. internal sealed class PlaceContentProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.PlaceContentConverter.OrGlobalValue(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceItemsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceItemsProperty.cs index 0675eadbe..5ec9dc74c 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceItemsProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceItemsProperty.cs @@ -1,6 +1,6 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// CSS Box Alignment 3 §7.3 + /// The place-items shorthand (CSS Box Alignment): <align-items> <justify-items>?. internal sealed class PlaceItemsProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.PlaceItemsConverter.OrGlobalValue(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceSelfProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceSelfProperty.cs index 2be5023ef..149ac0c7c 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceSelfProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceSelfProperty.cs @@ -1,6 +1,6 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// CSS Box Alignment 3 §6.3 + /// The place-self shorthand (CSS Box Alignment): <align-self> <justify-self>?. internal sealed class PlaceSelfProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.PlaceSelfConverter.OrGlobalValue(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/IPropertyValue.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/IPropertyValue.cs index ec9ac30e2..efc6acb15 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/IPropertyValue.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/IPropertyValue.cs @@ -6,36 +6,4 @@ internal interface IPropertyValue TokenValue Original { get; } TokenValue ExtractFor(string name); } - - /// - /// Implemented by an that carries a parsed, strongly-typed model - /// (a ) alongside its text, so the cascade can hand the parsed value straight - /// to the box without re-parsing the authored string. The cascade knows the concrete - /// from the property name it is applying (its typed setter casts to the matching - /// ITypedPropertyValue<T>). Grid templates (T = GridTemplate) are the first adopter. - /// - internal interface ITypedPropertyValue - { - CssProperty GetTypedValue(); - } - - internal static class PropertyValueExtensions - { - /// - /// Returns the parsed this value carries when it is an - /// for the requested ; otherwise false - /// (e.g. a global-keyword value or a value without a typed carrier). - /// - public static bool TryGetValue(this IPropertyValue value, out CssProperty typed) - { - if (value is ITypedPropertyValue carrier) - { - typed = carrier.GetTypedValue(); - return true; - } - - typed = null; - return false; - } - } } \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Images/ImageOrientationProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Images/ImageOrientationProperty.cs new file mode 100644 index 000000000..be832a2cc --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Images/ImageOrientationProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ImageOrientationProperty : Property + { + private static readonly IValueConverter StyleConverter = new ImageOrientationValueConverter().OrDefault(); + + internal ImageOrientationProperty() + : base(PropertyNames.ImageOrientation, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Images/ImageResolutionProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Images/ImageResolutionProperty.cs new file mode 100644 index 000000000..468195171 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Images/ImageResolutionProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ImageResolutionProperty : Property + { + private static readonly IValueConverter StyleConverter = new ImageResolutionValueConverter().OrDefault(); + + internal ImageResolutionProperty() + : base(PropertyNames.ImageResolution, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/AccentColorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/AccentColorProperty.cs new file mode 100644 index 000000000..a09b76328 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/AccentColorProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class AccentColorProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.CurrentColorConverter.OrAuto().OrDefault(); + + internal AccentColorProperty() + : base(PropertyNames.AccentColor, PropertyFlags.Inherited | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/CaretColorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/CaretColorProperty.cs new file mode 100644 index 000000000..321e08d18 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/CaretColorProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class CaretColorProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.CurrentColorConverter.OrAuto().OrDefault(); + + internal CaretColorProperty() + : base(PropertyNames.CaretColor, PropertyFlags.Inherited | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/InteractivityProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/InteractivityProperty.cs new file mode 100644 index 000000000..5fc3bcd1b --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/InteractivityProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class InteractivityProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("auto", "inert").OrDefault(); + + internal InteractivityProperty() + : base(PropertyNames.Interactivity, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/PointerEventsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/PointerEventsProperty.cs new file mode 100644 index 000000000..c361f1d30 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/PointerEventsProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class PointerEventsProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("auto", "none", "visiblePainted", "visibleFill", "visibleStroke", "visible", "painted", "fill", "stroke", "all").OrDefault(); + + internal PointerEventsProperty() + : base(PropertyNames.PointerEvents, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/TouchActionProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/TouchActionProperty.cs new file mode 100644 index 000000000..fab26fb36 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/TouchActionProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TouchActionProperty : Property + { + private static readonly IValueConverter StyleConverter = + new TouchActionValueConverter().OrDefault(); + + internal TouchActionProperty() + : base(PropertyNames.TouchAction) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/UserSelectProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/UserSelectProperty.cs new file mode 100644 index 000000000..921840731 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Interaction/UserSelectProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class UserSelectProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("auto", "text", "none", "all", "contain").OrDefault(); + + internal UserSelectProperty() + : base(PropertyNames.UserSelect) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/CounterSetProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/CounterSetProperty.cs new file mode 100644 index 000000000..85f720fff --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/CounterSetProperty.cs @@ -0,0 +1,20 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + using static Converters; + + /// + /// The counter-set property (CSS Lists 3): [ <counter-name> <integer>? ]+ | none. + /// + internal sealed class CounterSetProperty : Property + { + private static readonly IValueConverter StyleConverter = Continuous( + WithOrder(IdentifierConverter.Required(), IntegerConverter.Option(0))).OrDefault(); + + internal CounterSetProperty() + : base(PropertyNames.CounterSet) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/ListStyleProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/ListStyleProperty.cs index 23478381e..5b7b15bd7 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/ListStyleProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/ListStyleProperty.cs @@ -4,8 +4,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class ListStyleProperty : ShorthandProperty { - // CSS Lists 3 §3.6, a "none" goes to whichever of type and image isn't set otherwise, so it can't - // be claimed by position ("none square" parses like "square none"). + // list-style's operands are order-independent (CSS Lists 3 - "in any order"), so a token such as + // "none" that both list-style-type and list-style-image accept must not be claimed positionally. + // See TylerBrinks/ExCSS#185 ("list-style: none square"). private static readonly IValueConverter StyleConverter = WithAnyOrderIndependent( ListStyleConverter.Option().For(PropertyNames.ListStyleType), ListPositionConverter.Option().For(PropertyNames.ListStylePosition), diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockColorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockColorProperty.cs index 8c4ce4463..91adce475 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockColorProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockColorProperty.cs @@ -3,7 +3,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class BorderBlockColorProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.CurrentColorConverter.Periodic( - PropertyNames.BorderTopColor, PropertyNames.BorderBottomColor).OrDefault(); + PropertyNames.BorderBlockStartColor, PropertyNames.BorderBlockEndColor) + .OrDefault(); internal BorderBlockColorProperty() : base(PropertyNames.BorderBlockColor, PropertyFlags.Hashless | PropertyFlags.Animatable) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndColorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndColorProperty.cs new file mode 100644 index 000000000..5d984cbdb --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndColorProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderBlockEndColorProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.CurrentColorConverter.OrDefault(Color.Transparent); + + internal BorderBlockEndColorProperty() + : base(PropertyNames.BorderBlockEndColor) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndProperty.cs new file mode 100644 index 000000000..0dae436c5 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndProperty.cs @@ -0,0 +1,20 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + using static Converters; + + internal sealed class BorderBlockEndProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = WithAny( + LineWidthConverter.Option().For(PropertyNames.BorderBlockEndWidth), + LineStyleConverter.Option().For(PropertyNames.BorderBlockEndStyle), + CurrentColorConverter.Option().For(PropertyNames.BorderBlockEndColor) + ).OrDefault(); + + internal BorderBlockEndProperty() + : base(PropertyNames.BorderBlockEnd, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndStyleProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndStyleProperty.cs new file mode 100644 index 000000000..bd16e9382 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndStyleProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderBlockEndStyleProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.LineStyleConverter.OrDefault(LineStyle.None); + + internal BorderBlockEndStyleProperty() + : base(PropertyNames.BorderBlockEndStyle) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndWidthProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndWidthProperty.cs new file mode 100644 index 000000000..1d34ff46c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockEndWidthProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderBlockEndWidthProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.LineWidthConverter.OrDefault(Length.Medium); + + internal BorderBlockEndWidthProperty() + : base(PropertyNames.BorderBlockEndWidth, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockProperty.cs index eedd13847..f1765eee2 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockProperty.cs @@ -5,12 +5,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class BorderBlockProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = WithAny( - LineWidthConverter.Option() - .For(PropertyNames.BorderTopWidth, PropertyNames.BorderBottomWidth), - LineStyleConverter.Option() - .For(PropertyNames.BorderTopStyle, PropertyNames.BorderBottomStyle), - CurrentColorConverter.Option() - .For(PropertyNames.BorderTopColor, PropertyNames.BorderBottomColor) + LineWidthConverter.Option().For(PropertyNames.BorderBlockStartWidth, PropertyNames.BorderBlockEndWidth), + LineStyleConverter.Option().For(PropertyNames.BorderBlockStartStyle, PropertyNames.BorderBlockEndStyle), + CurrentColorConverter.Option().For(PropertyNames.BorderBlockStartColor, PropertyNames.BorderBlockEndColor) ).OrDefault(); internal BorderBlockProperty() diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartColorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartColorProperty.cs new file mode 100644 index 000000000..9c6728d80 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartColorProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderBlockStartColorProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.CurrentColorConverter.OrDefault(Color.Transparent); + + internal BorderBlockStartColorProperty() + : base(PropertyNames.BorderBlockStartColor) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartProperty.cs new file mode 100644 index 000000000..e0298a70f --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartProperty.cs @@ -0,0 +1,20 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + using static Converters; + + internal sealed class BorderBlockStartProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = WithAny( + LineWidthConverter.Option().For(PropertyNames.BorderBlockStartWidth), + LineStyleConverter.Option().For(PropertyNames.BorderBlockStartStyle), + CurrentColorConverter.Option().For(PropertyNames.BorderBlockStartColor) + ).OrDefault(); + + internal BorderBlockStartProperty() + : base(PropertyNames.BorderBlockStart, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartStyleProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartStyleProperty.cs new file mode 100644 index 000000000..d32bc94c5 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartStyleProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderBlockStartStyleProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.LineStyleConverter.OrDefault(LineStyle.None); + + internal BorderBlockStartStyleProperty() + : base(PropertyNames.BorderBlockStartStyle) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartWidthProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartWidthProperty.cs new file mode 100644 index 000000000..5ccd69114 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStartWidthProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderBlockStartWidthProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.LineWidthConverter.OrDefault(Length.Medium); + + internal BorderBlockStartWidthProperty() + : base(PropertyNames.BorderBlockStartWidth, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStyleProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStyleProperty.cs index ab14e27a5..a19767297 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStyleProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockStyleProperty.cs @@ -3,7 +3,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class BorderBlockStyleProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.LineStyleConverter.Periodic( - PropertyNames.BorderTopStyle, PropertyNames.BorderBottomStyle).OrDefault(); + PropertyNames.BorderBlockStartStyle, PropertyNames.BorderBlockEndStyle) + .OrDefault(); internal BorderBlockStyleProperty() : base(PropertyNames.BorderBlockStyle) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockWidthProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockWidthProperty.cs index 37b978e58..cb2d13f5f 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockWidthProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderBlockWidthProperty.cs @@ -3,7 +3,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class BorderBlockWidthProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.LineWidthConverter.Periodic( - PropertyNames.BorderTopWidth, PropertyNames.BorderBottomWidth).OrDefault(); + PropertyNames.BorderBlockStartWidth, PropertyNames.BorderBlockEndWidth) + .OrDefault(); internal BorderBlockWidthProperty() : base(PropertyNames.BorderBlockWidth, PropertyFlags.Animatable) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineColorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineColorProperty.cs index 8ff8b7dc5..d18b28422 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineColorProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineColorProperty.cs @@ -3,7 +3,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class BorderInlineColorProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.CurrentColorConverter.Periodic( - PropertyNames.BorderLeftColor, PropertyNames.BorderRightColor).OrDefault(); + PropertyNames.BorderInlineStartColor, PropertyNames.BorderInlineEndColor) + .OrDefault(); internal BorderInlineColorProperty() : base(PropertyNames.BorderInlineColor, PropertyFlags.Hashless | PropertyFlags.Animatable) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndColorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndColorProperty.cs new file mode 100644 index 000000000..ed01fd18e --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndColorProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderInlineEndColorProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.CurrentColorConverter.OrDefault(Color.Transparent); + + internal BorderInlineEndColorProperty() + : base(PropertyNames.BorderInlineEndColor) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndProperty.cs new file mode 100644 index 000000000..77b8d3f8e --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndProperty.cs @@ -0,0 +1,20 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + using static Converters; + + internal sealed class BorderInlineEndProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = WithAny( + LineWidthConverter.Option().For(PropertyNames.BorderInlineEndWidth), + LineStyleConverter.Option().For(PropertyNames.BorderInlineEndStyle), + CurrentColorConverter.Option().For(PropertyNames.BorderInlineEndColor) + ).OrDefault(); + + internal BorderInlineEndProperty() + : base(PropertyNames.BorderInlineEnd, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndStyleProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndStyleProperty.cs new file mode 100644 index 000000000..0033591cd --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndStyleProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderInlineEndStyleProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.LineStyleConverter.OrDefault(LineStyle.None); + + internal BorderInlineEndStyleProperty() + : base(PropertyNames.BorderInlineEndStyle) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndWidthProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndWidthProperty.cs new file mode 100644 index 000000000..52a4d6a97 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineEndWidthProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderInlineEndWidthProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.LineWidthConverter.OrDefault(Length.Medium); + + internal BorderInlineEndWidthProperty() + : base(PropertyNames.BorderInlineEndWidth, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineProperty.cs index c8bfe6d14..adcd63cb6 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineProperty.cs @@ -5,12 +5,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class BorderInlineProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = WithAny( - LineWidthConverter.Option() - .For(PropertyNames.BorderLeftWidth, PropertyNames.BorderRightWidth), - LineStyleConverter.Option() - .For(PropertyNames.BorderLeftStyle, PropertyNames.BorderRightStyle), - CurrentColorConverter.Option() - .For(PropertyNames.BorderLeftColor, PropertyNames.BorderRightColor) + LineWidthConverter.Option().For(PropertyNames.BorderInlineStartWidth, PropertyNames.BorderInlineEndWidth), + LineStyleConverter.Option().For(PropertyNames.BorderInlineStartStyle, PropertyNames.BorderInlineEndStyle), + CurrentColorConverter.Option().For(PropertyNames.BorderInlineStartColor, PropertyNames.BorderInlineEndColor) ).OrDefault(); internal BorderInlineProperty() diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartColorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartColorProperty.cs new file mode 100644 index 000000000..e0eff8f43 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartColorProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderInlineStartColorProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.CurrentColorConverter.OrDefault(Color.Transparent); + + internal BorderInlineStartColorProperty() + : base(PropertyNames.BorderInlineStartColor) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartProperty.cs new file mode 100644 index 000000000..25986c066 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartProperty.cs @@ -0,0 +1,20 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + using static Converters; + + internal sealed class BorderInlineStartProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = WithAny( + LineWidthConverter.Option().For(PropertyNames.BorderInlineStartWidth), + LineStyleConverter.Option().For(PropertyNames.BorderInlineStartStyle), + CurrentColorConverter.Option().For(PropertyNames.BorderInlineStartColor) + ).OrDefault(); + + internal BorderInlineStartProperty() + : base(PropertyNames.BorderInlineStart, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartStyleProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartStyleProperty.cs new file mode 100644 index 000000000..b6078c2a8 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartStyleProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderInlineStartStyleProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.LineStyleConverter.OrDefault(LineStyle.None); + + internal BorderInlineStartStyleProperty() + : base(PropertyNames.BorderInlineStartStyle) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartWidthProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartWidthProperty.cs new file mode 100644 index 000000000..7f70d3dd2 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStartWidthProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BorderInlineStartWidthProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.LineWidthConverter.OrDefault(Length.Medium); + + internal BorderInlineStartWidthProperty() + : base(PropertyNames.BorderInlineStartWidth, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStyleProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStyleProperty.cs index d3b00a08c..ddce3285d 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStyleProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineStyleProperty.cs @@ -3,7 +3,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class BorderInlineStyleProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.LineStyleConverter.Periodic( - PropertyNames.BorderLeftStyle, PropertyNames.BorderRightStyle).OrDefault(); + PropertyNames.BorderInlineStartStyle, PropertyNames.BorderInlineEndStyle) + .OrDefault(); internal BorderInlineStyleProperty() : base(PropertyNames.BorderInlineStyle) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineWidthProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineWidthProperty.cs index b482f3f9d..a51f1b176 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineWidthProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/BorderInlineWidthProperty.cs @@ -3,7 +3,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class BorderInlineWidthProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.LineWidthConverter.Periodic( - PropertyNames.BorderLeftWidth, PropertyNames.BorderRightWidth).OrDefault(); + PropertyNames.BorderInlineStartWidth, PropertyNames.BorderInlineEndWidth) + .OrDefault(); internal BorderInlineWidthProperty() : base(PropertyNames.BorderInlineWidth, PropertyFlags.Animatable) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetBlockEndProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetBlockEndProperty.cs new file mode 100644 index 000000000..9935af9d5 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetBlockEndProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class InsetBlockEndProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.AutoLengthOrPercentConverter.OrDefault(Keywords.Auto); + + internal InsetBlockEndProperty() + : base(PropertyNames.InsetBlockEnd, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetBlockProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetBlockProperty.cs index 43ab5835e..8a0e06330 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetBlockProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetBlockProperty.cs @@ -3,7 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class InsetBlockProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.AutoLengthOrPercentConverter.Periodic( - PropertyNames.Top, PropertyNames.Bottom) + PropertyNames.InsetBlockStart, PropertyNames.InsetBlockEnd) .OrDefault(Keywords.Auto); internal InsetBlockProperty() diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetBlockStartProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetBlockStartProperty.cs new file mode 100644 index 000000000..0adf3e289 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetBlockStartProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class InsetBlockStartProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.AutoLengthOrPercentConverter.OrDefault(Keywords.Auto); + + internal InsetBlockStartProperty() + : base(PropertyNames.InsetBlockStart, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetInlineEndProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetInlineEndProperty.cs new file mode 100644 index 000000000..ef28160c3 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetInlineEndProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class InsetInlineEndProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.AutoLengthOrPercentConverter.OrDefault(Keywords.Auto); + + internal InsetInlineEndProperty() + : base(PropertyNames.InsetInlineEnd, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetInlineProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetInlineProperty.cs index 27501ac21..83f1161d8 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetInlineProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetInlineProperty.cs @@ -3,7 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class InsetInlineProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.AutoLengthOrPercentConverter.Periodic( - PropertyNames.Left, PropertyNames.Right) + PropertyNames.InsetInlineStart, PropertyNames.InsetInlineEnd) .OrDefault(Keywords.Auto); internal InsetInlineProperty() diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetInlineStartProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetInlineStartProperty.cs new file mode 100644 index 000000000..dbce30032 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/InsetInlineStartProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class InsetInlineStartProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.AutoLengthOrPercentConverter.OrDefault(Keywords.Auto); + + internal InsetInlineStartProperty() + : base(PropertyNames.InsetInlineStart, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginBlockEndProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginBlockEndProperty.cs new file mode 100644 index 000000000..21c99f32b --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginBlockEndProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MarginBlockEndProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.AutoLengthOrPercentConverter.OrDefault(Length.Zero); + + internal MarginBlockEndProperty() + : base(PropertyNames.MarginBlockEnd, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginBlockProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginBlockProperty.cs index 52b9c5497..b2d9b5bf1 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginBlockProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginBlockProperty.cs @@ -3,7 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class MarginBlockProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.AutoLengthOrPercentConverter.Periodic( - PropertyNames.MarginTop, PropertyNames.MarginBottom) + PropertyNames.MarginBlockStart, PropertyNames.MarginBlockEnd) .OrDefault(Length.Zero); internal MarginBlockProperty() diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginBlockStartProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginBlockStartProperty.cs new file mode 100644 index 000000000..1d4c4d739 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginBlockStartProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MarginBlockStartProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.AutoLengthOrPercentConverter.OrDefault(Length.Zero); + + internal MarginBlockStartProperty() + : base(PropertyNames.MarginBlockStart, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginInlineEndProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginInlineEndProperty.cs new file mode 100644 index 000000000..4b3ef975c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginInlineEndProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MarginInlineEndProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.AutoLengthOrPercentConverter.OrDefault(Length.Zero); + + internal MarginInlineEndProperty() + : base(PropertyNames.MarginInlineEnd, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginInlineProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginInlineProperty.cs index 6d8178815..f7e987e28 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginInlineProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginInlineProperty.cs @@ -3,7 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class MarginInlineProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.AutoLengthOrPercentConverter.Periodic( - PropertyNames.MarginLeft, PropertyNames.MarginRight) + PropertyNames.MarginInlineStart, PropertyNames.MarginInlineEnd) .OrDefault(Length.Zero); internal MarginInlineProperty() diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginInlineStartProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginInlineStartProperty.cs new file mode 100644 index 000000000..7ad4a0248 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/MarginInlineStartProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MarginInlineStartProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.AutoLengthOrPercentConverter.OrDefault(Length.Zero); + + internal MarginInlineStartProperty() + : base(PropertyNames.MarginInlineStart, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingBlockEndProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingBlockEndProperty.cs new file mode 100644 index 000000000..b457b1428 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingBlockEndProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class PaddingBlockEndProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.LengthOrPercentConverter.OrDefault(Length.Zero); + + internal PaddingBlockEndProperty() + : base(PropertyNames.PaddingBlockEnd, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingBlockProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingBlockProperty.cs index 091d0eed8..11e8abda2 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingBlockProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingBlockProperty.cs @@ -3,7 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class PaddingBlockProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.LengthOrPercentConverter.Periodic( - PropertyNames.PaddingTop, PropertyNames.PaddingBottom) + PropertyNames.PaddingBlockStart, PropertyNames.PaddingBlockEnd) .OrDefault(Length.Zero); internal PaddingBlockProperty() diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingBlockStartProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingBlockStartProperty.cs new file mode 100644 index 000000000..670c45a98 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingBlockStartProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class PaddingBlockStartProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.LengthOrPercentConverter.OrDefault(Length.Zero); + + internal PaddingBlockStartProperty() + : base(PropertyNames.PaddingBlockStart, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingInlineEndProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingInlineEndProperty.cs new file mode 100644 index 000000000..0d200549f --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingInlineEndProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class PaddingInlineEndProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.LengthOrPercentConverter.OrDefault(Length.Zero); + + internal PaddingInlineEndProperty() + : base(PropertyNames.PaddingInlineEnd, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingInlineProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingInlineProperty.cs index 10e2c2bbc..749eec34a 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingInlineProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingInlineProperty.cs @@ -3,7 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class PaddingInlineProperty : ShorthandProperty { private static readonly IValueConverter StyleConverter = Converters.LengthOrPercentConverter.Periodic( - PropertyNames.PaddingLeft, PropertyNames.PaddingRight) + PropertyNames.PaddingInlineStart, PropertyNames.PaddingInlineEnd) .OrDefault(Length.Zero); internal PaddingInlineProperty() diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingInlineStartProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingInlineStartProperty.cs new file mode 100644 index 000000000..810b917a3 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Logical/PaddingInlineStartProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class PaddingInlineStartProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.LengthOrPercentConverter.OrDefault(Length.Zero); + + internal PaddingInlineStartProperty() + : base(PropertyNames.PaddingInlineStart, PropertyFlags.Unitless | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/MarksProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/MarksProperty.cs new file mode 100644 index 000000000..fa02d3842 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/MarksProperty.cs @@ -0,0 +1,18 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The marks page descriptor (CSS Paged Media 3 section 12): none | [ crop || cross ], + /// the printer's marks drawn outside the page box. + /// + internal sealed class MarksProperty : Property + { + private static readonly IValueConverter StyleConverter = new MarksValueConverter().OrDefault(); + + internal MarksProperty() + : base(PropertyNames.Marks) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskClipProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskClipProperty.cs new file mode 100644 index 000000000..01441d9bb --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskClipProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MaskClipProperty : Property + { + private static readonly IValueConverter StyleConverter = MaskConverters.ClipConverter.OrDefault(); + + internal MaskClipProperty() + : base(PropertyNames.MaskClip) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskCompositeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskCompositeProperty.cs new file mode 100644 index 000000000..c0b60828c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskCompositeProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MaskCompositeProperty : Property + { + private static readonly IValueConverter StyleConverter = MaskConverters.CompositeConverter.OrDefault(); + + internal MaskCompositeProperty() + : base(PropertyNames.MaskComposite) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskImageProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskImageProperty.cs new file mode 100644 index 000000000..35c930671 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskImageProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MaskImageProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.MultipleImageSourceConverter.OrDefault(); + + internal MaskImageProperty() + : base(PropertyNames.MaskImage) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskModeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskModeProperty.cs new file mode 100644 index 000000000..1c7d08173 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskModeProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MaskModeProperty : Property + { + private static readonly IValueConverter StyleConverter = MaskConverters.ModeConverter.OrDefault(); + + internal MaskModeProperty() + : base(PropertyNames.MaskMode) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskOriginProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskOriginProperty.cs new file mode 100644 index 000000000..c5935e345 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskOriginProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MaskOriginProperty : Property + { + private static readonly IValueConverter StyleConverter = MaskConverters.OriginConverter.OrDefault(); + + internal MaskOriginProperty() + : base(PropertyNames.MaskOrigin) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskPositionProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskPositionProperty.cs new file mode 100644 index 000000000..e9194d257 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskPositionProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MaskPositionProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.PointConverter.FromList().OrDefault(); + + internal MaskPositionProperty() + : base(PropertyNames.MaskPosition, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskRepeatProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskRepeatProperty.cs new file mode 100644 index 000000000..8ced37f57 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskRepeatProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MaskRepeatProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.BackgroundRepeatsConverter.FromList().OrDefault(); + + internal MaskRepeatProperty() + : base(PropertyNames.MaskRepeat) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskSizeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskSizeProperty.cs new file mode 100644 index 000000000..d803110a4 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Mask/MaskSizeProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MaskSizeProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.BackgroundSizeConverter.FromList().OrDefault(); + + internal MaskSizeProperty() + : base(PropertyNames.MaskSize, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Math/MathDepthProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Math/MathDepthProperty.cs new file mode 100644 index 000000000..aaf06a8dd --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Math/MathDepthProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MathDepthProperty : Property + { + private static readonly IValueConverter StyleConverter = + new MathDepthValueConverter().OrDefault(); + + internal MathDepthProperty() + : base(PropertyNames.MathDepth, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Math/MathShiftProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Math/MathShiftProperty.cs new file mode 100644 index 000000000..48f94b1d5 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Math/MathShiftProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MathShiftProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("normal", "compact").OrDefault(); + + internal MathShiftProperty() + : base(PropertyNames.MathShift, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Math/MathStyleProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Math/MathStyleProperty.cs new file mode 100644 index 000000000..a855218e7 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Math/MathStyleProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MathStyleProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("normal", "compact").OrDefault(); + + internal MathStyleProperty() + : base(PropertyNames.MathStyle, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Outline/OutlineOffsetProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Outline/OutlineOffsetProperty.cs new file mode 100644 index 000000000..2f16c0c0b --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Outline/OutlineOffsetProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class OutlineOffsetProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.LengthConverter.OrDefault(Length.Zero); + + internal OutlineOffsetProperty() + : base(PropertyNames.OutlineOffset, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Overflow/OverflowClipMarginProperties.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Overflow/OverflowClipMarginProperties.cs new file mode 100644 index 000000000..91b762206 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Overflow/OverflowClipMarginProperties.cs @@ -0,0 +1,115 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// The overflow-clip-margin-* longhands (CSS Overflow 4 §3.2): <visual-box> || <length [0,∞]>. + internal abstract class OverflowClipMarginLonghandProperty : Property + { + private static readonly IValueConverter LonghandConverter = + new GrammarValueConverter(OverflowClipMarginGrammar.TryParse).OrDefault(); + + protected OverflowClipMarginLonghandProperty(string name) + : base(name, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => LonghandConverter; + } + + internal sealed class OverflowClipMarginTopProperty : OverflowClipMarginLonghandProperty + { + internal OverflowClipMarginTopProperty() : base(PropertyNames.OverflowClipMarginTop) + { + } + } + + internal sealed class OverflowClipMarginRightProperty : OverflowClipMarginLonghandProperty + { + internal OverflowClipMarginRightProperty() : base(PropertyNames.OverflowClipMarginRight) + { + } + } + + internal sealed class OverflowClipMarginBottomProperty : OverflowClipMarginLonghandProperty + { + internal OverflowClipMarginBottomProperty() : base(PropertyNames.OverflowClipMarginBottom) + { + } + } + + internal sealed class OverflowClipMarginLeftProperty : OverflowClipMarginLonghandProperty + { + internal OverflowClipMarginLeftProperty() : base(PropertyNames.OverflowClipMarginLeft) + { + } + } + + internal sealed class OverflowClipMarginBlockStartProperty : OverflowClipMarginLonghandProperty + { + internal OverflowClipMarginBlockStartProperty() : base(PropertyNames.OverflowClipMarginBlockStart) + { + } + } + + internal sealed class OverflowClipMarginBlockEndProperty : OverflowClipMarginLonghandProperty + { + internal OverflowClipMarginBlockEndProperty() : base(PropertyNames.OverflowClipMarginBlockEnd) + { + } + } + + internal sealed class OverflowClipMarginInlineStartProperty : OverflowClipMarginLonghandProperty + { + internal OverflowClipMarginInlineStartProperty() : base(PropertyNames.OverflowClipMarginInlineStart) + { + } + } + + internal sealed class OverflowClipMarginInlineEndProperty : OverflowClipMarginLonghandProperty + { + internal OverflowClipMarginInlineEndProperty() : base(PropertyNames.OverflowClipMarginInlineEnd) + { + } + } + + /// The overflow-clip-margin shorthand: one value for all four physical sides. + internal sealed class OverflowClipMarginProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = new OverflowClipMarginShorthandValueConverter( + PropertyNames.OverflowClipMarginTop, PropertyNames.OverflowClipMarginRight, + PropertyNames.OverflowClipMarginBottom, PropertyNames.OverflowClipMarginLeft).OrDefault(); + + internal OverflowClipMarginProperty() + : base(PropertyNames.OverflowClipMargin, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// The overflow-clip-margin-block shorthand: one value for both block sides. + internal sealed class OverflowClipMarginBlockProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = new OverflowClipMarginShorthandValueConverter( + PropertyNames.OverflowClipMarginBlockStart, PropertyNames.OverflowClipMarginBlockEnd).OrDefault(); + + internal OverflowClipMarginBlockProperty() + : base(PropertyNames.OverflowClipMarginBlock, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// The overflow-clip-margin-inline shorthand: one value for both inline sides. + internal sealed class OverflowClipMarginInlineProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = new OverflowClipMarginShorthandValueConverter( + PropertyNames.OverflowClipMarginInlineStart, PropertyNames.OverflowClipMarginInlineEnd).OrDefault(); + + internal OverflowClipMarginInlineProperty() + : base(PropertyNames.OverflowClipMarginInline, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/PageNameProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/PageNameProperty.cs index 10e5346da..117feb846 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/PageNameProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/PageNameProperty.cs @@ -2,9 +2,14 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { using static Converters; + /// + /// The page property (CSS Paged Media 3 §3.4): assigns a box to a named page defined by an + /// @page <name> rule. + /// internal sealed class PageNameProperty : Property { - internal PageNameProperty() : base(PropertyNames.PageName) + internal PageNameProperty() + : base(PropertyNames.PageName) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/PageSizeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/PageSizeProperty.cs index db022367f..62fe198e2 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/PageSizeProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/PageSizeProperty.cs @@ -2,12 +2,19 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { using static Converters; + /// + /// The size descriptor of an @page rule (CSS Paged Media 3 §6.3): a page-size keyword + /// (A4, letter, …), an orientation (portrait/landscape), or one or two + /// explicit lengths. + /// internal sealed class PageSizeProperty : Property { - internal PageSizeProperty() : base(PropertyNames.Size) + internal PageSizeProperty() + : base(PropertyNames.Size) { } - internal override IValueConverter Converter => IdentifierConverter.Or(LengthConverter).Many(1, 2).OrDefault(); + internal override IValueConverter Converter => + IdentifierConverter.Or(LengthConverter).Many(1, 2).OrDefault(); } } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Property.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Property.cs index 682611ab3..93019d4e7 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Property.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Property.cs @@ -23,6 +23,10 @@ public override void ToCss(TextWriter writer, IStyleFormatter formatter) internal bool TrySetValue(TokenValue newTokenValue) { var tokenValue = newTokenValue ?? TokenValue.Initial; + + // A value containing var() can't be validated against this property's grammar here - the + // referenced custom property is only known per-element, at cascade time - so keep it verbatim + // via Converters.Any and let substitution resolve it later (CSS Variables 1 3). var converter = tokenValue.ContainsFunction(FunctionNames.Var) ? Converters.Any : Converter; var value = converter.Convert(tokenValue); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/QuotesProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/QuotesProperty.cs index 8238000a3..010ab3eb6 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/QuotesProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/QuotesProperty.cs @@ -3,7 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class QuotesProperty : Property { private static readonly IValueConverter StyleConverter = - Converters.EvenStringsConverter.OrNone().OrDefault(new[] { "«", "»" }); + Converters.EvenStringsConverter.OrNone().OrDefault(new[] {"«", "»"}); internal QuotesProperty() : base(PropertyNames.Quotes, PropertyFlags.Inherited) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/RowGapProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/RowGapProperty.cs index c7c7929fa..194365eea 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/RowGapProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/RowGapProperty.cs @@ -2,9 +2,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class RowGapProperty : Property { - private static readonly IValueConverter StyleConverter = Converters.LengthOrPercentConverter - .OrGlobalValue() - .OrDefault(0); + private static readonly IValueConverter StyleConverter = Converters.GapConverter.OrGlobalValue(); internal RowGapProperty() : base(PropertyNames.RowGap, PropertyFlags.Animatable) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/OverscrollBehaviorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/OverscrollBehaviorProperty.cs new file mode 100644 index 000000000..ed25590b2 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/OverscrollBehaviorProperty.cs @@ -0,0 +1,16 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class OverscrollBehaviorProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = ScrollConverters.OverscrollBehaviorConverter.Periodic( + PropertyNames.OverscrollBehaviorX, PropertyNames.OverscrollBehaviorY) + .OrDefault(); + + internal OverscrollBehaviorProperty() + : base(PropertyNames.OverscrollBehavior) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/OverscrollBehaviorXProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/OverscrollBehaviorXProperty.cs new file mode 100644 index 000000000..dd209da98 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/OverscrollBehaviorXProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class OverscrollBehaviorXProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.OverscrollBehaviorConverter.OrDefault(); + + internal OverscrollBehaviorXProperty() + : base(PropertyNames.OverscrollBehaviorX) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/OverscrollBehaviorYProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/OverscrollBehaviorYProperty.cs new file mode 100644 index 000000000..a7126481c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/OverscrollBehaviorYProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class OverscrollBehaviorYProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.OverscrollBehaviorConverter.OrDefault(); + + internal OverscrollBehaviorYProperty() + : base(PropertyNames.OverscrollBehaviorY) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollBehaviorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollBehaviorProperty.cs new file mode 100644 index 000000000..2afc2678d --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollBehaviorProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollBehaviorProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.ScrollBehaviorConverter.OrDefault(); + + internal ScrollBehaviorProperty() + : base(PropertyNames.ScrollBehavior) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginBottomProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginBottomProperty.cs new file mode 100644 index 000000000..9c450b789 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginBottomProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollMarginBottomProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.MarginConverter.OrDefault(); + + internal ScrollMarginBottomProperty() + : base(PropertyNames.ScrollMarginBottom, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginLeftProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginLeftProperty.cs new file mode 100644 index 000000000..2a297548b --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginLeftProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollMarginLeftProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.MarginConverter.OrDefault(); + + internal ScrollMarginLeftProperty() + : base(PropertyNames.ScrollMarginLeft, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginProperty.cs new file mode 100644 index 000000000..b7a5aadf0 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginProperty.cs @@ -0,0 +1,17 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollMarginProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = ScrollConverters.MarginConverter.Periodic( + PropertyNames.ScrollMarginTop, PropertyNames.ScrollMarginRight, PropertyNames.ScrollMarginBottom, + PropertyNames.ScrollMarginLeft) + .OrDefault(Length.Zero); + + internal ScrollMarginProperty() + : base(PropertyNames.ScrollMargin) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginRightProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginRightProperty.cs new file mode 100644 index 000000000..78f9a9346 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginRightProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollMarginRightProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.MarginConverter.OrDefault(); + + internal ScrollMarginRightProperty() + : base(PropertyNames.ScrollMarginRight, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginTopProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginTopProperty.cs new file mode 100644 index 000000000..51fc61948 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollMarginTopProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollMarginTopProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.MarginConverter.OrDefault(); + + internal ScrollMarginTopProperty() + : base(PropertyNames.ScrollMarginTop, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingBottomProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingBottomProperty.cs new file mode 100644 index 000000000..84562276e --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingBottomProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollPaddingBottomProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.PaddingConverter.OrDefault(); + + internal ScrollPaddingBottomProperty() + : base(PropertyNames.ScrollPaddingBottom, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingLeftProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingLeftProperty.cs new file mode 100644 index 000000000..8e1086b4d --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingLeftProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollPaddingLeftProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.PaddingConverter.OrDefault(); + + internal ScrollPaddingLeftProperty() + : base(PropertyNames.ScrollPaddingLeft, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingProperty.cs new file mode 100644 index 000000000..04575e619 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingProperty.cs @@ -0,0 +1,17 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollPaddingProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = ScrollConverters.PaddingConverter.Periodic( + PropertyNames.ScrollPaddingTop, PropertyNames.ScrollPaddingRight, PropertyNames.ScrollPaddingBottom, + PropertyNames.ScrollPaddingLeft) + .OrDefault(Length.Zero); + + internal ScrollPaddingProperty() + : base(PropertyNames.ScrollPadding) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingRightProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingRightProperty.cs new file mode 100644 index 000000000..deb539c5c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingRightProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollPaddingRightProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.PaddingConverter.OrDefault(); + + internal ScrollPaddingRightProperty() + : base(PropertyNames.ScrollPaddingRight, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingTopProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingTopProperty.cs new file mode 100644 index 000000000..f0e589fbe --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollPaddingTopProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollPaddingTopProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.PaddingConverter.OrDefault(); + + internal ScrollPaddingTopProperty() + : base(PropertyNames.ScrollPaddingTop, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollSnapAlignProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollSnapAlignProperty.cs new file mode 100644 index 000000000..fc6363938 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollSnapAlignProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollSnapAlignProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.ScrollSnapAlignConverter.OrDefault(); + + internal ScrollSnapAlignProperty() + : base(PropertyNames.ScrollSnapAlign) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollSnapStopProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollSnapStopProperty.cs new file mode 100644 index 000000000..a57ed90e5 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollSnapStopProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollSnapStopProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.ScrollSnapStopConverter.OrDefault(); + + internal ScrollSnapStopProperty() + : base(PropertyNames.ScrollSnapStop) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollSnapTypeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollSnapTypeProperty.cs new file mode 100644 index 000000000..8c54c729e --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollSnapTypeProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollSnapTypeProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.ScrollSnapTypeConverter.OrDefault(); + + internal ScrollSnapTypeProperty() + : base(PropertyNames.ScrollSnapType) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollbarColorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollbarColorProperty.cs new file mode 100644 index 000000000..9a59d1473 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollbarColorProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollbarColorProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.ScrollbarColorConverter.OrDefault(); + + internal ScrollbarColorProperty() + : base(PropertyNames.ScrollbarColor, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollbarGutterProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollbarGutterProperty.cs new file mode 100644 index 000000000..7778f1a89 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollbarGutterProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollbarGutterProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.ScrollbarGutterConverter.OrDefault(); + + internal ScrollbarGutterProperty() + : base(PropertyNames.ScrollbarGutter) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollbarWidthProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollbarWidthProperty.cs new file mode 100644 index 000000000..22c4291fd --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Scroll/ScrollbarWidthProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScrollbarWidthProperty : Property + { + private static readonly IValueConverter StyleConverter = ScrollConverters.ScrollbarWidthConverter.OrDefault(); + + internal ScrollbarWidthProperty() + : base(PropertyNames.ScrollbarWidth) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Shapes/ShapeImageThresholdProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Shapes/ShapeImageThresholdProperty.cs new file mode 100644 index 000000000..cdbdef945 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Shapes/ShapeImageThresholdProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ShapeImageThresholdProperty : Property + { + private static readonly IValueConverter StyleConverter = new NonNegativeValueConverter(Converters.PercentOrNumberConverter).OrDefault(); + + internal ShapeImageThresholdProperty() + : base(PropertyNames.ShapeImageThreshold, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Shapes/ShapeMarginProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Shapes/ShapeMarginProperty.cs new file mode 100644 index 000000000..1fa3131a8 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Shapes/ShapeMarginProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ShapeMarginProperty : Property + { + private static readonly IValueConverter StyleConverter = new NonNegativeValueConverter(Converters.LengthOrPercentConverter).OrDefault(); + + internal ShapeMarginProperty() + : base(PropertyNames.ShapeMargin, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Shapes/ShapeOutsideProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Shapes/ShapeOutsideProperty.cs new file mode 100644 index 000000000..423f62dab --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Shapes/ShapeOutsideProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ShapeOutsideProperty : Property + { + private static readonly IValueConverter StyleConverter = new ShapeOutsideValueConverter().OrDefault(); + + internal ShapeOutsideProperty() + : base(PropertyNames.ShapeOutside) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/ShorthandProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/ShorthandProperty.cs index 5f7a18b91..4229e646c 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/ShorthandProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/ShorthandProperty.cs @@ -12,24 +12,13 @@ public string Stringify(Property[] properties) return Converter.Construct(properties)?.CssText; } - public void Export(Property[] properties) + public virtual void Export(Property[] properties) { foreach (var property in properties) { var value = DeclaredValue.ExtractFor(property.Name); - // A longhand this shorthand's grammar didn't actually match any tokens for (e.g. - // "background-color" when "background: none" only matched the image slot) must reset - // to its initial value, per CSS Cascading - a shorthand declaration always sets every - // longhand it covers, explicitly or implicitly. An empty (but non-null) extracted - // TokenValue means exactly that "omitted", so it's treated the same as null here to - // reach TrySetValue's existing null -> TokenValue.Initial fallback; passing the empty - // value through as-is would instead fail conversion silently, leaving the longhand's - // previous declaration (from an earlier, lower-priority rule) as the winning cascade - // value instead of being properly overridden. - var effectiveValue = value != null && value.Count == 0 ? null : value; - - if (property.TrySetValue(effectiveValue)) property.IsImportant = IsImportant; + if (property.TrySetValue(value)) property.IsImportant = IsImportant; } } } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/StringSetProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/StringSetProperty.cs index 03b1cfe4a..235ea963b 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/StringSetProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/StringSetProperty.cs @@ -1,16 +1,18 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// string-set (CSS Generated Content 3 §2.7.1). + /// The string-set property (GCPM §7.3): sets one or more named strings from a content list, for + /// later use by the string() function (typically in page margin boxes). /// internal sealed class StringSetProperty : Property { - private static readonly IValueConverter ValueConverter = new StringSetValueConverter().OrDefault(); + private static readonly IValueConverter StyleConverter = new StringSetValueConverter().OrDefault(); - internal StringSetProperty() : base(PropertyNames.StringSet) + internal StringSetProperty() + : base(PropertyNames.StringSet) { } - internal override IValueConverter Converter => ValueConverter; + internal override IValueConverter Converter => StyleConverter; } } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeDasharrayProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeDasharrayProperty.cs index 6a6b52c66..bb9a51fa6 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeDasharrayProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeDasharrayProperty.cs @@ -2,10 +2,10 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class StrokeDasharrayProperty : Property { - private static readonly IValueConverter StyleConverter = Converters.StrokeDasharrayConverter.OrGlobalValue(); + private static readonly IValueConverter StyleConverter = Converters.StrokeDasharrayConverter; public StrokeDasharrayProperty() - : base(PropertyNames.StrokeDasharray, PropertyFlags.Animatable | PropertyFlags.Unitless) + : base(PropertyNames.StrokeDasharray, PropertyFlags.Inherited | PropertyFlags.Animatable | PropertyFlags.Unitless) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeDashoffsetProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeDashoffsetProperty.cs index 0d38028fb..2bfa5ffef 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeDashoffsetProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeDashoffsetProperty.cs @@ -2,10 +2,10 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class StrokeDashoffsetProperty : Property { - private static readonly IValueConverter StyleConverter = Converters.LengthOrPercentConverter.OrGlobalValue(); + private static readonly IValueConverter StyleConverter = Converters.LengthOrPercentOrNumberConverter; public StrokeDashoffsetProperty() - : base(PropertyNames.StrokeDashoffset, PropertyFlags.Animatable) + : base(PropertyNames.StrokeDashoffset, PropertyFlags.Inherited | PropertyFlags.Animatable) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeLinecapProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeLinecapProperty.cs index 46a0f9890..a1c342b3e 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeLinecapProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeLinecapProperty.cs @@ -6,7 +6,7 @@ internal sealed class StrokeLinecapProperty : Property Converters.StrokeLinecapConverter.OrDefault(StrokeLinecap.Butt); public StrokeLinecapProperty() - : base(PropertyNames.StrokeLinecap, PropertyFlags.Animatable) + : base(PropertyNames.StrokeLinecap, PropertyFlags.Inherited | PropertyFlags.Animatable) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeLinejoinProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeLinejoinProperty.cs index 0c8200192..cc39fca64 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeLinejoinProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeLinejoinProperty.cs @@ -2,10 +2,10 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class StrokeLinejoinProperty : Property { - private static readonly IValueConverter StyleConverter = Converters.StrokeLinejoinConverter.OrGlobalValue(); + private static readonly IValueConverter StyleConverter = Converters.StrokeLinejoinConverter; public StrokeLinejoinProperty() - : base(PropertyNames.StrokeLinejoin, PropertyFlags.Animatable) + : base(PropertyNames.StrokeLinejoin, PropertyFlags.Inherited | PropertyFlags.Animatable) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeMiterlimitProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeMiterlimitProperty.cs index 711fe2bc7..487b1851a 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeMiterlimitProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeMiterlimitProperty.cs @@ -2,10 +2,10 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class StrokeMiterlimitProperty : Property { - private static readonly IValueConverter StyleConverter = Converters.StrokeMiterlimitConverter.OrGlobalValue(); + private static readonly IValueConverter StyleConverter = Converters.StrokeMiterlimitConverter; public StrokeMiterlimitProperty() - : base(PropertyNames.StrokeMiterlimit, PropertyFlags.Animatable) + : base(PropertyNames.StrokeMiterlimit, PropertyFlags.Inherited | PropertyFlags.Animatable) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeOpacityProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeOpacityProperty.cs index 9f007e4c0..2dea047d4 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeOpacityProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeOpacityProperty.cs @@ -2,10 +2,10 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class StrokeOpacityProperty : Property { - private static readonly IValueConverter StyleConverter = Converters.NumberConverter.OrDefault(1f); + private static readonly IValueConverter StyleConverter = Converters.OptionalPercentOrNumberConverter; internal StrokeOpacityProperty() - : base(PropertyNames.StrokeOpacity, PropertyFlags.Animatable) + : base(PropertyNames.StrokeOpacity, PropertyFlags.Inherited | PropertyFlags.Animatable) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeProperty.cs index 882acb346..216ffd39c 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeProperty.cs @@ -2,10 +2,10 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class StrokeProperty : Property { - private static readonly IValueConverter StyleConverter = Converters.PaintConverter.OrGlobalValue(); + private static readonly IValueConverter StyleConverter = Converters.PaintConverter; internal StrokeProperty() - : base(PropertyNames.Stroke, PropertyFlags.Animatable) + : base(PropertyNames.Stroke, PropertyFlags.Inherited | PropertyFlags.Animatable) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeWidthProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeWidthProperty.cs index 56e7a7f6b..07f573ec6 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeWidthProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Stroke/StrokeWidthProperty.cs @@ -2,10 +2,10 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class StrokeWidthProperty : Property { - private static readonly IValueConverter StyleConverter = Converters.LengthOrPercentConverter.OrGlobalValue(); + private static readonly IValueConverter StyleConverter = Converters.LengthOrPercentOrNumberConverter; internal StrokeWidthProperty() - : base(PropertyNames.StrokeWidth, PropertyFlags.Animatable) + : base(PropertyNames.StrokeWidth, PropertyFlags.Inherited | PropertyFlags.Animatable) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/AlignmentBaselineProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/AlignmentBaselineProperty.cs new file mode 100644 index 000000000..9ba6229a2 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/AlignmentBaselineProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class AlignmentBaselineProperty : Property + { + private static readonly IValueConverter StyleConverter = SvgConverters.AlignmentBaselineConverter.OrDefault(); + + internal AlignmentBaselineProperty() + : base(PropertyNames.AlignBaseline, PropertyFlags.None) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/BaselineShiftProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/BaselineShiftProperty.cs new file mode 100644 index 000000000..8c874a692 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/BaselineShiftProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BaselineShiftProperty : Property + { + private static readonly IValueConverter StyleConverter = SvgConverters.BaselineShiftConverter.OrDefault(); + + internal BaselineShiftProperty() + : base(PropertyNames.BaselineShift, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/ClipRuleProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/ClipRuleProperty.cs new file mode 100644 index 000000000..f264ea482 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/ClipRuleProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ClipRuleProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.FillRuleConverter.OrDefault(); + + internal ClipRuleProperty() + : base(PropertyNames.ClipRule, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/ColorInterpolationFiltersProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/ColorInterpolationFiltersProperty.cs new file mode 100644 index 000000000..621d9a8e2 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/ColorInterpolationFiltersProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ColorInterpolationFiltersProperty : Property + { + private static readonly IValueConverter StyleConverter = SvgConverters.ColorInterpolationConverter.OrDefault(); + + internal ColorInterpolationFiltersProperty() + : base(PropertyNames.ColorInterpolationFilters, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/ColorInterpolationProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/ColorInterpolationProperty.cs new file mode 100644 index 000000000..3c9e39280 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/ColorInterpolationProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ColorInterpolationProperty : Property + { + private static readonly IValueConverter StyleConverter = SvgConverters.ColorInterpolationConverter.OrDefault(); + + internal ColorInterpolationProperty() + : base(PropertyNames.ColorInterpolation, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/CxProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/CxProperty.cs new file mode 100644 index 000000000..f226d4a68 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/CxProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class CxProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.LengthOrPercentConverter.OrDefault(); + + internal CxProperty() + : base(PropertyNames.Cx, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/CyProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/CyProperty.cs new file mode 100644 index 000000000..d20d35a42 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/CyProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class CyProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.LengthOrPercentConverter.OrDefault(); + + internal CyProperty() + : base(PropertyNames.Cy, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/DProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/DProperty.cs new file mode 100644 index 000000000..c74d7c5a7 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/DProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class DProperty : Property + { + private static readonly IValueConverter StyleConverter = SvgConverters.PathDataConverter.OrDefault(); + + internal DProperty() + : base(PropertyNames.D, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/DominantBaselineProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/DominantBaselineProperty.cs new file mode 100644 index 000000000..d21a346aa --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/DominantBaselineProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class DominantBaselineProperty : Property + { + private static readonly IValueConverter StyleConverter = SvgConverters.DominantBaselineConverter.OrDefault(); + + internal DominantBaselineProperty() + : base(PropertyNames.DominantBaseline, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/FloodColorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/FloodColorProperty.cs new file mode 100644 index 000000000..7b0aa8f12 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/FloodColorProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FloodColorProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.CurrentColorConverter.OrDefault(); + + internal FloodColorProperty() + : base(PropertyNames.FloodColor, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/FloodOpacityProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/FloodOpacityProperty.cs new file mode 100644 index 000000000..91e0f34dd --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/FloodOpacityProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class FloodOpacityProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.PercentOrNumberConverter.OrDefault(); + + internal FloodOpacityProperty() + : base(PropertyNames.FloodOpacity, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/InlineSizeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/InlineSizeProperty.cs new file mode 100644 index 000000000..4fccc41d0 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/InlineSizeProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class InlineSizeProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.AutoLengthOrPercentConverter.OrDefault(); + + internal InlineSizeProperty() + : base(PropertyNames.InlineSize, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/LightingColorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/LightingColorProperty.cs new file mode 100644 index 000000000..7474dedfb --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/LightingColorProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class LightingColorProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.CurrentColorConverter.OrDefault(); + + internal LightingColorProperty() + : base(PropertyNames.LightingColor, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerEndProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerEndProperty.cs new file mode 100644 index 000000000..15fef4200 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerEndProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MarkerEndProperty : Property + { + private static readonly IValueConverter StyleConverter = SvgConverters.MarkerConverter.OrDefault(); + + internal MarkerEndProperty() + : base(PropertyNames.MarkerEnd, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerMidProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerMidProperty.cs new file mode 100644 index 000000000..ff81b41b8 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerMidProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MarkerMidProperty : Property + { + private static readonly IValueConverter StyleConverter = SvgConverters.MarkerConverter.OrDefault(); + + internal MarkerMidProperty() + : base(PropertyNames.MarkerMid, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerProperty.cs new file mode 100644 index 000000000..841e2796e --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MarkerProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = SvgConverters.MarkerConverter.OrDefault(); + + internal MarkerProperty() + : base(PropertyNames.Marker, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerStartProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerStartProperty.cs new file mode 100644 index 000000000..bcbb93220 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/MarkerStartProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MarkerStartProperty : Property + { + private static readonly IValueConverter StyleConverter = SvgConverters.MarkerConverter.OrDefault(); + + internal MarkerStartProperty() + : base(PropertyNames.MarkerStart, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/PaintOrderProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/PaintOrderProperty.cs new file mode 100644 index 000000000..5f4743411 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/PaintOrderProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class PaintOrderProperty : Property + { + private static readonly IValueConverter StyleConverter = SvgConverters.PaintOrderConverter.OrDefault(); + + internal PaintOrderProperty() + : base(PropertyNames.PaintOrder, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/RProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/RProperty.cs new file mode 100644 index 000000000..5e8e768c4 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/RProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class RProperty : Property + { + private static readonly IValueConverter StyleConverter = SvgConverters.NonNegativeLengthOrPercentConverter.OrDefault(); + + internal RProperty() + : base(PropertyNames.R, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/RxProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/RxProperty.cs new file mode 100644 index 000000000..2e503eb75 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/RxProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class RxProperty : Property + { + private static readonly IValueConverter StyleConverter = SvgConverters.AutoNonNegativeLengthOrPercentConverter.OrDefault(); + + internal RxProperty() + : base(PropertyNames.Rx, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/RyProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/RyProperty.cs new file mode 100644 index 000000000..61c3cd515 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/RyProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class RyProperty : Property + { + private static readonly IValueConverter StyleConverter = SvgConverters.AutoNonNegativeLengthOrPercentConverter.OrDefault(); + + internal RyProperty() + : base(PropertyNames.Ry, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/StopColorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/StopColorProperty.cs new file mode 100644 index 000000000..36ffb461f --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/StopColorProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class StopColorProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.CurrentColorConverter.OrDefault(); + + internal StopColorProperty() + : base(PropertyNames.StopColor, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/StopOpacityProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/StopOpacityProperty.cs new file mode 100644 index 000000000..6c75369eb --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/StopOpacityProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class StopOpacityProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.PercentOrNumberConverter.OrDefault(); + + internal StopOpacityProperty() + : base(PropertyNames.StopOpacity, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/SvgConverters.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/SvgConverters.cs new file mode 100644 index 000000000..4766c3096 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/SvgConverters.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Value grammars for the SVG presentation properties (SVG 2, CSS Inline Layout 3, CSS Transforms 1). + /// + internal static class SvgConverters + { + private static IValueConverter KeywordsOf(params string[] keywords) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var keyword in keywords) map[keyword] = keyword; + return map.ToConverter(); + } + + // none | + public static readonly IValueConverter MarkerConverter = Converters.UrlConverter.OrNone(); + + // auto | sRGB | linearRGB + public static readonly IValueConverter ColorInterpolationConverter = + KeywordsOf("auto", "sRGB", "linearRGB"); + + // auto | text-bottom | alphabetic | ideographic | middle | central | mathematical | hanging | text-top + public static readonly IValueConverter DominantBaselineConverter = + KeywordsOf("auto", "text-bottom", "alphabetic", "ideographic", "middle", "central", "mathematical", + "hanging", "text-top"); + + // baseline | text-bottom | alphabetic | ideographic | middle | central | mathematical | hanging | text-top, + // plus the SVG 1.1 / SVG 2 spellings auto, before-edge, text-before-edge, after-edge, text-after-edge. + public static readonly IValueConverter AlignmentBaselineConverter = + KeywordsOf("auto", "baseline", "text-bottom", "alphabetic", "ideographic", "middle", "central", + "mathematical", "hanging", "text-top", "before-edge", "text-before-edge", "after-edge", + "text-after-edge"); + + // baseline | sub | super | top | center | bottom | + public static readonly IValueConverter BaselineShiftConverter = + Converters.LengthOrPercentConverter.Or(KeywordsOf("baseline", "sub", "super", "top", "center", "bottom")); + + // content-box | border-box | fill-box | stroke-box | view-box + public static readonly IValueConverter TransformBoxConverter = + KeywordsOf("content-box", "border-box", "fill-box", "stroke-box", "view-box"); + + public static readonly IValueConverter NonNegativeLengthOrPercentConverter = + new NonNegativeValueConverter(Converters.LengthOrPercentConverter); + + public static readonly IValueConverter AutoNonNegativeLengthOrPercentConverter = + NonNegativeLengthOrPercentConverter.OrAuto(); + + // normal | [ fill || stroke || markers ] + public static readonly IValueConverter PaintOrderConverter = new PaintOrderValueConverter(); + + // none | path( [ , ]? ) + public static readonly IValueConverter PathDataConverter = new PathDataValueConverter(); + + private sealed class NonNegativeValueConverter : IValueConverter + { + private readonly IValueConverter _converter; + + public NonNegativeValueConverter(IValueConverter converter) + { + _converter = converter; + } + + public IPropertyValue Convert(IEnumerable value) + { + var tokens = value.ToArray(); + var first = tokens.FirstOrDefault(t => t.Type != TokenType.Whitespace); + + if (first is UnitToken { Value: < 0f } or NumberToken { Value: < 0f }) return null; + + return _converter.Convert(tokens); + } + + public IPropertyValue Construct(Property[] properties) => _converter.Construct(properties); + } + + private sealed class PaintOrderValueConverter : IValueConverter + { + public IPropertyValue Convert(IEnumerable value) + { + var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (tokens.Length == 0 || tokens.Length > 3 || tokens.Any(t => t.Type != TokenType.Ident)) + return null; + + if (tokens.Length == 1 && tokens[0].Data.Isi("normal")) return new SvgValue(value); + + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var token in tokens) + { + if (!token.Data.Isi("fill") && !token.Data.Isi("stroke") && !token.Data.Isi("markers")) + return null; + + if (!seen.Add(token.Data)) return null; + } + + return new SvgValue(value); + } + + public IPropertyValue Construct(Property[] properties) => properties.Guard(); + } + + private sealed class PathDataValueConverter : IValueConverter + { + public IPropertyValue Convert(IEnumerable value) + { + var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (tokens.Length != 1) return null; + + if (tokens[0].Type == TokenType.Ident && tokens[0].Data.Isi("none")) return new SvgValue(value); + + if (tokens[0] is not FunctionToken function || !function.Data.Isi("path")) return null; + + var args = function.ArgumentTokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); + var index = 0; + + if (args.Length == 3 && args[0].Type == TokenType.Ident && + (args[0].Data.Isi("nonzero") || args[0].Data.Isi("evenodd")) && + args[1].Type == TokenType.Comma) + { + index = 2; + } + + // A bad (newline-terminated) string leaves further tokens behind, so the single-argument check + // already rejects it. Deliberately not reading StringToken.IsValid: its polarity has differed + // between lexer versions. + if (args.Length - index != 1 || args[index] is not StringToken path) return null; + + return IsPathData(path.Data) ? new SvgValue(value) : null; + } + + public IPropertyValue Construct(Property[] properties) => properties.Guard(); + + // Basic validation only: a non-empty path that starts with a moveto and uses only path commands, + // numbers and separators. The full SVG path grammar is left to the consumer. + private static bool IsPathData(string data) + { + var text = data.Trim(); + + if (text.Length == 0 || (text[0] != 'M' && text[0] != 'm')) return false; + + foreach (var c in text) + { + if (!char.IsWhiteSpace(c) && "MmLlHhVvCcSsQqTtAaZz0123456789.,+-eE".IndexOf(c) < 0) + return false; + } + + return true; + } + } + + private sealed class SvgValue : IPropertyValue + { + public SvgValue(IEnumerable tokens) + { + Original = new TokenValue(tokens); + } + + public string CssText => Original.Text; + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) => Original; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/TransformBoxProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/TransformBoxProperty.cs new file mode 100644 index 000000000..05a0a8ef7 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/TransformBoxProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TransformBoxProperty : Property + { + private static readonly IValueConverter StyleConverter = SvgConverters.TransformBoxConverter.OrDefault(); + + internal TransformBoxProperty() + : base(PropertyNames.TransformBox, PropertyFlags.None) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/XProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/XProperty.cs new file mode 100644 index 000000000..44f569e44 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/XProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class XProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.LengthOrPercentConverter.OrDefault(); + + internal XProperty() + : base(PropertyNames.X, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/YProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/YProperty.cs new file mode 100644 index 000000000..7e902ce2d --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Svg/YProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class YProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.LengthOrPercentConverter.OrDefault(); + + internal YProperty() + : base(PropertyNames.Y, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/HyphenationProperties.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/HyphenationProperties.cs new file mode 100644 index 000000000..5005e26a3 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/HyphenationProperties.cs @@ -0,0 +1,89 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// hyphenate-character (CSS Text 4 6.3.1): auto | <string>. + internal sealed class HyphenateCharacterProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.StringConverter.Or(Keywords.Auto).OrDefault(); + + internal HyphenateCharacterProperty() + : base(PropertyNames.HyphenateCharacter, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// hyphenate-limit-chars (CSS Text 4 6.3.4): [ auto | <integer> ]{1,3}. + internal sealed class HyphenateLimitCharsProperty : Property + { + private static readonly IValueConverter StyleConverter = + new GrammarValueConverter(HyphenateLimitCharsGrammar.IsValid).OrDefault(); + + internal HyphenateLimitCharsProperty() + : base(PropertyNames.HyphenateLimitChars, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// hyphenate-limit-lines (CSS Text 4 6.3.5): no-limit | <integer [0,inf]>. + internal sealed class HyphenateLimitLinesProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.NaturalIntegerConverter.Or("no-limit").OrDefault(); + + internal HyphenateLimitLinesProperty() + : base(PropertyNames.HyphenateLimitLines, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// hyphenate-limit-last (CSS Text 4 6.3.5): none | always | column | page | spread. + internal sealed class HyphenateLimitLastProperty : Property + { + private static readonly IValueConverter StyleConverter = + GrammarValueConverter.OneOf(Keywords.None, Keywords.Always, Keywords.Column, Keywords.Page, "spread") + .OrDefault(); + + internal HyphenateLimitLastProperty() + : base(PropertyNames.HyphenateLimitLast, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// hyphenate-limit-zone (CSS Text 4 6.3.3): <length-percentage>. + internal sealed class HyphenateLimitZoneProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.LengthOrPercentConverter.OrDefault(Length.Zero); + + internal HyphenateLimitZoneProperty() + : base(PropertyNames.HyphenateLimitZone, PropertyFlags.Inherited | PropertyFlags.Unitless) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// + /// hanging-punctuation (CSS Text 3 8): none | [ first || [ force-end | allow-end ] || last ]. + /// + internal sealed class HangingPunctuationProperty : Property + { + private static readonly IValueConverter StyleConverter = + new GrammarValueConverter(HangingPunctuationGrammar.IsValid).OrDefault(); + + internal HangingPunctuationProperty() + : base(PropertyNames.HangingPunctuation, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/HyphensProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/HyphensProperty.cs similarity index 64% rename from Source/HtmlRenderer/Core/CssEngine/StyleProperties/HyphensProperty.cs rename to Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/HyphensProperty.cs index 1d2e57761..27ddc37fe 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/HyphensProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/HyphensProperty.cs @@ -3,10 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class HyphensProperty : Property { private static readonly IValueConverter StyleConverter = - Converters.Assign(Keywords.None, Keywords.None) - .Or(Keywords.Manual, Keywords.Manual) - .Or(Keywords.Auto, Keywords.Auto) - .OrDefault(Keywords.Manual); + Converters.HyphensConverter.OrDefault(Hyphens.Manual); internal HyphensProperty() : base(PropertyNames.Hyphens, PropertyFlags.Inherited) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/LineBreakProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/LineBreakProperty.cs new file mode 100644 index 000000000..b07e2e282 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/LineBreakProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class LineBreakProperty : Property + { + private static readonly IValueConverter StyleConverter = TextFlowConverters.LineBreakConverter; + + internal LineBreakProperty() + : base(PropertyNames.LineBreak, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/LineLayoutProperties.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/LineLayoutProperties.cs new file mode 100644 index 000000000..89dacaf70 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/LineLayoutProperties.cs @@ -0,0 +1,75 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// line-clamp (CSS Overflow 4): none | <integer [1,inf]>. + internal sealed class LineClampProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.PositiveIntegerConverter.OrNone().OrDefault(); + + internal LineClampProperty() + : base(PropertyNames.LineClamp) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// block-ellipsis (CSS Overflow 4): none | auto | <string>. + internal sealed class BlockEllipsisProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.StringConverter.OrNone().OrAuto().OrDefault(); + + internal BlockEllipsisProperty() + : base(PropertyNames.BlockEllipsis, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// tab-size (CSS Text 4 3.6): <number [0,inf]> | <length [0,inf]>. + internal sealed class TabSizeProperty : Property + { + private static readonly IValueConverter StyleConverter = + new GrammarValueConverter(NonNegativeLengthGrammar.IsTabSize).OrDefault(); + + internal TabSizeProperty() + : base(PropertyNames.TabSize, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// + /// initial-letter (CSS Inline 3 7.4): + /// normal | <number [1,inf]> <integer [1,inf]>? | <number [1,inf]> [ drop | raise ]. + /// + internal sealed class InitialLetterProperty : Property + { + private static readonly IValueConverter StyleConverter = + new GrammarValueConverter(InitialLetterGrammar.IsValid).OrDefault(); + + internal InitialLetterProperty() + : base(PropertyNames.InitialLetter) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// line-height-step (CSS Rhythmic Sizing 1 3.1): none | <length [0,inf]>. + internal sealed class LineHeightStepProperty : Property + { + private static readonly IValueConverter StyleConverter = + new GrammarValueConverter(RhythmGrammar.IsNoneOrLength).OrDefault(); + + internal LineHeightStepProperty() + : base(PropertyNames.LineHeightStep, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/OverflowWrapProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/OverflowWrapProperty.cs index 19c1a4360..751a3889b 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/OverflowWrapProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/OverflowWrapProperty.cs @@ -5,7 +5,7 @@ internal sealed class OverflowWrapProperty : Property private static readonly IValueConverter StyleConverter = Converters.OverflowWrapConverter; public OverflowWrapProperty() - : base(PropertyNames.OverflowWrap) + : base(PropertyNames.OverflowWrap, PropertyFlags.Inherited) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/RhythmProperties.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/RhythmProperties.cs new file mode 100644 index 000000000..e55d4d968 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/RhythmProperties.cs @@ -0,0 +1,181 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + using static Converters; + + /// block-step-size (CSS Rhythmic Sizing 1 4.1): none | <length [0,inf]>. + internal sealed class BlockStepSizeProperty : Property + { + internal static readonly IValueConverter GrammarConverter = new GrammarValueConverter(RhythmGrammar.IsNoneOrLength); + + private static readonly IValueConverter StyleConverter = GrammarConverter.OrDefault(); + + internal BlockStepSizeProperty() + : base(PropertyNames.BlockStepSize) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// block-step-insert (CSS Rhythmic Sizing 1 4.2): margin-box | padding-box | content-box. + internal sealed class BlockStepInsertProperty : Property + { + internal static readonly IValueConverter GrammarConverter = + GrammarValueConverter.OneOf("margin-box", "padding-box", "content-box"); + + private static readonly IValueConverter StyleConverter = GrammarConverter.OrDefault(); + + internal BlockStepInsertProperty() + : base(PropertyNames.BlockStepInsert) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// block-step-align (CSS Rhythmic Sizing 1 4.3): auto | center | start | end. + internal sealed class BlockStepAlignProperty : Property + { + internal static readonly IValueConverter GrammarConverter = + GrammarValueConverter.OneOf(Keywords.Auto, "center", "start", "end"); + + private static readonly IValueConverter StyleConverter = GrammarConverter.OrDefault(); + + internal BlockStepAlignProperty() + : base(PropertyNames.BlockStepAlign) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// block-step-round (CSS Rhythmic Sizing 1 4.4): up | down | nearest. + internal sealed class BlockStepRoundProperty : Property + { + internal static readonly IValueConverter GrammarConverter = GrammarValueConverter.OneOf("up", "down", "nearest"); + + private static readonly IValueConverter StyleConverter = GrammarConverter.OrDefault(); + + internal BlockStepRoundProperty() + : base(PropertyNames.BlockStepRound) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// + /// block-step (CSS Rhythmic Sizing 1 4): + /// <'block-step-size'> || <'block-step-insert'> || <'block-step-align'> || <'block-step-round'>. + /// + internal sealed class BlockStepProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = WithAny( + BlockStepSizeProperty.GrammarConverter.Option().For(PropertyNames.BlockStepSize), + BlockStepInsertProperty.GrammarConverter.Option().For(PropertyNames.BlockStepInsert), + BlockStepAlignProperty.GrammarConverter.Option().For(PropertyNames.BlockStepAlign), + BlockStepRoundProperty.GrammarConverter.Option().For(PropertyNames.BlockStepRound)).OrDefault(); + + internal BlockStepProperty() + : base(PropertyNames.BlockStep, PropertyFlags.None) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// text-box-trim (CSS Inline 3 5.1): none | trim-start | trim-end | trim-both. + internal sealed class TextBoxTrimProperty : Property + { + internal static readonly IValueConverter GrammarConverter = + GrammarValueConverter.OneOf(Keywords.None, "trim-start", "trim-end", "trim-both"); + + private static readonly IValueConverter StyleConverter = GrammarConverter.OrDefault(); + + internal TextBoxTrimProperty() + : base(PropertyNames.TextBoxTrim) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// + /// text-box-edge (CSS Inline 3 5.2): + /// auto | [ text | cap | ex | ideographic | ideographic-ink ] [ text | alphabetic | ideographic | ideographic-ink ]?. + /// + internal sealed class TextBoxEdgeProperty : Property + { + internal static readonly IValueConverter GrammarConverter = new GrammarValueConverter(RhythmGrammar.IsTextBoxEdge); + + private static readonly IValueConverter StyleConverter = GrammarConverter.OrDefault(); + + internal TextBoxEdgeProperty() + : base(PropertyNames.TextBoxEdge, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } + + /// + /// text-box (CSS Inline 3 5.3): normal | [ <'text-box-trim'> || <'text-box-edge'> ]; + /// normal is none auto. + /// + internal sealed class TextBoxProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = new TextBoxNormalConverter().Or(WithAny( + TextBoxTrimProperty.GrammarConverter.Option().For(PropertyNames.TextBoxTrim), + TextBoxEdgeProperty.GrammarConverter.Option().For(PropertyNames.TextBoxEdge)).OrDefault()); + + internal TextBoxProperty() + : base(PropertyNames.TextBox, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + + /// normal: both longhands at their initial values. + private sealed class TextBoxNormalConverter : IValueConverter + { + public IPropertyValue Convert(IEnumerable value) + { + var tokens = value.ToArray(); + var significant = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + return significant.Length == 1 && significant[0].Type == TokenType.Ident && + significant[0].Data.Isi(Keywords.Normal) + ? new NormalValue(tokens) + : null; + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + + private sealed class NormalValue : IPropertyValue + { + public NormalValue(IEnumerable tokens) + { + Original = new TokenValue(tokens); + } + + public string CssText => Original.Text; + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) + { + if (name.Is(PropertyNames.TextBoxTrim)) return TokenValue.FromString(Keywords.None); + if (name.Is(PropertyNames.TextBoxEdge)) return TokenValue.FromString(Keywords.Auto); + + return null; + } + } + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAlignAllProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAlignAllProperty.cs new file mode 100644 index 000000000..ba4b4ed3d --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAlignAllProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextAlignAllProperty : Property + { + private static readonly IValueConverter StyleConverter = TextFlowConverters.TextAlignAllConverter; + + internal TextAlignAllProperty() + : base(PropertyNames.TextAlignAll, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAlignLastProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAlignLastProperty.cs index 5f75f6ed2..c87b28673 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAlignLastProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAlignLastProperty.cs @@ -2,10 +2,10 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class TextAlignLastProperty : Property { - private static readonly IValueConverter StyleConverter = Converters.TextAlignLastConverter; + private static readonly IValueConverter StyleConverter = Converters.TextAlignLastConverter.OrDefault(); public TextAlignLastProperty() - : base(PropertyNames.TextAlignLast) + : base(PropertyNames.TextAlignLast, PropertyFlags.Inherited) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAlignProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAlignProperty.cs index 65881ad2e..1348c649d 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAlignProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAlignProperty.cs @@ -1,9 +1,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - internal sealed class TextAlignProperty : Property + internal sealed class TextAlignProperty : ShorthandProperty { - private static readonly IValueConverter StyleConverter = - Converters.HorizontalAlignmentConverter.OrDefault(HorizontalAlignment.Left); + private static readonly IValueConverter StyleConverter = TextFlowConverters.TextAlignConverter; internal TextAlignProperty() : base(PropertyNames.TextAlign, PropertyFlags.Inherited) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAnchorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAnchorProperty.cs index fb43c4466..1ac99b2d0 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAnchorProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextAnchorProperty.cs @@ -5,7 +5,7 @@ internal sealed class TextAnchorProperty : Property private static readonly IValueConverter StyleConverter = Converters.TextAnchorConverter; public TextAnchorProperty() - : base(PropertyNames.TextAnchor) + : base(PropertyNames.TextAnchor, PropertyFlags.Inherited) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextCombineUprightProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextCombineUprightProperty.cs new file mode 100644 index 000000000..ead8137d3 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextCombineUprightProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextCombineUprightProperty : Property + { + private static readonly IValueConverter StyleConverter = TextFlowConverters.TextCombineUprightConverter; + + internal TextCombineUprightProperty() + : base(PropertyNames.TextCombineUpright, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationInsetProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationInsetProperty.cs new file mode 100644 index 000000000..390773923 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationInsetProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextDecorationInsetProperty : Property + { + private static readonly IValueConverter StyleConverter = + new TokenGrammarValueConverter(TextEmphasisGrammar.TryParseInset).OrDefault(); + + internal TextDecorationInsetProperty() + : base(PropertyNames.TextDecorationInset) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationProperty.cs index 325ce75e5..654433f6f 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationProperty.cs @@ -7,7 +7,8 @@ internal sealed class TextDecorationProperty : ShorthandProperty private static readonly IValueConverter StyleConverter = WithAny( ColorConverter.Option().For(PropertyNames.TextDecorationColor), TextDecorationStyleConverter.Option().For(PropertyNames.TextDecorationStyle), - TextDecorationLinesConverter.Option().For(PropertyNames.TextDecorationLine)).OrDefault(); + TextDecorationLinesConverter.Option().For(PropertyNames.TextDecorationLine), + TextDecorationThicknessProperty.ValueConverter.Option().For(PropertyNames.TextDecorationThickness)).OrDefault(); internal TextDecorationProperty() : base(PropertyNames.TextDecoration, PropertyFlags.Animatable) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationSkipInkProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationSkipInkProperty.cs new file mode 100644 index 000000000..8a03551cb --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationSkipInkProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextDecorationSkipInkProperty : Property + { + private static readonly IValueConverter StyleConverter = + TokenGrammarValueConverter.OneOf(Keywords.Auto, Keywords.None, Keywords.All).OrDefault(); + + internal TextDecorationSkipInkProperty() + : base(PropertyNames.TextDecorationSkipInk, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationSkipProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationSkipProperty.cs new file mode 100644 index 000000000..0e05fb0dc --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationSkipProperty.cs @@ -0,0 +1,16 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextDecorationSkipProperty : Property + { + // Legacy (CSS Text Decoration 3) skip list; the initial value is objects. + private static readonly IValueConverter StyleConverter = + new TokenGrammarValueConverter(TextEmphasisGrammar.TryParseSkip).OrDefault(); + + internal TextDecorationSkipProperty() + : base(PropertyNames.TextDecorationSkip) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationThicknessProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationThicknessProperty.cs new file mode 100644 index 000000000..e34c86928 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextDecorationThicknessProperty.cs @@ -0,0 +1,18 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextDecorationThicknessProperty : Property + { + // auto | from-font | ; shared with the text-decoration shorthand. + internal static readonly IValueConverter ValueConverter = + Converters.LengthOrPercentConverter.OrAuto().Or("from-font"); + + private static readonly IValueConverter StyleConverter = ValueConverter.OrDefault(); + + internal TextDecorationThicknessProperty() + : base(PropertyNames.TextDecorationThickness) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisColorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisColorProperty.cs new file mode 100644 index 000000000..54f68e09e --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisColorProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextEmphasisColorProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.ColorConverter.OrDefault(); + + internal TextEmphasisColorProperty() + : base(PropertyNames.TextEmphasisColor, PropertyFlags.Inherited | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisPositionProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisPositionProperty.cs new file mode 100644 index 000000000..4caf76edd --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisPositionProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextEmphasisPositionProperty : Property + { + private static readonly IValueConverter StyleConverter = + new TokenGrammarValueConverter(TextEmphasisGrammar.TryParsePosition).OrDefault(); + + internal TextEmphasisPositionProperty() + : base(PropertyNames.TextEmphasisPosition, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisProperty.cs new file mode 100644 index 000000000..b062ad942 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisProperty.cs @@ -0,0 +1,153 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// text-emphasis (CSS Text Decoration 4 4.4): the shorthand + /// <'text-emphasis-style'> || <'text-emphasis-color'>. The style may itself be several + /// tokens (filled circle) and the colour a function, so the value is split at the one boundary + /// between them; an omitted longhand resets to its initial value. + /// + internal sealed class TextEmphasisProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = new TextEmphasisShorthandConverter(); + + internal TextEmphasisProperty() + : base(PropertyNames.TextEmphasis, PropertyFlags.Inherited | PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + + private sealed class TextEmphasisShorthandConverter : IValueConverter + { + public IPropertyValue Convert(IEnumerable value) + { + // Keep the whitespace between a component's own tokens ("filled circle" must stay two words). + var raw = new List(value); + var significant = new List(); + + for (var i = 0; i < raw.Count; i++) + { + if (raw[i].Type != TokenType.Whitespace) significant.Add(i); + } + + if (significant.Count == 0) return null; + + List Slice(int first, int to) => + first >= to + ? new List() + : raw.GetRange(significant[first], significant[to - 1] - significant[first] + 1); + + // The style and the colour each occupy one contiguous run, in either order, so the only + // split that can work is a prefix/suffix one. + for (var split = 0; split <= significant.Count; split++) + { + var head = Slice(0, split); + var tail = Slice(split, significant.Count); + + if (Accepts(head, tail, out var style, out var color)) + return new EmphasisValue(style, color, Slice(0, significant.Count)); + } + + return null; + } + + private static bool IsStyle(List tokens) => TextEmphasisGrammar.TryParseStyle(tokens); + + private static bool IsColor(List tokens) => Converters.ColorConverter.Convert(tokens) != null; + + private static bool Accepts(List head, List tail, out List style, + out List color) + { + style = null; + color = null; + + if (head.Count == 0) + { + // Only one component, either of the two. + if (IsStyle(tail)) + { + style = tail; + return true; + } + + if (IsColor(tail)) + { + color = tail; + return true; + } + + return false; + } + + if (tail.Count == 0) return false; + + if (IsStyle(head) && IsColor(tail)) + { + style = head; + color = tail; + return true; + } + + if (IsColor(head) && IsStyle(tail)) + { + style = tail; + color = head; + return true; + } + + return false; + } + + public IPropertyValue Construct(Property[] properties) + { + var style = properties.FirstOrDefault(p => p.Name.Is(PropertyNames.TextEmphasisStyle)); + var color = properties.FirstOrDefault(p => p.Name.Is(PropertyNames.TextEmphasisColor)); + var parts = new List(); + + if (style is { HasValue: true } && !style.IsInitial) parts.Add(style.Value); + if (color is { HasValue: true } && !color.IsInitial) parts.Add(color.Value); + + return parts.Count == 0 ? null : new TextValue(string.Join(" ", parts)); + } + + private sealed class TextValue : IPropertyValue + { + public TextValue(string text) + { + CssText = text; + } + + public string CssText { get; } + + public TokenValue Original => TokenValue.FromString(CssText); + + public TokenValue ExtractFor(string name) => Original; + } + + private sealed class EmphasisValue : IPropertyValue + { + private readonly TokenValue _style; + private readonly TokenValue _color; + + public EmphasisValue(List style, List color, List all) + { + _style = style == null ? null : new TokenValue(style); + _color = color == null ? null : new TokenValue(color); + Original = new TokenValue(all); + } + + public string CssText => Original.Text; + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) => + name.Is(PropertyNames.TextEmphasisStyle) ? _style + : name.Is(PropertyNames.TextEmphasisColor) ? _color + : null; + } + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisStyleProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisStyleProperty.cs new file mode 100644 index 000000000..fbd969f74 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextEmphasisStyleProperty.cs @@ -0,0 +1,17 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextEmphasisStyleProperty : Property + { + internal static readonly IValueConverter ValueConverter = + new TokenGrammarValueConverter(TextEmphasisGrammar.TryParseStyle); + + private static readonly IValueConverter StyleConverter = ValueConverter.OrDefault(); + + internal TextEmphasisStyleProperty() + : base(PropertyNames.TextEmphasisStyle, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextIndentProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextIndentProperty.cs index 079e9447c..04c674b0c 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextIndentProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextIndentProperty.cs @@ -3,7 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class TextIndentProperty : Property { private static readonly IValueConverter StyleConverter = - Converters.LengthOrPercentConverter.OrDefault(Length.Zero); + new TokenGrammarValueConverter(TextIndentGrammar.TryParse).OrDefault(Length.Zero); internal TextIndentProperty() : base(PropertyNames.TextIndent, PropertyFlags.Inherited | PropertyFlags.Animatable) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextJustifyProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextJustifyProperty.cs index 568ef5024..d19408238 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextJustifyProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextJustifyProperty.cs @@ -5,7 +5,7 @@ internal sealed class TextJustifyProperty : Property private static readonly IValueConverter StyleConverter = Converters.TextJustifyConverter; public TextJustifyProperty() - : base(PropertyNames.TextJustify) + : base(PropertyNames.TextJustify, PropertyFlags.Inherited) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextOrientationProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextOrientationProperty.cs new file mode 100644 index 000000000..446473c6f --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextOrientationProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextOrientationProperty : Property + { + private static readonly IValueConverter StyleConverter = TextFlowConverters.TextOrientationConverter; + + internal TextOrientationProperty() + : base(PropertyNames.TextOrientation, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextOverflowProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextOverflowProperty.cs new file mode 100644 index 000000000..f0573cf32 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextOverflowProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextOverflowProperty : Property + { + private static readonly IValueConverter StyleConverter = Converters.TextOverflowConverter.OrDefault(); + + internal TextOverflowProperty() + : base(PropertyNames.TextOverflow) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextRenderingProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextRenderingProperty.cs new file mode 100644 index 000000000..d4c48abb6 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextRenderingProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextRenderingProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("auto", "optimizeSpeed", "optimizeLegibility", "geometricPrecision").OrDefault(); + + internal TextRenderingProperty() + : base(PropertyNames.TextRendering, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextShadowProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextShadowProperty.cs index 5a5f182a3..618a31f40 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextShadowProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextShadowProperty.cs @@ -2,7 +2,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class TextShadowProperty : Property { - private static readonly IValueConverter StyleConverter = Converters.MultipleShadowConverter.OrDefault(); + private static readonly IValueConverter StyleConverter = Converters.TextShadowConverter.OrDefault(); internal TextShadowProperty() : base(PropertyNames.TextShadow, PropertyFlags.Inherited | PropertyFlags.Animatable) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextUnderlineOffsetProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextUnderlineOffsetProperty.cs new file mode 100644 index 000000000..27ab8822f --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextUnderlineOffsetProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextUnderlineOffsetProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.LengthOrPercentConverter.OrAuto().OrDefault(); + + internal TextUnderlineOffsetProperty() + : base(PropertyNames.TextUnderlineOffset, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextUnderlinePositionProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextUnderlinePositionProperty.cs new file mode 100644 index 000000000..7920eea11 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextUnderlinePositionProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextUnderlinePositionProperty : Property + { + private static readonly IValueConverter StyleConverter = + new TokenGrammarValueConverter(TextUnderlinePositionGrammar.TryParse).OrDefault(); + + internal TextUnderlinePositionProperty() + : base(PropertyNames.TextUnderlinePosition, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextWrapModeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextWrapModeProperty.cs new file mode 100644 index 000000000..29d053ff9 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextWrapModeProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextWrapModeProperty : Property + { + private static readonly IValueConverter StyleConverter = TextFlowConverters.TextWrapModeConverter; + + internal TextWrapModeProperty() + : base(PropertyNames.TextWrapMode, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextWrapProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextWrapProperty.cs new file mode 100644 index 000000000..e53b84fc2 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextWrapProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextWrapProperty : ShorthandProperty + { + private static readonly IValueConverter StyleConverter = TextFlowConverters.TextWrapConverter; + + internal TextWrapProperty() + : base(PropertyNames.TextWrap, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextWrapStyleProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextWrapStyleProperty.cs new file mode 100644 index 000000000..aac0cdb89 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/TextWrapStyleProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TextWrapStyleProperty : Property + { + private static readonly IValueConverter StyleConverter = TextFlowConverters.TextWrapStyleConverter; + + internal TextWrapStyleProperty() + : base(PropertyNames.TextWrapStyle, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WhiteSpaceCollapseProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WhiteSpaceCollapseProperty.cs new file mode 100644 index 000000000..c5bb1137c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WhiteSpaceCollapseProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class WhiteSpaceCollapseProperty : Property + { + private static readonly IValueConverter StyleConverter = TextFlowConverters.WhiteSpaceCollapseConverter; + + internal WhiteSpaceCollapseProperty() + : base(PropertyNames.WhiteSpaceCollapse, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WhiteSpaceProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WhiteSpaceProperty.cs index 6ec2a5235..69d1f7f48 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WhiteSpaceProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WhiteSpaceProperty.cs @@ -1,9 +1,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - internal sealed class WhiteSpaceProperty : Property + internal sealed class WhiteSpaceProperty : ShorthandProperty { - private static readonly IValueConverter StyleConverter = - Converters.WhitespaceConverter.OrDefault(Whitespace.Normal); + private static readonly IValueConverter StyleConverter = TextFlowConverters.WhiteSpaceConverter; internal WhiteSpaceProperty() : base(PropertyNames.WhiteSpace, PropertyFlags.Inherited) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WordBreakProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WordBreakProperty.cs index 8e1e39acb..3845e8761 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WordBreakProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WordBreakProperty.cs @@ -5,7 +5,7 @@ internal sealed class WordBreakProperty : Property private static readonly IValueConverter StyleConverter = Converters.WordBreakConverter; public WordBreakProperty() - : base(PropertyNames.WordBreak) + : base(PropertyNames.WordBreak, PropertyFlags.Inherited) { } diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WritingModeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WritingModeProperty.cs new file mode 100644 index 000000000..2a4ed9b87 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Text/WritingModeProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class WritingModeProperty : Property + { + private static readonly IValueConverter StyleConverter = TextFlowConverters.WritingModeConverter; + + internal WritingModeProperty() + : base(PropertyNames.WritingMode, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/PerspectiveProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/PerspectiveProperty.cs index 1e46c67a5..0f10220a0 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/PerspectiveProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/PerspectiveProperty.cs @@ -3,7 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class PerspectiveProperty : Property { private static readonly IValueConverter StyleConverter = - Converters.LengthConverter.OrNone().OrDefault(Length.Zero); + Converters.LengthConverter.OrNone().OrDefault(Keywords.None); internal PerspectiveProperty() : base(PropertyNames.Perspective, PropertyFlags.Animatable) diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/RotateProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/RotateProperty.cs new file mode 100644 index 000000000..35d160595 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/RotateProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class RotateProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.RotateIndividualConverter.OrNone(); + + internal RotateProperty() + : base(PropertyNames.Rotate, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/ScaleProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/ScaleProperty.cs new file mode 100644 index 000000000..7c15b462e --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/ScaleProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ScaleProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.ScaleIndividualConverter.OrNone(); + + internal ScaleProperty() + : base(PropertyNames.Scale, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/TransformOriginProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/TransformOriginProperty.cs index 3c36ceacf..244186094 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/TransformOriginProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/TransformOriginProperty.cs @@ -4,7 +4,14 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class TransformOriginProperty : Property { - // CSS Transforms 1 §5. A length forces horizontal-then-vertical order, only keyword pairs may be swapped. + // CSS Transforms 1 (https://www.w3.org/TR/css-transforms-1/#transform-origin-property): + // [ left | center | right | top | bottom | ] + // | [ left | center | right | ] [ top | center | bottom | ] ? + // | [ [ center | left | right ] && [ center | top | bottom ] ] ? + // + // The two-value form is ORDERED (horizontal then vertical). Only the keyword-only + // form may be reordered ("center left" == "left center"). So "2px left" and "top 2px" are invalid: a + // length in one axis forces the ordered form, and the remaining token is a keyword for the wrong axis. private static readonly IValueConverter Horizontal = LengthOrPercentConverter .Or(Keywords.Left, Length.Zero) @@ -27,19 +34,28 @@ internal sealed class TransformOriginProperty : Property .Or(Keywords.Bottom, Length.Full) .Or(Keywords.Center, Length.Half); + // A one- or two-value with no z-offset (this is exactly the perspective-origin grammar). private static readonly IValueConverter Position = LengthOrPercentConverter.Or(Keywords.Center, Point.Center) + // Ordered horizontal-then-vertical (rejects a keyword in the wrong axis position). .Or(WithOrder(Horizontal.Option(Length.Half), Vertical.Option(Length.Half))) - // "center" fits both axes, so it must not be claimed positionally + // Keyword-only form, any order ("center left"): a token accepted by both axes must not be + // claimed positionally, so this needs order-independent matching. .Or(WithAnyOrderIndependent(HorizontalKeyword.Option(Length.Half), VerticalKeyword.Option(Length.Half))); - // Both axes required, the z-offset only follows a two-value position ("top 2px" is invalid) + // A genuine two-value position: BOTH a horizontal and a vertical component must be present. The axis + // converters are bare (not .Option), so each must actually consume a token. The z-offset only + // exists in this two-value production, so gating the z-offset behind this (rather than the shared outer + // WithOrder) is what makes a single-value position + length such as "top 2px" invalid. private static readonly IValueConverter TwoValuePosition = WithOrder(Horizontal, Vertical) .Or(WithAnyOrderIndependent(HorizontalKeyword, VerticalKeyword)); private static readonly IValueConverter StyleConverter = + // Two-value position followed by a (required) z-offset - this branch only matches when a + // third length token is actually present... WithOrder(TwoValuePosition, LengthConverter) + // ...otherwise a plain one- or two-value with no z-offset. .Or(Position) .OrDefault(Point.Center); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/TranslateProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/TranslateProperty.cs new file mode 100644 index 000000000..0b5c97ee1 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/TranslateProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TranslateProperty : Property + { + private static readonly IValueConverter StyleConverter = + Converters.TranslateIndividualConverter.OrNone(); + + internal TranslateProperty() + : base(PropertyNames.Translate, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transition/TransitionBehaviorProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transition/TransitionBehaviorProperty.cs new file mode 100644 index 000000000..8041b5b20 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transition/TransitionBehaviorProperty.cs @@ -0,0 +1,14 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class TransitionBehaviorProperty : Property + { + private static readonly IValueConverter ListConverter = TimelineConverters.TransitionBehavior.FromList().OrDefault(); + + internal TransitionBehaviorProperty() + : base(PropertyNames.TransitionBehavior) + { + } + + internal override IValueConverter Converter => ListConverter; + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transition/TransitionProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transition/TransitionProperty.cs index b9a1e5041..6be309874 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transition/TransitionProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transition/TransitionProperty.cs @@ -4,28 +4,26 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class TransitionProperty : ShorthandProperty { - // css-transitions-1 §2.5: none is only valid when the list has a single item + // CSS Transitions 1 §2.5, none is only valid as a single transition internal static readonly IValueConverter ListConverter = SingleTransition(AnimatableConverter).FromList() .Or(SingleTransition(AnimatableConverter.OrNone())) .OrDefault(); + private static IValueConverter SingleTransition(IValueConverter propertyConverter) + { + return WithAny( + propertyConverter.Option().For(PropertyNames.TransitionProperty), + TimeConverter.Option().For(PropertyNames.TransitionDuration), + TransitionConverter.Option().For(PropertyNames.TransitionTimingFunction), + TimeConverter.Option().For(PropertyNames.TransitionDelay), + TimelineConverters.TransitionBehavior.Option().For(PropertyNames.TransitionBehavior)); + } + internal TransitionProperty() : base(PropertyNames.Transition) { } internal override IValueConverter Converter => ListConverter; - - private static IValueConverter SingleTransition(IValueConverter propertyConverter) - { - return new TimeBasedShorthandConverter( - 1, // duration index - 3, // delay index - propertyConverter.Option().For(PropertyNames.TransitionProperty), // 0 - TimeConverter.Option().For(PropertyNames.TransitionDuration), // 1 - TransitionConverter.Option().For(PropertyNames.TransitionTimingFunction), // 2 - TimeConverter.Option().For(PropertyNames.TransitionDelay) // 3 - ); - } } -} +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/BufferedRenderingProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/BufferedRenderingProperty.cs new file mode 100644 index 000000000..7f141fc9c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/BufferedRenderingProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class BufferedRenderingProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("auto", "dynamic", "static").OrDefault(); + + internal BufferedRenderingProperty() + : base(PropertyNames.BufferedRendering) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ClipPathProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ClipPathProperty.cs index ea3021c45..14ac4ecbc 100644 --- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ClipPathProperty.cs +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ClipPathProperty.cs @@ -1,8 +1,5 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// - /// clip-path, CSS Masking 1 §5.1. Only none and a <basic-shape> without a <geometry-box>. - /// internal sealed class ClipPathProperty : Property { private static readonly IValueConverter StyleConverter = new ClipPathValueConverter().OrDefault(); diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ForcedColorAdjustProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ForcedColorAdjustProperty.cs new file mode 100644 index 000000000..72fd5b4cc --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ForcedColorAdjustProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ForcedColorAdjustProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("auto", "none", "preserve-parent-color").OrDefault(); + + internal ForcedColorAdjustProperty() + : base(PropertyNames.ForcedColorAdjust, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ImageRenderingProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ImageRenderingProperty.cs new file mode 100644 index 000000000..6526b2b8f --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ImageRenderingProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ImageRenderingProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("auto", "smooth", "high-quality", "crisp-edges", "pixelated").OrDefault(); + + internal ImageRenderingProperty() + : base(PropertyNames.ImageRendering, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/IsolationProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/IsolationProperty.cs new file mode 100644 index 000000000..74b53ae4f --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/IsolationProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class IsolationProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("auto", "isolate").OrDefault(); + + internal IsolationProperty() + : base(PropertyNames.Isolation) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/MixBlendModeProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/MixBlendModeProperty.cs new file mode 100644 index 000000000..b6663b291 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/MixBlendModeProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class MixBlendModeProperty : Property + { + private static readonly IValueConverter StyleConverter = + Map.BlendModes.ToConverter().OrDefault(BlendMode.Normal); + + internal MixBlendModeProperty() + : base(PropertyNames.MixBlendMode, PropertyFlags.Animatable) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/PrintColorAdjustProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/PrintColorAdjustProperty.cs new file mode 100644 index 000000000..686202f32 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/PrintColorAdjustProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class PrintColorAdjustProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("economy", "exact").OrDefault(); + + internal PrintColorAdjustProperty() + : base(PropertyNames.PrintColorAdjust, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ShapeRenderingProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ShapeRenderingProperty.cs new file mode 100644 index 000000000..805078f75 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ShapeRenderingProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class ShapeRenderingProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("auto", "optimizeSpeed", "crispEdges", "geometricPrecision").OrDefault(); + + internal ShapeRenderingProperty() + : base(PropertyNames.ShapeRendering, PropertyFlags.Inherited) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/VectorEffectProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/VectorEffectProperty.cs new file mode 100644 index 000000000..c21bdebe5 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/VectorEffectProperty.cs @@ -0,0 +1,15 @@ +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + internal sealed class VectorEffectProperty : Property + { + private static readonly IValueConverter StyleConverter = + new KeywordSetValueConverter("none", "non-scaling-stroke", "non-scaling-size", "non-rotation", "fixed-position").OrDefault(); + + internal VectorEffectProperty() + : base(PropertyNames.VectorEffect) + { + } + + internal override IValueConverter Converter => StyleConverter; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/TextEncoding.cs b/Source/HtmlRenderer/Core/CssEngine/TextEncoding.cs index 17ebb909a..0873a5978 100644 --- a/Source/HtmlRenderer/Core/CssEngine/TextEncoding.cs +++ b/Source/HtmlRenderer/Core/CssEngine/TextEncoding.cs @@ -9,8 +9,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal static class TextEncoding { - public static HashSet AvailableEncodings = new HashSet(from encoding in Encoding.GetEncodings() - select encoding.Name); + public static HashSet AvailableEncodings = new(from encoding in Encoding.GetEncodings() + select encoding.Name); public static readonly Encoding Utf8 = new UTF8Encoding(false); public static readonly Encoding Utf16Be = new UnicodeEncoding(true, false); @@ -65,6 +65,8 @@ private static Encoding GetEncoding(string name) { } + + // Catch all since WP8 does throw a different exception than W*. return Utf8; } diff --git a/Source/HtmlRenderer/Core/CssEngine/Tokens/CommentToken.cs b/Source/HtmlRenderer/Core/CssEngine/Tokens/CommentToken.cs index 163b6adb6..ef711a416 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Tokens/CommentToken.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Tokens/CommentToken.cs @@ -12,7 +12,7 @@ public CommentToken(string data, bool valid, TextPosition position) public override string ToValue() { - var trailing = IsValid ? string.Empty : "*/"; + var trailing = IsValid ? "*/" : string.Empty; return string.Concat("/*", Data, trailing); } } diff --git a/Source/HtmlRenderer/Core/CssEngine/Tokens/NumberToken.cs b/Source/HtmlRenderer/Core/CssEngine/Tokens/NumberToken.cs index 103917030..5bfeb7847 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Tokens/NumberToken.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Tokens/NumberToken.cs @@ -5,7 +5,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class NumberToken : Token { - private static readonly char[] FloatIndicators = { '.', 'e', 'E' }; + private static readonly char[] FloatIndicators = {'.', 'e', 'E'}; public NumberToken(string number, TextPosition position) : base(TokenType.Number, number, position) diff --git a/Source/HtmlRenderer/Core/CssEngine/Tokens/RangeToken.cs b/Source/HtmlRenderer/Core/CssEngine/Tokens/RangeToken.cs index 66f0cd0fa..a91191ef5 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Tokens/RangeToken.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Tokens/RangeToken.cs @@ -1,7 +1,35 @@ +using System.Collections.Generic; +using System.Globalization; + namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class RangeToken : Token { + private string[] _selectedRange; + private bool _selectedRangeComputed; + + private string[] GetRange() + { + var index = int.Parse(Start, NumberStyles.HexNumber); + + if (index > Symbols.MaximumCodepoint) return null; + + if (End == null) return new[] {index.ConvertFromUtf32()}; + + var list = new List(); + var f = int.Parse(End, NumberStyles.HexNumber); + + if (f > Symbols.MaximumCodepoint) f = Symbols.MaximumCodepoint; + + while (index <= f) + { + list.Add(index.ConvertFromUtf32()); + index++; + } + + return list.ToArray(); + } + public RangeToken(string range, TextPosition position) : base(TokenType.Range, range, position) { @@ -16,13 +44,31 @@ public RangeToken(string start, string end, TextPosition position) End = end; } - // Data is the range without "U+". + // Data holds the range without its "U+" prefix, so serializing it bare would emit "41-5A" - not a + // valid and not what was authored. public override string ToValue() { return string.Concat("U+", Data); } + //public bool IsEmpty => (SelectedRange == null) || (SelectedRange.Length == 0); public string Start { get; } public string End { get; } + + // Computed on demand: a legal range such as U+0-10FFFF expands to over a million strings, which is + // far too much to materialize eagerly in a token constructor for a value nothing may ever read. + public string[] SelectedRange + { + get + { + if (!_selectedRangeComputed) + { + _selectedRange = GetRange(); + _selectedRangeComputed = true; + } + + return _selectedRange; + } + } } -} +} \ No newline at end of file diff --git a/Source/HtmlRenderer/Core/CssEngine/Tokens/Token.cs b/Source/HtmlRenderer/Core/CssEngine/Tokens/Token.cs index 65a850e6d..798bc31f8 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Tokens/Token.cs +++ b/Source/HtmlRenderer/Core/CssEngine/Tokens/Token.cs @@ -2,8 +2,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal class Token { - public static readonly Token Whitespace = new Token(TokenType.Whitespace, " ", TextPosition.Empty); - public static readonly Token Comma = new Token(TokenType.Comma, ",", TextPosition.Empty); + public static readonly Token Whitespace = new(TokenType.Whitespace, " ", TextPosition.Empty); + public static readonly Token Comma = new(TokenType.Comma, ",", TextPosition.Empty); public Token(TokenType type, string data, TextPosition position) { diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/AspectRatioValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/AspectRatioValueConverter.cs index 0de3402d5..8c346f189 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/AspectRatioValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/AspectRatioValueConverter.cs @@ -4,14 +4,35 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// Validates an aspect-ratio value (CSS Sizing 4 §4.1) and keeps the authored text. + /// Validates the aspect-ratio value [ auto || <ratio> ] (CSS Sizing 4 4.1), where + /// <ratio> = <number [0,∞]> [ / <number [0,∞]> ]?. The || permits + /// auto and the ratio in either order, and either alone. The authored text is preserved. /// internal sealed class AspectRatioValueConverter : IValueConverter { public IPropertyValue Convert(IEnumerable value) { - var tokens = value.ToArray(); - return AspectRatioGrammar.TryParse(tokens, out _) ? new AspectRatioValue(tokens) : null; + var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (tokens.Length == 0) return null; + + var index = 0; + var hasAuto = false; + + if (IsAuto(tokens[index])) + { + hasAuto = true; + index++; + } + + if (index == tokens.Length) return hasAuto ? new AspectRatioValue(value) : null; + + if (!TryConsumeRatio(tokens, ref index)) return null; + + // "||" allows a trailing auto too (e.g. "16 / 9 auto"), but only if one wasn't already given. + if (index < tokens.Length && !hasAuto && IsAuto(tokens[index])) index++; + + return index == tokens.Length ? new AspectRatioValue(value) : null; } public IPropertyValue Construct(Property[] properties) @@ -19,6 +40,30 @@ public IPropertyValue Construct(Property[] properties) return properties.Guard(); } + private static bool TryConsumeRatio(IReadOnlyList tokens, ref int index) + { + if (index >= tokens.Count || !IsNonNegativeNumber(tokens[index])) return false; + index++; + + if (index < tokens.Count && IsSlash(tokens[index])) + { + index++; + if (index >= tokens.Count || !IsNonNegativeNumber(tokens[index])) return false; + index++; + } + + return true; + } + + private static bool IsAuto(Token token) => + token.Type == TokenType.Ident && token.Data.Isi(Keywords.Auto); + + private static bool IsSlash(Token token) => + token.Type == TokenType.Delim && token.Data == "/"; + + private static bool IsNonNegativeNumber(Token token) => + token is NumberToken number && number.Value >= 0f; + private sealed class AspectRatioValue : IPropertyValue { public AspectRatioValue(IEnumerable tokens) diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BackgroundPositionValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BackgroundPositionValueConverter.cs deleted file mode 100644 index 5e2e9ef85..000000000 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BackgroundPositionValueConverter.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace TheArtOfDev.HtmlRenderer.Core.CssEngine -{ - /// - /// Validates and serializes a single-layer background-position value through - /// . - /// - internal sealed class BackgroundPositionValueConverter : IValueConverter - { - public IPropertyValue Convert(IEnumerable value) - { - var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); - var parsed = BackgroundPositionGrammar.TryParse(tokens); - return parsed != null ? new BackgroundPositionValue(parsed, value) : null; - } - - public IPropertyValue Construct(Property[] properties) - { - return properties.Guard(); - } - - private sealed class BackgroundPositionValue : IPropertyValue - { - private readonly BackgroundPositionGrammar.ParsedPosition _parsed; - - public BackgroundPositionValue(BackgroundPositionGrammar.ParsedPosition parsed, IEnumerable tokens) - { - _parsed = parsed; - Original = new TokenValue(tokens); - } - - public string CssText => string.Join(" ", _parsed.AuthoredOrder.Select(ComponentText)); - - public TokenValue Original { get; } - - public TokenValue ExtractFor(string name) => Original; - - private static string ComponentText(BackgroundPositionGrammar.Component c) - { - if (c.Keyword == BackgroundPositionGrammar.AxisKeyword.None) - return LengthText(c.Offset); - - var keyword = KeywordText(c.Keyword); - return c.Offset != null ? keyword + " " + LengthText(c.Offset) : keyword; - } - - private static string KeywordText(BackgroundPositionGrammar.AxisKeyword keyword) - { - switch (keyword) - { - case BackgroundPositionGrammar.AxisKeyword.Left: - return Keywords.Left; - case BackgroundPositionGrammar.AxisKeyword.Right: - return Keywords.Right; - case BackgroundPositionGrammar.AxisKeyword.Top: - return Keywords.Top; - case BackgroundPositionGrammar.AxisKeyword.Bottom: - return Keywords.Bottom; - case BackgroundPositionGrammar.AxisKeyword.Center: - return Keywords.Center; - default: - return string.Empty; - } - } - - private static string LengthText(Token token) => - Converters.LengthOrPercentConverter.Convert(new[] { token })?.CssText ?? string.Empty; - } - } -} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BackgroundSizeValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BackgroundSizeValueConverter.cs deleted file mode 100644 index 491c832f4..000000000 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BackgroundSizeValueConverter.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace TheArtOfDev.HtmlRenderer.Core.CssEngine -{ - /// - /// Validates and serializes a single-layer background-size value via the shared - /// . - /// - internal sealed class BackgroundSizeValueConverter : IValueConverter - { - public IPropertyValue Convert(IEnumerable value) - { - var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); - var parsed = BackgroundSizeGrammar.TryParse(tokens); - return parsed != null ? new BackgroundSizeValue(parsed, value) : null; - } - - public IPropertyValue Construct(Property[] properties) - { - return properties.Guard(); - } - - private sealed class BackgroundSizeValue : IPropertyValue - { - private readonly BackgroundSizeGrammar.ParsedSize _parsed; - - public BackgroundSizeValue(BackgroundSizeGrammar.ParsedSize parsed, IEnumerable tokens) - { - _parsed = parsed; - Original = new TokenValue(tokens); - } - - public string CssText - { - get - { - if (_parsed.IsCover) return Keywords.Cover; - if (_parsed.IsContain) return Keywords.Contain; - - var width = ComponentText(_parsed.Width); - return _parsed.HasExplicitHeight ? width + " " + ComponentText(_parsed.Height) : width; - } - } - - public TokenValue Original { get; } - - public TokenValue ExtractFor(string name) => Original; - - private static string ComponentText(BackgroundSizeGrammar.Component c) => - c.IsAuto ? Keywords.Auto : Converters.LengthOrPercentConverter.Convert(new[] { c.Value })?.CssText ?? string.Empty; - } - } -} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BorderRadiusConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BorderRadiusConverter.cs index e71fa933d..77cf11469 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BorderRadiusConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BorderRadiusConverter.cs @@ -7,10 +7,22 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine internal sealed class BorderRadiusConverter : IValueConverter { - private readonly IValueConverter _converter = LengthOrPercentConverter.Periodic( - PropertyNames.BorderTopLeftRadius, PropertyNames.BorderTopRightRadius, - PropertyNames.BorderBottomRightRadius, - PropertyNames.BorderBottomLeftRadius); + private readonly IValueConverter _converter; + private readonly string[] _labels; + + // The border-radius shorthand: four corners, top-left first. + public BorderRadiusConverter() + : this(PropertyNames.BorderTopLeftRadius, PropertyNames.BorderTopRightRadius, + PropertyNames.BorderBottomRightRadius, PropertyNames.BorderBottomLeftRadius) + { + } + + // A radius shorthand over the given two or four longhands (border-top-radius, corner, ...). + public BorderRadiusConverter(params string[] labels) + { + _labels = labels; + _converter = LengthOrPercentConverter.Periodic(labels); + } public IPropertyValue Convert(IEnumerable value) { @@ -43,19 +55,13 @@ public IPropertyValue Convert(IEnumerable value) public IPropertyValue Construct(Property[] properties) { - if (properties.Length == 4) + if (properties.Length == _labels.Length) { var front = new List(); var back = new List(); - var props = new List - { - properties.First(m => m.Name.Is(PropertyNames.BorderTopLeftRadius)), - properties.First(m => m.Name.Is(PropertyNames.BorderTopRightRadius)), - properties.First( - m => m.Name.Is(PropertyNames.BorderBottomRightRadius)), - properties.First( - m => m.Name.Is(PropertyNames.BorderBottomLeftRadius)) - }; + var props = new List(); + + foreach (var label in _labels) props.Add(properties.First(m => m.Name.Is(label))); for (var i = 0; i < props.Count; i++) { diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BoxShadowValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BoxShadowValueConverter.cs deleted file mode 100644 index 049acc252..000000000 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BoxShadowValueConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace TheArtOfDev.HtmlRenderer.Core.CssEngine -{ - /// - /// Validates a box-shadow value with and keeps the authored text. - /// - internal sealed class BoxShadowValueConverter : IValueConverter - { - public IPropertyValue Convert(IEnumerable value) - { - var tokens = value.ToArray(); - return BoxShadowGrammar.TryParse(tokens) is null ? null : new BoxShadowValue(tokens); - } - - public IPropertyValue Construct(Property[] properties) - { - return properties.Guard(); - } - - private sealed class BoxShadowValue : IPropertyValue - { - public BoxShadowValue(IEnumerable tokens) - { - Original = new TokenValue(tokens); - } - - public string CssText => Original.Text; - - public TokenValue Original { get; } - - public TokenValue ExtractFor(string name) => Original; - } - } -} diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/CalcValueConverter.cs similarity index 61% rename from Source/HtmlRenderer/Core/CssEngine/Calc/CalcValueConverter.cs rename to Source/HtmlRenderer/Core/CssEngine/ValueConverters/CalcValueConverter.cs index 6a2f775cd..32cfe243a 100644 --- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/CalcValueConverter.cs @@ -3,8 +3,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// Accepts a calc(), min(), max() or clamp() whose type is one of allowed. A value with var() - /// gets here only after substitution. + /// Accepts a calc()/min()/max()/clamp() expression whose type-checked category is within + /// allowed, as a (parsed tree plus canonical text). Composed onto the existing length/percent/number/ + /// angle converters via .Or(...) so support cascades to every property built on them. /// internal sealed class CalcValueConverter : IValueConverter { @@ -17,14 +18,14 @@ public CalcValueConverter(CalcCategory allowed) public IPropertyValue Convert(IEnumerable value) { - if (!(value.OnlyOrDefault() is FunctionToken function)) return null; + if (value.OnlyOrDefault() is not FunctionToken function) return null; if (!CalcParser.IsCalcFamily(function.Data)) return null; var node = CalcParser.Parse(function); - if (node is null) return null; + if (node == null) return null; var category = CalcTypeChecker.Check(node); - if (category is null || (category.Value & ~_allowed) != 0) return null; + if (category == null || (category.Value & ~_allowed) != 0) return null; return new CalcValue(node, category.Value, value); } diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ClipPathValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ClipPathValueConverter.cs index b4b509c94..d2e00dac5 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ClipPathValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ClipPathValueConverter.cs @@ -4,7 +4,11 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// Accepts none or a shape recognizes and keeps the authored tokens as the value. + /// Validates a clip-path value (CSS Masking 1 §5.1): the keyword none or a + /// <basic-shape> - polygon(), inset(), circle() or ellipse() + /// (CSS Shapes 1 §3, plus rect()/xywh() from CSS Shapes 2 and path()), + /// optionally combined with a <geometry-box> in either order, or a lone + /// <geometry-box>. The authored text is preserved. The <url> form is out of scope here. /// internal sealed class ClipPathValueConverter : IValueConverter { @@ -12,12 +16,10 @@ public IPropertyValue Convert(IEnumerable value) { var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); - var isNone = tokens.Length == 1 && tokens[0].Type == TokenType.Ident && tokens[0].Data.Isi(Keywords.None); + if (tokens.Length == 1 && tokens[0].Type == TokenType.Ident && tokens[0].Data.Isi(Keywords.None)) + return new ClipPathValue(value); - if (!isNone && BasicShapeGrammar.TryParse(tokens) == null) - return null; - - return new ClipPathValue(value); + return IsShapeAndBox(tokens) ? new ClipPathValue(value) : null; } public IPropertyValue Construct(Property[] properties) @@ -25,6 +27,357 @@ public IPropertyValue Construct(Property[] properties) return properties.Guard(); } + private static readonly string[] GeometryBoxes = + { + Keywords.BorderBox, Keywords.PaddingBox, Keywords.ContentBox, + "margin-box", "fill-box", "stroke-box", "view-box" + }; + + // || (each at most once, either order) + private static bool IsShapeAndBox(IReadOnlyList tokens) + { + if (tokens.Count is < 1 or > 2) return false; + + var shapes = 0; + var boxes = 0; + + foreach (var token in tokens) + { + if (IsGeometryBox(token)) boxes++; + else if (IsBasicShape(token)) shapes++; + else return false; + } + + return shapes <= 1 && boxes <= 1; + } + + private static bool IsGeometryBox(Token token) => + token.Type == TokenType.Ident && GeometryBoxes.Any(b => token.Data.Isi(b)); + + // Used by shape-outside as well: a single-token . + internal static bool IsBasicShape(IReadOnlyList tokens) => + tokens.Count == 1 && IsBasicShape(tokens[0]); + + internal static bool IsBasicShape(Token token) + { + if (token is not FunctionToken function) return false; + + var args = function.ArgumentTokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (function.Data.Isi(FunctionNames.Polygon)) return IsPolygon(args); + if (function.Data.Isi(FunctionNames.Inset)) return IsInset(args); + if (function.Data.Isi(FunctionNames.Circle)) return IsCircle(args); + if (function.Data.Isi(FunctionNames.Ellipse)) return IsEllipse(args); + if (function.Data.Isi(FunctionNames.Rect)) return IsRect(args); + if (function.Data.Isi("xywh")) return IsXywh(args); + if (function.Data.Isi("path")) return IsPath(args); + + return false; + } + + // polygon( ? , [ ]# ) + private static bool IsPolygon(IReadOnlyList args) + { + var groups = SplitByComma(args); + if (groups == null || groups.Count == 0) return false; + + var first = 0; + + if (groups[0].Count == 1 && IsFillRule(groups[0][0])) + first = 1; + + if (first >= groups.Count) return false; + + for (var i = first; i < groups.Count; i++) + { + var group = groups[i]; + if (group.Count != 2 || !IsLengthPercentage(group[0]) || !IsLengthPercentage(group[1])) + return false; + } + + return true; + } + + // inset( {1,4} [ round ]? ) + private static bool IsInset(IReadOnlyList args) => IsBoxShape(args, false, false, false); + + // rect( [ | auto ]{4} [ round ]? ) + private static bool IsRect(IReadOnlyList args) => IsBoxShape(args, true, true, false); + + // xywh( {4} [ round ]? ); width and height must be non-negative + private static bool IsXywh(IReadOnlyList args) => IsBoxShape(args, true, false, true); + + private static bool IsBoxShape(IReadOnlyList args, bool exactlyFour, bool allowAuto, + bool nonNegativeSize) + { + if (args.Count == 0) return false; + + var roundIndex = -1; + for (var i = 0; i < args.Count; i++) + { + if (args[i].Type == TokenType.Ident && args[i].Data.Isi(Keywords.Round)) + { + roundIndex = i; + break; + } + } + + var lengths = roundIndex >= 0 ? args.Take(roundIndex).ToArray() : args.ToArray(); + + if (exactlyFour ? lengths.Length != 4 : lengths.Length is < 1 or > 4) return false; + if (lengths.Any(t => !(IsLengthPercentage(t) || allowAuto && IsAuto(t)))) return false; + if (nonNegativeSize && (IsNegative(lengths[2]) || IsNegative(lengths[3]))) return false; + + // "round" must be followed by a valid . + return roundIndex < 0 || IsBorderRadius(args.Skip(roundIndex + 1).ToArray()); + } + + // {1,4} [ / {1,4} ]? + private static bool IsBorderRadius(IReadOnlyList tokens) + { + var slash = -1; + for (var i = 0; i < tokens.Count; i++) + { + if (tokens[i].Type != TokenType.Delim || tokens[i].Data != "/") continue; + if (slash >= 0) return false; + slash = i; + } + + var horizontal = slash >= 0 ? tokens.Take(slash).ToArray() : tokens.ToArray(); + var vertical = slash >= 0 ? tokens.Skip(slash + 1).ToArray() : new Token[0]; + + return IsRadiusList(horizontal) && (slash < 0 || IsRadiusList(vertical)); + } + + private static bool IsRadiusList(IReadOnlyList tokens) => + tokens.Count is >= 1 and <= 4 && tokens.All(t => IsLengthPercentage(t) && !IsNegative(t)); + + private static bool IsAuto(Token token) => token.Type == TokenType.Ident && token.Data.Isi("auto"); + + private static bool IsNegative(Token token) => token is UnitToken { Value: < 0f } or NumberToken { Value: < 0f }; + + // path( ? , ) where the string is well-formed, non-empty SVG path data. + private static bool IsPath(IReadOnlyList args) + { + var groups = SplitByComma(args); + if (groups == null || groups.Count is not (1 or 2)) return false; + + if (groups.Count == 2 && !(groups[0].Count == 1 && IsFillRule(groups[0][0]))) return false; + + var str = groups[groups.Count - 1]; + return str.Count == 1 && str[0].Type == TokenType.String && IsPathData(str[0].Data); + } + + // Validates the SVG path-data mini-language (SVG 1.1 8.3.9): starts with a moveto, then commands with + // the right number of numeric arguments (implicitly repeatable); arc flags are single '0'/'1' digits. + internal static bool IsPathData(string data) + { + var pos = 0; + var first = true; + var command = '\0'; + + SkipSeparators(data, ref pos); + if (pos >= data.Length) return false; + + while (pos < data.Length) + { + var c = data[pos]; + + if (char.IsLetter(c)) + { + if ("MmLlHhVvCcSsQqTtAaZz".IndexOf(c) < 0) return false; + if (first && c != 'M' && c != 'm') return false; + + command = c; + first = false; + pos++; + SkipSeparators(data, ref pos); + + if (command is 'Z' or 'z') continue; + } + else if (command == '\0' || command is 'Z' or 'z') + { + return false; + } + + var count = ArgumentCount(command); + + for (var i = 0; i < count; i++) + { + var isFlag = (command is 'A' or 'a') && i is 3 or 4; + + if (isFlag ? !ReadFlag(data, ref pos) : !ReadNumber(data, ref pos)) return false; + + SkipSeparators(data, ref pos); + } + + // A moveto's implicit repeats are linetos. + if (command == 'M') command = 'L'; + else if (command == 'm') command = 'l'; + } + + return true; + } + + private static int ArgumentCount(char command) + { + switch (char.ToUpperInvariant(command)) + { + case 'M': case 'L': case 'T': return 2; + case 'H': case 'V': return 1; + case 'C': return 6; + case 'S': case 'Q': return 4; + case 'A': return 7; + default: return 0; + } + } + + private static void SkipSeparators(string s, ref int pos) + { + while (pos < s.Length && (char.IsWhiteSpace(s[pos]) || s[pos] == ',')) pos++; + } + + private static bool ReadFlag(string s, ref int pos) + { + if (pos >= s.Length || s[pos] != '0' && s[pos] != '1') return false; + pos++; + return true; + } + + // [+-]? (digits [. digits?]? | . digits) ([eE] [+-]? digits)? + private static bool ReadNumber(string s, ref int pos) + { + var i = pos; + if (i < s.Length && (s[i] == '+' || s[i] == '-')) i++; + + var digits = 0; + while (i < s.Length && char.IsDigit(s[i])) { i++; digits++; } + + if (i < s.Length && s[i] == '.') + { + i++; + while (i < s.Length && char.IsDigit(s[i])) { i++; digits++; } + } + + if (digits == 0) return false; + + if (i < s.Length && (s[i] == 'e' || s[i] == 'E')) + { + var j = i + 1; + if (j < s.Length && (s[j] == '+' || s[j] == '-')) j++; + var expDigits = 0; + while (j < s.Length && char.IsDigit(s[j])) { j++; expDigits++; } + if (expDigits > 0) i = j; + } + + pos = i; + return true; + } + + // circle( ? [ at ]? ) + private static bool IsCircle(IReadOnlyList args) + { + if (!SplitAt(args, out var radius, out var position)) return false; + return radius.Count switch + { + 0 => true, + 1 => IsShapeRadius(radius[0]), + _ => false + } && IsPosition(position); + } + + // ellipse( [ {2} ]? [ at ]? ) + private static bool IsEllipse(IReadOnlyList args) + { + if (!SplitAt(args, out var radius, out var position)) return false; + return radius.Count switch + { + 0 => true, + 2 => IsShapeRadius(radius[0]) && IsShapeRadius(radius[1]), + _ => false + } && IsPosition(position); + } + + // Splits the arguments at a standalone "at" ident. Returns false when "at" has nothing after it. + private static bool SplitAt(IReadOnlyList args, out IReadOnlyList before, + out IReadOnlyList after) + { + for (var i = 0; i < args.Count; i++) + { + if (args[i].Type == TokenType.Ident && args[i].Data.Isi(Keywords.At)) + { + before = args.Take(i).ToArray(); + after = args.Skip(i + 1).ToArray(); + return after.Count > 0; + } + } + + before = args; + after = new Token[0]; + return true; + } + + // A permissive : nothing (absent), or 1-4 tokens each an edge/center keyword or a + // length-percentage. Absent is valid (the caller only reaches here with the post-"at" tokens). + private static bool IsPosition(IReadOnlyList tokens) + { + if (tokens.Count == 0) return true; + if (tokens.Count > 4) return false; + + return tokens.All(t => IsPositionKeyword(t) || IsLengthPercentage(t)); + } + + private static bool IsPositionKeyword(Token token) + { + if (token.Type != TokenType.Ident) return false; + return token.Data.Isi(Keywords.Left) || token.Data.Isi(Keywords.Right) || + token.Data.Isi(Keywords.Top) || token.Data.Isi(Keywords.Bottom) || + token.Data.Isi(Keywords.Center); + } + + private static bool IsShapeRadius(Token token) + { + if (token.Type == TokenType.Ident) + return token.Data.Isi(Keywords.ClosestSide) || token.Data.Isi(Keywords.FarthestSide); + + // A is a non-negative (CSS Shapes 1 §3.2). + if (token is UnitToken { Value: < 0f } or NumberToken { Value: < 0f }) return false; + + return IsLengthPercentage(token); + } + + private static bool IsFillRule(Token token) => + token.Type == TokenType.Ident && (token.Data.Isi(Keywords.Nonzero) || token.Data.Isi(Keywords.Evenodd)); + + // CSS Values 4 §10, a math function resolving to a is one wherever that type is + private static bool IsLengthPercentage(Token token) => + token.Type is TokenType.Dimension or TokenType.Percentage || token is NumberToken { Value: 0f } || + token is FunctionToken && Converters.LengthOrPercentConverter.Convert(new[] { token }) != null; + + private static List> SplitByComma(IReadOnlyList tokens) + { + var groups = new List>(); + var current = new List(); + + foreach (var token in tokens) + { + if (token.Type == TokenType.Comma) + { + groups.Add(current); + current = new List(); + } + else + { + current.Add(token); + } + } + + groups.Add(current); + + // A leading, trailing or doubled comma leaves an empty group - always malformed here. + return tokens.Count > 0 && groups.Any(g => g.Count == 0) ? null : groups; + } + private sealed class ClipPathValue : IPropertyValue { public ClipPathValue(IEnumerable tokens) diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorInterpolationMethodGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorInterpolationMethodGrammar.cs new file mode 100644 index 000000000..28034ae66 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorInterpolationMethodGrammar.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The grammar of a gradient's <color-interpolation-method> prelude (CSS Images 4 3.1): + /// in [ <rectangular-color-space> | <polar-color-space> <hue-interpolation-method>? ]. + /// The prelude combines with a gradient's own direction/shape/position using ||, so it may appear + /// before or after it. + /// + internal static class ColorInterpolationMethodGrammar + { + private const string In = "in"; + private const string Hue = "hue"; + + private static readonly HashSet ColorSpaces = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "srgb", "srgb-linear", "display-p3", "a98-rgb", "prophoto-rgb", "rec2020", "lab", "oklab", + "xyz", "xyz-d65", "xyz-d50", "hsl", "hwb", "lch", "oklch" + }; + + private static readonly HashSet PolarColorSpaces = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "hsl", "hwb", "lch", "oklch" + }; + + private static readonly HashSet HueDirections = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "shorter", "longer", "increasing", "decreasing" + }; + + public static bool IsColorSpace(string name) => name != null && ColorSpaces.Contains(name); + + public static bool IsPolarColorSpace(string name) => name != null && PolarColorSpaces.Contains(name); + + public static bool IsHueDirection(string name) => name != null && HueDirections.Contains(name); + + /// + /// If (one gradient comma-group's tokens) contains an in + /// interpolation-method prelude, validates it and returns the group with the contiguous + /// in ... slice removed as (for the caller to validate as the + /// gradient's own direction/shape/position). Returns false when an in is present but the + /// method is malformed (unknown space, a hue method on a rectangular space or without the trailing + /// hue, or in with nothing after it). When no in is present, + /// is false, is the group + /// unchanged, and it returns true. + /// + public static bool TryExtractInterpolationMethod( + IEnumerable group, out List remainder, out bool hasInterpolationMethod) + { + var tokens = new List(group); + + var inIndex = IndexOfIdent(tokens, In); + if (inIndex < 0) + { + remainder = tokens; + hasInterpolationMethod = false; + return true; + } + + hasInterpolationMethod = true; + + var spaceIndex = NextNonWhitespace(tokens, inIndex + 1); + if (spaceIndex < 0 || !IsIdent(tokens[spaceIndex], out var spaceName) || !IsColorSpace(spaceName)) + { + remainder = null; + return false; + } + + var lastMethodIndex = spaceIndex; + + // A hue-interpolation method ( hue) is only valid after a polar space, and is only + // complete with the trailing "hue" keyword. + if (IsPolarColorSpace(spaceName)) + { + var directionIndex = NextNonWhitespace(tokens, spaceIndex + 1); + if (directionIndex >= 0 && IsIdent(tokens[directionIndex], out var direction) && + IsHueDirection(direction)) + { + var hueIndex = NextNonWhitespace(tokens, directionIndex + 1); + if (hueIndex < 0 || !IsIdent(tokens[hueIndex], out var hueWord) || + !string.Equals(hueWord, Hue, StringComparison.OrdinalIgnoreCase)) + { + remainder = null; + return false; + } + + lastMethodIndex = hueIndex; + } + } + + remainder = BuildRemainder(tokens, inIndex, lastMethodIndex); + return true; + } + + // The group without tokens [first..last], whitespace-trimmed at the edges so what is left validates + // the same as an un-prefixed group would. + private static List BuildRemainder(List tokens, int first, int last) + { + var remainder = new List(tokens.Count); + + for (var i = 0; i < tokens.Count; i++) + { + if (i < first || i > last) remainder.Add(tokens[i]); + } + + while (remainder.Count > 0 && remainder[0].Type == TokenType.Whitespace) remainder.RemoveAt(0); + while (remainder.Count > 0 && remainder[remainder.Count - 1].Type == TokenType.Whitespace) + remainder.RemoveAt(remainder.Count - 1); + + return remainder; + } + + private static int IndexOfIdent(List tokens, string data) + { + for (var i = 0; i < tokens.Count; i++) + { + if (IsIdent(tokens[i], out var value) && string.Equals(value, data, StringComparison.OrdinalIgnoreCase)) + return i; + } + + return -1; + } + + private static int NextNonWhitespace(List tokens, int start) + { + for (var i = start; i < tokens.Count; i++) + { + if (tokens[i].Type != TokenType.Whitespace) return i; + } + + return -1; + } + + private static bool IsIdent(Token token, out string data) + { + if (token.Type == TokenType.Ident) + { + data = token.Data; + return true; + } + + data = null; + return false; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorPairValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorPairValueConverter.cs new file mode 100644 index 000000000..fdb0401a0 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorPairValueConverter.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The arguments of light-dark(<color>, <color>) (CSS Color 5 section 4): exactly two comma-separated colors. + /// A declaration whose arms are not both colors is invalid, as it is in a browser. A parser has no color scheme to pick an + /// arm with, so the authored text is preserved and both arms are exposed through + /// for the consumer that does. + /// + internal sealed class ColorPairValueConverter : IValueConverter + { + public IPropertyValue Convert(IEnumerable value) + { + var tokens = value as Token[] ?? value.ToArray(); + if (!ColorFunctionExtensions.TrySplitColorPair(tokens, out var light, out var dark)) return null; + + // An arm is any color, light-dark() itself included (the nested form recurses through ColorConverter), and currentcolor. + if (Converters.CurrentColorConverter.Convert(light) is null || + Converters.CurrentColorConverter.Convert(dark) is null) + return null; + + return new PairValue(tokens); + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + + private sealed class PairValue : IPropertyValue + { + public PairValue(IEnumerable tokens) + { + Original = new TokenValue(tokens); + } + + public string CssText => Original.Text; + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) => Original; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorSchemeGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorSchemeGrammar.cs new file mode 100644 index 000000000..4a2ed06ac --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorSchemeGrammar.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The color-scheme grammar (CSS Color Adjustment 1 2): + /// normal | [ light | dark | <custom-ident> ]+ && only?. + /// + internal static class ColorSchemeGrammar + { + /// Validates whitespace-free tokens against the grammar. + internal static bool TryParse(IReadOnlyList tokens) + { + if (tokens == null || tokens.Count == 0) return false; + + if (tokens.Count == 1 && tokens[0].Type == TokenType.Ident && tokens[0].Data.Isi(Keywords.Normal)) + return true; + + var only = false; + var schemes = 0; + + for (var i = 0; i < tokens.Count; i++) + { + var token = tokens[i]; + if (token.Type != TokenType.Ident) return false; + + var name = token.Data; + + if (name.Isi(Keywords.Only)) + { + // "only" is written once, at either end of the list. + if (only || (i != 0 && i != tokens.Count - 1)) return false; + only = true; + continue; + } + + if (IsReserved(name)) return false; + + schemes++; + } + + return schemes > 0; + } + + // "normal", the CSS-wide keywords and "default" are not s (CSS Values 4 4.2). + private static bool IsReserved(string name) => + name.Isi(Keywords.Normal) || name.Isi(Keywords.Initial) || name.Isi(Keywords.Inherit) || + name.Isi(Keywords.Unset) || name.Isi("revert") || name.Isi("revert-layer") || name.Isi("default"); + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorSchemeValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorSchemeValueConverter.cs new file mode 100644 index 000000000..878321912 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ColorSchemeValueConverter.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Validates color-scheme (normal | [ light | dark | <custom-ident> ]+ && only?) + /// through . The authored text is preserved. + /// + internal sealed class ColorSchemeValueConverter : IValueConverter + { + public IPropertyValue Convert(IEnumerable value) + { + var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + return ColorSchemeGrammar.TryParse(tokens) ? new ColorSchemeValue(value) : null; + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + + private sealed class ColorSchemeValue : IPropertyValue + { + public ColorSchemeValue(IEnumerable tokens) + { + Original = new TokenValue(tokens); + } + + public string CssText => Original.Text; + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) => Original; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ConditionalStartsWithValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ConditionalStartsWithValueConverter.cs index 1ac0aee28..dd1f7a9e7 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ConditionalStartsWithValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ConditionalStartsWithValueConverter.cs @@ -21,7 +21,6 @@ public IPropertyValue Convert(IEnumerable value) var enumerator = value.GetEnumerator(); bool hasToken; - while ((hasToken = enumerator.MoveNext()) && enumerator.Current.Type == TokenType.Whitespace) { //Empty on purpose. diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ContainmentValueConverters.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ContainmentValueConverters.cs new file mode 100644 index 000000000..beffe0877 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ContainmentValueConverters.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// contain-intrinsic-size (CSS Sizing 4 §5.1): one or two auto? [ none | <length> ] + /// groups, the first for the width and the second for the height; one group serves both. A group is one + /// or two tokens, so the value is split at each auto that follows a complete group. + /// + internal sealed class ContainIntrinsicSizeValueConverter : IValueConverter + { + public IPropertyValue Convert(IEnumerable value) + { + var groups = new List>(); + + foreach (var token in value.Where(t => t.Type != TokenType.Whitespace)) + { + // "auto" opens a group; any other token completes the group it is in, or is a group of its own. + if (IsAuto(token) || groups.Count == 0 || !IsOpenGroup(groups[groups.Count - 1])) + { + groups.Add(new List { token }); + } + else + { + groups[groups.Count - 1].Add(token); + } + } + + if (groups.Count is 0 or > 2 || groups.Any(g => !ContainGrammar.TryParseIntrinsic(g))) return null; + + var width = Spaced(groups[0]); + var height = groups.Count == 2 ? Spaced(groups[1]) : width; + + return new MappedShorthandValue( + new Dictionary> + { + [PropertyNames.ContainIntrinsicWidth] = width, + [PropertyNames.ContainIntrinsicHeight] = height, + }, + value); + } + + public IPropertyValue Construct(Property[] properties) + { + var width = properties.FirstOrDefault(p => p.Name.Is(PropertyNames.ContainIntrinsicWidth))?.DeclaredValue; + var height = properties.FirstOrDefault(p => p.Name.Is(PropertyNames.ContainIntrinsicHeight))?.DeclaredValue; + + if (width == null || height == null) return null; + + var widthTokens = new List(width.Original); + var heightTokens = new List(height.Original); + var original = width.CssText == height.CssText + ? widthTokens + : new List(widthTokens.Concat(new[] { Token.Whitespace }).Concat(heightTokens)); + + return new MappedShorthandValue( + new Dictionary> + { + [PropertyNames.ContainIntrinsicWidth] = widthTokens, + [PropertyNames.ContainIntrinsicHeight] = heightTokens, + }, + original); + } + + private static bool IsAuto(Token token) => token.Type == TokenType.Ident && token.Data.Isi(Keywords.Auto); + + private static bool IsOpenGroup(List group) => group.Count == 1 && IsAuto(group[0]); + + // Whitespace is dropped by the grouping, so the group is rejoined with the single space it was written with. + private static List Spaced(List group) + { + var result = new List(); + + foreach (var token in group) + { + if (result.Count > 0) result.Add(Token.Whitespace); + + result.Add(token); + } + + return result; + } + } + + /// + /// The container shorthand (CSS Containment 3 §2): <'container-name'> [ / <'container-type'> ]?. + /// An omitted type resets container-type to normal. + /// + internal sealed class ContainerValueConverter : IValueConverter + { + public IPropertyValue Convert(IEnumerable value) + { + var front = new List(); + var back = new List(); + var current = front; + + foreach (var token in value) + { + if (token.Type == TokenType.Delim && token.Data.Is("/")) + { + if (current == back) return null; + + current = back; + } + else + { + current.Add(token); + } + } + + if (Converters.LiteralsConverter.Convert(front) == null) return null; + + var hasType = current == back; + + if (hasType && Converters.ContainerTypeConverter.Convert(back) == null) return null; + + return new MappedShorthandValue( + new Dictionary> + { + [PropertyNames.ContainerName] = Trim(front), + [PropertyNames.ContainerType] = hasType + ? Trim(back) + : new List { new Token(TokenType.Ident, Keywords.Normal, TextPosition.Empty) }, + }, + value); + } + + public IPropertyValue Construct(Property[] properties) + { + var name = properties.FirstOrDefault(p => p.Name.Is(PropertyNames.ContainerName))?.DeclaredValue; + var type = properties.FirstOrDefault(p => p.Name.Is(PropertyNames.ContainerType))?.DeclaredValue; + + if (name == null || type == null) return null; + + var nameTokens = new List(name.Original); + var typeTokens = new List(type.Original); + + // The shortest form: a "normal" type is what the shorthand resets to, so it is left out. + var original = type.CssText.Is(Keywords.Normal) + ? nameTokens + : new List(nameTokens + .Concat(new[] { Token.Whitespace, new Token(TokenType.Delim, "/", TextPosition.Empty), Token.Whitespace }) + .Concat(typeTokens)); + + return new MappedShorthandValue( + new Dictionary> + { + [PropertyNames.ContainerName] = nameTokens, + [PropertyNames.ContainerType] = typeTokens, + }, + original); + } + + private static List Trim(List tokens) + { + var start = 0; + var end = tokens.Count - 1; + + while (start <= end && tokens[start].Type == TokenType.Whitespace) start++; + while (end >= start && tokens[end].Type == TokenType.Whitespace) end--; + + return tokens.GetRange(start, end - start + 1); + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ContentFunctionConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ContentFunctionConverter.cs index 6c576fbbb..52be7a0bb 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ContentFunctionConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ContentFunctionConverter.cs @@ -1,10 +1,13 @@ +using System; using System.Collections.Generic; using System.Linq; namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// content() (CSS Generated Content 3 §2.7.3), without the marker argument. + /// The content() function used in content and string-set (GCPM §7.5): + /// content(), content(text), content(before), content(after), + /// content(first-letter). Default mode is text. /// internal sealed class ContentFunctionConverter : IValueConverter { @@ -12,28 +15,23 @@ internal sealed class ContentFunctionConverter : IValueConverter public IPropertyValue Convert(IEnumerable value) { - var first = value.OnlyOrDefault(); - - if (!(first is FunctionToken funcToken) || !funcToken.Data.Equals(FunctionNames.Content, System.StringComparison.OrdinalIgnoreCase)) + if (value.OnlyOrDefault() is not FunctionToken function || + !function.Data.Equals(FunctionNames.Content, StringComparison.OrdinalIgnoreCase)) + { return null; + } var mode = "text"; + var arg = function.ArgumentTokens.FirstOrDefault(t => + t.Type != TokenType.Whitespace && t.Type != TokenType.RoundBracketClose); - if (funcToken.ArgumentTokens.Any()) + if (arg != null) { - var argToken = funcToken.ArgumentTokens.FirstOrDefault(); - if (argToken is KeywordToken keywordToken) - { - var keyword = keywordToken.Data.ToLowerInvariant(); - if (!ValidModes.Contains(keyword)) - return null; + if (arg is not KeywordToken keyword) return null; - mode = keyword; - } - else - { - return null; - } + var name = keyword.Data.ToLowerInvariant(); + if (!ValidModes.Contains(name)) return null; + mode = name; } return new ContentFunctionValue(mode, value); @@ -54,18 +52,11 @@ public ContentFunctionValue(string mode, IEnumerable tokens) Original = new TokenValue(tokens); } - public string CssText => _mode == "text" - ? "content()" - : $"content({_mode})"; + public string CssText => _mode == "text" ? "content()" : $"content({_mode})"; public TokenValue Original { get; } - public TokenValue ExtractFor(string name) - { - return Original; - } - - public string Mode => _mode; + public TokenValue ExtractFor(string name) => Original; } } } diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/CornerShapeValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/CornerShapeValueConverter.cs new file mode 100644 index 000000000..f678d4db3 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/CornerShapeValueConverter.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Validates the CSS Borders 4 <corner-shape-value>: + /// round | scoop | bevel | notch | square | squircle | superellipse(<number> | infinity | -infinity). + /// The value is stored in its canonical form (keyword lower-cased, superellipse() argument normalized). + /// + internal sealed class CornerShapeValueConverter : IValueConverter + { + private static readonly string[] ShapeKeywords = + { + Keywords.Round, "scoop", Keywords.Bevel, "notch", Keywords.Square, "squircle" + }; + + public IPropertyValue Convert(IEnumerable value) + { + var text = Normalize(value); + + return text == null ? null : new CornerShapeValue(text, value); + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + + private static string Normalize(IEnumerable value) + { + var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (tokens.Length != 1) return null; + + var token = tokens[0]; + + if (token.Type == TokenType.Ident) + { + return ShapeKeywords.FirstOrDefault(keyword => keyword.Isi(token.Data)); + } + + if (token is FunctionToken function && function.Data.Isi("superellipse")) + { + var arguments = function.ArgumentTokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (arguments.Length != 1) return null; + + var argument = arguments[0]; + + if (argument is NumberToken number) + { + var parsed = double.Parse(number.Data, NumberStyles.Float, CultureInfo.InvariantCulture); + + return "superellipse(" + parsed.ToString("R", CultureInfo.InvariantCulture) + ")"; + } + + if (argument.Type == TokenType.Ident) + { + if (argument.Data.Isi("infinity")) return "superellipse(infinity)"; + if (argument.Data.Isi("-infinity")) return "superellipse(-infinity)"; + } + } + + return null; + } + + private sealed class CornerShapeValue : IPropertyValue + { + public CornerShapeValue(string text, IEnumerable tokens) + { + CssText = text; + Original = new TokenValue(tokens); + } + + public string CssText { get; } + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) => Original; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/CounterSystemValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/CounterSystemValueConverter.cs new file mode 100644 index 000000000..564f01f88 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/CounterSystemValueConverter.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Validates the system descriptor of @counter-style (CSS Counter Styles Level 3 §2.1): + /// cyclic | numeric | alphabetic | symbolic | additive | [fixed <integer>?] | [extends <counter-style-name>]. + /// Anything else is dropped at parse time; the authored value text is preserved verbatim. + /// + internal sealed class CounterSystemValueConverter : IValueConverter + { + // Idents that can never be a counter-style-name. + private static readonly string[] ReservedNames = + { "none", "inherit", "initial", "unset", "revert", "revert-layer", "default" }; + + public IPropertyValue Convert(IEnumerable value) + { + var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (tokens.Length == 0 || tokens[0].Type != TokenType.Ident) return null; + + var keyword = tokens[0].Data; + var valid = false; + + if (tokens.Length == 1) + { + valid = keyword.Isi("cyclic") || keyword.Isi("numeric") || keyword.Isi("alphabetic") || + keyword.Isi("symbolic") || keyword.Isi("additive") || keyword.Isi("fixed"); + } + else if (tokens.Length == 2) + { + if (keyword.Isi("fixed")) + { + valid = tokens[1] is NumberToken { IsInteger: true }; + } + else if (keyword.Isi("extends")) + { + valid = tokens[1].Type == TokenType.Ident && + !ReservedNames.Any(n => n.Isi(tokens[1].Data)); + } + } + + return valid ? new CounterSystemValue(value) : null; + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + + private sealed class CounterSystemValue : IPropertyValue + { + public CounterSystemValue(IEnumerable tokens) + { + Original = new TokenValue(tokens); + } + + public string CssText => Original.Text; + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) => Original; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/DictionaryValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/DictionaryValueConverter.cs index e41d02e6b..d8f6f94de 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/DictionaryValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/DictionaryValueConverter.cs @@ -4,9 +4,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { internal sealed class DictionaryValueConverter : IValueConverter { - private readonly IReadOnlyDictionary _values; + private readonly Dictionary _values; - public DictionaryValueConverter(IReadOnlyDictionary values) + public DictionaryValueConverter(Dictionary values) { _values = values; } diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/EasingArgumentRangeValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/EasingArgumentRangeValueConverter.cs new file mode 100644 index 000000000..69d742dd9 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/EasingArgumentRangeValueConverter.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Applies the argument ranges CSS Easing puts on steps() and cubic-bezier(), which the plain + /// number converters cannot express: the step count is at least 1 (at least 2 with jump-none), and the + /// x control values of a bezier lie in 0 to 1. A value that breaks one is invalid. + /// + internal sealed class EasingArgumentRangeValueConverter : IValueConverter + { + private readonly IValueConverter _inner; + + public EasingArgumentRangeValueConverter(IValueConverter inner) + { + _inner = inner; + } + + public IPropertyValue Convert(IEnumerable value) + { + var result = _inner.Convert(value); + + if (result == null) return null; + + if (!(value.OnlyOrDefault() is FunctionToken function)) return result; + + var args = SplitArguments(function.ArgumentTokens); + + if (function.Data.Isi(FunctionNames.Steps)) + { + if (args.Count == 0 || !(args[0].Count == 1 && args[0][0] is NumberToken count) || count.IntegerValue < 1) + { + return null; + } + + if (args.Count > 1 && args[1].Count == 1 && args[1][0].Data.Isi(Keywords.JumpNone) && count.IntegerValue < 2) + { + return null; + } + } + else if (function.Data.Isi(FunctionNames.CubicBezier)) + { + if (args.Count != 4 || !IsUnitRange(args[0]) || !IsUnitRange(args[2])) return null; + } + + return result; + } + + public IPropertyValue Construct(Property[] properties) + { + return _inner.Construct(properties); + } + + private static bool IsUnitRange(List argument) => + argument.Count == 1 && argument[0] is NumberToken number && number.Value >= 0f && number.Value <= 1f; + + private static List> SplitArguments(IEnumerable tokens) + { + var arguments = new List> { new List() }; + + foreach (var token in tokens.Where(t => t.Type != TokenType.Whitespace)) + { + if (token.Type == TokenType.Comma) + { + arguments.Add(new List()); + } + else + { + arguments[arguments.Count - 1].Add(token); + } + } + + return arguments; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/EndListValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/EndListValueConverter.cs index acaeabb90..516995dee 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/EndListValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/EndListValueConverter.cs @@ -9,16 +9,65 @@ internal sealed class EndListValueConverter : IValueConverter private readonly IValueConverter _endConverter; private readonly IValueConverter _listConverter; - public EndListValueConverter(IValueConverter listConverter, IValueConverter endConverter) + private readonly IReadOnlyDictionary> _layerDefaults; + private readonly IReadOnlyDictionary _layerFallbacks; + + /// + /// A list of layers, every one but the last converted by and the last by + /// (the background shorthand: a final layer that alone may carry a colour). + /// + /// The converter for every layer but the last. + /// The converter for the last layer. + /// + /// For the longhands that hold one value per layer: the value a layer takes when its text leaves that longhand out + /// (CSS Backgrounds 3 section 2.10). A longhand that is not listed (the colour, which only the last layer sets) is + /// taken from the layers that do set it. + /// + /// + /// For a longhand whose omitted value comes from another longhand of the same layer rather than from its own initial + /// value: background-clip takes what background-origin was given. Tried before . + /// + public EndListValueConverter(IValueConverter listConverter, IValueConverter endConverter, + IReadOnlyDictionary layerDefaults = null, IReadOnlyDictionary layerFallbacks = null) { _listConverter = listConverter; _endConverter = endConverter; + _layerFallbacks = layerFallbacks; + + // Tokenized once: ExtractFor runs for every longhand of every declaration, and the defaults never change. + if (layerDefaults != null) + { + var defaults = new Dictionary>(); + foreach (var pair in layerDefaults) defaults[pair.Key] = Tokenize(pair.Value); + _layerDefaults = defaults; + } + } + + private static List Tokenize(string text) + { + using var lexer = new Lexer(new TextSource(text)) { IsInValue = true }; + var tokens = new List(); + Token token; + + do + { + token = lexer.Get(); + if (token.Type != TokenType.EndOfFile) tokens.Add(token); + } while (token.Type != TokenType.EndOfFile); + + tokens.Trim(); + return tokens; } public IPropertyValue Convert(IEnumerable value) { var items = value.ToList(); var n = items.Count - 1; + + // A comma with nothing on one side of it is not a layer: the declaration is invalid. The layer converters accept an + // empty layer, and the hole this left in the longhands' comma lists used to hide that. + if (n > 0 && items.Exists(item => item.Count == 0)) return null; + var values = new IPropertyValue[n + 1]; for (var i = 0; i < n; i++) @@ -29,7 +78,7 @@ public IPropertyValue Convert(IEnumerable value) } values[n] = _endConverter.Convert(items[n]); - return values[n] != null ? new ListValue(values, value) : null; + return values[n] != null ? new ListValue(values, value, _layerDefaults, _layerFallbacks) : null; } public IPropertyValue Construct(Property[] properties) @@ -62,16 +111,21 @@ public IPropertyValue Construct(Property[] properties) values[i] = converter.Construct(dummies); } - return new ListValue(values, Enumerable.Empty()); + return new ListValue(values, Enumerable.Empty(), _layerDefaults, _layerFallbacks); } private sealed class ListValue : IPropertyValue { private readonly IPropertyValue[] _values; + private readonly IReadOnlyDictionary> _layerDefaults; + private readonly IReadOnlyDictionary _layerFallbacks; - public ListValue(IPropertyValue[] values, IEnumerable tokens) + public ListValue(IPropertyValue[] values, IEnumerable tokens, + IReadOnlyDictionary> layerDefaults, IReadOnlyDictionary layerFallbacks) { _values = values; + _layerDefaults = layerDefaults; + _layerFallbacks = layerFallbacks; Original = new TokenValue(tokens); } @@ -82,23 +136,50 @@ public string CssText public TokenValue Original { get; } + private static List ToTokens(TokenValue value) => value == null ? null : new List(value); + public TokenValue ExtractFor(string name) { - var tokens = new List(); + var layers = new List>(_values.Length); foreach (var value in _values) { - var extracted = value.ExtractFor(name); + var extracted = ToTokens(value.ExtractFor(name)); + + if ((extracted == null || extracted.Count == 0) && _layerFallbacks != null && + _layerFallbacks.TryGetValue(name, out var fallbackName)) + { + extracted = ToTokens(value.ExtractFor(fallbackName)); + } + + layers.Add(extracted is { Count: > 0 } ? extracted : null); + } + + // No layer names this longhand: it is left to its initial value, which an empty value stands for. + if (layers.TrueForAll(layer => layer == null)) return new TokenValue(Enumerable.Empty()); + + List layerDefault = null; + _layerDefaults?.TryGetValue(name, out layerDefault); + + var tokens = new List(); + + // Layers stay comma-separated when exported to a longhand, matching CssText above. A layer that leaves a + // per-layer longhand out takes that longhand's initial value so the list keeps one entry per layer; any other + // longhand (the colour of the last layer) is only taken from the layers that set it. Without this the join left + // a hole in the comma list, which no longhand accepts, and the whole longhand was lost. + foreach (var layer in layers) + { + var entry = layer ?? layerDefault; - if (extracted == null) continue; + if (entry == null) continue; if (tokens.Count > 0) tokens.Add(Token.Comma); - tokens.AddRange(extracted); + tokens.AddRange(entry); } return new TokenValue(tokens); } } } -} \ No newline at end of file +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ExtendedImageConverters.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ExtendedImageConverters.cs index 990f69815..5ec46d2b4 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ExtendedImageConverters.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ExtendedImageConverters.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; @@ -6,8 +7,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine using static Converters; /// - /// An image-set(), cross-fade() or element() value, CSS Images 4 §2.4, §2.6 and §2.7. - /// Only validated and serialized, the painter doesn't handle them. + /// The value of an image-set()/cross-fade()/element() function (CSS Images 4 §2/§3). + /// These are validated as syntactically-valid <image> values; the original tokens are kept + /// for serialization. /// internal sealed class ImageFunctionValue : IPropertyValue { @@ -23,90 +25,98 @@ public ImageFunctionValue(IEnumerable tokens) public TokenValue ExtractFor(string name) => Original; } - /// - /// The <image> inside image-set() and cross-fade(), only a url() or a gradient. - /// Using ImageSourceConverter here would be a construction cycle. - /// + // The inner of an image-set()/cross-fade() option - a url() or a gradient. Referencing the two + // base converters directly (rather than ImageSourceConverter) avoids a static-init cycle. internal static class ExtendedImage { - // Qualified, the bare name is the GradientConverter type + // Qualify GradientConverter - the bare name also resolves to the abstract GradientConverter type. public static readonly IValueConverter InnerImage = UrlConverter.Or(Converters.GradientConverter); } - /// element( <id-selector> ) — a single #id hash argument. + // element( ) - a single '#id' hash argument. internal sealed class ElementImageConverter : IValueConverter { public IPropertyValue Convert(IEnumerable value) { - // ToList() on tokens is the comma group splitter var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); - // An id that looks like a hex color, like #f00, is tokenized as a color - return tokens.Length == 1 && (tokens[0].Type == TokenType.Hash || tokens[0].Type == TokenType.Color) - ? new ImageFunctionValue(value) - : null; + // A '#id' is a single : an all-hex id lexes as a Color token, any other as a Hash. + var isSingleHash = tokens.Length == 1 && + (tokens[0].Type == TokenType.Hash || tokens[0].Type == TokenType.Color); + + return isSingleHash ? new ImageFunctionValue(value) : null; } public IPropertyValue Construct(Property[] properties) => properties.Guard(); } - /// image-set( <image-set-option># ), option = [ <image> | <string> ] [ <resolution> || type(<string>) ]?. + // image-set( # ), + // option = [ | ] [ || type() ]? internal sealed class ImageSetConverter : IValueConverter { public IPropertyValue Convert(IEnumerable value) { - var options = value.ToList(); // comma-separated groups + var options = value.ToList(); if (options.Count == 0 || options.Any(o => !IsOption(o))) return null; return new ImageFunctionValue(value); } private static bool IsOption(List option) { - var items = option.ToItems(); // whitespace/function-boundary split + var items = option.ToItems(); if (items.Count == 0) return false; // The source: a or a url()/gradient . if (StringConverter.Convert(items[0]) == null && ExtendedImage.InnerImage.Convert(items[0]) == null) return false; + // Any remaining items are a (including the `x` dppx alias) and/or type(). for (var i = 1; i < items.Count; i++) + { if (ResolutionConverter.Convert(items[i]) == null && !IsTypeFunction(items[i])) return false; + } return true; } private static bool IsTypeFunction(List item) { - var fn = item.Count == 1 ? item[0] as FunctionToken : null; - if (fn == null || !fn.Data.Equals("type", System.StringComparison.OrdinalIgnoreCase)) + if (item.Count != 1 || item[0] is not FunctionToken function || + !function.Data.Equals("type", StringComparison.OrdinalIgnoreCase)) + { return false; - var inner = fn.Where(t => t.Type != TokenType.Whitespace && t.Type != TokenType.RoundBracketClose).ToArray(); + } + + var inner = function.Where(t => + t.Type != TokenType.Whitespace && t.Type != TokenType.RoundBracketClose).ToArray(); + return inner.Length == 1 && inner[0].Type == TokenType.String; } public IPropertyValue Construct(Property[] properties) => properties.Guard(); } - /// cross-fade( <cf-image># ), <cf-image> = <percentage>? && [ <image> | <color> ]; - /// plus the legacy cross-fade( <image>, <image>, <percentage> ) form. + // cross-fade( # ), = ? && [ | ]; plus the legacy + // cross-fade( , , ) form. internal sealed class CrossFadeConverter : IValueConverter { public IPropertyValue Convert(IEnumerable value) { - var args = value.ToList(); // comma-separated groups + var args = value.ToList(); if (args.Count == 0) return null; - if (args.All(IsCrossFadeImage) || - (args.Count == 3 && IsCrossFadeImage(args[0]) && IsCrossFadeImage(args[1]) && PercentConverter.Convert(args[2]) != null)) - return new ImageFunctionValue(value); + var isModern = args.All(IsCrossFadeImage); + var isLegacy = args.Count == 3 && IsCrossFadeImage(args[0]) && IsCrossFadeImage(args[1]) && + PercentConverter.Convert(args[2]) != null; - return null; + return isModern || isLegacy ? new ImageFunctionValue(value) : null; } private static bool IsCrossFadeImage(List arg) { var hasImageOrColor = false; + foreach (var item in arg.ToItems()) { if (ExtendedImage.InnerImage.Convert(item) != null || ColorConverter.Convert(item) != null) diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FilterValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FilterValueConverter.cs new file mode 100644 index 000000000..7c5d91c3f --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FilterValueConverter.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// Validates a filter / backdrop-filter value through ; the authored text is preserved. + internal sealed class FilterValueConverter : IValueConverter + { + public IPropertyValue Convert(IEnumerable value) + { + return FilterGrammar.TryParse(value) != null ? new GrammarValue(value) : null; + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontGrammarValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontGrammarValueConverter.cs new file mode 100644 index 000000000..553fd5302 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontGrammarValueConverter.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Parses the whitespace-free token list of a value and, when it is valid, produces the canonical + /// serialization of that value. + /// + internal delegate bool FontGrammarParser(IReadOnlyList tokens, out string text); + + /// + /// A thin, text-preserving wrapper that validates a value against one of the hand-written font + /// grammars (, , ...) and keeps + /// the grammar's canonical text as the value. Mirrors . + /// + internal sealed class FontGrammarValueConverter : IValueConverter + { + private readonly FontGrammarParser _parser; + + public FontGrammarValueConverter(FontGrammarParser parser) + { + _parser = parser; + } + + /// A converter accepting exactly one of the given identifiers (any case). + public static IValueConverter ForKeywords(params string[] keywords) + { + return new FontGrammarValueConverter((IReadOnlyList tokens, out string text) => + FontGrammar.TryParseKeyword(tokens, keywords, out text)); + } + + public IPropertyValue Convert(IEnumerable value) + { + var all = value.ToArray(); + var significant = all.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (significant.Length == 0) return null; + + return _parser(significant, out var text) ? new GrammarValue(text, all) : null; + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + + private sealed class GrammarValue : IPropertyValue + { + public GrammarValue(string text, IEnumerable tokens) + { + CssText = text; + Original = new TokenValue(tokens); + } + + public string CssText { get; } + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) => Original; + } + } + + /// Small token helpers shared by the font grammars. + internal static class FontGrammar + { + public static bool IsIdent(Token token, string keyword) => + token.Type == TokenType.Ident && token.Data.Isi(keyword); + + public static bool Contains(string[] set, string value) => + Array.FindIndex(set, s => s.Isi(value)) >= 0; + + /// Exactly one identifier from ; the text is lower-cased. + public static bool TryParseKeyword(IReadOnlyList tokens, string[] keywords, out string text) + { + text = null; + + if (tokens.Count != 1 || tokens[0].Type != TokenType.Ident || !Contains(keywords, tokens[0].Data)) + return false; + + text = tokens[0].Data.ToLowerInvariant(); + return true; + } + + /// An OpenType tag: a string of exactly four printable ASCII characters. + public static bool IsOpenTypeTag(Token token) + { + return token.Type == TokenType.String && token.Data.Length == 4 && token.Data.All(IsPrintableAscii); + } + + public static bool IsPrintableAscii(char c) => c >= 0x20 && c <= 0x7E; + + public static string Quote(string value) => "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontPaletteValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontPaletteValueConverter.cs index 163899166..143bcf111 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontPaletteValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontPaletteValueConverter.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; @@ -6,9 +7,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine /// /// Validates a font-palette value (CSS Fonts Module Level 4): the keywords normal/ /// light/dark, a <dashed-ident> naming an @font-palette-values rule, or a - /// palette-mix() function (validated through the shared ). Anything - /// else is dropped at parse time. The authored value text is preserved verbatim (Layer B re-tokenizes and - /// resolves it in ActualFontPalette), mirroring . + /// palette-mix() of those. Anything else is dropped at parse time. The authored value text is + /// preserved verbatim, mirroring . /// internal sealed class FontPaletteValueConverter : IValueConverter { @@ -16,13 +16,70 @@ public IPropertyValue Convert(IEnumerable value) { var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); - bool valid = tokens.Length == 1 && tokens[0].Type == TokenType.Ident - ? IsKeywordOrDashedIdent(tokens[0].Data) - : PaletteMixGrammar.TryParse(tokens) != null; + var valid = tokens.Length == 1 && IsPalette(tokens[0]); return valid ? new FontPaletteValue(value) : null; } + private static bool IsPalette(Token token) => + token.Type == TokenType.Ident + ? IsKeywordOrDashedIdent(token.Data) + : token is FunctionToken fn && fn.Data.Isi("palette-mix") && IsPaletteMix(fn); + + // CSS Fonts 4 §9.1, palette-mix( , [ <'font-palette'> && ? ]# ), + // the interpolation method is required though the grammar marks it optional, see the changes section + private static bool IsPaletteMix(FunctionToken fn) + { + var groups = new List> { new List() }; + + foreach (var token in fn.ArgumentTokens.Where(t => t.Type != TokenType.Whitespace)) + { + if (token.Type == TokenType.Comma) groups.Add(new List()); + else groups[groups.Count - 1].Add(token); + } + + if (groups.Count < 2 || !IsInterpolationMethod(groups[0])) return false; + + var percentages = new List(); + + for (var i = 1; i < groups.Count; i++) + { + if (!TryGetOperand(groups[i], out var percentage)) return false; + if (percentage.HasValue) percentages.Add(percentage.Value); + } + + // Like color-mix(), explicit percentages that are all 0% leave nothing to mix + return percentages.Count < groups.Count - 1 || percentages.Any(p => p != 0f); + } + + // in [ hue ]?, the hue method only after a polar space + private static bool IsInterpolationMethod(List tokens) + { + if (tokens.Any(t => t.Type != TokenType.Ident)) return false; + if (tokens.Count < 2 || !tokens[0].Data.Isi("in") || + !ColorInterpolationMethodGrammar.IsColorSpace(tokens[1].Data)) return false; + + return tokens.Count == 2 || + tokens.Count == 4 && ColorInterpolationMethodGrammar.IsPolarColorSpace(tokens[1].Data) && + ColorInterpolationMethodGrammar.IsHueDirection(tokens[2].Data) && tokens[3].Data.Isi("hue"); + } + + private static bool TryGetOperand(List tokens, out float? percentage) + { + percentage = null; + + if (tokens.Count == 1) return IsPalette(tokens[0]); + if (tokens.Count != 2) return false; + + var pct = tokens.FirstOrDefault(t => t.Type == TokenType.Percentage) as UnitToken; + var palette = tokens.FirstOrDefault(t => !ReferenceEquals(t, pct)); + + if (pct == null || pct.Value is < 0f or > 100f || palette == null || !IsPalette(palette)) return false; + + percentage = pct.Value; + return true; + } + public IPropertyValue Construct(Property[] properties) { return properties.Guard(); @@ -30,7 +87,7 @@ public IPropertyValue Construct(Property[] properties) private static bool IsKeywordOrDashedIdent(string ident) => ident.Isi(Keywords.Normal) || ident.Isi("light") || ident.Isi("dark") || - ident.StartsWith("--", System.StringComparison.Ordinal); + ident.StartsWith("--", StringComparison.Ordinal); private sealed class FontPaletteValue : IPropertyValue { diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontSynthesisShorthandValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontSynthesisShorthandValueConverter.cs new file mode 100644 index 000000000..cee067f5a --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontSynthesisShorthandValueConverter.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The value converter of the font-synthesis shorthand (CSS Fonts 4 3.5): + /// none | [ weight || style || small-caps || position ]. A longhand the value names is + /// auto; every one it leaves out is none (so font-synthesis: weight still forbids + /// synthetic italic). That differs from the usual "an omitted longhand resets to its initial value", + /// which would make an omitted one auto. + /// + internal sealed class FontSynthesisShorthandValueConverter : IValueConverter + { + private static readonly string[] Longhands = + { + PropertyNames.FontSynthesisWeight, + PropertyNames.FontSynthesisStyle, + PropertyNames.FontSynthesisSmallCaps, + PropertyNames.FontSynthesisPosition + }; + + private static readonly string[] Keys = {"weight", "style", "small-caps", "position"}; + + public IPropertyValue Convert(IEnumerable value) + { + var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + if (tokens.Length == 0 || tokens.Length > Keys.Length) return null; + + var enabled = new bool[Keys.Length]; + + if (tokens.Length == 1 && FontGrammar.IsIdent(tokens[0], Keywords.None)) + { + return new SynthesisValue(enabled, value); + } + + foreach (var token in tokens) + { + var index = token.Type == TokenType.Ident ? System.Array.FindIndex(Keys, k => k.Isi(token.Data)) : -1; + + if (index < 0 || enabled[index]) return null; + + enabled[index] = true; + } + + return new SynthesisValue(enabled, value); + } + + public IPropertyValue Construct(Property[] properties) + { + var enabled = new bool[Keys.Length]; + + for (var i = 0; i < Longhands.Length; i++) + { + var property = properties.FirstOrDefault(m => m.Name.Is(Longhands[i])); + + if (property == null) return null; + + var text = property.Value; + + // Only auto/none (and the initial value, auto) can be written as the shorthand. + if (text.Is(Keywords.None)) continue; + + if (!text.Is(Keywords.Auto) && !text.Is(Keywords.Initial)) return null; + + enabled[i] = true; + } + + return new SynthesisValue(enabled, null); + } + + private sealed class SynthesisValue : IPropertyValue + { + private readonly bool[] _enabled; + + public SynthesisValue(bool[] enabled, IEnumerable tokens) + { + _enabled = enabled; + Original = new TokenValue(tokens ?? Enumerable.Empty()); + } + + public string CssText => _enabled.Any(e => e) + ? string.Join(" ", Keys.Where((_, i) => _enabled[i])) + : Keywords.None; + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) + { + var index = System.Array.FindIndex(Longhands, m => m.Is(name)); + + return index < 0 ? null : TokenValue.FromString(_enabled[index] ? Keywords.Auto : Keywords.None); + } + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontVariantShorthandValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontVariantShorthandValueConverter.cs new file mode 100644 index 000000000..c04b4f646 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/FontVariantShorthandValueConverter.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The value converter of the font-variant shorthand (CSS Fonts 4 6): the components of the + /// font-variant-* longhands in any order, or normal / none. Parsing is done by + /// ; this converter hands each longhand its slice of + /// the value, and rebuilds the shortest shorthand text from the longhands when serializing. + /// + internal sealed class FontVariantShorthandValueConverter : IValueConverter + { + // The slot order of FontVariantGrammar.TryParseShorthand, which is also the serialization order. + internal static readonly string[] Longhands = + { + PropertyNames.FontVariantLigatures, + PropertyNames.FontVariantAlternates, + PropertyNames.FontVariantCaps, + PropertyNames.FontVariantNumeric, + PropertyNames.FontVariantEastAsian, + PropertyNames.FontVariantPosition, + PropertyNames.FontVariantEmoji + }; + + public IPropertyValue Convert(IEnumerable value) + { + var all = value.ToArray(); + var significant = all.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + return FontVariantGrammar.TryParseShorthand(significant, out var texts, out var slots) + ? new ShorthandValue(Compose(texts), slots, all) + : null; + } + + public IPropertyValue Construct(Property[] properties) + { + var texts = new string[Longhands.Length]; + + for (var i = 0; i < Longhands.Length; i++) + { + var property = properties.FirstOrDefault(m => m.Name.Is(Longhands[i])); + + if (property == null) return null; + + texts[i] = property.Value; + } + + // All seven longhands share one CSS-wide keyword (e.g. "inherit"): so does the shorthand. + if (texts.All(t => t.Is(texts[0])) && IsGlobal(texts[0])) return new ShorthandValue(texts[0], null, null); + + // A CSS-wide keyword mixed with anything else cannot be written as a shorthand. The one + // exception is "initial": every font-variant longhand's initial value is "normal" (which is + // what the "font" shorthand resets them to), so it serializes as "normal". + for (var i = 0; i < texts.Length; i++) + { + if (texts[i].Is(Keywords.Initial)) texts[i] = Keywords.Normal; + else if (IsGlobal(texts[i])) return null; + } + + // "none" is only expressible as the shorthand's lone value, which resets everything else. + if (texts[0].Is(Keywords.None)) + { + return texts.Skip(1).All(t => t.Is(Keywords.Normal)) ? new ShorthandValue(Keywords.None, null, null) : null; + } + + return new ShorthandValue(Compose(texts), null, null); + } + + private static bool IsGlobal(string text) => + text.Is(Keywords.Inherit) || text.Is(Keywords.Initial) || text.Is(Keywords.Unset) || + text.Is(Keywords.Revert) || text.Is(Keywords.RevertLayer); + + // The shortest text: "normal" when nothing is set, otherwise just the longhands that are not + // "normal", in the longhands' order (a lone "none" is already handled by the callers). + private static string Compose(string[] texts) + { + var parts = texts.Where(t => !string.IsNullOrEmpty(t) && !t.Is(Keywords.Normal)).ToArray(); + + if (parts.Length != 0) return string.Join(" ", parts); + + return texts[0] != null && texts[0].Is(Keywords.None) ? Keywords.None : Keywords.Normal; + } + + private sealed class ShorthandValue : IPropertyValue + { + private readonly List[] _slots; + + public ShorthandValue(string cssText, List[] slots, IEnumerable tokens) + { + CssText = cssText; + _slots = slots; + Original = new TokenValue(tokens ?? Enumerable.Empty()); + } + + public string CssText { get; } + + public TokenValue Original { get; } + + // A longhand the authored value did not mention yields null, which resets it to "initial". + public TokenValue ExtractFor(string name) + { + if (_slots == null) return Original; + + var index = System.Array.FindIndex(Longhands, m => m.Is(name)); + + return index >= 0 && _slots[index] != null ? new TokenValue(_slots[index]) : null; + } + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GcpmElementFunctionConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GcpmElementFunctionConverter.cs new file mode 100644 index 000000000..52a072dbe --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GcpmElementFunctionConverter.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The CSS GCPM element() content function: + /// element(<custom-ident> [, first | start | last | first-except]?), which places an element + /// removed from flow by position: running(<custom-ident>). It is distinct from the CSS + /// Images element(#id) image function, whose argument is a hash and is not accepted here. + /// + internal sealed class GcpmElementFunctionConverter : IValueConverter + { + private static readonly string[] ValidKeywords = { "first", "start", "last", "first-except" }; + + public IPropertyValue Convert(IEnumerable value) + { + var args = TargetGrammar.FunctionArguments(value, FunctionNames.Element); + + if (args == null || args.Length == 0 || args.Length > 2 || !TargetGrammar.IsIdentifier(args[0])) + return null; + + var keyword = "first"; + + if (args.Length == 2) + { + if (!TargetGrammar.IsIdentifier(args[1]) || + !ValidKeywords.Contains(args[1].Data, StringComparer.OrdinalIgnoreCase)) + { + return null; + } + + keyword = args[1].Data.ToLowerInvariant(); + } + + var text = keyword == "first" + ? $"element({args[0].Data})" + : $"element({args[0].Data}, {keyword})"; + + return new GcpmFunctionValue(text, value); + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GcpmFunctionValue.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GcpmFunctionValue.cs new file mode 100644 index 000000000..6720cf8c4 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GcpmFunctionValue.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The parsed value shared by the GCPM / Generated Content function converters + /// (target-counter(), leader(), element(), ...): it keeps the original tokens and + /// serializes to the supplied canonical text. + /// + internal sealed class GcpmFunctionValue : IPropertyValue + { + public GcpmFunctionValue(string cssText, IEnumerable tokens) + { + CssText = cssText; + Original = new TokenValue(tokens); + } + + public string CssText { get; } + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) => Original; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GradientConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GradientConverter.cs index 6475ccdaa..c9c1007e6 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GradientConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GradientConverter.cs @@ -20,7 +20,8 @@ public IPropertyValue Construct(Property[] properties) return properties.Guard(); } - /// Validates a color stop position, a length or percentage unless overridden. + // The converter that parses a stop's position. Linear/radial stops are positioned with a + // ; a conic gradient overrides this with an (CSS Images 4). protected virtual IValueConverter StopPositionConverter => LengthOrPercentConverter; private IPropertyValue[] ToGradientStops(List> values, int offset) @@ -51,8 +52,11 @@ private IPropertyValue ToGradientStop(List value) if (firstPosition != null) items.RemoveAt(items.Count - 1); } - // CSS Images 4 §3.5.1: a stop can have two positions. Both are kept, the painter reads the - // serialized value. + // = {1,2} (CSS Images 4 3.5.1) - or + // {1,2} for a conic gradient, hence StopPositionConverter: a stop may carry + // two positions, equivalent to two same-colour stops, one at each position. Parsing + // right-to-left, the position taken above is the second (rightmost); a position immediately + // before it is the first, and the two are kept in source order for serialization. if (firstPosition != null && items.Count != 0) { var earlier = StopPositionConverter.Convert(items[items.Count - 1]); @@ -67,12 +71,13 @@ private IPropertyValue ToGradientStop(List value) if (items.Count != 0) { - color = ColorConverter.Convert(items[items.Count - 1]); + color = StopColorConverter.Convert(items[items.Count - 1]); if (color != null) items.RemoveAt(items.Count - 1); } - // A position without a color is a color hint, which only takes one position + // The two-position form is only defined for a ; a bare with no + // colour is a , which takes exactly one position. if (secondPosition != null && color == null) return null; return items.Count == 0 ? new StopValue(color, firstPosition, secondPosition, value) : null; @@ -80,6 +85,33 @@ private IPropertyValue ToGradientStop(List value) protected abstract IPropertyValue ConvertFirstArgument(IEnumerable value); + // A gradient stop's colour: any , including currentcolor (CSS Color 4). + private static IValueConverter StopColorConverter { get; } = ColorConverter.WithCurrentColor(); + + // Keeps the authored text of a prelude that carries a ; the grammar is + // validated by ColorInterpolationMethodGrammar but not otherwise interpreted. + protected static IPropertyValue CreatePlaceholder(IEnumerable tokens) + { + return new PlaceholderValue(tokens); + } + + private sealed class PlaceholderValue : IPropertyValue + { + public PlaceholderValue(IEnumerable tokens) + { + Original = new TokenValue(tokens); + } + + public string CssText => Original.Text; + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) + { + return Original; + } + } + private sealed class StopValue : IPropertyValue { private readonly IPropertyValue _color; @@ -117,18 +149,6 @@ public TokenValue ExtractFor(string name) } } - // A validated value that isn't modeled further, serialized as written - protected static IPropertyValue CreatePlaceholder(IEnumerable tokens) - => new PlaceholderValue(tokens); - - private sealed class PlaceholderValue : IPropertyValue - { - public PlaceholderValue(IEnumerable tokens) { Original = new TokenValue(tokens); } - public string CssText => Original.Text; - public TokenValue Original { get; } - public TokenValue ExtractFor(string name) => Original; - } - private sealed class GradientValue : IPropertyValue { private readonly IPropertyValue _initial; @@ -181,7 +201,7 @@ public LinearGradientConverter() protected override IPropertyValue ConvertFirstArgument(IEnumerable value) { - // CSS Images 4 §3.1 + // "[ | to ] || " (CSS Images 4). if (!ColorInterpolationMethodGrammar.TryExtractInterpolationMethod(value, out var remainder, out var hasIn)) return null; @@ -192,125 +212,101 @@ protected override IPropertyValue ConvertFirstArgument(IEnumerable value) } } - // CSS Images 4 §3.3.1 - internal sealed class ConicGradientConverter : GradientConverter + internal sealed class RadialGradientConverter : GradientConverter { - private readonly IValueConverter _prelude; + private readonly IValueConverter _converter; - public ConicGradientConverter() + public RadialGradientConverter() { - _prelude = WithAny( - AngleConverter.StartsWithKeyword(Keywords.From).Option(), - PointConverter.StartsWithKeyword(Keywords.At).Option(Point.Center)); - } + var position = PointConverter.StartsWithKeyword(Keywords.At).Option(Point.Center); + var circle = WithOrder(WithAny(Assign(Keywords.Circle, true).Option(true), + LengthConverter.Option()), + position); - // | - protected override IValueConverter StopPositionConverter { get; } = - AngleConverter.Or(PercentConverter).Or(new ZeroOrCalcPositionConverter()); + var ellipse = WithOrder(WithAny(Assign(Keywords.Ellipse, false).Option(false), + LengthOrPercentConverter.Many(2, 2).Option()), + position); + + var extents = WithOrder(WithAny(Toggle(Keywords.Circle, Keywords.Ellipse).Option(false), + Map.RadialGradientSizeModes.ToConverter()), position); + + _converter = circle.Or(ellipse.Or(extents)); + } protected override IPropertyValue ConvertFirstArgument(IEnumerable value) { + // "[ || ]? [ at ]? || ". if (!ColorInterpolationMethodGrammar.TryExtractInterpolationMethod(value, out var remainder, out var hasIn)) return null; - if (!hasIn) - return ConvertPrelude(value); + if (hasIn) + return remainder.Count == 0 || _converter.Convert(remainder) != null ? CreatePlaceholder(value) : null; - return remainder.Count == 0 || ConvertPrelude(remainder) != null ? CreatePlaceholder(value) : null; + return _converter.Convert(value); } + } - private IPropertyValue ConvertPrelude(IEnumerable value) + internal sealed class ConicGradientConverter : GradientConverter + { + private readonly IValueConverter _converter; + + public ConicGradientConverter() { - // A group starting with any other ident is the first color stop - Token first = null; - foreach (var t in value) - { - if (t.Type != TokenType.Whitespace) { first = t; break; } - } - if (first != null && first.Type == TokenType.Ident) - { - var id = first.Data; - if (!string.Equals(id, Keywords.From, System.StringComparison.OrdinalIgnoreCase) && - !string.Equals(id, Keywords.At, System.StringComparison.OrdinalIgnoreCase)) - return null; - } + // The conic prelude is "[ from ]? [ at ]?" (CSS Images 4 3.4). + var from = AngleConverter.StartsWithKeyword(Keywords.From).Option(); + var at = PointConverter.StartsWithKeyword(Keywords.At).Option(); - return _prelude.Convert(value); + _converter = WithOrder(from, at); } - // A unitless 0, or any calc()-family function - private sealed class ZeroOrCalcPositionConverter : IValueConverter + // A conic gradient's stops are positioned by angle, not length: uses an + // (CSS Images 4 3.4.1). + protected override IValueConverter StopPositionConverter { get; } = + AngleConverter.Or(PercentConverter) + .Or(new ZeroPositionConverter()) + .Or(new CalcValueConverter(CalcCategory.Angle | CalcCategory.Percentage)); + + // A bare 0 is accepted as an angular stop position (the universal zero, which otherwise + // requires a unit for). + private sealed class ZeroPositionConverter : IValueConverter { public IPropertyValue Convert(IEnumerable value) { - var only = value.OnlyOrDefault(); - if (only == null) return null; - - if (only.Type == TokenType.Number && ((NumberToken)only).Value == 0f) - return CreatePlaceholder(value); - - if (only is FunctionToken function && CalcParser.IsCalcFamily(function.Data)) - return CreatePlaceholder(value); - - return null; + return value.OnlyOrDefault() is NumberToken number && number.Value == 0f + ? CreatePlaceholder(value) + : null; } public IPropertyValue Construct(Property[] properties) => null; } - } - - internal sealed class RadialGradientConverter : GradientConverter - { - private readonly IValueConverter _converter; - - public RadialGradientConverter() - { - var position = PointConverter.StartsWithKeyword(Keywords.At).Option(Point.Center); - var circle = WithOrder(WithAny(Assign(Keywords.Circle, true).Option(true), - LengthConverter.Option()), - position); - - var ellipse = WithOrder(WithAny(Assign(Keywords.Ellipse, false).Option(false), - LengthOrPercentConverter.Many(2, 2).Option()), - position); - - var extents = WithOrder(WithAny(Toggle(Keywords.Circle, Keywords.Ellipse).Option(false), - Map.RadialGradientSizeModes.ToConverter()), position); - - _converter = circle.Or(ellipse.Or(extents)); - } protected override IPropertyValue ConvertFirstArgument(IEnumerable value) { - // CSS Images 4 §3.2.1 + // "[ from ]? [ at ]? || ". if (!ColorInterpolationMethodGrammar.TryExtractInterpolationMethod(value, out var remainder, out var hasIn)) return null; - if (!hasIn) - return ConvertShape(value); + if (hasIn) + return remainder.Count == 0 || ConvertPrelude(remainder) != null ? CreatePlaceholder(value) : null; - return remainder.Count == 0 || ConvertShape(remainder) != null ? CreatePlaceholder(value) : null; + return ConvertPrelude(value); } - private IPropertyValue ConvertShape(IEnumerable value) + private IPropertyValue ConvertPrelude(IEnumerable value) { - // A group starting with any other ident is the first color stop - Token first = null; - foreach (var t in value) - { - if (t.Type != TokenType.Whitespace) { first = t; break; } - } - if (first != null && first.Type == TokenType.Ident) + // The prelude is optional, but this is only called for the gradient's first comma group. If + // that group is actually the first color stop (no from/at), it must not be swallowed here - a + // group that begins with a color, an angle, or a percentage is a stop, so reject it as a + // prelude and let the caller treat it as the first stop. + foreach (var token in value) { - var id = first.Data; - if (!string.Equals(id, "circle", System.StringComparison.OrdinalIgnoreCase) && - !string.Equals(id, "ellipse", System.StringComparison.OrdinalIgnoreCase) && - !string.Equals(id, "at", System.StringComparison.OrdinalIgnoreCase) && - !string.Equals(id, "closest-side", System.StringComparison.OrdinalIgnoreCase) && - !string.Equals(id, "farthest-corner", System.StringComparison.OrdinalIgnoreCase) && - !string.Equals(id, "closest-corner", System.StringComparison.OrdinalIgnoreCase) && - !string.Equals(id, "farthest-side", System.StringComparison.OrdinalIgnoreCase)) - return null; + if (token.Type == TokenType.Whitespace) continue; + + var isPreludeKeyword = token.Type == TokenType.Ident && + (token.Data.Isi(Keywords.From) || token.Data.Isi(Keywords.At)); + + if (!isPreludeKeyword) return null; + break; } return _converter.Convert(value); diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GrammarValue.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GrammarValue.cs new file mode 100644 index 000000000..fbc00e5cf --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GrammarValue.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// A value validated by a grammar whose authored text is preserved verbatim. + internal sealed class GrammarValue : IPropertyValue + { + public GrammarValue(IEnumerable tokens) + { + Original = new TokenValue(tokens); + } + + public string CssText => Original.Text; + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) => Original; + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GrammarValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GrammarValueConverter.cs new file mode 100644 index 000000000..d9c5f5a8c --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GrammarValueConverter.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// A thin, text-preserving converter: accepts the value when grammar approves its tokens + /// (whitespace included, so calc() stays intact) and keeps the authored text as-is. Used by the properties whose grammar lives in a + /// static XGrammar.TryParse (hyphenation, hanging punctuation, rhythmic sizing, ...). + /// + internal sealed class GrammarValueConverter : IValueConverter + { + private readonly Func, bool> _grammar; + + public GrammarValueConverter(Func, bool> grammar) + { + _grammar = grammar; + } + + public IPropertyValue Convert(IEnumerable value) + { + var tokens = value.ToArray(); + + return tokens.Length > 0 && _grammar(tokens) ? new GrammarValue(tokens) : null; + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + + /// A grammar accepting exactly one of . + public static GrammarValueConverter OneOf(params string[] keywords) + { + return new GrammarValueConverter(tokens => + { + var significant = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); + return significant.Length == 1 && significant[0].Type == TokenType.Ident && + keywords.Any(k => significant[0].Data.Isi(k)); + }); + } + + private sealed class GrammarValue : IPropertyValue + { + public GrammarValue(IEnumerable tokens) + { + Original = new TokenValue(tokens); + } + + public string CssText => Original.Text; + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) => Original; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAreaShorthandValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAreaShorthandValueConverter.cs index f07193eac..e5b1e373c 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAreaShorthandValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAreaShorthandValueConverter.cs @@ -2,7 +2,12 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-area, CSS Grid 2 §8.4. + /// + /// The grid-area shorthand: <grid-line> [ / <grid-line> ]{0,3} in the order + /// row-start / column-start / row-end / column-end. Implements the CSS Grid §8.3.1 omitted-value copy + /// rule — a bare <custom-ident> propagates to the paired/all edges, so grid-area: main + /// fills the whole main area, while grid-area: 2 leaves the other edges auto. + /// internal sealed class GridAreaShorthandValueConverter : IValueConverter { public IPropertyValue Convert(IEnumerable value) @@ -11,12 +16,15 @@ public IPropertyValue Convert(IEnumerable value) if (c is null) return null; var rowStart = c[0]; + // column-start omitted → if row-start is a bare custom-ident, all four take it; else auto. var colStart = c.Count > 1 ? c[1] : rowStart.IsBareCustomIdent ? rowStart : (GridPlacementShorthand.Component?)null; + // row-end omitted → copy a bare custom-ident row-start, else auto. var rowEnd = c.Count > 2 ? c[2] : rowStart.IsBareCustomIdent ? rowStart : (GridPlacementShorthand.Component?)null; + // column-end omitted → copy a bare custom-ident column-start, else auto. var colEnd = c.Count > 3 ? c[3] - : colStart.HasValue && colStart.Value.IsBareCustomIdent ? colStart : (GridPlacementShorthand.Component?)null; + : colStart is { IsBareCustomIdent: true } ? colStart : (GridPlacementShorthand.Component?)null; var map = new Dictionary> { diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoFlowValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoFlowValueConverter.cs index baeece372..1ae9cf3c8 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoFlowValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoFlowValueConverter.cs @@ -3,14 +3,18 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-auto-flow, CSS Grid 2 §7.7. + /// + /// Validates a grid-auto-flow value (CSS Grid §7.7): [ row | column ] || dense — one of + /// row/column optionally combined with dense, in either order. The authored text is + /// preserved. + /// internal sealed class GridAutoFlowValueConverter : IValueConverter { public IPropertyValue Convert(IEnumerable value) { var tokens = value.ToArray(); var idents = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); - if (idents.Length < 1 || idents.Length > 2 || idents.Any(t => t.Type != TokenType.Ident)) + if (idents.Length is < 1 or > 2 || idents.Any(t => t.Type != TokenType.Ident)) return null; var hasAxis = false; @@ -19,7 +23,7 @@ public IPropertyValue Convert(IEnumerable value) { if (t.Data.Isi(Keywords.Row) || t.Data.Isi(Keywords.Column)) { - if (hasAxis) return null; + if (hasAxis) return null; // row and column are mutually exclusive hasAxis = true; } else if (t.Data.Isi(Keywords.Dense)) diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoTracksValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoTracksValueConverter.cs index 6a35f47ba..c5adf942a 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoTracksValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoTracksValueConverter.cs @@ -3,7 +3,10 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-auto-columns/-rows, CSS Grid 2 §7.6. + /// + /// Validates a grid-auto-columns/grid-auto-rows value: a <track-size>+ list + /// (no repeat()) via the shared . The authored text is preserved. + /// internal sealed class GridAutoTracksValueConverter : IValueConverter { public IPropertyValue Convert(IEnumerable value) diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridColumnRowShorthandValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridColumnRowShorthandValueConverter.cs index c50a47e04..8287de6c7 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridColumnRowShorthandValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridColumnRowShorthandValueConverter.cs @@ -3,8 +3,11 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// grid-row/grid-column, CSS Grid 2 §8.4. Not built from WithOrder(...).Option(), - /// that resets an omitted end to auto instead of copying a bare <custom-ident>. + /// The grid-column / grid-row shorthand: <grid-line> [ / <grid-line> ]? + /// expanding to the axis's -start/-end longhands. Implements the CSS Grid §8.3.1 + /// omitted-value copy rule that the generic WithOrder(...).Option() DSL cannot: when the end is + /// omitted, it copies a bare <custom-ident> start (so grid-column: main spans the + /// whole main area) but leaves a line number / span as auto. /// internal sealed class GridColumnRowShorthandValueConverter : IValueConverter { @@ -23,6 +26,7 @@ public IPropertyValue Convert(IEnumerable value) if (components is null) return null; var start = components[0]; + // end omitted → copy a bare custom-ident start, else auto. var end = components.Count > 1 ? components[1] : start.IsBareCustomIdent ? start : (GridPlacementShorthand.Component?)null; diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridLineValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridLineValueConverter.cs index d37e42c28..37f2d9507 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridLineValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridLineValueConverter.cs @@ -3,7 +3,11 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-row-start/-end, grid-column-start/-end, CSS Grid 2 §8.3. + /// + /// Validates a single grid <grid-line> (grid-column-start/-end, + /// grid-row-start/-end) through the shared — auto, an + /// integer line, or span N. The authored text is preserved. + /// internal sealed class GridLineValueConverter : IValueConverter { public IPropertyValue Convert(IEnumerable value) diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridPlacementShorthand.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridPlacementShorthand.cs index 9a6626592..e27bc0f2f 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridPlacementShorthand.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridPlacementShorthand.cs @@ -3,19 +3,24 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// Helpers for grid-row, grid-column and grid-area, CSS Grid 2 §8.4. + /// Shared helpers for the grid placement shorthands (grid-column/grid-row/ + /// grid-area): splitting a value on / into <grid-line> components, validating + /// each, and building the omitted-value copy result (CSS Grid §8.3.1). internal static class GridPlacementShorthand { - /// A <grid-line>, only a bare <custom-ident> is copied to omitted lines. + /// A parsed <grid-line> component: its authored tokens and whether it is a bare + /// <custom-ident> (the only form the omitted-value copy rule propagates). internal struct Component { public IReadOnlyList Tokens { get; set; } public bool IsBareCustomIdent { get; set; } } - private static readonly Token AutoToken = new Token(TokenType.Ident, Keywords.Auto, TextPosition.Empty); + private static readonly Token AutoToken = new(TokenType.Ident, Keywords.Auto, TextPosition.Empty); - /// Splits on /, null if a component is invalid or there are too many. + /// Splits the value on top-level / delimiters into <grid-line> components, + /// validating each via . Returns null on any invalid component or an + /// out-of-range component count. internal static List Split(IEnumerable value, int maxComponents) { var groups = new List> { new List() }; @@ -32,7 +37,9 @@ internal static List Split(IEnumerable value, int maxComponent var significant = group.Where(t => t.Type != TokenType.Whitespace).ToArray(); var parsed = GridLineGrammar.TryParse(significant); if (parsed is null) return null; - // Keep the inner whitespace, "span2" would lex as one ident. + // Preserve internal whitespace in the stored tokens (only trim the ends) so a two-token + // component like "span 2" round-trips as "span 2", not "span2" (which would re-lex as one + // ident). A bare is a single ident token that parsed to a named (non-Nth) line. components.Add(new Component { Tokens = Trim(group), @@ -43,11 +50,11 @@ internal static List Split(IEnumerable value, int maxComponent return components.Count >= 1 && components.Count <= maxComponents ? components : null; } - internal static IReadOnlyList Or(Component? component) - { - return component.HasValue ? component.Value.Tokens : new[] { AutoToken }; - } + /// The tokens for a longhand: the specified component's tokens, or auto when omitted. + internal static IReadOnlyList Or(Component? component) => + component is { } c ? c.Tokens : new[] { AutoToken }; + /// Drops leading and trailing whitespace tokens, keeping internal whitespace. private static IReadOnlyList Trim(List group) { var start = 0; @@ -58,7 +65,8 @@ private static IReadOnlyList Trim(List group) } } - /// A shorthand value with precomputed tokens per longhand. + /// A shorthand that emits a precomputed token stream for each of + /// its longhands (via ), used by the grid placement shorthands. internal sealed class MappedShorthandValue : IPropertyValue { private readonly IReadOnlyDictionary> _map; @@ -73,6 +81,6 @@ public MappedShorthandValue(IReadOnlyDictionary> ma public TokenValue Original { get; } public TokenValue ExtractFor(string name) => - _map.TryGetValue(name, out var tokens) ? new TokenValue(tokens) : new TokenValue(new Token[0]); + _map.TryGetValue(name, out var tokens) ? new TokenValue(tokens) : TokenValue.Empty; } } diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridShorthandValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridShorthandValueConverter.cs index a4be38913..f11ab9caf 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridShorthandValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridShorthandValueConverter.cs @@ -3,23 +3,34 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid, CSS Grid 2 §7.8. A longhand without a slice is reset to its initial value. + /// + /// The grid shorthand (CSS Grid §7.8): either a <grid-template>, or one of the two + /// auto-flow forms + /// <grid-template-rows> / [ auto-flow && dense? ] <grid-auto-columns>? and + /// [ auto-flow && dense? ] <grid-auto-rows>? / <grid-template-columns>. Expands to + /// the three grid-template-* longhands plus grid-auto-flow/-rows/-columns; + /// every longhand it doesn't set is reset to its initial value (an absent slice — §7.8). + /// internal sealed class GridShorthandValueConverter : IValueConverter { public IPropertyValue Convert(IEnumerable value) { - // Not ToList(), the IEnumerable extension of that name splits on commas. + // NB: the domain-specific IEnumerable.ToList() extension splits on commas; materialize the + // flat token list explicitly instead. var tokens = new List(value); + // Form 1: . The three template longhands are set; grid-auto-flow/-rows/-columns + // are omitted here so they reset to their initial values (row / auto / auto). if (GridTemplateShorthand.TryParseTemplate(tokens, out var rows, out var columns, out var areas)) { var templateMap = new Dictionary>(); - if (rows != null) templateMap[PropertyNames.GridTemplateRows] = rows; - if (columns != null) templateMap[PropertyNames.GridTemplateColumns] = columns; - if (areas != null) templateMap[PropertyNames.GridTemplateAreas] = areas; + if (rows is not null) templateMap[PropertyNames.GridTemplateRows] = rows; + if (columns is not null) templateMap[PropertyNames.GridTemplateColumns] = columns; + if (areas is not null) templateMap[PropertyNames.GridTemplateAreas] = areas; return new MappedShorthandValue(templateMap, tokens); } + // Forms 2/3: an auto-flow form — requires exactly one top-level '/'. var groups = GridTemplateShorthand.SplitTopLevelSlash(tokens); if (groups.Count != 2) return null; @@ -30,7 +41,7 @@ public IPropertyValue Convert(IEnumerable value) var leftHasFlow = left.Any(t => t.Type == TokenType.Ident && t.Data.Isi(Keywords.AutoFlow)); var rightHasFlow = right.Any(t => t.Type == TokenType.Ident && t.Data.Isi(Keywords.AutoFlow)); - if (leftHasFlow == rightHasFlow) return null; + if (leftHasFlow == rightHasFlow) return null; // exactly one side carries auto-flow var map = new Dictionary>(); @@ -43,7 +54,7 @@ public IPropertyValue Convert(IEnumerable value) map[PropertyNames.GridTemplateRows] = rowsSlice; map[PropertyNames.GridAutoFlow] = GridTemplateShorthand.BuildAutoFlow(column: true, dense); - if (autoColumns != null) map[PropertyNames.GridAutoColumns] = autoColumns; + if (autoColumns is not null) map[PropertyNames.GridAutoColumns] = autoColumns; } else { @@ -54,7 +65,7 @@ public IPropertyValue Convert(IEnumerable value) map[PropertyNames.GridTemplateColumns] = columnsSlice; map[PropertyNames.GridAutoFlow] = GridTemplateShorthand.BuildAutoFlow(column: false, dense); - if (autoRows != null) map[PropertyNames.GridAutoRows] = autoRows; + if (autoRows is not null) map[PropertyNames.GridAutoRows] = autoRows; } return new MappedShorthandValue(map, tokens); diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateAreasValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateAreasValueConverter.cs index dc646aac5..7a4047da9 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateAreasValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateAreasValueConverter.cs @@ -3,7 +3,11 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// grid-template-areas, CSS Grid 2 §7.3. + /// + /// Validates a grid-template-areas value: the keyword none or a rectangular grid of quoted + /// strings accepted by the shared (equal columns per row, each + /// named area a single filled rectangle). The authored text is preserved. + /// internal sealed class GridTemplateAreasValueConverter : IValueConverter { public IPropertyValue Convert(IEnumerable value) @@ -12,7 +16,7 @@ public IPropertyValue Convert(IEnumerable value) var isNone = tokens.Length == 1 && tokens[0].Type == TokenType.Ident && tokens[0].Data.Isi(Keywords.None); - if (!isNone && GridTemplateAreasGrammar.TryParse(tokens) == null) + if (!isNone && GridTemplateAreasGrammar.TryParse(tokens) is null) return null; return new GridTemplateAreasValue(value); diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthand.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthand.cs index 85333c25b..45294dcac 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthand.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthand.cs @@ -3,15 +3,24 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { - /// Parsing shared by grid-template (CSS Grid 2 §7.4) and grid (§7.8). + /// + /// Shared parsing for the CSS Grid mega-shorthands grid-template (CSS Grid 2 §7.4) and + /// grid (§7.8): splitting the value on the top-level /, running the areas-form row scanner, + /// and validating each partitioned slice against the existing longhand grammars + /// (/). The two converters stay + /// thin over this helper. + /// internal static class GridTemplateShorthand { - internal static readonly Token AutoToken = new Token(TokenType.Ident, Keywords.Auto, TextPosition.Empty); - private static readonly Token NoneToken = new Token(TokenType.Ident, Keywords.None, TextPosition.Empty); + internal static readonly Token AutoToken = new(TokenType.Ident, Keywords.Auto, TextPosition.Empty); + private static readonly Token NoneToken = new(TokenType.Ident, Keywords.None, TextPosition.Empty); /// - /// No nesting depth needed, a function is a single and [name] - /// groups only hold idents. + /// Splits the flat token stream on top-level / delimiters, preserving each group's + /// whitespace. No bracket/paren depth counter is needed: functions are single + /// s (their args nested) and [name] groups hold only idents, so a + /// / never appears nested at the flat level (same reasoning as + /// ). /// internal static List> SplitTopLevelSlash(IEnumerable value) { @@ -29,18 +38,24 @@ internal static List> SplitTopLevelSlash(IEnumerable value) internal static Token[] Significant(IEnumerable tokens) => tokens.Where(t => t.Type != TokenType.Whitespace).ToArray(); - /// One side of <rows> / <columns>, trimmed, or null if invalid. + /// + /// Validates one axis of the <rows> / <columns> form — a <track-list> or + /// the keyword none (both accepted by the grid-template-rows/-columns longhands) — + /// returning the edge-trimmed slice, or if invalid. + /// internal static IReadOnlyList TryTemplateAxis(IReadOnlyList raw) { var significant = Significant(raw); if (significant.Length == 0) return null; - if (significant.Length == 1 && significant[0].Type == TokenType.Ident && significant[0].Data.Isi(Keywords.None)) return Trim(raw); - return GridTrackListGrammar.TryParse(raw) != null ? Trim(raw) : null; + if (significant.Length == 1 && significant[0].Type == TokenType.Ident && significant[0].Data.Isi(Keywords.None)) + return Trim(raw); + return GridTrackListGrammar.TryParse(raw) is not null ? Trim(raw) : null; } private static bool ContainsRepeat(IEnumerable significant) => significant.Any(t => t is FunctionToken fn && fn.Data.Isi(FunctionNames.Repeat)); + /// Drops leading/trailing whitespace tokens, keeping internal whitespace. internal static IReadOnlyList Trim(IReadOnlyList tokens) { var start = 0; @@ -53,6 +68,7 @@ internal static IReadOnlyList Trim(IReadOnlyList tokens) return result; } + /// The grid-auto-flow token slice for the grid auto-flow forms. internal static IReadOnlyList BuildAutoFlow(bool column, bool dense) { var flow = new List { new Token(TokenType.Ident, column ? Keywords.Column : Keywords.Row, TextPosition.Empty) }; @@ -64,21 +80,26 @@ internal static IReadOnlyList BuildAutoFlow(bool column, bool dense) return flow; } - /// A null slice means the longhand is reset to its initial value. + /// + /// Parses a grid-template value (§7.4) into the three longhand slices. A + /// out-slice means "omit it" (the longhand resets to its initial value). Returns + /// for an invalid value (the whole declaration is then dropped). + /// internal static bool TryParseTemplate(IEnumerable value, out IReadOnlyList rows, out IReadOnlyList columns, out IReadOnlyList areas) { rows = columns = areas = null; var groups = SplitTopLevelSlash(value); - if (groups.Count > 2) return false; + if (groups.Count > 2) return false; // at most one top-level '/' var leftRaw = groups[0]; var rightRaw = groups.Count == 2 ? groups[1] : null; var left = Significant(leftRaw); var right = rightRaw is null ? null : Significant(rightRaw); - if (right == null && left.Length == 1 && left[0].Type == TokenType.Ident && left[0].Data.Isi(Keywords.None)) + // grid-template: none → all three longhands none. + if (right is null && left.Length == 1 && left[0].Type == TokenType.Ident && left[0].Data.Isi(Keywords.None)) { rows = columns = areas = new[] { NoneToken }; return true; @@ -88,6 +109,7 @@ internal static bool TryParseTemplate(IEnumerable value, if (left.Any(t => t.Type == TokenType.String)) { + // Areas form: [ ? ? ? ]+ [ / ]? if (!TryScanAreasForm(left, out var rowsTokens, out var areaTokens)) return false; if (GridTemplateAreasGrammar.TryParse(areaTokens) is null) return false; if (GridTrackListGrammar.TryParse(rowsTokens) is null) return false; @@ -95,9 +117,9 @@ internal static bool TryParseTemplate(IEnumerable value, rows = rowsTokens; areas = areaTokens; - if (rightRaw != null) + if (rightRaw is not null) { - // , no repeat() + // The trailing columns are an — a with no repeat(). if (right.Length == 0 || ContainsRepeat(right) || GridTrackListGrammar.TryParse(rightRaw) is null) return false; columns = Trim(rightRaw); @@ -106,6 +128,7 @@ internal static bool TryParseTemplate(IEnumerable value, return true; } + // rows / columns form: requires the slash; each side is a or `none`. if (rightRaw is null) return false; var rowsSlice = TryTemplateAxis(leftRaw); var columnsSlice = TryTemplateAxis(rightRaw); @@ -117,8 +140,10 @@ internal static bool TryParseTemplate(IEnumerable value, } /// - /// Splits the areas form into the grid-template-rows track list and the area strings, a row - /// without a track size gets auto. + /// Scans the areas-form left side, synthesizing the grid-template-rows track list + /// () and collecting the ordered <string> area tokens + /// (). Every emitted token is whitespace-separated so both round-trip + /// serialization (via ToText) and re-parsing are valid. /// private static bool TryScanAreasForm(Token[] toks, out List rowsOut, out List areaStrings) { @@ -133,6 +158,8 @@ private static bool TryScanAreasForm(Token[] toks, out List rowsOut, out if (t.Type == TokenType.SquareBracketOpen) { + // Copy the [ name … ] line-name group (idents only) at the current line position, kept + // compact as `[a b]` (edge spaces omitted, single space between names). if (rowsOut.Count > 0) rowsOut.Add(Token.Whitespace); rowsOut.Add(t); i++; @@ -145,8 +172,8 @@ private static bool TryScanAreasForm(Token[] toks, out List rowsOut, out firstName = false; i++; } - if (i >= toks.Length) return false; - rowsOut.Add(toks[i]); + if (i >= toks.Length) return false; // unclosed '[' + rowsOut.Add(toks[i]); // ']' i++; continue; } @@ -157,6 +184,7 @@ private static bool TryScanAreasForm(Token[] toks, out List rowsOut, out sawString = true; i++; + // Optional single-token for this row (else the row track is `auto`). if (i < toks.Length && toks[i].Type != TokenType.SquareBracketOpen && toks[i].Type != TokenType.String) { if (GridTrackListGrammar.TryParseTrackSizeList(new[] { toks[i] }) is null) return false; @@ -171,6 +199,7 @@ private static bool TryScanAreasForm(Token[] toks, out List rowsOut, out continue; } + // A track-size or stray token with no preceding string at this position is invalid. return false; } @@ -184,8 +213,9 @@ private static void Emit(List tokens, Token token) } /// - /// The [ auto-flow && dense? ] <track-size>* side of grid, a null - /// means it's reset to its initial value. + /// Parses the [ auto-flow && dense? ] <track-size>* side of a grid auto-flow + /// form. Returns when auto-flow is absent or the value is malformed; + /// is null when the optional track-size list is omitted (reset to initial). /// internal static bool TryParseAutoFlowSide(IReadOnlyList raw, out bool dense, out IReadOnlyList autoTracks) { diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthandValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthandValueConverter.cs index c2b99c572..64769e779 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthandValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthandValueConverter.cs @@ -3,8 +3,9 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// grid-template, CSS Grid 2 §7.4. resets a longhand without - /// a slice to its initial value. + /// The grid-template shorthand (CSS Grid §7.4), expanding to grid-template-rows / + /// grid-template-columns / grid-template-areas. An omitted axis is reset to its initial + /// value (none) by when its slice is absent. /// internal sealed class GridTemplateShorthandValueConverter : IValueConverter { @@ -14,9 +15,9 @@ public IPropertyValue Convert(IEnumerable value) return null; var map = new Dictionary>(); - if (rows != null) map[PropertyNames.GridTemplateRows] = rows; - if (columns != null) map[PropertyNames.GridTemplateColumns] = columns; - if (areas != null) map[PropertyNames.GridTemplateAreas] = areas; + if (rows is not null) map[PropertyNames.GridTemplateRows] = rows; + if (columns is not null) map[PropertyNames.GridTemplateColumns] = columns; + if (areas is not null) map[PropertyNames.GridTemplateAreas] = areas; return new MappedShorthandValue(map, value); } diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateValueConverter.cs index 250c7a98a..ff3a8532f 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateValueConverter.cs @@ -1,12 +1,15 @@ -using System; using System.Collections.Generic; using System.Linq; namespace TheArtOfDev.HtmlRenderer.Core.CssEngine { /// - /// grid-template-columns/-rows, CSS Grid 2 §7.2. The parsed template is kept as a typed - /// . + /// Validates a grid-template-columns/grid-template-rows value: accepts the keyword + /// none or any <track-list> the shared recognizes + /// (lengths, %, fr, auto, min-content/max-content, minmax(), + /// fit-content(), repeat()), rejecting everything else so an invalid template is dropped + /// at parse time. The authored text is preserved verbatim (mirrors + /// ). /// internal sealed class GridTemplateValueConverter : IValueConverter { @@ -16,14 +19,10 @@ public IPropertyValue Convert(IEnumerable value) var isNone = tokens.Length == 1 && tokens[0].Type == TokenType.Ident && tokens[0].Data.Isi(Keywords.None); - GridTemplate template = null; - if (!isNone) - { - template = GridTrackListGrammar.TryParse(tokens); - if (template is null) return null; - } + if (!isNone && GridTrackListGrammar.TryParse(tokens) is null) + return null; - return new GridTemplateValue(value, template); + return new GridTemplateValue(value); } public IPropertyValue Construct(Property[] properties) @@ -31,46 +30,11 @@ public IPropertyValue Construct(Property[] properties) return properties.Guard(); } - /// - /// For the cascade paths that only have a string: the initial value, a CSS-wide keyword, an inherited - /// value or a resolved var(). - /// - internal static CssProperty FromCssText(string value) - { - if (CssGlobalKeywords.TryParse(value, out var keyword)) - return CssProperty.Global(keyword); - if (value.IndexOf("var(", StringComparison.OrdinalIgnoreCase) >= 0) - return CssProperty.Unresolved(value); - var template = GridTrackListGrammar.TryParse(Tokenize(value)); - return CssProperty.FromValue(value, template); - } - - /// Like CssValueParser.GetCssTokens, without depending on Core.Parse. - private static List Tokenize(string value) - { - using (var lexer = new Lexer(value) { IsInValue = false }) - { - var tokens = new List(); - Token token; - do - { - token = lexer.Get(); - if (token.Type != TokenType.EndOfFile && token.Type != TokenType.Whitespace) - tokens.Add(token); - } while (token.Type != TokenType.EndOfFile); - return tokens; - } - } - - private sealed class GridTemplateValue : IPropertyValue, ITypedPropertyValue + private sealed class GridTemplateValue : IPropertyValue { - private readonly CssProperty _typed; - - public GridTemplateValue(IEnumerable tokens, GridTemplate template) + public GridTemplateValue(IEnumerable tokens) { Original = new TokenValue(tokens); - // null template for none - _typed = CssProperty.FromValue(Original.Text, template); } public string CssText => Original.Text; @@ -78,8 +42,6 @@ public GridTemplateValue(IEnumerable tokens, GridTemplate template) public TokenValue Original { get; } public TokenValue ExtractFor(string name) => Original; - - public CssProperty GetTypedValue() => _typed; } } } diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ImageOrientationValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ImageOrientationValueConverter.cs new file mode 100644 index 000000000..1d8799532 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ImageOrientationValueConverter.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// Validates an image-orientation value through ; the authored text is preserved. + internal sealed class ImageOrientationValueConverter : IValueConverter + { + public IPropertyValue Convert(IEnumerable value) + { + return ImageOrientationGrammar.IsValid(value) ? new GrammarValue(value) : null; + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ImageResolutionValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ImageResolutionValueConverter.cs new file mode 100644 index 000000000..52dcc013e --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ImageResolutionValueConverter.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// Validates an image-resolution value through ; the authored text is preserved. + internal sealed class ImageResolutionValueConverter : IValueConverter + { + public IPropertyValue Convert(IEnumerable value) + { + return ImageResolutionGrammar.IsValid(value) ? new GrammarValue(value) : null; + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ImplicitResetValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ImplicitResetValueConverter.cs new file mode 100644 index 000000000..6dcecffae --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ImplicitResetValueConverter.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Wraps a shorthand's converter for a shorthand that also resets longhands it cannot express (the + /// font shorthand resets font-kerning, font-feature-settings, the + /// font-variant-* longhands, ... CSS Fonts 4 7.7). Parsing is unchanged - the declaration + /// expansion resets any longhand the value does not mention to its initial value - but when + /// serializing, the shorthand can only be rebuilt from the longhands while every such reset longhand + /// still holds its initial value; otherwise the longhands would be lost. + /// + internal sealed class ImplicitResetValueConverter : IValueConverter + { + private readonly IValueConverter _converter; + private readonly Dictionary _initialValues; + + /// The shorthand's own converter. + /// For each implicitly reset longhand, the texts its initial value can + /// take ("initial" is always accepted). + public ImplicitResetValueConverter(IValueConverter converter, Dictionary initialValues) + { + _converter = converter; + _initialValues = initialValues; + } + + public IPropertyValue Convert(IEnumerable value) => _converter.Convert(value); + + public IPropertyValue Construct(Property[] properties) + { + foreach (var property in properties) + { + if (!_initialValues.TryGetValue(property.Name, out var accepted)) continue; + + var text = property.Value; + + if (!text.Is(Keywords.Initial) && !accepted.Any(a => a.Is(text))) return null; + } + + return _converter.Construct(properties); + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/KeywordGrammarValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/KeywordGrammarValueConverter.cs new file mode 100644 index 000000000..5bb166762 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/KeywordGrammarValueConverter.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Accepts a value whose significant tokens satisfy a keyword grammar, and preserves the authored text. + /// + internal sealed class KeywordGrammarValueConverter : IValueConverter + { + private readonly Func _isValid; + + public KeywordGrammarValueConverter(Func isValid) + { + _isValid = isValid; + } + + public IPropertyValue Convert(IEnumerable value) + { + var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + return tokens.Length != 0 && _isValid(tokens) ? new KeywordGrammarValue(value) : null; + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + + private sealed class KeywordGrammarValue : IPropertyValue + { + public KeywordGrammarValue(IEnumerable tokens) + { + Original = new TokenValue(tokens); + } + + public string CssText => Original.Text; + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) => Original; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/KeywordSetValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/KeywordSetValueConverter.cs new file mode 100644 index 000000000..13bef7188 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/KeywordSetValueConverter.cs @@ -0,0 +1,143 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Validates a value that is a single identifier from a fixed keyword set (matched case-insensitively) + /// and preserves the authored text verbatim. Used by the plain keyword properties that have no + /// dedicated value model (isolation, pointer-events, user-select, ...). + /// + internal sealed class KeywordSetValueConverter : TextValueConverter + { + private readonly string[] _keywords; + + public KeywordSetValueConverter(params string[] keywords) + { + _keywords = keywords; + } + + protected override bool IsValid(Token[] tokens) + { + return tokens.Length == 1 && tokens[0].Type == TokenType.Ident && + _keywords.Any(k => tokens[0].Data.Isi(k)); + } + } + + /// + /// touch-action (Pointer Events 3): auto | none | manipulation | + /// [ pan-x | pan-left | pan-right ] || [ pan-y | pan-up | pan-down ] || pinch-zoom. + /// + internal sealed class TouchActionValueConverter : TextValueConverter + { + private static readonly string[] Horizontal = {"pan-x", "pan-left", "pan-right"}; + private static readonly string[] Vertical = {"pan-y", "pan-up", "pan-down"}; + + protected override bool IsValid(Token[] tokens) + { + if (tokens.Length == 0 || tokens.Any(t => t.Type != TokenType.Ident)) return false; + + if (tokens.Length == 1 && + (tokens[0].Data.Isi(Keywords.Auto) || tokens[0].Data.Isi(Keywords.None) || + tokens[0].Data.Isi("manipulation"))) + { + return true; + } + + if (tokens.Length > 3) return false; + + var horizontal = false; + var vertical = false; + var pinch = false; + + foreach (var token in tokens) + { + if (Horizontal.Any(k => token.Data.Isi(k))) + { + if (horizontal) return false; + horizontal = true; + } + else if (Vertical.Any(k => token.Data.Isi(k))) + { + if (vertical) return false; + vertical = true; + } + else if (token.Data.Isi("pinch-zoom")) + { + if (pinch) return false; + pinch = true; + } + else + { + return false; + } + } + + return true; + } + } + + /// + /// math-depth (MathML Core): auto-add | add(<integer>) | <integer>. + /// + internal sealed class MathDepthValueConverter : TextValueConverter + { + protected override bool IsValid(Token[] tokens) + { + if (tokens.Length != 1) return false; + + var token = tokens[0]; + + switch (token.Type) + { + case TokenType.Ident: + return token.Data.Isi("auto-add"); + case TokenType.Number: + return IsInteger(token); + case TokenType.Function: + var arguments = ((FunctionToken) token).ArgumentTokens + .Where(t => t.Type != TokenType.Whitespace).ToArray(); + return token.Data.Isi("add") && arguments.Length == 1 && IsInteger(arguments[0]); + default: + return false; + } + } + + private static bool IsInteger(Token token) + { + return token.Type == TokenType.Number && ((NumberToken) token).IsInteger; + } + } + + /// Base for converters that validate a token sequence and keep the authored text. + internal abstract class TextValueConverter : IValueConverter + { + public IPropertyValue Convert(IEnumerable value) + { + var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray(); + + return IsValid(tokens) ? new TextValue(value) : null; + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + + protected abstract bool IsValid(Token[] tokens); + + private sealed class TextValue : IPropertyValue + { + public TextValue(IEnumerable tokens) + { + Original = new TokenValue(tokens); + } + + public string CssText => Original.Text; + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) => Original; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/LeaderFunctionConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/LeaderFunctionConverter.cs new file mode 100644 index 000000000..4e1e120b0 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/LeaderFunctionConverter.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The leader() function (CSS Generated Content 3): leader(<leader-type>?) where + /// <leader-type> = dotted | solid | space | <string>. A bare leader() is + /// equivalent to leader(dotted). + /// + internal sealed class LeaderFunctionConverter : IValueConverter + { + private static readonly string[] ValidKeywords = { Keywords.Dotted, Keywords.Solid, Keywords.Space }; + + public IPropertyValue Convert(IEnumerable value) + { + var args = TargetGrammar.FunctionArguments(value, FunctionNames.Leader); + + if (args == null || args.Length > 1) return null; + + if (args.Length == 1 && + args[0].Type != TokenType.String && + !(TargetGrammar.IsIdentifier(args[0]) && + ValidKeywords.Contains(args[0].Data, StringComparer.OrdinalIgnoreCase))) + { + return null; + } + + return new GcpmFunctionValue(new TokenValue(value).Text, value); + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/LinearEasingValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/LinearEasingValueConverter.cs new file mode 100644 index 000000000..59be25785 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/LinearEasingValueConverter.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Validates linear(<linear-stop-list>) (CSS Easing 2 2.1), where + /// <linear-stop-list> = [ <linear-stop> ]#{2,} and + /// <linear-stop> = <number> && <percentage>{0,2}. The number may come before or + /// after its (adjacent) percentages. The authored text is preserved. + /// + internal sealed class LinearEasingValueConverter : IValueConverter + { + public IPropertyValue Convert(IEnumerable value) + { + var function = value.OnlyOrDefault() as FunctionToken; + + if (function == null || !function.Data.Equals(FunctionNames.Linear, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return IsValidStopList(function.ArgumentTokens) ? new LinearEasingValue(value) : null; + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + + private static bool IsValidStopList(IEnumerable arguments) + { + var stops = new List> { new List() }; + + foreach (var token in arguments.Where(t => t.Type != TokenType.Whitespace)) + { + if (token.Type == TokenType.Comma) + { + stops.Add(new List()); + } + else + { + stops[stops.Count - 1].Add(token); + } + } + + return stops.Count >= 2 && stops.All(IsValidStop); + } + + // {0,2} or {1,2} + private static bool IsValidStop(List stop) + { + if (stop.Count < 1 || stop.Count > 3) return false; + + var numbers = stop.Count(IsFiniteNumber); + var percents = stop.Count(IsFinitePercentage); + + if (numbers != 1 || numbers + percents != stop.Count) return false; + + // The percentages must be adjacent: the number is first or last, never between them. + return IsFiniteNumber(stop[0]) || IsFiniteNumber(stop[stop.Count - 1]); + } + + private static bool IsFiniteNumber(Token token) => + token is NumberToken number && IsFinite(number.Value); + + private static bool IsFinitePercentage(Token token) => + token.Type == TokenType.Percentage && token is UnitToken percent && IsFinite(percent.Value); + + private static bool IsFinite(float value) => !float.IsNaN(value) && !float.IsInfinity(value); + + private sealed class LinearEasingValue : IPropertyValue + { + public LinearEasingValue(IEnumerable tokens) + { + Original = new TokenValue(tokens); + } + + public string CssText => Original.Text; + + public TokenValue Original { get; } + + public TokenValue ExtractFor(string name) => Original; + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/MarksValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/MarksValueConverter.cs new file mode 100644 index 000000000..21ee8f0a5 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/MarksValueConverter.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// The marks page descriptor (CSS Paged Media 3 section 12): none | [ crop || cross ]. + /// The keywords may appear in either order and are serialized as crop cross. + /// + internal sealed class MarksValueConverter : IValueConverter + { + public IPropertyValue Convert(IEnumerable value) + { + var names = new List(); + + foreach (var token in value) + { + if (token.Type == TokenType.Whitespace) continue; + if (token.Type != TokenType.Ident) return null; + names.Add(token.Data.ToLowerInvariant()); + } + + if (names.Count == 0 || names.Count > 2) return null; + + if (names.Count == 1 && names[0] == Keywords.None) return new GcpmFunctionValue(Keywords.None, value); + + var crop = names.Count(n => n == "crop"); + var cross = names.Count(n => n == "cross"); + if (crop > 1 || cross > 1 || crop + cross != names.Count) return null; + + var text = string.Join(" ", new[] { crop == 1 ? "crop" : null, cross == 1 ? "cross" : null } + .Where(n => n != null)); + + return new GcpmFunctionValue(text, value); + } + + public IPropertyValue Construct(Property[] properties) + { + return properties.Guard(); + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/MaskConverters.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/MaskConverters.cs new file mode 100644 index 000000000..cadd7abbd --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/MaskConverters.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Value converters of the CSS Masking 1 §6 mask-* longhands that have no background-* + /// counterpart. mask-image, mask-repeat, mask-position and mask-size reuse the + /// background converters, as the grammars are identical. + /// + internal static class MaskConverters + { + private static Dictionary Of(params string[] names) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var name in names) map[name] = name; + return map; + } + + // = alpha | luminance | match-source + public static readonly IValueConverter ModeConverter = + Of("match-source", "alpha", "luminance").ToConverter().FromList(); + + // = | view-box, = content-box | padding-box | border-box | fill-box | stroke-box + public static readonly IValueConverter OriginConverter = + Of("border-box", "padding-box", "content-box", "fill-box", "stroke-box", "view-box") + .ToConverter().FromList(); + + // [ | no-clip ]# + public static readonly IValueConverter ClipConverter = + Of("border-box", "padding-box", "content-box", "fill-box", "stroke-box", "view-box", "no-clip") + .ToConverter().FromList(); + + // = add | subtract | intersect | exclude + public static readonly IValueConverter CompositeConverter = + Of("add", "subtract", "intersect", "exclude").ToConverter().FromList(); + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/NonNegativeValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/NonNegativeValueConverter.cs new file mode 100644 index 000000000..79932d073 --- /dev/null +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/NonNegativeValueConverter.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TheArtOfDev.HtmlRenderer.Core.CssEngine +{ + /// + /// Wraps a single-token numeric converter and rejects a negative literal, for the grammars written + /// <length-percentage [0,∞]> or <number [0,∞]>. + /// + internal sealed class NonNegativeValueConverter : IValueConverter + { + private readonly IValueConverter _converter; + + public NonNegativeValueConverter(IValueConverter converter) + { + _converter = converter; + } + + public IPropertyValue Convert(IEnumerable value) + { + var tokens = value.ToArray(); + + foreach (var token in tokens) + { + if (token is UnitToken unit && unit.Value < 0f) return null; + if (token is NumberToken number && number.Value < 0f) return null; + } + + return _converter.Convert(tokens); + } + + public IPropertyValue Construct(Property[] properties) + { + return _converter.Construct(properties); + } + } +} diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/OneOrMoreValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/OneOrMoreValueConverter.cs index 5c064edd7..d22b2fd2b 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/OneOrMoreValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/OneOrMoreValueConverter.cs @@ -44,7 +44,7 @@ public IPropertyValue Construct(Property[] properties) for (var i = 0; i < properties.Length; i++) { - var value = _converter.Construct(new[] { properties[i] }); + var value = _converter.Construct(new[] {properties[i]}); if (value == null) return null; diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/OptionValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/OptionValueConverter.cs index 847bdc116..8de9f8f55 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/OptionValueConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/OptionValueConverter.cs @@ -19,14 +19,7 @@ public IPropertyValue Convert(IEnumerable value) public IPropertyValue Construct(Property[] properties) { - var value = _converter.Construct(properties); - // A longhand this optional shorthand slot maps to may carry the literal "initial" - // sentinel (see ShorthandProperty.Export) when the original shorthand text omitted it - - // that's correct for cascade purposes, but re-serializing the shorthand must omit it - // entirely (matching real browser serialization of e.g. "border: 1px outset" - the never- - // specified color must not round-trip as "1px outset initial"), the same as if this slot - // had never been set at all. - return value is null || value.CssText == Keywords.Initial ? new OptionValue(Enumerable.Empty()) : value; + return _converter.Construct(properties) ?? new OptionValue(Enumerable.Empty()); } private sealed class OptionValue : IPropertyValue @@ -63,10 +56,7 @@ public IPropertyValue Convert(IEnumerable value) public IPropertyValue Construct(Property[] properties) { - var value = _converter.Construct(properties); - // See OptionValueConverter.Construct above for why the "initial" sentinel must be - // suppressed here rather than serialized literally. - return value is null || value.CssText == Keywords.Initial ? new OptionValue(Enumerable.Empty()) : value; + return _converter.Construct(properties) ?? new OptionValue(Enumerable.Empty()); } private sealed class OptionValue : IPropertyValue diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/OrderIndependentOptionsConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/OrderIndependentOptionsConverter.cs index 43bd775e3..b7041b8d6 100644 --- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/OrderIndependentOptionsConverter.cs +++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/OrderIndependentOptionsConverter.cs @@ -6,19 +6,19 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine // Like UnorderedOptionsConverter (the WithAny combinator), but matches the operands in any order rather // than only in declaration order. WithAny matches greedily left-to-right, which fails whenever an // earlier converter claims a token a later one also accepts - e.g. "list-style: none square", where - // list-style-type takes "none" before list-style-image can. + // list-style-type takes "none" before list-style-image can (TylerBrinks/ExCSS#185). // // Use this ONLY for shorthands whose operands are order-independent with no positional tie-break. Some // "||" (any-order) shorthands still assign longhands by position and so must NOT be reordered: animation's // first