From ceb3b528f48d2dd77449f30800bdfecf1f6cc3ca Mon Sep 17 00:00:00 2001 From: Adrian Cochrane Date: Fri, 14 Jun 2019 17:04:54 +1200 Subject: [PATCH] Add and pass some unit tests. --- README.md | 12 +++- Stylish/Parse.hs | 102 ------------------------------ Stylish/Parse/Selector.hs | 49 -------------- Stylish/Parse/Utils.hs | 15 ----- Stylish.hs => src/Stylish.hs | 0 src/Stylish/Parse.hs | 91 ++++++++++++++++++++++++++ src/Stylish/Parse/Selector.hs | 63 ++++++++++++++++++ src/Stylish/Parse/Utils.hs | 34 ++++++++++ {Stylish => src/Stylish}/Style.hs | 0 stylish-haskell.cabal | 11 +++- test/Test.hs | 72 +++++++++++++++++++++ 11 files changed, 280 insertions(+), 169 deletions(-) delete mode 100644 Stylish/Parse.hs delete mode 100644 Stylish/Parse/Selector.hs delete mode 100644 Stylish/Parse/Utils.hs rename Stylish.hs => src/Stylish.hs (100%) create mode 100644 src/Stylish/Parse.hs create mode 100644 src/Stylish/Parse/Selector.hs create mode 100644 src/Stylish/Parse/Utils.hs rename {Stylish => src/Stylish}/Style.hs (100%) create mode 100644 test/Test.hs diff --git a/README.md b/README.md index 0a1a3ff..9511e4a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,14 @@ # Stylish Haskell Generic CSS style engine for Haskell, intended to aid the development of new browser engines. -Stylish Haskell implements CSS selection and cascade (but not inheritance) independant of the CSS at-rules and properties understood by the caller. +Stylish Haskell implements CSS selection and cascade (but not inheritance) independant of the CSS at-rules and properties understood by the caller. It is intended to ease the development of new browser engines, independant of their output targets. + +## Why Haskell? +No matter what you think about Haskell and other functional languages, there are great reasons to choose it for this project. + +The primary reason is that the biggest challenge in implementing a CSS engine is in defining all the various CSS properties, and as such it needs to be trivial to define each individual property. Haskell's pattern matching syntax is perfect for this, and it's laziness is useful. + +Though beyond that Haskell makes just as trivial to assemble functions as it does datastructures, which comes in very handy for parsing and interpreting programming languages like CSS selectors. + +## API +So far I've only implemented a CSS parser via the function `Stylish.Parse.parse` which returns a variant of the passed in `StyleSheet`. `StyleSheet` is a typeclass implementing the logic for parsing CSS atrules and storing style rules. diff --git a/Stylish/Parse.hs b/Stylish/Parse.hs deleted file mode 100644 index ea84a6c..0000000 --- a/Stylish/Parse.hs +++ /dev/null @@ -1,102 +0,0 @@ -module Stylish.Parse ( - parse, - skipAtRule, - StyleSheet, addRule, addAtRule, - StyleRule, - Selector(..), SimpleSelector(..), PropertyTest(..) - ) where - -import Data.CSS.Syntax.Tokens -import Stylish.Parse.Selector -import Parse.Utils - --------- ----- Output type class --------- -class StyleSheet s where - addRule :: s -> StyleRule -> s - addAtRule :: s -> Text -> [Token] -> (s, [Token]) - addAtRule self _ tokens = (self, skipAtRule tokens) - -data StyleRule = StyleRule [Selector] [(Text, [Token])] - --------- ----- Basic parsing --------- -parse :: StyleSheet s => s -> Text -> s -parse stylesheet source = parse' stylesheet tokenize source - --- Things to skip. -parse' stylesheet Whitespace:tokens = parse' stylesheet tokens -parse' stylesheet CDO:tokens = parse' stylesheet tokens -parse' stylesheet CDC:tokens = parse' stylesheet tokens -parse' stylesheet Comma:tokens = parse' stylesheet tokens -- TODO issue warnings. - -parse' stylesheet [] = stylesheet - -parse' stylesheet AtKeyword(kind):tokens = parse' stylesheet' tokens' - where (stylesheet', tokens') = addAtRule stylesheet kind tokens -parse' stylesheet tokens = parse' addRule stylesheet StyleRule selectors properties tokens' - where (selectors, block) = parseSelectors tokens - (properties, tokens') = parseProperties block -parse' stylesheet tokens = parse' (addRule stylesheet rule) tokens' - where (rule, tokens') = concatP (StyleRule) (parseSelector) (parseProperties) - --------- ----- Property parsing --------- -parseProperties LeftCurlyBracket:tokens = parseProperties' tokens -parseProperties Whitespace:tokens = parseProperties tokens - -parseProperties' Whitespace:tokens = parseProperties' tokens -parseProperties' (Ident name):tokens - | Colon:tokens' <- skipSpace tokens = - concatP (appendProp) (scanValue) (parseProperties') tokens' - where appendProp value tail = (name, value):tail -parseProperties' RightCurlyBracket:tokens = [], tokens -parseProperties' tokens = parseProperties' skipValue tokens - --------- ----- Skipping/Scanning utilities --------- -skipAtRule Semicolon:tokens = tokens -skipAtRule LeftCurlyBracket:tokens = skipBlock tokens - -skipAtRule LeftParen:tokens = skipAtRule skipBlock tokens -skipAtRule LeftSquareBracket:tokens = skipAtRule skipBlock tokens --- To ensure parens are balanced, should already be handled. -skipAtRule RightCurlyBracket:tokens = RightCurlyBracket:tokens -skipAtRule RightParen:tokens = RightParen:tokens -skipAtRule RightSquareBracket:tokens = RightSquareBracket:tokens - -skipAtRule _:tokens = skipAtRule tokens - -scanValue Semicolon:tokens = ([], tokens) -scanValue Whitespace:tokens = scanValue tokens - -scanValue LeftCurlyBracket:tokens = scanInner tokens scanValue -scanValue LeftParen:tokens = scanInner tokens scanValue -scanValue LeftSquareBracket:tokens = scanInner tokens scanValue -scanInner tokens cb = concatP (++) (scanBlock) (cb) --- To ensure parens are balanced, should already be handled. -scanValue RightCurlyBracket:tokens = ([], RightCurlyBracket:tokens) -scanValue RightParen:tokens = ([], RightParen:tokens) -scanValue RightSquareBracket:tokens = ([], RightSquareBracket:tokens) - -scanValue tokens = capture scanValue tokens - -scanValue tokens = snd scanValue tokens - --- TODO assert closing tags are correct --- But what should the error recovery be? -scanBlock RightCurlyBracket:tokens = ([RightCurlyBracket], tokens) -scanBlock RightParen:tokens = ([RightParen], tokens) -scanBlock RightSquareBracket:tokens = ([RightSquareBracket], tokens) - -scanBlock LeftCurlyBracket:tokens = scanInner tokens scanBlock -scanBlock LeftParen:tokens = scanInner tokens scanBlock -scanBlock RightSquareBracket:tokens = scanInner tokens scanBlock - -scanBlock tokens = capture scanBlock tokens - -skipBlock tokens = snd scanBlock tokens diff --git a/Stylish/Parse/Selector.hs b/Stylish/Parse/Selector.hs deleted file mode 100644 index fd6ad6b..0000000 --- a/Stylish/Parse/Selector.hs +++ /dev/null @@ -1,49 +0,0 @@ -module Stylish.Parse.Selector( - Selector(..), SimpleSelector(..), PropertyTest(..), - parseSelectors - ) where - -import Data.CSS.Syntax.Tokens -import Utils - -data Selector = Selector [SimpleSelector] -data SimpleSelector = Tag Text | Id Text | Class Text | Property Text PropertyTest | - Child | Descendant | Adjacent | Sibling -data PropertyTest = Exists | Equals Text | Suffix Text | Prefix Text | Substring Text | - Include Text | Dash Text - -parseSelectors :: [Token] -> (Selector, [Token]) -parseSelectors tokens = concatP (:) (parseSelector) (parseSelectorsTail) skipSpace tokens -parseSelectorsTail Comma:tokens = parseSelectors token -parseSelectorsTail tokens = ([], tokens) - -parseSelector' op tokens = (op:selector, tokens') - where (selector, tokens') = parseSelector tokens - -parseSelector Ident tag:tokens = parseSelector' Tag tag tokens -parseSelector Hash _ id:tokens = parseSelector' Id id tokens -parseSelector Delim '.':Ident class_:tokens = parseSelector' Class class_ tokens -parseSelector LeftSquareBracket:Ident prop:tokens = - concatP (appendPropertySel) (parsePropertySel) (parseSelector) tokens - where appendPropertySel test selector = (Property prop test):selector - -parseSelector Whitespace:tokens = parseCombinator skipSpace tokens -parseSelector Delim c:tokens = parseCombinator Delim c:tokens -parseSelector tokens = ([], tokens) - -parseCombinator Delim '>':tokens = parseSelector' Child skipSpace tokens -parseCombinator Delim '~':tokens = parseSelector' Sibling skipSpace tokens -parseCombinator Delim '+':tokens = parseSelector' Adjacent skipSpace tokens -parseCombinator tokens = parseSelector' Descendant tokens - -parsePropertySel RightSquareBracket:tokens = (Exists, tokens) -parsePropertySel Delim '=':tokens = parsePropertyVal (Equals) tokens -parsePropertySel SuffixMatch:tokens = parsePropertyVal (Suffix) tokens -parsePropertySel PrefixMatch:tokens = parsePropertyVal (Prefix) tokens -parsePropertySel SubstringMatch:tokens = parsePropertyVal (Substring) tokens -parsePropertySel IncludeMatch:tokens = parsePropertyVal (Include) tokens -parsePropertySel DashMatch:tokens = parsePropertyVal (Dash) tokens -parsePropertySel tokens = (Exists, skipBlock tokens) - -parsePropertyVal wrapper Ident val:RightSquareBracket:tokens = (wrapper val, tokens) -parsePropertyVal wrapper String val:RightSquareBracket:tokens = (wrapper val, tokens) diff --git a/Stylish/Parse/Utils.hs b/Stylish/Parse/Utils.hs deleted file mode 100644 index fbeeeb7..0000000 --- a/Stylish/Parse/Utils.hs +++ /dev/null @@ -1,15 +0,0 @@ -module Stylish.Parse.Utils( - concatP, capture, skipSpace - ) where - -import Data.CSS.Syntax.Tokens - -concatP join left right tokens = (join x y, remainder) - where (x, tokens') = left tokens - (y, remainder) = right tokens' - -capture cb token:tokens = (token:captured, tokens') - where (captured, tokens') = cb tokens - -skipSpace Whitespace:tokens = skipSpace tokens -skipSpace tokens = tokens diff --git a/Stylish.hs b/src/Stylish.hs similarity index 100% rename from Stylish.hs rename to src/Stylish.hs diff --git a/src/Stylish/Parse.hs b/src/Stylish/Parse.hs new file mode 100644 index 0000000..e81c05b --- /dev/null +++ b/src/Stylish/Parse.hs @@ -0,0 +1,91 @@ +module Stylish.Parse ( + parse, TrivialStyleSheet(..), + skipAtRule, + StyleSheet, addRule, addAtRule, + StyleRule(..), + Selector(..), SimpleSelector(..), PropertyTest(..) + ) where + +import Data.CSS.Syntax.Tokens +import Stylish.Parse.Selector +import Stylish.Parse.Utils + +import Data.Text.Internal (Text(..)) + +-------- +---- Output type class +-------- +class StyleSheet s where + addRule :: s -> StyleRule -> s + addAtRule :: s -> Text -> [Token] -> (s, [Token]) + addAtRule self _ tokens = (self, skipAtRule tokens) + +data StyleRule = StyleRule [Selector] [(Text, [Token])] deriving (Show, Eq) + +data TrivialStyleSheet = TrivialStyleSheet [StyleRule] deriving (Show, Eq) +instance StyleSheet TrivialStyleSheet where + addRule (TrivialStyleSheet self) rule = TrivialStyleSheet $ rule:self + +-------- +---- Basic parsing +-------- +parse :: StyleSheet s => s -> Text -> s +parse stylesheet source = parse' stylesheet $ tokenize source + +-- Things to skip. +parse' stylesheet (Whitespace:tokens) = parse' stylesheet tokens +parse' stylesheet (CDO:tokens) = parse' stylesheet tokens +parse' stylesheet (CDC:tokens) = parse' stylesheet tokens +parse' stylesheet (Comma:tokens) = parse' stylesheet tokens -- TODO issue warnings. + +parse' stylesheet [] = stylesheet + +parse' stylesheet (AtKeyword kind:tokens) = parse' stylesheet' tokens' + where (stylesheet', tokens') = addAtRule stylesheet kind tokens +parse' stylesheet tokens = parse' (addRule stylesheet rule) tokens' + where (rule, tokens') = concatP StyleRule parseSelectors parseProperties tokens + +-------- +---- Property parsing +-------- +parseProperties (LeftCurlyBracket:tokens) = parseProperties' tokens +parseProperties (Whitespace:tokens) = parseProperties tokens + +parseProperties' (Whitespace:tokens) = parseProperties' tokens +parseProperties' (Ident name:tokens) + | Colon:tokens' <- skipSpace tokens = + concatP appendProp scanValue parseProperties' tokens' + where appendProp value tail = (name, value):tail +parseProperties' (RightCurlyBracket:tokens) = ([], tokens) +parseProperties' tokens = parseProperties' (skipValue tokens) + +-------- +---- Skipping/Scanning utilities +-------- +skipAtRule :: [Token] -> [Token] +skipAtRule (Semicolon:tokens) = tokens +skipAtRule (LeftCurlyBracket:tokens) = skipBlock tokens + +skipAtRule (LeftParen:tokens) = skipAtRule $ skipBlock tokens +skipAtRule (LeftSquareBracket:tokens) = skipAtRule $ skipBlock tokens +-- To ensure parens are balanced, should already be handled. +skipAtRule (RightCurlyBracket:tokens) = RightCurlyBracket:tokens +skipAtRule (RightParen:tokens) = RightParen:tokens +skipAtRule (RightSquareBracket:tokens) = RightSquareBracket:tokens + +skipAtRule (_:tokens) = skipAtRule tokens + +scanValue (Semicolon:tokens) = ([], tokens) +scanValue (Whitespace:tokens) = scanValue tokens + +scanValue (LeftCurlyBracket:tokens) = scanInner tokens scanValue +scanValue (LeftParen:tokens) = scanInner tokens scanValue +scanValue (LeftSquareBracket:tokens) = scanInner tokens scanValue +-- To ensure parens are balanced, should already be handled. +scanValue (RightCurlyBracket:tokens) = ([], RightCurlyBracket:tokens) +scanValue (RightParen:tokens) = ([], RightParen:tokens) +scanValue (RightSquareBracket:tokens) = ([], RightSquareBracket:tokens) + +scanValue tokens = capture scanValue tokens + +skipValue tokens = snd $ scanValue tokens diff --git a/src/Stylish/Parse/Selector.hs b/src/Stylish/Parse/Selector.hs new file mode 100644 index 0000000..476ebfa --- /dev/null +++ b/src/Stylish/Parse/Selector.hs @@ -0,0 +1,63 @@ +module Stylish.Parse.Selector( + Selector(..), SimpleSelector(..), PropertyTest(..), + parseSelectors + ) where + +import Data.CSS.Syntax.Tokens +import Stylish.Parse.Utils + +import Data.Text.Internal (Text(..)) + +type Selector = [SimpleSelector] +data SimpleSelector = Tag Text | Id Text | Class Text | Property Text PropertyTest | + Child | Descendant | Adjacent | Sibling + deriving (Show, Eq) +data PropertyTest = Exists | Equals Text | Suffix Text | Prefix Text | Substring Text | + Include Text | Dash Text + deriving (Show, Eq) + +parseSelectors :: [Token] -> ([Selector], [Token]) +parseSelectors tokens = concatP (:) parseSelector parseSelectorsTail $ skipSpace tokens +parseSelectorsTail (Comma:tokens) = parseSelectors tokens +parseSelectorsTail tokens = ([], tokens) + +parseSelector' op tokens = (op:selector, tokens') + where (selector, tokens') = parseSelector tokens + +parseSelector :: [Token] -> (Selector, [Token]) +parseSelector (Delim '*':tokens) = parseSelector tokens +parseSelector (Ident tag:tokens) = parseSelector' (Tag tag) tokens +parseSelector (Hash _ id:tokens) = parseSelector' (Id id) tokens +parseSelector (Delim '.':Ident class_:tokens) = parseSelector' (Class class_) tokens +parseSelector (LeftSquareBracket:Ident prop:tokens) = + concatP appendPropertySel parsePropertySel parseSelector tokens + where appendPropertySel test selector = Property prop test : selector + +parseSelector (Whitespace:tokens) = parseCombinator $ skipSpace tokens +parseSelector (Delim c:tokens) | c `elem` ">~+" = parseCombinator $ Delim c:tokens +parseSelector tokens = ([], tokens) + +parseCombinator (Delim '>':tokens) = parseSelector' Child $ skipSpace tokens +parseCombinator (Delim '~':tokens) = parseSelector' Sibling $ skipSpace tokens +parseCombinator (Delim '+':tokens) = parseSelector' Adjacent $ skipSpace tokens +-- Take special care to avoid adding a trailing Descendant when not needed. +parseCombinator tokens@(LeftCurlyBracket:_) = ([], tokens) +parseCombinator tokens@(RightCurlyBracket:_) = ([], tokens) +parseCombinator tokens@(RightSquareBracket:_) = ([], tokens) +parseCombinator tokens@(Comma:_) = ([], tokens) + +parseCombinator tokens@(RightParen:_) = ([], tokens) + +parseCombinator tokens = parseSelector' Descendant tokens + +parsePropertySel (RightSquareBracket:tokens) = (Exists, tokens) +parsePropertySel (Delim '=':tokens) = parsePropertyVal (Equals) tokens +parsePropertySel (SuffixMatch:tokens) = parsePropertyVal (Suffix) tokens +parsePropertySel (PrefixMatch:tokens) = parsePropertyVal (Prefix) tokens +parsePropertySel (SubstringMatch:tokens) = parsePropertyVal (Substring) tokens +parsePropertySel (IncludeMatch:tokens) = parsePropertyVal (Include) tokens +parsePropertySel (DashMatch:tokens) = parsePropertyVal (Dash) tokens +parsePropertySel tokens = (Exists, skipBlock tokens) + +parsePropertyVal wrapper (Ident val:RightSquareBracket:tokens) = (wrapper val, tokens) +parsePropertyVal wrapper (String val:RightSquareBracket:tokens) = (wrapper val, tokens) diff --git a/src/Stylish/Parse/Utils.hs b/src/Stylish/Parse/Utils.hs new file mode 100644 index 0000000..e5e39b8 --- /dev/null +++ b/src/Stylish/Parse/Utils.hs @@ -0,0 +1,34 @@ +module Stylish.Parse.Utils( + concatP, capture, skipSpace, + scanBlock, skipBlock, scanInner + ) where + +import Data.CSS.Syntax.Tokens + +concatP join left right tokens = (join x y, remainder) + where + (x, tokens') = left tokens + (y, remainder) = right tokens' + +capture cb (token:tokens) = (token:captured, tokens') + where (captured, tokens') = cb tokens +capture _ [] = ([], []) + +skipSpace (Whitespace:tokens) = skipSpace tokens +skipSpace tokens = tokens + +-- TODO assert closing tags are correct +-- But what should the error recovery be? +scanBlock (RightCurlyBracket:tokens) = ([RightCurlyBracket], tokens) +scanBlock (RightParen:tokens) = ([RightParen], tokens) +scanBlock (RightSquareBracket:tokens) = ([RightSquareBracket], tokens) + +scanBlock (LeftCurlyBracket:tokens) = scanInner tokens scanBlock +scanBlock (LeftParen:tokens) = scanInner tokens scanBlock +scanBlock (LeftSquareBracket:tokens) = scanInner tokens scanBlock + +scanBlock tokens = capture scanBlock tokens + +skipBlock tokens = snd $ scanBlock tokens + +scanInner tokens cb = concatP (++) scanBlock cb tokens diff --git a/Stylish/Style.hs b/src/Stylish/Style.hs similarity index 100% rename from Stylish/Style.hs rename to src/Stylish/Style.hs diff --git a/stylish-haskell.cabal b/stylish-haskell.cabal index 6f19542..49f313f 100644 --- a/stylish-haskell.cabal +++ b/stylish-haskell.cabal @@ -57,11 +57,18 @@ library -- other-extensions: -- Other library packages from which modules are imported. - build-depends: base >=4.9 && <4.10, css-syntax + build-depends: base >=4.9 && <4.10, css-syntax, text -- Directories containing source files. - -- hs-source-dirs: + hs-source-dirs: src -- Base language which the package is written in. default-language: Haskell2010 +test-suite test-stylish + hs-source-dirs: src test + default-language: Haskell2010 + type: exitcode-stdio-1.0 + main-is: Test.hs + other-modules: Stylish.Parse + build-depends: base >=4.9 && <4.10, css-syntax, text, hspec, QuickCheck diff --git a/test/Test.hs b/test/Test.hs new file mode 100644 index 0000000..25eb3e4 --- /dev/null +++ b/test/Test.hs @@ -0,0 +1,72 @@ +{-# LANGUAGE OverloadedStrings #-} +module Main where + +import Test.Hspec +import Test.Hspec.QuickCheck +import Stylish.Parse +import Data.CSS.Syntax.Tokens + +main = hspec spec + +spec = do + describe "Canary" $ do + it "Test framework works" $ do + True `shouldBe` True + describe "Parsing" $ do + it "Ignores @rules" $ do + parse emptyStyle "@encoding 'utf-8';" `shouldBe` emptyStyle + parse emptyStyle " @encoding 'utf-8';" `shouldBe` emptyStyle + parse emptyStyle "@encoding 'utf-8'; " `shouldBe` emptyStyle + parse emptyStyle "@media print { a:link {color: green;} }" `shouldBe` emptyStyle + parse emptyStyle " @media print { a:link {color: green;} }" `shouldBe` emptyStyle + parse emptyStyle "@media print { a:link {color: green;} } " `shouldBe` emptyStyle + + parse emptyStyle "@encoding 'utf-8'; a {color:green}" `shouldBe` linkStyle + parse emptyStyle "a {color:green}@encoding 'utf-8';" `shouldBe` linkStyle + parse emptyStyle "@media print{a{color:black;}}a {color:green}" `shouldBe` linkStyle + parse emptyStyle "a {color:green} @media print {a{color:black;}}" `shouldBe` linkStyle + it "Parses style rules" $ do + -- Syntax examples from "Head First HTML & CSS with XHTML" + parse emptyStyle "bedroom { drapes: blue; carpet: wool shag; }" `shouldBe` TrivialStyleSheet [ + StyleRule [[Tag "bedroom"]] [ + ("drapes", [Ident "blue"]), + ("carpet", [Ident "wool", Ident "shag"]) + ]] + parse emptyStyle " bathroom{tile :1in white;drapes :pink}" `shouldBe` TrivialStyleSheet [ + StyleRule [[Tag "bathroom"]] [ + ("tile", [Dimension "1" (NVInteger 1) "in", Ident "white"]), + ("drapes", [Ident "pink"]) + ]] + it "Parses selectors" $ do + parse emptyStyle ".class {}" `shouldBe` TrivialStyleSheet [ + StyleRule [[Class "class"]] [] + ] + parse emptyStyle "*.class {}" `shouldBe` TrivialStyleSheet [ + StyleRule [[Class "class"]] [] + ] + parse emptyStyle "#id {}" `shouldBe` TrivialStyleSheet [ + StyleRule [[Id "id"]] [] + ] + parse emptyStyle "[attr] {}" `shouldBe` TrivialStyleSheet [ + StyleRule [[Property "attr" Exists]] [] + ] + parse emptyStyle "a , b {}" `shouldBe` TrivialStyleSheet [ + StyleRule [[Tag "a"], [Tag "b"]] [] + ] + parse emptyStyle "a b {}" `shouldBe` TrivialStyleSheet [ + StyleRule [[Tag "a", Descendant, Tag "b"]] [] + ] + parse emptyStyle "a > b {}" `shouldBe` TrivialStyleSheet [ + StyleRule [[Tag "a", Child, Tag "b"]] [] + ] + parse emptyStyle "a ~ b {}" `shouldBe` TrivialStyleSheet [ + StyleRule [[Tag "a", Sibling, Tag "b"]] [] + ] + parse emptyStyle "a + b {}" `shouldBe` TrivialStyleSheet [ + StyleRule [[Tag "a", Adjacent, Tag "b"]] [] + ] + +emptyStyle = TrivialStyleSheet [] +linkStyle = TrivialStyleSheet [ + StyleRule [[Tag "a"]] [("color", [Ident "green"])] + ] -- 2.30.2